package main import ( "encoding/binary" "flag" "fmt" "os" "os/signal" "path/filepath" "runtime" "sync" "syscall" "time" "git.quad4.io/Networks/Reticulum-Go/internal/config" "git.quad4.io/Networks/Reticulum-Go/internal/storage" "git.quad4.io/Networks/Reticulum-Go/pkg/buffer" "git.quad4.io/Networks/Reticulum-Go/pkg/channel" "git.quad4.io/Networks/Reticulum-Go/pkg/common" "git.quad4.io/Networks/Reticulum-Go/pkg/debug" "git.quad4.io/Networks/Reticulum-Go/pkg/destination" "git.quad4.io/Networks/Reticulum-Go/pkg/identity" "git.quad4.io/Networks/Reticulum-Go/pkg/interfaces" "git.quad4.io/Networks/Reticulum-Go/pkg/packet" "git.quad4.io/Networks/Reticulum-Go/pkg/transport" ) var ( interceptPackets = flag.Bool("intercept-packets", false, "Enable packet interception") interceptOutput = flag.String("intercept-output", "packets.log", "Output file for intercepted packets") ) const ( ANNOUNCE_RATE_TARGET = 3600 // Default target time between announces (1 hour) ANNOUNCE_RATE_GRACE = 3 // Number of grace announces before enforcing rate ANNOUNCE_RATE_PENALTY = 7200 // Additional penalty time for rate violations MAX_ANNOUNCE_HOPS = 128 // Maximum number of hops for announces APP_NAME = "Reticulum-Go Test Node" APP_ASPECT = "node" // Always use "node" for node announces ) type Reticulum struct { config *common.ReticulumConfig transport *transport.Transport interfaces []interfaces.Interface channels map[string]*channel.Channel buffers map[string]*buffer.Buffer pathRequests map[string]*common.PathRequest announceHistory map[string]announceRecord announceHistoryMu sync.RWMutex identity *identity.Identity destination *destination.Destination storage *storage.Manager // Node-specific information maxTransferSize int16 // Max transfer size in KB nodeEnabled bool // Whether this node is enabled nodeTimestamp int64 // Last node announcement timestamp } type announceRecord struct { timestamp int64 appData []byte } func NewReticulum(cfg *common.ReticulumConfig) (*Reticulum, error) { if cfg == nil { cfg = config.DefaultConfig() } // Set default app name and aspect if not provided if cfg.AppName == "" { cfg.AppName = APP_NAME } if cfg.AppAspect == "" { cfg.AppAspect = APP_ASPECT // Always use "node" for node announcements } if err := initializeDirectories(); err != nil { return nil, fmt.Errorf("failed to initialize directories: %v", err) } debug.Log(debug.DEBUG_INFO, "Directories initialized") // Initialize storage manager storageMgr, err := storage.NewManager() if err != nil { return nil, fmt.Errorf("failed to initialize storage manager: %v", err) } debug.Log(debug.DEBUG_INFO, "Storage manager initialized") t := transport.NewTransport(cfg) debug.Log(debug.DEBUG_INFO, "Transport initialized") // Load or create identity identityPath := storageMgr.GetIdentityPath() var ident *identity.Identity if _, err := os.Stat(identityPath); err == nil { // Identity file exists, load it ident, err = identity.FromFile(identityPath) if err != nil { return nil, fmt.Errorf("failed to load identity: %v", err) } debug.Log(debug.DEBUG_ERROR, "Loaded existing identity", common.STR_HASH, fmt.Sprintf(common.STR_FMT_HEX_LOW, ident.Hash())) } else { // Create new identity ident, err = identity.NewIdentity() if err != nil { return nil, fmt.Errorf("failed to create identity: %v", err) } debug.Log(debug.DEBUG_ERROR, "Created new identity", common.STR_HASH, fmt.Sprintf(common.STR_FMT_HEX_LOW, ident.Hash())) // Save it to disk if err := ident.ToFile(identityPath); err != nil { debug.Log(debug.DEBUG_ERROR, "Failed to save identity to file", common.STR_ERROR, err) } else { debug.Log(debug.DEBUG_INFO, "Identity saved to file", "path", identityPath) } } // Create destination debug.Log(debug.DEBUG_INFO, "Creating destination...") dest, err := destination.New( ident, destination.IN, destination.SINGLE, "nomadnetwork", t, "node", ) if err != nil { return nil, fmt.Errorf("failed to create destination: %v", err) } debug.Log(debug.DEBUG_INFO, "Created destination with hash", common.STR_HASH, fmt.Sprintf(common.STR_FMT_HEX_LOW, dest.GetHash())) // Set node metadata nodeTimestamp := time.Now().Unix() r := &Reticulum{ config: cfg, transport: t, interfaces: make([]interfaces.Interface, 0), channels: make(map[string]*channel.Channel), buffers: make(map[string]*buffer.Buffer), pathRequests: make(map[string]*common.PathRequest), announceHistory: make(map[string]announceRecord), identity: ident, destination: dest, storage: storageMgr, // Node-specific information maxTransferSize: common.NUM_500, // Default 500KB nodeEnabled: true, // Enabled by default nodeTimestamp: nodeTimestamp, } // Enable destination features dest.AcceptsLinks(true) // Enable ratchets and point to a file for persistence. // The actual path should probably be configurable. ratchetPath := ".git.quad4.io/Networks/Reticulum-Go/storage/ratchets/" + r.identity.GetHexHash() dest.EnableRatchets(ratchetPath) dest.SetProofStrategy(destination.PROVE_APP) debug.Log(debug.DEBUG_VERBOSE, "Configured destination features") // Initialize interfaces from config for name, ifaceConfig := range cfg.Interfaces { if !ifaceConfig.Enabled { continue } var iface interfaces.Interface var err error switch ifaceConfig.Type { case common.STR_TCP_CLIENT: iface, err = interfaces.NewTCPClientInterface( name, ifaceConfig.TargetHost, ifaceConfig.TargetPort, ifaceConfig.KISSFraming, ifaceConfig.I2PTunneled, ifaceConfig.Enabled, ) case "UDPInterface": iface, err = interfaces.NewUDPInterface( name, ifaceConfig.Address, ifaceConfig.TargetHost, ifaceConfig.Enabled, ) case "AutoInterface": iface, err = interfaces.NewAutoInterface(name, ifaceConfig) default: debug.Log(debug.DEBUG_CRITICAL, "Unknown interface type", common.STR_TYPE, ifaceConfig.Type) continue } if err != nil { if cfg.PanicOnInterfaceErr { return nil, fmt.Errorf("failed to create interface %s: %v", name, err) } debug.Log(debug.DEBUG_CRITICAL, "Error creating interface", common.STR_NAME, name, common.STR_ERROR, err) continue } // Set packet callback iface.SetPacketCallback(func(data []byte, ni common.NetworkInterface) { debug.Log(debug.DEBUG_INFO, "Packet callback called for interface", common.STR_NAME, ni.GetName(), "data_len", len(data)) if r.transport != nil { r.transport.HandlePacket(data, ni) } else { debug.Log(debug.DEBUG_CRITICAL, "Transport is nil in packet callback") } }) debug.Log(debug.DEBUG_ERROR, "Configuring interface", common.STR_NAME, name, common.STR_TYPE, ifaceConfig.Type) r.interfaces = append(r.interfaces, iface) debug.Log(debug.DEBUG_INFO, "Interface started successfully", common.STR_NAME, name) } return r, nil } func (r *Reticulum) handleInterface(iface common.NetworkInterface) { debug.Log(debug.DEBUG_INFO, "Setting up interface", common.STR_NAME, iface.GetName(), common.STR_TYPE, fmt.Sprintf("%T", iface)) ch := channel.NewChannel(&transportWrapper{r.transport}) r.channels[iface.GetName()] = ch rw := buffer.CreateBidirectionalBuffer( 1, 2, ch, func(size int) { data := make([]byte, size) debug.Log(debug.DEBUG_PACKETS, "Interface reading bytes from buffer", common.STR_NAME, iface.GetName(), "size", size) iface.ProcessIncoming(data) if len(data) > common.ZERO { debug.Log(debug.DEBUG_TRACE, "Interface received packet type", common.STR_NAME, iface.GetName(), common.STR_TYPE, fmt.Sprintf("0x%02x", data[0])) r.transport.HandlePacket(data, iface) } }, ) r.buffers[iface.GetName()] = &buffer.Buffer{ ReadWriter: rw, } } func (r *Reticulum) monitorInterfaces() { ticker := time.NewTicker(5 * time.Second) defer ticker.Stop() for range ticker.C { for _, iface := range r.interfaces { if tcpClient, ok := iface.(*interfaces.TCPClientInterface); ok { stats := fmt.Sprintf("Interface %s status - Connected: %v, TX: %d bytes (%.2f Kbps), RX: %d bytes (%.2f Kbps)", iface.GetName(), tcpClient.IsConnected(), tcpClient.GetTxBytes(), float64(tcpClient.GetTxBytes()*8)/(5*1024), tcpClient.GetRxBytes(), float64(tcpClient.GetRxBytes()*8)/(5*1024), ) if runtime.GOOS != "windows" { stats = fmt.Sprintf("%s, RTT: %v", stats, tcpClient.GetRTT()) } debug.Log(debug.DEBUG_VERBOSE, "Interface status", "stats", stats) } } } } func main() { flag.Parse() debug.Init() debug.Log(debug.DEBUG_CRITICAL, "Initializing Reticulum", "debug_level", debug.GetDebugLevel()) cfg, err := config.InitConfig() if err != nil { debug.GetLogger().Error("Failed to initialize config", common.STR_ERROR, err) os.Exit(1) } debug.Log(debug.DEBUG_ERROR, "Configuration loaded", "path", cfg.ConfigPath) if len(cfg.Interfaces) == 0 { debug.Log(debug.DEBUG_ERROR, "No interfaces configured, adding default interfaces") cfg.Interfaces = make(map[string]*common.InterfaceConfig) // Auto interface for local discovery cfg.Interfaces["Auto Discovery"] = &common.InterfaceConfig{ Type: "AutoInterface", Enabled: true, Name: "Auto Discovery", } cfg.Interfaces["Go-RNS-Testnet"] = &common.InterfaceConfig{ Type: common.STR_TCP_CLIENT, Enabled: false, TargetHost: "127.0.0.1", TargetPort: common.NUM_4242, Name: "Go-RNS-Testnet", } cfg.Interfaces["Quad4 TCP"] = &common.InterfaceConfig{ Type: common.STR_TCP_CLIENT, Enabled: true, TargetHost: "rns2.quad4.io", TargetPort: common.NUM_4242, Name: "Quad4 TCP", } } r, err := NewReticulum(cfg) if err != nil { debug.GetLogger().Error("Failed to create Reticulum instance", common.STR_ERROR, err) os.Exit(1) } // Start monitoring interfaces go r.monitorInterfaces() // Register announce handler handler := NewAnnounceHandler(r, []string{"*"}) r.transport.RegisterAnnounceHandler(handler) // Start Reticulum if err := r.Start(); err != nil { debug.GetLogger().Error("Failed to start Reticulum", common.STR_ERROR, err) os.Exit(1) } sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) <-sigChan debug.Log(debug.DEBUG_CRITICAL, "Shutting down...") if err := r.Stop(); err != nil { debug.Log(debug.DEBUG_CRITICAL, "Error during shutdown", common.STR_ERROR, err) } debug.Log(debug.DEBUG_CRITICAL, "Goodbye!") } type transportWrapper struct { *transport.Transport } func (tw *transportWrapper) GetRTT() float64 { return 0.1 } func (tw *transportWrapper) RTT() float64 { return tw.GetRTT() } func (tw *transportWrapper) GetStatus() byte { return transport.STATUS_ACTIVE } func (tw *transportWrapper) Send(data []byte) interface{} { p := &packet.Packet{ PacketType: packet.PacketTypeData, Hops: 0, Data: data, HeaderType: packet.HeaderType1, } err := tw.Transport.SendPacket(p) if err != nil { return nil } return p } func (tw *transportWrapper) Resend(p interface{}) error { if pkt, ok := p.(*packet.Packet); ok { return tw.Transport.SendPacket(pkt) } return fmt.Errorf("invalid packet type") } func (tw *transportWrapper) SetPacketTimeout(packet interface{}, callback func(interface{}), timeout time.Duration) { time.AfterFunc(timeout, func() { callback(packet) }) } func (tw *transportWrapper) SetPacketDelivered(packet interface{}, callback func(interface{})) { callback(packet) } func (tw *transportWrapper) GetLinkID() []byte { return nil } func (tw *transportWrapper) HandleInbound(pkt *packet.Packet) error { return nil } func (tw *transportWrapper) ValidateLinkProof(pkt *packet.Packet, networkIface common.NetworkInterface) error { return nil } func initializeDirectories() error { homeDir, err := os.UserHomeDir() if err != nil { return fmt.Errorf("failed to get home directory: %v", err) } basePath := filepath.Join(homeDir, ".reticulum-go") dirs := []string{ basePath, filepath.Join(basePath, common.STR_STORAGE), filepath.Join(basePath, common.STR_STORAGE, "destinations"), filepath.Join(basePath, common.STR_STORAGE, "identities"), filepath.Join(basePath, common.STR_STORAGE, "ratchets"), filepath.Join(basePath, common.STR_STORAGE, "cache"), filepath.Join(basePath, common.STR_STORAGE, "cache", "announces"), filepath.Join(basePath, common.STR_STORAGE, "resources"), } for _, dir := range dirs { if err := os.MkdirAll(dir, common.NUM_0700); err != nil { // #nosec G301 return fmt.Errorf("failed to create directory %s: %v", dir, err) } } return nil } func (r *Reticulum) Start() error { debug.Log(debug.DEBUG_ERROR, "Starting Reticulum...") if err := r.transport.Start(); err != nil { return fmt.Errorf("failed to start transport: %v", err) } debug.Log(debug.DEBUG_INFO, "Transport started successfully") // Start interfaces for _, iface := range r.interfaces { debug.Log(debug.DEBUG_ERROR, "Starting interface", "name", iface.GetName()) if err := iface.Start(); err != nil { if r.config.PanicOnInterfaceErr { return fmt.Errorf("failed to start interface %s: %v", iface.GetName(), err) } debug.Log(debug.DEBUG_CRITICAL, "Error starting interface", "name", iface.GetName(), "error", err) continue } if netIface, ok := iface.(common.NetworkInterface); ok { // Register interface with transport if err := r.transport.RegisterInterface(iface.GetName(), netIface); err != nil { debug.Log(debug.DEBUG_CRITICAL, "Failed to register interface with transport", "name", iface.GetName(), "error", err) } else { debug.Log(debug.DEBUG_INFO, "Registered interface with transport", "name", iface.GetName()) } r.handleInterface(netIface) } debug.Log(debug.DEBUG_INFO, "Interface started successfully", "name", iface.GetName()) } // Wait for interfaces to initialize time.Sleep(2 * time.Second) // Send initial announce debug.Log(debug.DEBUG_ERROR, "Sending initial announce") nodeName := "Reticulum-Go Test Node" r.destination.SetDefaultAppData([]byte(nodeName)) if err := r.destination.Announce(false, nil, nil); err != nil { debug.Log(debug.DEBUG_CRITICAL, "Failed to send initial announce", "error", err) } // Start periodic announce goroutine go func() { // Wait a bit before the first announce time.Sleep(5 * time.Second) for { debug.Log(debug.DEBUG_INFO, "Announcing destination...") err := r.destination.Announce(false, nil, nil) if err != nil { debug.Log(debug.DEBUG_CRITICAL, "Could not send announce", "error", err) } time.Sleep(60 * time.Second) } }() go r.monitorInterfaces() debug.Log(debug.DEBUG_ERROR, "Reticulum started successfully") return nil } func (r *Reticulum) Stop() error { debug.Log(debug.DEBUG_ERROR, "Stopping Reticulum...") for _, buf := range r.buffers { if err := buf.Close(); err != nil { debug.Log(debug.DEBUG_CRITICAL, "Error closing buffer", "error", err) } } for _, ch := range r.channels { if err := ch.Close(); err != nil { debug.Log(debug.DEBUG_CRITICAL, "Error closing channel", "error", err) } } for _, iface := range r.interfaces { if err := iface.Stop(); err != nil { debug.Log(debug.DEBUG_CRITICAL, "Error stopping interface", "name", iface.GetName(), "error", err) } } if err := r.transport.Close(); err != nil { return fmt.Errorf("failed to close transport: %v", err) } debug.Log(debug.DEBUG_ERROR, "Reticulum stopped successfully") return nil } type AnnounceHandler struct { aspectFilter []string reticulum *Reticulum } func NewAnnounceHandler(r *Reticulum, aspectFilter []string) *AnnounceHandler { return &AnnounceHandler{ aspectFilter: aspectFilter, reticulum: r, } } func (h *AnnounceHandler) AspectFilter() []string { return h.aspectFilter } func (h *AnnounceHandler) ReceivedAnnounce(destHash []byte, id interface{}, appData []byte) error { debug.Log(debug.DEBUG_INFO, "Received announce", "hash", fmt.Sprintf("%x", destHash)) debug.Log(debug.DEBUG_PACKETS, "Raw announce data", "data", fmt.Sprintf("%x", appData)) debug.Log(debug.DEBUG_INFO, "MAIN HANDLER: Received announce", "hash", fmt.Sprintf("%x", destHash), "appData_len", len(appData)) var isNode bool var nodeEnabled bool var nodeTimestamp int64 var nodeMaxSize int16 // Parse msgpack appData from transport announce format if len(appData) > common.ZERO { // appData is msgpack array [name, customData] if appData[0] == common.HEX_0x92 { // array of 2 elements // Skip array header and first element (name) pos := common.ONE if pos < len(appData) && appData[pos] == common.HEX_0xC4 { // bin 8 nameLen := int(appData[pos+1]) pos += common.TWO + nameLen if pos < len(appData) && appData[pos] == common.HEX_0xC4 { // bin 8 dataLen := int(appData[pos+1]) if pos+2+dataLen <= len(appData) { customData := appData[pos+2 : pos+2+dataLen] nodeName := string(customData) debug.Log(debug.DEBUG_INFO, "Parsed node name", "name", nodeName) debug.Log(debug.DEBUG_INFO, "Announced node", "name", nodeName) } } } } else { // Fallback: treat as raw node name nodeName := string(appData) debug.Log(debug.DEBUG_INFO, "Raw node name", "name", nodeName) debug.Log(debug.DEBUG_INFO, "Announced node", "name", nodeName) } } else { debug.Log(debug.DEBUG_INFO, "No appData (empty announce)") } // Type assert and log identity details if identity, ok := id.(*identity.Identity); ok { debug.Log(debug.DEBUG_ALL, "Identity details") debug.Log(debug.DEBUG_ALL, "Identity hash", "hash", identity.GetHexHash()) debug.Log(debug.DEBUG_ALL, "Identity public key", "key", fmt.Sprintf("%x", identity.GetPublicKey())) ratchets := identity.GetRatchets() debug.Log(debug.DEBUG_ALL, "Active ratchets", "count", len(ratchets)) if len(ratchets) > 0 { ratchetKey := identity.GetCurrentRatchetKey() if ratchetKey != nil { ratchetID := identity.GetRatchetID(ratchetKey) debug.Log(debug.DEBUG_ALL, "Current ratchet ID", "id", fmt.Sprintf("%x", ratchetID)) } } // Create a better record with more info recordType := "peer" if isNode { recordType = "node" debug.Log(debug.DEBUG_INFO, "Storing node in announce history", "enabled", nodeEnabled, "timestamp", nodeTimestamp, "maxsize", fmt.Sprintf("%dKB", nodeMaxSize)) } h.reticulum.announceHistoryMu.Lock() h.reticulum.announceHistory[identity.GetHexHash()] = announceRecord{ timestamp: time.Now().Unix(), appData: appData, } h.reticulum.announceHistoryMu.Unlock() debug.Log(debug.DEBUG_VERBOSE, "Stored announce in history", "type", recordType, "identity", identity.GetHexHash()) } return nil } func (h *AnnounceHandler) ReceivePathResponses() bool { return true } func (r *Reticulum) GetDestination() *destination.Destination { return r.destination } func (r *Reticulum) createNodeAppData() []byte { // Create a msgpack array with 3 elements // [Bool, Int32, Int16] for [enable, timestamp, max_transfer_size] appData := []byte{common.HEX_0x93} // Array with 3 elements // Element 0: Boolean for enable/disable peer if r.nodeEnabled { appData = append(appData, common.HEX_0xC3) // true } else { appData = append(appData, common.HEX_0xC2) // false } // Element 1: Int32 timestamp (current time) // Update the timestamp when creating new announcements r.nodeTimestamp = time.Now().Unix() appData = append(appData, common.HEX_0xD2) // int32 format timeBytes := make([]byte, common.FOUR) binary.BigEndian.PutUint32(timeBytes, uint32(r.nodeTimestamp)) // #nosec G115 appData = append(appData, timeBytes...) // Element 2: Int16 max transfer size in KB appData = append(appData, common.HEX_0xD1) // int16 format sizeBytes := make([]byte, common.TWO) binary.BigEndian.PutUint16(sizeBytes, uint16(r.maxTransferSize)) // #nosec G115 appData = append(appData, sizeBytes...) debug.Log(debug.DEBUG_ALL, "Created node appData", "enable", r.nodeEnabled, "timestamp", r.nodeTimestamp, "maxsize", r.maxTransferSize, "data", fmt.Sprintf("%x", appData)) return appData }