0.2.4
This commit is contained in:
40
To-Do
40
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
|
||||
@@ -100,3 +122,9 @@ Next Immediate Tasks:
|
||||
10. [ ] Add metrics collection for interfaces
|
||||
11. [ ] Implement client-side path caching
|
||||
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,14 +11,15 @@ 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 (
|
||||
@@ -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)
|
||||
}
|
||||
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()
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,35 +154,45 @@ func (c *Client) handleAnnounce(data []byte) {
|
||||
}
|
||||
|
||||
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)
|
||||
announceData := make([]byte, 0)
|
||||
|
||||
// Add timestamp (8 bytes, big-endian)
|
||||
timestamp := time.Now().Unix()
|
||||
// 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
|
||||
// Hops (1 byte)
|
||||
announceData = append(announceData, 0x00)
|
||||
|
||||
// Add flags (1 byte)
|
||||
announceData = append(announceData, byte(announce.ANNOUNCE_IDENTITY)) // Using identity announce type
|
||||
// Flags (1 byte)
|
||||
announceData = append(announceData, 0x00)
|
||||
|
||||
// Add app data with length prefix
|
||||
// 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()
|
||||
|
||||
|
||||
@@ -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,8 +4,8 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/pelletier/go-toml"
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
"github.com/pelletier/go-toml"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -88,6 +88,14 @@ func CreateDefaultConfig(path string) error {
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -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
|
||||
@@ -18,7 +18,7 @@ type Path struct {
|
||||
}
|
||||
|
||||
// Common callbacks
|
||||
type ProofRequestedCallback func(interface{}) bool
|
||||
type ProofRequestedCallback func([]byte, []byte)
|
||||
type LinkEstablishedCallback func(interface{})
|
||||
|
||||
// Request handler
|
||||
@@ -28,3 +28,9 @@ type RequestHandler struct {
|
||||
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")
|
||||
}
|
||||
@@ -348,3 +358,14 @@ 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,6 +22,12 @@ 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 {
|
||||
@@ -36,351 +38,16 @@ type Identity struct {
|
||||
ratchets map[string][]byte
|
||||
ratchetExpiry map[string]int64
|
||||
mutex sync.RWMutex
|
||||
appData []byte
|
||||
}
|
||||
|
||||
func New() (*Identity, error) {
|
||||
i := &Identity{
|
||||
ratchets: make(map[string][]byte),
|
||||
ratchetExpiry: make(map[string]int64),
|
||||
}
|
||||
|
||||
// Generate X25519 key pair
|
||||
var err error
|
||||
i.privateKey = make([]byte, curve25519.ScalarSize)
|
||||
if _, err = io.ReadFull(rand.Reader, i.privateKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Generate public key
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
i.signingKey = privateKey
|
||||
i.verificationKey = publicKey
|
||||
|
||||
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) ToBytes() []byte {
|
||||
data := make([]byte, KeySize/8)
|
||||
copy(data[:32], i.privateKey)
|
||||
copy(data[32:], i.signingKey)
|
||||
return data
|
||||
}
|
||||
|
||||
func (i *Identity) SaveToFile(path string) error {
|
||||
return os.WriteFile(path, i.ToBytes(), 0600)
|
||||
}
|
||||
|
||||
func LoadFromFile(path string) (*Identity, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return FromBytes(data)
|
||||
}
|
||||
|
||||
func (i *Identity) Encrypt(plaintext []byte, ratchets []byte) ([]byte, error) {
|
||||
// Generate ephemeral key pair
|
||||
ephemeralPrivate := make([]byte, curve25519.ScalarSize)
|
||||
if _, err := io.ReadFull(rand.Reader, ephemeralPrivate); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ephemeralPublic, err := curve25519.X25519(ephemeralPrivate, curve25519.Basepoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Perform key exchange
|
||||
sharedSecret, err := curve25519.X25519(ephemeralPrivate, i.publicKey)
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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) 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]
|
||||
}
|
||||
|
||||
func FullHash(data []byte) []byte {
|
||||
hash := sha256.Sum256(data)
|
||||
return hash[:]
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
hash := make([]byte, TruncatedHashLen/8)
|
||||
_, err := hex.Decode(hash, []byte(hexHash))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return hash, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func LoadIdentity(cfg *common.ReticulumConfig) (*Identity, error) {
|
||||
if cfg == nil {
|
||||
return nil, errors.New("config cannot be nil")
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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) 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 {
|
||||
return nil
|
||||
}
|
||||
ratchetID := fmt.Sprintf("%d", time.Now().Unix())
|
||||
i.AddRatchet(ratchetID, key)
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return latestKey
|
||||
}
|
||||
|
||||
func (i *Identity) EncryptSymmetric(plaintext []byte) ([]byte, error) {
|
||||
key := i.GetCurrentRatchetKey()
|
||||
if key == nil {
|
||||
return nil, errors.New("no ratchet key available")
|
||||
}
|
||||
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
|
||||
@@ -396,15 +63,11 @@ func (i *Identity) EncryptSymmetric(plaintext []byte) ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return gcm.Seal(nonce, nonce, plaintext, nil), nil
|
||||
}
|
||||
|
||||
func (i *Identity) DecryptSymmetric(ciphertext []byte) ([]byte, error) {
|
||||
key := i.GetCurrentRatchetKey()
|
||||
if key == nil {
|
||||
return nil, errors.New("no ratchet key available")
|
||||
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
|
||||
@@ -417,44 +80,264 @@ func (i *Identity) DecryptSymmetric(ciphertext []byte) ([]byte, error) {
|
||||
|
||||
nonceSize := gcm.NonceSize()
|
||||
if len(ciphertext) < nonceSize {
|
||||
return nil, errors.New("ciphertext too short")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
|
||||
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decryption failed: %w", err)
|
||||
return nil, 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
|
||||
}
|
||||
|
||||
func New() (*Identity, error) {
|
||||
i := &Identity{
|
||||
publicKey: append([]byte{}, publicKey...),
|
||||
ratchets: make(map[string][]byte),
|
||||
ratchetExpiry: make(map[string]int64),
|
||||
}
|
||||
|
||||
// Generate Ed25519 verification key from the X25519 public key
|
||||
hash := sha256.New()
|
||||
hash.Write(publicKey)
|
||||
seed := hash.Sum(nil)
|
||||
// Generate X25519 key pair
|
||||
i.privateKey = make([]byte, curve25519.ScalarSize)
|
||||
if _, err := io.ReadFull(rand.Reader, i.privateKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Use the first 32 bytes of the hash as the verification key
|
||||
i.verificationKey = ed25519.PublicKey(seed[:32])
|
||||
var err error
|
||||
i.publicKey, err = curve25519.X25519(i.privateKey, curve25519.Basepoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Generate Ed25519 signing keypair
|
||||
pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
i.signingKey = privKey
|
||||
i.verificationKey = pubKey
|
||||
|
||||
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) GetPrivateKey() []byte {
|
||||
return append(i.privateKey, i.signingKey...)
|
||||
}
|
||||
|
||||
func (i *Identity) Sign(data []byte) []byte {
|
||||
return ed25519.Sign(i.signingKey, data)
|
||||
}
|
||||
|
||||
func (i *Identity) Verify(data []byte, signature []byte) bool {
|
||||
return ed25519.Verify(i.verificationKey, data, signature)
|
||||
}
|
||||
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ephemeralPublic, err := curve25519.X25519(ephemeralPrivate, curve25519.Basepoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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 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
|
||||
}
|
||||
|
||||
// Encrypt using AES-GCM
|
||||
ciphertext, err := encryptAESGCM(key, plaintext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return append(ephemeralPublic, ciphertext...), nil
|
||||
}
|
||||
|
||||
func (i *Identity) Hash() []byte {
|
||||
h := sha256.New()
|
||||
h.Write(i.GetPublicKey())
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
func TruncatedHash(data []byte) []byte {
|
||||
h := sha256.New()
|
||||
h.Write(data)
|
||||
fullHash := h.Sum(nil)
|
||||
return fullHash[:TruncatedHashLen/8]
|
||||
}
|
||||
|
||||
func GetRandomHash() []byte {
|
||||
randomData := make([]byte, TruncatedHashLen/8)
|
||||
rand.Read(randomData)
|
||||
return TruncatedHash(randomData)
|
||||
}
|
||||
|
||||
func Remember(packetHash, destHash []byte, publicKey []byte, appData []byte) {
|
||||
knownDestinations[string(destHash)] = []interface{}{
|
||||
time.Now().Unix(),
|
||||
packetHash,
|
||||
publicKey,
|
||||
appData,
|
||||
}
|
||||
}
|
||||
|
||||
func ValidateAnnounce(packet []byte, destHash []byte, publicKey []byte, signature []byte, appData []byte) bool {
|
||||
if len(publicKey) != KeySize/8 {
|
||||
return false
|
||||
}
|
||||
|
||||
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) {
|
||||
// TODO: Implement persistence
|
||||
// For now just create new identity
|
||||
return New()
|
||||
}
|
||||
|
||||
func (i *Identity) GenerateHMACKey() []byte {
|
||||
hmacKey := make([]byte, HMACKeySize)
|
||||
if _, err := io.ReadFull(rand.Reader, hmacKey); err != nil {
|
||||
return nil
|
||||
}
|
||||
return hmacKey
|
||||
}
|
||||
|
||||
func (i *Identity) ComputeHMAC(key, message []byte) []byte {
|
||||
h := hmac.New(sha256.New, key)
|
||||
h.Write(message)
|
||||
return h.Sum(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, RatchetSize/8)
|
||||
if _, err := io.ReadFull(rand.Reader, key); err != nil {
|
||||
return nil
|
||||
}
|
||||
i.ratchets[string(key)] = key
|
||||
i.ratchetExpiry[string(key)] = time.Now().Unix() + RatchetExpiry
|
||||
return key
|
||||
}
|
||||
|
||||
// Return most recent ratchet key
|
||||
var latestKey []byte
|
||||
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, key []byte) ([]byte, error) {
|
||||
if len(key) != 32 {
|
||||
return nil, errors.New("invalid key length")
|
||||
}
|
||||
return encryptAESGCM(key, plaintext)
|
||||
}
|
||||
|
||||
func (i *Identity) DecryptSymmetric(ciphertext []byte, key []byte) ([]byte, error) {
|
||||
if len(key) != 32 {
|
||||
return nil, errors.New("invalid key length")
|
||||
}
|
||||
return decryptAESGCM(key, ciphertext)
|
||||
}
|
||||
|
||||
func (i *Identity) Decrypt(ciphertext []byte) ([]byte, error) {
|
||||
if len(ciphertext) < curve25519.PointSize {
|
||||
return nil, errors.New("ciphertext too short")
|
||||
}
|
||||
|
||||
ephemeralPublic := ciphertext[:curve25519.PointSize]
|
||||
encryptedData := ciphertext[curve25519.PointSize:]
|
||||
|
||||
// Compute shared secret
|
||||
sharedSecret, err := curve25519.X25519(i.privateKey, ephemeralPublic)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Decrypt data
|
||||
return decryptAESGCM(key, encryptedData)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
)
|
||||
@@ -15,15 +16,27 @@ const (
|
||||
)
|
||||
|
||||
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) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
)
|
||||
|
||||
@@ -41,17 +42,19 @@ type TCPClientInterface struct {
|
||||
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,139 +288,62 @@ func (tc *TCPClientInterface) GetName() string {
|
||||
return tc.Name
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) GetPacketCallback() common.PacketCallback {
|
||||
tc.mutex.RLock()
|
||||
defer tc.mutex.RUnlock()
|
||||
return tc.packetCallback
|
||||
}
|
||||
|
||||
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
|
||||
server net.Listener
|
||||
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
|
||||
spawned []*TCPClientInterface
|
||||
spawnedMutex sync.RWMutex
|
||||
packetCallback func([]byte, interface{})
|
||||
packetCallback common.PacketCallback
|
||||
detached bool
|
||||
}
|
||||
|
||||
func NewTCPServer(name string, bindAddr string, bindPort int, preferIPv6 bool, i2pTunneled bool) (*TCPServerInterface, error) {
|
||||
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 == "" {
|
||||
@@ -427,17 +353,37 @@ func (ts *TCPServerInterface) String() string {
|
||||
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
|
||||
addr *net.UDPAddr
|
||||
targetAddr *net.UDPAddr
|
||||
mutex sync.RWMutex
|
||||
readBuffer []byte
|
||||
txBytes uint64
|
||||
rxBytes uint64
|
||||
mtu int
|
||||
bitrate int
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
func NewUDPInterface(name string, listenAddr string, targetAddr string) (*UDPInterface, error) {
|
||||
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 fmt.Errorf("invalid target address: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
197
pkg/link/link.go
197
pkg/link/link.go
@@ -6,14 +6,15 @@ 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 (
|
||||
@@ -33,11 +34,16 @@ const (
|
||||
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{}
|
||||
destination *destination.Destination
|
||||
status byte
|
||||
establishedAt time.Time
|
||||
lastInbound time.Time
|
||||
@@ -52,57 +58,87 @@ type Link struct {
|
||||
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)
|
||||
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{},
|
||||
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{},
|
||||
resourceStrategy: ACCEPT_NONE,
|
||||
establishedCallback: establishedCb,
|
||||
closedCallback: closedCb,
|
||||
}
|
||||
}
|
||||
|
||||
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)...)
|
||||
destPublicKey := l.destination.GetPublicKey()
|
||||
if destPublicKey == nil {
|
||||
return errors.New("destination has no public key")
|
||||
}
|
||||
|
||||
// Encrypt and send identification
|
||||
err := l.SendPacket(idMsg)
|
||||
// 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
|
||||
@@ -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 {
|
||||
@@ -361,7 +401,7 @@ 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 {
|
||||
@@ -375,49 +415,6 @@ func (l *Link) SetResourceStrategy(strategy byte) error {
|
||||
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 {
|
||||
|
||||
@@ -1,64 +1,110 @@
|
||||
package packet
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
)
|
||||
|
||||
const (
|
||||
// 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 // Maximum size of payload data
|
||||
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
|
||||
Addresses []byte
|
||||
Context byte
|
||||
Data []byte
|
||||
AccessCode []byte // Optional: Only present if IFAC flag is set
|
||||
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
|
||||
}
|
||||
|
||||
// Set propagation type
|
||||
p.Header[0] |= (propagationType << 3) & PropagationFlags
|
||||
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 destination type
|
||||
p.Header[0] |= (destinationType << 1) & DestinationFlags
|
||||
p := &Packet{
|
||||
Header: [2]byte{flags, hops},
|
||||
Addresses: make([]byte, AddressSize),
|
||||
Data: data,
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
||||
@@ -2,13 +2,12 @@ package resource
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -65,6 +64,7 @@ type Resource struct {
|
||||
mutex sync.RWMutex
|
||||
data []byte
|
||||
fileHandle io.ReadWriteSeeker
|
||||
fileName string
|
||||
hash []byte
|
||||
randomHash []byte
|
||||
originalHash []byte
|
||||
@@ -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,7 +142,7 @@ 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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
@@ -603,3 +605,28 @@ func (t *Transport) findLink(dest []byte) *Link {
|
||||
// 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