Files
Reticulum-Go/pkg/interfaces/udp.go

213 lines
4.5 KiB
Go

// 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)
}
}
}