commit 5a1b4e14d94baf127ab65554a58e1f51e8aacfd2 Author: eyedeekay Date: Mon Feb 17 21:46:19 2025 -0500 part out sam3 diff --git a/README.md b/README.md new file mode 100644 index 00000000..5761ebc9 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +go-sam-go +========= + +Yet another pure-Go SAMv3.3 library. +Matches the API of `go-i2p/sam3` exactly but is a ground-up rewrite. diff --git a/common/SAM.go b/common/SAM.go new file mode 100644 index 00000000..1107a777 --- /dev/null +++ b/common/SAM.go @@ -0,0 +1,273 @@ +package common + +import ( + "bufio" + "bytes" + "fmt" + "io" + "net" + "os" + "strings" + + "github.com/go-i2p/i2pkeys" + "github.com/sirupsen/logrus" +) + +// Creates a new controller for the I2P routers SAM bridge. +func OldNewSAM(address string) (*SAM, error) { + log.WithField("address", address).Debug("Creating new SAM instance") + var s SAM + // TODO: clean this up by refactoring the connection setup and error handling logic + conn, err := net.Dial("tcp", address) + if err != nil { + log.WithError(err).Error("Failed to dial SAM address") + return nil, fmt.Errorf("error dialing to address '%s': %w", address, err) + } + if _, err := conn.Write(s.SAMEmit.HelloBytes()); err != nil { + log.WithError(err).Error("Failed to write hello message") + conn.Close() + return nil, fmt.Errorf("error writing to address '%s': %w", address, err) + } + buf := make([]byte, 256) + n, err := conn.Read(buf) + if err != nil { + log.WithError(err).Error("Failed to read SAM response") + conn.Close() + return nil, fmt.Errorf("error reading onto buffer: %w", err) + } + if strings.Contains(string(buf[:n]), HELLO_REPLY_OK) { + log.Debug("SAM hello successful") + s.SAMEmit.I2PConfig.SetSAMAddress(address) + s.Conn = conn + s.SAMResolver, err = NewSAMResolver(&s) + if err != nil { + log.WithError(err).Error("Failed to create SAM resolver") + return nil, fmt.Errorf("error creating resolver: %w", err) + } + return &s, nil + } else if string(buf[:n]) == HELLO_REPLY_NOVERSION { + log.Error("SAM bridge does not support SAMv3") + conn.Close() + return nil, fmt.Errorf("That SAM bridge does not support SAMv3.") + } else { + log.WithField("response", string(buf[:n])).Error("Unexpected SAM response") + conn.Close() + return nil, fmt.Errorf("%s", string(buf[:n])) + } +} + +func (sam *SAM) Keys() (k *i2pkeys.I2PKeys) { + //TODO: copy them? + log.Debug("Retrieving SAM keys") + k = sam.SAMEmit.I2PConfig.DestinationKeys + return +} + +// read public/private keys from an io.Reader +func (sam *SAM) ReadKeys(r io.Reader) (err error) { + log.Debug("Reading keys from io.Reader") + var keys i2pkeys.I2PKeys + keys, err = i2pkeys.LoadKeysIncompat(r) + if err == nil { + log.Debug("Keys loaded successfully") + sam.SAMEmit.I2PConfig.DestinationKeys = &keys + } + log.WithError(err).Error("Failed to load keys") + return +} + +// if keyfile fname does not exist +func (sam *SAM) EnsureKeyfile(fname string) (keys i2pkeys.I2PKeys, err error) { + log.WithError(err).Error("Failed to load keys") + if fname == "" { + // transient + keys, err = sam.NewKeys() + if err == nil { + sam.SAMEmit.I2PConfig.DestinationKeys = &keys + log.WithFields(logrus.Fields{ + "keys": keys, + }).Debug("Generated new transient keys") + } + } else { + // persistent + _, err = os.Stat(fname) + if os.IsNotExist(err) { + // make the keys + keys, err = sam.NewKeys() + if err == nil { + sam.SAMEmit.I2PConfig.DestinationKeys = &keys + // save keys + var f io.WriteCloser + f, err = os.OpenFile(fname, os.O_WRONLY|os.O_CREATE, 0600) + if err == nil { + err = i2pkeys.StoreKeysIncompat(keys, f) + f.Close() + log.Debug("Generated and saved new keys") + } + } + } else if err == nil { + // we haz key file + var f *os.File + f, err = os.Open(fname) + if err == nil { + keys, err = i2pkeys.LoadKeysIncompat(f) + if err == nil { + sam.SAMEmit.I2PConfig.DestinationKeys = &keys + log.Debug("Loaded existing keys from file") + } + } + } + } + if err != nil { + log.WithError(err).Error("Failed to ensure keyfile") + } + 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(sigType ...string) (i2pkeys.I2PKeys, error) { + log.WithField("sigType", sigType).Debug("Generating new keys") + sigtmp := "" + if len(sigType) > 0 { + sigtmp = sigType[0] + } + if _, err := sam.Conn.Write([]byte("DEST GENERATE " + sigtmp + "\n")); err != nil { + log.WithError(err).Error("Failed to write DEST GENERATE command") + return i2pkeys.I2PKeys{}, fmt.Errorf("error with writing in SAM: %w", err) + } + buf := make([]byte, 8192) + n, err := sam.Conn.Read(buf) + if err != nil { + log.WithError(err).Error("Failed to read SAM response for key generation") + return i2pkeys.I2PKeys{}, fmt.Errorf("error with reading in SAM: %w", 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 { + log.Error("Failed to parse keys from SAM response") + return i2pkeys.I2PKeys{}, fmt.Errorf("Failed to parse keys.") + } + } + log.Debug("Successfully generated new keys") + return i2pkeys.NewKeys(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) (i2pkeys.I2PAddr, error) { + log.WithField("name", name).Debug("Looking up address") + return sam.SAMResolver.Resolve(name) +} + +// Creates a new session with the style of either "STREAM", "DATAGRAM" or "RAW", +// for a new I2P tunnel with name id, using the cypher keys specified, with the +// I2CP/streaminglib-options as specified. Extra arguments can be specified by +// setting extra to something else than []string{}. +// This sam3 instance is now a session +func (sam *SAM) NewGenericSession(style, id string, keys i2pkeys.I2PKeys, extras []string) (net.Conn, error) { + log.WithFields(logrus.Fields{"style": style, "id": id}).Debug("Creating new generic session") + return sam.NewGenericSessionWithSignature(style, id, keys, SIG_EdDSA_SHA512_Ed25519, extras) +} + +func (sam *SAM) NewGenericSessionWithSignature(style, id string, keys i2pkeys.I2PKeys, sigType string, extras []string) (net.Conn, error) { + log.WithFields(logrus.Fields{"style": style, "id": id, "sigType": sigType}).Debug("Creating new generic session with signature") + return sam.NewGenericSessionWithSignatureAndPorts(style, id, "0", "0", keys, sigType, extras) +} + +// Creates a new session with the style of either "STREAM", "DATAGRAM" or "RAW", +// for a new I2P tunnel with name id, using the cypher keys specified, with the +// I2CP/streaminglib-options as specified. Extra arguments can be specified by +// setting extra to something else than []string{}. +// This sam3 instance is now a session +func (sam *SAM) NewGenericSessionWithSignatureAndPorts(style, id, from, to string, keys i2pkeys.I2PKeys, sigType string, extras []string) (net.Conn, error) { + log.WithFields(logrus.Fields{"style": style, "id": id, "from": from, "to": to, "sigType": sigType}).Debug("Creating new generic session with signature and ports") + + optStr := sam.SamOptionsString() + extraStr := strings.Join(extras, " ") + + conn := sam.Conn + fp := "" + tp := "" + if from != "0" { + fp = " FROM_PORT=" + from + } + if to != "0" { + tp = " TO_PORT=" + to + } + scmsg := []byte("SESSION CREATE STYLE=" + style + fp + tp + " ID=" + id + " DESTINATION=" + keys.String() + " " + optStr + extraStr + "\n") + + log.WithField("message", string(scmsg)).Debug("Sending SESSION CREATE message") + + for m, i := 0, 0; m != len(scmsg); i++ { + if i == 15 { + log.Error("Failed to write SESSION CREATE message after 15 attempts") + conn.Close() + return nil, fmt.Errorf("writing to SAM failed") + } + n, err := conn.Write(scmsg[m:]) + if err != nil { + log.WithError(err).Error("Failed to write to SAM connection") + conn.Close() + return nil, fmt.Errorf("writing to connection failed: %w", err) + } + m += n + } + buf := make([]byte, 4096) + n, err := conn.Read(buf) + if err != nil { + log.WithError(err).Error("Failed to read SAM response") + conn.Close() + return nil, fmt.Errorf("reading from connection failed: %w", err) + } + text := string(buf[:n]) + log.WithField("response", text).Debug("Received SAM response") + if strings.HasPrefix(text, SESSION_OK) { + if keys.String() != text[len(SESSION_OK):len(text)-1] { + log.Error("SAM created a tunnel with different keys than requested") + conn.Close() + return nil, fmt.Errorf("SAMv3 created a tunnel with keys other than the ones we asked it for") + } + log.Debug("Successfully created new session") + return conn, nil //&StreamSession{id, conn, keys, nil, sync.RWMutex{}, nil}, nil + } else if text == SESSION_DUPLICATE_ID { + log.Error("Duplicate tunnel name") + conn.Close() + return nil, fmt.Errorf("Duplicate tunnel name") + } else if text == SESSION_DUPLICATE_DEST { + log.Error("Duplicate destination") + conn.Close() + return nil, fmt.Errorf("Duplicate destination") + } else if text == SESSION_INVALID_KEY { + log.Error("Invalid key for SAM session") + conn.Close() + return nil, fmt.Errorf("Invalid key - SAM session") + } else if strings.HasPrefix(text, SESSION_I2P_ERROR) { + log.WithField("error", text[len(SESSION_I2P_ERROR):]).Error("I2P error") + conn.Close() + return nil, fmt.Errorf("I2P error " + text[len(SESSION_I2P_ERROR):]) + } else { + log.WithField("reply", text).Error("Unable to parse SAMv3 reply") + conn.Close() + return nil, fmt.Errorf("Unable to parse SAMv3 reply: " + text) + } +} + +// close this sam session +func (sam *SAM) Close() error { + log.Debug("Closing SAM session") + return sam.Conn.Close() +} diff --git a/common/config.go b/common/config.go new file mode 100644 index 00000000..ea352ac2 --- /dev/null +++ b/common/config.go @@ -0,0 +1,501 @@ +package common + +import ( + "fmt" + "math/rand" + "net" + "strconv" + "strings" + "time" + + "github.com/sirupsen/logrus" +) + +// Sam returns the SAM bridge address as a string in the format "host:port" +func (f *I2PConfig) Sam() string { + // Set default values + host := "127.0.0.1" + port := 7656 + + // Override defaults if config values are set + if f.SamHost != "" { + host = f.SamHost + } + if f.SamPort != 0 { + port = f.SamPort + } + + // Log the SAM address being constructed + log.WithFields(logrus.Fields{ + "host": host, + "port": port, + }).Debug("SAM address constructed") + + // Return formatted SAM address + return net.JoinHostPort(host, strconv.Itoa(port)) +} + +// SetSAMAddress sets the SAM bridge host and port from a combined address string. +// If no address is provided, it sets default values for the host and port. +func (f *I2PConfig) SetSAMAddress(addr string) { + // Set default values + f.SamHost = "127.0.0.1" + f.SamPort = 7656 + + // Split address into host and port components + host, port, err := net.SplitHostPort(addr) + if err != nil { + // If error occurs, assume only host is provided + f.SamHost = addr + } else { + f.SamHost = host + f.SamPort, _ = strconv.Atoi(port) + } + + // Log the configured SAM address + log.WithFields(logrus.Fields{ + "host": f.SamHost, + "port": f.SamPort, + }).Debug("SAM address set") +} + +// ID returns the tunnel name as a formatted string. If no tunnel name is set, +// generates a random 12-character name using lowercase letters. +func (f *I2PConfig) ID() string { + generator := rand.New(rand.NewSource(time.Now().UnixNano())) + // If no tunnel name set, generate random one + if f.TunName == "" { + // Generate 12 random lowercase letters + b := make([]byte, 12) + for i := range b { + b[i] = "abcdefghijklmnopqrstuvwxyz"[generator.Intn(26)] + } + f.TunName = string(b) + + // Log the generated name + log.WithField("TunName", f.TunName).Debug("Generated random tunnel name") + } + + // Return formatted ID string + return fmt.Sprintf("ID=%s", f.TunName) +} + +// Leasesetsettings returns the lease set configuration strings for I2P +// Returns three strings: lease set key, private key, and private signing key settings +func (f *I2PConfig) LeaseSetSettings() (string, string, string) { + // Initialize empty strings for each setting + var leaseSetKey, privateKey, privateSigningKey string + + // Set lease set key if configured + if f.LeaseSetKey != "" { + leaseSetKey = fmt.Sprintf(" i2cp.leaseSetKey=%s ", f.LeaseSetKey) + } + + // Set lease set private key if configured + if f.LeaseSetPrivateKey != "" { + privateKey = fmt.Sprintf(" i2cp.leaseSetPrivateKey=%s ", f.LeaseSetPrivateKey) + } + + // Set lease set private signing key if configured + if f.LeaseSetPrivateSigningKey != "" { + privateSigningKey = fmt.Sprintf(" i2cp.leaseSetPrivateSigningKey=%s ", f.LeaseSetPrivateSigningKey) + } + + // Log the constructed settings + log.WithFields(logrus.Fields{ + "leaseSetKey": leaseSetKey, + "leaseSetPrivateKey": privateKey, + "leaseSetPrivateSigningKey": privateSigningKey, + }).Debug("Lease set settings constructed") + + return leaseSetKey, privateKey, privateSigningKey +} + +// FromPort returns the FROM_PORT configuration string for SAM bridges >= 3.1 +// Returns an empty string if SAM version < 3.1 or if fromport is "0" +func (f *I2PConfig) FromPort() string { + // Check SAM version compatibility + if f.SamMax == "" || f.samMax() < 3.1 { + log.Debug("SAM version < 3.1, FromPort not applicable") + return "" + } + + // Return formatted FROM_PORT if fromport is set + if f.Fromport != "0" { + log.WithField("fromPort", f.Fromport).Debug("FromPort set") + return fmt.Sprintf(" FROM_PORT=%s ", f.Fromport) + } + + log.Debug("FromPort not set") + return "" +} + +// ToPort returns the TO_PORT configuration string for SAM bridges >= 3.1 +// Returns an empty string if SAM version < 3.1 or if toport is "0" +func (f *I2PConfig) ToPort() string { + // Check SAM version compatibility + if f.samMax() < 3.1 { + log.Debug("SAM version < 3.1, ToPort not applicable") + return "" + } + + // Return formatted TO_PORT if toport is set + if f.Toport != "0" { + log.WithField("toPort", f.Toport).Debug("ToPort set") + return fmt.Sprintf(" TO_PORT=%s ", f.Toport) + } + + log.Debug("ToPort not set") + return "" +} + +// SessionStyle returns the SAM session style configuration string +// If no style is set, defaults to "STREAM" +func (f *I2PConfig) SessionStyle() string { + if f.Style != "" { + // Log custom style setting + log.WithField("style", f.Style).Debug("Session style set") + return fmt.Sprintf(" STYLE=%s ", f.Style) + } + + // Log default style + log.Debug("Using default STREAM style") + return " STYLE=STREAM " +} + +// samMax returns the maximum SAM version supported as a float64 +// If parsing fails, returns default value 3.1 +func (f *I2PConfig) samMax() float64 { + // Parse SAM max version to integer + i, err := strconv.ParseFloat(f.SamMax, 64) + if err != nil { + log.WithError(err).Warn("Failed to parse SamMax, using default 3.1") + return 3.1 + } + + // Log the parsed version and return + log.WithField("samMax", i).Debug("SAM max version parsed") + return i +} + +// MinSAM returns the minimum SAM version supported as a string +// If no minimum version is set, returns default value "3.0" +func (f *I2PConfig) MinSAM() string { + if f.SamMin == "" { + log.Debug("Using default MinSAM: 3.0") + return "3.0" + } + log.WithField("minSAM", f.SamMin).Debug("MinSAM set") + return f.SamMin +} + +// MaxSAM returns the maximum SAM version supported as a string +// If no maximum version is set, returns default value "3.1" +func (f *I2PConfig) MaxSAM() string { + if f.SamMax == "" { + log.Debug("Using default MaxSAM: 3.1") + return "3.1" + } + log.WithField("maxSAM", f.SamMax).Debug("MaxSAM set") + return f.SamMax +} + +// DestinationKey returns the DESTINATION configuration string for the SAM bridge +// If destination keys are set, returns them as a string, otherwise returns "TRANSIENT" +func (f *I2PConfig) DestinationKey() string { + // Check if destination keys are set + if f.DestinationKeys != nil { + // Log the destination key being used + log.WithField("destinationKey", f.DestinationKeys.String()).Debug("Destination key set") + return fmt.Sprintf(" DESTINATION=%s ", f.DestinationKeys.String()) + } + + // Log and return transient destination + log.Debug("Using TRANSIENT destination") + return " DESTINATION=TRANSIENT " +} + +// SignatureType returns the SIGNATURE_TYPE configuration string for SAM bridges >= 3.1 +// Returns empty string if SAM version < 3.1 or if no signature type is set +func (f *I2PConfig) SignatureType() string { + // Check SAM version compatibility + if f.samMax() < 3.1 { + log.Debug("SAM version < 3.1, SignatureType not applicable") + return "" + } + + // Return formatted signature type if set + if f.SigType != "" { + log.WithField("sigType", f.SigType).Debug("Signature type set") + return fmt.Sprintf(" SIGNATURE_TYPE=%s ", f.SigType) + } + + log.Debug("Signature type not set") + return "" +} + +// EncryptLease returns the lease set encryption configuration string +// Returns "i2cp.encryptLeaseSet=true" if encryption is enabled, empty string otherwise +func (f *I2PConfig) EncryptLease() string { + if f.EncryptLeaseSet == true { + log.Debug("Lease set encryption enabled") + return fmt.Sprintf(" i2cp.encryptLeaseSet=true ") + } + log.Debug("Lease set encryption not enabled") + return "" +} + +// Reliability returns the message reliability configuration string for the SAM bridge +// If a reliability setting is specified, returns formatted i2cp.messageReliability setting +func (f *I2PConfig) Reliability() string { + if f.MessageReliability != "" { + // Log the reliability setting being used + log.WithField("reliability", f.MessageReliability).Debug("Message reliability set") + return fmt.Sprintf(" i2cp.messageReliability=%s ", f.MessageReliability) + } + + // Log when reliability is not set + log.Debug("Message reliability not set") + return "" +} + +// Reduce returns I2CP reduce-on-idle configuration settings as a string if enabled +func (f *I2PConfig) Reduce() string { + // If reduce idle is enabled, return formatted configuration string + if f.ReduceIdle == true { + // Log the reduce idle settings being applied + log.WithFields(logrus.Fields{ + "reduceIdle": f.ReduceIdle, + "reduceIdleTime": f.ReduceIdleTime, + "reduceIdleQuantity": f.ReduceIdleQuantity, + }).Debug("Reduce idle settings applied") + + // Return formatted configuration string using Sprintf + return fmt.Sprintf("i2cp.reduceOnIdle=%t"+ + "i2cp.reduceIdleTime=%d"+ + "i2cp.reduceQuantity=%d", + f.ReduceIdle, + f.ReduceIdleTime, + f.ReduceIdleQuantity) + } + + // Log when reduce idle is not enabled + log.Debug("Reduce idle settings not applied") + return "" +} + +// Close returns I2CP close-on-idle configuration settings as a string if enabled +func (f *I2PConfig) Close() string { + // If close idle is enabled, return formatted configuration string + if f.CloseIdle == true { + // Log the close idle settings being applied + log.WithFields(logrus.Fields{ + "closeIdle": f.CloseIdle, + "closeIdleTime": f.CloseIdleTime, + }).Debug("Close idle settings applied") + + // Return formatted configuration string using Sprintf + return fmt.Sprintf("i2cp.closeOnIdle=%t"+ + "i2cp.closeIdleTime=%d", + f.CloseIdle, + f.CloseIdleTime) + } + + // Log when close idle is not enabled + log.Debug("Close idle settings not applied") + return "" +} + +// DoZero returns the zero hop and fast receive configuration string settings +func (f *I2PConfig) DoZero() string { + // Build settings using slices for cleaner concatenation + var settings []string + + // Add inbound zero hop setting if enabled + if f.InAllowZeroHop == true { + settings = append(settings, fmt.Sprintf("inbound.allowZeroHop=%t", f.InAllowZeroHop)) + } + + // Add outbound zero hop setting if enabled + if f.OutAllowZeroHop == true { + settings = append(settings, fmt.Sprintf("outbound.allowZeroHop=%t", f.OutAllowZeroHop)) + } + + // Add fast receive setting if enabled + if f.FastRecieve == true { + settings = append(settings, fmt.Sprintf("i2cp.fastRecieve=%t", f.FastRecieve)) + } + + // Join all settings with spaces + result := strings.Join(settings, " ") + + // Log the final settings + log.WithField("zeroHopSettings", result).Debug("Zero hop settings applied") + + return result +} + +func (f *I2PConfig) InboundLength() string { + return fmt.Sprintf("inbound.length=%d", f.InLength) +} + +func (f *I2PConfig) OutboundLength() string { + return fmt.Sprintf("outbound.length=%d", f.OutLength) +} + +func (f *I2PConfig) InboundLengthVariance() string { + return fmt.Sprintf("inbound.lengthVariance=%d", f.InVariance) +} + +func (f *I2PConfig) OutboundLengthVariance() string { + return fmt.Sprintf("outbound.lengthVariance=%d", f.OutVariance) +} + +func (f *I2PConfig) InboundBackupQuantity() string { + return fmt.Sprintf("inbound.backupQuantity=%d", f.InBackupQuantity) +} + +func (f *I2PConfig) OutboundBackupQuantity() string { + return fmt.Sprintf("outbound.backupQuantity=%d", f.OutBackupQuantity) +} + +func (f *I2PConfig) InboundQuantity() string { + return fmt.Sprintf("inbound.quantity=%d", f.InQuantity) +} + +func (f *I2PConfig) OutboundQuantity() string { + return fmt.Sprintf("outbound.quantity=%d", f.OutQuantity) +} + +func (f *I2PConfig) UsingCompression() string { + return fmt.Sprintf("i2cp.gzip=%t", f.UseCompression) +} + +// Print returns a slice of strings containing all the I2P configuration settings +func (f *I2PConfig) Print() []string { + // Get lease set settings + lsk, lspk, lspsk := f.LeaseSetSettings() + + // Build the configuration settings slice + settings := []string{ + f.InboundLength(), + f.OutboundLength(), + f.InboundLengthVariance(), + f.OutboundLengthVariance(), + f.InboundBackupQuantity(), + f.OutboundBackupQuantity(), + f.InboundQuantity(), + f.OutboundQuantity(), + f.UsingCompression(), + f.DoZero(), // Zero hop settings + f.Reduce(), // Reduce idle settings + f.Close(), // Close idle settings + f.Reliability(), // Message reliability + f.EncryptLease(), // Lease encryption + lsk, lspk, lspsk, // Lease set keys + f.Accesslisttype(), // Access list type + f.Accesslist(), // Access list + f.LeaseSetEncryptionType(), // Lease set encryption type + } + + return settings +} + +// Accesslisttype returns the I2CP access list configuration string based on the AccessListType setting +func (f *I2PConfig) Accesslisttype() string { + switch f.AccessListType { + case "whitelist": + log.Debug("Access list type set to whitelist") + return fmt.Sprintf("i2cp.enableAccessList=true") + case "blacklist": + log.Debug("Access list type set to blacklist") + return fmt.Sprintf("i2cp.enableBlackList=true") + case "none": + log.Debug("Access list type set to none") + return "" + default: + log.Debug("Access list type not set") + return "" + } +} + +// Accesslist generates the I2CP access list configuration string based on the configured access list +func (f *I2PConfig) Accesslist() string { + // Only proceed if access list type and values are set + if f.AccessListType != "" && len(f.AccessList) > 0 { + // Join access list entries with commas + accessList := strings.Join(f.AccessList, ",") + + // Log the generated access list + log.WithField("accessList", accessList).Debug("Access list generated") + + // Return formatted access list configuration + return fmt.Sprintf("i2cp.accessList=%s", accessList) + } + + // Log when access list is not set + log.Debug("Access list not set") + return "" +} + +// LeaseSetEncryptionType returns the I2CP lease set encryption type configuration string. +// If no encryption type is set, returns default value "4,0". +// Validates that all encryption types are valid integers. +func (f *I2PConfig) LeaseSetEncryptionType() string { + // Use default encryption type if none specified + if f.LeaseSetEncryption == "" { + log.Debug("Using default lease set encryption type: 4,0") + return "i2cp.leaseSetEncType=4,0" + } + + // Validate each encryption type is a valid integer + for _, s := range strings.Split(f.LeaseSetEncryption, ",") { + if _, err := strconv.Atoi(s); err != nil { + log.WithField("invalidType", s).Panic("Invalid encrypted leaseSet type") + //panic("Invalid encrypted leaseSet type: " + s) + } + } + + // Log and return the configured encryption type + log.WithField("leaseSetEncType", f.LeaseSetEncryption).Debug("Lease set encryption type set") + return fmt.Sprintf("i2cp.leaseSetEncType=%s", f.LeaseSetEncryption) +} + +func NewConfig(opts ...func(*I2PConfig) error) (*I2PConfig, error) { + var config I2PConfig + config.SamHost = "127.0.0.1" + config.SamPort = 7656 + config.SamMin = DEFAULT_SAM_MIN + config.SamMax = DEFAULT_SAM_MAX + 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 _, opt := range opts { + if err := opt(&config); err != nil { + return nil, err + } + } + return &config, nil +} diff --git a/common/const.go b/common/const.go new file mode 100644 index 00000000..bc7eeb32 --- /dev/null +++ b/common/const.go @@ -0,0 +1,32 @@ +package common + +const DEFAULT_SAM_MIN = "3.1" +const DEFAULT_SAM_MAX = "3.3" + +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" +) + +const ( + SAM_RESULT_OK = "RESULT=OK" + SAM_RESULT_INVALID_KEY = "RESULT=INVALID_KEY" + SAM_RESULT_KEY_NOT_FOUND = "RESULT=KEY_NOT_FOUND" +) + +const ( + HELLO_REPLY_OK = "HELLO REPLY RESULT=OK" + HELLO_REPLY_NOVERSION = "HELLO REPLY RESULT=NOVERSION\n" +) diff --git a/common/emit-options.go b/common/emit-options.go new file mode 100644 index 00000000..973ad066 --- /dev/null +++ b/common/emit-options.go @@ -0,0 +1,436 @@ +package common + +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", sp) + } + if len(sp) == 2 { + var err error + c.I2PConfig.SamPort, err = strconv.Atoi(sp[1]) + if err != nil { + log.WithField("port", sp[1]).Error("Invalid SAM port") + return fmt.Errorf("Invalid SAM Port %s; non-number", sp[1]) + } + } + 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 port") + } +} + +// 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") + } +} + +// 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") + } +} + +// 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 length") + } +} + +// 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") + } +} + +// 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") + } +} + +// 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") + } +} + +// 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") + } +} + +// 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") + } +} + +// 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 = b + return nil + } + c.I2PConfig.EncryptLeaseSet = b + 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 tunnel quantity") + } +} + +// 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/common/emitter.go b/common/emitter.go new file mode 100644 index 00000000..0265019b --- /dev/null +++ b/common/emitter.go @@ -0,0 +1,106 @@ +package common + +import ( + "fmt" + "strings" +) + +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 +} diff --git a/common/log.go b/common/log.go new file mode 100644 index 00000000..a08b612c --- /dev/null +++ b/common/log.go @@ -0,0 +1,10 @@ +package common + +import logger "github.com/go-i2p/go-sam-go/log" + +var log = logger.GetSAM3Logger() + +func init() { + logger.InitializeSAM3Logger() + log = logger.GetSAM3Logger() +} diff --git a/common/resolver.go b/common/resolver.go new file mode 100644 index 00000000..7654952a --- /dev/null +++ b/common/resolver.go @@ -0,0 +1,82 @@ +package common + +import ( + "bufio" + "bytes" + "errors" + "strings" + + "github.com/go-i2p/i2pkeys" +) + +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 +} + +// Performs a lookup, probably this order: 1) routers known addresses, cached +// addresses, 3) by asking peers in the I2P network. +func (sam *SAMResolver) Resolve(name string) (i2pkeys.I2PAddr, error) { + log.WithField("name", name).Debug("Resolving name") + + if _, err := sam.Conn.Write([]byte("NAMING LOOKUP NAME=" + name + "\r\n")); err != nil { + log.WithError(err).Error("Failed to write to SAM connection") + sam.Close() + return i2pkeys.I2PAddr(""), err + } + buf := make([]byte, 4096) + n, err := sam.Conn.Read(buf) + if err != nil { + log.WithError(err).Error("Failed to read from SAM connection") + sam.Close() + return i2pkeys.I2PAddr(""), err + } + if n <= 13 || !strings.HasPrefix(string(buf[:n]), "NAMING REPLY ") { + log.Error("Failed to parse SAM response") + return i2pkeys.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() + log.WithField("text", text).Debug("Parsing SAM response token") + //log.Println("SAM3", text) + if text == SAM_RESULT_OK { + continue + } else if text == SAM_RESULT_INVALID_KEY { + errStr += "Invalid key - resolver." + log.Error("Invalid key in resolver") + } else if text == SAM_RESULT_KEY_NOT_FOUND { + errStr += "Unable to resolve " + name + log.WithField("name", name).Error("Unable to resolve name") + } else if text == "NAME="+name { + continue + } else if strings.HasPrefix(text, "VALUE=") { + addr := i2pkeys.I2PAddr(text[6:]) + log.WithField("addr", addr).Debug("Name resolved successfully") + return i2pkeys.I2PAddr(text[6:]), nil + } else if strings.HasPrefix(text, "MESSAGE=") { + errStr += " " + text[8:] + log.WithField("message", text[8:]).Warn("Received message from SAM") + } else { + continue + } + } + return i2pkeys.I2PAddr(""), errors.New(errStr) +} diff --git a/common/sam3.go b/common/sam3.go new file mode 100644 index 00000000..2b2c09f8 --- /dev/null +++ b/common/sam3.go @@ -0,0 +1,69 @@ +package common + +import ( + "fmt" + "net" + "strings" +) + +func NewSAM(address string) (*SAM, error) { + logger := log.WithField("address", address) + logger.Debug("Creating new SAM instance") + + conn, err := connectToSAM(address) + if err != nil { + return nil, err + } + defer func() { + if err != nil { + conn.Close() + } + }() + + s := &SAM{ + Conn: conn, + } + + if err = sendHelloAndValidate(conn, s); err != nil { + return nil, err + } + + s.SAMEmit.I2PConfig.SetSAMAddress(address) + + if s.SAMResolver, err = NewSAMResolver(s); err != nil { + return nil, fmt.Errorf("failed to create SAM resolver: %w", err) + } + + return s, nil +} + +func connectToSAM(address string) (net.Conn, error) { + conn, err := net.Dial("tcp", address) + if err != nil { + return nil, fmt.Errorf("failed to connect to SAM bridge at %s: %w", address, err) + } + return conn, nil +} + +func sendHelloAndValidate(conn net.Conn, s *SAM) error { + if _, err := conn.Write(s.SAMEmit.HelloBytes()); err != nil { + return fmt.Errorf("failed to send hello message: %w", err) + } + + buf := make([]byte, 256) + n, err := conn.Read(buf) + if err != nil { + return fmt.Errorf("failed to read SAM response: %w", err) + } + + response := string(buf[:n]) + switch { + case strings.Contains(response, HELLO_REPLY_OK): + log.Debug("SAM hello successful") + return nil + case response == HELLO_REPLY_NOVERSION: + return fmt.Errorf("SAM bridge does not support SAMv3") + default: + return fmt.Errorf("unexpected SAM response: %s", response) + } +} diff --git a/common/types.go b/common/types.go new file mode 100644 index 00000000..b95c9eb7 --- /dev/null +++ b/common/types.go @@ -0,0 +1,82 @@ +package common + +import ( + "fmt" + "net" + + "github.com/go-i2p/i2pkeys" +) + +// I2PConfig is a struct which manages I2P configuration options. +type I2PConfig struct { + SamHost string + SamPort int + TunName string + + SamMin string + SamMax string + + Fromport string + Toport string + + Style string + TunType string + + DestinationKeys *i2pkeys.I2PKeys + + SigType string + EncryptLeaseSet bool + LeaseSetKey string + LeaseSetPrivateKey string + LeaseSetPrivateSigningKey string + LeaseSetKeys i2pkeys.I2PKeys + InAllowZeroHop bool + OutAllowZeroHop bool + InLength int + OutLength int + InQuantity int + OutQuantity int + InVariance int + OutVariance int + InBackupQuantity int + OutBackupQuantity int + FastRecieve bool + UseCompression bool + MessageReliability string + CloseIdle bool + CloseIdleTime int + ReduceIdle bool + ReduceIdleTime int + ReduceIdleQuantity int + LeaseSetEncryption string + + //Streaming Library options + AccessListType string + AccessList []string +} + +type SAMEmit struct { + I2PConfig +} + +// Used for controlling I2Ps SAMv3. +type SAM struct { + SAMEmit + *SAMResolver + net.Conn +} + +type SAMResolver struct { + *SAM +} + +// 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/common/util.go b/common/util.go new file mode 100644 index 00000000..9ce5a96e --- /dev/null +++ b/common/util.go @@ -0,0 +1,59 @@ +package common + +import ( + "math/rand" + "net" + "strconv" + "strings" + "time" + + "github.com/sirupsen/logrus" +) + +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 +} + +func RandPort() string { + for { + s := rand.NewSource(time.Now().UnixNano()) + r := rand.New(s) + p := r.Intn(55534) + 10000 + port := strconv.Itoa(p) + if l, e := net.Listen("tcp", net.JoinHostPort("localhost", port)); e != nil { + continue + } else { + defer l.Close() + if l, e := net.Listen("udp", net.JoinHostPort("localhost", port)); e != nil { + continue + } else { + defer l.Close() + return strconv.Itoa(l.Addr().(*net.UDPAddr).Port) + } + } + + } +} diff --git a/datagram/datagram.go b/datagram/datagram.go new file mode 100644 index 00000000..b1575248 --- /dev/null +++ b/datagram/datagram.go @@ -0,0 +1,78 @@ +package datagram + +import ( + "errors" + "net" + "strconv" + + "github.com/go-i2p/go-sam-go/common" + "github.com/go-i2p/i2pkeys" + "github.com/sirupsen/logrus" +) + +// 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.I2PKeys, options []string, udpPort int) (*DatagramSession, error) { + log.WithFields(logrus.Fields{ + "id": id, + "udpPort": udpPort, + }).Debug("Creating new DatagramSession") + + if udpPort > 65335 || udpPort < 0 { + log.WithField("udpPort", udpPort).Error("Invalid UDP port") + return nil, errors.New("udpPort needs to be in the intervall 0-65335") + } + if udpPort == 0 { + udpPort = 7655 + log.Debug("Using default UDP port 7655") + } + lhost, _, err := common.SplitHostPort(s.LocalAddr().String()) + if err != nil { + log.WithError(err).Error("Failed to split local host port") + s.Close() + return nil, err + } + lUDPAddr, err := net.ResolveUDPAddr("udp4", lhost+":0") + if err != nil { + log.WithError(err).Error("Failed to resolve local UDP address") + return nil, err + } + udpconn, err := net.ListenUDP("udp4", lUDPAddr) + if err != nil { + log.WithError(err).Error("Failed to listen on UDP") + return nil, err + } + rhost, _, err := common.SplitHostPort(s.RemoteAddr().String()) + if err != nil { + log.WithError(err).Error("Failed to split remote host port") + s.Close() + return nil, err + } + rUDPAddr, err := net.ResolveUDPAddr("udp4", rhost+":"+strconv.Itoa(udpPort)) + if err != nil { + log.WithError(err).Error("Failed to resolve remote UDP address") + return nil, err + } + _, lport, err := net.SplitHostPort(udpconn.LocalAddr().String()) + if err != nil { + log.WithError(err).Error("Failed to get local port") + s.Close() + return nil, err + } + conn, err := s.NewGenericSession("DATAGRAM", id, keys, []string{" PORT=" + lport}) + if err != nil { + log.WithError(err).Error("Failed to create generic session") + return nil, err + } + + log.WithField("id", id).Info("DatagramSession created successfully") + datagramSession := &DatagramSession{ + SAM: s, + UDPConn: udpconn, + SAMUDPAddress: rUDPAddr, + RemoteI2PAddr: nil, + } + datagramSession.Conn = conn + return datagramSession, nil + //return &DatagramSession{s.address, id, conn, udpconn, keys, rUDPAddr, nil}, nil +} diff --git a/datagram/log.go b/datagram/log.go new file mode 100644 index 00000000..a1ea7128 --- /dev/null +++ b/datagram/log.go @@ -0,0 +1,10 @@ +package datagram + +import logger "github.com/go-i2p/go-sam-go/log" + +var log = logger.GetSAM3Logger() + +func init() { + logger.InitializeSAM3Logger() + log = logger.GetSAM3Logger() +} diff --git a/datagram/session.go b/datagram/session.go new file mode 100644 index 00000000..a5429d05 --- /dev/null +++ b/datagram/session.go @@ -0,0 +1,209 @@ +package datagram + +import ( + "bytes" + "errors" + "net" + "time" + + "github.com/go-i2p/go-sam-go/common" + "github.com/go-i2p/i2pkeys" + "github.com/sirupsen/logrus" +) + +func (s *DatagramSession) B32() string { + b32 := s.DestinationKeys.Addr().Base32() + log.WithField("b32", b32).Debug("Generated B32 address") + return b32 +} + +func (s *DatagramSession) Dial(net string, addr string) (*DatagramSession, error) { + log.WithFields(logrus.Fields{ + "net": net, + "addr": addr, + }).Debug("Dialing address") + netaddr, err := s.Lookup(addr) + if err != nil { + log.WithError(err).Error("Lookup failed") + return nil, err + } + return s.DialI2PRemote(net, netaddr) +} + +func (s *DatagramSession) DialRemote(net, addr string) (net.PacketConn, error) { + log.WithFields(logrus.Fields{ + "net": net, + "addr": addr, + }).Debug("Dialing remote address") + netaddr, err := s.Lookup(addr) + if err != nil { + log.WithError(err).Error("Lookup failed") + return nil, err + } + return s.DialI2PRemote(net, netaddr) +} + +func (s *DatagramSession) DialI2PRemote(net string, addr net.Addr) (*DatagramSession, error) { + log.WithFields(logrus.Fields{ + "net": net, + "addr": addr, + }).Debug("Dialing I2P remote address") + switch addr.(type) { + case *i2pkeys.I2PAddr: + s.RemoteI2PAddr = addr.(*i2pkeys.I2PAddr) + case i2pkeys.I2PAddr: + i2paddr := addr.(i2pkeys.I2PAddr) + s.RemoteI2PAddr = &i2paddr + } + return s, nil +} + +func (s *DatagramSession) RemoteAddr() net.Addr { + log.WithField("remoteAddr", s.RemoteI2PAddr).Debug("Getting remote address") + return s.RemoteI2PAddr +} + +// 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. +// implements net.PacketConn +func (s *DatagramSession) ReadFrom(b []byte) (n int, addr net.Addr, err error) { + log.Debug("Reading datagram") + // extra bytes to read the remote address of incomming datagram + 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 + n, saddr, err = s.UDPConn.ReadFromUDP(buf) + if err != nil { + log.WithError(err).Error("Failed to read from UDP") + return 0, i2pkeys.I2PAddr(""), err + } + if bytes.Equal(saddr.IP, s.SAMUDPAddress.IP) { + continue + } + break + } + i := bytes.IndexByte(buf, byte('\n')) + if i > 4096 || i > n { + log.Error("Could not parse incoming message remote address") + return 0, i2pkeys.I2PAddr(""), errors.New("Could not parse incomming message remote address.") + } + raddr, err := i2pkeys.NewI2PAddrFromString(string(buf[:i])) + if err != nil { + log.WithError(err).Error("Could not parse incoming message remote address") + return 0, i2pkeys.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) { + copy(b, buf[i+1:i+1+len(b)]) + return n - (i + 1), raddr, errors.New("Datagram did not fit into your buffer.") + } else { + copy(b, buf[i+1:n]) + log.WithField("bytesRead", n-(i+1)).Debug("Datagram read successfully") + return n - (i + 1), raddr, nil + } +} + +func (s *DatagramSession) Accept() (net.Conn, error) { + log.Debug("Accept called on DatagramSession") + return s, nil +} + +func (s *DatagramSession) Read(b []byte) (n int, err error) { + log.Debug("Reading from DatagramSession") + rint, _, rerr := s.ReadFrom(b) + return rint, rerr +} + +// 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 net.Addr) (n int, err error) { + log.WithFields(logrus.Fields{ + "addr": addr, + "datagramLen": len(b), + }).Debug("Writing datagram") + header := []byte("3.1 " + s.ID() + " " + addr.String() + "\n") + msg := append(header, b...) + n, err = s.UDPConn.WriteToUDP(msg, s.SAMUDPAddress) + if err != nil { + log.WithError(err).Error("Failed to write to UDP") + } else { + log.WithField("bytesWritten", n).Debug("Datagram written successfully") + } + return n, err +} + +func (s *DatagramSession) Write(b []byte) (int, error) { + log.WithField("dataLen", len(b)).Debug("Writing to DatagramSession") + return s.WriteTo(b, s.RemoteI2PAddr) +} + +// Closes the DatagramSession. Implements net.PacketConn +func (s *DatagramSession) Close() error { + log.Debug("Closing DatagramSession") + err := s.Conn.Close() + err2 := s.UDPConn.Close() + if err != nil { + log.WithError(err).Error("Failed to close connection") + return err + } + if err2 != nil { + log.WithError(err2).Error("Failed to close UDP connection") + } + return err2 +} + +// Returns the I2P destination of the DatagramSession. +func (s *DatagramSession) LocalI2PAddr() i2pkeys.I2PAddr { + addr := s.DestinationKeys.Addr() + log.WithField("localI2PAddr", addr).Debug("Getting local I2P address") + return addr +} + +// Implements net.PacketConn +func (s *DatagramSession) LocalAddr() net.Addr { + return s.LocalI2PAddr() +} + +func (s *DatagramSession) Addr() net.Addr { + return s.LocalI2PAddr() +} + +func (s *DatagramSession) Lookup(name string) (a net.Addr, err error) { + log.WithField("name", name).Debug("Looking up address") + var sam *common.SAM + sam, err = common.NewSAM(s.Sam()) + if err == nil { + defer sam.Close() + a, err = sam.Lookup(name) + } + log.WithField("address", a).Debug("Lookup successful") + return +} + +// 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 { + log.WithField("deadline", t).Debug("Setting deadline") + return s.UDPConn.SetDeadline(t) +} + +// Sets read deadline for the DatagramSession. Implements net.PacketConn +func (s *DatagramSession) SetReadDeadline(t time.Time) error { + log.WithField("readDeadline", t).Debug("Setting read deadline") + return s.UDPConn.SetReadDeadline(t) +} + +// Sets the write deadline for the DatagramSession. Implements net.Packetconn. +func (s *DatagramSession) SetWriteDeadline(t time.Time) error { + log.WithField("writeDeadline", t).Debug("Setting write deadline") + return s.UDPConn.SetWriteDeadline(t) +} + +func (s *DatagramSession) SetWriteBuffer(bytes int) error { + log.WithField("bytes", bytes).Debug("Setting write buffer") + return s.UDPConn.SetWriteBuffer(bytes) +} diff --git a/datagram/types.go b/datagram/types.go new file mode 100644 index 00000000..43e9cc8b --- /dev/null +++ b/datagram/types.go @@ -0,0 +1,21 @@ +package datagram + +import ( + "net" + + "github.com/go-i2p/go-sam-go/common" + "github.com/go-i2p/i2pkeys" +) + +type SAM common.SAM + +// 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 { + *SAM + UDPConn *net.UDPConn // used to deliver datagrams + SAMUDPAddress *net.UDPAddr // the SAM bridge UDP-port + RemoteI2PAddr *i2pkeys.I2PAddr // optional remote I2P address +} diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..a12ab78d --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module github.com/go-i2p/go-sam-go + +go 1.23.5 + +require ( + github.com/go-i2p/i2pkeys v0.33.92 + github.com/sirupsen/logrus v1.9.3 +) + +require golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..538ff81f --- /dev/null +++ b/go.sum @@ -0,0 +1,17 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-i2p/i2pkeys v0.33.92 h1:e2vx3vf7tNesaJ8HmAlGPOcfiGM86jzeIGxh27I9J2Y= +github.com/go-i2p/i2pkeys v0.33.92/go.mod h1:BRURQ/twxV0WKjZlFSKki93ivBi+MirZPWudfwTzMpE= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/log/log.go b/log/log.go new file mode 100644 index 00000000..b1862574 --- /dev/null +++ b/log/log.go @@ -0,0 +1,51 @@ +package log + +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/const.go b/primary/const.go new file mode 100644 index 00000000..d6af8c09 --- /dev/null +++ b/primary/const.go @@ -0,0 +1,3 @@ +package primary + +const SESSION_ADDOK = "SESSION STATUS RESULT=OK" diff --git a/primary/datagram.go b/primary/datagram.go new file mode 100644 index 00000000..e32a6bf1 --- /dev/null +++ b/primary/datagram.go @@ -0,0 +1,73 @@ +package primary + +import ( + "errors" + "net" + "strconv" + + "github.com/go-i2p/go-sam-go/common" + "github.com/go-i2p/go-sam-go/datagram" + "github.com/sirupsen/logrus" +) + +// 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 *PrimarySession) NewDatagramSubSession(id string, udpPort int) (*datagram.DatagramSession, error) { + log.WithFields(logrus.Fields{"id": id, "udpPort": udpPort}).Debug("NewDatagramSubSession called") + if udpPort > 65335 || udpPort < 0 { + log.WithField("udpPort", udpPort).Error("Invalid UDP port") + return nil, errors.New("udpPort needs to be in the intervall 0-65335") + } + if udpPort == 0 { + udpPort = 7655 + log.Debug("Using default UDP port 7655") + } + lhost, _, err := common.SplitHostPort(s.conn.LocalAddr().String()) + if err != nil { + log.WithError(err).Error("Failed to split local host port") + s.Close() + return nil, err + } + lUDPAddr, err := net.ResolveUDPAddr("udp4", lhost+":0") + if err != nil { + log.WithError(err).Error("Failed to resolve local UDP address") + return nil, err + } + udpconn, err := net.ListenUDP("udp4", lUDPAddr) + if err != nil { + log.WithError(err).Error("Failed to listen on UDP") + return nil, err + } + rhost, _, err := common.SplitHostPort(s.conn.RemoteAddr().String()) + if err != nil { + log.WithError(err).Error("Failed to split remote host port") + s.Close() + return nil, err + } + rUDPAddr, err := net.ResolveUDPAddr("udp4", rhost+":"+strconv.Itoa(udpPort)) + if err != nil { + log.WithError(err).Error("Failed to resolve remote UDP address") + return nil, err + } + _, lport, err := net.SplitHostPort(udpconn.LocalAddr().String()) + if err != nil { + log.WithError(err).Error("Failed to get local port") + s.Close() + return nil, err + } + conn, err := s.NewGenericSubSession("DATAGRAM", id, []string{"PORT=" + lport}) + if err != nil { + log.WithError(err).Error("Failed to create new generic sub-session") + return nil, err + } + + log.WithFields(logrus.Fields{"id": id, "localPort": lport}).Debug("Created new datagram sub-session") + datagramSession := &datagram.DatagramSession{ + SAM: (*datagram.SAM)(s.SAM), + SAMUDPAddress: rUDPAddr, + UDPConn: udpconn, + RemoteI2PAddr: nil, + } + datagramSession.Conn = conn + return datagramSession, nil +} diff --git a/primary/dialers.go b/primary/dialers.go new file mode 100644 index 00000000..106b82b9 --- /dev/null +++ b/primary/dialers.go @@ -0,0 +1,105 @@ +package primary + +import ( + "fmt" + "net" + "strings" + + "github.com/go-i2p/go-sam-go/common" + "github.com/go-i2p/go-sam-go/datagram" + "github.com/sirupsen/logrus" +) + +func (sam *PrimarySession) Dial(network, addr string) (net.Conn, error) { + log.WithFields(logrus.Fields{"network": network, "addr": addr}).Debug("Dial() called") + if network == "udp" || network == "udp4" || network == "udp6" { + //return sam.DialUDPI2P(network, network+addr[0:4], addr) + return sam.DialUDPI2P(network, network+addr[0:4], addr) + } + if network == "tcp" || network == "tcp4" || network == "tcp6" { + //return sam.DialTCPI2P(network, network+addr[0:4], addr) + return sam.DialTCPI2P(network, network+addr[0:4], addr) + } + log.WithField("network", network).Error("Invalid network type") + return nil, fmt.Errorf("Error: Must specify a valid network type") +} + +// DialTCP implements x/dialer +func (sam *PrimarySession) DialTCP(network string, laddr, raddr net.Addr) (net.Conn, error) { + log.WithFields(logrus.Fields{"network": network, "laddr": laddr, "raddr": raddr}).Debug("DialTCP() called") + ts, ok := sam.stsess[network+raddr.String()[0:4]] + var err error + if !ok { + ts, err = sam.NewUniqueStreamSubSession(network + raddr.String()[0:4]) + if err != nil { + log.WithError(err).Error("Failed to create new unique stream sub-session") + return nil, err + } + sam.stsess[network+raddr.String()[0:4]] = ts + ts, _ = sam.stsess[network+raddr.String()[0:4]] + } + return ts.Dial(network, raddr.String()) +} + +func (sam *PrimarySession) DialTCPI2P(network string, laddr, raddr string) (net.Conn, error) { + log.WithFields(logrus.Fields{"network": network, "laddr": laddr, "raddr": raddr}).Debug("DialTCPI2P() called") + ts, ok := sam.stsess[network+raddr[0:4]] + var err error + if !ok { + ts, err = sam.NewUniqueStreamSubSession(network + laddr) + if err != nil { + log.WithError(err).Error("Failed to create new unique stream sub-session") + return nil, err + } + sam.stsess[network+raddr[0:4]] = ts + ts, _ = sam.stsess[network+raddr[0:4]] + } + return ts.Dial(network, raddr) +} + +// DialUDP implements x/dialer +func (sam *PrimarySession) DialUDP(network string, laddr, raddr net.Addr) (net.PacketConn, error) { + log.WithFields(logrus.Fields{"network": network, "laddr": laddr, "raddr": raddr}).Debug("DialUDP() called") + ds, ok := sam.dgsess[network+raddr.String()[0:4]] + var err error + if !ok { + ds, err = sam.NewDatagramSubSession(network+raddr.String()[0:4], 0) + if err != nil { + log.WithError(err).Error("Failed to create new datagram sub-session") + return nil, err + } + sam.dgsess[network+raddr.String()[0:4]] = ds + ds, _ = sam.dgsess[network+raddr.String()[0:4]] + } + return ds.Dial(network, raddr.String()) +} + +func (sam *PrimarySession) DialUDPI2P(network, laddr, raddr string) (*datagram.DatagramSession, error) { + log.WithFields(logrus.Fields{"network": network, "laddr": laddr, "raddr": raddr}).Debug("DialUDPI2P() called") + ds, ok := sam.dgsess[network+raddr[0:4]] + var err error + if !ok { + ds, err = sam.NewDatagramSubSession(network+laddr, 0) + if err != nil { + log.WithError(err).Error("Failed to create new datagram sub-session") + return nil, err + } + sam.dgsess[network+raddr[0:4]] = ds + ds, _ = sam.dgsess[network+raddr[0:4]] + } + return ds.Dial(network, raddr) +} + +func (s *PrimarySession) Lookup(name string) (a net.Addr, err error) { + log.WithField("name", name).Debug("Lookup() called") + var sam *common.SAM + name = strings.Split(name, ":")[0] + sam, err = common.NewSAM(s.samAddr) + if err == nil { + log.WithField("addr", a).Debug("Lookup successful") + defer sam.Close() + a, err = sam.Lookup(name) + } + log.WithError(err).Error("Lookup failed") + return +} diff --git a/primary/generic.go b/primary/generic.go new file mode 100644 index 00000000..08bcae7e --- /dev/null +++ b/primary/generic.go @@ -0,0 +1,101 @@ +package primary + +import ( + "errors" + "net" + "strings" + + "github.com/sirupsen/logrus" + + "github.com/go-i2p/go-sam-go/common" +) + +// Creates a new session with the style of either "STREAM", "DATAGRAM" or "RAW", +// for a new I2P tunnel with name id, using the cypher keys specified, with the +// I2CP/streaminglib-options as specified. Extra arguments can be specified by +// setting extra to something else than []string{}. +// This sam3 instance is now a session +func (sam *PrimarySession) NewGenericSubSession(style, id string, extras []string) (net.Conn, error) { + log.WithFields(logrus.Fields{"style": style, "id": id, "extras": extras}).Debug("newGenericSubSession called") + return sam.NewGenericSubSessionWithSignature(style, id, extras) +} + +func (sam *PrimarySession) NewGenericSubSessionWithSignature(style, id string, extras []string) (net.Conn, error) { + log.WithFields(logrus.Fields{"style": style, "id": id, "extras": extras}).Debug("newGenericSubSessionWithSignature called") + return sam.NewGenericSubSessionWithSignatureAndPorts(style, id, "0", "0", extras) +} + +// Creates a new session with the style of either "STREAM", "DATAGRAM" or "RAW", +// for a new I2P tunnel with name id, using the cypher keys specified, with the +// I2CP/streaminglib-options as specified. Extra arguments can be specified by +// setting extra to something else than []string{}. +// This sam3 instance is now a session +func (sam *PrimarySession) NewGenericSubSessionWithSignatureAndPorts(style, id, from, to string, extras []string) (net.Conn, error) { + log.WithFields(logrus.Fields{"style": style, "id": id, "from": from, "to": to, "extras": extras}).Debug("newGenericSubSessionWithSignatureAndPorts called") + + conn := sam.conn + fp := "" + tp := "" + if from != "0" && from != "" { + fp = " FROM_PORT=" + from + } + if to != "0" && to != "" { + tp = " TO_PORT=" + to + } + scmsg := []byte("SESSION ADD STYLE=" + style + " ID=" + id + fp + tp + " " + strings.Join(extras, " ") + "\n") + + log.WithField("message", string(scmsg)).Debug("Sending SESSION ADD message") + + for m, i := 0, 0; m != len(scmsg); i++ { + if i == 15 { + conn.Close() + log.Error("Writing to SAM failed after 15 attempts") + return nil, errors.New("writing to SAM failed") + } + n, err := conn.Write(scmsg[m:]) + if err != nil { + log.WithError(err).Error("Failed to write to SAM connection") + conn.Close() + return nil, err + } + m += n + } + buf := make([]byte, 4096) + n, err := conn.Read(buf) + if err != nil { + log.WithError(err).Error("Failed to read from SAM connection") + conn.Close() + return nil, err + } + text := string(buf[:n]) + log.WithField("response", text).Debug("Received response from SAM") + //log.Println("SAM:", text) + if strings.HasPrefix(text, SESSION_ADDOK) { + //if sam.keys.String() != text[len(common.SESSION_ADDOK):len(text)-1] { + //conn.Close() + //return nil, errors.New("SAMv3 created a tunnel with keys other than the ones we asked it for") + //} + log.Debug("Session added successfully") + return conn, nil //&StreamSession{id, conn, keys, nil, sync.RWMutex{}, nil}, nil + } else if text == common.SESSION_DUPLICATE_ID { + log.Error("Duplicate tunnel name") + conn.Close() + return nil, errors.New("Duplicate tunnel name") + } else if text == common.SESSION_DUPLICATE_DEST { + log.Error("Duplicate destination") + conn.Close() + return nil, errors.New("Duplicate destination") + } else if text == common.SESSION_INVALID_KEY { + log.Error("Invalid key - Primary Session") + conn.Close() + return nil, errors.New("Invalid key - Primary Session") + } else if strings.HasPrefix(text, common.SESSION_I2P_ERROR) { + log.WithField("error", text[len(common.SESSION_I2P_ERROR):]).Error("I2P error") + conn.Close() + return nil, errors.New("I2P error " + text[len(common.SESSION_I2P_ERROR):]) + } else { + log.WithField("reply", text).Error("Unable to parse SAMv3 reply") + conn.Close() + return nil, errors.New("Unable to parse SAMv3 reply: " + text) + } +} diff --git a/primary/log.go b/primary/log.go new file mode 100644 index 00000000..ea6a3a00 --- /dev/null +++ b/primary/log.go @@ -0,0 +1,10 @@ +package primary + +import logger "github.com/go-i2p/go-sam-go/log" + +var log = logger.GetSAM3Logger() + +func init() { + logger.InitializeSAM3Logger() + log = logger.GetSAM3Logger() +} diff --git a/primary/primary.go b/primary/primary.go new file mode 100644 index 00000000..31c44f73 --- /dev/null +++ b/primary/primary.go @@ -0,0 +1,46 @@ +package primary + +/* +// 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) { + log.WithFields(logrus.Fields{"id": id, "options": options}).Debug("NewPrimarySession() called") + return sam.newPrimarySession(PrimarySessionSwitch, id, keys, options) +} + +func (sam *SAM) newPrimarySession(primarySessionSwitch string, 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{}) + 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 +} + +// Creates a new PrimarySession with the I2CP- and PRIMARYinglib options as +// specified. See the I2P documentation for a full list of options. +func (sam *SAM) NewPrimarySessionWithSignature(id string, keys i2pkeys.I2PKeys, options []string, sigType string) (*PrimarySession, error) { + log.WithFields(logrus.Fields{ + "id": id, + "options": options, + "sigType": sigType, + }).Debug("NewPrimarySessionWithSignature() called") + + conn, err := sam.newGenericSessionWithSignature(PrimarySessionSwitch, id, keys, sigType, options, []string{}) + 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 +} +*/ diff --git a/primary/raw.go b/primary/raw.go new file mode 100644 index 00000000..819a0cce --- /dev/null +++ b/primary/raw.go @@ -0,0 +1,74 @@ +package primary + +import ( + "errors" + "net" + "strconv" + + "github.com/go-i2p/go-sam-go/common" + "github.com/go-i2p/go-sam-go/raw" + "github.com/sirupsen/logrus" +) + +// 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 *PrimarySession) NewRawSubSession(id string, udpPort int) (*raw.RawSession, error) { + log.WithFields(logrus.Fields{"id": id, "udpPort": udpPort}).Debug("NewRawSubSession called") + + if udpPort > 65335 || udpPort < 0 { + log.WithField("udpPort", udpPort).Error("Invalid UDP port") + return nil, errors.New("udpPort needs to be in the intervall 0-65335") + } + if udpPort == 0 { + udpPort = 7655 + log.Debug("Using default UDP port 7655") + } + lhost, _, err := common.SplitHostPort(s.conn.LocalAddr().String()) + if err != nil { + log.WithError(err).Error("Failed to split local host port") + s.Close() + return nil, err + } + lUDPAddr, err := net.ResolveUDPAddr("udp4", lhost+":0") + if err != nil { + log.WithError(err).Error("Failed to resolve local UDP address") + return nil, err + } + udpconn, err := net.ListenUDP("udp4", lUDPAddr) + if err != nil { + log.WithError(err).Error("Failed to listen on UDP") + return nil, err + } + rhost, _, err := common.SplitHostPort(s.conn.RemoteAddr().String()) + if err != nil { + log.WithError(err).Error("Failed to split remote host port") + s.Close() + return nil, err + } + rUDPAddr, err := net.ResolveUDPAddr("udp4", rhost+":"+strconv.Itoa(udpPort)) + if err != nil { + log.WithError(err).Error("Failed to resolve remote UDP address") + return nil, err + } + _, lport, err := net.SplitHostPort(udpconn.LocalAddr().String()) + if err != nil { + log.WithError(err).Error("Failed to get local port") + s.Close() + return nil, err + } + // conn, err := s.newGenericSubSession("RAW", id, s.keys, options, []string{"PORT=" + lport}) + conn, err := s.NewGenericSubSession("RAW", id, []string{"PORT=" + lport}) + if err != nil { + log.WithError(err).Error("Failed to create new generic sub-session") + return nil, err + } + + log.WithFields(logrus.Fields{"id": id, "localPort": lport}).Debug("Created new raw sub-session") + rawSession := &raw.RawSession{ + SAM: (*raw.SAM)(s.SAM), + SAMUDPConn: udpconn, + SAMUDPAddr: rUDPAddr, + } + rawSession.Conn = conn + return rawSession, nil +} diff --git a/primary/stream.go b/primary/stream.go new file mode 100644 index 00000000..dc358311 --- /dev/null +++ b/primary/stream.go @@ -0,0 +1,57 @@ +package primary + +import ( + "github.com/go-i2p/go-sam-go/common" + "github.com/go-i2p/go-sam-go/stream" + "github.com/sirupsen/logrus" +) + +// Creates a new stream.StreamSession with the I2CP- and streaminglib options as +// specified. See the I2P documentation for a full list of options. +func (sam *PrimarySession) NewStreamSubSession(id string) (*stream.StreamSession, error) { + log.WithField("id", id).Debug("NewStreamSubSession called") + conn, err := sam.NewGenericSubSession("STREAM", id, []string{}) + if err != nil { + log.WithError(err).Error("Failed to create new generic sub-session") + return nil, err + } + streamSession := &stream.StreamSession{ + SAM: (*stream.SAM)(sam.SAM), + } + streamSession.Conn = conn + return streamSession, nil +} + +// Creates a new stream.StreamSession with the I2CP- and streaminglib options as +// specified. See the I2P documentation for a full list of options. +func (sam *PrimarySession) NewUniqueStreamSubSession(id string) (*stream.StreamSession, error) { + log.WithField("id", id).Debug("NewUniqueStreamSubSession called") + conn, err := sam.NewGenericSubSession("STREAM", id, []string{}) + if err != nil { + log.WithError(err).Error("Failed to create new generic sub-session") + return nil, err + } + fromPort, toPort := common.RandPort(), common.RandPort() + log.WithFields(logrus.Fields{"fromPort": fromPort, "toPort": toPort}).Debug("Generated random ports") + streamSession := &stream.StreamSession{ + SAM: (*stream.SAM)(sam.SAM), + } + streamSession.Conn = conn + return streamSession, nil +} + +// Creates a new stream.StreamSession with the I2CP- and streaminglib options as +// specified. See the I2P documentation for a full list of options. +func (sam *PrimarySession) NewStreamSubSessionWithPorts(id, from, to string) (*stream.StreamSession, error) { + log.WithFields(logrus.Fields{"id": id, "from": from, "to": to}).Debug("NewStreamSubSessionWithPorts called") + conn, err := sam.NewGenericSubSessionWithSignatureAndPorts("STREAM", id, from, to, []string{}) + if err != nil { + log.WithError(err).Error("Failed to create new generic sub-session with signature and ports") + return nil, err + } + streamSession := &stream.StreamSession{ + SAM: (*stream.SAM)(sam.SAM), + } + streamSession.Conn = conn + return streamSession, nil +} diff --git a/primary/types.go b/primary/types.go new file mode 100644 index 00000000..e6ef28fc --- /dev/null +++ b/primary/types.go @@ -0,0 +1,30 @@ +package primary + +import ( + "net" + "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" +) + +type SAM common.SAM + +// Represents a primary session. +type PrimarySession struct { + *SAM + samAddr string // address to the sam bridge (ipv4:port) + id string // tunnel name + conn net.Conn // connection to sam + keys i2pkeys.I2PKeys // i2p destination keys + Timeout time.Duration + Deadline time.Time + sigType string + Config common.SAMEmit + stsess map[string]*stream.StreamSession + dgsess map[string]*datagram.DatagramSession + // from string + // to string +} diff --git a/raw/log.go b/raw/log.go new file mode 100644 index 00000000..cf55c3dd --- /dev/null +++ b/raw/log.go @@ -0,0 +1,10 @@ +package raw + +import logger "github.com/go-i2p/go-sam-go/log" + +var log = logger.GetSAM3Logger() + +func init() { + logger.InitializeSAM3Logger() + log = logger.GetSAM3Logger() +} diff --git a/raw/raw.go b/raw/raw.go new file mode 100644 index 00000000..9628af0c --- /dev/null +++ b/raw/raw.go @@ -0,0 +1,73 @@ +package raw + +import ( + "errors" + "net" + "strconv" + + "github.com/go-i2p/go-sam-go/common" + "github.com/go-i2p/i2pkeys" + "github.com/sirupsen/logrus" +) + +// 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.I2PKeys, options []string, udpPort int) (*RawSession, error) { + log.WithFields(logrus.Fields{"id": id, "udpPort": udpPort}).Debug("Creating new RawSession") + + if udpPort > 65335 || udpPort < 0 { + log.WithField("udpPort", udpPort).Error("Invalid UDP port") + return nil, errors.New("udpPort needs to be in the interval 0-65335") + } + if udpPort == 0 { + udpPort = 7655 + log.Debug("Using default UDP port 7655") + } + lhost, _, err := common.SplitHostPort(s.LocalAddr().String()) + if err != nil { + log.Debug("Using default UDP port 7655") + s.Close() + return nil, err + } + lUDPAddr, err := net.ResolveUDPAddr("udp4", lhost+":0") + if err != nil { + log.WithError(err).Error("Failed to resolve local UDP address") + return nil, err + } + udpconn, err := net.ListenUDP("udp4", lUDPAddr) + if err != nil { + log.WithError(err).Error("Failed to listen on UDP") + return nil, err + } + rhost, _, err := common.SplitHostPort(s.RemoteAddr().String()) + if err != nil { + log.WithError(err).Error("Failed to split remote host port") + s.Close() + return nil, err + } + rUDPAddr, err := net.ResolveUDPAddr("udp4", rhost+":"+strconv.Itoa(udpPort)) + if err != nil { + log.WithError(err).Error("Failed to resolve remote UDP address") + return nil, err + } + _, lport, err := net.SplitHostPort(udpconn.LocalAddr().String()) + if err != nil { + log.WithError(err).Error("Failed to get local port") + return nil, err + } + conn, err := s.NewGenericSession("RAW", id, keys, []string{"PORT=" + lport}) + if err != nil { + log.WithError(err).Error("Failed to create new generic session") + return nil, err + } + log.WithFields(logrus.Fields{ + "id": id, + "localPort": lport, + "remoteUDPAddr": rUDPAddr, + }).Debug("Created new RawSession") + rawSession := &RawSession{ + SAM: s, + } + rawSession.Conn = conn + return rawSession, nil +} diff --git a/raw/types.go b/raw/types.go new file mode 100644 index 00000000..3f11c9c5 --- /dev/null +++ b/raw/types.go @@ -0,0 +1,21 @@ +package raw + +import ( + "net" + + "github.com/go-i2p/go-sam-go/common" +) + +type SAM common.SAM + +// 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 { + *SAM + SAMUDPConn *net.UDPConn // used to deliver datagrams + SAMUDPAddr *net.UDPAddr // the SAM bridge UDP-port +} diff --git a/stream/conn.go b/stream/conn.go new file mode 100644 index 00000000..63342c26 --- /dev/null +++ b/stream/conn.go @@ -0,0 +1,58 @@ +package stream + +import ( + "net" + "time" + + "github.com/go-i2p/i2pkeys" +) + +// Implements net.Conn +func (sc *StreamConn) Read(buf []byte) (int, error) { + n, err := sc.conn.Read(buf) + return n, err +} + +// Implements net.Conn +func (sc *StreamConn) Write(buf []byte) (int, error) { + n, err := sc.conn.Write(buf) + return n, err +} + +// Implements net.Conn +func (sc *StreamConn) Close() error { + return sc.conn.Close() +} + +func (sc *StreamConn) LocalAddr() net.Addr { + return sc.localAddr() +} + +// Implements net.Conn +func (sc *StreamConn) localAddr() i2pkeys.I2PAddr { + return sc.laddr +} + +func (sc *StreamConn) RemoteAddr() net.Addr { + return sc.remoteAddr() +} + +// Implements net.Conn +func (sc *StreamConn) remoteAddr() i2pkeys.I2PAddr { + return sc.raddr +} + +// Implements net.Conn +func (sc *StreamConn) SetDeadline(t time.Time) error { + return sc.conn.SetDeadline(t) +} + +// Implements net.Conn +func (sc *StreamConn) SetReadDeadline(t time.Time) error { + return sc.conn.SetReadDeadline(t) +} + +// Implements net.Conn +func (sc *StreamConn) SetWriteDeadline(t time.Time) error { + return sc.conn.SetWriteDeadline(t) +} diff --git a/stream/const.go b/stream/const.go new file mode 100644 index 00000000..cd5f3c95 --- /dev/null +++ b/stream/const.go @@ -0,0 +1,11 @@ +package stream + +const ( + ResultOK = "RESULT=OK" + ResultCantReachPeer = "RESULT=CANT_REACH_PEER" + ResultI2PError = "RESULT=I2P_ERROR" + ResultInvalidKey = "RESULT=INVALID_KEY" + ResultInvalidID = "RESULT=INVALID_ID" + ResultTimeout = "RESULT=TIMEOUT" + StreamConnectCommand = "STREAM CONNECT ID=" +) diff --git a/stream/dialers.go b/stream/dialers.go new file mode 100644 index 00000000..7c4aa209 --- /dev/null +++ b/stream/dialers.go @@ -0,0 +1,138 @@ +package stream + +import ( + "bufio" + "bytes" + "context" + "fmt" + "io" + "net" + "strings" + "time" + + "github.com/go-i2p/go-sam-go/common" + "github.com/go-i2p/i2pkeys" + "github.com/sirupsen/logrus" +) + +// context-aware dialer, eventually... +func (s *StreamSession) DialContext(ctx context.Context, n, addr string) (net.Conn, error) { + log.WithFields(logrus.Fields{"network": n, "addr": addr}).Debug("DialContext called") + return s.DialContextI2P(ctx, n, addr) +} + +// context-aware dialer, eventually... +func (s *StreamSession) DialContextI2P(ctx context.Context, n, addr string) (*StreamConn, error) { + log.WithFields(logrus.Fields{"network": n, "addr": addr}).Debug("DialContextI2P called") + if ctx == nil { + log.Panic("nil context") + panic("nil context") + } + deadline := s.deadline(ctx, time.Now()) + if !deadline.IsZero() { + if d, ok := ctx.Deadline(); !ok || deadline.Before(d) { + subCtx, cancel := context.WithDeadline(ctx, deadline) + defer cancel() + ctx = subCtx + } + } + + i2paddr, err := i2pkeys.NewI2PAddrFromString(addr) + if err != nil { + log.WithError(err).Error("Failed to create I2P address from string") + return nil, err + } + return s.DialI2P(i2paddr) +} + +// implement net.Dialer +func (s *StreamSession) Dial(n, addr string) (c net.Conn, err error) { + log.WithFields(logrus.Fields{"network": n, "addr": addr}).Debug("Dial called") + + var i2paddr i2pkeys.I2PAddr + var host string + host, _, err = net.SplitHostPort(addr) + //log.Println("Dialing:", host) + if err = common.IgnorePortError(err); err == nil { + // check for name + if strings.HasSuffix(host, ".b32.i2p") || strings.HasSuffix(host, ".i2p") { + // name lookup + i2paddr, err = s.Lookup(host) + log.WithFields(logrus.Fields{"host": host, "i2paddr": i2paddr}).Debug("Looked up I2P address") + } else { + // probably a destination + i2paddr, err = i2pkeys.NewI2PAddrFromBytes([]byte(host)) + //i2paddr = i2pkeys.I2PAddr(host) + //log.Println("Destination:", i2paddr, err) + log.WithFields(logrus.Fields{"host": host, "i2paddr": i2paddr}).Debug("Created I2P address from bytes") + } + if err == nil { + return s.DialI2P(i2paddr) + } + } + log.WithError(err).Error("Dial failed") + return +} + +// Dials to an I2P destination and returns a SAMConn, which implements a net.Conn. +func (s *StreamSession) DialI2P(addr i2pkeys.I2PAddr) (*StreamConn, error) { + log.WithField("addr", addr).Debug("DialI2P called") + sam, err := common.NewSAM(s.Sam()) + if err != nil { + log.WithError(err).Error("Failed to create new SAM instance") + return nil, err + } + conn := sam.Conn + _, err = conn.Write([]byte("STREAM CONNECT ID=" + s.ID() + s.FromPort() + s.ToPort() + " DESTINATION=" + addr.Base64() + " SILENT=false\n")) + if err != nil { + log.WithError(err).Error("Failed to write STREAM CONNECT command") + conn.Close() + return nil, err + } + buf := make([]byte, 4096) + n, err := conn.Read(buf) + if err != nil && err != io.EOF { + log.WithError(err).Error("Failed to write STREAM CONNECT command") + conn.Close() + return nil, err + } + scanner := bufio.NewScanner(bytes.NewReader(buf[:n])) + scanner.Split(bufio.ScanWords) + for scanner.Scan() { + switch scanner.Text() { + case "STREAM": + continue + case "STATUS": + continue + case ResultOK: + log.Debug("Successfully connected to I2P destination") + return &StreamConn{s.Addr(), addr, conn}, nil + case ResultCantReachPeer: + log.Error("Can't reach peer") + conn.Close() + return nil, fmt.Errorf("Can not reach peer") + case ResultI2PError: + log.Error("I2P internal error") + conn.Close() + return nil, fmt.Errorf("I2P internal error") + case ResultInvalidKey: + log.Error("Invalid key - Stream Session") + conn.Close() + return nil, fmt.Errorf("Invalid key - Stream Session") + case ResultInvalidID: + log.Error("Invalid tunnel ID") + conn.Close() + return nil, fmt.Errorf("Invalid tunnel ID") + case ResultTimeout: + log.Error("Connection timeout") + conn.Close() + return nil, fmt.Errorf("Timeout") + default: + log.WithField("error", scanner.Text()).Error("Unknown error") + conn.Close() + return nil, fmt.Errorf("Unknown error: %s : %s", scanner.Text(), string(buf[:n])) + } + } + log.Panic("Unexpected end of StreamSession.DialI2P()") + panic("sam3 go library error in StreamSession.DialI2P()") +} diff --git a/stream/listen.go b/stream/listen.go new file mode 100644 index 00000000..c46b6078 --- /dev/null +++ b/stream/listen.go @@ -0,0 +1,11 @@ +package stream + +import "github.com/sirupsen/logrus" + +// create a new stream listener to accept inbound connections +func (s *StreamSession) Listen() (*StreamListener, error) { + log.WithFields(logrus.Fields{"id": s.ID(), "laddr": s.Addr()}).Debug("Creating new StreamListener") + return &StreamListener{ + session: s, + }, nil +} diff --git a/stream/listener.go b/stream/listener.go new file mode 100644 index 00000000..b5cfdb6f --- /dev/null +++ b/stream/listener.go @@ -0,0 +1,133 @@ +package stream + +import ( + "bufio" + "errors" + "io" + "net" + "strconv" + "strings" + + "github.com/sirupsen/logrus" + + "github.com/go-i2p/go-sam-go/common" + "github.com/go-i2p/i2pkeys" +) + +func (l *StreamListener) From() string { + return l.session.Fromport +} + +func (l *StreamListener) To() string { + return l.session.Toport +} + +// get our address +// implements net.Listener +func (l *StreamListener) Addr() net.Addr { + return l.session.DestinationKeys.Addr() +} + +// implements net.Listener +func (l *StreamListener) Close() error { + return l.session.Close() +} + +// implements net.Listener +func (l *StreamListener) Accept() (net.Conn, error) { + return l.AcceptI2P() +} + +func ExtractPairString(input, value string) string { + log.WithFields(logrus.Fields{"input": input, "value": value}).Debug("ExtractPairString called") + parts := strings.Split(input, " ") + for _, part := range parts { + if strings.HasPrefix(part, value) { + kv := strings.SplitN(input, "=", 2) + if len(kv) == 2 { + log.WithFields(logrus.Fields{"key": kv[0], "value": kv[1]}).Debug("Pair extracted") + return kv[1] + } + } + } + log.WithFields(logrus.Fields{"input": input, "value": value}).Debug("No pair found") + return "" +} + +func ExtractPairInt(input, value string) int { + rv, err := strconv.Atoi(ExtractPairString(input, value)) + if err != nil { + log.WithFields(logrus.Fields{"input": input, "value": value}).Debug("No pair found") + return 0 + } + log.WithField("result", rv).Debug("Pair extracted and converted to int") + return rv +} + +func ExtractDest(input string) string { + log.WithField("input", input).Debug("ExtractDest called") + dest := strings.Split(input, " ")[0] + log.WithField("dest", dest).Debug("Destination extracted") + return strings.Split(input, " ")[0] +} + +// accept a new inbound connection +func (l *StreamListener) AcceptI2P() (*StreamConn, error) { + log.Debug("StreamListener.AcceptI2P() called") + s, err := common.NewSAM(l.session.Sam()) + if err == nil { + log.Debug("Connected to SAM bridge") + // we connected to sam + // send accept() command + _, err = io.WriteString(s.Conn, "STREAM ACCEPT ID="+l.session.ID()+" SILENT=false\n") + if err != nil { + log.WithError(err).Error("Failed to send STREAM ACCEPT command") + s.Close() + return nil, err + } + // read reply + rd := bufio.NewReader(s.Conn) + // read first line + line, err := rd.ReadString(10) + if err != nil { + log.WithError(err).Error("Failed to read SAM bridge response") + s.Close() + return nil, err + } + log.WithField("response", line).Debug("Received SAM bridge response") + log.Println(line) + if strings.HasPrefix(line, "STREAM STATUS RESULT=OK") { + // we gud read destination line + destline, err := rd.ReadString(10) + if err == nil { + dest := ExtractDest(destline) + l.session.Fromport = ExtractPairString(destline, "FROM_PORT") + l.session.Toport = ExtractPairString(destline, "TO_PORT") + // return wrapped connection + dest = strings.Trim(dest, "\n") + log.WithFields(logrus.Fields{ + "dest": dest, + "from": l.From(), + "to": l.To(), + }).Debug("Accepted new I2P connection") + return &StreamConn{ + laddr: l.session.Addr(), + raddr: i2pkeys.I2PAddr(dest), + conn: s.Conn, + }, nil + } else { + log.WithError(err).Error("Failed to read destination line") + s.Close() + return nil, err + } + } else { + log.WithField("line", line).Error("Invalid SAM response") + s.Close() + return nil, errors.New("invalid sam line: " + line) + } + } else { + log.WithError(err).Error("Failed to connect to SAM bridge") + s.Close() + return nil, err + } +} diff --git a/stream/log.go b/stream/log.go new file mode 100644 index 00000000..01e52196 --- /dev/null +++ b/stream/log.go @@ -0,0 +1,10 @@ +package stream + +import logger "github.com/go-i2p/go-sam-go/log" + +var log = logger.GetSAM3Logger() + +func init() { + logger.InitializeSAM3Logger() + log = logger.GetSAM3Logger() +} diff --git a/stream/session.go b/stream/session.go new file mode 100644 index 00000000..8f89a37b --- /dev/null +++ b/stream/session.go @@ -0,0 +1,107 @@ +package stream + +import ( + "context" + "net" + "time" + + "github.com/go-i2p/go-sam-go/common" + "github.com/go-i2p/i2pkeys" +) + +// Read reads data from the stream. +func (s *StreamSession) Read(buf []byte) (int, error) { + return s.Conn.Read(buf) +} + +// Write sends data over the stream. +func (s *StreamSession) Write(data []byte) (int, error) { + return s.Conn.Write(data) +} + +func (s *StreamSession) SetDeadline(t time.Time) error { + log.WithField("deadline", t).Debug("Setting deadline for StreamSession") + return s.Conn.SetDeadline(t) +} + +func (s *StreamSession) SetReadDeadline(t time.Time) error { + log.WithField("readDeadline", t).Debug("Setting read deadline for StreamSession") + return s.Conn.SetReadDeadline(t) +} + +func (s *StreamSession) SetWriteDeadline(t time.Time) error { + log.WithField("writeDeadline", t).Debug("Setting write deadline for StreamSession") + return s.Conn.SetWriteDeadline(t) +} + +func (s *StreamSession) From() string { + return s.Fromport +} + +func (s *StreamSession) To() string { + return s.Toport +} + +func (s *StreamSession) SignatureType() string { + return s.SignatureType() +} + +func (s *StreamSession) Close() error { + log.WithField("id", s.ID()).Debug("Closing StreamSession") + return s.Conn.Close() +} + +// Returns the I2P destination (the address) of the stream session +func (s *StreamSession) Addr() i2pkeys.I2PAddr { + return s.Addr() +} + +func (s *StreamSession) LocalAddr() net.Addr { + return s.Addr() +} + +// Returns the keys associated with the stream session +func (s *StreamSession) Keys() i2pkeys.I2PKeys { + return *s.DestinationKeys +} + +// lookup name, convenience function +func (s *StreamSession) Lookup(name string) (i2pkeys.I2PAddr, error) { + log.WithField("name", name).Debug("Looking up address") + sam, err := common.NewSAM(s.Sam()) + if err == nil { + addr, err := sam.Lookup(name) + defer sam.Close() + if err != nil { + log.WithError(err).Error("Lookup failed") + } else { + log.WithField("addr", addr).Debug("Lookup successful") + } + return addr, err + } + log.WithError(err).Error("Failed to create SAM instance for lookup") + return i2pkeys.I2PAddr(""), err +} + +/* +func (s *StreamSession) Cancel() chan *StreamSession { + ch := make(chan *StreamSession) + ch <- s + return ch +}*/ + +// deadline returns the earliest of: +// - now+Timeout +// - d.Deadline +// - the context's deadline +// +// Or zero, if none of Timeout, Deadline, or context's deadline is set. +func (s *StreamSession) deadline(ctx context.Context, now time.Time) (earliest time.Time) { + if s.Timeout != 0 { // including negative, for historical reasons + earliest = now.Add(s.Timeout) + } + if d, ok := ctx.Deadline(); ok { + earliest = minNonzeroTime(earliest, d) + } + return minNonzeroTime(earliest, s.Deadline) +} diff --git a/stream/stream.go b/stream/stream.go new file mode 100644 index 00000000..56a6fe03 --- /dev/null +++ b/stream/stream.go @@ -0,0 +1,56 @@ +package stream + +import ( + "github.com/go-i2p/i2pkeys" + "github.com/sirupsen/logrus" +) + +// 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.I2PKeys, options []string) (*StreamSession, error) { + log.WithFields(logrus.Fields{"id": id, "options": options}).Debug("Creating new StreamSession") + conn, err := sam.NewGenericSession("STREAM", id, keys, []string{}) + if err != nil { + return nil, err + } + log.WithField("id", id).Debug("Created new StreamSession") + streamSession := &StreamSession{ + SAM: sam, + } + streamSession.Conn = conn + return streamSession, nil +} + +// 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) NewStreamSessionWithSignature(id string, keys i2pkeys.I2PKeys, options []string, sigType string) (*StreamSession, error) { + log.WithFields(logrus.Fields{"id": id, "options": options, "sigType": sigType}).Debug("Creating new StreamSession with signature") + conn, err := sam.NewGenericSessionWithSignature("STREAM", id, keys, sigType, []string{}) + if err != nil { + return nil, err + } + log.WithFields(logrus.Fields{"id": id, "sigType": sigType}).Debug("Created new StreamSession with signature") + log.WithField("id", id).Debug("Created new StreamSession") + streamSession := &StreamSession{ + SAM: sam, + } + streamSession.Conn = conn + return streamSession, nil +} + +// 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) NewStreamSessionWithSignatureAndPorts(id, from, to string, keys i2pkeys.I2PKeys, options []string, sigType string) (*StreamSession, error) { + log.WithFields(logrus.Fields{"id": id, "from": from, "to": to, "options": options, "sigType": sigType}).Debug("Creating new StreamSession with signature and ports") + conn, err := sam.NewGenericSessionWithSignatureAndPorts("STREAM", id, from, to, keys, sigType, []string{}) + if err != nil { + return nil, err + } + log.WithFields(logrus.Fields{"id": id, "from": from, "to": to, "sigType": sigType}).Debug("Created new StreamSession with signature and ports") + log.WithField("id", id).Debug("Created new StreamSession") + streamSession := &StreamSession{ + SAM: sam, + } + streamSession.Conn = conn + return streamSession, nil +} diff --git a/stream/types.go b/stream/types.go new file mode 100644 index 00000000..28903c9b --- /dev/null +++ b/stream/types.go @@ -0,0 +1,29 @@ +package stream + +import ( + "net" + "time" + + "github.com/go-i2p/go-sam-go/common" + "github.com/go-i2p/i2pkeys" +) + +type SAM common.SAM + +// Represents a streaming session. +type StreamSession struct { + *SAM + Timeout time.Duration + Deadline time.Time +} + +type StreamListener struct { + // parent stream session + session *StreamSession +} + +type StreamConn struct { + laddr i2pkeys.I2PAddr + raddr i2pkeys.I2PAddr + conn net.Conn +} diff --git a/stream/util.go b/stream/util.go new file mode 100644 index 00000000..932b445c --- /dev/null +++ b/stream/util.go @@ -0,0 +1,13 @@ +package stream + +import "time" + +func minNonzeroTime(a, b time.Time) time.Time { + if a.IsZero() { + return b + } + if b.IsZero() || a.Before(b) { + return a + } + return b +}