0.2.4
This commit is contained in:
42
To-Do
42
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
|
||||
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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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{})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
277
pkg/link/link.go
277
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user