This commit is contained in:
Jeff Becker
2016-02-10 17:54:17 -05:00
parent e6936eed2f
commit 59f815f2e7
11 changed files with 481 additions and 518 deletions

View File

@ -1,34 +1,31 @@
package sam3
import (
"bytes"
"bytes"
"crypto/sha256"
"encoding/base32"
"encoding/base64"
"errors"
"io"
"strings"
"io"
"strings"
)
var (
i2pB64enc *base64.Encoding = base64.NewEncoding("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-~")
i2pB32enc *base32.Encoding = base32.NewEncoding("abcdefghijklmnopqrstuvwxyz234567")
)
// The public and private keys associated with an I2P destination. I2P hides the
// details of exactly what this is, so treat them as blobs, but generally: One
// pair of DSA keys, one pair of ElGamal keys, and sometimes (almost never) also
// a certificate. String() returns you the full content of I2PKeys and Addr()
// a certificate. String() returns you the full content of I2PKeys and Addr()
// returns the public keys.
type I2PKeys struct {
addr I2PAddr // only the public key
both string // both public and private keys
addr I2PAddr // only the public key
both string // both public and private keys
}
// Creates I2PKeys from an I2PAddr and a public/private keypair string (as
// Creates I2PKeys from an I2PAddr and a public/private keypair string (as
// generated by String().)
func NewKeys(addr I2PAddr, both string) I2PKeys {
return I2PKeys{addr, both}
@ -36,21 +33,21 @@ func NewKeys(addr I2PAddr, both string) I2PKeys {
// load keys from non standard format
func LoadKeysIncompat(r io.Reader) (k I2PKeys, err error) {
var buff bytes.Buffer
_, err = io.Copy(&buff, r)
if err == nil {
parts := strings.Split(buff.String(), "\n")
k = I2PKeys{I2PAddr(parts[0]), parts[1]}
}
return
var buff bytes.Buffer
_, err = io.Copy(&buff, r)
if err == nil {
parts := strings.Split(buff.String(), "\n")
k = I2PKeys{I2PAddr(parts[0]), parts[1]}
}
return
}
// store keys in non standard format
func StoreKeysIncompat(k I2PKeys, w io.Writer) (err error) {
_, err = io.WriteString(w, k.addr.Base64()+"\n"+k.both)
return
_, err = io.WriteString(w, k.addr.Base64()+"\n"+k.both)
return
}
// Returns the public keys of the I2PKeys.
func (k I2PKeys) Addr() I2PAddr {
return k.addr
@ -73,14 +70,14 @@ type I2PDestHash [32]byte
// create a desthash from a string b32.i2p address
func DestHashFromString(str string) (dhash I2PDestHash, err error) {
if strings.HasSuffix(str, ".b32.i2p") && len(str) == 60 {
// valid
_, err = i2pB32enc.Decode(dhash[:], []byte(str[:52]+"===="))
} else {
// invalid
err = errors.New("invalid desthash format")
}
return
if strings.HasSuffix(str, ".b32.i2p") && len(str) == 60 {
// valid
_, err = i2pB32enc.Decode(dhash[:], []byte(str[:52]+"===="))
} else {
// invalid
err = errors.New("invalid desthash format")
}
return
}
// get string representation of i2p dest hash
@ -108,14 +105,14 @@ func (a I2PAddr) Network() string {
// Creates a new I2P address from a base64-encoded string. Checks if the address
// addr is in correct format. (If you know for sure it is, use I2PAddr(addr).)
func NewI2PAddrFromString(addr string) (I2PAddr, error) {
if strings.HasSuffix(addr, ".i2p") {
if strings.HasSuffix(addr, ".b32.i2p") {
return I2PAddr(""), errors.New("cannot convert .b32.i2p to full destination")
}
// strip off .i2p if it's there
addr = addr[:len(addr)-4]
}
addr = strings.Trim(addr, "\t\n\r\f ")
if strings.HasSuffix(addr, ".i2p") {
if strings.HasSuffix(addr, ".b32.i2p") {
return I2PAddr(""), errors.New("cannot convert .b32.i2p to full destination")
}
// strip off .i2p if it's there
addr = addr[:len(addr)-4]
}
addr = strings.Trim(addr, "\t\n\r\f ")
// very basic check
if len(addr) > 4096 || len(addr) < 516 {
return I2PAddr(""), errors.New("Not an I2P address")
@ -146,14 +143,14 @@ func (addr I2PAddr) ToBytes() ([]byte, error) {
return buf, nil
}
// Returns the *.b32.i2p address of the I2P address. It is supposed to be a
// somewhat human-manageable 64 character long pseudo-domain name equivalent of
// the 516+ characters long default base64-address (the I2PAddr format). It is
// not possible to turn the base32-address back into a usable I2PAddr without
// Returns the *.b32.i2p address of the I2P address. It is supposed to be a
// somewhat human-manageable 64 character long pseudo-domain name equivalent of
// the 516+ characters long default base64-address (the I2PAddr format). It is
// not possible to turn the base32-address back into a usable I2PAddr without
// performing a Lookup(). Lookup only works if you are using the I2PAddr from
// which the b32 address was generated.
func (addr I2PAddr) Base32() (str string) {
return addr.DestHash().String()
return addr.DestHash().String()
}
func (addr I2PAddr) DestHash() (h I2PDestHash) {
@ -161,8 +158,8 @@ func (addr I2PAddr) DestHash() (h I2PDestHash) {
b, _ := addr.ToBytes()
hash.Write(b)
digest := hash.Sum(nil)
copy(h[:], digest)
return
copy(h[:], digest)
return
}
// Makes any string into a *.b32.i2p human-readable I2P address. This makes no

View File

@ -1,15 +1,15 @@
package sam3
import (
"time"
"net"
"time"
)
// Implements net.Conn
type SAMConn struct {
laddr I2PAddr
raddr I2PAddr
conn net.Conn
laddr I2PAddr
raddr I2PAddr
conn net.Conn
}
// Implements net.Conn
@ -30,7 +30,7 @@ func (sc SAMConn) Close() error {
}
func (sc SAMConn) LocalAddr() net.Addr {
return sc.localAddr()
return sc.localAddr()
}
// Implements net.Conn
@ -39,7 +39,7 @@ func (sc SAMConn) localAddr() I2PAddr {
}
func (sc SAMConn) RemoteAddr() net.Addr {
return sc.remoteAddr()
return sc.remoteAddr()
}
// Implements net.Conn
@ -61,5 +61,3 @@ func (sc SAMConn) SetReadDeadline(t time.Time) error {
func (sc SAMConn) SetWriteDeadline(t time.Time) error {
return sc.conn.SetWriteDeadline(t)
}

View File

@ -1,9 +1,9 @@
package sam3
import (
"fmt"
"net"
"strconv"
"fmt"
"net"
"strconv"
)
// sam config
@ -13,61 +13,61 @@ type Options map[string]string
// obtain sam options as list of strings
func (opts Options) AsList() (ls []string) {
for k, v := range opts {
ls = append(ls, fmt.Sprintf("%s=%s", k, v))
}
return
for k, v := range opts {
ls = append(ls, fmt.Sprintf("%s=%s", k, v))
}
return
}
// Config is the config type for the sam connector api for i2p which allows applications to 'speak' with i2p
type Config struct {
Addr string
Opts Options
Session string
Keyfile string
Addr string
Opts Options
Session string
Keyfile string
}
// create new sam connector from config with a stream session
func (cfg *Config) StreamSession() (session *StreamSession, err error) {
// connect
var s *SAM
s, err = NewSAM(cfg.Addr)
if err == nil {
// ensure keys exist
var keys I2PKeys
keys, err = s.EnsureKeyfile(cfg.Keyfile)
if err == nil {
// create session
session, err = s.NewStreamSession(cfg.Session, keys, cfg.Opts.AsList())
}
}
return
// connect
var s *SAM
s, err = NewSAM(cfg.Addr)
if err == nil {
// ensure keys exist
var keys I2PKeys
keys, err = s.EnsureKeyfile(cfg.Keyfile)
if err == nil {
// create session
session, err = s.NewStreamSession(cfg.Session, keys, cfg.Opts.AsList())
}
}
return
}
// create new sam datagram session from config
func (cfg *Config) DatagramSession() (session *DatagramSession, err error) {
// connect
var s *SAM
s, err = NewSAM(cfg.Addr)
if err == nil {
// ensure keys exist
var keys I2PKeys
keys, err = s.EnsureKeyfile(cfg.Keyfile)
if err == nil {
// determine udp port
var portstr string
_, portstr, err = net.SplitHostPort(cfg.Addr)
if err == nil {
var port int
port, err = strconv.Atoi(portstr)
if err == nil && port > 0 {
// udp port is 1 lower
port --
// create session
session, err = s.NewDatagramSession(cfg.Session, keys, cfg.Opts.AsList(), port)
}
}
}
}
return
// connect
var s *SAM
s, err = NewSAM(cfg.Addr)
if err == nil {
// ensure keys exist
var keys I2PKeys
keys, err = s.EnsureKeyfile(cfg.Keyfile)
if err == nil {
// determine udp port
var portstr string
_, portstr, err = net.SplitHostPort(cfg.Addr)
if err == nil {
var port int
port, err = strconv.Atoi(portstr)
if err == nil && port > 0 {
// udp port is 1 lower
port--
// create session
session, err = s.NewDatagramSession(cfg.Session, keys, cfg.Opts.AsList(), port)
}
}
}
}
return
}

View File

@ -10,18 +10,18 @@ import (
// The DatagramSession implements net.PacketConn. It works almost like ordinary
// UDP, except that datagrams may be at most 31kB large. These datagrams are
// also end-to-end encrypted, signed and includes replay-protection. And they
// also end-to-end encrypted, signed and includes replay-protection. And they
// are also built to be surveillance-resistant (yey!).
type DatagramSession struct {
samAddr string // address to the sam bridge (ipv4:port)
id string // tunnel name
conn net.Conn // connection to sam bridge
udpconn *net.UDPConn // used to deliver datagrams
keys I2PKeys // i2p destination keys
rUDPAddr *net.UDPAddr // the SAM bridge UDP-port
samAddr string // address to the sam bridge (ipv4:port)
id string // tunnel name
conn net.Conn // connection to sam bridge
udpconn *net.UDPConn // used to deliver datagrams
keys I2PKeys // i2p destination keys
rUDPAddr *net.UDPAddr // the SAM bridge UDP-port
}
// Creates a new datagram session. udpPort is the UDP port SAM is listening on,
// Creates a new datagram session. udpPort is the UDP port SAM is listening on,
// and if you set it to zero, it will use SAMs standard UDP port.
func (s *SAM) NewDatagramSession(id string, keys I2PKeys, options []string, udpPort int) (*DatagramSession, error) {
if udpPort > 65335 || udpPort < 0 {
@ -35,7 +35,7 @@ func (s *SAM) NewDatagramSession(id string, keys I2PKeys, options []string, udpP
s.Close()
return nil, err
}
lUDPAddr, err := net.ResolveUDPAddr("udp4", lhost + ":0")
lUDPAddr, err := net.ResolveUDPAddr("udp4", lhost+":0")
if err != nil {
return nil, err
}
@ -48,7 +48,7 @@ func (s *SAM) NewDatagramSession(id string, keys I2PKeys, options []string, udpP
s.Close()
return nil, err
}
rUDPAddr, err := net.ResolveUDPAddr("udp4", rhost + ":" + strconv.Itoa(udpPort))
rUDPAddr, err := net.ResolveUDPAddr("udp4", rhost+":"+strconv.Itoa(udpPort))
if err != nil {
return nil, err
}
@ -60,12 +60,12 @@ func (s *SAM) NewDatagramSession(id string, keys I2PKeys, options []string, udpP
return &DatagramSession{s.address, id, conn, udpconn, keys, rUDPAddr}, nil
}
// Reads one datagram sent to the destination of the DatagramSession. Returns
// Reads one datagram sent to the destination of the DatagramSession. Returns
// the number of bytes read, from what address it was sent, or an error.
func (s *DatagramSession) ReadFrom(b []byte) (n int, addr I2PAddr, err error) {
// extra bytes to read the remote address of incomming datagram
buf := make([]byte, len(b) + 4096)
buf := make([]byte, len(b)+4096)
for {
// very basic protection: only accept incomming UDP messages from the IP of the SAM bridge
var saddr *net.UDPAddr
@ -87,16 +87,16 @@ func (s *DatagramSession) ReadFrom(b []byte) (n int, addr I2PAddr, err error) {
return 0, I2PAddr(""), errors.New("Could not parse incomming message remote address: " + err.Error())
}
// shift out the incomming address to contain only the data received
if ( n - i+1 ) > len(b) {
if (n - i + 1) > len(b) {
copy(b, buf[i+1:i+1+len(b)])
return n-(i+1), raddr, errors.New("Datagram did not fit into your buffer.")
return n - (i + 1), raddr, errors.New("Datagram did not fit into your buffer.")
} else {
copy(b, buf[i+1:n])
return n-(i+1), raddr, nil
return n - (i + 1), raddr, nil
}
}
// Sends one signed datagram to the destination specified. At the time of
// Sends one signed datagram to the destination specified. At the time of
// writing, maximum size is 31 kilobyte, but this may change in the future.
// Implements net.PacketConn.
func (s *DatagramSession) WriteTo(b []byte, addr I2PAddr) (n int, err error) {
@ -121,7 +121,7 @@ func (s *DatagramSession) LocalAddr() I2PAddr {
return s.keys.Addr()
}
// Sets read and write deadlines for the DatagramSession. Implements
// Sets read and write deadlines for the DatagramSession. Implements
// net.PacketConn and does the same thing. Setting write deadlines for datagrams
// is seldom done.
func (s *DatagramSession) SetDeadline(t time.Time) error {
@ -137,5 +137,3 @@ func (s *DatagramSession) SetReadDeadline(t time.Time) error {
func (s *DatagramSession) SetWriteDeadline(t time.Time) error {
return s.udpconn.SetWriteDeadline(t)
}

View File

@ -6,7 +6,6 @@ import (
"time"
)
func Test_DatagramServerClient(t *testing.T) {
if testing.Short() {
return
@ -24,7 +23,7 @@ func Test_DatagramServerClient(t *testing.T) {
t.Fail()
return
}
// fmt.Println("\tServer: My address: " + keys.Addr().Base32())
// fmt.Println("\tServer: My address: " + keys.Addr().Base32())
fmt.Println("\tServer: Creating tunnel")
ds, err := sam.NewDatagramSession("DGserverTun", keys, []string{"inbound.length=0", "outbound.length=0", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"}, 0)
if err != nil {
@ -33,7 +32,7 @@ func Test_DatagramServerClient(t *testing.T) {
return
}
c, w := make(chan bool), make(chan bool)
go func(c, w chan(bool)) {
go func(c, w chan (bool)) {
sam2, err := NewSAM(yoursam)
if err != nil {
c <- false
@ -52,22 +51,22 @@ func Test_DatagramServerClient(t *testing.T) {
return
}
defer ds2.Close()
// fmt.Println("\tClient: Servers address: " + ds.LocalAddr().Base32())
// fmt.Println("\tClient: Clients address: " + ds2.LocalAddr().Base32())
// fmt.Println("\tClient: Servers address: " + ds.LocalAddr().Base32())
// fmt.Println("\tClient: Clients address: " + ds2.LocalAddr().Base32())
fmt.Println("\tClient: Tries to send datagram to server")
for {
select {
default :
_, err = ds2.WriteTo([]byte("Hello datagram-world! <3 <3 <3 <3 <3 <3"), ds.LocalAddr())
if err != nil {
fmt.Println("\tClient: Failed to send datagram: " + err.Error())
c <- false
return
}
time.Sleep(5 * time.Second)
case <-w :
fmt.Println("\tClient: Sent datagram, quitting.")
default:
_, err = ds2.WriteTo([]byte("Hello datagram-world! <3 <3 <3 <3 <3 <3"), ds.LocalAddr())
if err != nil {
fmt.Println("\tClient: Failed to send datagram: " + err.Error())
c <- false
return
}
time.Sleep(5 * time.Second)
case <-w:
fmt.Println("\tClient: Sent datagram, quitting.")
return
}
}
c <- true
@ -82,19 +81,14 @@ func Test_DatagramServerClient(t *testing.T) {
return
}
fmt.Println("\tServer: Received datagram: " + string(buf[:n]))
// fmt.Println("\tServer: Senders address was: " + saddr.Base32())
// fmt.Println("\tServer: Senders address was: " + saddr.Base32())
}
func ExampleDatagramSession() {
// Creates a new DatagramSession, which behaves just like a net.PacketConn.
const samBridge = "127.0.0.1:7656"
sam, err := NewSAM(samBridge)
if err != nil {
fmt.Println(err.Error())
@ -134,4 +128,3 @@ func ExampleDatagramSession() {
// Output:
//Got message: Hello myself!
}

26
raw.go
View File

@ -9,21 +9,21 @@ import (
)
// The RawSession provides no authentication of senders, and there is no sender
// address attached to datagrams, so all communication is anonymous. The
// address attached to datagrams, so all communication is anonymous. The
// messages send are however still endpoint-to-endpoint encrypted. You
// need to figure out a way to identify and authenticate clients yourself, iff
// that is needed. Raw datagrams may be at most 32 kB in size. There is no
// that is needed. Raw datagrams may be at most 32 kB in size. There is no
// overhead of authentication, which is the reason to use this..
type RawSession struct {
samAddr string // address to the sam bridge (ipv4:port)
id string // tunnel name
conn net.Conn // connection to sam bridge
udpconn *net.UDPConn // used to deliver datagrams
keys I2PKeys // i2p destination keys
rUDPAddr *net.UDPAddr // the SAM bridge UDP-port
samAddr string // address to the sam bridge (ipv4:port)
id string // tunnel name
conn net.Conn // connection to sam bridge
udpconn *net.UDPConn // used to deliver datagrams
keys I2PKeys // i2p destination keys
rUDPAddr *net.UDPAddr // the SAM bridge UDP-port
}
// Creates a new raw session. udpPort is the UDP port SAM is listening on,
// Creates a new raw session. udpPort is the UDP port SAM is listening on,
// and if you set it to zero, it will use SAMs standard UDP port.
func (s *SAM) NewRawSession(id string, keys I2PKeys, options []string, udpPort int) (*RawSession, error) {
if udpPort > 65335 || udpPort < 0 {
@ -37,7 +37,7 @@ func (s *SAM) NewRawSession(id string, keys I2PKeys, options []string, udpPort i
s.Close()
return nil, err
}
lUDPAddr, err := net.ResolveUDPAddr("udp4", lhost + ":0")
lUDPAddr, err := net.ResolveUDPAddr("udp4", lhost+":0")
if err != nil {
return nil, err
}
@ -50,7 +50,7 @@ func (s *SAM) NewRawSession(id string, keys I2PKeys, options []string, udpPort i
s.Close()
return nil, err
}
rUDPAddr, err := net.ResolveUDPAddr("udp4", rhost + ":" + strconv.Itoa(udpPort))
rUDPAddr, err := net.ResolveUDPAddr("udp4", rhost+":"+strconv.Itoa(udpPort))
if err != nil {
return nil, err
}
@ -62,7 +62,7 @@ func (s *SAM) NewRawSession(id string, keys I2PKeys, options []string, udpPort i
return &RawSession{s.address, id, conn, udpconn, keys, rUDPAddr}, nil
}
// Reads one raw datagram sent to the destination of the DatagramSession. Returns
// Reads one raw datagram sent to the destination of the DatagramSession. Returns
// the number of bytes read. Who sent the raw message can not be determined at
// this layer - you need to do it (in a secure way!).
func (s *RawSession) Read(b []byte) (n int, err error) {
@ -116,5 +116,3 @@ func (s *RawSession) SetReadDeadline(t time.Time) error {
func (s *RawSession) SetWriteDeadline(t time.Time) error {
return s.udpconn.SetWriteDeadline(t)
}

382
sam3.go
View File

@ -2,185 +2,183 @@
package sam3
import (
"bufio"
"bytes"
"net"
"errors"
"io"
"os"
"strings"
"bufio"
"bytes"
"errors"
"io"
"net"
"os"
"strings"
)
// Used for controlling I2Ps SAMv3.
type SAM struct {
address string
conn net.Conn
keys *I2PKeys
address string
conn net.Conn
keys *I2PKeys
}
const (
session_OK = "SESSION STATUS RESULT=OK DESTINATION="
session_DUPLICATE_ID = "SESSION STATUS RESULT=DUPLICATED_ID\n"
session_DUPLICATE_DEST = "SESSION STATUS RESULT=DUPLICATED_DEST\n"
session_INVALID_KEY = "SESSION STATUS RESULT=INVALID_KEY\n"
session_I2P_ERROR = "SESSION STATUS RESULT=I2P_ERROR MESSAGE="
session_OK = "SESSION STATUS RESULT=OK DESTINATION="
session_DUPLICATE_ID = "SESSION STATUS RESULT=DUPLICATED_ID\n"
session_DUPLICATE_DEST = "SESSION STATUS RESULT=DUPLICATED_DEST\n"
session_INVALID_KEY = "SESSION STATUS RESULT=INVALID_KEY\n"
session_I2P_ERROR = "SESSION STATUS RESULT=I2P_ERROR MESSAGE="
)
// Creates a new controller for the I2P routers SAM bridge.
func NewSAM(address string) (*SAM, error) {
// TODO: clean this up
conn, err := net.Dial("tcp", address)
if err != nil {
return nil, err
}
if _, err := conn.Write([]byte("HELLO VERSION MIN=3.0 MAX=3.0\n")); err != nil {
conn.Close()
return nil, err
}
buf := make([]byte, 256)
n, err := conn.Read(buf)
if err != nil {
conn.Close()
return nil, err
}
if string(buf[:n]) == "HELLO REPLY RESULT=OK VERSION=3.0\n" {
return &SAM{address, conn, nil}, nil
} else if string(buf[:n]) == "HELLO REPLY RESULT=NOVERSION\n" {
conn.Close()
return nil, errors.New("That SAM bridge does not support SAMv3.")
} else {
conn.Close()
return nil, errors.New(string(buf[:n]))
}
// TODO: clean this up
conn, err := net.Dial("tcp", address)
if err != nil {
return nil, err
}
if _, err := conn.Write([]byte("HELLO VERSION MIN=3.0 MAX=3.0\n")); err != nil {
conn.Close()
return nil, err
}
buf := make([]byte, 256)
n, err := conn.Read(buf)
if err != nil {
conn.Close()
return nil, err
}
if string(buf[:n]) == "HELLO REPLY RESULT=OK VERSION=3.0\n" {
return &SAM{address, conn, nil}, nil
} else if string(buf[:n]) == "HELLO REPLY RESULT=NOVERSION\n" {
conn.Close()
return nil, errors.New("That SAM bridge does not support SAMv3.")
} else {
conn.Close()
return nil, errors.New(string(buf[:n]))
}
}
func (sam *SAM) Keys() (k *I2PKeys) {
//TODO: copy them?
k = sam.keys
return
//TODO: copy them?
k = sam.keys
return
}
// read public/private keys from an io.Reader
func (sam *SAM) ReadKeys(r io.Reader) (err error) {
var keys I2PKeys
keys, err = LoadKeysIncompat(r)
if err == nil {
sam.keys = &keys
}
return
var keys I2PKeys
keys, err = LoadKeysIncompat(r)
if err == nil {
sam.keys = &keys
}
return
}
// if keyfile fname does not exist
func (sam *SAM) EnsureKeyfile(fname string) (keys I2PKeys, err error) {
if fname == "" {
// transient
keys, err = sam.NewKeys()
if err == nil {
sam.keys = &keys
}
} else {
// persistant
_, err = os.Stat(fname)
if os.IsNotExist(err) {
// make the keys
keys, err = sam.NewKeys()
if err == nil {
sam.keys = &keys
// save keys
var f io.WriteCloser
f, err = os.OpenFile(fname, os.O_WRONLY | os.O_CREATE, 0600)
if err == nil {
err = StoreKeysIncompat(keys, f)
f.Close()
}
}
} else if err == nil {
// we haz key file
var f *os.File
f, err = os.Open(fname)
if err == nil {
keys, err = LoadKeysIncompat(f)
if err == nil {
sam.keys = &keys
}
}
}
}
return
if fname == "" {
// transient
keys, err = sam.NewKeys()
if err == nil {
sam.keys = &keys
}
} else {
// persistant
_, err = os.Stat(fname)
if os.IsNotExist(err) {
// make the keys
keys, err = sam.NewKeys()
if err == nil {
sam.keys = &keys
// save keys
var f io.WriteCloser
f, err = os.OpenFile(fname, os.O_WRONLY|os.O_CREATE, 0600)
if err == nil {
err = StoreKeysIncompat(keys, f)
f.Close()
}
}
} else if err == nil {
// we haz key file
var f *os.File
f, err = os.Open(fname)
if err == nil {
keys, err = LoadKeysIncompat(f)
if err == nil {
sam.keys = &keys
}
}
}
}
return
}
// Creates the I2P-equivalent of an IP address, that is unique and only the one
// who has the private keys can send messages from. The public keys are the I2P
// desination (the address) that anyone can send messages to.
func (sam *SAM) NewKeys() (I2PKeys, error) {
if _, err := sam.conn.Write([]byte("DEST GENERATE\n")); err != nil {
return I2PKeys{}, err
}
buf := make([]byte, 8192)
n, err := sam.conn.Read(buf)
if err != nil {
return I2PKeys{}, err
}
s := bufio.NewScanner(bytes.NewReader(buf[:n]))
s.Split(bufio.ScanWords)
if _, err := sam.conn.Write([]byte("DEST GENERATE\n")); err != nil {
return I2PKeys{}, err
}
buf := make([]byte, 8192)
n, err := sam.conn.Read(buf)
if err != nil {
return I2PKeys{}, err
}
s := bufio.NewScanner(bytes.NewReader(buf[:n]))
s.Split(bufio.ScanWords)
var pub, priv string
for s.Scan() {
text := s.Text()
if text == "DEST" {
continue
} else if text == "REPLY" {
continue
} else if strings.HasPrefix(text, "PUB=") {
pub = text[4:]
} else if strings.HasPrefix(text, "PRIV=") {
priv = text[5:]
} else {
return I2PKeys{}, errors.New("Failed to parse keys.")
}
}
return I2PKeys{I2PAddr(pub), priv}, nil
var pub, priv string
for s.Scan() {
text := s.Text()
if text == "DEST" {
continue
} else if text == "REPLY" {
continue
} else if strings.HasPrefix(text, "PUB=") {
pub = text[4:]
} else if strings.HasPrefix(text, "PRIV=") {
priv = text[5:]
} else {
return I2PKeys{}, errors.New("Failed to parse keys.")
}
}
return I2PKeys{I2PAddr(pub), priv}, nil
}
// Performs a lookup, probably this order: 1) routers known addresses, cached
// addresses, 3) by asking peers in the I2P network.
func (sam *SAM) Lookup(name string) (I2PAddr, error) {
if _, err := sam.conn.Write([]byte("NAMING LOOKUP NAME=" + name + "\n")); err != nil {
return I2PAddr(""), err
}
buf := make([]byte, 4096)
n, err := sam.conn.Read(buf)
if err != nil {
return I2PAddr(""), err
}
if n <= 13 || !strings.HasPrefix(string(buf[:n]), "NAMING REPLY ") {
return I2PAddr(""), errors.New("Failed to parse.")
}
s := bufio.NewScanner(bytes.NewReader(buf[13:n]))
s.Split(bufio.ScanWords)
if _, err := sam.conn.Write([]byte("NAMING LOOKUP NAME=" + name + "\n")); err != nil {
return I2PAddr(""), err
}
buf := make([]byte, 4096)
n, err := sam.conn.Read(buf)
if err != nil {
return I2PAddr(""), err
}
if n <= 13 || !strings.HasPrefix(string(buf[:n]), "NAMING REPLY ") {
return I2PAddr(""), errors.New("Failed to parse.")
}
s := bufio.NewScanner(bytes.NewReader(buf[13:n]))
s.Split(bufio.ScanWords)
errStr := ""
for s.Scan() {
text := s.Text()
if text == "RESULT=OK" {
continue
} else if text == "RESULT=INVALID_KEY" {
errStr += "Invalid key."
} else if text == "RESULT=KEY_NOT_FOUND" {
errStr += "Unable to resolve " + name
} else if text == "NAME=" + name {
continue
} else if strings.HasPrefix(text, "VALUE=") {
return I2PAddr(text[6:]), nil
} else if strings.HasPrefix(text, "MESSAGE=") {
errStr += " " + text[8:]
} else {
continue
}
}
return I2PAddr(""), errors.New(errStr)
errStr := ""
for s.Scan() {
text := s.Text()
if text == "RESULT=OK" {
continue
} else if text == "RESULT=INVALID_KEY" {
errStr += "Invalid key."
} else if text == "RESULT=KEY_NOT_FOUND" {
errStr += "Unable to resolve " + name
} else if text == "NAME="+name {
continue
} else if strings.HasPrefix(text, "VALUE=") {
return I2PAddr(text[6:]), nil
} else if strings.HasPrefix(text, "MESSAGE=") {
errStr += " " + text[8:]
} else {
continue
}
}
return I2PAddr(""), errors.New(errStr)
}
// Creates a new session with the style of either "STREAM", "DATAGRAM" or "RAW",
@ -190,57 +188,57 @@ func (sam *SAM) Lookup(name string) (I2PAddr, error) {
// This sam3 instance is now a session
func (sam *SAM) newGenericSession(style, id string, keys I2PKeys, options []string, extras []string) (net.Conn, error) {
optStr := ""
for _, opt := range options {
optStr += opt + " "
}
optStr := ""
for _, opt := range options {
optStr += opt + " "
}
conn := sam.conn
scmsg := []byte("SESSION CREATE STYLE=" + style + " ID=" + id + " DESTINATION=" + keys.String() + " " + optStr + strings.Join(extras, " ") + "\n")
for m, i:=0, 0; m!=len(scmsg); i++ {
if i == 15 {
conn.Close()
return nil, errors.New("writing to SAM failed")
}
n, err := conn.Write(scmsg[m:])
if err != nil {
conn.Close()
return nil, err
}
m += n
}
buf := make([]byte, 4096)
n, err := conn.Read(buf)
if err != nil {
conn.Close()
return nil, err
}
text := string(buf[:n])
if strings.HasPrefix(text, session_OK) {
if keys.String() != text[len(session_OK):len(text)-1] {
conn.Close()
return nil, errors.New("SAMv3 created a tunnel with keys other than the ones we asked it for")
}
return conn, nil //&StreamSession{id, conn, keys, nil, sync.RWMutex{}, nil}, nil
} else if text == session_DUPLICATE_ID {
conn.Close()
return nil, errors.New("Duplicate tunnel name")
} else if text == session_DUPLICATE_DEST {
conn.Close()
return nil, errors.New("Duplicate destination")
} else if text == session_INVALID_KEY {
conn.Close()
return nil, errors.New("Invalid key")
} else if strings.HasPrefix(text, session_I2P_ERROR) {
conn.Close()
return nil, errors.New("I2P error " + text[len(session_I2P_ERROR):])
} else {
conn.Close()
return nil, errors.New("Unable to parse SAMv3 reply: " + text)
}
conn := sam.conn
scmsg := []byte("SESSION CREATE STYLE=" + style + " ID=" + id + " DESTINATION=" + keys.String() + " " + optStr + strings.Join(extras, " ") + "\n")
for m, i := 0, 0; m != len(scmsg); i++ {
if i == 15 {
conn.Close()
return nil, errors.New("writing to SAM failed")
}
n, err := conn.Write(scmsg[m:])
if err != nil {
conn.Close()
return nil, err
}
m += n
}
buf := make([]byte, 4096)
n, err := conn.Read(buf)
if err != nil {
conn.Close()
return nil, err
}
text := string(buf[:n])
if strings.HasPrefix(text, session_OK) {
if keys.String() != text[len(session_OK):len(text)-1] {
conn.Close()
return nil, errors.New("SAMv3 created a tunnel with keys other than the ones we asked it for")
}
return conn, nil //&StreamSession{id, conn, keys, nil, sync.RWMutex{}, nil}, nil
} else if text == session_DUPLICATE_ID {
conn.Close()
return nil, errors.New("Duplicate tunnel name")
} else if text == session_DUPLICATE_DEST {
conn.Close()
return nil, errors.New("Duplicate destination")
} else if text == session_INVALID_KEY {
conn.Close()
return nil, errors.New("Invalid key")
} else if strings.HasPrefix(text, session_I2P_ERROR) {
conn.Close()
return nil, errors.New("I2P error " + text[len(session_I2P_ERROR):])
} else {
conn.Close()
return nil, errors.New("Unable to parse SAMv3 reply: " + text)
}
}
// close this sam session
func (sam *SAM) Close() error {
return sam.conn.Close()
return sam.conn.Close()
}

View File

@ -1,19 +1,13 @@
package sam3
import (
"fmt"
"testing"
"time"
)
const yoursam = "127.0.0.1:7656"
func Test_Basic(t *testing.T) {
fmt.Println("Test_Basic")
fmt.Println("\tAttaching to SAM at " + yoursam)
@ -23,7 +17,7 @@ func Test_Basic(t *testing.T) {
t.Fail()
return
}
fmt.Println("\tCreating new keys...")
keys, err := sam.NewKeys()
if err != nil {
@ -33,14 +27,14 @@ func Test_Basic(t *testing.T) {
fmt.Println("\tAddress created: " + keys.Addr().Base32())
fmt.Println("\tI2PKeys: " + string(keys.both)[:50] + "(...etc)")
}
addr2, err := sam.Lookup("zzz.i2p")
if err != nil {
fmt.Println(err.Error())
t.Fail()
} else {
fmt.Println("\tzzz.i2p = " + addr2.Base32())
}
}
if err := sam.Close(); err != nil {
fmt.Println(err.Error())
@ -48,7 +42,6 @@ func Test_Basic(t *testing.T) {
}
}
/*
func Test_GenericSession(t *testing.T) {
if testing.Short() {
@ -95,13 +88,6 @@ func Test_GenericSession(t *testing.T) {
}
*/
func Test_RawServerClient(t *testing.T) {
if testing.Short() {
return
@ -127,7 +113,7 @@ func Test_RawServerClient(t *testing.T) {
return
}
c, w := make(chan bool), make(chan bool)
go func(c, w chan(bool)) {
go func(c, w chan (bool)) {
sam2, err := NewSAM(yoursam)
if err != nil {
c <- false
@ -149,17 +135,17 @@ func Test_RawServerClient(t *testing.T) {
fmt.Println("\tClient: Tries to send raw datagram to server")
for {
select {
default :
_, err = rs2.WriteTo([]byte("Hello raw-world! <3 <3 <3 <3 <3 <3"), rs.LocalAddr())
if err != nil {
fmt.Println("\tClient: Failed to send raw datagram: " + err.Error())
c <- false
return
}
time.Sleep(5 * time.Second)
case <-w :
fmt.Println("\tClient: Sent raw datagram, quitting.")
default:
_, err = rs2.WriteTo([]byte("Hello raw-world! <3 <3 <3 <3 <3 <3"), rs.LocalAddr())
if err != nil {
fmt.Println("\tClient: Failed to send raw datagram: " + err.Error())
c <- false
return
}
time.Sleep(5 * time.Second)
case <-w:
fmt.Println("\tClient: Sent raw datagram, quitting.")
return
}
}
c <- true
@ -174,7 +160,5 @@ func Test_RawServerClient(t *testing.T) {
return
}
fmt.Println("\tServer: Received datagram: " + string(buf[:n]))
// fmt.Println("\tServer: Senders address was: " + saddr.Base32())
// fmt.Println("\tServer: Senders address was: " + saddr.Base32())
}

203
stream.go
View File

@ -4,17 +4,17 @@ import (
"bufio"
"bytes"
"errors"
"io"
"io"
"net"
"strings"
)
// Represents a streaming session.
type StreamSession struct {
samAddr string // address to the sam bridge (ipv4:port)
id string // tunnel name
conn net.Conn // connection to sam
keys I2PKeys // i2p destination keys
samAddr string // address to the sam bridge (ipv4:port)
id string // tunnel name
conn net.Conn // connection to sam
keys I2PKeys // i2p destination keys
}
// Returns the local tunnel name of the I2P tunnel used for the stream session
@ -23,7 +23,7 @@ func (ss StreamSession) ID() string {
}
func (ss *StreamSession) Close() error {
return ss.conn.Close()
return ss.conn.Close()
}
// Returns the I2P destination (the address) of the stream session
@ -36,7 +36,7 @@ func (ss StreamSession) Keys() I2PKeys {
return ss.keys
}
// Creates a new StreamSession with the I2CP- and streaminglib options as
// Creates a new StreamSession with the I2CP- and streaminglib options as
// specified. See the I2P documentation for a full list of options.
func (sam *SAM) NewStreamSession(id string, keys I2PKeys, options []string) (*StreamSession, error) {
conn, err := sam.newGenericSession("STREAM", id, keys, options, []string{})
@ -48,40 +48,40 @@ func (sam *SAM) NewStreamSession(id string, keys I2PKeys, options []string) (*St
// lookup name, convienence function
func (s *StreamSession) Lookup(name string) (I2PAddr, error) {
sam, err := NewSAM(s.samAddr)
if err == nil {
addr, err := sam.Lookup(name)
sam.Close()
return addr, err
}
return I2PAddr(""), err
sam, err := NewSAM(s.samAddr)
if err == nil {
addr, err := sam.Lookup(name)
sam.Close()
return addr, err
}
return I2PAddr(""), err
}
// implement net.Dialer
func (s *StreamSession) Dial(n, addr string) (c net.Conn, err error) {
var i2paddr I2PAddr
var host string
host, _, err = net.SplitHostPort(addr)
if err == nil {
// check for name
if strings.HasSuffix(host, ".b32.i2p") || strings.HasSuffix(host, ".i2p") {
// name lookup
var sam *SAM
sam, err = NewSAM(s.samAddr)
if err == nil {
i2paddr, err = sam.Lookup(host)
sam.Close()
}
} else {
// probably a destination
i2paddr = I2PAddr(host)
}
if err == nil {
return s.DialI2P(i2paddr)
}
}
return
var i2paddr I2PAddr
var host string
host, _, err = net.SplitHostPort(addr)
if err == nil {
// check for name
if strings.HasSuffix(host, ".b32.i2p") || strings.HasSuffix(host, ".i2p") {
// name lookup
var sam *SAM
sam, err = NewSAM(s.samAddr)
if err == nil {
i2paddr, err = sam.Lookup(host)
}
sam.Close()
} else {
// probably a destination
i2paddr = I2PAddr(host)
}
if err == nil {
return s.DialI2P(i2paddr)
}
}
return
}
// Dials to an I2P destination and returns a SAMConn, which implements a net.Conn.
@ -91,7 +91,7 @@ func (s *StreamSession) DialI2P(addr I2PAddr) (*SAMConn, error) {
return nil, err
}
conn := sam.conn
_,err = conn.Write([]byte("STREAM CONNECT ID=" + s.id + " DESTINATION=" + addr.Base64() + " SILENT=false\n"))
_, err = conn.Write([]byte("STREAM CONNECT ID=" + s.id + " DESTINATION=" + addr.Base64() + " SILENT=false\n"))
if err != nil {
conn.Close()
return nil, err
@ -106,29 +106,29 @@ func (s *StreamSession) DialI2P(addr I2PAddr) (*SAMConn, error) {
scanner.Split(bufio.ScanWords)
for scanner.Scan() {
switch scanner.Text() {
case "STREAM" :
case "STREAM":
continue
case "STATUS" :
case "STATUS":
continue
case "RESULT=OK" :
case "RESULT=OK":
return &SAMConn{s.keys.addr, addr, conn}, nil
case "RESULT=CANT_REACH_PEER" :
sam.Close()
case "RESULT=CANT_REACH_PEER":
conn.Close()
return nil, errors.New("Can not reach peer")
case "RESULT=I2P_ERROR" :
sam.Close()
case "RESULT=I2P_ERROR":
conn.Close()
return nil, errors.New("I2P internal error")
case "RESULT=INVALID_KEY" :
sam.Close()
case "RESULT=INVALID_KEY":
conn.Close()
return nil, errors.New("Invalid key")
case "RESULT=INVALID_ID" :
sam.Close()
case "RESULT=INVALID_ID":
conn.Close()
return nil, errors.New("Invalid tunnel ID")
case "RESULT=TIMEOUT" :
sam.Close()
case "RESULT=TIMEOUT":
sam.Close()
return nil, errors.New("Timeout")
default:
sam.Close()
default:
conn.Close()
return nil, errors.New("Unknown error: " + scanner.Text() + " : " + string(buf[:n]))
}
}
@ -137,74 +137,73 @@ func (s *StreamSession) DialI2P(addr I2PAddr) (*SAMConn, error) {
// create a new stream listener to accept inbound connections
func (s *StreamSession) Listen() (*StreamListener, error) {
return &StreamListener{
session: s,
id: s.id,
laddr: s.keys.Addr(),
}, nil
return &StreamListener{
session: s,
id: s.id,
laddr: s.keys.Addr(),
}, nil
}
type StreamListener struct {
// parent stream session
session *StreamSession
// our session id
id string
// our local address for this sam socket
laddr I2PAddr
// parent stream session
session *StreamSession
// our session id
id string
// our local address for this sam socket
laddr I2PAddr
}
// get our address
// implements net.Listener
func (l *StreamListener) Addr() net.Addr {
return l.laddr
return l.laddr
}
// implements net.Listener
func (l *StreamListener) Close() error {
return l.session.Close()
return l.session.Close()
}
// implements net.Listener
func (l *StreamListener) Accept() (net.Conn, error) {
return l.AcceptI2P()
return l.AcceptI2P()
}
// accept a new inbound connection
func (l *StreamListener) AcceptI2P() (*SAMConn, error) {
s, err := NewSAM(l.session.samAddr)
if err == nil {
// we connected to sam
// send accept() command
_, err = io.WriteString(s.conn, "STREAM ACCEPT ID="+l.id+" SILENT=false\n")
// read reply
rd := bufio.NewReader(s.conn)
// read first line
line, err := rd.ReadString(10)
if err == nil {
if strings.HasPrefix(line, "STREAM STATUS RESULT=OK") {
// we gud read destination line
dest, err := rd.ReadString(10)
if err == nil {
// return wrapped connection
dest = strings.Trim(dest, "\n")
return &SAMConn{
laddr: l.laddr,
raddr: I2PAddr(dest),
conn: s.conn,
}, nil
} else {
s.Close()
return nil, err
}
} else {
s.Close()
return nil, errors.New("invalid sam line: "+line)
}
} else {
s.Close()
return nil, err
}
}
return nil, err
s, err := NewSAM(l.session.samAddr)
if err == nil {
// we connected to sam
// send accept() command
_, err = io.WriteString(s.conn, "STREAM ACCEPT ID="+l.id+" SILENT=false\n")
// read reply
rd := bufio.NewReader(s.conn)
// read first line
line, err := rd.ReadString(10)
if err == nil {
if strings.HasPrefix(line, "STREAM STATUS RESULT=OK") {
// we gud read destination line
dest, err := rd.ReadString(10)
if err == nil {
// return wrapped connection
dest = strings.Trim(dest, "\n")
return &SAMConn{
laddr: l.laddr,
raddr: I2PAddr(dest),
conn: s.conn,
}, nil
} else {
s.Close()
return nil, err
}
} else {
s.Close()
return nil, errors.New("invalid sam line: " + line)
}
} else {
s.Close()
return nil, err
}
}
return nil, err
}

View File

@ -6,7 +6,6 @@ import (
"testing"
)
func Test_StreamingDial(t *testing.T) {
if testing.Short() {
return
@ -52,7 +51,7 @@ func Test_StreamingDial(t *testing.T) {
if _, err := conn.Write([]byte("GET /\n")); err != nil {
fmt.Println(err.Error())
t.Fail()
return
return
}
buf := make([]byte, 4096)
n, err := conn.Read(buf)
@ -67,7 +66,7 @@ func Test_StreamingServerClient(t *testing.T) {
if testing.Short() {
return
}
fmt.Println("Test_StreamingServerClient")
sam, err := NewSAM(yoursam)
if err != nil {
@ -86,7 +85,7 @@ func Test_StreamingServerClient(t *testing.T) {
return
}
c, w := make(chan bool), make(chan bool)
go func(c, w chan(bool)) {
go func(c, w chan (bool)) {
if !(<-w) {
return
}
@ -140,18 +139,17 @@ func Test_StreamingServerClient(t *testing.T) {
}
defer conn.Close()
buf := make([]byte, 512)
n,err := conn.Read(buf)
n, err := conn.Read(buf)
fmt.Printf("\tClient exited successfully: %t\n", <-c)
fmt.Println("\tServer: received from Client: " + string(buf[:n]))
}
func ExampleStreamSession() {
// Creates a new StreamingSession, dials to zzz.i2p and gets a SAMConn
// which behaves just like a normal net.Conn.
const samBridge = "127.0.0.1:7656"
sam, err := NewSAM(samBridge)
if err != nil {
fmt.Println(err.Error())
@ -174,7 +172,7 @@ func ExampleStreamSession() {
fmt.Println(err.Error())
return
}
conn, err := ss.DialI2P(someone)
if err != nil {
fmt.Println(err.Error())
@ -184,7 +182,7 @@ func ExampleStreamSession() {
fmt.Println("Sending HTTP GET /")
if _, err := conn.Write([]byte("GET /\n")); err != nil {
fmt.Println(err.Error())
return
return
}
buf := make([]byte, 4096)
n, err := conn.Read(buf)
@ -194,7 +192,7 @@ func ExampleStreamSession() {
fmt.Println("Read HTTP/HTML from zzz.i2p")
}
return
// Output:
//Sending HTTP GET /
//Read HTTP/HTML from zzz.i2p
@ -204,7 +202,7 @@ func ExampleStreamListener() {
// One server Accept()ing on a StreamListener, and one client that Dials
// through I2P to the server. Server writes "Hello world!" through a SAMConn
// (which implements net.Conn) and the client prints the message.
const samBridge = "127.0.0.1:7656"
sam, err := NewSAM(samBridge)
@ -213,7 +211,7 @@ func ExampleStreamListener() {
return
}
defer sam.Close()
keys, err := sam.NewKeys()
keys, err := sam.NewKeys()
if err != nil {
fmt.Println(err.Error())
return
@ -229,7 +227,7 @@ func ExampleStreamListener() {
return
}
defer csam.Close()
keys, err := csam.NewKeys()
keys, err := csam.NewKeys()
if err != nil {
fmt.Println(err.Error())
return
@ -255,7 +253,7 @@ func ExampleStreamListener() {
}
fmt.Println(string(buf[:n]))
quit <- true
}(keys.Addr()) // end of client
}(keys.Addr()) // end of client
ss, err := sam.NewStreamSession("server_example", keys, Options_Small)
if err != nil {
@ -273,9 +271,9 @@ func ExampleStreamListener() {
return
}
conn.Write([]byte("Hello world!"))
<-quit // waits for client to die, for example only
// Output:
//Hello world!
}

View File

@ -2,39 +2,39 @@ package sam3
// Examples and suggestions for options when creating sessions.
var (
// Suitable options if you are shuffling A LOT of traffic. If unused, this
// Suitable options if you are shuffling A LOT of traffic. If unused, this
// will waste your resources.
Options_Humongous = []string{"inbound.length=3", "outbound.length=3",
"inbound.lengthVariance=1", "outbound.lengthVariance=1",
"inbound.backupQuantity=3", "outbound.backupQuantity=3",
"inbound.quantity=6", "outbound.quantity=6"}
// Suitable for shuffling a lot of traffic.
"inbound.lengthVariance=1", "outbound.lengthVariance=1",
"inbound.backupQuantity=3", "outbound.backupQuantity=3",
"inbound.quantity=6", "outbound.quantity=6"}
// Suitable for shuffling a lot of traffic.
Options_Fat = []string{"inbound.length=3", "outbound.length=3",
"inbound.lengthVariance=1", "outbound.lengthVariance=1",
"inbound.backupQuantity=1", "outbound.backupQuantity=1",
"inbound.quantity=4", "outbound.quantity=4"}
"inbound.lengthVariance=1", "outbound.lengthVariance=1",
"inbound.backupQuantity=1", "outbound.backupQuantity=1",
"inbound.quantity=4", "outbound.quantity=4"}
// Suitable for shuffling medium amounts of traffic.
Options_Medium = []string{"inbound.length=3", "outbound.length=3",
"inbound.lengthVariance=1", "outbound.lengthVariance=1",
"inbound.backupQuantity=0", "outbound.backupQuantity=0",
"inbound.quantity=2", "outbound.quantity=2"}
"inbound.lengthVariance=1", "outbound.lengthVariance=1",
"inbound.backupQuantity=0", "outbound.backupQuantity=0",
"inbound.quantity=2", "outbound.quantity=2"}
// Suitable only for small dataflows, and very short lasting connections:
// You only have one tunnel in each direction, so if any of the nodes
// You only have one tunnel in each direction, so if any of the nodes
// through which any of your two tunnels pass through go offline, there will
// be a complete halt in the dataflow, until a new tunnel is built.
Options_Small = []string{"inbound.length=3", "outbound.length=3",
"inbound.lengthVariance=1", "outbound.lengthVariance=1",
"inbound.backupQuantity=0", "outbound.backupQuantity=0",
"inbound.quantity=1", "outbound.quantity=1"}
"inbound.lengthVariance=1", "outbound.lengthVariance=1",
"inbound.backupQuantity=0", "outbound.backupQuantity=0",
"inbound.quantity=1", "outbound.quantity=1"}
// Does not use any anonymization, you connect directly to others tunnel
// endpoints, thus revealing your identity but not theirs. Use this only
// if you don't care.
Options_Warning_ZeroHop = []string{"inbound.length=0", "outbound.length=0",
"inbound.lengthVariance=0", "outbound.lengthVariance=0",
"inbound.backupQuantity=0", "outbound.backupQuantity=0",
"inbound.quantity=2", "outbound.quantity=2"}
"inbound.lengthVariance=0", "outbound.lengthVariance=0",
"inbound.backupQuantity=0", "outbound.backupQuantity=0",
"inbound.quantity=2", "outbound.quantity=2"}
)