diff --git a/client.go b/client.go index 2bda40a..fb5e463 100644 --- a/client.go +++ b/client.go @@ -6,6 +6,7 @@ import ( "compress/zlib" "encoding/binary" "io" + "os" "strings" "sync" ) @@ -94,6 +95,8 @@ type Client struct { lookupRequestId uint32 } +var defaultConfigFile = "/.i2cp.conf" + // NewClient creates a new i2p client with the specified callbacks func NewClient(callbacks ClientCallBacks) (c *Client) { c = new(Client) @@ -112,10 +115,28 @@ func NewClient(callbacks ClientCallBacks) (c *Client) { func (c *Client) setDefaultProperties() { c.properties[CLIENT_PROP_ROUTER_ADDRESS] = "127.0.0.1" c.properties[CLIENT_PROP_ROUTER_PORT] = "7654" - // TODO PARSE I2CP config file + home := os.Getenv("HOME") + if len(home) == 0 { + return + } + config := home + defaultConfigFile + Debug(CLIENT, "Loading config file %s", config) + ParseConfig(config, func(name, value string) { + if prop := c.propFromString(name); prop >= 0 { + c.SetProperty(prop, value) + } + }) +} + +func (c *Client) propFromString(name string) ClientProperty { + for i := 0; ClientProperty(i) < NR_OF_I2CP_CLIENT_PROPERTIES; i++ { + if sessionOptions[i] == name { + return ClientProperty(i) + } + } + return ClientProperty(-1) } -// TODO Write messageGetDate func (c *Client) messageGetDate(queue bool) { Debug(PROTOCOL, "Sending GetDateMessage") c.messageStream.Reset() @@ -576,12 +597,13 @@ func (c *Client) ProcessIO() error { } } c.lock.Unlock() + var err error for c.tcp.CanRead() { - if err := c.recvMessage(I2CP_MSG_ANY, c.messageStream, true); err != nil { + if err = c.recvMessage(I2CP_MSG_ANY, c.messageStream, true); err != nil { return err } } - return nil + return err } func (c *Client) DestinationLookup(session *Session, address string) (requestId uint32) { diff --git a/crypto.go b/crypto.go index 91718fa..72658c1 100644 --- a/crypto.go +++ b/crypto.go @@ -1,14 +1,12 @@ package go_i2cp import ( - "crypto" "crypto/dsa" "crypto/rand" "crypto/sha1" "crypto/sha256" "encoding/base32" "encoding/base64" - "fmt" "hash" "io" "math/big" diff --git a/destination.go b/destination.go index 3cd2d28..ec8e2d9 100644 --- a/destination.go +++ b/destination.go @@ -1,9 +1,7 @@ package go_i2cp import ( - "crypto" "errors" - "github.com/cryptix/go/crypt" "math/big" "os" "strings" @@ -86,7 +84,7 @@ func NewDestinationFromBase64(base64 string) (dest Destination, err error) { return NewDestinationFromMessage(&decoded) } -func NewDestinationFromFile(file os.File) (dest Destination, err error) { +func NewDestinationFromFile(file *os.File) (dest Destination, err error) { var stream Stream stream.loadFile(file) return NewDestinationFromStream(&stream) diff --git a/i2pc.go b/i2pc.go index b29e12c..0a3d374 100644 --- a/i2pc.go +++ b/i2pc.go @@ -1,13 +1 @@ package go_i2cp - -func Init() { - tcpInit() -} - -func Deinit() { - tcpDeinit() -} - -const ( - CLIENT uint32 = (iota + 1) << 8 -) diff --git a/lease.go b/lease.go new file mode 100644 index 0000000..3a3f124 --- /dev/null +++ b/lease.go @@ -0,0 +1,22 @@ +package go_i2cp + +type Lease struct { + tunnelGateway [32]byte // sha256 of the RouterIdentity of the tunnel gateway + tunnelId uint32 + endDate uint64 +} + +func NewLeaseFromStream(stream *Stream) (l *Lease, err error) { + l = &Lease{} + stream.Read(l.tunnelGateway[:]) + l.tunnelId, err = stream.ReadUint32() + l.endDate, err = stream.ReadUint64() + return +} + +func (l *Lease) WriteToMessage(stream *Stream) (err error) { + _, err = stream.Write(l.tunnelGateway[:]) + err = stream.WriteUint32(l.tunnelId) + err = stream.WriteUint64(l.endDate) + return +} diff --git a/session.go b/session.go index bbfe903..b6c51de 100644 --- a/session.go +++ b/session.go @@ -1,10 +1,93 @@ package go_i2cp -import "bytes" +type SessionMessageStatus int + +const ( + I2CP_MSG_STATUS_AVAILABLE SessionMessageStatus = iota + I2CP_MSG_STATUS_ACCEPTED + I2CP_MSG_STATUS_BEST_EFFORT_SUCCESS + I2CP_MSG_STATUS_BEST_EFFORT_FAILURE + I2CP_MSG_STATUS_GUARANTEED_SUCCESS + I2CP_MSG_STATUS_GUARANTEED_FAILURE + I2CP_MSG_STATUS_LOCAL_SUCCESS + I2CP_MSG_STATUS_LOCAL_FAILURE + I2CP_MSG_STATUS_ROUTER_FAILURE + I2CP_MSG_STATUS_NETWORK_FAILURE + I2CP_MSG_STATUS_BAD_SESSION + I2CP_MSG_STATUS_BAD_MESSAGE + I2CP_MSG_STATUS_OVERFLOW_FAILURE + I2CP_MSG_STATUS_MESSAGE_EXPIRED + I2CP_MSG_STATUS_MESSAGE_BAD_LOCAL_LEASESET + I2CP_MSG_STATUS_MESSAGE_NO_LOCAL_TUNNELS + I2CP_MSG_STATUS_MESSAGE_UNSUPPORTED_ENCRYPTION + I2CP_MSG_STATUS_MESSAGE_BAD_DESTINATION + I2CP_MSG_STATUS_MESSAGE_BAD_LEASESET + I2CP_MSG_STATUS_MESSAGE_EXPIRED_LEASESET + I2CP_MSG_STATUS_MESSAGE_NO_LEASESET +) + +type SessionStatus int + +const ( + I2CP_SESSION_STATUS_DESTROYED SessionStatus = iota + I2CP_SESSION_STATUS_CREATED + I2CP_SESSION_STATUS_UPDATED + I2CP_SESSION_STATUS_INVALID +) + +type SessionCallbacks struct { + onMessage func(session *Session, protocol uint8, srcPort, destPort uint16, payload *Stream) + onStatus func(session *Session, status SessionStatus) + onDestination func(session *Session, requestId uint32, address string, dest *Destination) +} type Session struct { + id uint16 + config *SessionConfig + client *Client + callbacks *SessionCallbacks } -func (session Session) dispatchMessage(protocol uint8, srcPort, destPort uint16, payload *Stream) { - +func NewSession(client *Client, callbacks SessionCallbacks, destFilename string) (sess *Session) { + sess = &Session{} + sess.client = client + sess.config = &SessionConfig{} + sess.callbacks = &callbacks + return +} +func (session *Session) SendMessage(destination *Destination, protocol uint8, srcPort, destPort uint16, payload *Stream, nonce uint32) { + session.client.msgSendMessage(session, destination, protocol, srcPort, destPort, payload, nonce, true) +} +func (session *Session) Destination() *Destination { + return session.config.destination +} +func (session *Session) dispatchMessage(protocol uint8, srcPort, destPort uint16, payload *Stream) { + if session.callbacks == nil || session.callbacks.onMessage == nil { + return + } + session.callbacks.onMessage(session, protocol, srcPort, destPort, payload) +} + +func (session *Session) dispatchDestination(requestId uint32, address string, destination *Destination) { + if session.callbacks == nil || session.callbacks.onDestination == nil { + return + } + session.callbacks.onDestination(session, requestId, address, destination) +} + +func (session *Session) dispatchStatus(status SessionStatus) { + switch status { + case I2CP_SESSION_STATUS_CREATED: + Debug(SESSION, "Session %p is created", session) + case I2CP_SESSION_STATUS_DESTROYED: + Debug(SESSION, "Session %p is destroyed", session) + case I2CP_SESSION_STATUS_UPDATED: + Debug(SESSION, "Session %p is updated", session) + case I2CP_SESSION_STATUS_INVALID: + Debug(SESSION, "Session %p is invalid", session) + } + if session.callbacks == nil || session.callbacks.onStatus == nil { + return + } + session.callbacks.onStatus(session, status) } diff --git a/session_config.go b/session_config.go new file mode 100644 index 0000000..8e0ecce --- /dev/null +++ b/session_config.go @@ -0,0 +1,166 @@ +package go_i2cp + +import ( + "bufio" + "os" + "regexp" + "time" +) + +type SessionConfigProperty int + +const ( + SESSION_CONFIG_PROP_CRYPTO_LOW_TAG_THRESHOLD SessionConfigProperty = iota + SESSION_CONFIG_PROP_CRYPTO_TAGS_TO_SEND + + SESSION_CONFIG_PROP_I2CP_DONT_PUBLISH_LEASE_SET + SESSION_CONFIG_PROP_I2CP_FAST_RECEIVE + SESSION_CONFIG_PROP_I2CP_GZIP + SESSION_CONFIG_PROP_I2CP_MESSAGE_RELIABILITY + SESSION_CONFIG_PROP_I2CP_PASSWORD + SESSION_CONFIG_PROP_I2CP_USERNAME + + SESSION_CONFIG_PROP_INBOUND_ALLOW_ZERO_HOP + SESSION_CONFIG_PROP_INBOUND_BACKUP_QUANTITY + SESSION_CONFIG_PROP_INBOUND_IP_RESTRICTION + SESSION_CONFIG_PROP_INBOUND_LENGTH + SESSION_CONFIG_PROP_INBOUND_LENGTH_VARIANCE + SESSION_CONFIG_PROP_INBOUND_NICKNAME + SESSION_CONFIG_PROP_INBOUND_QUANTITY + + SESSION_CONFIG_PROP_OUTBOUND_ALLOW_ZERO_HOP + SESSION_CONFIG_PROP_OUTBOUND_BACKUP_QUANTITY + SESSION_CONFIG_PROP_OUTBOUND_IP_RESTRICTION + SESSION_CONFIG_PROP_OUTBOUND_LENGTH + SESSION_CONFIG_PROP_OUTBOUND_LENGTH_VARIANCE + SESSION_CONFIG_PROP_OUTBOUND_NICKNAME + SESSION_CONFIG_PROP_OUTBOUND_PRIORITY + SESSION_CONFIG_PROP_OUTBOUND_QUANTITY + + NR_OF_SESSION_CONFIG_PROPERTIES +) + +var sessionOptions = [NR_OF_SESSION_CONFIG_PROPERTIES]string{ + "crypto.lowTagThreshold", + "crypto.tagsToSend", + "i2cp.dontPublishLeaseSet", + "i2cp.fastReceive", + "i2cp.gzip", + "i2cp.messageReliability", + "i2cp.password", + "i2cp.username", + + "inbound.allowZeroHop", + "inbound.backupQuantity", + "inbound.IPRestriction", + "inbound.length", + "inbound.lengthVariance", + "inbound.nickname", + "inbound.quantity", + + "outbound.allowZeroHop", + "outbound.backupQuantity", + "outbound.IPRestriction", + "outbound.length", + "outbound.lengthVariance", + "outbound.nickname", + "outbound.priority", + "outbound.quantity", +} +var configRegex = regexp.MustCompile("\\s*([\\w.]+)=\\s*(.+)\\s*;\\s*") + +type SessionConfig struct { + properties [NR_OF_SESSION_CONFIG_PROPERTIES]string + date uint64 + destination *Destination +} + +func NewSessionConfigFromDestinationFile(filename string) (config SessionConfig) { + var home string + if file, err := os.Open(filename); err == nil { + dest, err := NewDestinationFromFile(file) + config.destination = &dest + if err != nil { + Warning(SESSION_CONFIG, "Failed to load destination from file '%s', a new destination will be generated.", filename) + } + } + if config.destination == nil { + dest, err := NewDestination() + config.destination = &dest + } + if len(filename) > 0 { + config.destination.WriteToFile(filename) + } + home = os.Getenv("HOME") + if len(home) > 0 { + configFile := home + "/.i2cp.conf" + ParseConfig(configFile, func(name, value string) { + if prop := config.propFromString(name); prop >= 0 { + config.SetProperty(prop, value) + } + }) + } + return config +} +func (config *SessionConfig) writeToMessage(stream *Stream) { + config.destination.WriteToMessage(stream) + config.writeMappingToMessage(stream) + stream.WriteUint64(uint64(time.Now().Unix())) + GetCryptoInstance().WriteSignatureToStream(&config.destination.sgk, stream) +} +func (config *SessionConfig) writeMappingToMessage(stream *Stream) (err error) { + is := NewStream(make([]byte, 0xffff)) + count := 0 + for i := 0; i < int(NR_OF_SESSION_CONFIG_PROPERTIES); i++ { + var option string + if sc.properties[i] == "" { + continue + } + option = sc.configOptLookup(SessionConfigProperty(i)) + if option == "" { + continue + } + is.Write([]byte(option + "=" + sc.properties[i] + ";")) + count++ + } + Debug(SESSION_CONFIG, "Writing %d options to mapping table", count) + err = stream.WriteUint16(uint16(is.Len())) + if is.Len() > 0 { + _, err = stream.Write(is.Bytes()) + } + return +} +func (config *SessionConfig) configOptLookup(property SessionConfigProperty) string { + return sessionOptions[property] +} +func (config *SessionConfig) propFromString(name string) SessionConfigProperty { + for i := 0; SessionConfigProperty(i) < NR_OF_SESSION_CONFIG_PROPERTIES; i++ { + if sessionOptions[i] == name { + return SessionConfigProperty(i) + } + } + return SessionConfigProperty(-1) +} +func (config *SessionConfig) SetProperty(prop SessionConfigProperty, value string) { + config.properties[prop] = value +} +func ParseConfig(s string, cb func(string, string)) { + file, err := os.Open(s) + if err != nil { + Error(SESSION_CONFIG, err.Error()) + return + } + Debug(SESSION_CONFIG, "Parsing config file '%s'", s) + scan := bufio.NewScanner(file) + for scan.Scan() { + line := scan.Text() + groups := configRegex.FindStringSubmatch(line) + if len(groups) != 3 { + continue + } + cb(groups[1], groups[2]) + } + if err := scan.Err(); err != nil { + Error(SESSION_CONFIG, "reading input from %s config %s", s, err.Error()) + } +} diff --git a/stream.go b/stream.go index 7d7ea2c..e35ce93 100644 --- a/stream.go +++ b/stream.go @@ -1,32 +1,31 @@ package go_i2cp import ( - "fmt" - "os" - "encoding/binary" "bytes" - "io" + "encoding/binary" + "os" ) type Stream = bytes.Buffer + func NewStream(buf []byte) *Stream { return bytes.NewBuffer(buf) } func (s *Stream) ReadUint16() (r uint16, err error) { bts := make([]byte, 2) - _, err = s.Read(bts) + _, err = s.Read(bts) r = binary.LittleEndian.Uint16(bts) return } func (s *Stream) ReadUint32() (r uint32, err error) { bts := make([]byte, 4) - _, err = s.Read(bts) + _, err = s.Read(bts) r = binary.LittleEndian.Uint32(bts) return } func (s *Stream) ReadUint64() (r uint64, err error) { bts := make([]byte, 8) - _, err = s.Read(bts) + _, err = s.Read(bts) r = binary.LittleEndian.Uint64(bts) return } @@ -49,7 +48,7 @@ func (s *Stream) WriteUint64(i uint64) (err error) { _, err = s.Write(bts) return } -func (s *Stream) loadFile(f os.File) (err error) { +func (s *Stream) loadFile(f *os.File) (err error) { _, err = f.Read(s.Bytes()) return } @@ -113,4 +112,4 @@ func (s *Stream) ReadUint8p(len uint32) []uint8 { defer s.Skip(len) return s.data[s.p:len] } -*/ \ No newline at end of file +*/ diff --git a/tcp.go b/tcp.go index d5c1860..e1d2894 100644 --- a/tcp.go +++ b/tcp.go @@ -1,35 +1,114 @@ package go_i2cp -import "net" +import ( + "crypto/tls" + "crypto/x509" + "io" + "net" + "time" +) -func tcpInit() { - address = net.TCPAddr{ - IP: net.ParseIP("127.0.0.1"), - Port: 7654, - } +type TcpProperty int + +const ( + TCP_PROP_ADDRESS TcpProperty = iota + TCP_PROP_PORT + TCP_PROP_USE_TLS + TCP_PROP_TLS_CLIENT_CERTIFICATE + NR_OF_TCP_PROPERTIES +) + +var CAFile = "/etc/ssl/certs/ca-certificates.crt" +var defaultRouterAddress = "127.0.0.1:7654" + +const USE_TLS = true + +func (tcp *Tcp) Init() (err error) { + tcp.address, err = net.ResolveTCPAddr("tcp", defaultRouterAddress) + return } -func tcpDeinit() { - -} - -var address net.TCPAddr -var conn *net.TCPConn - func (tcp *Tcp) Connect() (err error) { - conn, err = net.DialTCP("tcp", nil, &address ) + if USE_TLS { + roots, err := x509.SystemCertPool() + tcp.tlsConn, err = tls.Dial("tcp", tcp.address.String(), &tls.Config{RootCAs: roots}) + err = tcp.tlsConn.Handshake() + } else { + tcp.conn, err = net.DialTCP("tcp", nil, tcp.address) + if err == nil { + err = tcp.conn.SetKeepAlive(true) + } + } return } func (tcp *Tcp) Send(buf *Stream) (i int, err error) { - i, err = conn.Write(buf.Bytes()) + if USE_TLS { + i, err = tcp.tlsConn.Write(buf.Bytes()) + } else { + i, err = tcp.conn.Write(buf.Bytes()) + } return } func (tcp *Tcp) Receive(buf *Stream) (i int, err error) { - i, err = conn.Read(buf.Bytes()) + if USE_TLS { + i, err = tcp.tlsConn.Read(buf.Bytes()) + } else { + i, err = tcp.conn.Read(buf.Bytes()) + } + return +} + +func (tcp *Tcp) CanRead() bool { + var one []byte + if USE_TLS { + tcp.tlsConn.SetReadDeadline(time.Now()) + if _, err := tcp.tlsConn.Read(one); err == io.EOF { + Debug(TCP, "%s detected closed LAN connection", tcp.address.String()) + defer tcp.Disconnect() + return false + } else { + var zero time.Time + tcp.tlsConn.SetReadDeadline(zero) + return tcp.tlsConn.ConnectionState().HandshakeComplete + } + } else { + tcp.conn.SetReadDeadline(time.Now()) + if _, err := tcp.conn.Read(one); err == io.EOF { + Debug(TCP, "%s detected closed LAN connection", tcp.address.String()) + defer tcp.Disconnect() + return false + } else { + var zero time.Time + tcp.conn.SetReadDeadline(zero) + return true + } + } +} + +func (tcp *Tcp) Disconnect() { + if USE_TLS { + tcp.tlsConn.Close() + } else { + tcp.conn.Close() + } +} + +func (tcp *Tcp) IsConnected() bool { + return tcp.CanRead() +} + +func (tcp *Tcp) SetProperty(property TcpProperty, value string) { + tcp.properties[property] = value +} +func (tcp *Tcp) GetProperty(property TcpProperty) string { + return tcp.properties[property] } type Tcp struct { - -} \ No newline at end of file + address *net.TCPAddr + conn *net.TCPConn + tlsConn *tls.Conn + properties [NR_OF_TCP_PROPERTIES]string +}