diff --git a/Makefile b/Makefile index 2d3b352..eb539a5 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ BINARY_UNIX=$(BINARY_NAME)_unix BUILD_DIR=bin -MAIN_PACKAGES=./cmd/reticulum-go ./cmd/rns-announce +MAIN_PACKAGE=./cmd/reticulum-go ALL_PACKAGES=$$(go list ./... | grep -v /vendor/) @@ -19,8 +19,7 @@ all: clean deps build test build: @mkdir -p $(BUILD_DIR) - $(GOBUILD) -o $(BUILD_DIR)/reticulum-go ./cmd/reticulum-go - $(GOBUILD) -o $(BUILD_DIR)/rns-announce ./cmd/rns-announce + $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME) $(MAIN_PACKAGE) clean: @rm -rf $(BUILD_DIR) @@ -38,24 +37,18 @@ deps: $(GOMOD) verify build-linux: - CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/reticulum-go ./cmd/reticulum-go - CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/rns-announce ./cmd/rns-announce + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64 $(MAIN_PACKAGE) build-windows: - CGO_ENABLED=0 GOOS=windows GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/reticulum-windows-amd64.exe ./cmd/reticulum-go - CGO_ENABLED=0 GOOS=windows GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/rns-announce-windows-amd64.exe ./cmd/rns-announce + CGO_ENABLED=0 GOOS=windows GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-windows-amd64.exe $(MAIN_PACKAGE) build-darwin: - CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/reticulum-darwin-amd64 ./cmd/reticulum-go - CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/rns-announce-darwin-amd64 ./cmd/rns-announce + CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-amd64 $(MAIN_PACKAGE) build-all: build-linux build-windows build-darwin -run-reticulum: - @./$(BUILD_DIR)/reticulum-go - -run-announce: - @./$(BUILD_DIR)/rns-announce +run: + @./$(BUILD_DIR)/$(BINARY_NAME) install: $(GOMOD) download @@ -63,7 +56,7 @@ install: help: @echo "Available targets:" @echo " all - Clean, download dependencies, build and test" - @echo " build - Build binaries" + @echo " build - Build binary" @echo " clean - Remove build artifacts" @echo " test - Run tests" @echo " coverage - Generate test coverage report" @@ -72,6 +65,5 @@ help: @echo " build-windows- Build for Windows" @echo " build-darwin - Build for MacOS" @echo " build-all - Build for all platforms" - @echo " run-reticulum- Run reticulum binary" - @echo " run-announce - Run announce binary" + @echo " run - Run reticulum binary" @echo " install - Install dependencies" \ No newline at end of file diff --git a/cmd/reticulum-go/main.go b/cmd/reticulum-go/main.go index 3f9279e..695cce9 100644 --- a/cmd/reticulum-go/main.go +++ b/cmd/reticulum-go/main.go @@ -1,28 +1,44 @@ package main import ( + "flag" "fmt" "log" + "math/rand" "os" "os/signal" "syscall" "time" "github.com/Sudo-Ivan/reticulum-go/internal/config" + "github.com/Sudo-Ivan/reticulum-go/pkg/announce" "github.com/Sudo-Ivan/reticulum-go/pkg/buffer" "github.com/Sudo-Ivan/reticulum-go/pkg/channel" "github.com/Sudo-Ivan/reticulum-go/pkg/common" "github.com/Sudo-Ivan/reticulum-go/pkg/interfaces" "github.com/Sudo-Ivan/reticulum-go/pkg/packet" "github.com/Sudo-Ivan/reticulum-go/pkg/transport" + "github.com/Sudo-Ivan/reticulum-go/pkg/identity" ) +var ( + debugLevel = flag.Int("debug", 4, "Debug level (0-7)") +) + +func debugLog(level int, format string, v ...interface{}) { + if *debugLevel >= level { + log.Printf("[DEBUG-%d] %s", level, fmt.Sprintf(format, v...)) + } +} + type Reticulum struct { - config *common.ReticulumConfig - transport *transport.Transport - interfaces []interfaces.Interface - channels map[string]*channel.Channel - buffers map[string]*buffer.Buffer + config *common.ReticulumConfig + transport *transport.Transport + interfaces []interfaces.Interface + channels map[string]*channel.Channel + buffers map[string]*buffer.Buffer + announceHandlers map[string][]announce.AnnounceHandler + pathRequests map[string]*common.PathRequest } func NewReticulum(cfg *common.ReticulumConfig) (*Reticulum, error) { @@ -30,218 +46,163 @@ func NewReticulum(cfg *common.ReticulumConfig) (*Reticulum, error) { cfg = config.DefaultConfig() } - t, err := transport.NewTransport(cfg) - if err != nil { - return nil, fmt.Errorf("failed to initialize transport: %v", err) + if err := initializeDirectories(); err != nil { + return nil, fmt.Errorf("failed to initialize directories: %v", err) } + debugLog(3, "Directories initialized") + + t := transport.NewTransport(cfg) + debugLog(3, "Transport initialized") return &Reticulum{ - config: cfg, - transport: t, - interfaces: make([]interfaces.Interface, 0), - channels: make(map[string]*channel.Channel), - buffers: make(map[string]*buffer.Buffer), + config: cfg, + transport: t, + interfaces: make([]interfaces.Interface, 0), + channels: make(map[string]*channel.Channel), + buffers: make(map[string]*buffer.Buffer), + announceHandlers: make(map[string][]announce.AnnounceHandler), + pathRequests: make(map[string]*common.PathRequest), }, nil } func (r *Reticulum) handleInterface(iface common.NetworkInterface) { - // Create channel using transport wrapper + debugLog(2, "Setting up interface %s", iface.GetName()) + ch := channel.NewChannel(&transportWrapper{r.transport}) r.channels[iface.GetName()] = ch + debugLog(3, "Created channel for interface %s", iface.GetName()) - // Create bidirectional buffer rw := buffer.CreateBidirectionalBuffer( - 1, // Receive stream ID - 2, // Send stream ID + 1, + 2, ch, func(size int) { - // Handle data ready callback data := make([]byte, size) iface.ProcessIncoming(data) - r.transport.HandlePacket(data, iface) + + if len(data) > 0 && data[0] == announce.PACKET_TYPE_ANNOUNCE { + r.handleAnnounce(data, iface) + } else { + r.transport.HandlePacket(data, iface) + } + + debugLog(5, "Processed %d bytes from interface %s", size, iface.GetName()) }, ) - // Store the buffer r.buffers[iface.GetName()] = &buffer.Buffer{ ReadWriter: rw, } - // Set up packet callback iface.SetPacketCallback(func(data []byte, ni common.NetworkInterface) { if buf, ok := r.buffers[ni.GetName()]; ok { if _, err := buf.Write(data); err != nil { - log.Printf("Error writing to buffer for interface %s: %v", ni.GetName(), err) + debugLog(1, "Error writing to buffer for interface %s: %v", ni.GetName(), err) } + debugLog(6, "Written %d bytes to interface %s buffer", len(data), ni.GetName()) } r.transport.HandlePacket(data, ni) }) } -func (r *Reticulum) Start() error { - log.Printf("Starting Reticulum...") +func (r *Reticulum) monitorInterfaces() { + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() - 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, - ifaceConfig.TargetPort, - ifaceConfig.KISSFraming, - ifaceConfig.I2PTunneled, - ifaceConfig.Enabled, - ) - if err != nil { - 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 + for range ticker.C { + for _, iface := range r.interfaces { + if tcpClient, ok := iface.(*interfaces.TCPClientInterface); ok { + debugLog(4, "Interface %s status - Connected: %v, RTT: %v", + iface.GetName(), + tcpClient.IsConnected(), + tcpClient.GetRTT(), + ) } - 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, - ifaceConfig.Port, - ifaceConfig.KISSFraming, - ifaceConfig.I2PTunneled, - ifaceConfig.PreferIPv6, - ) - if err != nil { - 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 - } - 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, - target, - ifaceConfig.Enabled, - ) - if err != nil { - 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 - } - iface = udp - - case "AutoInterface": - 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) - continue - } - - if iface != nil { - 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) - r.handleInterface(netIface) - 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 { - // Close all buffers - for _, buf := range r.buffers { - if err := buf.Close(); err != nil { - log.Printf("Error closing buffer: %v", err) - } - } - - // Close all channels - for _, ch := range r.channels { - if err := ch.Close(); err != nil { - log.Printf("Error closing channel: %v", err) - } - } - - // Stop interfaces - 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 fmt.Errorf("failed to close transport: %v", err) - } - return nil } func main() { - log.Printf("Initializing Reticulum...") + flag.Parse() + debugLog(1, "Initializing Reticulum (Debug Level: %d)...", *debugLevel) cfg, err := config.InitConfig() if err != nil { log.Fatalf("Failed to initialize config: %v", err) } + debugLog(2, "Configuration loaded from: %s", cfg.ConfigPath) + + // Add default TCP interfaces if none configured + if len(cfg.Interfaces) == 0 { + debugLog(2, "No interfaces configured, adding default TCP interfaces") + cfg.Interfaces = make(map[string]*common.InterfaceConfig) + + cfg.Interfaces["amsterdam"] = &common.InterfaceConfig{ + Type: "TCPClientInterface", + Enabled: true, + TargetHost: "amsterdam.connect.reticulum.network", + TargetPort: 4965, + Name: "amsterdam", + } + + cfg.Interfaces["btb"] = &common.InterfaceConfig{ + Type: "TCPClientInterface", + Enabled: true, + TargetHost: "reticulum.betweentheborders.com", + TargetPort: 4242, + Name: "btb", + } + } r, err := NewReticulum(cfg) if err != nil { log.Fatalf("Failed to create Reticulum instance: %v", err) } + go r.monitorInterfaces() + + // Register announce handler using the instance's transport + handler := &AnnounceHandler{ + aspectFilter: []string{"*"}, // Handle all aspects + } + r.transport.RegisterAnnounceHandler(handler) + + // Create a destination to announce + dest, err := identity.NewIdentity() + if err != nil { + log.Fatalf("Failed to create identity: %v", err) + } + + // Create announce for the destination + announce, err := announce.NewAnnounce( + dest, + []byte("Reticulum-Go"), // App data + nil, // No ratchet ID + false, // Not a path response + ) + if err != nil { + log.Fatalf("Failed to create announce: %v", err) + } + + // Propagate announce to all interfaces periodically + go func() { + ticker := time.NewTicker(5 * time.Minute) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + for _, iface := range r.interfaces { + if netIface, ok := iface.(common.NetworkInterface); ok { + if err := announce.Propagate([]common.NetworkInterface{netIface}); err != nil { + debugLog(1, "Failed to propagate announce: %v", err) + } + } + } + } + } + }() + if err := r.Start(); err != nil { log.Fatalf("Failed to start Reticulum: %v", err) } @@ -250,11 +211,11 @@ func main() { signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) <-sigChan - log.Printf("\nShutting down...") + debugLog(1, "Shutting down...") if err := r.Stop(); err != nil { - log.Printf("Error during shutdown: %v", err) + debugLog(1, "Error during shutdown: %v", err) } - log.Printf("Goodbye!") + debugLog(1, "Goodbye!") } // Update transportWrapper to use packet.Packet @@ -306,3 +267,188 @@ func (tw *transportWrapper) SetPacketTimeout(packet interface{}, callback func(i func (tw *transportWrapper) SetPacketDelivered(packet interface{}, callback func(interface{})) { callback(packet) } + +func initializeDirectories() error { + dirs := []string{ + ".reticulum-go", + ".reticulum-go/storage", + ".reticulum-go/storage/destinations", + ".reticulum-go/storage/identities", + ".reticulum-go/storage/ratchets", + } + + for _, dir := range dirs { + if err := os.MkdirAll(dir, 0755); err != nil { + return fmt.Errorf("failed to create directory %s: %v", dir, err) + } + } + return nil +} + +func (r *Reticulum) Start() error { + debugLog(2, "Starting Reticulum...") + + if err := r.transport.Start(); err != nil { + return fmt.Errorf("failed to start transport: %v", err) + } + debugLog(3, "Transport started successfully") + + for name, ifaceConfig := range r.config.Interfaces { + if !ifaceConfig.Enabled { + debugLog(2, "Skipping disabled interface %s", name) + continue + } + + debugLog(2, "Configuring interface %s (type=%s)...", name, ifaceConfig.Type) + var iface interfaces.Interface + + switch ifaceConfig.Type { + case "TCPClientInterface": + client, err := interfaces.NewTCPClientInterface( + ifaceConfig.Name, + ifaceConfig.TargetHost, + ifaceConfig.TargetPort, + ifaceConfig.KISSFraming, + ifaceConfig.I2PTunneled, + ifaceConfig.Enabled, + ) + if err != nil { + if r.config.PanicOnInterfaceErr { + return fmt.Errorf("failed to create TCP client interface %s: %v", name, err) + } + debugLog(1, "Failed to create TCP client interface %s: %v", name, err) + continue + } + iface = client + + case "TCPServerInterface": + server, err := interfaces.NewTCPServerInterface( + ifaceConfig.Name, + ifaceConfig.Address, + ifaceConfig.Port, + ifaceConfig.KISSFraming, + ifaceConfig.I2PTunneled, + ifaceConfig.PreferIPv6, + ) + if err != nil { + if r.config.PanicOnInterfaceErr { + return fmt.Errorf("failed to create TCP server interface %s: %v", name, err) + } + debugLog(1, "Failed to create TCP server interface %s: %v", name, err) + continue + } + iface = server + } + + if iface != nil { + debugLog(2, "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) + } + debugLog(1, "Failed to start interface %s: %v", name, err) + continue + } + + netIface := iface.(common.NetworkInterface) + r.handleInterface(netIface) + r.interfaces = append(r.interfaces, iface) + debugLog(3, "Interface %s started successfully", name) + } + } + + debugLog(2, "Reticulum started successfully") + return nil +} + +func (r *Reticulum) Stop() error { + debugLog(2, "Stopping Reticulum...") + + for _, buf := range r.buffers { + if err := buf.Close(); err != nil { + debugLog(1, "Error closing buffer: %v", err) + } + } + + for _, ch := range r.channels { + if err := ch.Close(); err != nil { + debugLog(1, "Error closing channel: %v", err) + } + } + + for _, iface := range r.interfaces { + if err := iface.Stop(); err != nil { + debugLog(1, "Error stopping interface %s: %v", iface.GetName(), err) + } + } + + if err := r.transport.Close(); err != nil { + return fmt.Errorf("failed to close transport: %v", err) + } + + debugLog(2, "Reticulum stopped successfully") + return nil +} + +func (r *Reticulum) handleAnnounce(data []byte, iface common.NetworkInterface) { + a := &announce.Announce{} + if err := a.HandleAnnounce(data); err != nil { + debugLog(1, "Error handling announce: %v", err) + return + } + + // Add random delay before propagation (0-2 seconds) + delay := time.Duration(rand.Float64() * 2 * float64(time.Second)) + time.Sleep(delay) + + // Check interface modes and propagate according to RNS rules + for _, otherIface := range r.interfaces { + if otherIface.GetName() == iface.GetName() { + continue + } + + srcMode := iface.GetMode() + dstMode := otherIface.GetMode() + + // Skip propagation based on interface modes + if srcMode == common.IF_MODE_ACCESS_POINT && dstMode != common.IF_MODE_FULL { + debugLog(4, "Skipping announce propagation from AP to non-full mode interface") + continue + } + if srcMode == common.IF_MODE_ROAMING && dstMode == common.IF_MODE_ACCESS_POINT { + debugLog(4, "Skipping announce propagation from roaming to AP interface") + continue + } + + // Check if interface has bandwidth available for announces + if err := a.Propagate([]common.NetworkInterface{otherIface}); err != nil { + debugLog(1, "Error propagating announce: %v", err) + } + } +} + +type AnnounceHandler struct { + aspectFilter []string +} + +func (h *AnnounceHandler) AspectFilter() []string { + return h.aspectFilter +} + +func (h *AnnounceHandler) ReceivedAnnounce(destHash []byte, identity interface{}, appData []byte) error { + debugLog(3, "Received announce from %x", destHash) + + if len(appData) > 0 { + debugLog(3, "Announce contained app data: %s", string(appData)) + } + + if id, ok := identity.([]byte); ok { + debugLog(4, "Identity: %x", id) + } + + return nil +} + +func (h *AnnounceHandler) ReceivePathResponses() bool { + return true +} diff --git a/cmd/rns-announce/main.go b/cmd/rns-announce/main.go deleted file mode 100644 index 8d8af93..0000000 --- a/cmd/rns-announce/main.go +++ /dev/null @@ -1,298 +0,0 @@ -package main - -import ( - "encoding/binary" - "errors" - "flag" - "fmt" - "log" - "os" - "os/signal" - "syscall" - "time" - - "github.com/Sudo-Ivan/reticulum-go/internal/config" - "github.com/Sudo-Ivan/reticulum-go/pkg/announce" - "github.com/Sudo-Ivan/reticulum-go/pkg/buffer" - "github.com/Sudo-Ivan/reticulum-go/pkg/channel" - "github.com/Sudo-Ivan/reticulum-go/pkg/common" - "github.com/Sudo-Ivan/reticulum-go/pkg/identity" - "github.com/Sudo-Ivan/reticulum-go/pkg/packet" - "github.com/Sudo-Ivan/reticulum-go/pkg/transport" -) - -type AnnounceClient struct { - config *common.ReticulumConfig - identity *identity.Identity - interval time.Duration - announceID []byte - data string - transport *transport.Transport - channel *channel.Channel - buffer *buffer.RawChannelWriter - done chan struct{} -} - -func NewAnnounceClient(cfg *common.ReticulumConfig, interval time.Duration, data string) (*AnnounceClient, error) { - id, err := identity.New() - if err != nil { - return nil, err - } - - t, err := transport.NewTransport(cfg) - if err != nil { - return nil, err - } - - // Create transport wrapper that implements LinkInterface - tw := &transportWrapper{t} - - // Create channel using transport wrapper - ch := channel.NewChannel(tw) - - // Create buffer writer for streaming data - writer := buffer.NewRawChannelWriter(1, ch) - - announceID := identity.GetRandomHash() - - client := &AnnounceClient{ - config: cfg, - identity: id, - interval: interval, - announceID: announceID, - data: data, - transport: t, - channel: ch, - buffer: writer, - done: make(chan struct{}), - } - - // Register announce handler - handler := &AnnounceHandler{ - aspectFilter: []string{"*"}, - } - t.RegisterAnnounceHandler(handler) - - return client, nil -} - -func (c *AnnounceClient) handlePacket(data []byte) error { - if len(data) < 2 { - return errors.New("packet too short") - } - - header := data[0] - packetType := header & 0x03 // Extract packet type from header - - switch packetType { - case announce.PACKET_TYPE_ANNOUNCE: - log.Printf("Processing announce packet") - return c.processAnnounce(data[1:]) - } - - return nil -} - -func (c *AnnounceClient) processAnnounce(data []byte) error { - if len(data) < 16 { - return errors.New("invalid announce packet length") - } - - destHash := data[:16] - announceType := data[16] - - if announceType == announce.ANNOUNCE_IDENTITY { - pubKey := data[17:81] // Ed25519 public key is 32 bytes - appDataLen := binary.BigEndian.Uint16(data[81:83]) - appData := data[83 : 83+appDataLen] - - log.Printf("Received announce from %x", destHash) - log.Printf("Public key: %x", pubKey) - log.Printf("App data: %s", string(appData)) - } - - return nil -} - -func (c *AnnounceClient) Start() error { - if err := c.transport.Start(); err != nil { - return err - } - - log.Printf("Starting announce client with interval: %v", c.interval) - log.Printf("Announce data: %s", c.data) - log.Printf("Announce ID: %x", c.announceID) - - ticker := time.NewTicker(c.interval) - defer ticker.Stop() - - // Initial announce - log.Printf("Sending initial announce...") - if err := c.announce(); err != nil { - return err - } - log.Printf("Initial announce sent successfully") - - for { - select { - case <-ticker.C: - log.Printf("Sending periodic announce...") - if err := c.announce(); err != nil { - log.Printf("Failed to send announce: %v", err) - } else { - log.Printf("Announce sent successfully") - } - case <-c.done: - return nil - } - } -} - -func (c *AnnounceClient) announce() error { - // Create announce packet - announceData := []byte(c.data) - - packet := announce.NewAnnouncePacket( - c.identity.GetPublicKey(), - announceData, - c.announceID, - ) - - // Write through buffer system - _, err := c.buffer.Write(packet.Data) - if err != nil { - return fmt.Errorf("failed to write announce: %v", err) - } - - return nil -} - -func (c *AnnounceClient) Stop() { - close(c.done) - c.buffer.Close() - if err := c.transport.Close(); err != nil { - log.Printf("Error closing transport: %v", err) - } -} - -// Add AnnounceHandler type -type AnnounceHandler struct { - aspectFilter []string -} - -func (h *AnnounceHandler) ReceivedAnnounce(destHash []byte, announcedIdentity interface{}, appData []byte) error { - // Type assert the identity if needed - if id, ok := announcedIdentity.(*identity.Identity); ok { - log.Printf("Received announce from %x (Identity: %x)", destHash, id.GetPublicKey()) - } else { - log.Printf("Received announce from %x", destHash) - } - log.Printf("App data: %s", string(appData)) - return nil -} - -func (h *AnnounceHandler) ReceivePathResponses() bool { - return true -} - -// Add AspectFilter method to satisfy the interface -func (h *AnnounceHandler) AspectFilter() []string { - return h.aspectFilter -} - -// Add transportWrapper implementation -type transportWrapper struct { - *transport.Transport -} - -func (tw *transportWrapper) GetRTT() float64 { - return 0.1 // Default value for now -} - -func (tw *transportWrapper) RTT() float64 { - return tw.GetRTT() -} - -func (tw *transportWrapper) GetStatus() int { - return transport.STATUS_ACTIVE -} - -func (tw *transportWrapper) Send(data []byte) interface{} { - p := &packet.Packet{ - Header: [2]byte{ - packet.PacketTypeData, - 0, - }, - Data: data, - Addresses: make([]byte, packet.AddressSize), - Context: 0, - Timestamp: time.Now(), - } - - 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(p interface{}, callback func(interface{}), timeout time.Duration) { - time.AfterFunc(timeout, func() { - callback(p) - }) -} - -func (tw *transportWrapper) SetPacketDelivered(p interface{}, callback func(interface{})) { - callback(p) -} - -func main() { - interval := flag.Int("interval", 600, "Announce interval in seconds") - announceData := flag.String("data", "Hello Reticulum", "Data to announce") - configPath := flag.String("config", "", "Path to config file") - flag.Parse() - - log.Printf("Initializing announce client...") - log.Printf("Config path: %s", *configPath) - log.Printf("Interval: %d seconds", *interval) - log.Printf("Data: %s", *announceData) - - cfg, err := config.InitConfig() - if err != nil { - log.Fatalf("Failed to initialize config: %v", err) - } - - if *configPath != "" { - cfg.ConfigPath = *configPath - log.Printf("Using custom config path: %s", *configPath) - } - - client, err := NewAnnounceClient(cfg, time.Duration(*interval)*time.Second, *announceData) - if err != nil { - log.Fatalf("Failed to create announce client: %v", err) - } - - log.Printf("Client created successfully") - log.Printf("Press Ctrl+C to stop...") - - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) - - go func() { - <-sigChan - log.Printf("\nShutting down...") - client.Stop() - os.Exit(0) - }() - - if err := client.Start(); err != nil { - log.Fatalf("Error running announce client: %v", err) - } -} diff --git a/configs/test-client1.toml b/configs/test-client1.toml deleted file mode 100644 index 4a2a5cb..0000000 --- a/configs/test-client1.toml +++ /dev/null @@ -1,31 +0,0 @@ -enable_transport = true -share_instance = true -shared_instance_port = 37428 -instance_control_port = 37429 -panic_on_interface_error = false -loglevel = 4 - -[interfaces] - [[Local UDP]] - type = "UDPInterface" - interface_enabled = true - address = "0.0.0.0" - port = 37697 - - [[Auto Discovery]] - type = "AutoInterface" - interface_enabled = true - discovery_port = 29717 - data_port = 42672 - - [interfaces."RNS Testnet Amsterdam"] - type = "TCPClientInterface" - enabled = true - target_host = "amsterdam.connect.reticulum.network" - target_port = 4965 - - [interfaces."RNS Testnet BetweenTheBorders"] - type = "TCPClientInterface" - enabled = true - target_host = "reticulum.betweentheborders.com" - target_port = 4242 \ No newline at end of file diff --git a/configs/test-client2.toml b/configs/test-client2.toml deleted file mode 100644 index 5d9c3b4..0000000 --- a/configs/test-client2.toml +++ /dev/null @@ -1,31 +0,0 @@ -enable_transport = true -share_instance = true -shared_instance_port = 37430 -instance_control_port = 37431 -panic_on_interface_error = false -loglevel = 4 - -[interfaces] - [interfaces."RNS Testnet Amsterdam"] - type = "TCPClientInterface" - enabled = true - target_host = "amsterdam.connect.reticulum.network" - target_port = 4965 - - [interfaces."RNS Testnet BetweenTheBorders"] - type = "TCPClientInterface" - enabled = true - target_host = "reticulum.betweentheborders.com" - target_port = 4242 - - [[Local UDP]] - type = "UDPInterface" - interface_enabled = true - address = "0.0.0.0" - port = 37698 - - [[Auto Discovery]] - type = "AutoInterface" - interface_enabled = true - discovery_port = 29718 - data_port = 42673 \ No newline at end of file diff --git a/pkg/announce/announce.go b/pkg/announce/announce.go index 8083359..6a8a7fb 100644 --- a/pkg/announce/announce.go +++ b/pkg/announce/announce.go @@ -54,7 +54,7 @@ type AnnounceHandler interface { } type Announce struct { - mutex sync.RWMutex + mutex *sync.RWMutex destinationHash []byte identity *identity.Identity appData []byte @@ -65,6 +65,7 @@ type Announce struct { retries int handlers []AnnounceHandler ratchetID []byte + packet []byte } func New(dest *identity.Identity, appData []byte, pathResponse bool) (*Announce, error) { @@ -73,24 +74,30 @@ func New(dest *identity.Identity, appData []byte, pathResponse bool) (*Announce, } a := &Announce{ - identity: dest, - appData: appData, - hops: 0, - timestamp: time.Now().Unix(), - pathResponse: pathResponse, - retries: 0, - handlers: make([]AnnounceHandler, 0), + mutex: &sync.RWMutex{}, + identity: dest, + appData: appData, + hops: 0, + timestamp: time.Now().Unix(), + pathResponse: pathResponse, + retries: 0, + handlers: make([]AnnounceHandler, 0), } - // Generate truncated hash - hash := sha256.New() - hash.Write(dest.GetPublicKey()) - a.destinationHash = hash.Sum(nil)[:identity.TRUNCATED_HASHLENGTH/8] + // Generate truncated hash from public key + pubKey := dest.GetPublicKey() + hash := sha256.Sum256(pubKey) + a.destinationHash = hash[:identity.TRUNCATED_HASHLENGTH/8] + + // Get current ratchet ID if enabled + currentRatchet := dest.GetCurrentRatchetKey() + if currentRatchet != nil { + a.ratchetID = dest.GetRatchetID(currentRatchet) + } // Sign announce data signData := append(a.destinationHash, a.appData...) - if dest.GetRatchetID(nil) != nil { - a.ratchetID = dest.GetRatchetID(nil) + if a.ratchetID != nil { signData = append(signData, a.ratchetID...) } a.signature = dest.Sign(signData) @@ -147,22 +154,53 @@ func (a *Announce) HandleAnnounce(data []byte) error { a.mutex.Lock() defer a.mutex.Unlock() - if len(data) < identity.TRUNCATED_HASHLENGTH/8+identity.KEYSIZE/8+1 { + // Minimum packet size validation (2 header + 16 hash + 32 pubkey + 1 hops + 2 appdata len + 64 sig) + if len(data) < 117 { return errors.New("invalid announce data length") } - destHash := data[:identity.TRUNCATED_HASHLENGTH/8] - publicKey := data[identity.TRUNCATED_HASHLENGTH/8 : identity.TRUNCATED_HASHLENGTH/8+identity.KEYSIZE/8] - hopCount := data[identity.TRUNCATED_HASHLENGTH/8+identity.KEYSIZE/8] - + // Parse header + header := data[:2] + hopCount := header[1] if hopCount > MAX_HOPS { return errors.New("announce exceeded maximum hop count") } - // Extract app data and signature - dataStart := identity.TRUNCATED_HASHLENGTH/8 + identity.KEYSIZE/8 + 1 - appData := data[dataStart : len(data)-ed25519.SignatureSize] - signature := data[len(data)-ed25519.SignatureSize:] + // Extract fields + destHash := data[2:18] + publicKey := data[18:50] + hopsByte := data[50] + + // Validate hop count matches header + if hopsByte != hopCount { + return errors.New("inconsistent hop count in packet") + } + + // Extract app data length and content + appDataLen := binary.BigEndian.Uint16(data[51:53]) + appDataEnd := 53 + int(appDataLen) + + if appDataEnd > len(data) { + return errors.New("invalid app data length") + } + + appData := data[53:appDataEnd] + + // Handle ratchet ID if present + var ratchetID []byte + signatureStart := appDataEnd + + remainingBytes := len(data) - appDataEnd + if remainingBytes > ed25519.SignatureSize { + ratchetID = data[appDataEnd : len(data)-ed25519.SignatureSize] + signatureStart = len(data) - ed25519.SignatureSize + } + + if signatureStart+ed25519.SignatureSize > len(data) { + return errors.New("invalid signature position") + } + + signature := data[signatureStart:] // Create announced identity announcedIdentity := identity.FromPublicKey(publicKey) @@ -170,10 +208,9 @@ func (a *Announce) HandleAnnounce(data []byte) error { return errors.New("invalid identity public key") } - // Verify signature including ratchet if present + // Verify signature signData := append(destHash, appData...) - if len(appData) > 32 { // Check for ratchet - ratchetID := appData[len(appData)-32:] + if ratchetID != nil { signData = append(signData, ratchetID...) } @@ -227,37 +264,45 @@ func CreateHeader(ifacFlag byte, headerType byte, contextFlag byte, propType byt func (a *Announce) CreatePacket() []byte { packet := make([]byte, 0) - // Create header for announce packet + // Create header according to spec header := CreateHeader( - IFAC_NONE, // No interface authentication + IFAC_NONE, // No interface auth HEADER_TYPE_1, // One address field 0x00, // Context flag unset PROP_TYPE_BROADCAST, // Broadcast propagation DEST_TYPE_SINGLE, // Single destination PACKET_TYPE_ANNOUNCE, // Announce packet type - byte(a.hops), // Current hop count + a.hops, // Current hop count ) packet = append(packet, header...) // Add destination hash (16 bytes) packet = append(packet, a.destinationHash...) - // Add context byte - packet = append(packet, ANNOUNCE_IDENTITY) - // Add public key packet = append(packet, a.identity.GetPublicKey()...) + // Add hop count byte + packet = append(packet, byte(a.hops)) + // Add app data with length prefix - if a.appData != nil { - lenBytes := make([]byte, 2) - binary.BigEndian.PutUint16(lenBytes, uint16(len(a.appData))) - packet = append(packet, lenBytes...) - packet = append(packet, a.appData...) + appDataLen := make([]byte, 2) + binary.BigEndian.PutUint16(appDataLen, uint16(len(a.appData))) + packet = append(packet, appDataLen...) + packet = append(packet, a.appData...) + + // Add ratchet ID if present + if a.ratchetID != nil { + packet = append(packet, a.ratchetID...) } // Add signature - packet = append(packet, a.signature...) + signData := append(a.destinationHash, a.appData...) + if a.ratchetID != nil { + signData = append(signData, a.ratchetID...) + } + signature := a.identity.Sign(signData) + packet = append(packet, signature...) return packet } @@ -268,25 +313,82 @@ type AnnouncePacket struct { func NewAnnouncePacket(pubKey []byte, appData []byte, announceID []byte) *AnnouncePacket { packet := &AnnouncePacket{} - + // Build packet data packet.Data = make([]byte, 0, len(pubKey)+len(appData)+len(announceID)+4) - + // Add header packet.Data = append(packet.Data, PACKET_TYPE_ANNOUNCE) packet.Data = append(packet.Data, ANNOUNCE_IDENTITY) - + // Add public key packet.Data = append(packet.Data, pubKey...) - + // Add app data length and content appDataLen := make([]byte, 2) binary.BigEndian.PutUint16(appDataLen, uint16(len(appData))) packet.Data = append(packet.Data, appDataLen...) packet.Data = append(packet.Data, appData...) - + // Add announce ID packet.Data = append(packet.Data, announceID...) - + return packet } + +// NewAnnounce creates a new announce packet for a destination +func NewAnnounce(identity *identity.Identity, appData []byte, ratchetID []byte, pathResponse bool) (*Announce, error) { + if identity == nil { + return nil, errors.New("identity cannot be nil") + } + + a := &Announce{ + identity: identity, + appData: appData, + ratchetID: ratchetID, + pathResponse: pathResponse, + destinationHash: identity.Hash(), + hops: 0, + mutex: &sync.RWMutex{}, + handlers: make([]AnnounceHandler, 0), + } + + // Create announce packet + packet := make([]byte, 0) + + // Add header (2 bytes) + packet = append(packet, PACKET_TYPE_ANNOUNCE) + packet = append(packet, byte(a.hops)) + + // Add destination hash (16 bytes) + packet = append(packet, a.destinationHash...) + + // Add public key (32 bytes) + packet = append(packet, identity.GetPublicKey()...) + + // Add hop count (1 byte) + packet = append(packet, byte(a.hops)) + + // Add app data with length prefix (2 bytes + data) + appDataLen := make([]byte, 2) + binary.BigEndian.PutUint16(appDataLen, uint16(len(appData))) + packet = append(packet, appDataLen...) + packet = append(packet, appData...) + + // Add ratchet ID if present + if ratchetID != nil { + packet = append(packet, ratchetID...) + } + + // Add signature + signData := append(a.destinationHash, appData...) + if ratchetID != nil { + signData = append(signData, ratchetID...) + } + signature := identity.Sign(signData) + packet = append(packet, signature...) + + a.packet = packet + + return a, nil +} diff --git a/pkg/announce/handler.go b/pkg/announce/handler.go new file mode 100644 index 0000000..a5e03a9 --- /dev/null +++ b/pkg/announce/handler.go @@ -0,0 +1,5 @@ +package announce + +type Handler interface { + ReceivedAnnounce(destHash []byte, identity interface{}, appData []byte) error +} diff --git a/pkg/common/types.go b/pkg/common/types.go index fe06585..159d084 100644 --- a/pkg/common/types.go +++ b/pkg/common/types.go @@ -15,6 +15,7 @@ type Path struct { NextHop []byte Hops uint8 LastUpdated time.Time + HopCount uint8 } // Common callbacks diff --git a/pkg/identity/identity.go b/pkg/identity/identity.go index 27ae367..9822745 100644 --- a/pkg/identity/identity.go +++ b/pkg/identity/identity.go @@ -17,24 +17,24 @@ import ( "encoding/hex" + "github.com/Sudo-Ivan/reticulum-go/pkg/common" "golang.org/x/crypto/curve25519" "golang.org/x/crypto/hkdf" - "github.com/Sudo-Ivan/reticulum-go/pkg/common" ) const ( - CURVE = "Curve25519" - KEYSIZE = 512 // Combined length of encryption key (256) and signing key (256) - RATCHETSIZE = 256 - RATCHET_EXPIRY = 2592000 // 30 days in seconds + CURVE = "Curve25519" + KEYSIZE = 512 // Combined length of encryption key (256) and signing key (256) + RATCHETSIZE = 256 + RATCHET_EXPIRY = 2592000 // 30 days in seconds TRUNCATED_HASHLENGTH = 128 - NAME_HASH_LENGTH = 80 - + NAME_HASH_LENGTH = 80 + // Token constants for Fernet-like spec - TOKEN_OVERHEAD = 16 // AES block size + TOKEN_OVERHEAD = 16 // AES block size AES128_BLOCKSIZE = 16 - HASHLENGTH = 256 - SIGLENGTH = KEYSIZE + HASHLENGTH = 256 + SIGLENGTH = KEYSIZE ) type Identity struct { @@ -45,10 +45,10 @@ type Identity struct { hash []byte hexHash string appData []byte - - ratchets map[string][]byte - ratchetExpiry map[string]int64 - mutex sync.RWMutex + + ratchets map[string][]byte + ratchetExpiry map[string]int64 + mutex *sync.RWMutex } var ( @@ -170,7 +170,7 @@ func (i *Identity) Encrypt(plaintext []byte, ratchet []byte) ([]byte, error) { if _, err := io.ReadFull(rand.Reader, ephemeralPrivKey); err != nil { return nil, err } - + ephemeralPubKey, err := curve25519.X25519(ephemeralPrivKey, curve25519.Basepoint) if err != nil { return nil, err @@ -183,7 +183,7 @@ func (i *Identity) Encrypt(plaintext []byte, ratchet []byte) ([]byte, error) { } // Generate shared secret - sharedSecret, err := curve25519.X25519(ephemeralPrivKey, targetKey) + sharedSecret, err := curve25519.X25519(ephemeralPrivKey, targetKey) if err != nil { return nil, err } @@ -255,15 +255,15 @@ func GetRandomHash() []byte { return TruncatedHash(randomData) } -func Remember(packetHash, destHash []byte, publicKey []byte, appData []byte) { - if len(destHash) > TRUNCATED_HASHLENGTH/8 { - destHash = destHash[:TRUNCATED_HASHLENGTH/8] - } +func Remember(packet []byte, destHash []byte, publicKey []byte, appData []byte) { + hashStr := hex.EncodeToString(destHash) - knownDestinations[string(destHash)] = []interface{}{ - time.Now().Unix(), - packetHash, - publicKey, + // Store destination data as [packet, destHash, identity, appData] + id := FromPublicKey(publicKey) + knownDestinations[hashStr] = []interface{}{ + packet, + destHash, + id, appData, } } @@ -464,7 +464,7 @@ func (i *Identity) tryRatchetDecryption(peerPubBytes, ciphertext, ratchet []byte return nil, nil, err } ratchetID := i.GetRatchetID(ratchetPubBytes) - + // Generate shared key sharedKey, err := curve25519.X25519(ratchetPriv, peerPubBytes) if err != nil { @@ -618,3 +618,71 @@ func (i *Identity) GetRatchetID(ratchetPubBytes []byte) []byte { hash := sha256.Sum256(ratchetPubBytes) return hash[:NAME_HASH_LENGTH/8] } + +func GetKnownDestination(hash string) ([]interface{}, bool) { + if data, exists := knownDestinations[hash]; exists { + return data, true + } + return nil, false +} + +func (i *Identity) GetHexHash() string { + if i.hexHash == "" { + i.hexHash = hex.EncodeToString(i.Hash()) + } + return i.hexHash +} + +func (i *Identity) GetRatchetKey(id string) ([]byte, bool) { + ratchetPersistLock.Lock() + defer ratchetPersistLock.Unlock() + + key, exists := knownRatchets[id] + return key, exists +} + +func (i *Identity) SetRatchetKey(id string, key []byte) { + ratchetPersistLock.Lock() + defer ratchetPersistLock.Unlock() + + knownRatchets[id] = key +} + +// NewIdentity creates a new Identity instance with fresh keys +func NewIdentity() (*Identity, error) { + // Generate Ed25519 signing keypair + pubKey, privKey, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return nil, fmt.Errorf("failed to generate Ed25519 keypair: %v", err) + } + + // Generate X25519 encryption keypair + var encPrivKey [32]byte + if _, err := io.ReadFull(rand.Reader, encPrivKey[:]); err != nil { + return nil, fmt.Errorf("failed to generate X25519 private key: %v", err) + } + + encPubKey, err := curve25519.X25519(encPrivKey[:], curve25519.Basepoint) + if err != nil { + return nil, fmt.Errorf("failed to generate X25519 public key: %v", err) + } + + i := &Identity{ + privateKey: encPrivKey[:], + publicKey: encPubKey, + signingKey: privKey, + verificationKey: pubKey, + ratchets: make(map[string][]byte), + ratchetExpiry: make(map[string]int64), + mutex: &sync.RWMutex{}, + } + + // Generate hash + combinedPub := make([]byte, KEYSIZE/8) + copy(combinedPub[:KEYSIZE/16], i.publicKey) + copy(combinedPub[KEYSIZE/16:], i.verificationKey) + hash := sha256.Sum256(combinedPub) + i.hash = hash[:] + + return i, nil +} diff --git a/pkg/interfaces/tcp.go b/pkg/interfaces/tcp.go index be7e392..a04f1a1 100644 --- a/pkg/interfaces/tcp.go +++ b/pkg/interfaces/tcp.go @@ -4,7 +4,9 @@ import ( "fmt" "net" "sync" + "syscall" "time" + "unsafe" "github.com/Sudo-Ivan/reticulum-go/pkg/common" ) @@ -25,6 +27,8 @@ const ( TCP_PROBES = 12 RECONNECT_WAIT = 5 INITIAL_TIMEOUT = 5 + INITIAL_BACKOFF = time.Second + MAX_BACKOFF = time.Minute * 5 ) type TCPClientInterface struct { @@ -45,15 +49,18 @@ type TCPClientInterface struct { enabled bool } -func NewTCPClient(name string, targetHost string, targetPort int, kissFraming bool, i2pTunneled bool, enabled bool) (*TCPClientInterface, error) { +func NewTCPClientInterface(name string, targetHost string, targetPort int, kissFraming bool, i2pTunneled bool, enabled bool) (*TCPClientInterface, error) { tc := &TCPClientInterface{ - BaseInterface: NewBaseInterface(name, common.IF_TYPE_TCP, enabled), - targetAddr: targetHost, - targetPort: targetPort, - kissFraming: kissFraming, - i2pTunneled: i2pTunneled, - initiator: true, - enabled: enabled, + BaseInterface: NewBaseInterface(name, common.IF_TYPE_TCP, enabled), + targetAddr: targetHost, + targetPort: targetPort, + kissFraming: kissFraming, + i2pTunneled: i2pTunneled, + initiator: true, + enabled: enabled, + maxReconnectTries: TCP_PROBES, + packetBuffer: make([]byte, 0), + neverConnected: true, } if enabled { @@ -64,6 +71,7 @@ func NewTCPClient(name string, targetHost string, targetPort int, kissFraming bo } tc.conn = conn tc.Online = true + go tc.readLoop() } return tc, nil @@ -79,6 +87,7 @@ func (tc *TCPClientInterface) Start() error { if tc.conn != nil { tc.Online = true + go tc.readLoop() return nil } @@ -89,6 +98,7 @@ func (tc *TCPClientInterface) Start() error { } tc.conn = conn tc.Online = true + go tc.readLoop() return nil } @@ -166,17 +176,26 @@ func (tc *TCPClientInterface) handlePacket(data []byte) { return } - packetType := data[0] + tc.mutex.Lock() + tc.packetType = data[0] + tc.mutex.Unlock() + payload := data[1:] - switch packetType { - case 0x01: // Path request - tc.BaseInterface.ProcessIncoming(payload) + switch tc.packetType { + case 0x01: // Announce packet + if len(payload) >= 53 { // Minimum announce size + tc.BaseInterface.ProcessIncoming(payload) + } case 0x02: // Link packet if len(payload) < 40 { // minimum size for link packet return } tc.BaseInterface.ProcessIncoming(payload) + case 0x03: // Announce packet + tc.BaseInterface.ProcessIncoming(payload) + case 0x04: // Transport packet + tc.BaseInterface.ProcessIncoming(payload) default: // Unknown packet type return @@ -286,38 +305,53 @@ func (tc *TCPClientInterface) reconnect() { tc.reconnecting = true tc.mutex.Unlock() + backoff := time.Second + maxBackoff := time.Minute * 5 retries := 0 + for retries < tc.maxReconnectTries { tc.teardown() addr := fmt.Sprintf("%s:%d", tc.targetAddr, tc.targetPort) + conn, err := net.Dial("tcp", addr) if err == nil { tc.mutex.Lock() tc.conn = conn tc.Online = true + tc.neverConnected = false tc.reconnecting = false tc.mutex.Unlock() - // Restart read loop go tc.readLoop() return } - retries++ - // Wait before retrying - select { - case <-time.After(RECONNECT_WAIT * time.Second): - continue + // Log reconnection attempt + fmt.Printf("Failed to reconnect to %s (attempt %d/%d): %v\n", + addr, retries+1, tc.maxReconnectTries, err) + + // Wait with exponential backoff + time.Sleep(backoff) + + // Increase backoff time exponentially + backoff *= 2 + if backoff > maxBackoff { + backoff = maxBackoff } + + retries++ } - // Failed to reconnect after max retries tc.mutex.Lock() tc.reconnecting = false tc.mutex.Unlock() + + // If we've exhausted all retries, perform final teardown tc.teardown() + fmt.Printf("Failed to reconnect to %s after %d attempts\n", + fmt.Sprintf("%s:%d", tc.targetAddr, tc.targetPort), tc.maxReconnectTries) } func (tc *TCPClientInterface) Enable() { @@ -332,6 +366,55 @@ func (tc *TCPClientInterface) Disable() { tc.Online = false } +func (tc *TCPClientInterface) IsConnected() bool { + tc.mutex.RLock() + defer tc.mutex.RUnlock() + return tc.conn != nil && tc.Online && !tc.reconnecting +} + +func getRTTFromSocket(fd uintptr) time.Duration { + var info syscall.TCPInfo + size := uint32(syscall.SizeofTCPInfo) + + _, _, err := syscall.Syscall6( + syscall.SYS_GETSOCKOPT, + fd, + syscall.SOL_TCP, + syscall.TCP_INFO, + uintptr(unsafe.Pointer(&info)), + uintptr(unsafe.Pointer(&size)), + 0, + ) + + if err != 0 { + return 0 + } + + // RTT is in microseconds, convert to Duration + return time.Duration(info.Rtt) * time.Microsecond +} + +func (tc *TCPClientInterface) GetRTT() time.Duration { + tc.mutex.RLock() + defer tc.mutex.RUnlock() + + if !tc.IsConnected() { + return 0 + } + + if tcpConn, ok := tc.conn.(*net.TCPConn); ok { + var rtt time.Duration + if info, err := tcpConn.SyscallConn(); err == nil { + info.Control(func(fd uintptr) { + rtt = getRTTFromSocket(fd) + }) + return rtt + } + } + + return 0 +} + type TCPServerInterface struct { BaseInterface connections map[string]net.Conn @@ -344,7 +427,7 @@ type TCPServerInterface struct { packetCallback common.PacketCallback } -func NewTCPServer(name string, bindAddr string, bindPort int, kissFraming bool, i2pTunneled bool, preferIPv6 bool) (*TCPServerInterface, error) { +func NewTCPServerInterface(name string, bindAddr string, bindPort int, kissFraming bool, i2pTunneled bool, preferIPv6 bool) (*TCPServerInterface, error) { ts := &TCPServerInterface{ BaseInterface: BaseInterface{ Name: name, diff --git a/pkg/packet/packet.go b/pkg/packet/packet.go index 5b42265..d43af17 100644 --- a/pkg/packet/packet.go +++ b/pkg/packet/packet.go @@ -1,9 +1,8 @@ package packet import ( - "crypto/rand" - "encoding/binary" "errors" + "time" ) const ( @@ -38,6 +37,7 @@ const ( HopsField = 0xFF ) +// Packet represents a network packet in the Reticulum protocol type Packet struct { Header [2]byte Addresses []byte @@ -45,48 +45,10 @@ type Packet struct { Data []byte AccessCode []byte RandomBlob []byte + Timestamp time.Time } -func NewAnnouncePacket(destHash []byte, publicKey []byte, appData []byte) (*Packet, error) { - p := &Packet{ - Header: [2]byte{0, 0}, // Start with 0 hops - Addresses: make([]byte, AddressSize), - Data: make([]byte, 0, MaxDataSize), - } - - // 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 -} - +// NewPacket creates a new packet with the specified parameters 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") @@ -96,6 +58,7 @@ func NewPacket(packetType byte, flags byte, hops byte, destKey []byte, data []by Header: [2]byte{flags, hops}, Addresses: make([]byte, AddressSize), Data: data, + Timestamp: time.Now(), } // Set packet type in flags @@ -107,38 +70,7 @@ func NewPacket(packetType byte, flags byte, hops byte, destKey []byte, data []by return p, nil } -func (p *Packet) SetAccessCode(code []byte) { - p.AccessCode = code - p.Header[0] |= IFACFlag -} - -func (p *Packet) SetContext(context byte) { - p.Context = context - p.Header[0] |= ContextFlag -} - -func (p *Packet) SetData(data []byte) error { - if len(data) > MaxDataSize { - return errors.New("data exceeds maximum allowed size") - } - p.Data = data - return nil -} - -func (p *Packet) SetAddress(index int, address []byte) error { - if len(address) != AddressSize { - return errors.New("invalid address size") - } - - offset := index * AddressSize - if offset+AddressSize > len(p.Addresses) { - return errors.New("address index out of range") - } - - copy(p.Addresses[offset:], address) - return nil -} - +// Serialize converts the packet into a byte slice func (p *Packet) Serialize() ([]byte, error) { totalSize := HeaderSize + len(p.Addresses) + ContextSize + len(p.Data) if p.AccessCode != nil { @@ -172,46 +104,13 @@ func (p *Packet) Serialize() ([]byte, error) { return buffer, nil } -func ParsePacket(data []byte) (*Packet, error) { - if len(data) < HeaderSize { - return nil, errors.New("packet data too short") - } - - p := &Packet{ - Header: [2]byte{data[0], data[1]}, - } - - offset := HeaderSize - - // Handle access code if present - if p.Header[0]&IFACFlag != 0 { - // Access code handling would go here - // For now, we'll assume no access code - return nil, errors.New("access code handling not implemented") - } - - // Determine address size based on header type - addrLen := AddressSize - if p.Header[0]&HeaderTypeFlag != 0 { - addrLen = 2 * AddressSize - } - - if len(data[offset:]) < addrLen+ContextSize { - return nil, errors.New("packet data too short for addresses and context") - } - - // Copy addresses - p.Addresses = make([]byte, addrLen) - copy(p.Addresses, data[offset:offset+addrLen]) - offset += addrLen - - // Copy context - p.Context = data[offset] - offset++ - - // Copy remaining data - p.Data = make([]byte, len(data)-offset) - copy(p.Data, data[offset:]) - - return p, nil +type AnnouncePacket struct { + Header [2]byte + DestHash []byte + PublicKey []byte + AppData []byte + RandomBlob []byte + Signature []byte + HopCount byte + Timestamp time.Time } diff --git a/pkg/pathfinder/pathfinder.go b/pkg/pathfinder/pathfinder.go new file mode 100644 index 0000000..04a216b --- /dev/null +++ b/pkg/pathfinder/pathfinder.go @@ -0,0 +1,34 @@ +package pathfinder + +import "time" + +type PathFinder struct { + paths map[string]Path +} + +type Path struct { + NextHop []byte + Interface string + HopCount byte + LastUpdated int64 +} + +func NewPathFinder() *PathFinder { + return &PathFinder{ + paths: make(map[string]Path), + } +} + +func (p *PathFinder) AddPath(destHash string, nextHop []byte, iface string, hops byte) { + p.paths[destHash] = Path{ + NextHop: nextHop, + Interface: iface, + HopCount: hops, + LastUpdated: time.Now().Unix(), + } +} + +func (p *PathFinder) GetPath(destHash string) (Path, bool) { + path, exists := p.paths[destHash] + return path, exists +} diff --git a/pkg/rate/rate.go b/pkg/rate/rate.go new file mode 100644 index 0000000..0a75bea --- /dev/null +++ b/pkg/rate/rate.go @@ -0,0 +1,44 @@ +package rate + +import ( + "sync" + "time" +) + +type Limiter struct { + rate float64 + interval time.Duration + lastUpdate time.Time + allowance float64 + mutex sync.Mutex +} + +func NewLimiter(rate float64, interval time.Duration) *Limiter { + return &Limiter{ + rate: rate, + interval: interval, + lastUpdate: time.Now(), + allowance: rate, + } +} + +func (l *Limiter) Allow() bool { + l.mutex.Lock() + defer l.mutex.Unlock() + + now := time.Now() + elapsed := now.Sub(l.lastUpdate) + l.lastUpdate = now + + l.allowance += elapsed.Seconds() * l.rate + if l.allowance > l.rate { + l.allowance = l.rate + } + + if l.allowance < 1.0 { + return false + } + + l.allowance -= 1.0 + return true +} \ No newline at end of file diff --git a/pkg/resolver/resolver.go b/pkg/resolver/resolver.go new file mode 100644 index 0000000..71c395e --- /dev/null +++ b/pkg/resolver/resolver.go @@ -0,0 +1,74 @@ +package resolver + +import ( + "crypto/sha256" + "encoding/hex" + "errors" + "strings" + "sync" + + "github.com/Sudo-Ivan/reticulum-go/pkg/identity" +) + +type Resolver struct { + cache map[string]*identity.Identity + cacheLock sync.RWMutex +} + +func New() *Resolver { + return &Resolver{ + cache: make(map[string]*identity.Identity), + } +} + +func (r *Resolver) ResolveIdentity(fullName string) (*identity.Identity, error) { + if fullName == "" { + return nil, errors.New("empty identity name") + } + + r.cacheLock.RLock() + if cachedIdentity, exists := r.cache[fullName]; exists { + r.cacheLock.RUnlock() + return cachedIdentity, nil + } + r.cacheLock.RUnlock() + + // Hash the full name to create a deterministic identity + h := sha256.New() + h.Write([]byte(fullName)) + nameHash := h.Sum(nil)[:identity.NAME_HASH_LENGTH/8] + hashStr := hex.EncodeToString(nameHash) + + // Check if this identity is known + if knownData, exists := identity.GetKnownDestination(hashStr); exists { + if id, ok := knownData[2].(*identity.Identity); ok { + r.cacheLock.Lock() + r.cache[fullName] = id + r.cacheLock.Unlock() + return id, nil + } + } + + // Split name into parts for hierarchical resolution + parts := strings.Split(fullName, ".") + if len(parts) < 2 { + return nil, errors.New("invalid identity name format") + } + + // Create new identity if not found + id, err := identity.New() + if err != nil { + return nil, err + } + + r.cacheLock.Lock() + r.cache[fullName] = id + r.cacheLock.Unlock() + + return id, nil +} + +func ResolveIdentity(fullName string) (*identity.Identity, error) { + r := New() + return r.ResolveIdentity(fullName) +} diff --git a/pkg/transport/transport.go b/pkg/transport/transport.go index 8d77ed2..d5ee7e0 100644 --- a/pkg/transport/transport.go +++ b/pkg/transport/transport.go @@ -1,16 +1,21 @@ package transport import ( + "crypto/sha256" "encoding/binary" "errors" "fmt" + "log" + "math/rand" "net" "sync" "time" + "github.com/Sudo-Ivan/reticulum-go/pkg/announce" "github.com/Sudo-Ivan/reticulum-go/pkg/common" - "github.com/Sudo-Ivan/reticulum-go/pkg/identity" "github.com/Sudo-Ivan/reticulum-go/pkg/packet" + "github.com/Sudo-Ivan/reticulum-go/pkg/pathfinder" + "github.com/Sudo-Ivan/reticulum-go/pkg/rate" ) var ( @@ -56,6 +61,13 @@ const ( STATUS_ACTIVE = 1 STATUS_CLOSED = 2 STATUS_FAILED = 3 + + AnnounceRatePercent = 2.0 // 2% of bandwidth for announces + PATHFINDER_M = 8 // Maximum hop count + AnnounceRateKbps = 20.0 // 20 Kbps for announces + + MAX_HOPS = 128 // Default m value for announce propagation + PROPAGATION_RATE = 0.02 // 2% bandwidth cap for announces ) type PathInfo struct { @@ -66,29 +78,35 @@ type PathInfo struct { } type Transport struct { + mutex sync.RWMutex config *common.ReticulumConfig interfaces map[string]common.NetworkInterface - paths map[string]*common.Path - announceHandlers []AnnounceHandler - mutex sync.RWMutex - handlerLock sync.RWMutex - pathLock sync.RWMutex links map[string]*Link + announceRate *rate.Limiter + seenAnnounces map[string]bool + pathfinder *pathfinder.PathFinder + announceHandlers []announce.Handler + paths map[string]*common.Path } -func NewTransport(config *common.ReticulumConfig) (*Transport, error) { +type Path struct { + NextHop []byte + Interface common.NetworkInterface + HopCount byte +} + +func NewTransport(cfg *common.ReticulumConfig) *Transport { t := &Transport{ - config: config, - interfaces: make(map[string]common.NetworkInterface), - paths: make(map[string]*common.Path), - links: make(map[string]*Link), + interfaces: make(map[string]common.NetworkInterface), + paths: make(map[string]*common.Path), + seenAnnounces: make(map[string]bool), + announceRate: rate.NewLimiter(PROPAGATION_RATE, 1), + mutex: sync.RWMutex{}, + config: cfg, + links: make(map[string]*Link), + pathfinder: pathfinder.NewPathFinder(), } - - transportMutex.Lock() - transportInstance = t - transportMutex.Unlock() - - return t, nil + return t } // Add GetTransportInstance function @@ -249,40 +267,39 @@ func (l *Link) Send(data []byte) interface{} { return packet } -type AnnounceHandler interface { - AspectFilter() []string - ReceivedAnnounce(destinationHash []byte, announcedIdentity interface{}, appData []byte) error - ReceivePathResponses() bool -} - -func (t *Transport) RegisterAnnounceHandler(handler AnnounceHandler) { - t.handlerLock.Lock() - defer t.handlerLock.Unlock() - - // Check for duplicate handlers - for _, h := range t.announceHandlers { - if h == handler { - return - } - } - +func (t *Transport) RegisterAnnounceHandler(handler announce.Handler) { + t.mutex.Lock() + defer t.mutex.Unlock() t.announceHandlers = append(t.announceHandlers, handler) } -func (t *Transport) DeregisterAnnounceHandler(handler AnnounceHandler) { - t.handlerLock.Lock() - defer t.handlerLock.Unlock() +func (t *Transport) UnregisterAnnounceHandler(handler announce.Handler) { + t.mutex.Lock() + defer t.mutex.Unlock() for i, h := range t.announceHandlers { if h == handler { t.announceHandlers = append(t.announceHandlers[:i], t.announceHandlers[i+1:]...) - return + break + } + } +} + +func (t *Transport) notifyAnnounceHandlers(destHash []byte, identity interface{}, appData []byte) { + t.mutex.RLock() + handlers := make([]announce.Handler, len(t.announceHandlers)) + copy(handlers, t.announceHandlers) + t.mutex.RUnlock() + + for _, handler := range handlers { + if err := handler.ReceivedAnnounce(destHash, identity, appData); err != nil { + log.Printf("Error in announce handler: %v", err) } } } func (t *Transport) HasPath(destinationHash []byte) bool { - t.pathLock.RLock() - defer t.pathLock.RUnlock() + t.mutex.RLock() + defer t.mutex.RUnlock() path, exists := t.paths[string(destinationHash)] if !exists { @@ -299,20 +316,20 @@ func (t *Transport) HasPath(destinationHash []byte) bool { } func (t *Transport) HopsTo(destinationHash []byte) uint8 { - t.pathLock.RLock() - defer t.pathLock.RUnlock() + t.mutex.RLock() + defer t.mutex.RUnlock() path, exists := t.paths[string(destinationHash)] if !exists { return PathfinderM } - return path.Hops + return path.HopCount } func (t *Transport) NextHop(destinationHash []byte) []byte { - t.pathLock.RLock() - defer t.pathLock.RUnlock() + t.mutex.RLock() + defer t.mutex.RUnlock() path, exists := t.paths[string(destinationHash)] if !exists { @@ -323,8 +340,8 @@ func (t *Transport) NextHop(destinationHash []byte) []byte { } func (t *Transport) NextHopInterface(destinationHash []byte) string { - t.pathLock.RLock() - defer t.pathLock.RUnlock() + t.mutex.RLock() + defer t.mutex.RUnlock() path, exists := t.paths[string(destinationHash)] if !exists { @@ -350,8 +367,8 @@ func (t *Transport) RequestPath(destinationHash []byte, onInterface string, tag } func (t *Transport) UpdatePath(destinationHash []byte, nextHop []byte, interfaceName string, hops uint8) { - t.pathLock.Lock() - defer t.pathLock.Unlock() + t.mutex.Lock() + defer t.mutex.Unlock() iface, err := t.GetInterface(interfaceName) if err != nil { @@ -359,22 +376,18 @@ func (t *Transport) UpdatePath(destinationHash []byte, nextHop []byte, interface } t.paths[string(destinationHash)] = &common.Path{ - Interface: iface, NextHop: nextHop, + Interface: iface, Hops: hops, LastUpdated: time.Now(), } } func (t *Transport) HandleAnnounce(destinationHash []byte, identity []byte, appData []byte, announceHash []byte) { - t.handlerLock.RLock() - defer t.handlerLock.RUnlock() + t.mutex.RLock() + defer t.mutex.RUnlock() - for _, handler := range t.announceHandlers { - if handler.ReceivePathResponses() || announceHash != nil { - handler.ReceivedAnnounce(destinationHash, identity, appData) - } - } + t.notifyAnnounceHandlers(destinationHash, identity, appData) } func (t *Transport) NewDestination(identity interface{}, direction int, destType int, appName string, aspects ...string) *Destination { @@ -639,30 +652,53 @@ func (t *Transport) handleAnnouncePacket(data []byte, iface common.NetworkInterf return } - destHash := data[:32] - var identityData, appData []byte - - if len(data) > 32 { - splitPoint := 32 - for i := 32; i < len(data); i++ { - if data[i] == 0x00 { - splitPoint = i - break - } - } - identityData = data[32:splitPoint] - if splitPoint < len(data)-1 { - appData = data[splitPoint+1:] - } + p := &packet.Packet{ + Data: data, + Header: [2]byte{ + 0x04, // Announce packet type + 0x00, // Initial hop count + }, } - // Use identity package's GetRandomHash - announceHash := identity.GetRandomHash() - - // Use interface name in announce handling - if iface != nil { - t.HandleAnnounce(destHash, identityData, appData, announceHash) + announceHash := sha256.Sum256(data) + if t.seenAnnounces[string(announceHash[:])] { + return } + + // Record this announce + t.seenAnnounces[string(announceHash[:])] = true + + // Process the announce + if err := t.handleAnnounce(p); err != nil { + log.Printf("Error handling announce: %v", err) + return + } + + // Broadcast to other interfaces based on interface mode + t.mutex.RLock() + for name, otherIface := range t.interfaces { + // Skip the interface we received from + if name == iface.GetName() { + continue + } + + // Check interface modes for propagation rules + srcMode := iface.GetMode() + dstMode := otherIface.GetMode() + + // Skip propagation based on interface modes + if srcMode == common.IF_MODE_ACCESS_POINT && dstMode != common.IF_MODE_FULL { + continue + } + if srcMode == common.IF_MODE_ROAMING && dstMode == common.IF_MODE_ACCESS_POINT { + continue + } + + if err := otherIface.Send(p.Data, ""); err != nil { + log.Printf("Error broadcasting announce to %s: %v", name, err) + } + } + t.mutex.RUnlock() } func (t *Transport) findLink(dest []byte) *Link { @@ -831,32 +867,31 @@ func (l *Link) RTT() float64 { return l.GetRTT() } -func (l *Link) Resend(packet interface{}) error { - if p, ok := packet.(*LinkPacket); ok { - p.Timestamp = time.Now() - return p.send() +func (l *Link) Resend(p interface{}) error { + if pkt, ok := p.(*packet.Packet); ok { + t := GetTransportInstance() + if t == nil { + return fmt.Errorf("transport not initialized") + } + return t.SendPacket(pkt) } - return errors.New("invalid packet type") + return fmt.Errorf("invalid packet type") } -func (l *Link) SetPacketTimeout(packet interface{}, callback func(interface{}), timeout time.Duration) { - if p, ok := packet.(*LinkPacket); ok { - // Start timeout timer +func (l *Link) SetPacketTimeout(p interface{}, callback func(interface{}), timeout time.Duration) { + if pkt, ok := p.(*packet.Packet); ok { time.AfterFunc(timeout, func() { - callback(p) + callback(pkt) }) } } -func (l *Link) SetPacketDelivered(packet interface{}, callback func(interface{})) { - if p, ok := packet.(*LinkPacket); ok { - // Update RTT +func (l *Link) SetPacketDelivered(p interface{}, callback func(interface{})) { + if pkt, ok := p.(*packet.Packet); ok { l.mutex.Lock() - l.rtt = time.Since(p.Timestamp) + l.rtt = time.Since(time.Now()) l.mutex.Unlock() - - // Call delivery callback - callback(p) + callback(pkt) } } @@ -865,3 +900,49 @@ func (l *Link) GetStatus() int { defer l.mutex.RUnlock() return l.status } + +func (t *Transport) handleAnnounce(p *packet.Packet) error { + // Skip if we've seen this announce before + announceHash := sha256.Sum256(p.Data) + if t.seenAnnounces[string(announceHash[:])] { + return nil + } + + // Record this announce + t.seenAnnounces[string(announceHash[:])] = true + + // Extract announce fields + if len(p.Data) < 53 { // Minimum size for announce packet + return errors.New("invalid announce packet size") + } + + // Don't forward if max hops reached + if p.Header[1] >= MAX_HOPS { + return nil + } + + // Add random delay before retransmission (0-2 seconds) + delay := time.Duration(rand.Float64() * 2 * float64(time.Second)) + time.Sleep(delay) + + // Check bandwidth allocation for announces + if !t.announceRate.Allow() { + return nil + } + + // Increment hop count and retransmit + p.Header[1]++ + return t.broadcastAnnouncePacket(p) +} + +func (t *Transport) broadcastAnnouncePacket(p *packet.Packet) error { + t.mutex.RLock() + defer t.mutex.RUnlock() + + for _, iface := range t.interfaces { + if err := iface.Send(p.Data, ""); err != nil { + return fmt.Errorf("failed to broadcast announce: %w", err) + } + } + return nil +}