// SPDX-License-Identifier: 0BSD // Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io //go:build !tinygo // +build !tinygo package interfaces import ( "fmt" "net" "sync" "git.quad4.io/Networks/Reticulum-Go/pkg/common" "git.quad4.io/Networks/Reticulum-Go/pkg/debug" ) type UDPInterface struct { BaseInterface conn *net.UDPConn addr *net.UDPAddr targetAddr *net.UDPAddr readBuffer []byte done chan struct{} stopOnce sync.Once } func NewUDPInterface(name string, addr string, target string, enabled bool) (*UDPInterface, error) { udpAddr, err := net.ResolveUDPAddr("udp", addr) if err != nil { return nil, err } var targetAddr *net.UDPAddr if target != "" { targetAddr, err = net.ResolveUDPAddr("udp", target) if err != nil { return nil, err } } ui := &UDPInterface{ BaseInterface: NewBaseInterface(name, common.IF_TYPE_UDP, enabled), addr: udpAddr, targetAddr: targetAddr, readBuffer: make([]byte, common.NUM_1064), done: make(chan struct{}), } ui.MTU = common.NUM_1064 return ui, nil } func (ui *UDPInterface) Detach() { ui.Mutex.Lock() defer ui.Mutex.Unlock() ui.Detached = true ui.Online = false if ui.conn != nil { ui.conn.Close() // #nosec G104 } ui.stopOnce.Do(func() { if ui.done != nil { close(ui.done) } }) } func (ui *UDPInterface) Send(data []byte, addr string) error { debug.Log(debug.DEBUG_ALL, "UDP interface sending bytes", "name", ui.Name, "bytes", len(data)) if !ui.IsEnabled() { return fmt.Errorf("interface not enabled") } if ui.targetAddr == nil { return fmt.Errorf("no target address configured") } ui.Mutex.Lock() ui.TxBytes += uint64(len(data)) ui.Mutex.Unlock() _, err := ui.conn.WriteTo(data, ui.targetAddr) if err != nil { debug.Log(debug.DEBUG_CRITICAL, "UDP interface write failed", "name", ui.Name, "error", err) } else { debug.Log(debug.DEBUG_ALL, "UDP interface sent bytes successfully", "name", ui.Name, "bytes", len(data)) } return err } func (ui *UDPInterface) ProcessOutgoing(data []byte) error { if !ui.IsOnline() { return fmt.Errorf("interface offline") } _, err := ui.conn.Write(data) if err != nil { return fmt.Errorf("UDP write failed: %v", err) } ui.Mutex.Lock() ui.TxBytes += uint64(len(data)) ui.Mutex.Unlock() return nil } func (ui *UDPInterface) GetConn() net.Conn { return ui.conn } func (ui *UDPInterface) Start() error { ui.Mutex.Lock() if ui.conn != nil { ui.Mutex.Unlock() return fmt.Errorf("UDP interface already started") } // Only recreate done if it's nil or was closed select { case <-ui.done: ui.done = make(chan struct{}) ui.stopOnce = sync.Once{} default: if ui.done == nil { ui.done = make(chan struct{}) ui.stopOnce = sync.Once{} } } ui.Mutex.Unlock() conn, err := net.ListenUDP("udp", ui.addr) if err != nil { return err } ui.conn = conn // Enable broadcast mode if we have a target address if ui.targetAddr != nil { // Get the raw connection file descriptor to set SO_BROADCAST if err := conn.SetReadBuffer(common.NUM_1064); err != nil { debug.Log(debug.DEBUG_ERROR, "Failed to set read buffer size", "error", err) } if err := conn.SetWriteBuffer(common.NUM_1064); err != nil { debug.Log(debug.DEBUG_ERROR, "Failed to set write buffer size", "error", err) } } ui.Mutex.Lock() ui.Online = true ui.Mutex.Unlock() // Start the read loop in a goroutine go ui.readLoop() return nil } func (ui *UDPInterface) Stop() error { ui.Detach() return nil } func (ui *UDPInterface) readLoop() { buffer := make([]byte, common.NUM_1064) for { ui.Mutex.RLock() online := ui.Online detached := ui.Detached conn := ui.conn done := ui.done ui.Mutex.RUnlock() if !online || detached || conn == nil { return } select { case <-done: return default: } n, remoteAddr, err := conn.ReadFromUDP(buffer) if err != nil { ui.Mutex.RLock() stillOnline := ui.Online ui.Mutex.RUnlock() if stillOnline { debug.Log(debug.DEBUG_ERROR, "Error reading from UDP interface", "name", ui.Name, "error", err) } return } ui.Mutex.Lock() // #nosec G115 - Network read sizes are always positive and within safe range ui.RxBytes += uint64(n) // Auto-discover target address from first packet if not set if ui.targetAddr == nil { debug.Log(debug.DEBUG_ALL, "UDP interface discovered peer", "name", ui.Name, "peer", remoteAddr.String()) ui.targetAddr = remoteAddr } callback := ui.packetCallback ui.Mutex.Unlock() if callback != nil { callback(buffer[:n], ui) } } }