From c50775ebfd83533f1a7175ba3f2022af6cbb566a Mon Sep 17 00:00:00 2001 From: eyedeekay Date: Sun, 23 Feb 2025 18:01:43 -0500 Subject: [PATCH] start on a wrapper for sam3 --- .gitignore | 2 + SAMConn.go | 10 + common/emitter_test.go | 132 ++++++++++++ common/util.go | 6 +- config.go | 62 ++++++ datagram.go | 13 ++ datagram_test.go | 182 ++++++++++++++++ emit-options.go | 436 +++++++++++++++++++++++++++++++++++++++ emit.go | 141 +++++++++++++ log.go | 51 +++++ primary.go | 40 ++++ primary/primary.go | 52 ++++- primary_datagram_test.go | 148 +++++++++++++ primary_stream_test.go | 306 +++++++++++++++++++++++++++ raw.go | 15 ++ resolver.go | 24 +++ sam3.go | 101 +++++++++ sam3_test.go | 164 +++++++++++++++ stream.go | 29 +++ streamListener.go | 9 + stream_test.go | 283 +++++++++++++++++++++++++ suggestedOptions.go | 120 +++++++++++ 22 files changed, 2313 insertions(+), 13 deletions(-) create mode 100644 SAMConn.go create mode 100644 common/emitter_test.go create mode 100644 config.go create mode 100644 datagram.go create mode 100644 datagram_test.go create mode 100644 emit-options.go create mode 100644 emit.go create mode 100644 log.go create mode 100644 primary.go create mode 100644 primary_datagram_test.go create mode 100644 primary_stream_test.go create mode 100644 raw.go create mode 100644 resolver.go create mode 100644 sam3.go create mode 100644 sam3_test.go create mode 100644 stream.go create mode 100644 streamListener.go create mode 100644 stream_test.go create mode 100644 suggestedOptions.go diff --git a/.gitignore b/.gitignore index bd801ab..e55f364 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ review.md SAMv3.md +testplan.md +err \ No newline at end of file diff --git a/SAMConn.go b/SAMConn.go new file mode 100644 index 0000000..e37d990 --- /dev/null +++ b/SAMConn.go @@ -0,0 +1,10 @@ +package sam3 + +import ( + "github.com/go-i2p/go-sam-go/stream" +) + +// Implements net.Conn +type SAMConn struct { + *stream.StreamConn +} diff --git a/common/emitter_test.go b/common/emitter_test.go new file mode 100644 index 0000000..4d07390 --- /dev/null +++ b/common/emitter_test.go @@ -0,0 +1,132 @@ +package common + +import ( + "bytes" + "testing" +) + +var ( + from = RandPort() + to = RandPort() +) + +func setupTestEmit() *SAMEmit { + return &SAMEmit{ + I2PConfig: I2PConfig{ + SamHost: "127.0.0.1", + SamPort: 7656, + SamMin: "3.0", + SamMax: "3.1", + Style: "STREAM", + TunName: "testid", + Fromport: from, + Toport: to, + SigType: "ED25519", + }, + } +} + +func TestSAMEmit_Hello(t *testing.T) { + emit := setupTestEmit() + want := "HELLO VERSION MIN=3.0 MAX=3.1 \n" + + t.Run("string output", func(t *testing.T) { + if got := emit.Hello(); got != want { + t.Errorf("Hello() = %v, want %v", got, want) + } + }) + + t.Run("byte output", func(t *testing.T) { + if got := emit.HelloBytes(); !bytes.Equal(got, []byte(want)) { + t.Errorf("HelloBytes() = %v, want %v", got, []byte(want)) + } + }) +} + +func TestSAMEmit_GenerateDestination(t *testing.T) { + emit := setupTestEmit() + want := "DEST GENERATE ED25519 \n" + + t.Run("string output", func(t *testing.T) { + if got := emit.GenerateDestination(); got != want { + t.Errorf("GenerateDestination() = %v, want %v", got, want) + } + }) + + t.Run("byte output", func(t *testing.T) { + if got := emit.GenerateDestinationBytes(); !bytes.Equal(got, []byte(want)) { + t.Errorf("GenerateDestinationBytes() = %v, want %v", got, []byte(want)) + } + }) +} + +func TestSAMEmit_Lookup(t *testing.T) { + tests := []struct { + name string + lookup string + want string + }{ + {"basic lookup", "test.i2p", "NAMING LOOKUP NAME=test.i2p \n"}, + {"empty lookup", "", "NAMING LOOKUP NAME= \n"}, + } + + emit := setupTestEmit() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := emit.Lookup(tt.lookup); got != tt.want { + t.Errorf("Lookup() = %v, want %v", got, tt.want) + } + if got := emit.LookupBytes(tt.lookup); !bytes.Equal(got, []byte(tt.want)) { + t.Errorf("LookupBytes() = %v, want %v", got, []byte(tt.want)) + } + }) + } +} + +func TestSAMEmit_Connect(t *testing.T) { + tests := []struct { + name string + dest string + want string + }{ + { + "basic connect", + "destination123", + "STREAM CONNECT ID=testid " + from + " " + to + " DESTINATION=destination123 \n", + }, + { + "empty destination", + "", + "STREAM CONNECT ID=testid " + from + " " + to + " DESTINATION= \n", + }, + } + + emit := setupTestEmit() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := emit.Connect(tt.dest); got != tt.want { + t.Errorf("Connect() = %v, want %v", got, tt.want) + } + if got := emit.ConnectBytes(tt.dest); !bytes.Equal(got, []byte(tt.want)) { + t.Errorf("ConnectBytes() = %v, want %v", got, []byte(tt.want)) + } + }) + } +} + +func TestSAMEmit_Accept(t *testing.T) { + emit := setupTestEmit() + want := "STREAM ACCEPT ID=testid " + from + " " + to + "" + + t.Run("string output", func(t *testing.T) { + if got := emit.Accept(); got != want { + t.Errorf("Accept() = %v, want %v", got, want) + } + }) + + t.Run("byte output", func(t *testing.T) { + if got := emit.AcceptBytes(); !bytes.Equal(got, []byte(want)) { + t.Errorf("AcceptBytes() = %v, want %v", got, []byte(want)) + } + }) +} diff --git a/common/util.go b/common/util.go index 6cf9819..18aba08 100644 --- a/common/util.go +++ b/common/util.go @@ -71,8 +71,10 @@ func ExtractDest(input string) string { return strings.Split(input, " ")[0] } -var randSource = rand.NewSource(time.Now().UnixNano()) -var randGen = rand.New(randSource) +var ( + randSource = rand.NewSource(time.Now().UnixNano()) + randGen = rand.New(randSource) +) func RandPort() string { for { diff --git a/config.go b/config.go new file mode 100644 index 0000000..cb5a740 --- /dev/null +++ b/config.go @@ -0,0 +1,62 @@ +package sam3 + +import ( + "fmt" + + "github.com/go-i2p/go-sam-go/common" +) + +// I2PConfig is a struct which manages I2P configuration options +type I2PConfig struct { + common.I2PConfig +} + +func NewConfig(opts ...func(*I2PConfig) error) (*I2PConfig, error) { + var config I2PConfig + config.SamHost = "127.0.0.1" + config.SamPort = 7656 + config.SamMin = "3.0" + config.SamMax = "3.2" + config.TunName = "" + config.TunType = "server" + config.Style = "STREAM" + config.InLength = 3 + config.OutLength = 3 + config.InQuantity = 2 + config.OutQuantity = 2 + config.InVariance = 1 + config.OutVariance = 1 + config.InBackupQuantity = 3 + config.OutBackupQuantity = 3 + config.InAllowZeroHop = false + config.OutAllowZeroHop = false + config.EncryptLeaseSet = false + config.LeaseSetKey = "" + config.LeaseSetPrivateKey = "" + config.LeaseSetPrivateSigningKey = "" + config.FastRecieve = false + config.UseCompression = true + config.ReduceIdle = false + config.ReduceIdleTime = 15 + config.ReduceIdleQuantity = 4 + config.CloseIdle = false + config.CloseIdleTime = 300000 + config.MessageReliability = "none" + for _, o := range opts { + if err := o(&config); err != nil { + return nil, err + } + } + return &config, nil +} + +// options map +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 +} diff --git a/datagram.go b/datagram.go new file mode 100644 index 0000000..1d3643e --- /dev/null +++ b/datagram.go @@ -0,0 +1,13 @@ +package sam3 + +import ( + "github.com/go-i2p/go-sam-go/datagram" +) + +// 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 +// are also built to be surveillance-resistant (yey!). +type DatagramSession struct { + datagram.DatagramSession +} diff --git a/datagram_test.go b/datagram_test.go new file mode 100644 index 0000000..f14698a --- /dev/null +++ b/datagram_test.go @@ -0,0 +1,182 @@ +package sam3 + +import ( + "fmt" + "testing" + "time" +) + +func Test_DatagramServerClient(t *testing.T) { + if testing.Short() { + return + } + + fmt.Println("Test_DatagramServerClient") + sam, err := NewSAM(yoursam) + if err != nil { + t.Fail() + return + } + defer sam.Close() + keys, err := sam.NewKeys() + if err != nil { + t.Fail() + return + } + // 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 { + fmt.Println("Server: Failed to create tunnel: " + err.Error()) + t.Fail() + return + } + c, w := make(chan bool), make(chan bool) + go func(c, w chan (bool)) { + sam2, err := NewSAM(yoursam) + if err != nil { + c <- false + return + } + defer sam2.Close() + keys, err := sam2.NewKeys() + if err != nil { + c <- false + return + } + fmt.Println("\tClient: Creating tunnel") + ds2, err := sam2.NewDatagramSession("DGclientTun", keys, []string{"inbound.length=0", "outbound.length=0", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"}, 0) + if err != nil { + c <- false + return + } + defer ds2.Close() + // 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.") + return + } + } + c <- true + }(c, w) + buf := make([]byte, 512) + fmt.Println("\tServer: ReadFrom() waiting...") + n, _, err := ds.ReadFrom(buf) + w <- true + if err != nil { + fmt.Println("\tServer: Failed to ReadFrom(): " + err.Error()) + t.Fail() + return + } + fmt.Println("\tServer: Received datagram: " + string(buf[:n])) + // 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()) + return + } + keys, err := sam.NewKeys() + if err != nil { + fmt.Println(err.Error()) + return + } + myself := keys.Addr() + + // See the example Option_* variables. + dg, err := sam.NewDatagramSession("DGTUN", keys, Options_Small, 0) + if err != nil { + fmt.Println(err.Error()) + return + } + someone, err := sam.Lookup("zzz.i2p") + if err != nil { + fmt.Println(err.Error()) + return + } + + dg.WriteTo([]byte("Hello stranger!"), someone) + dg.WriteTo([]byte("Hello myself!"), myself) + + buf := make([]byte, 31*1024) + n, _, err := dg.ReadFrom(buf) + if err != nil { + fmt.Println(err.Error()) + return + } + fmt.Println("Got message: '" + string(buf[:n]) + "'") + fmt.Println("Got message: " + string(buf[:n])) + + return + // Output: + // Got message: Hello myself! +} + +func ExampleMiniDatagramSession() { + // 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()) + return + } + keys, err := sam.NewKeys() + if err != nil { + fmt.Println(err.Error()) + return + } + myself := keys.Addr() + + // See the example Option_* variables. + dg, err := sam.NewDatagramSession("MINIDGTUN", keys, Options_Small, 0) + if err != nil { + fmt.Println(err.Error()) + return + } + someone, err := sam.Lookup("zzz.i2p") + if err != nil { + fmt.Println(err.Error()) + return + } + err = dg.SetWriteBuffer(14 * 1024) + if err != nil { + fmt.Println(err.Error()) + return + } + + dg.WriteTo([]byte("Hello stranger!"), someone) + dg.WriteTo([]byte("Hello myself!"), myself) + + buf := make([]byte, 31*1024) + n, _, err := dg.ReadFrom(buf) + if err != nil { + fmt.Println(err.Error()) + return + } + log.Println("Got message: '" + string(buf[:n]) + "'") + fmt.Println("Got message: " + string(buf[:n])) + + return + // Output: + // Got message: Hello myself! +} diff --git a/emit-options.go b/emit-options.go new file mode 100644 index 0000000..da1b42d --- /dev/null +++ b/emit-options.go @@ -0,0 +1,436 @@ +package sam3 + +import ( + "fmt" + "strconv" + "strings" + + "github.com/sirupsen/logrus" +) + +// Option is a SAMEmit Option +type Option func(*SAMEmit) error + +// SetType sets the type of the forwarder server +func SetType(s string) func(*SAMEmit) error { + return func(c *SAMEmit) error { + if s == "STREAM" { + c.Style = s + log.WithField("style", s).Debug("Set session style") + return nil + } else if s == "DATAGRAM" { + c.Style = s + log.WithField("style", s).Debug("Set session style") + return nil + } else if s == "RAW" { + c.Style = s + log.WithField("style", s).Debug("Set session style") + return nil + } + log.WithField("style", s).Error("Invalid session style") + return fmt.Errorf("Invalid session STYLE=%s, must be STREAM, DATAGRAM, or RAW", s) + } +} + +// SetSAMAddress sets the SAM address all-at-once +func SetSAMAddress(s string) func(*SAMEmit) error { + return func(c *SAMEmit) error { + sp := strings.Split(s, ":") + if len(sp) > 2 { + log.WithField("address", s).Error("Invalid SAM address") + return fmt.Errorf("Invalid address string: %s", s) + } + if len(sp) == 2 { + port, err := strconv.Atoi(sp[1]) + if err != nil { + log.WithField("port", sp[1]).Error("Invalid SAM port: non-number") + return fmt.Errorf("Invalid SAM port %s; non-number", sp[1]) + } + c.I2PConfig.SamPort = port + } + c.I2PConfig.SamHost = sp[0] + log.WithFields(logrus.Fields{ + "host": c.I2PConfig.SamHost, + "port": c.I2PConfig.SamPort, + }).Debug("Set SAM address") + return nil + } +} + +// SetSAMHost sets the host of the SAMEmit's SAM bridge +func SetSAMHost(s string) func(*SAMEmit) error { + return func(c *SAMEmit) error { + c.I2PConfig.SamHost = s + log.WithField("host", s).Debug("Set SAM host") + return nil + } +} + +// SetSAMPort sets the port of the SAMEmit's SAM bridge using a string +func SetSAMPort(s string) func(*SAMEmit) error { + return func(c *SAMEmit) error { + port, err := strconv.Atoi(s) + if err != nil { + log.WithField("port", s).Error("Invalid SAM port: non-number") + return fmt.Errorf("Invalid SAM port %s; non-number", s) + } + if port < 65536 && port > -1 { + c.I2PConfig.SamPort = port + log.WithField("port", s).Debug("Set SAM port") + return nil + } + log.WithField("port", port).Error("Invalid SAM port") + return fmt.Errorf("Invalid SAM port: out of range") + } +} + +// SetName sets the host of the SAMEmit's SAM bridge +func SetName(s string) func(*SAMEmit) error { + return func(c *SAMEmit) error { + c.I2PConfig.TunName = s + log.WithField("name", s).Debug("Set tunnel name") + return nil + } +} + +// SetInLength sets the number of hops inbound +func SetInLength(u int) func(*SAMEmit) error { + return func(c *SAMEmit) error { + if u < 7 && u >= 0 { + c.I2PConfig.InLength = u + log.WithField("inLength", u).Debug("Set inbound tunnel length") + return nil + } + log.WithField("inLength", u).Error("Invalid inbound tunnel length") + return fmt.Errorf("Invalid inbound tunnel length: out of range") + } +} + +// SetOutLength sets the number of hops outbound +func SetOutLength(u int) func(*SAMEmit) error { + return func(c *SAMEmit) error { + if u < 7 && u >= 0 { + c.I2PConfig.OutLength = u + log.WithField("outLength", u).Debug("Set outbound tunnel length") + return nil + } + log.WithField("outLength", u).Error("Invalid outbound tunnel length") + return fmt.Errorf("Invalid outbound tunnel length: out of range") + } +} + +// SetInVariance sets the variance of a number of hops inbound +func SetInVariance(i int) func(*SAMEmit) error { + return func(c *SAMEmit) error { + if i < 7 && i > -7 { + c.I2PConfig.InVariance = i + log.WithField("inVariance", i).Debug("Set inbound tunnel variance") + return nil + } + log.WithField("inVariance", i).Error("Invalid inbound tunnel variance") + return fmt.Errorf("Invalid inbound tunnel variance: out of range") + } +} + +// SetOutVariance sets the variance of a number of hops outbound +func SetOutVariance(i int) func(*SAMEmit) error { + return func(c *SAMEmit) error { + if i < 7 && i > -7 { + c.I2PConfig.OutVariance = i + log.WithField("outVariance", i).Debug("Set outbound tunnel variance") + return nil + } + log.WithField("outVariance", i).Error("Invalid outbound tunnel variance") + return fmt.Errorf("Invalid outbound tunnel variance: out of range") + } +} + +// SetInQuantity sets the inbound tunnel quantity +func SetInQuantity(u int) func(*SAMEmit) error { + return func(c *SAMEmit) error { + if u <= 16 && u > 0 { + c.I2PConfig.InQuantity = u + log.WithField("inQuantity", u).Debug("Set inbound tunnel quantity") + return nil + } + log.WithField("inQuantity", u).Error("Invalid inbound tunnel quantity") + return fmt.Errorf("Invalid inbound tunnel quantity: out of range") + } +} + +// SetOutQuantity sets the outbound tunnel quantity +func SetOutQuantity(u int) func(*SAMEmit) error { + return func(c *SAMEmit) error { + if u <= 16 && u > 0 { + c.I2PConfig.OutQuantity = u + log.WithField("outQuantity", u).Debug("Set outbound tunnel quantity") + return nil + } + log.WithField("outQuantity", u).Error("Invalid outbound tunnel quantity") + return fmt.Errorf("Invalid outbound tunnel quantity: out of range") + } +} + +// SetInBackups sets the inbound tunnel backups +func SetInBackups(u int) func(*SAMEmit) error { + return func(c *SAMEmit) error { + if u < 6 && u >= 0 { + c.I2PConfig.InBackupQuantity = u + log.WithField("inBackups", u).Debug("Set inbound tunnel backups") + return nil + } + log.WithField("inBackups", u).Error("Invalid inbound tunnel backup quantity") + return fmt.Errorf("Invalid inbound tunnel backup quantity: out of range") + } +} + +// SetOutBackups sets the inbound tunnel backups +func SetOutBackups(u int) func(*SAMEmit) error { + return func(c *SAMEmit) error { + if u < 6 && u >= 0 { + c.I2PConfig.OutBackupQuantity = u + log.WithField("outBackups", u).Debug("Set outbound tunnel backups") + return nil + } + log.WithField("outBackups", u).Error("Invalid outbound tunnel backup quantity") + return fmt.Errorf("Invalid outbound tunnel backup quantity: out of range") + } +} + +// SetEncrypt tells the router to use an encrypted leaseset +func SetEncrypt(b bool) func(*SAMEmit) error { + return func(c *SAMEmit) error { + if b { + c.I2PConfig.EncryptLeaseSet = true + return nil + } + c.I2PConfig.EncryptLeaseSet = false + log.WithField("encrypt", b).Debug("Set lease set encryption") + return nil + } +} + +// SetLeaseSetKey sets the host of the SAMEmit's SAM bridge +func SetLeaseSetKey(s string) func(*SAMEmit) error { + return func(c *SAMEmit) error { + c.I2PConfig.LeaseSetKey = s + log.WithField("leaseSetKey", s).Debug("Set lease set key") + return nil + } +} + +// SetLeaseSetPrivateKey sets the host of the SAMEmit's SAM bridge +func SetLeaseSetPrivateKey(s string) func(*SAMEmit) error { + return func(c *SAMEmit) error { + c.I2PConfig.LeaseSetPrivateKey = s + log.WithField("leaseSetPrivateKey", s).Debug("Set lease set private key") + return nil + } +} + +// SetLeaseSetPrivateSigningKey sets the host of the SAMEmit's SAM bridge +func SetLeaseSetPrivateSigningKey(s string) func(*SAMEmit) error { + return func(c *SAMEmit) error { + c.I2PConfig.LeaseSetPrivateSigningKey = s + log.WithField("leaseSetPrivateSigningKey", s).Debug("Set lease set private signing key") + return nil + } +} + +// SetMessageReliability sets the host of the SAMEmit's SAM bridge +func SetMessageReliability(s string) func(*SAMEmit) error { + return func(c *SAMEmit) error { + c.I2PConfig.MessageReliability = s + log.WithField("messageReliability", s).Debug("Set message reliability") + return nil + } +} + +// SetAllowZeroIn tells the tunnel to accept zero-hop peers +func SetAllowZeroIn(b bool) func(*SAMEmit) error { + return func(c *SAMEmit) error { + if b { + c.I2PConfig.InAllowZeroHop = true + return nil + } + c.I2PConfig.InAllowZeroHop = false + log.WithField("allowZeroIn", b).Debug("Set allow zero-hop inbound") + return nil + } +} + +// SetAllowZeroOut tells the tunnel to accept zero-hop peers +func SetAllowZeroOut(b bool) func(*SAMEmit) error { + return func(c *SAMEmit) error { + if b { + c.I2PConfig.OutAllowZeroHop = true + return nil + } + c.I2PConfig.OutAllowZeroHop = false + log.WithField("allowZeroOut", b).Debug("Set allow zero-hop outbound") + return nil + } +} + +// SetCompress tells clients to use compression +func SetCompress(b bool) func(*SAMEmit) error { + return func(c *SAMEmit) error { + if b { + c.I2PConfig.UseCompression = true + return nil + } + c.I2PConfig.UseCompression = false + log.WithField("compress", b).Debug("Set compression") + return nil + } +} + +// SetFastRecieve tells clients to use compression +func SetFastRecieve(b bool) func(*SAMEmit) error { + return func(c *SAMEmit) error { + if b { + c.I2PConfig.FastRecieve = true + return nil + } + c.I2PConfig.FastRecieve = false + log.WithField("fastReceive", b).Debug("Set fast receive") + return nil + } +} + +// SetReduceIdle tells the connection to reduce it's tunnels during extended idle time. +func SetReduceIdle(b bool) func(*SAMEmit) error { + return func(c *SAMEmit) error { + if b { + c.I2PConfig.ReduceIdle = true + return nil + } + c.I2PConfig.ReduceIdle = false + log.WithField("reduceIdle", b).Debug("Set reduce idle") + return nil + } +} + +// SetReduceIdleTime sets the time to wait before reducing tunnels to idle levels +func SetReduceIdleTime(u int) func(*SAMEmit) error { + return func(c *SAMEmit) error { + c.I2PConfig.ReduceIdleTime = 300000 + if u >= 6 { + idleTime := (u * 60) * 1000 + c.I2PConfig.ReduceIdleTime = idleTime + log.WithField("reduceIdleTime", idleTime).Debug("Set reduce idle time") + return nil + } + log.WithField("minutes", u).Error("Invalid reduce idle timeout") + return fmt.Errorf("Invalid reduce idle timeout (Measured in minutes) %v", u) + } +} + +// SetReduceIdleTimeMs sets the time to wait before reducing tunnels to idle levels in milliseconds +func SetReduceIdleTimeMs(u int) func(*SAMEmit) error { + return func(c *SAMEmit) error { + c.I2PConfig.ReduceIdleTime = 300000 + if u >= 300000 { + c.I2PConfig.ReduceIdleTime = u + log.WithField("reduceIdleTimeMs", u).Debug("Set reduce idle time in milliseconds") + return nil + } + log.WithField("milliseconds", u).Error("Invalid reduce idle timeout") + return fmt.Errorf("Invalid reduce idle timeout (Measured in milliseconds) %v", u) + } +} + +// SetReduceIdleQuantity sets minimum number of tunnels to reduce to during idle time +func SetReduceIdleQuantity(u int) func(*SAMEmit) error { + return func(c *SAMEmit) error { + if u < 5 { + c.I2PConfig.ReduceIdleQuantity = u + log.WithField("reduceIdleQuantity", u).Debug("Set reduce idle quantity") + return nil + } + log.WithField("quantity", u).Error("Invalid reduce tunnel quantity") + return fmt.Errorf("Invalid reduce idle tunnel quantity: out of range") + } +} + +// SetCloseIdle tells the connection to close it's tunnels during extended idle time. +func SetCloseIdle(b bool) func(*SAMEmit) error { + return func(c *SAMEmit) error { + if b { + c.I2PConfig.CloseIdle = true + return nil + } + c.I2PConfig.CloseIdle = false + return nil + } +} + +// SetCloseIdleTime sets the time to wait before closing tunnels to idle levels +func SetCloseIdleTime(u int) func(*SAMEmit) error { + return func(c *SAMEmit) error { + c.I2PConfig.CloseIdleTime = 300000 + if u >= 6 { + idleTime := (u * 60) * 1000 + c.I2PConfig.CloseIdleTime = idleTime + log.WithFields(logrus.Fields{ + "minutes": u, + "milliseconds": idleTime, + }).Debug("Set close idle time") + return nil + } + log.WithField("minutes", u).Error("Invalid close idle timeout") + return fmt.Errorf("Invalid close idle timeout (Measured in minutes) %v", u) + } +} + +// SetCloseIdleTimeMs sets the time to wait before closing tunnels to idle levels in milliseconds +func SetCloseIdleTimeMs(u int) func(*SAMEmit) error { + return func(c *SAMEmit) error { + c.I2PConfig.CloseIdleTime = 300000 + if u >= 300000 { + c.I2PConfig.CloseIdleTime = u + log.WithField("closeIdleTimeMs", u).Debug("Set close idle time in milliseconds") + return nil + } + return fmt.Errorf("Invalid close idle timeout (Measured in milliseconds) %v", u) + } +} + +// SetAccessListType tells the system to treat the AccessList as a whitelist +func SetAccessListType(s string) func(*SAMEmit) error { + return func(c *SAMEmit) error { + if s == "whitelist" { + c.I2PConfig.AccessListType = "whitelist" + log.Debug("Set access list type to whitelist") + return nil + } else if s == "blacklist" { + c.I2PConfig.AccessListType = "blacklist" + log.Debug("Set access list type to blacklist") + return nil + } else if s == "none" { + c.I2PConfig.AccessListType = "" + log.Debug("Set access list type to none") + return nil + } else if s == "" { + c.I2PConfig.AccessListType = "" + log.Debug("Set access list type to none") + return nil + } + return fmt.Errorf("Invalid Access list type (whitelist, blacklist, none)") + } +} + +// SetAccessList tells the system to treat the AccessList as a whitelist +func SetAccessList(s []string) func(*SAMEmit) error { + return func(c *SAMEmit) error { + if len(s) > 0 { + for _, a := range s { + c.I2PConfig.AccessList = append(c.I2PConfig.AccessList, a) + } + log.WithField("accessList", s).Debug("Set access list") + return nil + } + log.Debug("No access list set (empty list provided)") + return nil + } +} diff --git a/emit.go b/emit.go new file mode 100644 index 0000000..43b07aa --- /dev/null +++ b/emit.go @@ -0,0 +1,141 @@ +package sam3 + +import ( + "fmt" + "net" + "strings" + + "github.com/go-i2p/go-sam-go/common" + "github.com/sirupsen/logrus" +) + +type SAMEmit struct { + common.SAMEmit +} + +func (e *SAMEmit) SamOptionsString() string { + optStr := strings.Join(e.I2PConfig.Print(), " ") + log.WithField("optStr", optStr).Debug("Generated option string") + return optStr +} + +func (e *SAMEmit) Hello() string { + hello := fmt.Sprintf("HELLO VERSION MIN=%s MAX=%s \n", e.I2PConfig.MinSAM(), e.I2PConfig.MaxSAM()) + log.WithField("hello", hello).Debug("Generated HELLO command") + return hello +} + +func (e *SAMEmit) HelloBytes() []byte { + return []byte(e.Hello()) +} + +func (e *SAMEmit) GenerateDestination() string { + dest := fmt.Sprintf("DEST GENERATE %s \n", e.I2PConfig.SignatureType()) + log.WithField("destination", dest).Debug("Generated DEST GENERATE command") + return dest +} + +func (e *SAMEmit) GenerateDestinationBytes() []byte { + return []byte(e.GenerateDestination()) +} + +func (e *SAMEmit) Lookup(name string) string { + lookup := fmt.Sprintf("NAMING LOOKUP NAME=%s \n", name) + log.WithField("lookup", lookup).Debug("Generated NAMING LOOKUP command") + return lookup +} + +func (e *SAMEmit) LookupBytes(name string) []byte { + return []byte(e.Lookup(name)) +} + +func (e *SAMEmit) Create() string { + create := fmt.Sprintf( + // //1 2 3 4 5 6 7 + "SESSION CREATE %s%s%s%s%s%s%s \n", + e.I2PConfig.SessionStyle(), // 1 + e.I2PConfig.FromPort(), // 2 + e.I2PConfig.ToPort(), // 3 + e.I2PConfig.ID(), // 4 + e.I2PConfig.DestinationKey(), // 5 + e.I2PConfig.SignatureType(), // 6 + e.SamOptionsString(), // 7 + ) + log.WithField("create", create).Debug("Generated SESSION CREATE command") + return create +} + +func (e *SAMEmit) CreateBytes() []byte { + fmt.Println("sam command: " + e.Create()) + return []byte(e.Create()) +} + +func (e *SAMEmit) Connect(dest string) string { + connect := fmt.Sprintf( + "STREAM CONNECT ID=%s %s %s DESTINATION=%s \n", + e.I2PConfig.ID(), + e.I2PConfig.FromPort(), + e.I2PConfig.ToPort(), + dest, + ) + log.WithField("connect", connect).Debug("Generated STREAM CONNECT command") + return connect +} + +func (e *SAMEmit) ConnectBytes(dest string) []byte { + return []byte(e.Connect(dest)) +} + +func (e *SAMEmit) Accept() string { + accept := fmt.Sprintf( + "STREAM ACCEPT ID=%s %s %s", + e.I2PConfig.ID(), + e.I2PConfig.FromPort(), + e.I2PConfig.ToPort(), + ) + log.WithField("accept", accept).Debug("Generated STREAM ACCEPT command") + return accept +} + +func (e *SAMEmit) AcceptBytes() []byte { + return []byte(e.Accept()) +} + +func NewEmit(opts ...func(*SAMEmit) error) (*SAMEmit, error) { + var emit SAMEmit + for _, o := range opts { + if err := o(&emit); err != nil { + log.WithError(err).Error("Failed to apply option") + return nil, err + } + } + log.Debug("New SAMEmit instance created") + return &emit, nil +} + +func IgnorePortError(err error) error { + if err == nil { + return nil + } + if strings.Contains(err.Error(), "missing port in address") { + log.Debug("Ignoring 'missing port in address' error") + err = nil + } + return err +} + +func SplitHostPort(hostport string) (string, string, error) { + host, port, err := net.SplitHostPort(hostport) + if err != nil { + if IgnorePortError(err) == nil { + log.WithField("host", hostport).Debug("Using full string as host, port set to 0") + host = hostport + port = "0" + } + } + log.WithFields(logrus.Fields{ + "host": host, + "port": port, + }).Debug("Split host and port") + return host, port, nil +} diff --git a/log.go b/log.go new file mode 100644 index 0000000..e7c0001 --- /dev/null +++ b/log.go @@ -0,0 +1,51 @@ +package sam3 + +import ( + "io/ioutil" + "os" + "strings" + "sync" + + "github.com/sirupsen/logrus" +) + +var ( + log *logrus.Logger + once sync.Once +) + +func InitializeSAM3Logger() { + once.Do(func() { + log = logrus.New() + // We do not want to log by default + log.SetOutput(ioutil.Discard) + log.SetLevel(logrus.PanicLevel) + // Check if DEBUG_I2P is set + if logLevel := os.Getenv("DEBUG_I2P"); logLevel != "" { + log.SetOutput(os.Stdout) + switch strings.ToLower(logLevel) { + case "debug": + log.SetLevel(logrus.DebugLevel) + case "warn": + log.SetLevel(logrus.WarnLevel) + case "error": + log.SetLevel(logrus.ErrorLevel) + default: + log.SetLevel(logrus.DebugLevel) + } + log.WithField("level", log.GetLevel()).Debug("Logging enabled.") + } + }) +} + +// GetSAM3Logger returns the initialized logger +func GetSAM3Logger() *logrus.Logger { + if log == nil { + InitializeSAM3Logger() + } + return log +} + +func init() { + InitializeSAM3Logger() +} diff --git a/primary.go b/primary.go new file mode 100644 index 0000000..9db8828 --- /dev/null +++ b/primary.go @@ -0,0 +1,40 @@ +package sam3 + +import ( + "math/rand" + "strconv" + "time" + + "github.com/go-i2p/go-sam-go/primary" +) + +const ( + session_ADDOK = "SESSION STATUS RESULT=OK" +) + +func randport() string { + s := rand.NewSource(time.Now().UnixNano()) + r := rand.New(s) + p := r.Intn(55534) + 10000 + port := strconv.Itoa(p) + log.WithField("port", port).Debug("Generated random port") + return strconv.Itoa(p) +} + +// Represents a primary session. +type PrimarySession struct { + *primary.PrimarySession +} + +var PrimarySessionSwitch = "MASTER" + +func (p *PrimarySession) NewStreamSubSession(id string) (*StreamSession, error) { + log.WithField("id", id).Debug("NewStreamSubSession called") + session, err := p.PrimarySession.NewStreamSubSession(id) + if err != nil { + return nil, err + } + return &StreamSession{ + StreamSession: session, + }, nil +} diff --git a/primary/primary.go b/primary/primary.go index 31c44f7..7f6a195 100644 --- a/primary/primary.go +++ b/primary/primary.go @@ -1,6 +1,17 @@ package primary -/* +import ( + "time" + + "github.com/go-i2p/go-sam-go/common" + "github.com/go-i2p/go-sam-go/datagram" + "github.com/go-i2p/go-sam-go/stream" + "github.com/go-i2p/i2pkeys" + "github.com/sirupsen/logrus" +) + +var PrimarySessionSwitch string = "MASTER" + // Creates a new PrimarySession with the I2CP- and streaminglib options as // specified. See the I2P documentation for a full list of options. func (sam *SAM) NewPrimarySession(id string, keys i2pkeys.I2PKeys, options []string) (*PrimarySession, error) { @@ -8,21 +19,31 @@ func (sam *SAM) NewPrimarySession(id string, keys i2pkeys.I2PKeys, options []str return sam.newPrimarySession(PrimarySessionSwitch, id, keys, options) } -func (sam *SAM) newPrimarySession(primarySessionSwitch string, id string, keys i2pkeys.I2PKeys, options []string) (*PrimarySession, error) { +func (sam *SAM) newPrimarySession(primarySessionSwitch, id string, keys i2pkeys.I2PKeys, options []string) (*PrimarySession, error) { log.WithFields(logrus.Fields{ "primarySessionSwitch": primarySessionSwitch, "id": id, "options": options, }).Debug("newPrimarySession() called") - conn, err := sam.newGenericSession(primarySessionSwitch, id, keys, options, []string{}) + conn, err := sam.NewGenericSession(primarySessionSwitch, id, keys, options) if err != nil { log.WithError(err).Error("Failed to create new generic session") return nil, err } - ssesss := make(map[string]*StreamSession) - dsesss := make(map[string]*DatagramSession) - return &PrimarySession{sam.Config.I2PConfig.Sam(), id, conn, keys, time.Duration(600 * time.Second), time.Now(), Sig_NONE, sam.Config, ssesss, dsesss}, nil + return &PrimarySession{ + SAM: sam, + samAddr: "", + id: id, + conn: conn, + keys: keys, + Timeout: 0, + Deadline: time.Time{}, + sigType: "", + Config: common.SAMEmit{}, + stsess: map[string]*stream.StreamSession{}, + dgsess: map[string]*datagram.DatagramSession{}, + }, nil } // Creates a new PrimarySession with the I2CP- and PRIMARYinglib options as @@ -34,13 +55,22 @@ func (sam *SAM) NewPrimarySessionWithSignature(id string, keys i2pkeys.I2PKeys, "sigType": sigType, }).Debug("NewPrimarySessionWithSignature() called") - conn, err := sam.newGenericSessionWithSignature(PrimarySessionSwitch, id, keys, sigType, options, []string{}) + conn, err := sam.NewGenericSessionWithSignature(PrimarySessionSwitch, id, keys, sigType, options) if err != nil { log.WithError(err).Error("Failed to create new generic session with signature") return nil, err } - ssesss := make(map[string]*stream.StreamSession) - dsesss := make(map[string]*datagram.DatagramSession) - return &PrimarySession{sam.Config.I2PConfig.Sam(), id, conn, keys, time.Duration(600 * time.Second), time.Now(), sigType, sam.Config, ssesss, dsesss}, nil + return &PrimarySession{ + SAM: sam, + samAddr: "", + id: id, + conn: conn, + keys: keys, + Timeout: 0, + Deadline: time.Time{}, + sigType: sigType, + Config: common.SAMEmit{}, + stsess: map[string]*stream.StreamSession{}, + dgsess: map[string]*datagram.DatagramSession{}, + }, nil } -*/ diff --git a/primary_datagram_test.go b/primary_datagram_test.go new file mode 100644 index 0000000..8786585 --- /dev/null +++ b/primary_datagram_test.go @@ -0,0 +1,148 @@ +package sam3 + +import ( + "fmt" + "testing" + "time" +) + +func Test_PrimaryDatagramServerClient(t *testing.T) { + if testing.Short() { + return + } + + fmt.Println("Test_PrimaryDatagramServerClient") + earlysam, err := NewSAM(yoursam) + if err != nil { + t.Fail() + return + } + defer earlysam.Close() + keys, err := earlysam.NewKeys() + if err != nil { + t.Fail() + return + } + + sam, err := earlysam.NewPrimarySession("PrimaryTunnel", keys, []string{"inbound.length=0", "outbound.length=0", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"}) + if err != nil { + t.Fail() + return + } + defer sam.Close() + // fmt.Println("\tServer: My address: " + keys.Addr().Base32()) + fmt.Println("\tServer: Creating tunnel") + ds, err := sam.NewDatagramSubSession("PrimaryTunnel"+RandString(), 0) + if err != nil { + fmt.Println("Server: Failed to create tunnel: " + err.Error()) + t.Fail() + return + } + defer ds.Close() + c, w := make(chan bool), make(chan bool) + go func(c, w chan (bool)) { + sam2, err := NewSAM(yoursam) + if err != nil { + c <- false + return + } + defer sam2.Close() + keys, err := sam2.NewKeys() + if err != nil { + c <- false + return + } + fmt.Println("\tClient: Creating tunnel") + ds2, err := sam2.NewDatagramSession("PRIMARYClientTunnel", keys, []string{"inbound.length=0", "outbound.length=0", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"}, 0) + if err != nil { + c <- false + return + } + defer ds2.Close() + // fmt.Println("\tClient: Servers address: " + ds.LocalAddr().Base32()) + // fmt.Println("\tClient: Clients address: " + ds2.LocalAddr().Base32()) + fmt.Println("\tClient: Tries to send primary to server") + for { + select { + default: + _, err = ds2.WriteTo([]byte("Hello primary-world! <3 <3 <3 <3 <3 <3"), ds.LocalAddr()) + if err != nil { + fmt.Println("\tClient: Failed to send primary: " + err.Error()) + c <- false + return + } + time.Sleep(5 * time.Second) + case <-w: + fmt.Println("\tClient: Sent primary, quitting.") + return + } + } + c <- true + }(c, w) + buf := make([]byte, 512) + fmt.Println("\tServer: ReadFrom() waiting...") + n, _, err := ds.ReadFrom(buf) + w <- true + if err != nil { + fmt.Println("\tServer: Failed to ReadFrom(): " + err.Error()) + t.Fail() + return + } + fmt.Println("\tServer: Received primary: " + string(buf[:n])) + // fmt.Println("\tServer: Senders address was: " + saddr.Base32()) +} + +func ExamplePrimaryDatagramSession() { + // Creates a new PrimarySession, then creates a Datagram subsession on top of it + + const samBridge = "127.0.0.1:7656" + + earlysam, err := NewSAM(samBridge) + if err != nil { + fmt.Println(err.Error()) + return + } + defer earlysam.Close() + keys, err := earlysam.NewKeys() + if err != nil { + fmt.Println(err.Error()) + return + } + myself := keys.Addr() + + sam, err := earlysam.NewPrimarySession("PrimaryTunnel", keys, []string{"inbound.length=0", "outbound.length=0", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"}) + if err != nil { + fmt.Println(err.Error()) + return + } + defer sam.Close() + + // See the example Option_* variables. + dg, err := sam.NewDatagramSubSession("DGTUN"+RandString(), 0) + if err != nil { + fmt.Println(err.Error()) + return + } + defer dg.Close() + someone, err := earlysam.Lookup("zzz.i2p") + if err != nil { + fmt.Println(err.Error()) + return + } + + dg.WriteTo([]byte("Hello stranger!"), someone) + dg.WriteTo([]byte("Hello myself!"), myself) + + buf := make([]byte, 31*1024) + n, _, err := dg.ReadFrom(buf) + if err != nil { + fmt.Println(err.Error()) + return + } + fmt.Println("Got message: '" + string(buf[:n]) + "'") + fmt.Println("Got message: " + string(buf[:n])) + + return + // Output: + // Got message: Hello myself! +} diff --git a/primary_stream_test.go b/primary_stream_test.go new file mode 100644 index 0000000..4bf50ad --- /dev/null +++ b/primary_stream_test.go @@ -0,0 +1,306 @@ +package sam3 + +import ( + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + "time" +) + +func Test_PrimaryStreamingDial(t *testing.T) { + if testing.Short() { + return + } + fmt.Println("Test_PrimaryStreamingDial") + earlysam, err := NewSAM(yoursam) + if err != nil { + t.Fail() + return + } + defer earlysam.Close() + keys, err := earlysam.NewKeys() + if err != nil { + t.Fail() + return + } + + sam, err := earlysam.NewPrimarySession("PrimaryTunnel", keys, []string{"inbound.length=0", "outbound.length=0", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"}) + if err != nil { + t.Fail() + return + } + defer sam.Close() + fmt.Println("\tBuilding tunnel") + ss, err := sam.NewStreamSubSession("primaryStreamTunnel") + if err != nil { + fmt.Println(err.Error()) + t.Fail() + return + } + defer ss.Close() + fmt.Println("\tNotice: This may fail if your I2P node is not well integrated in the I2P network.") + fmt.Println("\tLooking up i2p-projekt.i2p") + forumAddr, err := earlysam.Lookup("i2p-projekt.i2p") + if err != nil { + fmt.Println(err.Error()) + t.Fail() + return + } + fmt.Println("\tDialing i2p-projekt.i2p(", forumAddr.Base32(), forumAddr.DestHash().Hash(), ")") + conn, err := ss.DialI2P(forumAddr) + if err != nil { + fmt.Println(err.Error()) + t.Fail() + return + } + defer conn.Close() + fmt.Println("\tSending HTTP GET /") + if _, err := conn.Write([]byte("GET /\n")); err != nil { + fmt.Println(err.Error()) + t.Fail() + return + } + buf := make([]byte, 4096) + n, err := conn.Read(buf) + if !strings.Contains(strings.ToLower(string(buf[:n])), "http") && !strings.Contains(strings.ToLower(string(buf[:n])), "html") { + fmt.Printf("\tProbably failed to StreamSession.DialI2P(i2p-projekt.i2p)? It replied %d bytes, but nothing that looked like http/html", n) + } else { + fmt.Println("\tRead HTTP/HTML from i2p-projekt.i2p") + } +} + +func Test_PrimaryStreamingServerClient(t *testing.T) { + if testing.Short() { + return + } + + fmt.Println("Test_StreamingServerClient") + earlysam, err := NewSAM(yoursam) + if err != nil { + t.Fail() + return + } + defer earlysam.Close() + keys, err := earlysam.NewKeys() + if err != nil { + t.Fail() + return + } + + sam, err := earlysam.NewPrimarySession("PrimaryServerClientTunnel", keys, []string{"inbound.length=0", "outbound.length=0", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"}) + if err != nil { + t.Fail() + return + } + defer sam.Close() + fmt.Println("\tServer: Creating tunnel") + ss, err := sam.NewUniqueStreamSubSession("PrimaryServerClientTunnel") + if err != nil { + return + } + defer ss.Close() + time.Sleep(time.Second * 10) + c, w := make(chan bool), make(chan bool) + go func(c, w chan (bool)) { + if !(<-w) { + return + } + /* + sam2, err := NewSAM(yoursam) + if err != nil { + c <- false + return + } + defer sam2.Close() + keys, err := sam2.NewKeys() + if err != nil { + c <- false + return + } + */ + + fmt.Println("\tClient: Creating tunnel") + ss2, err := sam.NewStreamSubSession("primaryExampleClientTun") + if err != nil { + c <- false + return + } + defer ss2.Close() + fmt.Println("\tClient: Connecting to server") + conn, err := ss2.DialI2P(ss.Addr()) + if err != nil { + c <- false + return + } + fmt.Println("\tClient: Connected to tunnel") + defer conn.Close() + _, err = conn.Write([]byte("Hello world <3 <3 <3 <3 <3 <3")) + if err != nil { + c <- false + return + } + c <- true + }(c, w) + l, err := ss.Listen() + if err != nil { + fmt.Println("ss.Listen(): " + err.Error()) + t.Fail() + w <- false + return + } + defer l.Close() + w <- true + fmt.Println("\tServer: Accept()ing on tunnel") + conn, err := l.Accept() + if err != nil { + t.Fail() + fmt.Println("Failed to Accept(): " + err.Error()) + return + } + defer conn.Close() + buf := make([]byte, 512) + n, err := conn.Read(buf) + fmt.Printf("\tClient exited successfully: %t\n", <-c) + fmt.Println("\tServer: received from Client: " + string(buf[:n])) +} + +func ExamplePrimaryStreamSession() { + // Creates a new StreamingSession, dials to idk.i2p and gets a SAMConn + // which behaves just like a normal net.Conn. + + const samBridge = "127.0.0.1:7656" + + earlysam, err := NewSAM(yoursam) + if err != nil { + log.Fatal(err.Error()) + return + } + defer earlysam.Close() + keys, err := earlysam.NewKeys() + if err != nil { + log.Fatal(err.Error()) + return + } + + sam, err := earlysam.NewPrimarySession("PrimaryStreamSessionTunnel", keys, []string{"inbound.length=0", "outbound.length=0", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"}) + if err != nil { + log.Fatal(err.Error()) + return + } + defer sam.Close() + conn, err := sam.Dial("tcp", "idk.i2p") // someone.Base32()) + if err != nil { + fmt.Println(err.Error()) + return + } + defer conn.Close() + fmt.Println("Sending HTTP GET /") + if _, err := conn.Write([]byte("GET /\n")); err != nil { + fmt.Println(err.Error()) + return + } + buf := make([]byte, 4096) + n, err := conn.Read(buf) + if !strings.Contains(strings.ToLower(string(buf[:n])), "http") && !strings.Contains(strings.ToLower(string(buf[:n])), "html") { + fmt.Printf("Probably failed to StreamSession.DialI2P(idk.i2p)? It replied %d bytes, but nothing that looked like http/html", n) + log.Printf("Probably failed to StreamSession.DialI2P(idk.i2p)? It replied %d bytes, but nothing that looked like http/html", n) + } else { + fmt.Println("Read HTTP/HTML from idk.i2p") + log.Println("Read HTTP/HTML from idk.i2p") + } + // Output: + // Sending HTTP GET / + // Read HTTP/HTML from idk.i2p +} + +func ExamplePrimaryStreamListener() { + // 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" + + var ss *StreamSession + go func() { + earlysam, err := NewSAM(yoursam) + if err != nil { + log.Fatal(err.Error()) + return + } + defer earlysam.Close() + keys, err := earlysam.NewKeys() + if err != nil { + log.Fatal(err.Error()) + return + } + sam, err := earlysam.NewPrimarySession("PrimaryListenerTunnel", keys, []string{"inbound.length=0", "outbound.length=0", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"}) + if err != nil { + log.Fatal(err.Error()) + return + } + defer sam.Close() + ss, err = sam.NewStreamSubSession("PrimaryListenerServerTunnel2") + if err != nil { + fmt.Println(err.Error()) + return + } + defer ss.Close() + l, err := ss.Listen() + if err != nil { + fmt.Println(err.Error()) + return + } + defer l.Close() + // fmt.Println("Serving on primary listener", l.Addr().String()) + if err := http.Serve(l, &exitHandler{}); err != nil { + fmt.Println(err.Error()) + } + }() + time.Sleep(time.Second * 10) + latesam, err := NewSAM(yoursam) + if err != nil { + log.Fatal(err.Error()) + return + } + defer latesam.Close() + keys2, err := latesam.NewKeys() + if err != nil { + log.Fatal(err.Error()) + return + } + sc, err := latesam.NewStreamSession("PrimaryListenerClientTunnel2", keys2, []string{"inbound.length=0", "outbound.length=0", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"}) + if err != nil { + fmt.Println(err.Error()) + return + } + defer sc.Close() + client := http.Client{ + Transport: &http.Transport{ + Dial: sc.Dial, + }, + } + // resp, err := client.Get("http://" + "idk.i2p") //ss.Addr().Base32()) + resp, err := client.Get("http://" + ss.Addr().Base32()) + if err != nil { + fmt.Println(err.Error()) + return + } + defer resp.Body.Close() + r, err := ioutil.ReadAll(resp.Body) + if err != nil { + fmt.Println(err.Error()) + return + } + fmt.Println("Got response: " + string(r)) + + // Output: + // Got response: Hello world! +} + +type exitHandler struct{} + +func (e *exitHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Hello world!")) +} diff --git a/raw.go b/raw.go new file mode 100644 index 0000000..865a003 --- /dev/null +++ b/raw.go @@ -0,0 +1,15 @@ +package sam3 + +import ( + "github.com/go-i2p/go-sam-go/raw" +) + +// The RawSession provides no authentication of senders, and there is no sender +// 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 +// overhead of authentication, which is the reason to use this.. +type RawSession struct { + *raw.RawSession +} diff --git a/resolver.go b/resolver.go new file mode 100644 index 0000000..177446f --- /dev/null +++ b/resolver.go @@ -0,0 +1,24 @@ +package sam3 + +type SAMResolver struct { + *SAM +} + +func NewSAMResolver(parent *SAM) (*SAMResolver, error) { + log.Debug("Creating new SAMResolver from existing SAM instance") + var s SAMResolver + s.SAM = parent + return &s, nil +} + +func NewFullSAMResolver(address string) (*SAMResolver, error) { + log.WithField("address", address).Debug("Creating new full SAMResolver") + var s SAMResolver + var err error + s.SAM, err = NewSAM(address) + if err != nil { + log.WithError(err).Error("Failed to create new SAM instance") + return nil, err + } + return &s, nil +} diff --git a/sam3.go b/sam3.go new file mode 100644 index 0000000..84fed7f --- /dev/null +++ b/sam3.go @@ -0,0 +1,101 @@ +// Library for I2Ps SAMv3 bridge (https://geti2p.com) +package sam3 + +import ( + "math/rand" + + "github.com/go-i2p/go-sam-go/common" + "github.com/go-i2p/go-sam-go/datagram" + "github.com/go-i2p/go-sam-go/primary" + "github.com/go-i2p/go-sam-go/stream" + "github.com/go-i2p/i2pkeys" +) + +func init() { + InitializeSAM3Logger() +} + +// Used for controlling I2Ps SAMv3. +type SAM struct { + *common.SAM +} + +// Creates a new stream session by wrapping stream.NewStreamSession +func (s *SAM) NewStreamSession(param1 string, keys i2pkeys.I2PKeys, param3 []string) (*StreamSession, error) { + sam := stream.SAM(*s.SAM) + ss, err := sam.NewStreamSession(param1, keys, param3) + if err != nil { + return nil, err + } + streamSession := &StreamSession{ + StreamSession: ss, + } + return streamSession, nil +} + +// Creates a new Datagram session by wrapping datagram.NewDatagramSession +func (s *SAM) NewDatagramSession(id string, keys i2pkeys.I2PKeys, options []string, port int) (*DatagramSession, error) { + sam := datagram.SAM(*s.SAM) + dgs, err := sam.NewDatagramSession(id, keys, options, port) + if err != nil { + return nil, err + } + datagramSession := DatagramSession{ + DatagramSession: *dgs, + } + return &datagramSession, nil +} + +func (s *SAM) NewPrimarySession(id string, keys i2pkeys.I2PKeys, options []string) (*PrimarySession, error) { + sam := primary.SAM(*s.SAM) + ps, err := sam.NewPrimarySession(id, keys, options) + if err != nil { + return nil, err + } + primarySession := PrimarySession{ + PrimarySession: ps, + } + return &primarySession, nil +} + +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=" +) + +const ( + Sig_NONE = "SIGNATURE_TYPE=EdDSA_SHA512_Ed25519" + Sig_DSA_SHA1 = "SIGNATURE_TYPE=DSA_SHA1" + Sig_ECDSA_SHA256_P256 = "SIGNATURE_TYPE=ECDSA_SHA256_P256" + Sig_ECDSA_SHA384_P384 = "SIGNATURE_TYPE=ECDSA_SHA384_P384" + Sig_ECDSA_SHA512_P521 = "SIGNATURE_TYPE=ECDSA_SHA512_P521" + Sig_EdDSA_SHA512_Ed25519 = "SIGNATURE_TYPE=EdDSA_SHA512_Ed25519" +) + +var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + +func RandString() string { + n := 4 + b := make([]rune, n) + for i := range b { + b[i] = letters[rand.Intn(len(letters))] + } + log.WithField("randomString", string(b)).Debug("Generated random string") + return string(b) +} + +// Creates a new controller for the I2P routers SAM bridge. +func NewSAM(address string) (*SAM, error) { + is, err := common.NewSAM(address) + if err != nil { + log.WithError(err).Error("Failed to create new SAM instance") + return nil, err + } + s := &SAM{ + SAM: is, + } + return s, nil +} diff --git a/sam3_test.go b/sam3_test.go new file mode 100644 index 0000000..847182f --- /dev/null +++ b/sam3_test.go @@ -0,0 +1,164 @@ +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) + sam, err := NewSAM(yoursam) + if err != nil { + fmt.Println(err.Error()) + t.Fail() + return + } + + fmt.Println("\tCreating new keys...") + keys, err := sam.NewKeys() + if err != nil { + fmt.Println(err.Error()) + t.Fail() + } else { + fmt.Println("\tAddress created: " + keys.Addr().Base32()) + fmt.Println("\tI2PKeys: " + string(keys.String())[: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()) + t.Fail() + } +} + +/* +func Test_GenericSession(t *testing.T) { + if testing.Short() { + return + } + fmt.Println("Test_GenericSession") + sam, err := NewSAM(yoursam) + if err != nil { + fmt.Println(err.Error) + t.Fail() + return + } + keys, err := sam.NewKeys() + if err != nil { + fmt.Println(err.Error()) + t.Fail() + } else { + conn1, err := sam.newGenericSession("STREAM", "testTun", keys, []string{}) + if err != nil { + fmt.Println(err.Error()) + t.Fail() + } else { + conn1.Close() + } + conn2, err := sam.newGenericSession("STREAM", "testTun", keys, []string{"inbound.length=1", "outbound.length=1", "inbound.lengthVariance=1", "outbound.lengthVariance=1", "inbound.quantity=1", "outbound.quantity=1"}) + if err != nil { + fmt.Println(err.Error()) + t.Fail() + } else { + conn2.Close() + } + conn3, err := sam.newGenericSession("DATAGRAM", "testTun", keys, []string{"inbound.length=1", "outbound.length=1", "inbound.lengthVariance=1", "outbound.lengthVariance=1", "inbound.quantity=1", "outbound.quantity=1"}) + if err != nil { + fmt.Println(err.Error()) + t.Fail() + } else { + conn3.Close() + } + } + if err := sam.Close(); err != nil { + fmt.Println(err.Error()) + t.Fail() + } +} +*/ + +func Test_RawServerClient(t *testing.T) { + if testing.Short() { + return + } + + fmt.Println("Test_RawServerClient") + sam, err := NewSAM(yoursam) + if err != nil { + t.Fail() + return + } + defer sam.Close() + keys, err := sam.NewKeys() + if err != nil { + t.Fail() + return + } + fmt.Println("\tServer: Creating tunnel") + rs, err := sam.NewDatagramSession("RAWserverTun", keys, []string{"inbound.length=0", "outbound.length=0", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"}, 0) + if err != nil { + fmt.Println("Server: Failed to create tunnel: " + err.Error()) + t.Fail() + return + } + c, w := make(chan bool), make(chan bool) + go func(c, w chan (bool)) { + sam2, err := NewSAM(yoursam) + if err != nil { + c <- false + return + } + defer sam2.Close() + keys, err := sam2.NewKeys() + if err != nil { + c <- false + return + } + fmt.Println("\tClient: Creating tunnel") + rs2, err := sam2.NewDatagramSession("RAWclientTun", keys, []string{"inbound.length=0", "outbound.length=0", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"}, 0) + if err != nil { + c <- false + return + } + defer rs2.Close() + 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.") + return + } + } + c <- true + }(c, w) + buf := make([]byte, 512) + fmt.Println("\tServer: Read() waiting...") + n, _, err := rs.ReadFrom(buf) + w <- true + if err != nil { + fmt.Println("\tServer: Failed to Read(): " + err.Error()) + t.Fail() + return + } + fmt.Println("\tServer: Received datagram: " + string(buf[:n])) + // fmt.Println("\tServer: Senders address was: " + saddr.Base32()) +} diff --git a/stream.go b/stream.go new file mode 100644 index 0000000..5203dd4 --- /dev/null +++ b/stream.go @@ -0,0 +1,29 @@ +package sam3 + +import ( + "time" + + "github.com/go-i2p/go-sam-go/stream" +) + +// Represents a streaming session. +type StreamSession struct { + *stream.StreamSession +} + +/* +func (s *StreamSession) Cancel() chan *StreamSession { + ch := make(chan *StreamSession) + ch <- s + return ch +}*/ + +func minNonzeroTime(a, b time.Time) time.Time { + if a.IsZero() { + return b + } + if b.IsZero() || a.Before(b) { + return a + } + return b +} diff --git a/streamListener.go b/streamListener.go new file mode 100644 index 0000000..77e3686 --- /dev/null +++ b/streamListener.go @@ -0,0 +1,9 @@ +package sam3 + +import ( + "github.com/go-i2p/go-sam-go/stream" +) + +type StreamListener struct { + *stream.StreamListener +} diff --git a/stream_test.go b/stream_test.go new file mode 100644 index 0000000..8add136 --- /dev/null +++ b/stream_test.go @@ -0,0 +1,283 @@ +package sam3 + +import ( + "fmt" + "strings" + "testing" + + "github.com/go-i2p/i2pkeys" +) + +func Test_StreamingDial(t *testing.T) { + if testing.Short() { + return + } + fmt.Println("Test_StreamingDial") + sam, err := NewSAM(yoursam) + if err != nil { + fmt.Println(err.Error()) + t.Fail() + return + } + defer sam.Close() + keys, err := sam.NewKeys() + if err != nil { + fmt.Println(err.Error()) + t.Fail() + return + } + fmt.Println("\tBuilding tunnel") + ss, err := sam.NewStreamSession("streamTun", keys, []string{"inbound.length=1", "outbound.length=1", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"}) + if err != nil { + fmt.Println(err.Error()) + t.Fail() + return + } + fmt.Println("\tNotice: This may fail if your I2P node is not well integrated in the I2P network.") + fmt.Println("\tLooking up i2p-projekt.i2p") + forumAddr, err := sam.Lookup("i2p-projekt.i2p") + if err != nil { + fmt.Println(err.Error()) + t.Fail() + return + } + fmt.Println("\tDialing i2p-projekt.i2p(", forumAddr.Base32(), forumAddr.DestHash().Hash(), ")") + conn, err := ss.DialI2P(forumAddr) + if err != nil { + fmt.Println(err.Error()) + t.Fail() + return + } + defer conn.Close() + fmt.Println("\tSending HTTP GET /") + if _, err := conn.Write([]byte("GET /\n")); err != nil { + fmt.Println(err.Error()) + t.Fail() + return + } + buf := make([]byte, 4096) + n, err := conn.Read(buf) + if !strings.Contains(strings.ToLower(string(buf[:n])), "http") && !strings.Contains(strings.ToLower(string(buf[:n])), "html") { + fmt.Printf("\tProbably failed to StreamSession.DialI2P(i2p-projekt.i2p)? It replied %d bytes, but nothing that looked like http/html", n) + } else { + fmt.Println("\tRead HTTP/HTML from i2p-projekt.i2p") + } +} + +func Test_StreamingServerClient(t *testing.T) { + if testing.Short() { + return + } + + fmt.Println("Test_StreamingServerClient") + sam, err := NewSAM(yoursam) + if err != nil { + t.Fail() + return + } + defer sam.Close() + keys, err := sam.NewKeys() + if err != nil { + t.Fail() + return + } + fmt.Println("\tServer: Creating tunnel") + ss, err := sam.NewStreamSession("serverTun", keys, []string{"inbound.length=0", "outbound.length=0", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"}) + if err != nil { + return + } + c, w := make(chan bool), make(chan bool) + go func(c, w chan (bool)) { + if !(<-w) { + return + } + sam2, err := NewSAM(yoursam) + if err != nil { + c <- false + return + } + defer sam2.Close() + keys, err := sam2.NewKeys() + if err != nil { + c <- false + return + } + fmt.Println("\tClient: Creating tunnel") + ss2, err := sam2.NewStreamSession("clientTun", keys, []string{"inbound.length=0", "outbound.length=0", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"}) + if err != nil { + c <- false + return + } + fmt.Println("\tClient: Connecting to server") + conn, err := ss2.DialI2P(ss.Addr()) + if err != nil { + c <- false + return + } + fmt.Println("\tClient: Connected to tunnel") + defer conn.Close() + _, err = conn.Write([]byte("Hello world <3 <3 <3 <3 <3 <3")) + if err != nil { + c <- false + return + } + c <- true + }(c, w) + l, err := ss.Listen() + if err != nil { + fmt.Println("ss.Listen(): " + err.Error()) + t.Fail() + w <- false + return + } + defer l.Close() + w <- true + fmt.Println("\tServer: Accept()ing on tunnel") + conn, err := l.Accept() + if err != nil { + t.Fail() + fmt.Println("Failed to Accept(): " + err.Error()) + return + } + defer conn.Close() + buf := make([]byte, 512) + 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 idk.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()) + return + } + defer sam.Close() + keys, err := sam.NewKeys() + if err != nil { + fmt.Println(err.Error()) + return + } + // See the example Option_* variables. + ss, err := sam.NewStreamSession("stream_example", keys, Options_Small) + if err != nil { + fmt.Println(err.Error()) + return + } + someone, err := sam.Lookup("idk.i2p") + if err != nil { + fmt.Println(err.Error()) + return + } + + conn, err := ss.DialI2P(someone) + if err != nil { + fmt.Println(err.Error()) + return + } + defer conn.Close() + fmt.Println("Sending HTTP GET /") + if _, err := conn.Write([]byte("GET /\n")); err != nil { + fmt.Println(err.Error()) + return + } + buf := make([]byte, 4096) + n, err := conn.Read(buf) + if !strings.Contains(strings.ToLower(string(buf[:n])), "http") && !strings.Contains(strings.ToLower(string(buf[:n])), "html") { + fmt.Printf("Probably failed to StreamSession.DialI2P(idk.i2p)? It replied %d bytes, but nothing that looked like http/html", n) + log.Printf("Probably failed to StreamSession.DialI2P(idk.i2p)? It replied %d bytes, but nothing that looked like http/html", n) + } else { + fmt.Println("Read HTTP/HTML from idk.i2p") + log.Println("Read HTTP/HTML from idk.i2p") + } + return + + // Output: + // Sending HTTP GET / + // Read HTTP/HTML from idk.i2p +} + +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) + if err != nil { + fmt.Println(err.Error()) + return + } + defer sam.Close() + keys, err := sam.NewKeys() + if err != nil { + fmt.Println(err.Error()) + return + } + + quit := make(chan bool) + + // Client connecting to the server + go func(server i2pkeys.I2PAddr) { + csam, err := NewSAM(samBridge) + if err != nil { + fmt.Println(err.Error()) + return + } + defer csam.Close() + keys, err := csam.NewKeys() + if err != nil { + fmt.Println(err.Error()) + return + } + cs, err := csam.NewStreamSession("client_example", keys, Options_Small) + if err != nil { + fmt.Println(err.Error()) + quit <- false + return + } + conn, err := cs.DialI2P(server) + if err != nil { + fmt.Println(err.Error()) + quit <- false + return + } + buf := make([]byte, 256) + n, err := conn.Read(buf) + if err != nil { + fmt.Println(err.Error()) + quit <- false + return + } + fmt.Println(string(buf[:n])) + quit <- true + }(keys.Addr()) // end of client + + ss, err := sam.NewStreamSession("server_example", keys, Options_Small) + if err != nil { + fmt.Println(err.Error()) + return + } + l, err := ss.Listen() + if err != nil { + fmt.Println(err.Error()) + return + } + conn, err := l.Accept() + if err != nil { + fmt.Println(err.Error()) + return + } + conn.Write([]byte("Hello world!")) + + <-quit // waits for client to die, for example only + + // Output: + // Hello world! +} diff --git a/suggestedOptions.go b/suggestedOptions.go new file mode 100644 index 0000000..a55ba49 --- /dev/null +++ b/suggestedOptions.go @@ -0,0 +1,120 @@ +package sam3 + +import ( + "net" + "os" + "strings" + + "github.com/sirupsen/logrus" +) + +// Examples and suggestions for options when creating sessions. +var ( + // 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. + Options_Large = []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", + } + + // Suitable for shuffling a lot of traffic quickly with minimum + // anonymity. Uses 1 hop and multiple tunnels. + Options_Wide = []string{ + "inbound.length=1", "outbound.length=1", + "inbound.lengthVariance=1", "outbound.lengthVariance=1", + "inbound.backupQuantity=2", "outbound.backupQuantity=2", + "inbound.quantity=3", "outbound.quantity=3", + } + + // 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", + } + + // Sensible defaults for most people + Options_Default = []string{ + "inbound.length=3", "outbound.length=3", + "inbound.lengthVariance=0", "outbound.lengthVariance=0", + "inbound.backupQuantity=1", "outbound.backupQuantity=1", + "inbound.quantity=1", "outbound.quantity=1", + } + + // Suitable only for small dataflows, and very short lasting connections: + // 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", + } + + // 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", + } +) + +func getEnv(key, fallback string) string { + InitializeSAM3Logger() + value, ok := os.LookupEnv(key) + if !ok { + log.WithFields(logrus.Fields{ + "key": key, + "fallback": fallback, + }).Debug("Environment variable not set, using fallback") + return fallback + } + log.WithFields(logrus.Fields{ + "key": key, + "value": value, + }).Debug("Retrieved environment variable") + return value +} + +var ( + SAM_HOST = getEnv("sam_host", "127.0.0.1") + SAM_PORT = getEnv("sam_port", "7656") +) + +func SAMDefaultAddr(fallforward string) string { + if fallforward == "" { + addr := net.JoinHostPort(SAM_HOST, SAM_PORT) + log.WithField("addr", addr).Debug("Using default SAM address") + return addr + } + log.WithField("addr", fallforward).Debug("Using fallforward SAM address") + return fallforward +} + +func GenerateOptionString(opts []string) string { + optStr := strings.Join(opts, " ") + log.WithField("options", optStr).Debug("Generating option string") + if strings.Contains(optStr, "i2cp.leaseSetEncType") { + log.Debug("i2cp.leaseSetEncType already present in options") + return optStr + } + finalOpts := optStr + " i2cp.leaseSetEncType=4,0" + log.WithField("finalOptions", finalOpts).Debug("Added default i2cp.leaseSetEncType to options") + return finalOpts + // return optStr + " i2cp.leaseSetEncType=4,0" +}