From 28d46921d3ac15a5a76a7b0d9ee99d4ff8db80b4 Mon Sep 17 00:00:00 2001 From: Sudo-Ivan Date: Tue, 31 Dec 2024 13:49:05 -0600 Subject: [PATCH] 0.3.0 --- cmd/reticulum/main.go | 119 +++++++++++----- internal/config/config.go | 35 +++-- pkg/announce/announce.go | 2 +- pkg/common/config.go | 4 + pkg/common/constants.go | 1 + pkg/common/interfaces.go | 50 +++++++ pkg/common/types.go | 2 +- pkg/interfaces/auto.go | 277 +++++++++++++++++++++++++++++++++++++ pkg/transport/transport.go | 8 +- 9 files changed, 445 insertions(+), 53 deletions(-) create mode 100644 pkg/interfaces/auto.go diff --git a/cmd/reticulum/main.go b/cmd/reticulum/main.go index 5a06913..eb66590 100644 --- a/cmd/reticulum/main.go +++ b/cmd/reticulum/main.go @@ -24,24 +24,38 @@ func NewReticulum(cfg *common.ReticulumConfig) (*Reticulum, error) { cfg = config.DefaultConfig() } - // Initialize transport t, err := transport.NewTransport(cfg) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to initialize transport: %v", err) } return &Reticulum{ - config: cfg, - transport: t, + config: cfg, + transport: t, + interfaces: make([]interfaces.Interface, 0), }, nil } func (r *Reticulum) Start() error { - for _, ifaceConfig := range r.config.Interfaces { + log.Printf("Starting Reticulum...") + + if err := r.transport.Start(); err != nil { + return fmt.Errorf("failed to start transport: %v", err) + } + log.Printf("Transport started successfully") + + for name, ifaceConfig := range r.config.Interfaces { + if !ifaceConfig.Enabled { + log.Printf("Skipping disabled interface %s", name) + continue + } + + log.Printf("Configuring interface %s (type=%s)...", name, ifaceConfig.Type) var iface interfaces.Interface switch ifaceConfig.Type { case "TCPClientInterface": + log.Printf("Creating TCP client interface %s -> %s:%d", name, ifaceConfig.TargetHost, ifaceConfig.TargetPort) client, err := interfaces.NewTCPClient( ifaceConfig.Name, ifaceConfig.TargetHost, @@ -51,18 +65,16 @@ func (r *Reticulum) Start() error { ifaceConfig.Enabled, ) if err != nil { - log.Printf("Failed to create TCP interface %s: %v", ifaceConfig.Name, err) + if r.config.PanicOnInterfaceErr { + return fmt.Errorf("failed to create TCP client interface %s: %v", name, err) + } + log.Printf("Failed to create TCP client interface %s: %v", name, err) continue } - - if err := client.Start(); err != nil { - log.Printf("Failed to start TCP interface %s: %v", ifaceConfig.Name, err) - continue - } - iface = client case "TCPServerInterface": + log.Printf("Creating TCP server interface %s on %s:%d", name, ifaceConfig.Address, ifaceConfig.Port) server, err := interfaces.NewTCPServer( ifaceConfig.Name, ifaceConfig.Address, @@ -72,41 +84,51 @@ func (r *Reticulum) Start() error { ifaceConfig.PreferIPv6, ) if err != nil { - log.Printf("Failed to create TCP server interface %s: %v", ifaceConfig.Name, err) + if r.config.PanicOnInterfaceErr { + return fmt.Errorf("failed to create TCP server interface %s: %v", name, err) + } + log.Printf("Failed to create TCP server interface %s: %v", name, err) continue } - - if err := server.Start(); err != nil { - log.Printf("Failed to start TCP server interface %s: %v", ifaceConfig.Name, err) - continue - } - iface = server case "UDPInterface": addr := fmt.Sprintf("%s:%d", ifaceConfig.Address, ifaceConfig.Port) - + target := "" + if ifaceConfig.TargetAddress != "" { + target = fmt.Sprintf("%s:%d", ifaceConfig.TargetHost, ifaceConfig.TargetPort) + } + log.Printf("Creating UDP interface %s on %s -> %s", name, addr, target) udp, err := interfaces.NewUDPInterface( ifaceConfig.Name, addr, - "", // No target address for server initially + target, ifaceConfig.Enabled, ) if err != nil { - log.Printf("Failed to create UDP interface %s: %v", ifaceConfig.Name, err) + if r.config.PanicOnInterfaceErr { + return fmt.Errorf("failed to create UDP interface %s: %v", name, err) + } + log.Printf("Failed to create UDP interface %s: %v", name, err) continue } - - if err := udp.Start(); err != nil { - log.Printf("Failed to start UDP interface %s: %v", ifaceConfig.Name, err) - continue - } - iface = udp case "AutoInterface": - log.Printf("AutoInterface type not yet implemented") - continue + log.Printf("Creating Auto interface %s (group=%s, discovery=%d, data=%d)", + name, ifaceConfig.GroupID, ifaceConfig.DiscoveryPort, ifaceConfig.DataPort) + auto, err := interfaces.NewAutoInterface( + ifaceConfig.Name, + ifaceConfig, + ) + if err != nil { + if r.config.PanicOnInterfaceErr { + return fmt.Errorf("failed to create Auto interface %s: %v", name, err) + } + log.Printf("Failed to create Auto interface %s: %v", name, err) + continue + } + iface = auto default: log.Printf("Unknown interface type: %s", ifaceConfig.Type) @@ -114,50 +136,71 @@ func (r *Reticulum) Start() error { } if iface != nil { - // Set packet callback to transport - iface.SetPacketCallback(r.transport.HandlePacket) + log.Printf("Starting interface %s...", name) + if err := iface.Start(); err != nil { + if r.config.PanicOnInterfaceErr { + return fmt.Errorf("failed to start interface %s: %v", name, err) + } + log.Printf("Failed to start interface %s: %v", name, err) + continue + } + + netIface := iface.(common.NetworkInterface) + + callback := func(data []byte, ni common.NetworkInterface) { + r.transport.HandlePacket(data, ni) + } + + netIface.SetPacketCallback(callback) r.interfaces = append(r.interfaces, iface) log.Printf("Created and started interface %s (type=%v, enabled=%v)", iface.GetName(), iface.GetType(), iface.IsEnabled()) + log.Printf("Interface %s started successfully", name) } } log.Printf("Reticulum initialized with config at: %s", r.config.ConfigPath) + log.Printf("Press Ctrl+C to stop...") return nil } func (r *Reticulum) Stop() error { + for _, iface := range r.interfaces { + if err := iface.Stop(); err != nil { + log.Printf("Error stopping interface %s: %v", iface.GetName(), err) + } + } + if err := r.transport.Close(); err != nil { - return err + return fmt.Errorf("failed to close transport: %v", err) } return nil } func main() { - // Initialize configuration + log.Printf("Initializing Reticulum...") + cfg, err := config.InitConfig() if err != nil { log.Fatalf("Failed to initialize config: %v", err) } - // Create new reticulum instance r, err := NewReticulum(cfg) if err != nil { log.Fatalf("Failed to create Reticulum instance: %v", err) } - // Start reticulum if err := r.Start(); err != nil { log.Fatalf("Failed to start Reticulum: %v", err) } - // Wait for interrupt signal sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) <-sigChan - // Clean shutdown + log.Printf("\nShutting down...") if err := r.Stop(); err != nil { log.Printf("Error during shutdown: %v", err) } + log.Printf("Goodbye!") } diff --git a/internal/config/config.go b/internal/config/config.go index 690bf3a..341acaf 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -16,7 +16,7 @@ const ( func DefaultConfig() *common.ReticulumConfig { return &common.ReticulumConfig{ - EnableTransport: false, + EnableTransport: true, ShareInstance: true, SharedInstancePort: DefaultSharedInstancePort, InstanceControlPort: DefaultInstanceControlPort, @@ -74,26 +74,38 @@ func SaveConfig(cfg *common.ReticulumConfig) error { func CreateDefaultConfig(path string) error { cfg := DefaultConfig() - // Add default interface - cfg.Interfaces["Default Interface"] = &common.InterfaceConfig{ - Type: "AutoInterface", - Enabled: false, + // Add Auto Interface + cfg.Interfaces["Auto Discovery"] = &common.InterfaceConfig{ + Type: "AutoInterface", + Enabled: true, + GroupID: "reticulum", + DiscoveryScope: "link", + DiscoveryPort: 29716, + DataPort: 42671, } - // Add default quad4net interface - cfg.Interfaces["quad4net tcp"] = &common.InterfaceConfig{ + // Add RNS Amsterdam Testnet interface + cfg.Interfaces["RNS Testnet Amsterdam"] = &common.InterfaceConfig{ Type: "TCPClientInterface", Enabled: true, - TargetHost: "rns.quad4.io", + TargetHost: "amsterdam.connect.reticulum.network", + TargetPort: 4965, + } + + // Add RNS BetweenTheBorders Testnet interface + cfg.Interfaces["RNS Testnet BetweenTheBorders"] = &common.InterfaceConfig{ + Type: "TCPClientInterface", + Enabled: true, + TargetHost: "reticulum.betweentheborders.com", TargetPort: 4242, } - // Add default UDP interface - cfg.Interfaces["local udp"] = &common.InterfaceConfig{ + // Add local UDP interface + cfg.Interfaces["Local UDP"] = &common.InterfaceConfig{ Type: "UDPInterface", Enabled: true, Address: "0.0.0.0", - Port: 37696, // Default RNS port + Port: 37696, } data, err := toml.Marshal(cfg) @@ -101,7 +113,6 @@ func CreateDefaultConfig(path string) error { return err } - // Create config directory if it doesn't exist if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { return err } diff --git a/pkg/announce/announce.go b/pkg/announce/announce.go index 3910320..869ad4b 100644 --- a/pkg/announce/announce.go +++ b/pkg/announce/announce.go @@ -49,7 +49,7 @@ const ( type AnnounceHandler interface { AspectFilter() []string - ReceivedAnnounce(destinationHash []byte, announcedIdentity *identity.Identity, appData []byte) error + ReceivedAnnounce(destinationHash []byte, announcedIdentity interface{}, appData []byte) error ReceivePathResponses() bool } diff --git a/pkg/common/config.go b/pkg/common/config.go index 0fd77d5..7914dc6 100644 --- a/pkg/common/config.go +++ b/pkg/common/config.go @@ -34,6 +34,10 @@ type InterfaceConfig struct { MaxReconnTries int `toml:"max_reconnect_tries"` Bitrate int64 `toml:"bitrate"` MTU int `toml:"mtu"` + GroupID string + DiscoveryScope string + DiscoveryPort int + DataPort int } // ReticulumConfig represents the main configuration structure diff --git a/pkg/common/constants.go b/pkg/common/constants.go index 5a71330..23d38ae 100644 --- a/pkg/common/constants.go +++ b/pkg/common/constants.go @@ -9,6 +9,7 @@ const ( IF_TYPE_I2P IF_TYPE_BLUETOOTH IF_TYPE_SERIAL + IF_TYPE_AUTO // Interface Modes IF_MODE_FULL InterfaceMode = iota diff --git a/pkg/common/interfaces.go b/pkg/common/interfaces.go index 4cd11bb..22a3d9f 100644 --- a/pkg/common/interfaces.go +++ b/pkg/common/interfaces.go @@ -4,6 +4,7 @@ import ( "net" "sync" "time" + "encoding/binary" ) // NetworkInterface defines the interface for all network communication methods @@ -68,6 +69,7 @@ func NewBaseInterface(name string, ifaceType InterfaceType, enabled bool) BaseIn Mode: IF_MODE_FULL, Enabled: enabled, MTU: DEFAULT_MTU, + Bitrate: BITRATE_MINIMUM, } } @@ -138,3 +140,51 @@ func (i *BaseInterface) Disable() { i.Enabled = false i.Online = false } + +// Default implementations that should be overridden by specific interfaces +func (i *BaseInterface) Start() error { + return nil +} + +func (i *BaseInterface) Stop() error { + return nil +} + +func (i *BaseInterface) GetConn() net.Conn { + return nil +} + +func (i *BaseInterface) Send(data []byte, address string) error { + return i.ProcessOutgoing(data) +} + +func (i *BaseInterface) ProcessIncoming(data []byte) { + if i.PacketCallback != nil { + i.PacketCallback(data, i) + } +} + +func (i *BaseInterface) ProcessOutgoing(data []byte) error { + return nil +} + +func (i *BaseInterface) SendPathRequest(data []byte) error { + return i.Send(data, "") +} + +func (i *BaseInterface) SendLinkPacket(dest []byte, data []byte, timestamp time.Time) error { + // Create link packet + packet := make([]byte, 0, len(dest)+len(data)+9) // 1 byte type + dest + 8 byte timestamp + packet = append(packet, 0x02) // Link packet type + packet = append(packet, dest...) + + // Add timestamp + ts := make([]byte, 8) + binary.BigEndian.PutUint64(ts, uint64(timestamp.Unix())) + packet = append(packet, ts...) + + // Add data + packet = append(packet, data...) + + return i.Send(packet, "") +} \ No newline at end of file diff --git a/pkg/common/types.go b/pkg/common/types.go index 2bc84fd..fe06585 100644 --- a/pkg/common/types.go +++ b/pkg/common/types.go @@ -20,7 +20,7 @@ type Path struct { // Common callbacks type ProofRequestedCallback func([]byte, []byte) type LinkEstablishedCallback func(interface{}) -type PacketCallback func([]byte, interface{}) +type PacketCallback func([]byte, NetworkInterface) // RequestHandler manages path requests and responses type RequestHandler struct { diff --git a/pkg/interfaces/auto.go b/pkg/interfaces/auto.go new file mode 100644 index 0000000..3253050 --- /dev/null +++ b/pkg/interfaces/auto.go @@ -0,0 +1,277 @@ +package interfaces + +import ( + "fmt" + "log" + "net" + "sync" + "time" + + "github.com/Sudo-Ivan/reticulum-go/pkg/common" +) + +const ( + DEFAULT_DISCOVERY_PORT = 29716 + DEFAULT_DATA_PORT = 42671 + BITRATE_GUESS = 10 * 1000 * 1000 + PEERING_TIMEOUT = 7500 * time.Millisecond + SCOPE_LINK = "2" + SCOPE_ADMIN = "4" + SCOPE_SITE = "5" + SCOPE_ORGANISATION = "8" + SCOPE_GLOBAL = "e" +) + +type AutoInterface struct { + BaseInterface + groupID []byte + discoveryPort int + dataPort int + discoveryScope string + peers map[string]*Peer + linkLocalAddrs []string + adoptedInterfaces map[string]string + interfaceServers map[string]*net.UDPConn + multicastEchoes map[string]time.Time + mutex sync.RWMutex + outboundConn *net.UDPConn +} + +type Peer struct { + ifaceName string + lastHeard time.Time + conn *net.UDPConn +} + +func NewAutoInterface(name string, config *common.InterfaceConfig) (*AutoInterface, error) { + base := &BaseInterface{ + Name: name, + Mode: common.IF_MODE_FULL, + Type: common.IF_TYPE_AUTO, + Online: false, + Enabled: config.Enabled, + Detached: false, + IN: false, + OUT: false, + MTU: common.DEFAULT_MTU, + Bitrate: BITRATE_MINIMUM, + } + + ai := &AutoInterface{ + BaseInterface: *base, + discoveryPort: DEFAULT_DISCOVERY_PORT, + dataPort: DEFAULT_DATA_PORT, + discoveryScope: SCOPE_LINK, + peers: make(map[string]*Peer), + linkLocalAddrs: make([]string, 0), + adoptedInterfaces: make(map[string]string), + interfaceServers: make(map[string]*net.UDPConn), + multicastEchoes: make(map[string]time.Time), + } + + if config.Port != 0 { + ai.discoveryPort = config.Port + } + + if config.GroupID != "" { + ai.groupID = []byte(config.GroupID) + } else { + ai.groupID = []byte("reticulum") + } + + return ai, nil +} + +func (ai *AutoInterface) Start() error { + interfaces, err := net.Interfaces() + if err != nil { + return fmt.Errorf("failed to list interfaces: %v", err) + } + + for _, iface := range interfaces { + if err := ai.configureInterface(&iface); err != nil { + log.Printf("Failed to configure interface %s: %v", iface.Name, err) + continue + } + } + + if len(ai.adoptedInterfaces) == 0 { + return fmt.Errorf("no suitable interfaces found") + } + + go ai.peerJobs() + return nil +} + +func (ai *AutoInterface) configureInterface(iface *net.Interface) error { + addrs, err := iface.Addrs() + if err != nil { + return err + } + + for _, addr := range addrs { + if ipnet, ok := addr.(*net.IPNet); ok && ipnet.IP.IsLinkLocalUnicast() { + ai.adoptedInterfaces[iface.Name] = ipnet.IP.String() + ai.multicastEchoes[iface.Name] = time.Now() + + if err := ai.startDiscoveryListener(iface); err != nil { + return err + } + + if err := ai.startDataListener(iface); err != nil { + return err + } + + break + } + } + + return nil +} + +func (ai *AutoInterface) startDiscoveryListener(iface *net.Interface) error { + addr := &net.UDPAddr{ + IP: net.ParseIP(fmt.Sprintf("ff%s%s::1", ai.discoveryScope, SCOPE_LINK)), + Port: ai.discoveryPort, + Zone: iface.Name, + } + + conn, err := net.ListenMulticastUDP("udp6", iface, addr) + if err != nil { + return err + } + + go ai.handleDiscovery(conn, iface.Name) + return nil +} + +func (ai *AutoInterface) startDataListener(iface *net.Interface) error { + addr := &net.UDPAddr{ + IP: net.IPv6zero, + Port: ai.dataPort, + Zone: iface.Name, + } + + conn, err := net.ListenUDP("udp6", addr) + if err != nil { + return err + } + + ai.interfaceServers[iface.Name] = conn + go ai.handleData(conn) + return nil +} + +func (ai *AutoInterface) handleDiscovery(conn *net.UDPConn, ifaceName string) { + buf := make([]byte, 1024) + for { + n, remoteAddr, err := conn.ReadFromUDP(buf) + if err != nil { + log.Printf("Discovery read error: %v", err) + continue + } + + ai.handlePeerAnnounce(remoteAddr, buf[:n], ifaceName) + } +} + +func (ai *AutoInterface) handleData(conn *net.UDPConn) { + buf := make([]byte, ai.GetMTU()) + for { + n, _, err := conn.ReadFromUDP(buf) + if err != nil { + if !ai.IsDetached() { + log.Printf("Data read error: %v", err) + } + return + } + + if callback := ai.GetPacketCallback(); callback != nil { + callback(buf[:n], ai) + } + } +} + +func (ai *AutoInterface) handlePeerAnnounce(addr *net.UDPAddr, data []byte, ifaceName string) { + ai.mutex.Lock() + defer ai.mutex.Unlock() + + peerAddr := addr.IP.String() + + for _, localAddr := range ai.linkLocalAddrs { + if peerAddr == localAddr { + ai.multicastEchoes[ifaceName] = time.Now() + return + } + } + + if _, exists := ai.peers[peerAddr]; !exists { + ai.peers[peerAddr] = &Peer{ + ifaceName: ifaceName, + lastHeard: time.Now(), + } + log.Printf("Added peer %s on %s", peerAddr, ifaceName) + } else { + ai.peers[peerAddr].lastHeard = time.Now() + } +} + +func (ai *AutoInterface) peerJobs() { + ticker := time.NewTicker(PEERING_TIMEOUT) + for range ticker.C { + ai.mutex.Lock() + now := time.Now() + + for addr, peer := range ai.peers { + if now.Sub(peer.lastHeard) > PEERING_TIMEOUT { + delete(ai.peers, addr) + log.Printf("Removed timed out peer %s", addr) + } + } + + ai.mutex.Unlock() + } +} + +func (ai *AutoInterface) Send(data []byte, address string) error { + ai.mutex.RLock() + defer ai.mutex.RUnlock() + + for _, peer := range ai.peers { + addr := &net.UDPAddr{ + IP: net.ParseIP(address), + Port: ai.dataPort, + Zone: peer.ifaceName, + } + + if ai.outboundConn == nil { + var err error + ai.outboundConn, err = net.ListenUDP("udp6", &net.UDPAddr{Port: 0}) + if err != nil { + return err + } + } + + if _, err := ai.outboundConn.WriteToUDP(data, addr); err != nil { + log.Printf("Failed to send to peer %s: %v", address, err) + continue + } + } + + return nil +} + +func (ai *AutoInterface) Stop() error { + ai.mutex.Lock() + defer ai.mutex.Unlock() + + for _, server := range ai.interfaceServers { + server.Close() + } + + if ai.outboundConn != nil { + ai.outboundConn.Close() + } + + return nil +} diff --git a/pkg/transport/transport.go b/pkg/transport/transport.go index fb264f4..3df48a6 100644 --- a/pkg/transport/transport.go +++ b/pkg/transport/transport.go @@ -644,7 +644,7 @@ func (t *Transport) handleAnnouncePacket(data []byte, iface common.NetworkInterf // Use identity package's GetRandomHash announceHash := identity.GetRandomHash() - + // Use interface name in announce handling if iface != nil { t.HandleAnnounce(destHash, identityData, appData, announceHash) @@ -789,3 +789,9 @@ func (l *Link) HandleResource(resource interface{}) bool { return false } } + +func (t *Transport) Start() error { + t.mutex.Lock() + defer t.mutex.Unlock() + return nil +}