24 Commits

Author SHA1 Message Date
ca51512ef9 refactor resolver 2024-12-09 14:15:48 -05:00
18dc0e6a9f Delete some unused functions 2024-12-08 16:21:20 -05:00
8279daa0e9 More of the great + purge, where we make all our formatters consistent. Oh also it looks like past idk was like 'hmm, I don't know for sure if I should leave NewKeys in sam3 now that I've moved i2pkeys to it's own library. That won't be confusing to future idk, who will never have a week like future idk is having right now.' Screw that guy(past idk). 2024-12-08 13:49:52 -05:00
bfbe7f638a Fix formatting when creating sessions with ports, which was breaking primary sessions 2024-12-08 13:34:42 -05:00
493db0aaaf move options 2024-12-08 13:22:39 -05:00
d7e8c77275 fix bug in address setting function, work on removing some more redundant code and config options 2024-12-01 10:10:58 -05:00
058a02e6cc Move just a spectacular amount of config stuff into a config package because 2024-11-30 19:18:01 -05:00
86e5b7c368 Move just a spectacular amount of config stuff into a config package because 2024-11-30 18:49:30 -05:00
1c1652f234 Move just a spectacular amount of config stuff into a config package because 2024-11-30 18:34:01 -05:00
1f27cb656e Expand the common package some more to include reply parser from goSam, move version to smarter structure 2024-11-29 21:07:26 -05:00
82eeba6275 Start abstracting away UDP session management 2024-11-24 19:11:04 -05:00
eea1f916cd split on space not newline 2024-11-24 01:08:43 -05:00
ac4ef13405 automatically chunk datagrams 2024-11-23 14:19:02 -05:00
dd2d0c029f Work on refactoring and potential concurrency issues, but I think primary is broken on master, users should stick to tagged versions unless they want to help 2024-11-23 00:47:49 -05:00
d8dafef068 fmt 2024-11-22 20:35:39 -05:00
2dc0890d17 if AcceptI2P errors out in an Accept call, close the resulting conn and return nil 2024-11-22 20:05:43 -05:00
5303ea6c34 if AcceptI2P errors out in an Accept call, close the resulting conn and return nil 2024-11-22 20:05:34 -05:00
fdfa4240fb Add the ability to set and use DatagramOptions in a DatagramSession 2024-11-22 19:16:35 -05:00
a22cde30eb use Sprintf instead of concatenation for command 2024-11-22 18:48:09 -05:00
88786afa0c Use external logger 2024-11-21 18:46:29 -05:00
b5cacb3ece remove the helper library 2024-11-20 23:41:25 -05:00
37205fc8a2 organize imports 2024-11-20 23:32:26 -05:00
ba5b2ca853 update release process 2024-11-16 16:22:38 -05:00
f4ca627cd8 update examples 2024-11-14 10:43:00 -05:00
33 changed files with 1545 additions and 1013 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
*~
*.swp
README.md.asc
log

View File

@ -1,6 +1,6 @@
USER_GH=eyedeekay
VERSION=0.33.9
USER_GH=go-i2p
VERSION=0.33.92
CREDIT='contributors to this release: @hkh4n, @eyedeekay'
packagename=sam3
@ -24,7 +24,7 @@ copier:
echo 'for f in $$(ls); do scp $$f/*.deb user@192.168.99.106:~/DEBIAN_PKGS/$$f/main/; done' >> deb/copy.sh
fmt:
find . -name '*.go' -exec gofmt -w -s {} \;
find . -name '*.go' -exec gofumpt -w -s -extra {} \;
upload-linux:
github-release upload -R -u $(USER_GH) -r "$(packagename)" -t $(VERSION) -l `sha256sum ` -n "$(packagename)" -f "$(packagename)"

View File

@ -1,17 +1,9 @@
# README #
## !!IMPORTANT!!
In the next version, I'll be moving the `i2pkeys` directory to it's own repository
so I can avoid import cycle headaches. Please migrate to the new `i2pkeys` repository
before upgrading your sam3 dependencies. You can probably do this by running:
```sh
find . -name '*.go' -exec sed -i 's|github.com/eyedeekay/sam3/i2pkeys|github.com/eyedeekay/i2pkeys|g' {} \;
```
STATUS: This project is maintained. I will respond to issues, pull requests, and feature requests within a few days.
[![Go Report Card](https://goreportcard.com/badge/github.com/go-i2p/sam3)](https://goreportcard.com/report/github.com/go-i2p/sam3)
# README #
go library for the I2P [SAMv3.0](https://geti2p.net/en/docs/api/samv3) bridge, used to build anonymous/pseudonymous end-to-end encrypted sockets.
@ -52,8 +44,8 @@ This library is much better than ccondom (that use BOB), much more stable and mu
package main
import (
"github.com/eyedeekay/sam3"
"github.com/eyedeekay/sam3/i2pkeys"
"github.com/go-i2p/sam3"
"github.com/go-i2p/sam3/i2pkeys"
"fmt"
)

View File

@ -7,64 +7,60 @@ import (
"github.com/go-i2p/i2pkeys"
)
/*
import (
. "github.com/go-i2p/i2pkeys"
)
*/
// SAMConn sets up a SAM connection.
// Implements net.Conn
type SAMConn struct {
laddr i2pkeys.I2PAddr
raddr i2pkeys.I2PAddr
conn net.Conn
net.Conn
}
// Implements net.Conn
// Read implements net.Conn
func (sc *SAMConn) Read(buf []byte) (int, error) {
n, err := sc.conn.Read(buf)
n, err := sc.Conn.Read(buf)
return n, err
}
// Implements net.Conn
// Write Implements net.Conn
func (sc *SAMConn) Write(buf []byte) (int, error) {
n, err := sc.conn.Write(buf)
n, err := sc.Conn.Write(buf)
return n, err
}
// Implements net.Conn
// Close Implements net.Conn
func (sc *SAMConn) Close() error {
return sc.conn.Close()
return sc.Conn.Close()
}
// LocalAddr Implements net.Conn
func (sc *SAMConn) LocalAddr() net.Addr {
return sc.localAddr()
}
// Implements net.Conn
func (sc *SAMConn) localAddr() i2pkeys.I2PAddr {
return sc.laddr
}
// RemoteAddr Implements net.Conn
func (sc *SAMConn) RemoteAddr() net.Addr {
return sc.remoteAddr()
}
// Implements net.Conn
func (sc *SAMConn) remoteAddr() i2pkeys.I2PAddr {
return sc.raddr
}
// Implements net.Conn
// SetDeadline Implements net.Conn
func (sc *SAMConn) SetDeadline(t time.Time) error {
return sc.conn.SetDeadline(t)
return sc.Conn.SetDeadline(t)
}
// Implements net.Conn
// SetReadDeadline Implements net.Conn
func (sc *SAMConn) SetReadDeadline(t time.Time) error {
return sc.conn.SetReadDeadline(t)
return sc.Conn.SetReadDeadline(t)
}
// Implements net.Conn
// SetWriteDeadline Implements net.Conn
func (sc *SAMConn) SetWriteDeadline(t time.Time) error {
return sc.conn.SetWriteDeadline(t)
return sc.Conn.SetWriteDeadline(t)
}

60
common/formatter.go Normal file
View File

@ -0,0 +1,60 @@
package common
import (
"fmt"
"strings"
)
type SAMFormatter struct {
Version ProtocolVersion
}
// Common SAM protocol message types
const (
HelloMsg = "HELLO"
SessionMsg = "SESSION"
StreamMsg = "STREAM"
DatagramMsg = "DATAGRAM"
RawMsg = "RAW"
PrimaryMSG = "PRIMARY"
NamingMsg = "NAMING"
)
func NewSAMFormatter(version ProtocolVersion) *SAMFormatter {
return &SAMFormatter{Version: version}
}
// FormatHello formats the initial handshake message
func (f *SAMFormatter) FormatHello() string {
return fmt.Sprintf("HELLO VERSION MIN=%s MAX=%s\n", f.Version, f.Version)
}
// FormatSession formats a session creation message
func (f *SAMFormatter) FormatSession(style, id string, options map[string]string) string {
optStr := formatOptions(options)
return fmt.Sprintf("SESSION CREATE STYLE=%s ID=%s%s\n", style, id, optStr)
}
// FormatDatagram formats a datagram message
func (f *SAMFormatter) FormatDatagram(sessionID, dest string, options map[string]string) string {
optStr := formatOptions(options)
return fmt.Sprintf("DATAGRAM SEND ID=%s DESTINATION=%s%s\n", sessionID, dest, optStr)
}
// FormatNamingLookup formats a naming lookup message
func (f *SAMFormatter) FormatNamingLookup(name string) string {
return fmt.Sprintf("NAMING LOOKUP NAME=%s\n", name)
}
// Helper function to format options
func formatOptions(options map[string]string) string {
if len(options) == 0 {
return ""
}
var opts []string
for k, v := range options {
opts = append(opts, fmt.Sprintf(" %s=%s", k, v))
}
return strings.Join(opts, "")
}

87
common/reply.go Normal file
View File

@ -0,0 +1,87 @@
// common/reply.go
package common
import (
"fmt"
"strings"
)
// Reply represents a parsed SAM bridge response
type Reply struct {
Topic string // e.g., "HELLO", "SESSION", "STREAM", etc.
Type string // Usually "REPLY"
Result string // "OK" or error message
KeyValues map[string]string // Additional key-value pairs in the response
}
// ParseReply parses a raw SAM bridge response into a structured Reply
func ParseReply(response string) (*Reply, error) {
parts := strings.Fields(response)
if len(parts) < 3 {
return nil, fmt.Errorf("invalid reply format: %s", response)
}
reply := &Reply{
Topic: parts[0],
Type: parts[1],
KeyValues: make(map[string]string),
}
// Parse remaining key=value pairs
for _, part := range parts[2:] {
if kv := strings.SplitN(part, "=", 2); len(kv) == 2 {
key := strings.ToUpper(kv[0])
if key == "RESULT" {
reply.Result = kv[1]
} else {
reply.KeyValues[key] = kv[1]
}
}
}
if reply.Result == "" {
return nil, fmt.Errorf("missing RESULT in reply: %s", response)
}
return reply, nil
}
// IsOk returns true if the reply indicates success
func (r *Reply) IsOk() bool {
return r.Result == "OK"
}
// Error returns an error if the reply indicates failure
func (r *Reply) Error() error {
if r.IsOk() {
return nil
}
return fmt.Errorf("%s failed: %s", r.Topic, r.Result)
}
// Value safely retrieves a value from KeyValues
func (r *Reply) Value(key string) (string, bool) {
v, ok := r.KeyValues[strings.ToUpper(key)]
return v, ok
}
// MustValue gets a value or panics if not found
func (r *Reply) MustValue(key string) string {
if v, ok := r.Value(key); ok {
return v
}
panic(fmt.Sprintf("required key not found: %s", key))
}
// Specific reply type checkers
func (r *Reply) IsHello() bool {
return r.Topic == HelloMsg && r.Type == "REPLY"
}
func (r *Reply) IsSession() bool {
return r.Topic == SessionMsg && r.Type == "REPLY"
}
func (r *Reply) IsNaming() bool {
return r.Topic == NamingMsg && r.Type == "REPLY"
}

140
common/udp.go Normal file
View File

@ -0,0 +1,140 @@
package common
import (
"errors"
"fmt"
"net"
"strconv"
"time"
"github.com/go-i2p/logger"
)
// Package common provides shared UDP common functionality for SAM sessions
//
// It handles:
// - UDP port validation and defaults
// - Address resolution
// - Connection setup
// - Logging
//
// Example Usage:
//
// cfg := &UDPSessionConfig{
// Port: 7655,
// ParentConn: samConn,
// Log: logger,
// }
//
// session, err := NewUDPSession(cfg)
// if err != nil {
// // Handle error
// }
// defer session.Close()
// UDPSessionConfig holds all UDP session configuration
type UDPSessionConfig struct {
Port int
ParentConn net.Conn
Log *logger.Logger
DefaultPort int
AllowZeroPort bool
Style string
FromPort string
ToPort string
ReadTimeout time.Duration
WriteTimeout time.Duration
}
// UDPSession represents an established UDP session
type UDPSession struct {
LocalAddr *net.UDPAddr
RemoteAddr *net.UDPAddr
Conn *net.UDPConn
}
func (u *UDPSession) SetReadTimeout(timeout time.Duration) error {
if u.Conn != nil {
return u.Conn.SetReadDeadline(time.Now().Add(timeout))
}
return nil
}
func (u *UDPSession) SetWriteTimeout(timeout time.Duration) error {
if u.Conn != nil {
return u.Conn.SetWriteDeadline(time.Now().Add(timeout))
}
return nil
}
func (u UDPSession) LocalPort() int {
return u.LocalAddr.Port
}
func (u UDPSession) Close() {
u.Conn.Close()
}
// NewUDPSession creates and configures a new UDP session
func NewUDPSession(cfg *UDPSessionConfig) (*UDPSession, error) {
if err := validatePort(cfg.Port, cfg.AllowZeroPort); err != nil {
cfg.Log.WithError(err).Error("Invalid UDP port configuration")
return nil, err
}
port := cfg.Port
if port == 0 {
port = cfg.DefaultPort
cfg.Log.WithField("port", port).Debug("Using default UDP port")
}
laddr, raddr, err := resolveAddresses(cfg.ParentConn, port)
if err != nil {
return nil, fmt.Errorf("address resolution failed: %w", err)
}
conn, err := net.ListenUDP("udp4", laddr)
if err != nil {
return nil, fmt.Errorf("UDP listen failed: %w", err)
}
return &UDPSession{
LocalAddr: laddr,
RemoteAddr: raddr,
Conn: conn,
}, nil
}
func validatePort(port int, allowZero bool) error {
if port < 0 || port > 65535 {
return errors.New("port must be between 0-65535")
}
if port == 0 && !allowZero {
return errors.New("port 0 not allowed in this context")
}
return nil
}
func resolveAddresses(parent net.Conn, remotePort int) (*net.UDPAddr, *net.UDPAddr, error) {
lhost, _, err := net.SplitHostPort(parent.LocalAddr().String())
if err != nil {
return nil, nil, err
}
laddr, err := net.ResolveUDPAddr("udp4", lhost+":0")
if err != nil {
return nil, nil, err
}
rhost, _, err := net.SplitHostPort(parent.RemoteAddr().String())
if err != nil {
return nil, nil, err
}
raddr, err := net.ResolveUDPAddr("udp4", rhost+":"+strconv.Itoa(remotePort))
if err != nil {
return nil, nil, err
}
return laddr, raddr, nil
}

19
common/version.go Normal file
View File

@ -0,0 +1,19 @@
package common
type ProtocolVersion string
type Version struct {
String ProtocolVersion
Number float64
}
var (
SAM31Version = Version{
String: "3.1",
Number: 3.1,
}
SAM33Version = Version{
String: "3.3",
Number: 3.3,
}
)

436
config.go
View File

@ -2,63 +2,34 @@ package sam3
import (
"fmt"
"github.com/sirupsen/logrus"
"math/rand"
"net"
"strconv"
"strings"
"time"
"github.com/sirupsen/logrus"
"github.com/go-i2p/i2pkeys"
"github.com/go-i2p/sam3/common"
"github.com/go-i2p/sam3/config"
)
const DEFAULT_LEASESET_TYPE = "i2cp.leaseSetEncType=4"
// I2PConfig is a struct which manages I2P configuration options
type I2PConfig struct {
SamHost string
SamPort string
TunName string
SamMin string
SamMax string
Fromport string
Toport string
Style string
TunType string
common.SAMFormatter
config.SessionOptions
config.TransportOptions
config.TunnelOptions
config.EncryptedLeaseSetOptions
DestinationKeys i2pkeys.I2PKeys
SigType string
EncryptLeaseSet string
LeaseSetKey string
LeaseSetPrivateKey string
LeaseSetPrivateSigningKey string
LeaseSetKeys i2pkeys.I2PKeys
InAllowZeroHop string
OutAllowZeroHop string
InLength string
OutLength string
InQuantity string
OutQuantity string
InVariance string
OutVariance string
InBackupQuantity string
OutBackupQuantity string
FastRecieve string
UseCompression string
MessageReliability string
CloseIdle string
CloseIdleTime string
ReduceIdle string
ReduceIdleTime string
ReduceIdleQuantity string
LeaseSetEncryption string
//Streaming Library options
// Streaming Library options
AccessListType string
AccessList []string
}
// Sam returns the SAM address in the form of "host:port"
func (f *I2PConfig) Sam() string {
host := "127.0.0.1"
port := "7656"
@ -72,9 +43,10 @@ func (f *I2PConfig) Sam() string {
"host": host,
"port": port,
}).Debug("SAM address constructed")
return host + ":" + port
return fmt.Sprintf("%s:%s", host, port)
}
// SetSAMAddress sets the SAM address from a string in the form of "host:port"
func (f *I2PConfig) SetSAMAddress(addr string) {
hp := strings.Split(addr, ":")
if len(hp) == 1 {
@ -82,203 +54,89 @@ func (f *I2PConfig) SetSAMAddress(addr string) {
} else if len(hp) == 2 {
f.SamPort = hp[1]
f.SamHost = hp[0]
} else {
if f.SamHost == "" {
f.SamHost = "127.0.0.1"
}
if f.SamPort == "" {
f.SamPort = "7656"
}
}
f.SamPort = "7656"
f.SamHost = "127.0.0.1"
log.WithFields(logrus.Fields{
"host": f.SamHost,
"port": f.SamPort,
}).Debug("SAM address set")
i2pkeys.DefaultSAMAddress = f.Sam()
}
// ID returns the tunnel name in the form of "ID=name"
func (f *I2PConfig) ID() string {
if f.TunName == "" {
if f.NickName == "" {
b := make([]byte, 12)
for i := range b {
b[i] = "abcdefghijklmnopqrstuvwxyz"[rand.Intn(len("abcdefghijklmnopqrstuvwxyz"))]
}
f.TunName = string(b)
log.WithField("TunName", f.TunName).Debug("Generated random tunnel name")
f.NickName = string(b)
log.WithField("NickName", f.NickName).Debug("Generated random tunnel name")
}
return " ID=" + f.TunName + " "
}
func (f *I2PConfig) Leasesetsettings() (string, string, string) {
var r, s, t string
if f.LeaseSetKey != "" {
r = " i2cp.leaseSetKey=" + f.LeaseSetKey + " "
}
if f.LeaseSetPrivateKey != "" {
s = " i2cp.leaseSetPrivateKey=" + f.LeaseSetPrivateKey + " "
}
if f.LeaseSetPrivateSigningKey != "" {
t = " i2cp.leaseSetPrivateSigningKey=" + f.LeaseSetPrivateSigningKey + " "
}
log.WithFields(logrus.Fields{
"leaseSetKey": r,
"leaseSetPrivateKey": s,
"leaseSetPrivateSigningKey": t,
}).Debug("Lease set settings constructed")
return r, s, t
}
func (f *I2PConfig) FromPort() string {
if f.samMax() < 3.1 {
log.Debug("SAM version < 3.1, FromPort not applicable")
return ""
}
if f.Fromport != "0" {
log.WithField("fromPort", f.Fromport).Debug("FromPort set")
return " FROM_PORT=" + f.Fromport + " "
}
log.Debug("FromPort not set")
return ""
}
func (f *I2PConfig) ToPort() string {
if f.samMax() < 3.1 {
log.Debug("SAM version < 3.1, ToPort not applicable")
return ""
}
if f.Toport != "0" {
log.WithField("toPort", f.Toport).Debug("ToPort set")
return " TO_PORT=" + f.Toport + " "
}
log.Debug("ToPort not set")
return ""
}
func (f *I2PConfig) SessionStyle() string {
if f.Style != "" {
log.WithField("style", f.Style).Debug("Session style set")
return " STYLE=" + f.Style + " "
}
log.Debug("Using default STREAM style")
return " STYLE=STREAM "
}
func (f *I2PConfig) samMax() float64 {
i, err := strconv.Atoi(f.SamMax)
if err != nil {
log.WithError(err).Warn("Failed to parse SamMax, using default 3.1")
return 3.1
}
log.WithField("samMax", float64(i)).Debug("SAM max version parsed")
return float64(i)
return fmt.Sprintf(" ID=%s ", f.NickName)
}
// MinSAM returns the minimum SAM version required in major.minor form
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
min, _ := f.GetVersions()
return string(min)
}
// MaxSAM returns the maximum SAM version required in major.minor form
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
_, max := f.GetVersions()
return string(max)
}
func (f *I2PConfig) GetVersions() (min, max common.ProtocolVersion) {
if f.SamMin == "" {
min = common.SAM31Version.String
} else {
min = common.ProtocolVersion(f.SamMin)
}
if f.SamMax == "" {
max = common.SAM33Version.String
log.Debug("Using default MaxSAM: 3.3")
} else {
max = common.ProtocolVersion(f.SamMax)
}
return min, max
}
// DestinationKey returns the destination key setting in the form of "DESTINATION=key"
func (f *I2PConfig) DestinationKey() string {
if &f.DestinationKeys != nil {
log.WithField("destinationKey", f.DestinationKeys.String()).Debug("Destination key set")
return " DESTINATION=" + f.DestinationKeys.String() + " "
fmt.Sprintf(" DESTINATION=%s ", f.DestinationKeys.String())
}
log.Debug("Using TRANSIENT destination")
return " DESTINATION=TRANSIENT "
}
func (f *I2PConfig) SignatureType() string {
if f.samMax() < 3.1 {
log.Debug("SAM version < 3.1, SignatureType not applicable")
return ""
}
if f.SigType != "" {
log.WithField("sigType", f.SigType).Debug("Signature type set")
return " SIGNATURE_TYPE=" + f.SigType + " "
}
log.Debug("Signature type not set")
return ""
}
func (f *I2PConfig) EncryptLease() string {
if f.EncryptLeaseSet == "true" {
log.Debug("Lease set encryption enabled")
return " i2cp.encryptLeaseSet=true "
}
log.Debug("Lease set encryption not enabled")
return ""
}
func (f *I2PConfig) Reliability() string {
if f.MessageReliability != "" {
log.WithField("reliability", f.MessageReliability).Debug("Message reliability set")
return " i2cp.messageReliability=" + f.MessageReliability + " "
}
log.Debug("Message reliability not set")
return ""
}
func (f *I2PConfig) Reduce() string {
if f.ReduceIdle == "true" {
log.WithFields(logrus.Fields{
"reduceIdle": f.ReduceIdle,
"reduceIdleTime": f.ReduceIdleTime,
"reduceIdleQuantity": f.ReduceIdleQuantity,
}).Debug("Reduce idle settings applied")
return "i2cp.reduceOnIdle=" + f.ReduceIdle + "i2cp.reduceIdleTime=" + f.ReduceIdleTime + "i2cp.reduceQuantity=" + f.ReduceIdleQuantity
}
log.Debug("Reduce idle settings not applied")
return ""
}
func (f *I2PConfig) Close() string {
if f.CloseIdle == "true" {
log.WithFields(logrus.Fields{
"closeIdle": f.CloseIdle,
"closeIdleTime": f.CloseIdleTime,
}).Debug("Close idle settings applied")
return "i2cp.closeOnIdle=" + f.CloseIdle + "i2cp.closeIdleTime=" + f.CloseIdleTime
}
log.Debug("Close idle settings not applied")
return ""
}
func (f *I2PConfig) DoZero() string {
r := ""
if f.InAllowZeroHop == "true" {
r += " inbound.allowZeroHop=" + f.InAllowZeroHop + " "
}
if f.OutAllowZeroHop == "true" {
r += " outbound.allowZeroHop= " + f.OutAllowZeroHop + " "
}
if f.FastRecieve == "true" {
r += " " + f.FastRecieve + " "
}
log.WithField("zeroHopSettings", r).Debug("Zero hop settings applied")
return r
}
// Print returns the full config as a string
func (f *I2PConfig) Print() []string {
lsk, lspk, lspsk := f.Leasesetsettings()
return []string{
//f.targetForPort443(),
"inbound.length=" + f.InLength,
"outbound.length=" + f.OutLength,
"inbound.lengthVariance=" + f.InVariance,
"outbound.lengthVariance=" + f.OutVariance,
"inbound.backupQuantity=" + f.InBackupQuantity,
"outbound.backupQuantity=" + f.OutBackupQuantity,
"inbound.quantity=" + f.InQuantity,
"outbound.quantity=" + f.OutQuantity,
f.DoZero(),
// f.targetForPort443(),
f.InboundLength(),
f.OutboundLength(),
f.InboundVariance(),
f.OutboundVariance(),
f.InboundBackupQuantity(),
f.OutboundBackupQuantity(),
f.InboundQuantity(),
f.OutboundQuantity(),
f.InboundDoZero(),
f.OutboundDoZero(),
//"i2cp.fastRecieve=" + f.FastRecieve,
"i2cp.gzip=" + f.UseCompression,
f.DoFastReceive(),
f.UsesCompression(),
f.Reduce(),
f.Close(),
f.Reliability(),
@ -290,6 +148,7 @@ func (f *I2PConfig) Print() []string {
}
}
// Accesslisttype returns the access list type
func (f *I2PConfig) Accesslisttype() string {
if f.AccessListType == "whitelist" {
log.Debug("Access list type set to whitelist")
@ -305,65 +164,62 @@ func (f *I2PConfig) Accesslisttype() string {
return ""
}
// Accesslist returns the access list in the form of "i2cp.accessList=list"
func (f *I2PConfig) Accesslist() string {
if f.AccessListType != "" && len(f.AccessList) > 0 {
r := ""
for _, s := range f.AccessList {
r += s + ","
}
r := strings.Join(f.AccessList, ",")
log.WithField("accessList", r).Debug("Access list generated")
return "i2cp.accessList=" + strings.TrimSuffix(r, ",")
return fmt.Sprintf(" i2cp.accessList=%s ", r)
}
log.Debug("Access list not set")
return ""
}
func (f *I2PConfig) LeaseSetEncryptionType() string {
if f.LeaseSetEncryption == "" {
log.Debug("Using default lease set encryption type: 4,0")
return "i2cp.leaseSetEncType=4,0"
}
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.WithField("leaseSetEncType", f.LeaseSetEncryption).Debug("Lease set encryption type set")
return "i2cp.leaseSetEncType=" + f.LeaseSetEncryption
}
// NewConfig returns a new config with default values or updates them with functional arguments
func NewConfig(opts ...func(*I2PConfig) error) (*I2PConfig, error) {
var config I2PConfig
config.SamHost = "127.0.0.1"
config.SamPort = "7656"
config.SamMin = "3.0"
config.SamMax = "3.2"
config.TunName = ""
config.TunType = "server"
config.Style = "STREAM"
config.InLength = "3"
config.OutLength = "3"
config.InQuantity = "2"
config.OutQuantity = "2"
config.InVariance = "1"
config.OutVariance = "1"
config.InBackupQuantity = "3"
config.OutBackupQuantity = "3"
config.InAllowZeroHop = "false"
config.OutAllowZeroHop = "false"
config.EncryptLeaseSet = "false"
config.LeaseSetKey = ""
config.LeaseSetPrivateKey = ""
config.LeaseSetPrivateSigningKey = ""
config.FastRecieve = "false"
config.UseCompression = "true"
config.ReduceIdle = "false"
config.ReduceIdleTime = "15"
config.ReduceIdleQuantity = "4"
config.CloseIdle = "false"
config.CloseIdleTime = "300000"
config.MessageReliability = "none"
config := I2PConfig{
EncryptedLeaseSetOptions: config.EncryptedLeaseSetOptions{
EncryptLeaseSet: false,
LeaseSetKey: "",
LeaseSetPrivateKey: "",
LeaseSetPrivateSigningKey: "",
LeaseSetEncryption: DEFAULT_LEASESET_TYPE,
},
TunnelOptions: config.TunnelOptions{
InAllowZeroHop: false,
OutAllowZeroHop: false,
InLength: 3,
OutLength: 3,
InQuantity: 2,
OutQuantity: 2,
InVariance: 1,
OutVariance: 1,
InBackupQuantity: 3,
OutBackupQuantity: 3,
},
SessionOptions: config.SessionOptions{
NickName: "",
Style: "STREAM",
SigType: "EdDSA_SHA512_Ed25519",
InFromPort: "",
OutToPort: "",
Protocol: "",
UDPPort: 0,
SamHost: "127.0.0.1",
SamPort: "7656",
SamMin: string(common.SAM31Version.String),
SamMax: string(common.SAM33Version.String),
},
TransportOptions: config.TransportOptions{
UseCompression: "true",
FastReceive: "false",
MessageReliability: "none",
CloseIdleTimeout: 5 * time.Minute,
ReduceIdleQuantity: 1,
ReduceIdle: false,
CloseIdle: false,
},
}
for _, o := range opts {
if err := o(&config); err != nil {
return nil, err
@ -371,67 +227,3 @@ func NewConfig(opts ...func(*I2PConfig) error) (*I2PConfig, error) {
}
return &config, nil
}
// options map
type Options map[string]string
// obtain sam options as list of strings
func (opts Options) AsList() (ls []string) {
for k, v := range opts {
ls = append(ls, fmt.Sprintf("%s=%s", k, v))
}
return
}
// Config is the config type for the sam connector api for i2p which allows applications to 'speak' with i2p
type Config struct {
Addr string
Opts Options
Session string
Keyfile string
}
// create new sam connector from config with a stream session
func (cfg *Config) StreamSession() (session *StreamSession, err error) {
// connect
var s *SAM
s, err = NewSAM(cfg.Addr)
if err == nil {
// ensure keys exist
var keys i2pkeys.I2PKeys
keys, err = s.EnsureKeyfile(cfg.Keyfile)
if err == nil {
// create session
session, err = s.NewStreamSession(cfg.Session, keys, cfg.Opts.AsList())
}
}
return
}
// create new sam datagram session from config
func (cfg *Config) DatagramSession() (session *DatagramSession, err error) {
// connect
var s *SAM
s, err = NewSAM(cfg.Addr)
if err == nil {
// ensure keys exist
var keys i2pkeys.I2PKeys
keys, err = s.EnsureKeyfile(cfg.Keyfile)
if err == nil {
// determine udp port
var portstr string
_, portstr, err = net.SplitHostPort(cfg.Addr)
if IgnorePortError(err) == nil {
var port int
port, err = strconv.Atoi(portstr)
if err == nil && port > 0 {
// udp port is 1 lower
port--
// create session
session, err = s.NewDatagramSession(cfg.Session, keys, cfg.Opts.AsList(), port)
}
}
}
}
return
}

83
config/leaseset.go Normal file
View File

@ -0,0 +1,83 @@
package config
import (
"fmt"
"strconv"
"strings"
"github.com/go-i2p/i2pkeys"
"github.com/sirupsen/logrus"
)
type EncryptedLeaseSetOptions struct {
EncryptLeaseSet bool
LeaseSetKey string
LeaseSetPrivateKey string
LeaseSetPrivateSigningKey string
LeaseSetKeys i2pkeys.I2PKeys
LeaseSetEncryption string
}
// EncryptLease returns the lease set encryption setting in the form of "i2cp.encryptLeaseSet=true"
func (f *EncryptedLeaseSetOptions) EncryptLease() string {
if f.EncryptLeaseSet {
log.Debug("Lease set encryption enabled")
return " i2cp.encryptLeaseSet=true "
}
log.Debug("Lease set encryption not enabled")
return ""
}
// LeaseSetEncryptionType returns the lease set encryption type in the form of "i2cp.leaseSetEncType=type"
func (f *EncryptedLeaseSetOptions) LeaseSetEncryptionType() string {
if f.LeaseSetEncryption == "" {
log.Debug("Using default lease set encryption type: 4,0")
return "i2cp.leaseSetEncType=4,0"
}
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.WithField("leaseSetEncType", f.LeaseSetEncryption).Debug("Lease set encryption type set")
return fmt.Sprintf(" i2cp.leaseSetEncType=%s ", f.LeaseSetEncryption)
}
func (f *EncryptedLeaseSetOptions) leaseSetKey() string {
if f.LeaseSetKey != "" {
return fmt.Sprintf(" i2cp.leaseSetKey=%s ", f.LeaseSetKey)
}
return ""
}
func (f *EncryptedLeaseSetOptions) leaseSetPrivateKey() string {
if f.LeaseSetPrivateKey != "" {
return fmt.Sprintf(" i2cp.leaseSetPrivateKey=%s ", f.LeaseSetPrivateKey)
}
return ""
}
func (f *EncryptedLeaseSetOptions) leaseSetPrivateSigningKey() string {
if f.LeaseSetPrivateSigningKey != "" {
return fmt.Sprintf(" i2cp.leaseSetPrivateSigningKey=%s ", f.LeaseSetPrivateSigningKey)
}
return ""
}
// Leasesetsettings returns the lease set settings in the form of "i2cp.leaseSetKey=key i2cp.leaseSetPrivateKey=key i2cp.leaseSetPrivateSigningKey=key"
func (f *EncryptedLeaseSetOptions) Leasesetsettings() (string, string, string) {
if f.EncryptLeaseSet {
var r, s, t string
r = f.leaseSetKey()
s = f.leaseSetPrivateKey()
t = f.leaseSetPrivateSigningKey()
log.WithFields(logrus.Fields{
"leaseSetKey": r,
"leaseSetPrivateKey": s,
"leaseSetPrivateSigningKey": t,
}).Debug("Lease set settings constructed")
return r, s, t
}
return "", "", ""
}

5
config/log.go Normal file
View File

@ -0,0 +1,5 @@
package config
import logger "github.com/go-i2p/sam3/log"
var log = logger.GetSAM3Logger()

84
config/session.go Normal file
View File

@ -0,0 +1,84 @@
package config
import (
"fmt"
"strconv"
"github.com/go-i2p/sam3/common"
)
type SessionOptions struct {
NickName string
Style string
SigType string
InFromPort string
OutToPort string
Protocol string
UDPPort int
SamHost string
SamPort string
SamMin string
SamMax string
}
func (f *SessionOptions) samMax() float64 {
i, err := strconv.Atoi(f.SamMax)
if err != nil {
log.WithError(err).Warn("Failed to parse SamMax, using default 3.1")
return 3.1
}
log.WithField("samMax", float64(i)).Debug("SAM max version parsed")
return float64(i)
}
// SignatureType returns the signature type setting in the form of "SIGNATURE_TYPE=type"
func (f *SessionOptions) SignatureType() string {
if f.samMax() < common.SAM31Version.Number {
log.Debug("SAM version < 3.1, SignatureType not applicable")
return ""
}
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 ""
}
// FromPort returns the from port setting in the form of "FROM_PORT=port"
func (f *SessionOptions) FromPort() string {
if f.samMax() < common.SAM31Version.Number {
log.Debug("SAM version < 3.1, FromPort not applicable")
return ""
}
if f.InFromPort != "0" {
log.WithField("fromPort", f.InFromPort).Debug("FromPort set")
return fmt.Sprintf(" FROM_PORT=%s ", f.InFromPort)
}
log.Debug("FromPort not set")
return ""
}
// ToPort returns the to port setting in the form of "TO_PORT=port"
func (f *SessionOptions) ToPort() string {
if f.samMax() < common.SAM31Version.Number {
log.Debug("SAM version < 3.1, ToPort not applicable")
return ""
}
if f.OutToPort != "0" {
log.WithField("toPort", f.OutToPort).Debug("ToPort set")
return fmt.Sprintf(" TO_PORT=%s ", f.OutToPort)
}
log.Debug("ToPort not set")
return ""
}
// SessionStyle returns the session style setting in the form of "STYLE=style"
func (f *SessionOptions) SessionStyle() string {
if f.Style != "" {
log.WithField("style", f.Style).Debug("Session style set")
return fmt.Sprintf(" STYLE=%s ", f.Style)
}
log.Debug("Using default STREAM style")
return " STYLE=STREAM "
}

95
config/transport.go Normal file
View File

@ -0,0 +1,95 @@
package config
import (
"fmt"
"strconv"
"time"
"github.com/sirupsen/logrus"
)
func boolToStr(b bool) string {
if b {
return "true"
}
return "false"
}
// Add transport options
type TransportOptions struct {
UseCompression string
FastReceive string
MessageReliability string
CloseIdleTimeout time.Duration
CloseIdle bool
ReduceIdleTimeout time.Duration
ReduceIdle bool
ReduceIdleQuantity int
}
func (f *TransportOptions) ReduceOnIdle() string {
return boolToStr(f.ReduceIdle)
}
func (f *TransportOptions) ReduceQuantity() string {
return strconv.Itoa(f.ReduceIdleQuantity)
}
func (f *TransportOptions) CloseOnIdle() string {
return boolToStr(f.CloseIdle)
}
func (f *TransportOptions) DoFastReceive() string {
if f.FastReceive == "true" {
log.Debug("Fast receive enabled")
return " i2cp.fastReceive=true "
}
log.Debug("Fast receive disabled")
return ""
}
// Reliability returns the message reliability setting in the form of "i2cp.messageReliability=reliability"
func (f *TransportOptions) Reliability() string {
if f.MessageReliability != "" {
log.WithField("reliability", f.MessageReliability).Debug("Message reliability set")
return fmt.Sprintf(" i2cp.messageReliability=%s ", f.MessageReliability)
}
log.Debug("Message reliability not set")
return ""
}
// Reduce returns the reduce idle settings in the form of "i2cp.reduceOnIdle=true i2cp.reduceIdleTime=time i2cp.reduceQuantity=quantity"
func (f *TransportOptions) Reduce() string {
if f.ReduceIdle {
log.WithFields(logrus.Fields{
"reduceIdle": f.ReduceIdle,
"reduceIdleTime": f.ReduceIdleTimeout.String(),
"reduceIdleQuantity": f.ReduceIdleQuantity,
}).Debug("Reduce idle settings applied")
return fmt.Sprintf(" i2cp.reduceOnIdle=%s i2cp.reduceIdleTime=%s i2cp.reduceQuantity=%d ", f.ReduceOnIdle(), f.ReduceIdleTimeout.String(), f.ReduceIdleQuantity)
}
log.Debug("Reduce idle settings not applied")
return ""
}
// Close returns the close idle settings in the form of "i2cp.closeOnIdle=true i2cp.closeIdleTime=time"
func (f *TransportOptions) Close() string {
if f.CloseIdle {
log.WithFields(logrus.Fields{
"closeIdle": f.CloseIdle,
"closeIdleTime": f.CloseIdleTimeout.String(),
}).Debug("Close idle settings applied")
return fmt.Sprintf(" i2cp.closeOnIdle=%s i2cp.closeIdleTime=%s ", f.CloseOnIdle(), f.CloseIdleTimeout.String())
}
log.Debug("Close idle settings not applied")
return ""
}
func (f *TransportOptions) UsesCompression() string {
if f.UseCompression == "true" {
log.Debug("Compression enabled")
return " i2cp.useCompression=true "
}
log.Debug("Compression disabled")
return ""
}

69
config/tunnel.go Normal file
View File

@ -0,0 +1,69 @@
package config
import (
"fmt"
"strconv"
)
type TunnelOptions struct {
InAllowZeroHop bool
OutAllowZeroHop bool
InLength int
OutLength int
InQuantity int
OutQuantity int
InVariance int
OutVariance int
InBackupQuantity int
OutBackupQuantity int
}
func (f *TunnelOptions) InboundDoZero() string {
val := boolToStr(f.InAllowZeroHop)
return fmt.Sprintf(" inbound.allowZeroHop=%s ", val)
}
func (f *TunnelOptions) OutboundDoZero() string {
val := boolToStr(f.OutAllowZeroHop)
return fmt.Sprintf(" outbound.allowZeroHop=%s ", val)
}
func (f *TunnelOptions) InboundLength() string {
val := strconv.Itoa(f.InLength)
return fmt.Sprintf(" inbound.length=%s ", val)
}
func (f *TunnelOptions) OutboundLength() string {
val := strconv.Itoa(f.OutLength)
return fmt.Sprintf(" outbound.length=%s ", val)
}
func (f *TunnelOptions) InboundQuantity() string {
val := strconv.Itoa(f.InQuantity)
return fmt.Sprintf(" inbound.quantity=%s ", val)
}
func (f *TunnelOptions) OutboundQuantity() string {
val := strconv.Itoa(f.OutQuantity)
return fmt.Sprintf(" outbound.quantity=%s ", val)
}
func (f *TunnelOptions) InboundVariance() string {
val := strconv.Itoa(f.InVariance)
return fmt.Sprintf(" inbound.variance=%s ", val)
}
func (f *TunnelOptions) OutboundVariance() string {
val := strconv.Itoa(f.OutVariance)
return fmt.Sprintf(" outbound.variance=%s ", val)
}
func (f *TunnelOptions) InboundBackupQuantity() string {
val := strconv.Itoa(f.InBackupQuantity)
return fmt.Sprintf(" inbound.backupQuantity=%s ", val)
}
func (f *TunnelOptions) OutboundBackupQuantity() string {
val := strconv.Itoa(f.OutBackupQuantity)
return fmt.Sprintf(" outbound.backupQuantity=%s ", val)
}

View File

@ -3,12 +3,15 @@ package sam3
import (
"bytes"
"errors"
"github.com/sirupsen/logrus"
"fmt"
"net"
"strconv"
"sync"
"time"
"github.com/sirupsen/logrus"
"github.com/go-i2p/i2pkeys"
"github.com/go-i2p/sam3/common"
)
// The DatagramSession implements net.PacketConn. It works almost like ordinary
@ -19,56 +22,38 @@ type DatagramSession struct {
samAddr string // address to the sam bridge (ipv4:port)
id string // tunnel name
conn net.Conn // connection to sam bridge
udpconn *net.UDPConn // used to deliver datagrams
keys i2pkeys.I2PKeys // i2p destination keys
rUDPAddr *net.UDPAddr // the SAM bridge UDP-port
remoteAddr *i2pkeys.I2PAddr // optional remote I2P address
common.UDPSession
*DatagramOptions
}
// 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) {
func (s *SAM) NewDatagramSession(id string, keys i2pkeys.I2PKeys, options []string, udpPort int, datagramOptions ...DatagramOptions) (*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")
udpSessionConfig := &common.UDPSessionConfig{
Port: udpPort,
ParentConn: s.conn,
Log: log,
DefaultPort: 7655,
AllowZeroPort: true,
// Add required session parameters
Style: "DATAGRAM",
FromPort: "0", // Allow dynamic port assignment
ToPort: "0",
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
}
if udpPort == 0 {
udpPort = 7655
log.Debug("Using default UDP port 7655")
}
lhost, _, err := SplitHostPort(s.conn.LocalAddr().String())
udpconn, err := common.NewUDPSession(udpSessionConfig)
if err != nil {
log.WithError(err).Error("Failed to split local host port")
s.Close()
log.WithError(err).Error("Failed to create UDP session")
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 := 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())
_, lport, err := net.SplitHostPort(udpconn.Conn.LocalAddr().String())
if err != nil {
log.WithError(err).Error("Failed to get local port")
s.Close()
@ -79,9 +64,25 @@ func (s *SAM) NewDatagramSession(id string, keys i2pkeys.I2PKeys, options []stri
log.WithError(err).Error("Failed to create generic session")
return nil, err
}
if len(datagramOptions) > 0 {
return &DatagramSession{
samAddr: s.address,
id: id,
conn: conn,
keys: keys,
UDPSession: *udpconn,
DatagramOptions: &datagramOptions[0],
}, nil
}
log.WithField("id", id).Info("DatagramSession created successfully")
return &DatagramSession{s.address, id, conn, udpconn, keys, rUDPAddr, nil}, nil
// return &DatagramSession{s.address, id, conn, udpconn, keys, rUDPAddr, nil, nil}, nil
return &DatagramSession{
samAddr: s.address,
id: id,
conn: conn,
keys: keys,
UDPSession: *udpconn,
}, nil
}
func (s *DatagramSession) B32() string {
@ -90,7 +91,7 @@ func (s *DatagramSession) B32() string {
return b32
}
func (s *DatagramSession) Dial(net string, addr string) (*DatagramSession, error) {
func (s *DatagramSession) Dial(net, addr string) (*DatagramSession, error) {
log.WithFields(logrus.Fields{
"net": net,
"addr": addr,
@ -141,23 +142,29 @@ func (s *DatagramSession) RemoteAddr() net.Addr {
// 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)
// Use sync.Pool for buffers
bufPool := sync.Pool{
New: func() interface{} {
return make([]byte, len(b)+4096)
},
}
buf := bufPool.Get().([]byte)
defer bufPool.Put(buf)
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)
n, saddr, err = s.UDPSession.Conn.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.rUDPAddr.IP) {
if bytes.Equal(saddr.IP, s.UDPSession.RemoteAddr.IP) {
continue
}
break
}
i := bytes.IndexByte(buf, byte('\n'))
i := bytes.IndexByte(buf, byte(' '))
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.")
@ -189,6 +196,11 @@ func (s *DatagramSession) Read(b []byte) (n int, err error) {
return rint, rerr
}
const (
MAX_DATAGRAM_SIZE = 31744 // Max reliable size
RECOMMENDED_SIZE = 11264 // 11KB recommended max
)
// 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.
@ -197,9 +209,23 @@ func (s *DatagramSession) WriteTo(b []byte, addr net.Addr) (n int, err error) {
"addr": addr,
"datagramLen": len(b),
}).Debug("Writing datagram")
header := []byte("3.1 " + s.id + " " + addr.String() + "\n")
if len(b) > MAX_DATAGRAM_SIZE {
return 0, errors.New("datagram exceeds maximum size")
}
// Use chunking for anything above recommended size
if len(b) > RECOMMENDED_SIZE {
return s.writeChunked(b, addr)
}
// Single message path
if s.DatagramOptions != nil {
return s.writeToWithOptions(b, addr.(i2pkeys.I2PAddr))
}
header := []byte(fmt.Sprintf("3.1 %s %s\n", s.id, addr.(i2pkeys.I2PAddr).String()))
msg := append(header, b...)
n, err = s.udpconn.WriteToUDP(msg, s.rUDPAddr)
n, err = s.UDPSession.Conn.WriteToUDP(msg, s.UDPSession.RemoteAddr)
if err != nil {
log.WithError(err).Error("Failed to write to UDP")
} else {
@ -208,6 +234,75 @@ func (s *DatagramSession) WriteTo(b []byte, addr net.Addr) (n int, err error) {
return n, err
}
func (s *DatagramSession) writeChunked(b []byte, addr net.Addr) (total int, err error) {
chunkSize := RECOMMENDED_SIZE - 256 // Allow for header overhead
chunks := (len(b) + chunkSize - 1) / chunkSize
log.WithFields(logrus.Fields{
"totalSize": len(b),
"chunks": chunks,
}).Debug("Splitting datagram into chunks")
for i := 0; i < chunks; i++ {
start := i * chunkSize
end := start + chunkSize
if end > len(b) {
end = len(b)
}
chunk := b[start:end]
var n int
// Single write path that handles both cases
if s.DatagramOptions != nil {
n, err = s.writeToWithOptions(chunk, addr.(i2pkeys.I2PAddr))
} else {
header := []byte(fmt.Sprintf("3.1 %s %s %d %d\n", s.id, addr.(i2pkeys.I2PAddr).String(), i, chunks))
msg := append(header, chunk...)
n, err = s.UDPSession.Conn.WriteToUDP(msg, s.UDPSession.RemoteAddr)
}
if err != nil {
return total, fmt.Errorf("chunk %d/%d failed: %w", i+1, chunks, err)
}
total += n
if i < chunks-1 {
time.Sleep(50 * time.Millisecond)
}
}
return total, nil
}
type DatagramOptions struct {
SendTags int
TagThreshold int
Expires int
SendLeaseset bool
}
func (s *DatagramSession) writeToWithOptions(b []byte, addr i2pkeys.I2PAddr) (n int, err error) {
header := []byte(fmt.Sprintf("3.3 %s %s", s.id, addr.String()))
if s.DatagramOptions != nil {
if s.DatagramOptions.SendTags > 0 {
header = append(header, []byte(fmt.Sprintf(" SEND_TAGS=%d", s.DatagramOptions.SendTags))...)
}
if s.DatagramOptions.TagThreshold > 0 {
header = append(header, []byte(fmt.Sprintf(" TAG_THRESHOLD=%d", s.DatagramOptions.TagThreshold))...)
}
if s.DatagramOptions.Expires > 0 {
header = append(header, []byte(fmt.Sprintf(" EXPIRES=%d", s.DatagramOptions.Expires))...)
}
if s.DatagramOptions.SendLeaseset {
header = append(header, []byte(" SEND_LEASESET=true")...)
}
}
header = append(header, '\n')
msg := append(header, b...)
return s.UDPSession.Conn.WriteToUDP(msg, s.UDPSession.RemoteAddr)
}
func (s *DatagramSession) Write(b []byte) (int, error) {
log.WithField("dataLen", len(b)).Debug("Writing to DatagramSession")
return s.WriteTo(b, s.remoteAddr)
@ -217,7 +312,7 @@ func (s *DatagramSession) Write(b []byte) (int, error) {
func (s *DatagramSession) Close() error {
log.Debug("Closing DatagramSession")
err := s.conn.Close()
err2 := s.udpconn.Close()
err2 := s.UDPSession.Conn.Close()
if err != nil {
log.WithError(err).Error("Failed to close connection")
return err
@ -261,22 +356,22 @@ func (s *DatagramSession) Lookup(name string) (a net.Addr, err error) {
// is seldom done.
func (s *DatagramSession) SetDeadline(t time.Time) error {
log.WithField("deadline", t).Debug("Setting deadline")
return s.udpconn.SetDeadline(t)
return s.UDPSession.Conn.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)
return s.UDPSession.Conn.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)
return s.UDPSession.Conn.SetWriteDeadline(t)
}
func (s *DatagramSession) SetWriteBuffer(bytes int) error {
log.WithField("bytes", bytes).Debug("Setting write buffer")
return s.udpconn.SetWriteBuffer(bytes)
return s.UDPSession.Conn.SetWriteBuffer(bytes)
}

View File

@ -4,6 +4,8 @@ import (
"fmt"
"testing"
"time"
sam3opts "github.com/go-i2p/sam3/opts"
)
func Test_DatagramServerClient(t *testing.T) {
@ -102,7 +104,7 @@ func ExampleDatagramSession() {
myself := keys.Addr()
// See the example Option_* variables.
dg, err := sam.NewDatagramSession("DGTUN", keys, Options_Small, 0)
dg, err := sam.NewDatagramSession("DGTUN", keys, sam3opts.Options_Small, 0)
if err != nil {
fmt.Println(err.Error())
return
@ -127,7 +129,7 @@ func ExampleDatagramSession() {
return
// Output:
//Got message: Hello myself!
// Got message: Hello myself!
}
func ExampleMiniDatagramSession() {
@ -148,7 +150,7 @@ func ExampleMiniDatagramSession() {
myself := keys.Addr()
// See the example Option_* variables.
dg, err := sam.NewDatagramSession("MINIDGTUN", keys, Options_Small, 0)
dg, err := sam.NewDatagramSession("MINIDGTUN", keys, sam3opts.Options_Small, 0)
if err != nil {
fmt.Println(err.Error())
return
@ -178,5 +180,5 @@ func ExampleMiniDatagramSession() {
return
// Output:
//Got message: Hello myself!
// Got message: Hello myself!
}

View File

@ -2,9 +2,11 @@ package sam3
import (
"fmt"
"github.com/sirupsen/logrus"
"strconv"
"strings"
"time"
"github.com/sirupsen/logrus"
)
// Option is a SAMEmit Option
@ -81,7 +83,7 @@ func SetSAMPort(s string) func(*SAMEmit) error {
// 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
c.I2PConfig.SessionOptions.NickName = s
log.WithField("name", s).Debug("Set tunnel name")
return nil
}
@ -91,7 +93,7 @@ func SetName(s string) func(*SAMEmit) error {
func SetInLength(u int) func(*SAMEmit) error {
return func(c *SAMEmit) error {
if u < 7 && u >= 0 {
c.I2PConfig.InLength = strconv.Itoa(u)
c.I2PConfig.InLength = u
log.WithField("inLength", u).Debug("Set inbound tunnel length")
return nil
}
@ -104,7 +106,7 @@ func SetInLength(u int) func(*SAMEmit) error {
func SetOutLength(u int) func(*SAMEmit) error {
return func(c *SAMEmit) error {
if u < 7 && u >= 0 {
c.I2PConfig.OutLength = strconv.Itoa(u)
c.I2PConfig.OutLength = u
log.WithField("outLength", u).Debug("Set outbound tunnel length")
return nil
}
@ -117,7 +119,7 @@ func SetOutLength(u int) func(*SAMEmit) error {
func SetInVariance(i int) func(*SAMEmit) error {
return func(c *SAMEmit) error {
if i < 7 && i > -7 {
c.I2PConfig.InVariance = strconv.Itoa(i)
c.I2PConfig.InVariance = i
log.WithField("inVariance", i).Debug("Set inbound tunnel variance")
return nil
}
@ -130,7 +132,7 @@ func SetInVariance(i int) func(*SAMEmit) error {
func SetOutVariance(i int) func(*SAMEmit) error {
return func(c *SAMEmit) error {
if i < 7 && i > -7 {
c.I2PConfig.OutVariance = strconv.Itoa(i)
c.I2PConfig.OutVariance = i
log.WithField("outVariance", i).Debug("Set outbound tunnel variance")
return nil
}
@ -143,7 +145,7 @@ func SetOutVariance(i int) func(*SAMEmit) error {
func SetInQuantity(u int) func(*SAMEmit) error {
return func(c *SAMEmit) error {
if u <= 16 && u > 0 {
c.I2PConfig.InQuantity = strconv.Itoa(u)
c.I2PConfig.InQuantity = u
log.WithField("inQuantity", u).Debug("Set inbound tunnel quantity")
return nil
}
@ -156,7 +158,7 @@ func SetInQuantity(u int) func(*SAMEmit) error {
func SetOutQuantity(u int) func(*SAMEmit) error {
return func(c *SAMEmit) error {
if u <= 16 && u > 0 {
c.I2PConfig.OutQuantity = strconv.Itoa(u)
c.I2PConfig.OutQuantity = u
log.WithField("outQuantity", u).Debug("Set outbound tunnel quantity")
return nil
}
@ -169,7 +171,7 @@ func SetOutQuantity(u int) func(*SAMEmit) error {
func SetInBackups(u int) func(*SAMEmit) error {
return func(c *SAMEmit) error {
if u < 6 && u >= 0 {
c.I2PConfig.InBackupQuantity = strconv.Itoa(u)
c.I2PConfig.InBackupQuantity = u
log.WithField("inBackups", u).Debug("Set inbound tunnel backups")
return nil
}
@ -182,7 +184,7 @@ func SetInBackups(u int) func(*SAMEmit) error {
func SetOutBackups(u int) func(*SAMEmit) error {
return func(c *SAMEmit) error {
if u < 6 && u >= 0 {
c.I2PConfig.OutBackupQuantity = strconv.Itoa(u)
c.I2PConfig.OutBackupQuantity = u
log.WithField("outBackups", u).Debug("Set outbound tunnel backups")
return nil
}
@ -194,11 +196,7 @@ func SetOutBackups(u int) func(*SAMEmit) error {
// SetEncrypt tells the router to use an encrypted leaseset
func SetEncrypt(b bool) func(*SAMEmit) error {
return func(c *SAMEmit) error {
if b {
c.I2PConfig.EncryptLeaseSet = "true"
return nil
}
c.I2PConfig.EncryptLeaseSet = "false"
c.I2PConfig.EncryptLeaseSet = b
log.WithField("encrypt", b).Debug("Set lease set encryption")
return nil
}
@ -234,7 +232,7 @@ func SetLeaseSetPrivateSigningKey(s string) func(*SAMEmit) error {
// 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
c.I2PConfig.TransportOptions.MessageReliability = s
log.WithField("messageReliability", s).Debug("Set message reliability")
return nil
}
@ -243,11 +241,7 @@ func SetMessageReliability(s string) func(*SAMEmit) error {
// 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"
c.I2PConfig.InAllowZeroHop = b
log.WithField("allowZeroIn", b).Debug("Set allow zero-hop inbound")
return nil
}
@ -256,11 +250,7 @@ func SetAllowZeroIn(b bool) func(*SAMEmit) error {
// 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"
c.I2PConfig.OutAllowZeroHop = b
log.WithField("allowZeroOut", b).Debug("Set allow zero-hop outbound")
return nil
}
@ -283,10 +273,10 @@ func SetCompress(b bool) func(*SAMEmit) error {
func SetFastRecieve(b bool) func(*SAMEmit) error {
return func(c *SAMEmit) error {
if b {
c.I2PConfig.FastRecieve = "true"
c.I2PConfig.TransportOptions.FastReceive = "true"
return nil
}
c.I2PConfig.FastRecieve = "false"
c.I2PConfig.TransportOptions.FastReceive = "false"
log.WithField("fastReceive", b).Debug("Set fast receive")
return nil
}
@ -295,11 +285,7 @@ func SetFastRecieve(b bool) func(*SAMEmit) error {
// 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"
c.I2PConfig.ReduceIdle = b
log.WithField("reduceIdle", b).Debug("Set reduce idle")
return nil
}
@ -308,11 +294,14 @@ func SetReduceIdle(b bool) func(*SAMEmit) error {
// 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"
c.I2PConfig.TransportOptions.ReduceIdleTimeout = 300000
if u >= 6 {
idleTime := strconv.Itoa((u * 60) * 1000)
c.I2PConfig.ReduceIdleTime = idleTime
log.WithField("reduceIdleTime", idleTime).Debug("Set reduce idle time")
idleTime := (u * 60) * 1000
c.I2PConfig.TransportOptions.ReduceIdleTimeout = time.Duration(idleTime)
log.WithFields(logrus.Fields{
"minutes": u,
"milliseconds": idleTime,
}).Debug("Set reduce idle time")
return nil
}
log.WithField("minutes", u).Error("Invalid reduce idle timeout")
@ -323,13 +312,12 @@ func SetReduceIdleTime(u int) func(*SAMEmit) error {
// 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"
c.I2PConfig.TransportOptions.ReduceIdleTimeout = 300000
if u >= 300000 {
c.I2PConfig.ReduceIdleTime = strconv.Itoa(u)
c.I2PConfig.TransportOptions.ReduceIdleTimeout = time.Duration(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)
}
}
@ -338,7 +326,7 @@ func SetReduceIdleTimeMs(u int) func(*SAMEmit) error {
func SetReduceIdleQuantity(u int) func(*SAMEmit) error {
return func(c *SAMEmit) error {
if u < 5 {
c.I2PConfig.ReduceIdleQuantity = strconv.Itoa(u)
c.I2PConfig.ReduceIdleQuantity = u
log.WithField("reduceIdleQuantity", u).Debug("Set reduce idle quantity")
return nil
}
@ -350,11 +338,7 @@ func SetReduceIdleQuantity(u int) func(*SAMEmit) error {
// 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"
c.I2PConfig.CloseIdle = b
return nil
}
}
@ -362,10 +346,10 @@ func SetCloseIdle(b bool) func(*SAMEmit) error {
// 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"
c.I2PConfig.TransportOptions.CloseIdleTimeout = 300000
if u >= 6 {
idleTime := strconv.Itoa((u * 60) * 1000)
c.I2PConfig.CloseIdleTime = idleTime
idleTime := (u * 60) * 1000
c.I2PConfig.TransportOptions.CloseIdleTimeout = time.Duration(idleTime)
log.WithFields(logrus.Fields{
"minutes": u,
"milliseconds": idleTime,
@ -380,9 +364,9 @@ func SetCloseIdleTime(u int) func(*SAMEmit) error {
// 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"
c.I2PConfig.TransportOptions.CloseIdleTimeout = 300000
if u >= 300000 {
c.I2PConfig.CloseIdleTime = strconv.Itoa(u)
c.I2PConfig.TransportOptions.CloseIdleTimeout = time.Duration(u)
log.WithField("closeIdleTimeMs", u).Debug("Set close idle time in milliseconds")
return nil
}

24
emit.go
View File

@ -2,9 +2,10 @@ package sam3
import (
"fmt"
"github.com/sirupsen/logrus"
"net"
"strings"
"github.com/sirupsen/logrus"
)
type SAMEmit struct {
@ -47,27 +48,6 @@ 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.OptStr(), //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",

10
go.mod
View File

@ -1,9 +1,13 @@
module github.com/go-i2p/sam3
go 1.12
go 1.23.3
require (
github.com/go-i2p/i2pkeys v0.0.0-20241108200332-e4f5ccdff8c4
github.com/go-i2p/i2pkeys v0.33.92
github.com/go-i2p/logger v0.0.0-20241123010126-3050657e5d0c
github.com/sirupsen/logrus v1.9.3
golang.org/x/sys v0.27.0 // indirect
)
require golang.org/x/sys v0.27.0 // indirect
replace github.com/go-i2p/i2pkeys v0.33.92 => ../i2pkeys

5
go.sum
View File

@ -1,8 +1,8 @@
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.0.0-20241108200332-e4f5ccdff8c4 h1:LRjaRCzg1ieGKZjELlaIg06Fx04RHzQLsWMYp1H6PQ4=
github.com/go-i2p/i2pkeys v0.0.0-20241108200332-e4f5ccdff8c4/go.mod h1:m5TlHjPZrU5KbTd7Lr+I2rljyC6aJ88HdkeMQXV0U0E=
github.com/go-i2p/logger v0.0.0-20241123010126-3050657e5d0c h1:VTiECn3dFEmUlZjto+wOwJ7SSJTHPLyNprQMR5HzIMI=
github.com/go-i2p/logger v0.0.0-20241123010126-3050657e5d0c/go.mod h1:te7Zj3g3oMeIl8uBXAgO62UKmZ6m6kHRNg1Mm+X8Hzk=
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=
@ -13,7 +13,6 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
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=

View File

@ -1,130 +0,0 @@
package sam
import (
"io/ioutil"
"log"
"net"
"os"
"github.com/go-i2p/i2pkeys"
"github.com/go-i2p/sam3"
)
// HEY! If you're looking at this, there's a good chance that `github.com/go-i2p/onramp`
// is a better fit! Check it out.
func NetListener(name, samaddr, keyspath string) (net.Listener, error) {
return I2PListener(name, sam3.SAMDefaultAddr(samaddr), keyspath)
}
// I2PListener is a convenience function which takes a SAM tunnel name, a SAM address and a filename.
// If the file contains I2P keys, it will create a service using that address. If the file does not
// exist, keys will be generated and stored in that file.
func I2PListener(name, samaddr, keyspath string) (*sam3.StreamListener, error) {
log.Printf("Starting and registering I2P service, please wait a couple of minutes...")
listener, err := I2PStreamSession(name, sam3.SAMDefaultAddr(samaddr), keyspath)
if err != nil {
return nil, err
}
if keyspath != "" {
err = ioutil.WriteFile(keyspath+".i2p.public.txt", []byte(listener.Keys().Addr().Base32()), 0644)
if err != nil {
log.Fatalf("error storing I2P base32 address in adjacent text file, %s", err)
}
}
log.Printf("Listening on: %s", listener.Addr().Base32())
return listener.Listen()
}
// I2PStreamSession is a convenience function which returns a sam3.StreamSession instead
// of a sam3.StreamListener. It also takes care of setting a persisitent key on behalf
// of the user.
func I2PStreamSession(name, samaddr, keyspath string) (*sam3.StreamSession, error) {
log.Printf("Starting and registering I2P session...")
sam, err := sam3.NewSAM(sam3.SAMDefaultAddr(samaddr))
if err != nil {
log.Fatalf("error connecting to SAM to %s: %s", sam3.SAMDefaultAddr(samaddr), err)
}
keys, err := GenerateOrLoadKeys(keyspath, sam)
if err != nil {
return nil, err
}
stream, err := sam.NewStreamSession(name, *keys, sam3.Options_Medium)
return stream, err
}
// I2PDataGramsession is a convenience function which returns a sam3.DatagramSession.
// It also takes care of setting a persisitent key on behalf of the user.
func I2PDatagramSession(name, samaddr, keyspath string) (*sam3.DatagramSession, error) {
log.Printf("Starting and registering I2P session...")
sam, err := sam3.NewSAM(sam3.SAMDefaultAddr(samaddr))
if err != nil {
log.Fatalf("error connecting to SAM to %s: %s", sam3.SAMDefaultAddr(samaddr), err)
}
keys, err := GenerateOrLoadKeys(keyspath, sam)
if err != nil {
return nil, err
}
gram, err := sam.NewDatagramSession(name, *keys, sam3.Options_Medium, 0)
return gram, err
}
// I2PPrimarySession is a convenience function which returns a sam3.PrimarySession.
// It also takes care of setting a persisitent key on behalf of the user.
func I2PPrimarySession(name, samaddr, keyspath string) (*sam3.PrimarySession, error) {
log.Printf("Starting and registering I2P session...")
sam, err := sam3.NewSAM(sam3.SAMDefaultAddr(samaddr))
if err != nil {
log.Fatalf("error connecting to SAM to %s: %s", sam3.SAMDefaultAddr(samaddr), err)
}
keys, err := GenerateOrLoadKeys(keyspath, sam)
if err != nil {
return nil, err
}
gram, err := sam.NewPrimarySession(name, *keys, sam3.Options_Medium)
return gram, err
}
// GenerateOrLoadKeys is a convenience function which takes a filename and a SAM session.
// if the SAM session is nil, a new one will be created with the defaults.
// The keyspath must be the path to a place to store I2P keys. The keyspath will be suffixed with
// .i2p.private for the private keys, and public.txt for the b32 addresses.
// If the keyspath.i2p.private file does not exist, keys will be generated and stored in that file.
// if the keyspath.i2p.private does exist, keys will be loaded from that location and returned
func GenerateOrLoadKeys(keyspath string, sam *sam3.SAM) (keys *i2pkeys.I2PKeys, err error) {
if sam == nil {
sam, err = sam3.NewSAM(sam3.SAMDefaultAddr("127.0.0.1:7656"))
if err != nil {
return nil, err
}
}
if _, err := os.Stat(keyspath + ".i2p.private"); os.IsNotExist(err) {
f, err := os.Create(keyspath + ".i2p.private")
if err != nil {
log.Fatalf("unable to open I2P keyfile for writing: %s", err)
}
defer f.Close()
tkeys, err := sam.NewKeys()
if err != nil {
log.Fatalf("unable to generate I2P Keys, %s", err)
}
keys = &tkeys
err = i2pkeys.StoreKeysIncompat(*keys, f)
if err != nil {
log.Fatalf("unable to save newly generated I2P Keys, %s", err)
}
} else {
tkeys, err := i2pkeys.LoadKeys(keyspath + ".i2p.private")
if err != nil {
log.Fatalf("unable to load I2P Keys: %e", err)
}
keys = &tkeys
}
return keys, nil
}
// GenerateKeys is a shorter version of GenerateOrLoadKeys which generates keys and stores them in a file.
// it always uses a new default SAM session.
func GenerateKeys(keyspath string) (keys *i2pkeys.I2PKeys, err error) {
return GenerateOrLoadKeys(keyspath, nil)
}

49
log.go
View File

@ -1,50 +1,5 @@
package sam3
import (
"github.com/sirupsen/logrus"
"io/ioutil"
"os"
"strings"
"sync"
)
import logger "github.com/go-i2p/sam3/log"
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()
}
var log = logger.GetSAM3Logger()

123
opts/suggestedOptions.go Normal file
View File

@ -0,0 +1,123 @@
package sam3opts
import (
"net"
"os"
"strings"
logger "github.com/go-i2p/sam3/log"
"github.com/sirupsen/logrus"
)
var log = logger.GetSAM3Logger()
// Examples and suggestions for options when creating sessions.
var (
// Suitable options if you are shuffling A LOT of traffic. If unused, this
// will waste your resources.
Options_Humongous = []string{
"inbound.length=3", "outbound.length=3",
"inbound.lengthVariance=1", "outbound.lengthVariance=1",
"inbound.backupQuantity=3", "outbound.backupQuantity=3",
"inbound.quantity=6", "outbound.quantity=6",
}
// Suitable for shuffling a lot of traffic.
Options_Large = []string{
"inbound.length=3", "outbound.length=3",
"inbound.lengthVariance=1", "outbound.lengthVariance=1",
"inbound.backupQuantity=1", "outbound.backupQuantity=1",
"inbound.quantity=4", "outbound.quantity=4",
}
// Suitable for shuffling a lot of traffic quickly with minimum
// anonymity. Uses 1 hop and multiple tunnels.
Options_Wide = []string{
"inbound.length=1", "outbound.length=1",
"inbound.lengthVariance=1", "outbound.lengthVariance=1",
"inbound.backupQuantity=2", "outbound.backupQuantity=2",
"inbound.quantity=3", "outbound.quantity=3",
}
// Suitable for shuffling medium amounts of traffic.
Options_Medium = []string{
"inbound.length=3", "outbound.length=3",
"inbound.lengthVariance=1", "outbound.lengthVariance=1",
"inbound.backupQuantity=0", "outbound.backupQuantity=0",
"inbound.quantity=2", "outbound.quantity=2",
}
// Sensible defaults for most people
Options_Default = []string{
"inbound.length=3", "outbound.length=3",
"inbound.lengthVariance=0", "outbound.lengthVariance=0",
"inbound.backupQuantity=1", "outbound.backupQuantity=1",
"inbound.quantity=1", "outbound.quantity=1",
}
// Suitable only for small dataflows, and very short lasting connections:
// You only have one tunnel in each direction, so if any of the nodes
// through which any of your two tunnels pass through go offline, there will
// be a complete halt in the dataflow, until a new tunnel is built.
Options_Small = []string{
"inbound.length=3", "outbound.length=3",
"inbound.lengthVariance=1", "outbound.lengthVariance=1",
"inbound.backupQuantity=0", "outbound.backupQuantity=0",
"inbound.quantity=1", "outbound.quantity=1",
}
// Does not use any anonymization, you connect directly to others tunnel
// endpoints, thus revealing your identity but not theirs. Use this only
// if you don't care.
Options_Warning_ZeroHop = []string{
"inbound.length=0", "outbound.length=0",
"inbound.lengthVariance=0", "outbound.lengthVariance=0",
"inbound.backupQuantity=0", "outbound.backupQuantity=0",
"inbound.quantity=2", "outbound.quantity=2",
}
)
func getEnv(key, fallback string) string {
logger.InitializeSAM3Logger()
value, ok := os.LookupEnv(key)
if !ok {
log.WithFields(logrus.Fields{
"key": key,
"fallback": fallback,
}).Debug("Environment variable not set, using fallback")
return fallback
}
log.WithFields(logrus.Fields{
"key": key,
"value": value,
}).Debug("Retrieved environment variable")
return value
}
var (
SAM_HOST = getEnv("sam_host", "127.0.0.1")
SAM_PORT = getEnv("sam_port", "7656")
)
func SAMDefaultAddr(fallforward string) string {
if fallforward == "" {
addr := net.JoinHostPort(SAM_HOST, SAM_PORT)
log.WithField("addr", addr).Debug("Using default SAM address")
return addr
}
log.WithField("addr", fallforward).Debug("Using fallforward SAM address")
return fallforward
}
func GenerateOptionString(opts []string) string {
optStr := strings.Join(opts, " ")
log.WithField("options", optStr).Debug("Generating option string")
if strings.Contains(optStr, "i2cp.leaseSetEncType") {
log.Debug("i2cp.leaseSetEncType already present in options")
return optStr
}
finalOpts := optStr + " i2cp.leaseSetEncType=4,0"
log.WithField("finalOptions", finalOpts).Debug("Added default i2cp.leaseSetEncType to options")
return finalOpts
// return optStr + " i2cp.leaseSetEncType=4,0"
}

View File

@ -3,14 +3,17 @@ package sam3
import (
"errors"
"fmt"
"github.com/sirupsen/logrus"
"math/rand"
"net"
"strconv"
"strings"
"sync"
"time"
"github.com/sirupsen/logrus"
"github.com/go-i2p/i2pkeys"
"github.com/go-i2p/sam3/common"
)
const (
@ -38,6 +41,7 @@ type PrimarySession struct {
Config SAMEmit
stsess map[string]*StreamSession
dgsess map[string]*DatagramSession
sync.RWMutex
// from string
// to string
}
@ -81,11 +85,11 @@ func (ss *PrimarySession) Keys() i2pkeys.I2PKeys {
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)
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)
return sam.DialTCPI2P(network, network+addr[0:4], addr)
}
log.WithField("network", network).Error("Invalid network type")
@ -95,32 +99,39 @@ func (sam *PrimarySession) Dial(network, addr string) (net.Conn, error) {
// 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")
sam.RLock()
ts, ok := sam.stsess[network+raddr.String()[0:4]]
var err error
sam.RUnlock()
if !ok {
ts, err = sam.NewUniqueStreamSubSession(network + raddr.String()[0:4])
sam.Lock()
ts, err := sam.NewUniqueStreamSubSession(network + raddr.String()[0:4])
if err != nil {
log.WithError(err).Error("Failed to create new unique stream sub-session")
sam.Unlock()
return nil, err
}
sam.stsess[network+raddr.String()[0:4]] = ts
ts, _ = sam.stsess[network+raddr.String()[0:4]]
sam.Unlock()
}
return ts.Dial(network, raddr.String())
}
func (sam *PrimarySession) DialTCPI2P(network string, laddr, raddr string) (net.Conn, error) {
func (sam *PrimarySession) DialTCPI2P(network, laddr, raddr string) (net.Conn, error) {
log.WithFields(logrus.Fields{"network": network, "laddr": laddr, "raddr": raddr}).Debug("DialTCPI2P() called")
sam.RLock()
ts, ok := sam.stsess[network+raddr[0:4]]
var err error
sam.RUnlock()
if !ok {
ts, err = sam.NewUniqueStreamSubSession(network + laddr)
sam.Lock()
ts, err := sam.NewUniqueStreamSubSession(network + laddr)
if err != nil {
log.WithError(err).Error("Failed to create new unique stream sub-session")
sam.Unlock()
return nil, err
}
sam.stsess[network+raddr[0:4]] = ts
ts, _ = sam.stsess[network+raddr[0:4]]
sam.Unlock()
}
return ts.Dial(network, raddr)
}
@ -128,32 +139,38 @@ func (sam *PrimarySession) DialTCPI2P(network string, laddr, raddr string) (net.
// 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")
sam.RLock()
ds, ok := sam.dgsess[network+raddr.String()[0:4]]
var err error
sam.RUnlock()
if !ok {
ds, err = sam.NewDatagramSubSession(network+raddr.String()[0:4], 0)
sam.Lock()
ds, err := sam.NewDatagramSubSession(network+raddr.String()[0:4], 0)
if err != nil {
log.WithError(err).Error("Failed to create new datagram sub-session")
sam.Unlock()
return nil, err
}
sam.dgsess[network+raddr.String()[0:4]] = ds
ds, _ = sam.dgsess[network+raddr.String()[0:4]]
sam.Unlock()
}
return ds.Dial(network, raddr.String())
}
func (sam *PrimarySession) DialUDPI2P(network, laddr, raddr string) (*DatagramSession, error) {
log.WithFields(logrus.Fields{"network": network, "laddr": laddr, "raddr": raddr}).Debug("DialUDPI2P() called")
sam.RLock()
ds, ok := sam.dgsess[network+raddr[0:4]]
var err error
sam.RUnlock()
if !ok {
ds, err = sam.NewDatagramSubSession(network+laddr, 0)
sam.Lock()
ds, err := sam.NewDatagramSubSession(network+laddr, 0)
if err != nil {
log.WithError(err).Error("Failed to create new datagram sub-session")
sam.Unlock()
return nil, err
}
sam.dgsess[network+raddr[0:4]] = ds
ds, _ = sam.dgsess[network+raddr[0:4]]
sam.Unlock()
}
return ds.Dial(network, raddr)
}
@ -194,7 +211,7 @@ func (sam *SAM) NewPrimarySession(id string, keys i2pkeys.I2PKeys, options []str
return sam.newPrimarySession(PrimarySessionSwitch, id, keys, options)
}
func (sam *SAM) newPrimarySession(primarySessionSwitch string, id string, keys i2pkeys.I2PKeys, options []string) (*PrimarySession, error) {
func (sam *SAM) newPrimarySession(primarySessionSwitch, id string, keys i2pkeys.I2PKeys, options []string) (*PrimarySession, error) {
log.WithFields(logrus.Fields{
"primarySessionSwitch": primarySessionSwitch,
"id": id,
@ -208,7 +225,19 @@ func (sam *SAM) newPrimarySession(primarySessionSwitch string, id string, keys i
}
ssesss := make(map[string]*StreamSession)
dsesss := make(map[string]*DatagramSession)
return &PrimarySession{sam.Config.I2PConfig.Sam(), id, conn, keys, time.Duration(600 * time.Second), time.Now(), Sig_NONE, sam.Config, ssesss, dsesss}, nil
return &PrimarySession{
samAddr: sam.SAMEmit.I2PConfig.Sam(),
id: id,
conn: conn,
keys: keys,
Timeout: time.Duration(600 * time.Second),
Deadline: time.Now(),
sigType: Sig_NONE,
Config: sam.SAMEmit,
stsess: ssesss,
dgsess: dsesss,
RWMutex: sync.RWMutex{},
}, nil
}
// Creates a new PrimarySession with the I2CP- and PRIMARYinglib options as
@ -227,7 +256,19 @@ func (sam *SAM) NewPrimarySessionWithSignature(id string, keys i2pkeys.I2PKeys,
}
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(), sigType, sam.Config, ssesss, dsesss}, nil
return &PrimarySession{
samAddr: sam.SAMEmit.I2PConfig.Sam(),
id: id,
conn: conn,
keys: keys,
Timeout: time.Duration(600 * time.Second),
Deadline: time.Now(),
sigType: sigType,
Config: sam.SAMEmit,
stsess: ssesss,
dgsess: dsesss,
RWMutex: sync.RWMutex{},
}, nil
}
// Creates a new session with the style of either "STREAM", "DATAGRAM" or "RAW",
@ -254,15 +295,7 @@ func (sam *PrimarySession) newGenericSubSessionWithSignatureAndPorts(style, id,
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")
scmsg := []byte(fmt.Sprintf("SESSION ADD STYLE=%s ID=%s FROM_PORT=%s TO_PORT=%s %s\n", style, id, from, to, strings.Join(extras, " ")))
log.WithField("message", string(scmsg)).Debug("Sending SESSION ADD message")
@ -289,7 +322,7 @@ func (sam *PrimarySession) newGenericSubSessionWithSignatureAndPorts(style, id,
}
text := string(buf[:n])
log.WithField("response", text).Debug("Received response from SAM")
//log.Println("SAM:", text)
// log.Println("SAM:", text)
if strings.HasPrefix(text, session_ADDOK) {
//if sam.keys.String() != text[len(session_ADDOK):len(text)-1] {
//conn.Close()
@ -343,7 +376,7 @@ func (sam *PrimarySession) NewUniqueStreamSubSession(id string) (*StreamSession,
}
fromPort, toPort := randport(), randport()
log.WithFields(logrus.Fields{"fromPort": fromPort, "toPort": toPort}).Debug("Generated random ports")
//return &StreamSession{sam.Config.I2PConfig.Sam(), id, conn, sam.keys, time.Duration(600 * time.Second), time.Now(), Sig_NONE, randport(), randport()}, nil
// return &StreamSession{sam.Config.I2PConfig.Sam(), id, conn, sam.keys, time.Duration(600 * time.Second), time.Now(), Sig_NONE, randport(), randport()}, nil
return &StreamSession{sam.Config.I2PConfig.Sam(), id, conn, sam.keys, time.Duration(600 * time.Second), time.Now(), Sig_NONE, fromPort, toPort}, nil
}
@ -371,44 +404,27 @@ func (s *PrimarySession) I2PListener(name string) (*StreamListener, error) {
// 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) (*DatagramSession, error) {
func (s *PrimarySession) NewDatagramSubSession(id string, udpPort int, datagramOptions ...DatagramOptions) (*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")
udpSessionConfig := &common.UDPSessionConfig{
Port: udpPort,
ParentConn: s.conn,
Log: log,
DefaultPort: 7655,
AllowZeroPort: true,
// Add required session parameters
Style: "DATAGRAM",
FromPort: "0", // Allow dynamic port assignment
ToPort: "0",
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
}
if udpPort == 0 {
udpPort = 7655
log.Debug("Using default UDP port 7655")
}
lhost, _, err := SplitHostPort(s.conn.LocalAddr().String())
udpConn, err := common.NewUDPSession(udpSessionConfig)
if err != nil {
log.WithError(err).Error("Failed to split local host port")
s.Close()
log.WithError(err).Error("Failed to create UDP session")
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 := 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())
_, lport, err := net.SplitHostPort(udpConn.Conn.LocalAddr().String())
if err != nil {
log.WithError(err).Error("Failed to get local port")
s.Close()
@ -419,9 +435,33 @@ func (s *PrimarySession) NewDatagramSubSession(id string, udpPort int) (*Datagra
log.WithError(err).Error("Failed to create new generic sub-session")
return nil, err
}
if len(datagramOptions) > 0 {
// return &DatagramSession{s.Config.I2PConfig.Sam(), id, conn, udpconn, s.keys, rUDPAddr, nil, &datagramOptions[0]}, nil
return &DatagramSession{
samAddr: s.Config.I2PConfig.Sam(),
id: id,
conn: conn,
keys: s.keys,
UDPSession: *udpConn,
DatagramOptions: &datagramOptions[0],
}, nil
}
opts := &DatagramOptions{
SendTags: 0,
TagThreshold: 0,
Expires: 0,
SendLeaseset: false,
}
log.WithFields(logrus.Fields{"id": id, "localPort": lport}).Debug("Created new datagram sub-session")
return &DatagramSession{s.Config.I2PConfig.Sam(), id, conn, udpconn, s.keys, rUDPAddr, nil}, nil
// return &DatagramSession{s.Config.I2PConfig.Sam(), id, conn, udpconn, s.keys, rUDPAddr, nil, opts}, nil
return &DatagramSession{
samAddr: s.Config.I2PConfig.Sam(),
id: id,
conn: conn,
keys: s.keys,
UDPSession: *udpConn,
DatagramOptions: opts,
}, nil
}
// Creates a new raw session. udpPort is the UDP port SAM is listening on,

View File

@ -144,5 +144,5 @@ func ExamplePrimaryDatagramSession() {
return
// Output:
//Got message: Hello myself!
// Got message: Hello myself!
}

View File

@ -41,14 +41,14 @@ func Test_PrimaryStreamingDial(t *testing.T) {
}
defer ss.Close()
fmt.Println("\tNotice: This may fail if your I2P node is not well integrated in the I2P network.")
fmt.Println("\tLooking up i2p-projekt.i2p")
forumAddr, err := earlysam.Lookup("i2p-projekt.i2p")
fmt.Println("\tLooking up idk.i2p")
forumAddr, err := ss.Lookup("idk.i2p")
if err != nil {
fmt.Println(err.Error())
t.Fail()
return
}
fmt.Println("\tDialing i2p-projekt.i2p(", forumAddr.Base32(), forumAddr.DestHash().Hash(), ")")
fmt.Println("\tDialing idk.i2p(", forumAddr.Base32(), forumAddr.DestHash().Hash(), ")")
conn, err := ss.DialI2P(forumAddr)
if err != nil {
fmt.Println(err.Error())
@ -65,9 +65,9 @@ func Test_PrimaryStreamingDial(t *testing.T) {
buf := make([]byte, 4096)
n, err := conn.Read(buf)
if !strings.Contains(strings.ToLower(string(buf[:n])), "http") && !strings.Contains(strings.ToLower(string(buf[:n])), "html") {
fmt.Printf("\tProbably failed to StreamSession.DialI2P(i2p-projekt.i2p)? It replied %d bytes, but nothing that looked like http/html", n)
fmt.Printf("\tProbably failed to StreamSession.DialI2P(idk.i2p)? It replied %d bytes, but nothing that looked like http/html", n)
} else {
fmt.Println("\tRead HTTP/HTML from i2p-projekt.i2p")
fmt.Println("\tRead HTTP/HTML from idk.i2p")
}
}
@ -190,7 +190,7 @@ func ExamplePrimaryStreamSession() {
return
}
defer sam.Close()
conn, err := sam.Dial("tcp", "idk.i2p") //someone.Base32())
conn, err := sam.Dial("tcp", "idk.i2p") // someone.Base32())
if err != nil {
fmt.Println(err.Error())
return
@ -211,8 +211,8 @@ func ExamplePrimaryStreamSession() {
log.Println("Read HTTP/HTML from idk.i2p")
}
// Output:
//Sending HTTP GET /
//Read HTTP/HTML from idk.i2p
// Sending HTTP GET /
// Read HTTP/HTML from idk.i2p
}
func ExamplePrimaryStreamListener() {
@ -253,7 +253,7 @@ func ExamplePrimaryStreamListener() {
return
}
defer l.Close()
//fmt.Println("Serving on primary listener", l.Addr().String())
// fmt.Println("Serving on primary listener", l.Addr().String())
if err := http.Serve(l, &exitHandler{}); err != nil {
fmt.Println(err.Error())
}
@ -281,7 +281,7 @@ func ExamplePrimaryStreamListener() {
Dial: sc.Dial,
},
}
//resp, err := client.Get("http://" + "idk.i2p") //ss.Addr().Base32())
// resp, err := client.Get("http://" + "idk.i2p") //ss.Addr().Base32())
resp, err := client.Get("http://" + ss.Addr().Base32())
if err != nil {
fmt.Println(err.Error())
@ -299,8 +299,7 @@ func ExamplePrimaryStreamListener() {
// Got response: Hello world!
}
type exitHandler struct {
}
type exitHandler struct{}
func (e *exitHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello world!"))

28
raw.go
View File

@ -1,13 +1,13 @@
package sam3
import (
"bytes"
"errors"
"github.com/sirupsen/logrus"
"net"
"strconv"
"time"
"github.com/sirupsen/logrus"
"github.com/go-i2p/i2pkeys"
)
@ -82,32 +82,32 @@ func (s *SAM) NewRawSession(id string, keys i2pkeys.I2PKeys, options []string, u
"remoteUDPAddr": rUDPAddr,
}).Debug("Created new RawSession")
return &RawSession{s.Config.I2PConfig.Sam(), id, conn, udpconn, keys, rUDPAddr}, nil
return &RawSession{s.SAMEmit.I2PConfig.Sam(), id, conn, udpconn, keys, rUDPAddr}, nil
}
// Reads one raw datagram sent to the destination of the DatagramSession. Returns
// Read one raw datagram sent to the destination of the DatagramSession. Returns
// the number of bytes read. Who sent the raw message can not be determined at
// this layer - you need to do it (in a secure way!).
func (s *RawSession) Read(b []byte) (n int, err error) {
log.Debug("Attempting to read raw datagram")
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(b)
if err != nil {
log.WithError(err).Error("Failed to read from UDP")
return 0, err
}
if bytes.Equal(saddr.IP, s.rUDPAddr.IP) {
log.WithField("senderIP", saddr.IP).Debug("Received datagram from SAM bridge IP")
continue
}
break
}
log.WithField("bytesRead", n).Debug("Successfully read raw datagram")
return n, nil
// Verify source is SAM bridge
if saddr.IP.Equal(s.rUDPAddr.IP) && saddr.Port == s.rUDPAddr.Port {
log.WithField("bytesRead", n).Debug("Successfully read raw datagram")
return n, nil
}
// Log unexpected source
log.Printf("Ignored datagram from unauthorized source: %v", saddr)
continue
}
}
// Sends one raw datagram to the destination specified. At the time of writing,

View File

@ -2,85 +2,123 @@ package sam3
import (
"bufio"
"bytes"
"errors"
"context"
"fmt"
"strings"
"time"
"github.com/go-i2p/i2pkeys"
)
// SAMResolver handles name resolution for I2P addresses
type SAMResolver struct {
*SAM
sam *SAM
}
// ResolveResult represents the possible outcomes of name resolution
type ResolveResult struct {
Address i2pkeys.I2PAddr
Error error
}
const (
defaultTimeout = 30 * time.Second
samReplyPrefix = "NAMING REPLY "
)
// NewSAMResolver creates a resolver from an existing SAM instance
func NewSAMResolver(parent *SAM) (*SAMResolver, error) {
log.Debug("Creating new SAMResolver from existing SAM instance")
var s SAMResolver
s.SAM = parent
return &s, nil
if parent == nil {
return nil, fmt.Errorf("parent SAM instance required")
}
return &SAMResolver{sam: parent}, nil
}
// NewFullSAMResolver creates a new resolver with its own SAM connection
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
sam, err := NewSAM(address)
if err != nil {
return nil, fmt.Errorf("creating SAM connection: %w", err)
}
return &SAMResolver{sam: sam}, 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 == "RESULT=OK" {
continue
} else if text == "RESULT=INVALID_KEY" {
errStr += "Invalid key - resolver."
log.Error("Invalid key in resolver")
} else if text == "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)
func (r *SAMResolver) Resolve(name string) (i2pkeys.I2PAddr, error) {
return r.ResolveWithContext(context.Background(), name)
}
// Resolve looks up an I2P address by name with context support
func (r *SAMResolver) ResolveWithContext(ctx context.Context, name string) (i2pkeys.I2PAddr, error) {
if name == "" {
return "", fmt.Errorf("name cannot be empty")
}
// Create query
query := fmt.Sprintf("NAMING LOOKUP NAME=%s\n", name)
// Set up timeout if context doesn't have one
if _, hasTimeout := ctx.Deadline(); !hasTimeout {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, defaultTimeout)
defer cancel()
}
// Write query with context awareness
if err := r.writeWithContext(ctx, query); err != nil {
return "", fmt.Errorf("writing query: %w", err)
}
// Read and parse response
return r.readResponse(ctx, name)
}
func (r *SAMResolver) writeWithContext(ctx context.Context, query string) error {
done := make(chan error, 1)
go func() {
_, err := r.sam.conn.Write([]byte(query))
done <- err
}()
select {
case err := <-done:
return err
case <-ctx.Done():
return ctx.Err()
}
}
func (r *SAMResolver) readResponse(ctx context.Context, name string) (i2pkeys.I2PAddr, error) {
reader := bufio.NewReader(r.sam.conn)
// Read first line
line, err := reader.ReadString('\n')
if err != nil {
return "", fmt.Errorf("reading response: %w", err)
}
if !strings.HasPrefix(line, samReplyPrefix) {
return "", fmt.Errorf("invalid response format")
}
// Parse response
fields := strings.Fields(strings.TrimPrefix(line, samReplyPrefix))
for _, field := range fields {
switch {
case field == "RESULT=OK":
continue
case field == "RESULT=INVALID_KEY":
return "", fmt.Errorf("invalid key")
case field == "RESULT=KEY_NOT_FOUND":
return "", fmt.Errorf("name not found: %s", name)
case field == "NAME="+name:
continue
case strings.HasPrefix(field, "VALUE="):
return i2pkeys.I2PAddr(strings.TrimPrefix(field, "VALUE=")), nil
case strings.HasPrefix(field, "MESSAGE="):
return "", fmt.Errorf("SAM error: %s", strings.TrimPrefix(field, "MESSAGE="))
}
}
return "", fmt.Errorf("unable to resolve %s", name)
}

133
sam3.go
View File

@ -4,32 +4,35 @@ package sam3
import (
"bufio"
"bytes"
"errors"
"fmt"
"github.com/sirupsen/logrus"
"io"
"math/rand"
"net"
"os"
"strings"
"github.com/go-i2p/i2pkeys"
"github.com/sirupsen/logrus"
. "github.com/go-i2p/i2pkeys"
"github.com/go-i2p/i2pkeys"
"github.com/go-i2p/sam3/common"
logger "github.com/go-i2p/sam3/log"
)
func init() {
InitializeSAM3Logger()
logger.InitializeSAM3Logger()
}
// Used for controlling I2Ps SAMv3.
// This implements the "Control Socket" for all connections.
type SAM struct {
address string
conn net.Conn
resolver *SAMResolver
Config SAMEmit
keys *i2pkeys.I2PKeys
sigType int
address string
conn net.Conn
keys *i2pkeys.I2PKeys
sigType int
formatter *common.SAMFormatter
version common.Version
SAMEmit
*SAMResolver
}
const (
@ -64,31 +67,38 @@ func RandString() string {
// Creates a new controller for the I2P routers SAM bridge.
func NewSAM(address string) (*SAM, error) {
log.WithField("address", address).Debug("Creating new SAM instance")
var s SAM
s := SAM{
address: address,
version: common.SAM31Version,
formatter: common.NewSAMFormatter(common.SAM31Version.String),
}
// TODO: clean this up
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.Config.HelloBytes()); err != nil {
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)
/*buf := make([]byte, 256)
n, err := conn.Read(buf)*/
reader := bufio.NewReader(conn)
response, err := reader.ReadString('\n')
if err != nil {
log.WithError(err).Error("Failed to read SAM response")
conn.Close()
return nil, fmt.Errorf("error reading onto buffer: %w", err)
return nil, fmt.Errorf("error reading SAM response: %w", err)
}
buf := []byte(response)
n := len(buf)
if strings.Contains(string(buf[:n]), "HELLO REPLY RESULT=OK") {
log.Debug("SAM hello successful")
s.Config.I2PConfig.SetSAMAddress(address)
s.SAMEmit.I2PConfig.SetSAMAddress(address)
s.conn = conn
//s.Config.I2PConfig.DestinationKeys = nil
s.resolver, err = NewSAMResolver(&s)
// s.Config.I2PConfig.DestinationKeys = nil
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)
@ -97,18 +107,18 @@ func NewSAM(address string) (*SAM, error) {
} else if string(buf[:n]) == "HELLO REPLY RESULT=NOVERSION\n" {
log.Error("SAM bridge does not support SAMv3")
conn.Close()
return nil, errors.New("That SAM bridge does not support SAMv3.")
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, errors.New(string(buf[:n]))
return nil, fmt.Errorf(string(buf[:n]))
}
}
func (sam *SAM) Keys() (k *i2pkeys.I2PKeys) {
//TODO: copy them?
// TODO: copy them?
log.Debug("Retrieving SAM keys")
k = &sam.Config.I2PConfig.DestinationKeys
k = &sam.SAMEmit.I2PConfig.DestinationKeys
return
}
@ -119,7 +129,7 @@ func (sam *SAM) ReadKeys(r io.Reader) (err error) {
keys, err = i2pkeys.LoadKeysIncompat(r)
if err == nil {
log.Debug("Keys loaded successfully")
sam.Config.I2PConfig.DestinationKeys = keys
sam.SAMEmit.I2PConfig.DestinationKeys = keys
}
log.WithError(err).Error("Failed to load keys")
return
@ -132,7 +142,7 @@ func (sam *SAM) EnsureKeyfile(fname string) (keys i2pkeys.I2PKeys, err error) {
// transient
keys, err = sam.NewKeys()
if err == nil {
sam.Config.I2PConfig.DestinationKeys = keys
sam.SAMEmit.I2PConfig.DestinationKeys = keys
log.WithFields(logrus.Fields{
"keys": keys,
}).Debug("Generated new transient keys")
@ -144,10 +154,10 @@ func (sam *SAM) EnsureKeyfile(fname string) (keys i2pkeys.I2PKeys, err error) {
// make the keys
keys, err = sam.NewKeys()
if err == nil {
sam.Config.I2PConfig.DestinationKeys = keys
sam.SAMEmit.I2PConfig.DestinationKeys = keys
// save keys
var f io.WriteCloser
f, err = os.OpenFile(fname, os.O_WRONLY|os.O_CREATE, 0600)
f, err = os.OpenFile(fname, os.O_WRONLY|os.O_CREATE, 0o600)
if err == nil {
err = i2pkeys.StoreKeysIncompat(keys, f)
f.Close()
@ -161,7 +171,7 @@ func (sam *SAM) EnsureKeyfile(fname string) (keys i2pkeys.I2PKeys, err error) {
if err == nil {
keys, err = i2pkeys.LoadKeysIncompat(f)
if err == nil {
sam.Config.I2PConfig.DestinationKeys = keys
sam.SAMEmit.I2PConfig.DestinationKeys = keys
log.Debug("Loaded existing keys from file")
}
}
@ -176,13 +186,19 @@ func (sam *SAM) EnsureKeyfile(fname string) (keys i2pkeys.I2PKeys, err error) {
// 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.
// Add constant for recommended sig type
const (
DEFAULT_SIG_TYPE = "SIGNATURE_TYPE=7" // EdDSA_SHA512_Ed25519
)
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 sigType == nil {
sigType = []string{DEFAULT_SIG_TYPE}
}
if _, err := sam.conn.Write([]byte("DEST GENERATE " + sigtmp + "\n")); err != nil {
scmsg := []byte(fmt.Sprintf("DEST GENERATE %s\n", sigType[0]))
if _, err := sam.conn.Write(scmsg); err != nil {
log.WithError(err).Error("Failed to write DEST GENERATE command")
return i2pkeys.I2PKeys{}, fmt.Errorf("error with writing in SAM: %w", err)
}
@ -208,18 +224,18 @@ func (sam *SAM) NewKeys(sigType ...string) (i2pkeys.I2PKeys, error) {
priv = text[5:]
} else {
log.Error("Failed to parse keys from SAM response")
return i2pkeys.I2PKeys{}, errors.New("Failed to parse keys.")
return i2pkeys.I2PKeys{}, fmt.Errorf("Failed to parse keys.")
}
}
log.Debug("Successfully generated new keys")
return NewKeys(I2PAddr(pub), priv), nil
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.resolver.Resolve(name)
return sam.SAMResolver.Resolve(name)
}
// Creates a new session with the style of either "STREAM", "DATAGRAM" or "RAW",
@ -227,12 +243,12 @@ func (sam *SAM) Lookup(name string) (i2pkeys.I2PAddr, error) {
// 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, options []string, extras []string) (net.Conn, error) {
func (sam *SAM) newGenericSession(style, id string, keys i2pkeys.I2PKeys, options, 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_NONE, options, extras)
}
func (sam *SAM) newGenericSessionWithSignature(style, id string, keys i2pkeys.I2PKeys, sigType string, options []string, extras []string) (net.Conn, error) {
func (sam *SAM) newGenericSessionWithSignature(style, id string, keys i2pkeys.I2PKeys, sigType string, options, 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, options, extras)
}
@ -242,29 +258,24 @@ func (sam *SAM) newGenericSessionWithSignature(style, id string, keys i2pkeys.I2
// 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, options []string, extras []string) (net.Conn, error) {
func (sam *SAM) newGenericSessionWithSignatureAndPorts(style, id, from, to string, keys i2pkeys.I2PKeys, sigType string, options, 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 := GenerateOptionString(options)
conn := sam.conn
fp := ""
tp := ""
if from != "0" {
fp = " FROM_PORT=" + from
scmsg := []byte(fmt.Sprintf("SESSION CREATE STYLE=%s FROM_PORT=%s TO_PORT=%s ID=%s DESTINATION=%s %s %s\n", style, from, to, id, keys.String(), optStr, strings.Join(extras, " ")))
if style == "PRIMARY" || style == "MASTER" {
scmsg = []byte(fmt.Sprintf("SESSION CREATE STYLE=%s ID=%s DESTINATION=%s %s %s\n", style, id, keys.String(), optStr, strings.Join(extras, " ")))
}
if to != "0" {
tp = " TO_PORT=" + to
}
scmsg := []byte("SESSION CREATE STYLE=" + style + fp + tp + " ID=" + id + " DESTINATION=" + keys.String() + " " + optStr + strings.Join(extras, " ") + "\n")
log.WithField("message", string(scmsg)).Debug("Sending SESSION CREATE message")
log.WithField("message", string(scmsg)).Debug("Sending SESSION CREATE message", string(scmsg))
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, errors.New("writing to SAM failed")
return nil, fmt.Errorf("writing to SAM failed")
}
n, err := conn.Write(scmsg[m:])
if err != nil {
@ -287,35 +298,45 @@ func (sam *SAM) newGenericSessionWithSignatureAndPorts(style, id, from, to strin
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, errors.New("SAMv3 created a tunnel with keys other than the ones we asked it for")
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, errors.New("Duplicate tunnel name")
return nil, fmt.Errorf("Duplicate tunnel name")
} else if text == session_DUPLICATE_DEST {
log.Error("Duplicate destination")
conn.Close()
return nil, errors.New("Duplicate destination")
return nil, fmt.Errorf("Duplicate destination")
} else if text == session_INVALID_KEY {
log.Error("Invalid key for SAM session")
conn.Close()
return nil, errors.New("Invalid key - SAM session")
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, errors.New("I2P error " + text[len(session_I2P_ERROR):])
return nil, fmt.Errorf("I2P error %s", text[len(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)
return nil, fmt.Errorf("Unable to parse SAMv3 reply: %s", text)
}
}
// close this sam session
// Close this sam session
func (sam *SAM) Close() error {
log.Debug("Closing SAM session")
return sam.conn.Close()
}
// CloseNotify the socket with a QUIT message
func (sam *SAM) CloseNotify() error {
log.Debug("Quitting SAM session")
_, err := sam.conn.Write([]byte("QUIT\n"))
if err != nil {
return fmt.Errorf("close notification failed: %v", err)
}
return nil
}

View File

@ -5,12 +5,14 @@ import (
"bytes"
"context"
"errors"
"github.com/sirupsen/logrus"
"fmt"
"io"
"net"
"strings"
"time"
"github.com/sirupsen/logrus"
"github.com/go-i2p/i2pkeys"
)
@ -97,7 +99,7 @@ func (sam *SAM) NewStreamSession(id string, keys i2pkeys.I2PKeys, options []stri
return nil, err
}
log.WithField("id", id).Debug("Created new StreamSession")
return &StreamSession{sam.Config.I2PConfig.Sam(), id, conn, keys, time.Duration(600 * time.Second), time.Now(), Sig_NONE, "0", "0"}, nil
return &StreamSession{sam.SAMEmit.I2PConfig.Sam(), id, conn, keys, time.Duration(600 * time.Second), time.Now(), Sig_NONE, "0", "0"}, nil
}
// Creates a new StreamSession with the I2CP- and streaminglib options as
@ -109,7 +111,7 @@ func (sam *SAM) NewStreamSessionWithSignature(id string, keys i2pkeys.I2PKeys, o
return nil, err
}
log.WithFields(logrus.Fields{"id": id, "sigType": sigType}).Debug("Created new StreamSession with signature")
return &StreamSession{sam.Config.I2PConfig.Sam(), id, conn, keys, time.Duration(600 * time.Second), time.Now(), sigType, "0", "0"}, nil
return &StreamSession{sam.SAMEmit.I2PConfig.Sam(), id, conn, keys, time.Duration(600 * time.Second), time.Now(), sigType, "0", "0"}, nil
}
// Creates a new StreamSession with the I2CP- and streaminglib options as
@ -121,7 +123,7 @@ func (sam *SAM) NewStreamSessionWithSignatureAndPorts(id, from, to string, keys
return nil, err
}
log.WithFields(logrus.Fields{"id": id, "from": from, "to": to, "sigType": sigType}).Debug("Created new StreamSession with signature and ports")
return &StreamSession{sam.Config.I2PConfig.Sam(), id, conn, keys, time.Duration(600 * time.Second), time.Now(), sigType, from, to}, nil
return &StreamSession{sam.SAMEmit.I2PConfig.Sam(), id, conn, keys, time.Duration(600 * time.Second), time.Now(), sigType, from, to}, nil
}
// lookup name, convenience function
@ -212,7 +214,7 @@ func (s *StreamSession) Dial(n, addr string) (c net.Conn, err error) {
var i2paddr i2pkeys.I2PAddr
var host string
host, _, err = SplitHostPort(addr)
//log.Println("Dialing:", host)
// log.Println("Dialing:", host)
if err = IgnorePortError(err); err == nil {
// check for name
if strings.HasSuffix(host, ".b32.i2p") || strings.HasSuffix(host, ".i2p") {
@ -222,8 +224,8 @@ func (s *StreamSession) Dial(n, addr string) (c net.Conn, err error) {
} else {
// probably a destination
i2paddr, err = i2pkeys.NewI2PAddrFromBytes([]byte(host))
//i2paddr = i2pkeys.I2PAddr(host)
//log.Println("Destination:", i2paddr, err)
// 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 {
@ -243,7 +245,12 @@ func (s *StreamSession) DialI2P(addr i2pkeys.I2PAddr) (*SAMConn, error) {
return nil, err
}
conn := sam.conn
_, err = conn.Write([]byte("STREAM CONNECT ID=" + s.id + " FROM_PORT=" + s.from + " TO_PORT=" + s.to + " DESTINATION=" + addr.Base64() + " SILENT=false\n"))
cmd := fmt.Sprintf("STREAM CONNECT ID=%s DESTINATION=%s FROM_PORT=%s TO_PORT=%s SILENT=false\n",
s.id,
addr.Base64(),
s.from,
s.to)
_, err = conn.Write([]byte(cmd))
if err != nil {
log.WithError(err).Error("Failed to write STREAM CONNECT command")
conn.Close()
@ -290,7 +297,7 @@ func (s *StreamSession) DialI2P(addr i2pkeys.I2PAddr) (*SAMConn, error) {
default:
log.WithField("error", scanner.Text()).Error("Unknown error")
conn.Close()
return nil, errors.New("Unknown error: " + scanner.Text() + " : " + string(buf[:n]))
return nil, fmt.Errorf("Unknown error: %s : %s", scanner.Text(), string(buf[:n]))
}
}
log.Panic("Unexpected end of StreamSession.DialI2P()")

View File

@ -2,12 +2,15 @@ package sam3
import (
"bufio"
"errors"
"github.com/sirupsen/logrus"
"context"
"fmt"
"io"
"net"
"strconv"
"strings"
"time"
"github.com/sirupsen/logrus"
"github.com/go-i2p/i2pkeys"
)
@ -42,7 +45,15 @@ func (l *StreamListener) Close() error {
// implements net.Listener
func (l *StreamListener) Accept() (net.Conn, error) {
return l.AcceptI2P()
conn, err := l.AcceptI2P()
if err != nil {
// Clean up on accept failure
if conn != nil {
conn.Close()
}
return nil, err
}
return conn, nil
}
func ExtractPairString(input, value string) string {
@ -74,67 +85,94 @@ func ExtractPairInt(input, value string) int {
func ExtractDest(input string) string {
log.WithField("input", input).Debug("ExtractDest called")
dest := strings.Split(input, " ")[0]
log.WithField("dest", dest).Debug("Destination extracted")
log.WithField("dest", dest).Debug("Destination extracted", dest)
return strings.Split(input, " ")[0]
}
// accept a new inbound connection
func (l *StreamListener) AcceptI2P() (*SAMConn, error) {
log.Debug("StreamListener.AcceptI2P() called")
s, err := NewSAM(l.session.samAddr)
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.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.from = ExtractPairString(destline, "FROM_PORT")
l.session.to = ExtractPairString(destline, "TO_PORT")
// return wrapped connection
dest = strings.Trim(dest, "\n")
log.WithFields(logrus.Fields{
"dest": dest,
"from": l.session.from,
"to": l.session.to,
}).Debug("Accepted new I2P connection")
return &SAMConn{
laddr: l.laddr,
raddr: i2pkeys.I2PAddr(dest),
conn: s.conn,
}, nil
} else {
log.WithError(err).Error("Failed to read destination line")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
done := make(chan struct{})
var conn *SAMConn
var err error
go func() {
defer close(done)
log.Debug("StreamListener.AcceptI2P() called")
s, err := NewSAM(l.session.samAddr)
if err == nil {
log.Debug("Connected to SAM bridge")
// we connected to sam
// send accept() command
acceptFmt := fmt.Sprintf("STREAM ACCEPT ID=%s SILENT=false", l.id)
_, err = io.WriteString(s.conn, acceptFmt)
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
}
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 {
if err == io.EOF {
err = fmt.Errorf("connection closed after OK")
}
err = fmt.Errorf("error reading destination: %s", err.Error())
}
if err == nil {
// Validate destination format
dest := ExtractDest(destline)
if !strings.HasPrefix(dest, "") {
err = fmt.Errorf("invalid destination format")
}
l.session.from = ExtractPairString(destline, "FROM_PORT")
l.session.to = ExtractPairString(destline, "TO_PORT")
// return wrapped connection
dest = strings.Trim(dest, "\n")
log.WithFields(logrus.Fields{
"dest": dest,
"from": l.session.from,
"to": l.session.to,
}).Debug("Accepted new I2P connection")
conn = &SAMConn{
laddr: l.laddr,
raddr: i2pkeys.I2PAddr(dest),
Conn: s.conn,
}
err = nil
} else {
log.WithError(err).Error("Failed to read destination line")
s.Close()
return
}
} else {
log.WithField("line", line).Error("Invalid SAM response")
s.Close()
err = fmt.Errorf("invalid sam line: %s", line)
}
} else {
log.WithField("line", line).Error("Invalid SAM response")
log.WithError(err).Error("Failed to connect to SAM bridge")
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
}()
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-done:
return conn, err
}
}

View File

@ -6,6 +6,7 @@ import (
"testing"
"github.com/go-i2p/i2pkeys"
sam3opts "github.com/go-i2p/sam3/opts"
)
func Test_StreamingDial(t *testing.T) {
@ -164,7 +165,7 @@ func ExampleStreamSession() {
return
}
// See the example Option_* variables.
ss, err := sam.NewStreamSession("stream_example", keys, Options_Small)
ss, err := sam.NewStreamSession("stream_example", keys, sam3opts.Options_Small)
if err != nil {
fmt.Println(err.Error())
return
@ -198,8 +199,8 @@ func ExampleStreamSession() {
return
// Output:
//Sending HTTP GET /
//Read HTTP/HTML from idk.i2p
// Sending HTTP GET /
// Read HTTP/HTML from idk.i2p
}
func ExampleStreamListener() {
@ -236,7 +237,7 @@ func ExampleStreamListener() {
fmt.Println(err.Error())
return
}
cs, err := csam.NewStreamSession("client_example", keys, Options_Small)
cs, err := csam.NewStreamSession("client_example", keys, sam3opts.Options_Small)
if err != nil {
fmt.Println(err.Error())
quit <- false
@ -259,7 +260,7 @@ func ExampleStreamListener() {
quit <- true
}(keys.Addr()) // end of client
ss, err := sam.NewStreamSession("server_example", keys, Options_Small)
ss, err := sam.NewStreamSession("server_example", keys, sam3opts.Options_Small)
if err != nil {
fmt.Println(err.Error())
return
@ -279,5 +280,5 @@ func ExampleStreamListener() {
<-quit // waits for client to die, for example only
// Output:
//Hello world!
// Hello world!
}

View File

@ -1,63 +1,15 @@
package sam3
import (
"github.com/sirupsen/logrus"
"fmt"
"net"
"net/http"
"os"
"strings"
)
// Examples and suggestions for options when creating sessions.
var (
// Suitable options if you are shuffling A LOT of traffic. If unused, this
// will waste your resources.
Options_Humongous = []string{"inbound.length=3", "outbound.length=3",
"inbound.lengthVariance=1", "outbound.lengthVariance=1",
"inbound.backupQuantity=3", "outbound.backupQuantity=3",
"inbound.quantity=6", "outbound.quantity=6"}
// Suitable for shuffling a lot of traffic.
Options_Large = []string{"inbound.length=3", "outbound.length=3",
"inbound.lengthVariance=1", "outbound.lengthVariance=1",
"inbound.backupQuantity=1", "outbound.backupQuantity=1",
"inbound.quantity=4", "outbound.quantity=4"}
// Suitable for shuffling a lot of traffic quickly with minimum
// anonymity. Uses 1 hop and multiple tunnels.
Options_Wide = []string{"inbound.length=1", "outbound.length=1",
"inbound.lengthVariance=1", "outbound.lengthVariance=1",
"inbound.backupQuantity=2", "outbound.backupQuantity=2",
"inbound.quantity=3", "outbound.quantity=3"}
// Suitable for shuffling medium amounts of traffic.
Options_Medium = []string{"inbound.length=3", "outbound.length=3",
"inbound.lengthVariance=1", "outbound.lengthVariance=1",
"inbound.backupQuantity=0", "outbound.backupQuantity=0",
"inbound.quantity=2", "outbound.quantity=2"}
// Sensible defaults for most people
Options_Default = []string{"inbound.length=3", "outbound.length=3",
"inbound.lengthVariance=0", "outbound.lengthVariance=0",
"inbound.backupQuantity=1", "outbound.backupQuantity=1",
"inbound.quantity=1", "outbound.quantity=1"}
// Suitable only for small dataflows, and very short lasting connections:
// You only have one tunnel in each direction, so if any of the nodes
// through which any of your two tunnels pass through go offline, there will
// be a complete halt in the dataflow, until a new tunnel is built.
Options_Small = []string{"inbound.length=3", "outbound.length=3",
"inbound.lengthVariance=1", "outbound.lengthVariance=1",
"inbound.backupQuantity=0", "outbound.backupQuantity=0",
"inbound.quantity=1", "outbound.quantity=1"}
// Does not use any anonymization, you connect directly to others tunnel
// endpoints, thus revealing your identity but not theirs. Use this only
// if you don't care.
Options_Warning_ZeroHop = []string{"inbound.length=0", "outbound.length=0",
"inbound.lengthVariance=0", "outbound.lengthVariance=0",
"inbound.backupQuantity=0", "outbound.backupQuantity=0",
"inbound.quantity=2", "outbound.quantity=2"}
logger "github.com/go-i2p/sam3/log"
sam3opts "github.com/go-i2p/sam3/opts"
"github.com/sirupsen/logrus"
)
func PrimarySessionString() string {
@ -83,7 +35,7 @@ func PrimarySessionString() string {
log.WithError(err).Debug("Failed to create new keys, assuming MASTER session")
return "MASTER"
}
primarySession, err := testSam.newPrimarySession("PRIMARY", "primaryTestTunnel", newKeys, Options_Small)
primarySession, err := testSam.newPrimarySession("PRIMARY", "primaryTestTunnel", newKeys, sam3opts.Options_Small)
if err != nil {
log.WithError(err).Debug("Failed to create primary session, assuming MASTER session")
return "MASTER"
@ -99,7 +51,7 @@ func PrimarySessionString() string {
var PrimarySessionSwitch string = PrimarySessionString()
func getEnv(key, fallback string) string {
InitializeSAM3Logger()
logger.InitializeSAM3Logger()
value, ok := os.LookupEnv(key)
if !ok {
log.WithFields(logrus.Fields{
@ -115,8 +67,10 @@ func getEnv(key, fallback string) string {
return value
}
var SAM_HOST = getEnv("sam_host", "127.0.0.1")
var SAM_PORT = getEnv("sam_port", "7656")
var (
SAM_HOST = getEnv("sam_host", "127.0.0.1")
SAM_PORT = getEnv("sam_port", "7656")
)
func SAMDefaultAddr(fallforward string) string {
if fallforward == "" {
@ -135,8 +89,7 @@ func GenerateOptionString(opts []string) string {
log.Debug("i2cp.leaseSetEncType already present in options")
return optStr
}
finalOpts := optStr + " i2cp.leaseSetEncType=4,0"
finalOpts := fmt.Sprintf("%s i2cp.leaseSetEncType=4,0", optStr)
log.WithField("finalOptions", finalOpts).Debug("Added default i2cp.leaseSetEncType to options")
return finalOpts
//return optStr + " i2cp.leaseSetEncType=4,0"
}