This commit is contained in:
Sudo-Ivan
2024-12-30 03:50:52 -06:00
parent 0f5f5cbb13
commit decbd8f29a
16 changed files with 1046 additions and 970 deletions

42
To-Do
View File

@@ -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
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

View File

@@ -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)
}
}
}
}

View File

@@ -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)

View File

@@ -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)
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}
type InterfaceMode byte
type InterfaceType byte
// PacketCallback defines the function signature for packet handling
type PacketCallback func([]byte, interface{})

View File

@@ -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
}

View File

@@ -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
}
// Decrypt data
return decryptAESGCM(key, encryptedData)
}

View File

@@ -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
}
}

View File

@@ -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
}
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
}

View File

@@ -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
}
}
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
}

View File

@@ -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
}
}

View File

@@ -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
)
)

View File

@@ -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
}
}

View File

@@ -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)
}
}

View File

@@ -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
}
}
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
}