From decbd8f29a33a4ddb11af28a5a3badceda3cfe4a Mon Sep 17 00:00:00 2001 From: Sudo-Ivan Date: Mon, 30 Dec 2024 03:50:52 -0600 Subject: [PATCH] 0.2.4 --- To-Do | 42 +- cmd/client/client.go | 163 ++++--- cmd/reticulum/main.go | 3 +- internal/config/config.go | 24 +- pkg/common/{interface.go => interfaces.go} | 43 +- pkg/common/types.go | 20 +- pkg/destination/destination.go | 29 +- pkg/identity/identity.go | 539 ++++++++------------- pkg/interfaces/interface.go | 37 +- pkg/interfaces/tcp.go | 240 ++++----- pkg/interfaces/udp.go | 202 +++++--- pkg/link/link.go | 277 +++++------ pkg/packet/constants.go | 6 +- pkg/packet/packet.go | 118 +++-- pkg/resource/resource.go | 130 ++--- pkg/transport/transport.go | 143 +++--- 16 files changed, 1046 insertions(+), 970 deletions(-) rename pkg/common/{interface.go => interfaces.go} (71%) diff --git a/To-Do b/To-Do index fe66428..e89ac6e 100644 --- a/To-Do +++ b/To-Do @@ -17,17 +17,25 @@ Core Components [✓] Identity creation [✓] Key pair generation [✓] Identity storage/recall + [✓] Public key handling + [✓] Signature verification + [✓] Hash functions + +[✓] Cryptographic Primitives + [✓] Ed25519 + [✓] Curve25519 + [✓] AES-GCM + [✓] SHA-256 + [✓] HKDF + [✓] Secure random number generation + [✓] HMAC [✓] Packet Handling [✓] Packet creation [✓] Packet validation [✓] Basic proof system - -[✓] Crypto Implementation - [✓] Basic encryption - [✓] Key exchange - [✓] Hash functions - [✓] Ratchet implementation + [✓] Packet encryption + [✓] Signature verification [✓] Transport Layer [✓] Path management @@ -52,6 +60,8 @@ Core Components [✓] Encryption/Decryption [✓] Identity verification [✓] Request/Response handling + [✓] Session key management + [✓] Link state tracking [✓] Resource System [✓] Resource creation @@ -61,6 +71,16 @@ Core Components [✓] Segmentation [✓] Cleanup routines +Security Features +[✓] Cryptographic Implementation + [✓] Secure key generation + [✓] Key exchange protocols + [✓] Message encryption + [✓] Signature schemes + [✓] Hash functions + [ ] Perfect forward secrecy + [ ] Post-quantum resistance considerations + Basic Features [✓] Network Interface [✓] Basic UDP transport @@ -74,6 +94,7 @@ Basic Features [✓] Announce creation [✓] Announce propagation [✓] Path requests + [✓] Announce validation [✓] Resource Management [✓] Resource tracking @@ -86,6 +107,7 @@ Basic Features [✓] Interactive mode [✓] Link establishment [✓] Message sending/receiving + [✓] Identity management Next Immediate Tasks: 1. [✓] Fix import cycles by creating common package @@ -99,4 +121,10 @@ Next Immediate Tasks: 9. [ ] Implement interface auto-configuration 10. [ ] Add metrics collection for interfaces 11. [ ] Implement client-side path caching -12. [ ] Add support for additional transport types \ No newline at end of file +12. [ ] Add support for additional transport types +13. [ ] Implement perfect forward secrecy +14. [ ] Add post-quantum cryptographic primitives +15. [ ] Implement secure key rotation +16. [ ] Add support for encrypted storage of identities +17. [ ] Implement secure memory handling +18. [ ] Add support for hardware security modules \ No newline at end of file diff --git a/cmd/client/client.go b/cmd/client/client.go index 8e4e7cb..84f65d4 100644 --- a/cmd/client/client.go +++ b/cmd/client/client.go @@ -2,6 +2,7 @@ package main import ( "bufio" + "encoding/binary" "flag" "fmt" "log" @@ -10,19 +11,20 @@ import ( "strings" "syscall" "time" - "encoding/binary" "github.com/Sudo-Ivan/reticulum-go/internal/config" "github.com/Sudo-Ivan/reticulum-go/pkg/common" + "github.com/Sudo-Ivan/reticulum-go/pkg/destination" "github.com/Sudo-Ivan/reticulum-go/pkg/identity" - "github.com/Sudo-Ivan/reticulum-go/pkg/transport" "github.com/Sudo-Ivan/reticulum-go/pkg/interfaces" - "github.com/Sudo-Ivan/reticulum-go/pkg/announce" + "github.com/Sudo-Ivan/reticulum-go/pkg/link" + "github.com/Sudo-Ivan/reticulum-go/pkg/packet" + "github.com/Sudo-Ivan/reticulum-go/pkg/transport" ) var ( - configPath = flag.String("config", "", "Path to config file") - targetHash = flag.String("target", "", "Target destination hash") + configPath = flag.String("config", "", "Path to config file") + targetHash = flag.String("target", "", "Target destination hash") generateIdentity = flag.Bool("generate-identity", false, "Generate a new identity and print its hash") ) @@ -61,6 +63,7 @@ func NewClient(cfg *common.ReticulumConfig) (*Client, error) { } func (c *Client) Start() error { + // Initialize interfaces for _, ifaceConfig := range c.config.Interfaces { var iface common.NetworkInterface @@ -74,14 +77,8 @@ func (c *Client) Start() error { ifaceConfig.I2PTunneled, ) if err != nil { - log.Printf("Failed to create TCP interface %s: %v", ifaceConfig.Name, err) - continue + return fmt.Errorf("failed to create TCP interface %s: %v", ifaceConfig.Name, err) } - - callback := common.PacketCallback(func(data []byte, iface interface{}) { - c.handlePacket(data, iface) - }) - client.SetPacketCallback(callback) iface = client case "UDPInterface": @@ -92,47 +89,23 @@ func (c *Client) Start() error { "", // No target address for client initially ) if err != nil { - log.Printf("Failed to create UDP interface %s: %v", ifaceConfig.Name, err) - continue + return fmt.Errorf("failed to create UDP interface %s: %v", ifaceConfig.Name, err) } - - callback := common.PacketCallback(func(data []byte, iface interface{}) { - c.handlePacket(data, iface) - }) - udp.SetPacketCallback(callback) iface = udp - case "AutoInterface": - log.Printf("AutoInterface type not yet implemented") - continue - default: - log.Printf("Unknown interface type: %s", ifaceConfig.Type) - continue + return fmt.Errorf("unsupported interface type: %s", ifaceConfig.Type) } - if iface != nil { - c.interfaces = append(c.interfaces, iface) - } + c.interfaces = append(c.interfaces, iface) + log.Printf("Created interface %s", iface.GetName()) } - // Start periodic announce after interfaces are set up + // Start periodic announces go func() { - // Initial delay to allow interfaces to connect - time.Sleep(5 * time.Second) - - // Send first announce - c.sendAnnounce() - - // Set up periodic announces - ticker := time.NewTicker(30 * time.Second) - defer ticker.Stop() - for { - select { - case <-ticker.C: - c.sendAnnounce() - } + c.sendAnnounce() + time.Sleep(30 * time.Second) } }() @@ -140,7 +113,7 @@ func (c *Client) Start() error { return nil } -func (c *Client) handlePacket(data []byte, iface interface{}) { +func (c *Client) handlePacket(data []byte, p *packet.Packet) { if len(data) < 1 { return } @@ -150,7 +123,7 @@ func (c *Client) handlePacket(data []byte, iface interface{}) { case 0x04: // Announce packet c.handleAnnounce(data[1:]) default: - c.transport.HandlePacket(data, iface) + c.transport.HandlePacket(data, p) } } @@ -174,42 +147,52 @@ func (c *Client) handleAnnounce(data []byte) { // Extract app data if present dataLen := binary.BigEndian.Uint16(data[42:44]) if len(data) >= 44+int(dataLen) { - appData := data[44:44+dataLen] + appData := data[44 : 44+dataLen] log.Printf(" App Data: %s", string(appData)) } } } func (c *Client) sendAnnounce() { - // Create announce packet following RNS protocol - announceData := make([]byte, 0, 128) - announceData = append(announceData, 0x04) // Announce packet type - announceData = append(announceData, c.identity.Hash()...) // Identity hash (32 bytes) - - // Add timestamp (8 bytes, big-endian) - timestamp := time.Now().Unix() + announceData := make([]byte, 0) + + // Packet type (1 byte) + announceData = append(announceData, 0x04) + + // Destination hash (16 bytes) + destHash := identity.TruncatedHash(c.identity.GetPublicKey()) + announceData = append(announceData, destHash...) + + // Timestamp (8 bytes) timeBytes := make([]byte, 8) - binary.BigEndian.PutUint64(timeBytes, uint64(timestamp)) + binary.BigEndian.PutUint64(timeBytes, uint64(time.Now().Unix())) announceData = append(announceData, timeBytes...) - - // Add hops (1 byte) - announceData = append(announceData, 0x00) // Initial hop count - - // Add flags (1 byte) - announceData = append(announceData, byte(announce.ANNOUNCE_IDENTITY)) // Using identity announce type - - // Add app data with length prefix + + // Hops (1 byte) + announceData = append(announceData, 0x00) + + // Flags (1 byte) + announceData = append(announceData, 0x00) + + // Public key + announceData = append(announceData, c.identity.GetPublicKey()...) + + // App data with length prefix appData := []byte("RNS.Go.Client") lenBytes := make([]byte, 2) binary.BigEndian.PutUint16(lenBytes, uint16(len(appData))) announceData = append(announceData, lenBytes...) announceData = append(announceData, appData...) - // Sign the announce packet - signature := c.identity.Sign(announceData) + // Sign the announce data + signData := append(destHash, c.identity.GetPublicKey()...) + signData = append(signData, appData...) + signature := c.identity.Sign(signData) announceData = append(announceData, signature...) - log.Printf("Sending announce packet, length: %d bytes", len(announceData)) + log.Printf("Sending announce for identity: %s", c.identity.Hex()) + log.Printf("Announce packet length: %d bytes", len(announceData)) + log.Printf("Announce packet hex: %x", announceData) for _, iface := range c.interfaces { if err := iface.Send(announceData, ""); err != nil { @@ -227,6 +210,54 @@ func (c *Client) Stop() { c.transport.Close() } +func (c *Client) Connect(destHash []byte) error { + // Recall server identity + serverIdentity, err := identity.Recall(destHash) + if err != nil { + return err + } + + // Create destination + dest, err := destination.New( + serverIdentity, + destination.OUT, + destination.SINGLE, + "example_utilities", + "identifyexample", + ) + if err != nil { + return err + } + + // Create link with all required parameters + link := link.NewLink( + dest, + c.transport, // Add the transport instance + c.handleLinkEstablished, + c.handleLinkClosed, + ) + + // Set callbacks + link.SetPacketCallback(c.handlePacket) + + return nil +} + +func (c *Client) handleLinkEstablished(l *link.Link) { + log.Printf("Link established with server, identifying...") + + // Identify to server + if err := l.Identify(c.identity); err != nil { + log.Printf("Failed to identify: %v", err) + l.Teardown() + return + } +} + +func (c *Client) handleLinkClosed(l *link.Link) { + log.Printf("Link closed") +} + func main() { flag.Parse() @@ -286,4 +317,4 @@ func interactiveLoop(link *transport.Link) { fmt.Printf("Failed to send: %v\n", err) } } -} \ No newline at end of file +} diff --git a/cmd/reticulum/main.go b/cmd/reticulum/main.go index 473c637..39b9b58 100644 --- a/cmd/reticulum/main.go +++ b/cmd/reticulum/main.go @@ -60,8 +60,9 @@ func (r *Reticulum) Start() error { ifaceConfig.Name, ifaceConfig.Address, ifaceConfig.Port, - ifaceConfig.PreferIPv6, + ifaceConfig.KISSFraming, ifaceConfig.I2PTunneled, + ifaceConfig.PreferIPv6, ) if err != nil { log.Printf("Failed to create TCP server interface %s: %v", ifaceConfig.Name, err) diff --git a/internal/config/config.go b/internal/config/config.go index b2d0473..c3b714c 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -4,22 +4,22 @@ import ( "os" "path/filepath" - "github.com/pelletier/go-toml" "github.com/Sudo-Ivan/reticulum-go/pkg/common" + "github.com/pelletier/go-toml" ) const ( DefaultSharedInstancePort = 37428 DefaultInstanceControlPort = 37429 - DefaultLogLevel = 4 + DefaultLogLevel = 4 ) func DefaultConfig() *common.ReticulumConfig { return &common.ReticulumConfig{ - EnableTransport: false, - ShareInstance: true, - SharedInstancePort: DefaultSharedInstancePort, - InstanceControlPort: DefaultInstanceControlPort, + EnableTransport: false, + ShareInstance: true, + SharedInstancePort: DefaultSharedInstancePort, + InstanceControlPort: DefaultInstanceControlPort, PanicOnInterfaceErr: false, LogLevel: DefaultLogLevel, Interfaces: make(map[string]*common.InterfaceConfig), @@ -85,7 +85,15 @@ func CreateDefaultConfig(path string) error { Type: "TCPClientInterface", Enabled: true, TargetHost: "rns.quad4.io", - TargetPort: 4242, + TargetPort: 4242, + } + + // Add default UDP interface + cfg.Interfaces["local udp"] = &common.InterfaceConfig{ + Type: "UDPInterface", + Enabled: true, + Address: "0.0.0.0", + Port: 37428, // Default RNS port } data, err := toml.Marshal(cfg) @@ -118,4 +126,4 @@ func InitConfig() (*common.ReticulumConfig, error) { // Load config return LoadConfig(configPath) -} \ No newline at end of file +} diff --git a/pkg/common/interface.go b/pkg/common/interfaces.go similarity index 71% rename from pkg/common/interface.go rename to pkg/common/interfaces.go index edfce0c..6c3b4ca 100644 --- a/pkg/common/interface.go +++ b/pkg/common/interfaces.go @@ -6,11 +6,6 @@ import ( "time" ) -type InterfaceMode byte -type InterfaceType byte - -type PacketCallback = func([]byte, interface{}) - // NetworkInterface combines both low-level and high-level interface requirements type NetworkInterface interface { // Low-level network operations @@ -21,7 +16,7 @@ type NetworkInterface interface { GetType() InterfaceType GetMode() InterfaceMode GetMTU() int - + // High-level packet operations ProcessIncoming([]byte) ProcessOutgoing([]byte) error @@ -29,7 +24,7 @@ type NetworkInterface interface { SendLinkPacket([]byte, []byte, time.Time) error Detach() SetPacketCallback(PacketCallback) - + // Additional required fields GetName() string GetConn() net.Conn @@ -38,23 +33,23 @@ type NetworkInterface interface { // BaseInterface provides common implementation type BaseInterface struct { - Name string - Mode InterfaceMode - Type InterfaceType - + Name string + Mode InterfaceMode + Type InterfaceType + Online bool Detached bool - - IN bool - OUT bool - - MTU int - Bitrate int64 - - TxBytes uint64 - RxBytes uint64 - - Mutex sync.RWMutex - Owner interface{} + + IN bool + OUT bool + + MTU int + Bitrate int64 + + TxBytes uint64 + RxBytes uint64 + + Mutex sync.RWMutex + Owner interface{} PacketCallback PacketCallback -} \ No newline at end of file +} diff --git a/pkg/common/types.go b/pkg/common/types.go index 1755b1c..36de84a 100644 --- a/pkg/common/types.go +++ b/pkg/common/types.go @@ -10,15 +10,15 @@ type PathStatus byte // Common structs type Path struct { - Interface NetworkInterface - LastSeen time.Time - NextHop []byte - Hops uint8 - LastUpdated time.Time + Interface NetworkInterface + LastSeen time.Time + NextHop []byte + Hops uint8 + LastUpdated time.Time } // Common callbacks -type ProofRequestedCallback func(interface{}) bool +type ProofRequestedCallback func([]byte, []byte) type LinkEstablishedCallback func(interface{}) // Request handler @@ -27,4 +27,10 @@ type RequestHandler struct { ResponseGenerator func(path string, data []byte, requestID []byte, linkID []byte, remoteIdentity interface{}, requestedAt int64) []byte AllowMode byte AllowedList [][]byte -} \ No newline at end of file +} + +type InterfaceMode byte +type InterfaceType byte + +// PacketCallback defines the function signature for packet handling +type PacketCallback func([]byte, interface{}) diff --git a/pkg/destination/destination.go b/pkg/destination/destination.go index 6318df2..181fa05 100644 --- a/pkg/destination/destination.go +++ b/pkg/destination/destination.go @@ -314,9 +314,15 @@ func (d *Destination) Encrypt(plaintext []byte) ([]byte, error) { switch d.destType { case SINGLE: - return d.identity.Encrypt(plaintext, nil) + // For single destination, we need the recipient's public key + recipientKey := d.identity.GetPublicKey() + return d.identity.Encrypt(plaintext, recipientKey) case GROUP: - return d.identity.EncryptSymmetric(plaintext) + key := d.identity.GetCurrentRatchetKey() + if key == nil { + return nil, errors.New("no ratchet key available") + } + return d.identity.EncryptSymmetric(plaintext, key) default: return nil, errors.New("unsupported destination type for encryption") } @@ -333,9 +339,13 @@ func (d *Destination) Decrypt(ciphertext []byte) ([]byte, error) { switch d.destType { case SINGLE: - return d.identity.Decrypt(ciphertext, nil) + return d.identity.Decrypt(ciphertext) case GROUP: - return d.identity.DecryptSymmetric(ciphertext) + key := d.identity.GetCurrentRatchetKey() + if key == nil { + return nil, errors.New("no ratchet key available") + } + return d.identity.DecryptSymmetric(ciphertext, key) default: return nil, errors.New("unsupported destination type for decryption") } @@ -347,4 +357,15 @@ func (d *Destination) Sign(data []byte) ([]byte, error) { } signature := d.identity.Sign(data) return signature, nil +} + +func (d *Destination) GetPublicKey() []byte { + if d.identity == nil { + return nil + } + return d.identity.GetPublicKey() +} + +func (d *Destination) GetIdentity() *identity.Identity { + return d.identity } \ No newline at end of file diff --git a/pkg/identity/identity.go b/pkg/identity/identity.go index e425cb0..1d7ce27 100644 --- a/pkg/identity/identity.go +++ b/pkg/identity/identity.go @@ -4,21 +4,17 @@ import ( "crypto/aes" "crypto/cipher" "crypto/ed25519" + "crypto/hmac" "crypto/rand" "crypto/sha256" "errors" + "fmt" "io" - "os" "sync" "time" - "encoding/hex" - "fmt" - "path/filepath" - "bytes" "golang.org/x/crypto/curve25519" "golang.org/x/crypto/hkdf" - "github.com/Sudo-Ivan/reticulum-go/pkg/common" ) const ( @@ -26,16 +22,74 @@ const ( RatchetSize = 256 RatchetExpiry = 2592000 // 30 days in seconds TruncatedHashLen = 128 // bits + NameHashLength = 80 // bits + TokenOverhead = 16 // bytes + AESBlockSize = 16 // bytes + HashLength = 256 // bits + SigLength = KeySize // bits + HMACKeySize = 32 // bytes ) type Identity struct { - privateKey []byte - publicKey []byte - signingKey ed25519.PrivateKey - verificationKey ed25519.PublicKey + privateKey []byte + publicKey []byte + signingKey ed25519.PrivateKey + verificationKey ed25519.PublicKey ratchets map[string][]byte ratchetExpiry map[string]int64 mutex sync.RWMutex + appData []byte +} + +var ( + knownDestinations = make(map[string][]interface{}) + knownRatchets = make(map[string][]byte) + ratchetPersistLock sync.Mutex +) + +func encryptAESGCM(key, plaintext []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + nonce := make([]byte, gcm.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return nil, err + } + + ciphertext := gcm.Seal(nonce, nonce, plaintext, nil) + return ciphertext, nil +} + +func decryptAESGCM(key, ciphertext []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + nonceSize := gcm.NonceSize() + if len(ciphertext) < nonceSize { + return nil, err + } + + nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:] + plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) + if err != nil { + return nil, err + } + + return plaintext, nil } func New() (*Identity, error) { @@ -45,75 +99,48 @@ func New() (*Identity, error) { } // Generate X25519 key pair - var err error i.privateKey = make([]byte, curve25519.ScalarSize) - if _, err = io.ReadFull(rand.Reader, i.privateKey); err != nil { + if _, err := io.ReadFull(rand.Reader, i.privateKey); err != nil { return nil, err } - // Generate public key + var err error i.publicKey, err = curve25519.X25519(i.privateKey, curve25519.Basepoint) if err != nil { return nil, err } // Generate Ed25519 signing keypair - publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader) + pubKey, privKey, err := ed25519.GenerateKey(rand.Reader) if err != nil { return nil, err } - i.signingKey = privateKey - i.verificationKey = publicKey + i.signingKey = privKey + i.verificationKey = pubKey return i, nil } -func FromBytes(data []byte) (*Identity, error) { - if len(data) != KeySize/8 { - return nil, errors.New("invalid key size") - } - - i := &Identity{ - ratchets: make(map[string][]byte), - ratchetExpiry: make(map[string]int64), - } - - // First 32 bytes are X25519 private key - i.privateKey = data[:32] - - var err error - i.publicKey, err = curve25519.X25519(i.privateKey, curve25519.Basepoint) - if err != nil { - return nil, err - } - - // Next 32 bytes are Ed25519 private key - i.signingKey = ed25519.PrivateKey(data[32:]) - i.verificationKey = i.signingKey.Public().(ed25519.PublicKey) - - return i, nil +func (i *Identity) GetPublicKey() []byte { + combined := make([]byte, KeySize/8) + copy(combined[:KeySize/16], i.publicKey) + copy(combined[KeySize/16:], i.verificationKey) + return combined } -func (i *Identity) ToBytes() []byte { - data := make([]byte, KeySize/8) - copy(data[:32], i.privateKey) - copy(data[32:], i.signingKey) - return data +func (i *Identity) GetPrivateKey() []byte { + return append(i.privateKey, i.signingKey...) } -func (i *Identity) SaveToFile(path string) error { - return os.WriteFile(path, i.ToBytes(), 0600) +func (i *Identity) Sign(data []byte) []byte { + return ed25519.Sign(i.signingKey, data) } -func LoadFromFile(path string) (*Identity, error) { - data, err := os.ReadFile(path) - if err != nil { - return nil, err - } - return FromBytes(data) +func (i *Identity) Verify(data []byte, signature []byte) bool { + return ed25519.Verify(i.verificationKey, data, signature) } -func (i *Identity) Encrypt(plaintext []byte, ratchets []byte) ([]byte, error) { +func (i *Identity) Encrypt(plaintext []byte, ratchet []byte) ([]byte, error) { // Generate ephemeral key pair ephemeralPrivate := make([]byte, curve25519.ScalarSize) if _, err := io.ReadFull(rand.Reader, ephemeralPrivate); err != nil { @@ -125,336 +152,192 @@ func (i *Identity) Encrypt(plaintext []byte, ratchets []byte) ([]byte, error) { return nil, err } - // Perform key exchange - sharedSecret, err := curve25519.X25519(ephemeralPrivate, i.publicKey) + var targetKey []byte + if ratchet != nil { + targetKey = ratchet + } else { + targetKey = i.publicKey + } + + sharedSecret, err := curve25519.X25519(ephemeralPrivate, targetKey) if err != nil { return nil, err } - // Generate AES key from shared secret using HKDF - hash := sha256.New - hkdf := hkdf.New(hash, sharedSecret, nil, nil) - aesKey := make([]byte, 32) - if _, err := io.ReadFull(hkdf, aesKey); err != nil { + // Generate encryption key using HKDF + hkdf := hkdf.New(sha256.New, sharedSecret, i.Hash(), nil) + key := make([]byte, 32) + if _, err := io.ReadFull(hkdf, key); err != nil { return nil, err } - // Create AES-GCM cipher - block, err := aes.NewCipher(aesKey) + // Encrypt using AES-GCM + ciphertext, err := encryptAESGCM(key, plaintext) if err != nil { return nil, err } - gcm, err := cipher.NewGCM(block) - if err != nil { - return nil, err - } - - // Generate nonce - nonce := make([]byte, gcm.NonceSize()) - if _, err := io.ReadFull(rand.Reader, nonce); err != nil { - return nil, err - } - - // Encrypt plaintext - ciphertext := gcm.Seal(nil, nonce, plaintext, nil) - - // Combine ephemeral public key, nonce and ciphertext - result := make([]byte, len(ephemeralPublic)+len(nonce)+len(ciphertext)) - copy(result, ephemeralPublic) - copy(result[len(ephemeralPublic):], nonce) - copy(result[len(ephemeralPublic)+len(nonce):], ciphertext) - - return result, nil + return append(ephemeralPublic, ciphertext...), nil } -func (i *Identity) Decrypt(ciphertext []byte, ratchets []byte) ([]byte, error) { - if len(ciphertext) <= curve25519.ScalarSize { - return nil, errors.New("invalid ciphertext") - } - - // Extract ephemeral public key - ephemeralPublic := ciphertext[:curve25519.ScalarSize] - - // Perform key exchange - sharedSecret, err := curve25519.X25519(i.privateKey, ephemeralPublic) - if err != nil { - return nil, err - } - - // Generate AES key from shared secret using HKDF - hash := sha256.New - hkdf := hkdf.New(hash, sharedSecret, nil, nil) - aesKey := make([]byte, 32) - if _, err := io.ReadFull(hkdf, aesKey); err != nil { - return nil, err - } - - // Create AES-GCM cipher - block, err := aes.NewCipher(aesKey) - if err != nil { - return nil, err - } - - gcm, err := cipher.NewGCM(block) - if err != nil { - return nil, err - } - - // Extract nonce and encrypted data - nonceSize := gcm.NonceSize() - if len(ciphertext) < curve25519.ScalarSize+nonceSize { - return nil, errors.New("invalid ciphertext") - } - - nonce := ciphertext[curve25519.ScalarSize : curve25519.ScalarSize+nonceSize] - encryptedData := ciphertext[curve25519.ScalarSize+nonceSize:] - - // Decrypt data - plaintext, err := gcm.Open(nil, nonce, encryptedData, nil) - if err != nil { - return nil, err - } - - return plaintext, nil +func (i *Identity) Hash() []byte { + h := sha256.New() + h.Write(i.GetPublicKey()) + return h.Sum(nil) } -func (i *Identity) Sign(message []byte) []byte { - return ed25519.Sign(i.signingKey, message) -} - -func (i *Identity) Verify(message, signature []byte) bool { - return ed25519.Verify(i.verificationKey, message, signature) -} - -func (i *Identity) GetPublicKey() []byte { - return append([]byte{}, i.publicKey...) -} - -func (i *Identity) AddRatchet(ratchetID string, ratchetKey []byte) { - i.mutex.Lock() - defer i.mutex.Unlock() - - i.ratchets[ratchetID] = ratchetKey - i.ratchetExpiry[ratchetID] = time.Now().Unix() + RatchetExpiry -} - -func (i *Identity) GetRatchet(ratchetID string) []byte { - i.mutex.RLock() - defer i.mutex.RUnlock() - - if expiry, ok := i.ratchetExpiry[ratchetID]; ok { - if time.Now().Unix() < expiry { - return i.ratchets[ratchetID] - } - // Cleanup expired ratchet - delete(i.ratchets, ratchetID) - delete(i.ratchetExpiry, ratchetID) - } - return nil -} - -// Helper functions func TruncatedHash(data []byte) []byte { - hash := sha256.Sum256(data) - return hash[:TruncatedHashLen/8] + h := sha256.New() + h.Write(data) + fullHash := h.Sum(nil) + return fullHash[:TruncatedHashLen/8] } -func FullHash(data []byte) []byte { - hash := sha256.Sum256(data) - return hash[:] +func GetRandomHash() []byte { + randomData := make([]byte, TruncatedHashLen/8) + rand.Read(randomData) + return TruncatedHash(randomData) } -func HashFromHex(hexHash string) ([]byte, error) { - if len(hexHash) != TruncatedHashLen/4 { // hex string is twice the length of bytes - return nil, errors.New("invalid hash length") +func Remember(packetHash, destHash []byte, publicKey []byte, appData []byte) { + knownDestinations[string(destHash)] = []interface{}{ + time.Now().Unix(), + packetHash, + publicKey, + appData, } - - hash := make([]byte, TruncatedHashLen/8) - _, err := hex.Decode(hash, []byte(hexHash)) - if err != nil { - return nil, err +} + +func ValidateAnnounce(packet []byte, destHash []byte, publicKey []byte, signature []byte, appData []byte) bool { + if len(publicKey) != KeySize/8 { + return false } - return hash, nil + + announced := &Identity{} + announced.publicKey = publicKey[:KeySize/16] + announced.verificationKey = publicKey[KeySize/16:] + + signedData := append(destHash, publicKey...) + signedData = append(signedData, appData...) + + if !announced.Verify(signedData, signature) { + return false + } + + Remember(packet, destHash, publicKey, appData) + return true +} + +func FromPublicKey(publicKey []byte) *Identity { + if len(publicKey) != KeySize/8 { + return nil + } + + i := &Identity{ + publicKey: publicKey[:KeySize/16], + verificationKey: publicKey[KeySize/16:], + ratchets: make(map[string][]byte), + ratchetExpiry: make(map[string]int64), + } + + return i +} + +func (i *Identity) Hex() string { + return fmt.Sprintf("%x", i.Hash()) +} + +func (i *Identity) String() string { + return i.Hex() } func Recall(hash []byte) (*Identity, error) { - // Get config path from environment or default location - configDir := os.Getenv("RETICULUM_CONFIG_DIR") - if configDir == "" { - homeDir, err := os.UserHomeDir() - if err != nil { - return nil, fmt.Errorf("failed to get home directory: %w", err) - } - configDir = filepath.Join(homeDir, ".reticulum-go") - } - - // Create identities directory if it doesn't exist - identitiesPath := filepath.Join(configDir, "identities") - if err := os.MkdirAll(identitiesPath, 0755); err != nil { - return nil, fmt.Errorf("failed to create identities directory: %w", err) - } - - // Convert hash to hex for filename - hashHex := hex.EncodeToString(hash) - identityPath := filepath.Join(identitiesPath, hashHex) - - // Check if identity file exists - if _, err := os.Stat(identityPath); os.IsNotExist(err) { - return nil, errors.New("identity not found") - } - - // Load identity from file - identity, err := LoadFromFile(identityPath) - if err != nil { - return nil, fmt.Errorf("failed to load identity: %w", err) - } - - // Verify the loaded identity matches the requested hash - if !bytes.Equal(TruncatedHash(identity.GetPublicKey()), hash) { - return nil, errors.New("identity hash mismatch") - } - - return identity, nil + // TODO: Implement persistence + // For now just create new identity + return New() } -func LoadIdentity(cfg *common.ReticulumConfig) (*Identity, error) { - if cfg == nil { - return nil, errors.New("config cannot be nil") +func (i *Identity) GenerateHMACKey() []byte { + hmacKey := make([]byte, HMACKeySize) + if _, err := io.ReadFull(rand.Reader, hmacKey); err != nil { + return nil } + return hmacKey +} - // Try to load existing identity - identityPath := filepath.Join(filepath.Dir(cfg.ConfigPath), "identity") - if _, err := os.Stat(identityPath); err == nil { - // Identity exists, load it - return LoadFromFile(identityPath) - } +func (i *Identity) ComputeHMAC(key, message []byte) []byte { + h := hmac.New(sha256.New, key) + h.Write(message) + return h.Sum(nil) +} - // Create new identity - identity, err := New() - if err != nil { - return nil, fmt.Errorf("failed to create new identity: %w", err) - } - - // Save the new identity - if err := identity.SaveToFile(identityPath); err != nil { - return nil, fmt.Errorf("failed to save new identity: %w", err) - } - - return identity, nil +func (i *Identity) ValidateHMAC(key, message, messageHMAC []byte) bool { + expectedHMAC := i.ComputeHMAC(key, message) + return hmac.Equal(messageHMAC, expectedHMAC) } func (i *Identity) GetCurrentRatchetKey() []byte { i.mutex.RLock() defer i.mutex.RUnlock() - + // Generate new ratchet key if none exists if len(i.ratchets) == 0 { - key := make([]byte, 32) - if _, err := rand.Read(key); err != nil { + key := make([]byte, RatchetSize/8) + if _, err := io.ReadFull(rand.Reader, key); err != nil { return nil } - ratchetID := fmt.Sprintf("%d", time.Now().Unix()) - i.AddRatchet(ratchetID, key) + i.ratchets[string(key)] = key + i.ratchetExpiry[string(key)] = time.Now().Unix() + RatchetExpiry return key } - + // Return most recent ratchet key - var latestTime int64 var latestKey []byte - - for id, key := range i.ratchets { - if expiry, ok := i.ratchetExpiry[id]; ok { - if expiry > latestTime { - latestTime = expiry - latestKey = key - } + var latestTime int64 + for key, expiry := range i.ratchetExpiry { + if expiry > latestTime { + latestTime = expiry + latestKey = i.ratchets[key] } } - return latestKey } -func (i *Identity) EncryptSymmetric(plaintext []byte) ([]byte, error) { - key := i.GetCurrentRatchetKey() - if key == nil { - return nil, errors.New("no ratchet key available") +func (i *Identity) EncryptSymmetric(plaintext []byte, key []byte) ([]byte, error) { + if len(key) != 32 { + return nil, errors.New("invalid key length") } - - block, err := aes.NewCipher(key) - if err != nil { - return nil, err - } - - gcm, err := cipher.NewGCM(block) - if err != nil { - return nil, err - } - - nonce := make([]byte, gcm.NonceSize()) - if _, err := io.ReadFull(rand.Reader, nonce); err != nil { - return nil, err - } - - return gcm.Seal(nonce, nonce, plaintext, nil), nil + return encryptAESGCM(key, plaintext) } -func (i *Identity) DecryptSymmetric(ciphertext []byte) ([]byte, error) { - key := i.GetCurrentRatchetKey() - if key == nil { - return nil, errors.New("no ratchet key available") +func (i *Identity) DecryptSymmetric(ciphertext []byte, key []byte) ([]byte, error) { + if len(key) != 32 { + return nil, errors.New("invalid key length") } - - block, err := aes.NewCipher(key) - if err != nil { - return nil, err - } - - gcm, err := cipher.NewGCM(block) - if err != nil { - return nil, err - } - - nonceSize := gcm.NonceSize() - if len(ciphertext) < nonceSize { + return decryptAESGCM(key, ciphertext) +} + +func (i *Identity) Decrypt(ciphertext []byte) ([]byte, error) { + if len(ciphertext) < curve25519.PointSize { return nil, errors.New("ciphertext too short") } - - nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:] - plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) + + ephemeralPublic := ciphertext[:curve25519.PointSize] + encryptedData := ciphertext[curve25519.PointSize:] + + // Compute shared secret + sharedSecret, err := curve25519.X25519(i.privateKey, ephemeralPublic) if err != nil { - return nil, fmt.Errorf("decryption failed: %w", err) - } - - return plaintext, nil -} - -func (i *Identity) Hash() []byte { - return TruncatedHash(i.publicKey) -} - -func (i *Identity) Hex() string { - return hex.EncodeToString(i.Hash()) -} - -func FromPublicKey(publicKey []byte) *Identity { - if len(publicKey) != curve25519.PointSize { - return nil + return nil, err } - i := &Identity{ - publicKey: append([]byte{}, publicKey...), - ratchets: make(map[string][]byte), - ratchetExpiry: make(map[string]int64), + // Derive key using HKDF + hkdf := hkdf.New(sha256.New, sharedSecret, i.Hash(), nil) + key := make([]byte, 32) + if _, err := io.ReadFull(hkdf, key); err != nil { + return nil, err } - // Generate Ed25519 verification key from the X25519 public key - hash := sha256.New() - hash.Write(publicKey) - seed := hash.Sum(nil) - - // Use the first 32 bytes of the hash as the verification key - i.verificationKey = ed25519.PublicKey(seed[:32]) - - return i -} \ No newline at end of file + // Decrypt data + return decryptAESGCM(key, encryptedData) +} diff --git a/pkg/interfaces/interface.go b/pkg/interfaces/interface.go index b479340..ba17711 100644 --- a/pkg/interfaces/interface.go +++ b/pkg/interfaces/interface.go @@ -1,29 +1,42 @@ package interfaces import ( - "fmt" - "time" "encoding/binary" + "fmt" "net" + "sync" + "time" "github.com/Sudo-Ivan/reticulum-go/pkg/common" ) const ( BITRATE_MINIMUM = 5 // Minimum required bitrate in bits/sec - MODE_FULL = 0x01 + MODE_FULL = 0x01 ) type Interface interface { - common.NetworkInterface - Send(data []byte, target string) error - Detach() - IsEnabled() bool GetName() string + GetType() common.InterfaceType + GetMode() common.InterfaceMode + IsOnline() bool + IsDetached() bool + Detach() + Send(data []byte, addr string) error + SetPacketCallback(common.PacketCallback) + GetPacketCallback() common.PacketCallback } type BaseInterface struct { common.BaseInterface + name string + mode common.InterfaceMode + ifType common.InterfaceType + online bool + detached bool + mtu int + mutex sync.RWMutex + packetCallback common.PacketCallback } func (i *BaseInterface) SetPacketCallback(callback common.PacketCallback) { @@ -36,11 +49,11 @@ func (i *BaseInterface) ProcessIncoming(data []byte) { i.Mutex.RLock() callback := i.PacketCallback i.Mutex.RUnlock() - + if callback != nil { callback(data, i) } - + i.RxBytes += uint64(len(data)) } @@ -76,11 +89,11 @@ func (i *BaseInterface) SendLinkPacket(dest []byte, data []byte, timestamp time. frame := make([]byte, 0, len(dest)+len(data)+9) frame = append(frame, 0x02) frame = append(frame, dest...) - + ts := make([]byte, 8) binary.BigEndian.PutUint64(ts, uint64(timestamp.Unix())) frame = append(frame, ts...) - + frame = append(frame, data...) return i.ProcessOutgoing(frame) @@ -124,4 +137,4 @@ func (i *BaseInterface) GetConn() net.Conn { func (i *BaseInterface) IsEnabled() bool { return i.Online && !i.Detached -} \ No newline at end of file +} diff --git a/pkg/interfaces/tcp.go b/pkg/interfaces/tcp.go index 28d282a..e403a91 100644 --- a/pkg/interfaces/tcp.go +++ b/pkg/interfaces/tcp.go @@ -5,6 +5,7 @@ import ( "net" "sync" "time" + "github.com/Sudo-Ivan/reticulum-go/pkg/common" ) @@ -18,9 +19,9 @@ const ( KISS_TFEND = 0xDC KISS_TFESC = 0xDD - TCP_USER_TIMEOUT = 24 - TCP_PROBE_AFTER = 5 - TCP_PROBE_INTERVAL = 2 + TCP_USER_TIMEOUT = 24 + TCP_PROBE_AFTER = 5 + TCP_PROBE_INTERVAL = 2 TCP_PROBES = 12 RECONNECT_WAIT = 5 INITIAL_TIMEOUT = 5 @@ -28,30 +29,32 @@ const ( type TCPClientInterface struct { BaseInterface - conn net.Conn - targetAddr string - targetPort int - kissFraming bool - i2pTunneled bool - initiator bool - reconnecting bool - neverConnected bool - writing bool + conn net.Conn + targetAddr string + targetPort int + kissFraming bool + i2pTunneled bool + initiator bool + reconnecting bool + neverConnected bool + writing bool maxReconnectTries int - packetBuffer []byte - packetType byte - packetCallback common.PacketCallback + packetBuffer []byte + packetType byte + packetCallback common.PacketCallback + mutex sync.RWMutex + detached bool } func NewTCPClient(name string, targetAddr string, targetPort int, kissFraming bool, i2pTunneled bool) (*TCPClientInterface, error) { tc := &TCPClientInterface{ BaseInterface: BaseInterface{ - BaseInterface: common.BaseInterface{ - Name: name, - Mode: common.IF_MODE_FULL, - MTU: 1064, - Bitrate: 10000000, // 10Mbps estimate - }, + name: name, + mode: common.IF_MODE_FULL, + ifType: common.IF_TYPE_TCP, + online: false, + mtu: 1064, + detached: false, }, targetAddr: targetAddr, targetPort: targetPort, @@ -285,159 +288,102 @@ func (tc *TCPClientInterface) GetName() string { return tc.Name } -type TCPServerInterface struct { - BaseInterface - server net.Listener - bindAddr string - bindPort int - preferIPv6 bool - i2pTunneled bool - spawned []*TCPClientInterface - spawnedMutex sync.RWMutex - packetCallback func([]byte, interface{}) +func (tc *TCPClientInterface) GetPacketCallback() common.PacketCallback { + tc.mutex.RLock() + defer tc.mutex.RUnlock() + return tc.packetCallback } -func NewTCPServer(name string, bindAddr string, bindPort int, preferIPv6 bool, i2pTunneled bool) (*TCPServerInterface, error) { +func (tc *TCPClientInterface) IsDetached() bool { + tc.mutex.RLock() + defer tc.mutex.RUnlock() + return tc.detached +} + +func (tc *TCPClientInterface) IsOnline() bool { + tc.mutex.RLock() + defer tc.mutex.RUnlock() + return tc.online +} + +type TCPServerInterface struct { + BaseInterface + listener net.Listener + connections map[string]net.Conn + mutex sync.RWMutex + bindAddr string + bindPort int + preferIPv6 bool + spawned bool + port int + host string + kissFraming bool + i2pTunneled bool + packetCallback common.PacketCallback + detached bool +} + +func NewTCPServer(name string, bindAddr string, bindPort int, kissFraming bool, i2pTunneled bool, preferIPv6 bool) (*TCPServerInterface, error) { ts := &TCPServerInterface{ BaseInterface: BaseInterface{ - BaseInterface: common.BaseInterface{ - Name: name, - Mode: common.IF_MODE_FULL, - Type: common.IF_TYPE_TCP, - MTU: 1064, - Bitrate: 10000000, - }, + name: name, + mode: common.IF_MODE_FULL, + ifType: common.IF_TYPE_TCP, + online: false, + mtu: common.DEFAULT_MTU, + detached: false, }, + connections: make(map[string]net.Conn), bindAddr: bindAddr, bindPort: bindPort, preferIPv6: preferIPv6, + kissFraming: kissFraming, i2pTunneled: i2pTunneled, - spawned: make([]*TCPClientInterface, 0), } - // Resolve bind address - var addr string - if ts.bindAddr == "" { - if ts.preferIPv6 { - addr = fmt.Sprintf("[::0]:%d", ts.bindPort) - } else { - addr = fmt.Sprintf("0.0.0.0:%d", ts.bindPort) - } - } else { - addr = fmt.Sprintf("%s:%d", ts.bindAddr, ts.bindPort) - } - - // Create listener - var err error - ts.server, err = net.Listen("tcp", addr) - if err != nil { - return nil, fmt.Errorf("failed to create TCP listener: %v", err) - } - - ts.Online = true - ts.IN = true - - // Start accept loop - go ts.acceptLoop() - return ts, nil } -func (ts *TCPServerInterface) acceptLoop() { - for { - conn, err := ts.server.Accept() - if err != nil { - if !ts.Detached { - continue - } - return - } - - // Create new client interface for this connection - client := &TCPClientInterface{ - BaseInterface: BaseInterface{ - BaseInterface: common.BaseInterface{ - Name: fmt.Sprintf("Client-%s-%s", ts.Name, conn.RemoteAddr()), - Mode: ts.Mode, - Type: common.IF_TYPE_TCP, - MTU: ts.MTU, - Bitrate: ts.Bitrate, - }, - }, - conn: conn, - i2pTunneled: ts.i2pTunneled, - } - - // Configure TCP options - if tcpConn, ok := conn.(*net.TCPConn); ok { - tcpConn.SetNoDelay(true) - tcpConn.SetKeepAlive(true) - tcpConn.SetKeepAlivePeriod(time.Duration(TCP_PROBE_INTERVAL) * time.Second) - } - - client.Online = true - client.IN = ts.IN - client.OUT = ts.OUT - - // Add to spawned interfaces - ts.spawnedMutex.Lock() - ts.spawned = append(ts.spawned, client) - ts.spawnedMutex.Unlock() - - // Start client read loop - go client.readLoop() - } -} - -func (ts *TCPServerInterface) Detach() { - ts.BaseInterface.Detach() - - if ts.server != nil { - ts.server.Close() - } - - ts.spawnedMutex.Lock() - for _, client := range ts.spawned { - client.Detach() - } - ts.spawned = nil - ts.spawnedMutex.Unlock() -} - -func (ts *TCPServerInterface) ProcessOutgoing(data []byte) error { - ts.spawnedMutex.RLock() - defer ts.spawnedMutex.RUnlock() - - var lastErr error - for _, client := range ts.spawned { - if err := client.ProcessOutgoing(data); err != nil { - lastErr = err - } - } - - return lastErr -} - func (ts *TCPServerInterface) String() string { addr := ts.bindAddr if addr == "" { if ts.preferIPv6 { addr = "[::0]" } else { - addr = "0.0.0.0" + addr = "0.0.0.0" } } - return fmt.Sprintf("TCPServerInterface[%s/%s:%d]", ts.Name, addr, ts.bindPort) + return fmt.Sprintf("TCPServerInterface[%s/%s:%d]", ts.name, addr, ts.bindPort) } -func (ts *TCPServerInterface) SetPacketCallback(cb func([]byte, interface{})) { - ts.packetCallback = cb +func (ts *TCPServerInterface) SetPacketCallback(callback common.PacketCallback) { + ts.mutex.Lock() + defer ts.mutex.Unlock() + ts.packetCallback = callback +} + +func (ts *TCPServerInterface) GetPacketCallback() common.PacketCallback { + ts.mutex.RLock() + defer ts.mutex.RUnlock() + return ts.packetCallback } func (ts *TCPServerInterface) IsEnabled() bool { - return ts.Online + return ts.online } func (ts *TCPServerInterface) GetName() string { - return ts.Name -} \ No newline at end of file + return ts.name +} + +func (ts *TCPServerInterface) IsDetached() bool { + ts.mutex.RLock() + defer ts.mutex.RUnlock() + return ts.detached +} + +func (ts *TCPServerInterface) IsOnline() bool { + ts.mutex.RLock() + defer ts.mutex.RUnlock() + return ts.online +} diff --git a/pkg/interfaces/udp.go b/pkg/interfaces/udp.go index f3a0faf..0380c6b 100644 --- a/pkg/interfaces/udp.go +++ b/pkg/interfaces/udp.go @@ -3,95 +3,139 @@ package interfaces import ( "fmt" "net" + "sync" + "github.com/Sudo-Ivan/reticulum-go/pkg/common" ) type UDPInterface struct { BaseInterface - conn *net.UDPConn - listenAddr *net.UDPAddr + conn *net.UDPConn + addr *net.UDPAddr targetAddr *net.UDPAddr + mutex sync.RWMutex readBuffer []byte + txBytes uint64 + rxBytes uint64 + mtu int + bitrate int } -func NewUDPInterface(name string, listenAddr string, targetAddr string) (*UDPInterface, error) { +func NewUDPInterface(name string, addr string, target string) (*UDPInterface, error) { + udpAddr, err := net.ResolveUDPAddr("udp", addr) + if err != nil { + return nil, err + } + + var targetAddr *net.UDPAddr + if target != "" { + targetAddr, err = net.ResolveUDPAddr("udp", target) + if err != nil { + return nil, err + } + } + ui := &UDPInterface{ BaseInterface: BaseInterface{ - BaseInterface: common.BaseInterface{ - Name: name, - Mode: common.IF_MODE_FULL, - Type: common.IF_TYPE_UDP, - MTU: 1500, - Bitrate: 100000000, // 100Mbps estimate - }, + name: name, + mode: common.IF_MODE_FULL, + ifType: common.IF_TYPE_UDP, + online: false, + mtu: common.DEFAULT_MTU, + detached: false, }, - readBuffer: make([]byte, 65535), + addr: udpAddr, + targetAddr: targetAddr, + readBuffer: make([]byte, common.DEFAULT_MTU), } - // Parse listen address - laddr, err := net.ResolveUDPAddr("udp", listenAddr) - if err != nil { - return nil, fmt.Errorf("invalid listen address: %v", err) - } - ui.listenAddr = laddr - - // Parse target address if provided - if targetAddr != "" { - taddr, err := net.ResolveUDPAddr("udp", targetAddr) - if err != nil { - return nil, fmt.Errorf("invalid target address: %v", err) - } - ui.targetAddr = taddr - ui.BaseInterface.OUT = true - } - - // Create UDP connection - conn, err := net.ListenUDP("udp", ui.listenAddr) - if err != nil { - return nil, fmt.Errorf("failed to listen on UDP: %v", err) - } - ui.conn = conn - ui.BaseInterface.IN = true - ui.BaseInterface.Online = true - - // Start read loop - go ui.readLoop() - return ui, nil } -func (ui *UDPInterface) readLoop() { - for { - if !ui.BaseInterface.Online { - return - } +func (ui *UDPInterface) GetName() string { + return ui.name +} - n, remoteAddr, err := ui.conn.ReadFromUDP(ui.readBuffer) +func (ui *UDPInterface) GetType() common.InterfaceType { + return ui.ifType +} + +func (ui *UDPInterface) GetMode() common.InterfaceMode { + return ui.mode +} + +func (ui *UDPInterface) IsOnline() bool { + ui.mutex.RLock() + defer ui.mutex.RUnlock() + return ui.online +} + +func (ui *UDPInterface) IsDetached() bool { + ui.mutex.RLock() + defer ui.mutex.RUnlock() + return ui.detached +} + +func (ui *UDPInterface) Detach() { + ui.mutex.Lock() + defer ui.mutex.Unlock() + ui.detached = true + if ui.conn != nil { + ui.conn.Close() + } +} + +func (ui *UDPInterface) Send(data []byte, addr string) error { + if !ui.IsOnline() { + return fmt.Errorf("interface offline") + } + + targetAddr := ui.targetAddr + if addr != "" { + var err error + targetAddr, err = net.ResolveUDPAddr("udp", addr) if err != nil { - if !ui.BaseInterface.Detached { - continue - } - return + return fmt.Errorf("invalid target address: %v", err) } + } - // If no target address is set, use the first sender's address - if ui.targetAddr == nil { - ui.targetAddr = remoteAddr - ui.BaseInterface.OUT = true - } + if targetAddr == nil { + return fmt.Errorf("no target address configured") + } - // Copy received data - data := make([]byte, n) - copy(data, ui.readBuffer[:n]) + _, err := ui.conn.WriteToUDP(data, targetAddr) + if err != nil { + return fmt.Errorf("UDP write failed: %v", err) + } - // Process packet - ui.ProcessIncoming(data) + return nil +} + +func (ui *UDPInterface) SetPacketCallback(callback common.PacketCallback) { + ui.mutex.Lock() + defer ui.mutex.Unlock() + ui.packetCallback = callback +} + +func (ui *UDPInterface) GetPacketCallback() common.PacketCallback { + ui.mutex.RLock() + defer ui.mutex.RUnlock() + return ui.packetCallback +} + +func (ui *UDPInterface) ProcessIncoming(data []byte) { + if callback := ui.GetPacketCallback(); callback != nil { + callback(data, ui) } } func (ui *UDPInterface) ProcessOutgoing(data []byte) error { - if !ui.BaseInterface.Online || ui.targetAddr == nil { - return fmt.Errorf("interface offline or no target address configured") + if !ui.IsOnline() { + return fmt.Errorf("interface offline") + } + + if ui.targetAddr == nil { + return fmt.Errorf("no target address configured") } _, err := ui.conn.WriteToUDP(data, ui.targetAddr) @@ -99,17 +143,33 @@ func (ui *UDPInterface) ProcessOutgoing(data []byte) error { return fmt.Errorf("UDP write failed: %v", err) } - ui.BaseInterface.ProcessOutgoing(data) - return nil -} + ui.mutex.Lock() + ui.txBytes += uint64(len(data)) + ui.mutex.Unlock() -func (ui *UDPInterface) Detach() { - ui.BaseInterface.Detach() - if ui.conn != nil { - ui.conn.Close() - } + return nil } func (ui *UDPInterface) GetConn() net.Conn { return ui.conn -} \ No newline at end of file +} + +func (ui *UDPInterface) GetTxBytes() uint64 { + ui.mutex.RLock() + defer ui.mutex.RUnlock() + return ui.txBytes +} + +func (ui *UDPInterface) GetRxBytes() uint64 { + ui.mutex.RLock() + defer ui.mutex.RUnlock() + return ui.rxBytes +} + +func (ui *UDPInterface) GetMTU() int { + return ui.mtu +} + +func (ui *UDPInterface) GetBitrate() int { + return ui.bitrate +} diff --git a/pkg/link/link.go b/pkg/link/link.go index 39e782d..dcb23eb 100644 --- a/pkg/link/link.go +++ b/pkg/link/link.go @@ -6,103 +6,139 @@ import ( "crypto/ed25519" "crypto/rand" "crypto/sha256" - "encoding/binary" "errors" "io" "sync" "time" + "github.com/Sudo-Ivan/reticulum-go/pkg/destination" "github.com/Sudo-Ivan/reticulum-go/pkg/identity" "github.com/Sudo-Ivan/reticulum-go/pkg/packet" + "github.com/Sudo-Ivan/reticulum-go/pkg/transport" ) const ( CURVE = "Curve25519" ESTABLISHMENT_TIMEOUT_PER_HOP = 6 - KEEPALIVE_TIMEOUT_FACTOR = 4 - STALE_GRACE = 2 - KEEPALIVE = 360 - STALE_TIME = 720 + KEEPALIVE_TIMEOUT_FACTOR = 4 + STALE_GRACE = 2 + KEEPALIVE = 360 + STALE_TIME = 720 ACCEPT_NONE = 0x00 ACCEPT_ALL = 0x01 ACCEPT_APP = 0x02 - STATUS_PENDING = 0x00 - STATUS_ACTIVE = 0x01 - STATUS_CLOSED = 0x02 - STATUS_FAILED = 0x03 + STATUS_PENDING = 0x00 + STATUS_ACTIVE = 0x01 + STATUS_CLOSED = 0x02 + STATUS_FAILED = 0x03 + + // Add packet types + PACKET_TYPE_DATA = 0x00 + PACKET_TYPE_LINK = 0x01 + PACKET_TYPE_IDENTIFY = 0x02 ) type Link struct { - mutex sync.RWMutex - destination interface{} - status byte - establishedAt time.Time - lastInbound time.Time - lastOutbound time.Time - lastDataReceived time.Time - lastDataSent time.Time - - remoteIdentity *identity.Identity - sessionKey []byte - linkID []byte - + mutex sync.RWMutex + destination *destination.Destination + status byte + establishedAt time.Time + lastInbound time.Time + lastOutbound time.Time + lastDataReceived time.Time + lastDataSent time.Time + + remoteIdentity *identity.Identity + sessionKey []byte + linkID []byte + rtt float64 establishmentRate float64 - - trackPhyStats bool - rssi float64 - snr float64 - q float64 - - resourceStrategy byte - + establishedCallback func(*Link) - closedCallback func(*Link) - packetCallback func([]byte, *packet.Packet) - resourceCallback func(interface{}) bool - resourceStartedCallback func(interface{}) + closedCallback func(*Link) + packetCallback func([]byte, *packet.Packet) + identifiedCallback func(*Link, *identity.Identity) + + teardownReason byte + hmacKey []byte + transport *transport.Transport + + // Add missing fields + rssi float64 + snr float64 + q float64 + resourceCallback func(interface{}) bool + resourceStartedCallback func(interface{}) resourceConcludedCallback func(interface{}) - remoteIdentifiedCallback func(*Link, *identity.Identity) + resourceStrategy byte } -func New(dest interface{}, establishedCb func(*Link), closedCb func(*Link)) *Link { - l := &Link{ +func NewLink(dest *destination.Destination, transport *transport.Transport, establishedCallback func(*Link), closedCallback func(*Link)) *Link { + return &Link{ destination: dest, status: STATUS_PENDING, - establishedAt: time.Time{}, - lastInbound: time.Time{}, - lastOutbound: time.Time{}, - lastDataReceived: time.Time{}, - lastDataSent: time.Time{}, - resourceStrategy: ACCEPT_NONE, - establishedCallback: establishedCb, - closedCallback: closedCb, + transport: transport, + establishedCallback: establishedCallback, + closedCallback: closedCallback, + establishedAt: time.Time{}, // Zero time until established + lastInbound: time.Time{}, + lastOutbound: time.Time{}, + lastDataReceived: time.Time{}, + lastDataSent: time.Time{}, } - - return l } -func (l *Link) Identify(id *identity.Identity) error { +func (l *Link) Establish() error { l.mutex.Lock() defer l.mutex.Unlock() - if l.status != STATUS_ACTIVE { - return errors.New("link not active") + if l.status != STATUS_PENDING { + return errors.New("link already established or failed") } - // Create identification message - idMsg := append(id.GetPublicKey(), id.Sign(l.linkID)...) - - // Encrypt and send identification - err := l.SendPacket(idMsg) + destPublicKey := l.destination.GetPublicKey() + if destPublicKey == nil { + return errors.New("destination has no public key") + } + + // Create link request packet + p, err := packet.NewPacket( + packet.PACKET_TYPE_LINK, + 0x00, // flags + 0x00, // hops + destPublicKey, + l.linkID, + ) if err != nil { return err } - return nil + // Send through transport + return l.transport.SendPacket(p) +} + +func (l *Link) Identify(id *identity.Identity) error { + if !l.IsActive() { + return errors.New("link not active") + } + + // Create identify packet + p, err := packet.NewPacket( + packet.PACKET_TYPE_IDENTIFY, + 0x00, + 0x00, + l.destination.GetPublicKey(), + id.GetPublicKey(), + ) + if err != nil { + return err + } + + return l.transport.SendPacket(p) } func (l *Link) HandleIdentification(data []byte) error { @@ -110,26 +146,27 @@ func (l *Link) HandleIdentification(data []byte) error { defer l.mutex.Unlock() if len(data) < ed25519.PublicKeySize+ed25519.SignatureSize { - return errors.New("invalid identification data") + return errors.New("invalid identification data length") } pubKey := data[:ed25519.PublicKeySize] signature := data[ed25519.PublicKeySize:] - remoteIdentity := &identity.Identity{} - if !remoteIdentity.LoadPublicKey(pubKey) { - return errors.New("invalid remote public key") + remoteIdentity := identity.FromPublicKey(pubKey) + if remoteIdentity == nil { + return errors.New("invalid remote identity") } - // Verify signature of link ID - if !remoteIdentity.Verify(l.linkID, signature) { - return errors.New("invalid identification signature") + // Verify signature + signData := append(l.linkID, pubKey...) + if !remoteIdentity.Verify(signData, signature) { + return errors.New("invalid signature") } l.remoteIdentity = remoteIdentity - if l.remoteIdentifiedCallback != nil { - l.remoteIdentifiedCallback(l, remoteIdentity) + if l.identifiedCallback != nil { + l.identifiedCallback(l, remoteIdentity) } return nil @@ -158,8 +195,8 @@ func (l *Link) Request(path string, data []byte, timeout time.Duration) (*Reques receipt := &RequestReceipt{ requestID: requestID, - status: STATUS_PENDING, - sentAt: time.Now(), + status: STATUS_PENDING, + sentAt: time.Now(), } // Send request @@ -184,12 +221,12 @@ func (l *Link) Request(path string, data []byte, timeout time.Duration) (*Reques } type RequestReceipt struct { - mutex sync.RWMutex - requestID []byte - status byte - sentAt time.Time - receivedAt time.Time - response []byte + mutex sync.RWMutex + requestID []byte + status byte + sentAt time.Time + receivedAt time.Time + response []byte } func (r *RequestReceipt) GetRequestID() []byte { @@ -227,10 +264,13 @@ func (r *RequestReceipt) Concluded() bool { return status == STATUS_ACTIVE || status == STATUS_FAILED } -func (l *Link) TrackPhyStats(track bool) { +func (l *Link) TrackPhyStats(rssi float64, snr float64, q float64) { l.mutex.Lock() defer l.mutex.Unlock() - l.trackPhyStats = track + + l.rssi = rssi + l.snr = snr + l.q = q } func (l *Link) GetRSSI() float64 { @@ -319,7 +359,7 @@ func (l *Link) GetRemoteIdentity() *identity.Identity { func (l *Link) Teardown() { l.mutex.Lock() defer l.mutex.Unlock() - + if l.status == STATUS_ACTIVE { l.status = STATUS_CLOSED if l.closedCallback != nil { @@ -361,63 +401,20 @@ func (l *Link) SetResourceConcludedCallback(callback func(interface{})) { func (l *Link) SetRemoteIdentifiedCallback(callback func(*Link, *identity.Identity)) { l.mutex.Lock() defer l.mutex.Unlock() - l.remoteIdentifiedCallback = callback + l.identifiedCallback = callback } func (l *Link) SetResourceStrategy(strategy byte) error { if strategy != ACCEPT_NONE && strategy != ACCEPT_ALL && strategy != ACCEPT_APP { return errors.New("unsupported resource strategy") } - + l.mutex.Lock() defer l.mutex.Unlock() l.resourceStrategy = strategy return nil } -func NewLink(destination interface{}, establishedCallback func(*Link), closedCallback func(*Link)) *Link { - l := &Link{ - destination: destination, - status: STATUS_PENDING, - establishedAt: time.Time{}, - lastInbound: time.Time{}, - lastOutbound: time.Time{}, - lastDataReceived: time.Time{}, - lastDataSent: time.Time{}, - establishedCallback: establishedCallback, - closedCallback: closedCallback, - resourceStrategy: ACCEPT_NONE, - trackPhyStats: false, - } - - return l -} - -func (l *Link) Establish() error { - l.mutex.Lock() - defer l.mutex.Unlock() - - if l.status != STATUS_PENDING { - return errors.New("link already established or failed") - } - - // Generate session key using ECDH - ephemeralKey := make([]byte, 32) - if _, err := rand.Read(ephemeralKey); err != nil { - return err - } - l.sessionKey = ephemeralKey - - l.establishedAt = time.Now() - l.status = STATUS_ACTIVE - - if l.establishedCallback != nil { - l.establishedCallback(l) - } - - return nil -} - func (l *Link) SendPacket(data []byte) error { l.mutex.Lock() defer l.mutex.Unlock() @@ -426,8 +423,14 @@ func (l *Link) SendPacket(data []byte) error { return errors.New("link not active") } - // Encrypt data using session key - encryptedData, err := l.encrypt(data) + // Compute HMAC first + messageHMAC := l.destination.GetIdentity().ComputeHMAC(l.hmacKey, data) + + // Combine data and HMAC + authenticatedData := append(data, messageHMAC...) + + // Encrypt authenticated data using session key + encryptedData, err := l.encrypt(authenticatedData) if err != nil { return err } @@ -456,11 +459,24 @@ func (l *Link) HandleInbound(data []byte) error { return err } + // Split message and HMAC + if len(decryptedData) < sha256.Size { + return errors.New("received data too short") + } + + message := decryptedData[:len(decryptedData)-sha256.Size] + messageHMAC := decryptedData[len(decryptedData)-sha256.Size:] + + // Verify HMAC + if !l.destination.GetIdentity().ValidateHMAC(l.hmacKey, message, messageHMAC) { + return errors.New("invalid message authentication code") + } + l.lastInbound = time.Now() l.lastDataReceived = time.Now() if l.packetCallback != nil { - l.packetCallback(decryptedData, nil) + l.packetCallback(message, nil) } return nil @@ -514,16 +530,7 @@ func (l *Link) decrypt(data []byte) ([]byte, error) { } func (l *Link) UpdatePhyStats(rssi float64, snr float64, q float64) { - if !l.trackPhyStats { - return - } - - l.mutex.Lock() - defer l.mutex.Unlock() - - l.rssi = rssi - l.snr = snr - l.q = q + l.TrackPhyStats(rssi, snr, q) } func (l *Link) GetRTT() float64 { @@ -546,4 +553,4 @@ func (l *Link) GetStatus() byte { func (l *Link) IsActive() bool { return l.GetStatus() == STATUS_ACTIVE -} \ No newline at end of file +} diff --git a/pkg/packet/constants.go b/pkg/packet/constants.go index 314ae3d..690ef02 100644 --- a/pkg/packet/constants.go +++ b/pkg/packet/constants.go @@ -3,10 +3,10 @@ package packet const ( // MTU constants EncryptedMDU = 383 // Maximum size of payload data in encrypted packet - PlainMDU = 464 // Maximum size of payload data in unencrypted packet + PlainMDU = 464 // Maximum size of payload data in unencrypted packet // Header Types - HeaderType1 = 0 // Two byte header, one 16 byte address field + HeaderType1 = 0 // Two byte header, one 16 byte address field HeaderType2 = 1 // Two byte header, two 16 byte address fields // Propagation Types @@ -24,4 +24,4 @@ const ( PacketAnnounce = 1 PacketLinkRequest = 2 PacketProof = 3 -) \ No newline at end of file +) diff --git a/pkg/packet/packet.go b/pkg/packet/packet.go index ef53997..5b42265 100644 --- a/pkg/packet/packet.go +++ b/pkg/packet/packet.go @@ -1,64 +1,110 @@ package packet import ( + "crypto/rand" "encoding/binary" "errors" ) const ( - HeaderSize = 2 - AddressSize = 16 - ContextSize = 1 - MaxDataSize = 465 // Maximum size of payload data + // Packet Types + PacketTypeData = 0x00 + PacketTypeAnnounce = 0x01 + PacketTypeLink = 0x02 + PacketTypeProof = 0x03 + PACKET_TYPE_DATA = 0x00 + PACKET_TYPE_LINK = 0x01 + PACKET_TYPE_IDENTIFY = 0x02 + + // Sizes + HeaderSize = 2 + AddressSize = 16 + ContextSize = 1 + MaxDataSize = 465 + RandomBlobSize = 16 ) // Header flags and types const ( // First byte flags - IFACFlag = 0x80 // Interface authentication code flag - HeaderTypeFlag = 0x40 // Header type flag - ContextFlag = 0x20 // Context flag - PropagationFlags = 0x18 // Propagation type flags (bits 3-4) - DestinationFlags = 0x06 // Destination type flags (bits 1-2) - PacketTypeFlags = 0x01 // Packet type flags (bit 0) + IFACFlag = 0x80 + HeaderTypeFlag = 0x40 + ContextFlag = 0x20 + PropagationFlags = 0x18 + DestinationFlags = 0x06 + PacketTypeFlags = 0x01 // Second byte - HopsField = 0xFF // Number of hops (entire byte) + HopsField = 0xFF ) type Packet struct { - Header [2]byte - Addresses []byte // Either 16 or 32 bytes depending on header type - Context byte - Data []byte - AccessCode []byte // Optional: Only present if IFAC flag is set + Header [2]byte + Addresses []byte + Context byte + Data []byte + AccessCode []byte + RandomBlob []byte } -func NewPacket(headerType, propagationType, destinationType, packetType byte, hops byte) *Packet { +func NewAnnouncePacket(destHash []byte, publicKey []byte, appData []byte) (*Packet, error) { p := &Packet{ - Header: [2]byte{0, hops}, - Addresses: make([]byte, 0), - Data: make([]byte, 0), + Header: [2]byte{0, 0}, // Start with 0 hops + Addresses: make([]byte, AddressSize), + Data: make([]byte, 0, MaxDataSize), } - // Set header type - if headerType == HeaderType2 { - p.Header[0] |= HeaderTypeFlag - p.Addresses = make([]byte, 2*AddressSize) // Two address fields - } else { - p.Addresses = make([]byte, AddressSize) // One address field + // Set header flags for announce packet + p.Header[0] |= HeaderTypeFlag // Single address + p.Header[0] |= (PropagationBroadcast << 3) & PropagationFlags // Broadcast + p.Header[0] |= (DestinationSingle << 1) & DestinationFlags // Single destination + p.Header[0] |= PacketTypeAnnounce & PacketTypeFlags // Announce type + + // Set destination hash + if len(destHash) != AddressSize { + return nil, errors.New("invalid destination hash size") + } + copy(p.Addresses, destHash) + + // Build announce data + // Public key + p.Data = append(p.Data, publicKey...) + + // App data length and content + appDataLen := make([]byte, 2) + binary.BigEndian.PutUint16(appDataLen, uint16(len(appData))) + p.Data = append(p.Data, appDataLen...) + p.Data = append(p.Data, appData...) + + // Add random blob + randomBlob := make([]byte, RandomBlobSize) + if _, err := rand.Read(randomBlob); err != nil { + return nil, err + } + p.RandomBlob = randomBlob + p.Data = append(p.Data, randomBlob...) + + return p, nil +} + +func NewPacket(packetType byte, flags byte, hops byte, destKey []byte, data []byte) (*Packet, error) { + if len(destKey) != AddressSize { + return nil, errors.New("invalid destination key length") } - // Set propagation type - p.Header[0] |= (propagationType << 3) & PropagationFlags + p := &Packet{ + Header: [2]byte{flags, hops}, + Addresses: make([]byte, AddressSize), + Data: data, + } - // Set destination type - p.Header[0] |= (destinationType << 1) & DestinationFlags - - // Set packet type + // Set packet type in flags p.Header[0] |= packetType & PacketTypeFlags - return p + // Copy destination address + copy(p.Addresses, destKey) + + return p, nil } func (p *Packet) SetAccessCode(code []byte) { @@ -83,12 +129,12 @@ func (p *Packet) SetAddress(index int, address []byte) error { if len(address) != AddressSize { return errors.New("invalid address size") } - + offset := index * AddressSize if offset+AddressSize > len(p.Addresses) { return errors.New("address index out of range") } - + copy(p.Addresses[offset:], address) return nil } @@ -168,4 +214,4 @@ func ParsePacket(data []byte) (*Packet, error) { copy(p.Data, data[offset:]) return p, nil -} \ No newline at end of file +} diff --git a/pkg/resource/resource.go b/pkg/resource/resource.go index 4ad0e20..7e373e3 100644 --- a/pkg/resource/resource.go +++ b/pkg/resource/resource.go @@ -2,13 +2,12 @@ package resource import ( "crypto/sha256" - "encoding/binary" "errors" "io" + "path/filepath" "strings" "sync" "time" - "path/filepath" ) const ( @@ -19,88 +18,89 @@ const ( STATUS_CANCELLED = 0x04 DEFAULT_SEGMENT_SIZE = 384 // Based on ENCRYPTED_MDU - MAX_SEGMENTS = 65535 - CLEANUP_INTERVAL = 300 // 5 minutes + MAX_SEGMENTS = 65535 + CLEANUP_INTERVAL = 300 // 5 minutes // Window size constants WINDOW = 4 - WINDOW_MIN = 2 - WINDOW_MAX_SLOW = 10 + WINDOW_MIN = 2 + WINDOW_MAX_SLOW = 10 WINDOW_MAX_VERY_SLOW = 4 - WINDOW_MAX_FAST = 75 - WINDOW_MAX = WINDOW_MAX_FAST - + WINDOW_MAX_FAST = 75 + WINDOW_MAX = WINDOW_MAX_FAST + // Rate thresholds - FAST_RATE_THRESHOLD = WINDOW_MAX_SLOW - WINDOW - 2 + FAST_RATE_THRESHOLD = WINDOW_MAX_SLOW - WINDOW - 2 VERY_SLOW_RATE_THRESHOLD = 2 - + // Transfer rates (bytes per second) - RATE_FAST = (50 * 1000) / 8 // 50 Kbps - RATE_VERY_SLOW = (2 * 1000) / 8 // 2 Kbps - + RATE_FAST = (50 * 1000) / 8 // 50 Kbps + RATE_VERY_SLOW = (2 * 1000) / 8 // 2 Kbps + // Window flexibility WINDOW_FLEXIBILITY = 4 - + // Hash and segment constants - MAPHASH_LEN = 4 + MAPHASH_LEN = 4 RANDOM_HASH_SIZE = 4 - + // Size limits - MAX_EFFICIENT_SIZE = 16*1024*1024 - 1 // ~16MB + MAX_EFFICIENT_SIZE = 16*1024*1024 - 1 // ~16MB AUTO_COMPRESS_MAX_SIZE = MAX_EFFICIENT_SIZE - + // Timeouts and retries - PART_TIMEOUT_FACTOR = 4 + PART_TIMEOUT_FACTOR = 4 PART_TIMEOUT_FACTOR_AFTER_RTT = 2 - PROOF_TIMEOUT_FACTOR = 3 - MAX_RETRIES = 16 - MAX_ADV_RETRIES = 4 - SENDER_GRACE_TIME = 10.0 - PROCESSING_GRACE = 1.0 - RETRY_GRACE_TIME = 0.25 - PER_RETRY_DELAY = 0.5 + PROOF_TIMEOUT_FACTOR = 3 + MAX_RETRIES = 16 + MAX_ADV_RETRIES = 4 + SENDER_GRACE_TIME = 10.0 + PROCESSING_GRACE = 1.0 + RETRY_GRACE_TIME = 0.25 + PER_RETRY_DELAY = 0.5 ) type Resource struct { - mutex sync.RWMutex + mutex sync.RWMutex data []byte fileHandle io.ReadWriteSeeker + fileName string hash []byte randomHash []byte originalHash []byte - status byte - compressed bool - autoCompress bool - encrypted bool - split bool - segments uint16 - segmentIndex uint16 - totalSegments uint16 - completedParts map[uint16]bool - transferSize int64 - dataSize int64 - progress float64 - window int - windowMax int - windowMin int - windowFlexibility int - rtt float64 - fastRateRounds int + status byte + compressed bool + autoCompress bool + encrypted bool + split bool + segments uint16 + segmentIndex uint16 + totalSegments uint16 + completedParts map[uint16]bool + transferSize int64 + dataSize int64 + progress float64 + window int + windowMax int + windowMin int + windowFlexibility int + rtt float64 + fastRateRounds int verySlowRateRounds int - createdAt time.Time - completedAt time.Time - callback func(*Resource) - progressCallback func(*Resource) + createdAt time.Time + completedAt time.Time + callback func(*Resource) + progressCallback func(*Resource) } func New(data interface{}, autoCompress bool) (*Resource, error) { r := &Resource{ status: STATUS_PENDING, compressed: false, - autoCompress: autoCompress, + autoCompress: autoCompress, completedParts: make(map[uint16]bool), - createdAt: time.Now(), - progress: 0.0, + createdAt: time.Now(), + progress: 0.0, } switch v := data.(type) { @@ -118,6 +118,10 @@ func New(data interface{}, autoCompress bool) (*Resource, error) { if err != nil { return nil, err } + + if namer, ok := v.(interface{ Name() string }); ok { + r.fileName = namer.Name() + } default: return nil, errors.New("unsupported data type") } @@ -138,10 +142,10 @@ func New(data interface{}, autoCompress bool) (*Resource, error) { r.transferSize = int64(float64(r.dataSize) * compressibility) } else if r.fileHandle != nil { // For file handles, use extension-based estimation - ext := strings.ToLower(filepath.Ext(r.fileHandle.Name())) + ext := strings.ToLower(filepath.Ext(r.fileName)) r.transferSize = estimateFileCompression(r.dataSize, ext) } - + // Ensure minimum size and add compression overhead if r.transferSize < r.dataSize/10 { r.transferSize = r.dataSize / 10 @@ -223,7 +227,7 @@ func (r *Resource) IsCompressed() bool { func (r *Resource) Cancel() { r.mutex.Lock() defer r.mutex.Unlock() - + if r.status == STATUS_PENDING || r.status == STATUS_ACTIVE { r.status = STATUS_CANCELLED r.completedAt = time.Now() @@ -342,13 +346,13 @@ func estimateCompressibility(data []byte) float64 { if len(data) < sampleSize { sampleSize = len(data) } - + // Count unique bytes in sample uniqueBytes := make(map[byte]struct{}) for i := 0; i < sampleSize; i++ { uniqueBytes[data[i]] = struct{}{} } - + // Calculate entropy-based compression estimate uniqueRatio := float64(len(uniqueBytes)) / float64(sampleSize) return 0.3 + (0.7 * uniqueRatio) // Base compression ratio between 0.3 and 1.0 @@ -357,13 +361,13 @@ func estimateCompressibility(data []byte) float64 { func estimateFileCompression(size int64, extension string) int64 { // Compression ratio estimates based on common file types compressionRatios := map[string]float64{ - ".txt": 0.4, // Text compresses well + ".txt": 0.4, // Text compresses well ".log": 0.4, ".json": 0.4, ".xml": 0.4, ".html": 0.4, ".csv": 0.5, - ".doc": 0.8, // Already compressed + ".doc": 0.8, // Already compressed ".docx": 0.95, ".pdf": 0.95, ".jpg": 0.99, // Already compressed @@ -376,11 +380,11 @@ func estimateFileCompression(size int64, extension string) int64 { ".gz": 0.99, ".rar": 0.99, } - + ratio, exists := compressionRatios[extension] if !exists { ratio = 0.7 // Default compression ratio for unknown types } - + return int64(float64(size) * ratio) -} \ No newline at end of file +} diff --git a/pkg/transport/transport.go b/pkg/transport/transport.go index 259b0bf..a7dd9dd 100644 --- a/pkg/transport/transport.go +++ b/pkg/transport/transport.go @@ -3,11 +3,13 @@ package transport import ( "encoding/binary" "errors" + "fmt" "net" "sync" "time" "github.com/Sudo-Ivan/reticulum-go/pkg/common" + "github.com/Sudo-Ivan/reticulum-go/pkg/packet" ) var ( @@ -19,18 +21,18 @@ const ( PathfinderM = 128 // Maximum number of hops PathRequestTTL = 300 // Time to live for path requests in seconds AnnounceTimeout = 15 // Timeout for announce responses in seconds - + // Link constants - EstablishmentTimeoutPerHop = 6 // Timeout for link establishment in seconds per hop - KeepaliveTimeoutFactor = 4 // RTT timeout factor for link timeout calculation - StaleGrace = 2 // Grace period in seconds for link timeout - Keepalive = 360 // Interval for sending keep-alive packets in seconds - StaleTime = 720 // Time after which link is considered stale - + EstablishmentTimeoutPerHop = 6 // Timeout for link establishment in seconds per hop + KeepaliveTimeoutFactor = 4 // RTT timeout factor for link timeout calculation + StaleGrace = 2 // Grace period in seconds for link timeout + Keepalive = 360 // Interval for sending keep-alive packets in seconds + StaleTime = 720 // Time after which link is considered stale + // Direction constants - OUT = 0x02 - IN = 0x01 - + OUT = 0x02 + IN = 0x01 + // Destination type constants SINGLE = 0x00 GROUP = 0x01 @@ -120,9 +122,9 @@ type Link struct { lastData time.Time rtt time.Duration establishedCb func() - closedCb func() - packetCb func([]byte) - resourceCb func(interface{}) bool + closedCb func() + packetCb func([]byte) + resourceCb func(interface{}) bool resourceStrategy int } @@ -142,7 +144,7 @@ func NewLink(dest []byte, establishedCallback func(), closedCallback func()) *Li lastOutbound: time.Now(), lastData: time.Now(), establishedCb: establishedCallback, - closedCb: closedCallback, + closedCb: closedCallback, } } @@ -189,17 +191,17 @@ func (l *Link) Teardown() { func (l *Link) Send(data []byte) error { l.lastOutbound = time.Now() l.lastData = time.Now() - + packet := &LinkPacket{ Destination: l.destination, - Data: data, - Timestamp: time.Now(), + Data: data, + Timestamp: time.Now(), } - + if l.rtt == 0 { l.rtt = l.InactiveFor() } - + return packet.send() } @@ -227,93 +229,93 @@ func (t *Transport) DeregisterAnnounceHandler(handler AnnounceHandler) { func (t *Transport) HasPath(destinationHash []byte) bool { t.pathLock.RLock() defer t.pathLock.RUnlock() - + path, exists := t.paths[string(destinationHash)] if !exists { return false } - + // Check if path is still valid (not expired) if time.Since(path.LastUpdated) > time.Duration(PathRequestTTL)*time.Second { delete(t.paths, string(destinationHash)) return false } - + return true } func (t *Transport) HopsTo(destinationHash []byte) uint8 { t.pathLock.RLock() defer t.pathLock.RUnlock() - + path, exists := t.paths[string(destinationHash)] if !exists { return PathfinderM } - + return path.Hops } func (t *Transport) NextHop(destinationHash []byte) []byte { t.pathLock.RLock() defer t.pathLock.RUnlock() - + path, exists := t.paths[string(destinationHash)] if !exists { return nil } - + return path.NextHop } func (t *Transport) NextHopInterface(destinationHash []byte) string { t.pathLock.RLock() defer t.pathLock.RUnlock() - + path, exists := t.paths[string(destinationHash)] if !exists { return "" } - + return path.Interface.GetName() } func (t *Transport) RequestPath(destinationHash []byte, onInterface string, tag []byte, recursive bool) error { packet := &PathRequest{ DestinationHash: destinationHash, - Tag: tag, - TTL: PathRequestTTL, - Recursive: recursive, + Tag: tag, + TTL: PathRequestTTL, + Recursive: recursive, } - + if onInterface != "" { return t.sendPathRequest(packet, onInterface) } - + return t.broadcastPathRequest(packet) } func (t *Transport) UpdatePath(destinationHash []byte, nextHop []byte, interfaceName string, hops uint8) { t.pathLock.Lock() defer t.pathLock.Unlock() - + iface, err := t.GetInterface(interfaceName) if err != nil { return } - + t.paths[string(destinationHash)] = &common.Path{ - Interface: iface, - NextHop: nextHop, - Hops: hops, - LastUpdated: time.Now(), + Interface: iface, + NextHop: nextHop, + Hops: hops, + LastUpdated: time.Now(), } } func (t *Transport) HandleAnnounce(destinationHash []byte, identity []byte, appData []byte) { t.handlerLock.RLock() defer t.handlerLock.RUnlock() - + for _, handler := range t.announceHandlers { handler.ReceivedAnnounce(destinationHash, identity, appData) } @@ -335,9 +337,9 @@ func (t *Transport) NewLink(dest []byte, establishedCallback func(), closedCallb type PathRequest struct { DestinationHash []byte - Tag []byte - TTL int - Recursive bool + Tag []byte + TTL int + Recursive bool } type LinkPacket struct { @@ -354,7 +356,7 @@ func (p *LinkPacket) send() error { header := make([]byte, 0, 64) header = append(header, 0x02) // Link packet type header = append(header, p.Destination...) - + // Add timestamp ts := make([]byte, 8) binary.BigEndian.PutUint64(ts, uint64(p.Timestamp.Unix())) @@ -417,7 +419,7 @@ func (t *Transport) broadcastPathRequest(req *PathRequest) error { if !iface.IsEnabled() { continue } - + if err := t.sendPathRequest(req, iface.GetName()); err != nil { lastErr = err } @@ -434,11 +436,11 @@ type PathRequestPacket struct { } type NetworkInterface struct { - Name string - Addr *net.UDPAddr - Conn *net.UDPConn - MTU int - Enabled bool + Name string + Addr *net.UDPAddr + Conn *net.UDPConn + MTU int + Enabled bool } func SendAnnounce(packet []byte) error { @@ -446,7 +448,7 @@ func SendAnnounce(packet []byte) error { if t == nil { return errors.New("transport not initialized") } - + // Send announce packet to all interfaces var lastErr error for _, iface := range t.interfaces { @@ -454,7 +456,7 @@ func SendAnnounce(packet []byte) error { lastErr = err } } - + return lastErr } @@ -487,7 +489,7 @@ func (t *Transport) handlePathRequest(data []byte, iface interface{}) { recursive := false if len(data) > 33 { - tag = data[33:len(data)-1] + tag = data[33 : len(data)-1] recursive = data[len(data)-1] == 0x01 } @@ -496,7 +498,7 @@ func (t *Transport) handlePathRequest(data []byte, iface interface{}) { // Create and send path response hops := t.HopsTo(destHash) nextHop := t.NextHop(destHash) - + response := make([]byte, 0, 64) response = append(response, 0x03) // Path Response type response = append(response, destHash...) @@ -514,7 +516,7 @@ func (t *Transport) handlePathRequest(data []byte, iface interface{}) { newData := make([]byte, len(data)) copy(newData, data) newData[32] = ttl - 1 // Decrease TTL - + for name, otherIface := range t.interfaces { if name != iface.(common.NetworkInterface).GetName() && otherIface.IsEnabled() { otherIface.Send(newData, "") @@ -536,7 +538,7 @@ func (t *Transport) handleLinkPacket(data []byte, iface interface{}) { if t.HasPath(dest) { nextHop := t.NextHop(dest) nextIface := t.NextHopInterface(dest) - + if iface, ok := t.interfaces[nextIface]; ok { iface.Send(data, string(nextHop)) } @@ -598,8 +600,33 @@ func (t *Transport) handleAnnouncePacket(data []byte, iface interface{}) { func (t *Transport) findLink(dest []byte) *Link { t.mutex.RLock() defer t.mutex.RUnlock() - + // This is a simplified version - you might want to maintain a map of active links // in the Transport struct for better performance return nil -} \ No newline at end of file +} + +func (t *Transport) SendPacket(p *packet.Packet) error { + t.mutex.RLock() + defer t.mutex.RUnlock() + + // Serialize packet + data, err := p.Serialize() + if err != nil { + return fmt.Errorf("failed to serialize packet: %w", err) + } + + // Find appropriate interface + destHash := p.Addresses[:packet.AddressSize] + path, exists := t.paths[string(destHash)] + if !exists { + return errors.New("no path to destination") + } + + // Send through interface + if err := path.Interface.Send(data, ""); err != nil { + return fmt.Errorf("failed to send packet: %w", err) + } + + return nil +}