Merge main into tinygo and fix conflicts
This commit is contained in:
@@ -1,49 +1,112 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
//go:build !tinygo
|
||||
// +build !tinygo
|
||||
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||
)
|
||||
|
||||
const (
|
||||
HW_MTU = 1196
|
||||
DEFAULT_DISCOVERY_PORT = 29716
|
||||
DEFAULT_DATA_PORT = 42671
|
||||
DEFAULT_GROUP_ID = "reticulum"
|
||||
BITRATE_GUESS = 10 * 1000 * 1000
|
||||
PEERING_TIMEOUT = 7500 * time.Millisecond
|
||||
SCOPE_LINK = "2"
|
||||
SCOPE_ADMIN = "4"
|
||||
SCOPE_SITE = "5"
|
||||
SCOPE_ORGANISATION = "8"
|
||||
SCOPE_GLOBAL = "e"
|
||||
PEERING_TIMEOUT = 22 * time.Second
|
||||
ANNOUNCE_INTERVAL = 1600 * time.Millisecond
|
||||
PEER_JOB_INTERVAL = 4 * time.Second
|
||||
MCAST_ECHO_TIMEOUT = 6500 * time.Millisecond
|
||||
|
||||
SCOPE_LINK = "2"
|
||||
SCOPE_ADMIN = "4"
|
||||
SCOPE_SITE = "5"
|
||||
SCOPE_ORGANISATION = "8"
|
||||
SCOPE_GLOBAL = "e"
|
||||
|
||||
MCAST_ADDR_TYPE_PERMANENT = "0"
|
||||
MCAST_ADDR_TYPE_TEMPORARY = "1"
|
||||
)
|
||||
|
||||
type AutoInterface struct {
|
||||
BaseInterface
|
||||
groupID []byte
|
||||
discoveryPort int
|
||||
dataPort int
|
||||
discoveryScope string
|
||||
peers map[string]*Peer
|
||||
linkLocalAddrs []string
|
||||
adoptedInterfaces map[string]string
|
||||
interfaceServers map[string]net.Conn
|
||||
multicastEchoes map[string]time.Time
|
||||
mutex sync.RWMutex
|
||||
outboundConn net.Conn
|
||||
groupID []byte
|
||||
groupHash []byte
|
||||
discoveryPort int
|
||||
dataPort int
|
||||
discoveryScope string
|
||||
multicastAddrType string
|
||||
mcastDiscoveryAddr string
|
||||
ifacNetname string
|
||||
peers map[string]*Peer
|
||||
linkLocalAddrs []string
|
||||
adoptedInterfaces map[string]*AdoptedInterface
|
||||
interfaceServers map[string]*net.UDPConn
|
||||
discoveryServers map[string]*net.UDPConn
|
||||
multicastEchoes map[string]time.Time
|
||||
timedOutInterfaces map[string]time.Time
|
||||
allowedInterfaces []string
|
||||
ignoredInterfaces []string
|
||||
outboundConn *net.UDPConn
|
||||
announceInterval time.Duration
|
||||
peerJobInterval time.Duration
|
||||
peeringTimeout time.Duration
|
||||
mcastEchoTimeout time.Duration
|
||||
done chan struct{}
|
||||
stopOnce sync.Once
|
||||
}
|
||||
|
||||
type AdoptedInterface struct {
|
||||
name string
|
||||
linkLocalAddr string
|
||||
index int
|
||||
}
|
||||
|
||||
type Peer struct {
|
||||
ifaceName string
|
||||
lastHeard time.Time
|
||||
conn net.PacketConn
|
||||
addr *net.UDPAddr
|
||||
}
|
||||
|
||||
func NewAutoInterface(name string, config *common.InterfaceConfig) (*AutoInterface, error) {
|
||||
groupID := DEFAULT_GROUP_ID
|
||||
if config.GroupID != "" {
|
||||
groupID = config.GroupID
|
||||
}
|
||||
|
||||
discoveryScope := SCOPE_LINK
|
||||
if config.DiscoveryScope != "" {
|
||||
discoveryScope = normalizeScope(config.DiscoveryScope)
|
||||
}
|
||||
|
||||
multicastAddrType := MCAST_ADDR_TYPE_TEMPORARY
|
||||
|
||||
discoveryPort := DEFAULT_DISCOVERY_PORT
|
||||
if config.DiscoveryPort != 0 {
|
||||
discoveryPort = config.DiscoveryPort
|
||||
}
|
||||
|
||||
dataPort := DEFAULT_DATA_PORT
|
||||
if config.DataPort != 0 {
|
||||
dataPort = config.DataPort
|
||||
}
|
||||
|
||||
groupHash := sha256.Sum256([]byte(groupID))
|
||||
|
||||
ifacNetname := hex.EncodeToString(groupHash[:])[:16]
|
||||
mcastAddr := fmt.Sprintf("ff%s%s::%s", discoveryScope, multicastAddrType, ifacNetname)
|
||||
|
||||
ai := &AutoInterface{
|
||||
BaseInterface: BaseInterface{
|
||||
Name: name,
|
||||
@@ -52,74 +115,303 @@ func NewAutoInterface(name string, config *common.InterfaceConfig) (*AutoInterfa
|
||||
Online: false,
|
||||
Enabled: config.Enabled,
|
||||
Detached: false,
|
||||
IN: false,
|
||||
IN: true,
|
||||
OUT: false,
|
||||
MTU: common.DEFAULT_MTU,
|
||||
Bitrate: BITRATE_MINIMUM,
|
||||
MTU: HW_MTU,
|
||||
Bitrate: BITRATE_GUESS,
|
||||
},
|
||||
discoveryPort: DEFAULT_DISCOVERY_PORT,
|
||||
dataPort: DEFAULT_DATA_PORT,
|
||||
discoveryScope: SCOPE_LINK,
|
||||
peers: make(map[string]*Peer),
|
||||
linkLocalAddrs: make([]string, 0),
|
||||
adoptedInterfaces: make(map[string]string),
|
||||
interfaceServers: make(map[string]net.Conn),
|
||||
multicastEchoes: make(map[string]time.Time),
|
||||
}
|
||||
|
||||
if config.Port != 0 {
|
||||
ai.discoveryPort = config.Port
|
||||
}
|
||||
|
||||
if config.GroupID != "" {
|
||||
ai.groupID = []byte(config.GroupID)
|
||||
} else {
|
||||
ai.groupID = []byte("reticulum")
|
||||
groupID: []byte(groupID),
|
||||
groupHash: groupHash[:],
|
||||
discoveryPort: discoveryPort,
|
||||
dataPort: dataPort,
|
||||
discoveryScope: discoveryScope,
|
||||
multicastAddrType: multicastAddrType,
|
||||
mcastDiscoveryAddr: mcastAddr,
|
||||
ifacNetname: ifacNetname,
|
||||
peers: make(map[string]*Peer),
|
||||
linkLocalAddrs: make([]string, 0),
|
||||
adoptedInterfaces: make(map[string]*AdoptedInterface),
|
||||
interfaceServers: make(map[string]*net.UDPConn),
|
||||
discoveryServers: make(map[string]*net.UDPConn),
|
||||
multicastEchoes: make(map[string]time.Time),
|
||||
timedOutInterfaces: make(map[string]time.Time),
|
||||
allowedInterfaces: make([]string, 0),
|
||||
ignoredInterfaces: make([]string, 0),
|
||||
announceInterval: ANNOUNCE_INTERVAL,
|
||||
peerJobInterval: PEER_JOB_INTERVAL,
|
||||
peeringTimeout: PEERING_TIMEOUT,
|
||||
mcastEchoTimeout: MCAST_ECHO_TIMEOUT,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
|
||||
debug.Log(debug.DEBUG_INFO, "AutoInterface configured", "name", name, "group", groupID, "mcast_addr", mcastAddr)
|
||||
return ai, nil
|
||||
}
|
||||
|
||||
func normalizeScope(scope string) string {
|
||||
switch scope {
|
||||
case "link", "2":
|
||||
return SCOPE_LINK
|
||||
case "admin", "4":
|
||||
return SCOPE_ADMIN
|
||||
case "site", "5":
|
||||
return SCOPE_SITE
|
||||
case "organisation", "organization", "8":
|
||||
return SCOPE_ORGANISATION
|
||||
case "global", "e":
|
||||
return SCOPE_GLOBAL
|
||||
default:
|
||||
return SCOPE_LINK
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeMulticastType(mtype string) string {
|
||||
switch mtype {
|
||||
case "permanent", "0":
|
||||
return MCAST_ADDR_TYPE_PERMANENT
|
||||
case "temporary", "1":
|
||||
return MCAST_ADDR_TYPE_TEMPORARY
|
||||
default:
|
||||
return MCAST_ADDR_TYPE_TEMPORARY
|
||||
}
|
||||
}
|
||||
|
||||
func (ai *AutoInterface) Start() error {
|
||||
// TinyGo doesn't support net.Interfaces() or multicast UDP
|
||||
// AutoInterface requires these features, so return an error
|
||||
return fmt.Errorf("AutoInterface not supported in TinyGo - requires interface enumeration and multicast UDP")
|
||||
ai.Mutex.Lock()
|
||||
// Only recreate done if it's nil or was closed
|
||||
select {
|
||||
case <-ai.done:
|
||||
ai.done = make(chan struct{})
|
||||
ai.stopOnce = sync.Once{}
|
||||
default:
|
||||
if ai.done == nil {
|
||||
ai.done = make(chan struct{})
|
||||
ai.stopOnce = sync.Once{}
|
||||
}
|
||||
}
|
||||
ai.Mutex.Unlock()
|
||||
|
||||
interfaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list interfaces: %v", err)
|
||||
}
|
||||
|
||||
for _, iface := range interfaces {
|
||||
if ai.shouldIgnoreInterface(iface.Name) {
|
||||
debug.Log(debug.DEBUG_TRACE, "Ignoring interface", "name", iface.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(ai.allowedInterfaces) > 0 && !ai.isAllowedInterface(iface.Name) {
|
||||
debug.Log(debug.DEBUG_TRACE, "Interface not in allowed list", "name", iface.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
ifaceCopy := iface
|
||||
// bearer:disable go_gosec_memory_memory_aliasing
|
||||
if err := ai.configureInterface(&ifaceCopy); err != nil {
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Failed to configure interface", "name", iface.Name, "error", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if len(ai.adoptedInterfaces) == 0 {
|
||||
return fmt.Errorf("no suitable interfaces found")
|
||||
}
|
||||
|
||||
ai.Online = true
|
||||
ai.IN = true
|
||||
ai.OUT = true
|
||||
|
||||
go ai.peerJobs()
|
||||
go ai.announceLoop()
|
||||
|
||||
debug.Log(debug.DEBUG_INFO, "AutoInterface started", "adopted", len(ai.adoptedInterfaces))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ai *AutoInterface) shouldIgnoreInterface(name string) bool {
|
||||
ignoreList := []string{"lo", "lo0", "tun0", "awdl0", "llw0", "en5", "dummy0"}
|
||||
|
||||
for _, ignored := range ai.ignoredInterfaces {
|
||||
if name == ignored {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
for _, ignored := range ignoreList {
|
||||
if name == ignored {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (ai *AutoInterface) isAllowedInterface(name string) bool {
|
||||
for _, allowed := range ai.allowedInterfaces {
|
||||
if name == allowed {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (ai *AutoInterface) configureInterface(iface *net.Interface) error {
|
||||
// Not supported in TinyGo
|
||||
return fmt.Errorf("configureInterface not supported in TinyGo")
|
||||
if iface.Flags&net.FlagUp == 0 {
|
||||
return fmt.Errorf("interface is down")
|
||||
}
|
||||
|
||||
if iface.Flags&net.FlagLoopback != 0 {
|
||||
return fmt.Errorf("loopback interface")
|
||||
}
|
||||
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var linkLocalAddr string
|
||||
for _, addr := range addrs {
|
||||
if ipnet, ok := addr.(*net.IPNet); ok {
|
||||
if ipnet.IP.To4() == nil && ipnet.IP.IsLinkLocalUnicast() {
|
||||
linkLocalAddr = ipnet.IP.String()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if linkLocalAddr == "" {
|
||||
return fmt.Errorf("no link-local IPv6 address found")
|
||||
}
|
||||
|
||||
ai.Mutex.Lock()
|
||||
ai.adoptedInterfaces[iface.Name] = &AdoptedInterface{
|
||||
name: iface.Name,
|
||||
linkLocalAddr: linkLocalAddr,
|
||||
index: iface.Index,
|
||||
}
|
||||
ai.linkLocalAddrs = append(ai.linkLocalAddrs, linkLocalAddr)
|
||||
ai.multicastEchoes[iface.Name] = time.Now()
|
||||
ai.Mutex.Unlock()
|
||||
|
||||
if err := ai.startDiscoveryListener(iface); err != nil {
|
||||
return fmt.Errorf("failed to start discovery listener: %v", err)
|
||||
}
|
||||
|
||||
if err := ai.startDataListener(iface); err != nil {
|
||||
return fmt.Errorf("failed to start data listener: %v", err)
|
||||
}
|
||||
|
||||
debug.Log(debug.DEBUG_INFO, "Configured interface", "name", iface.Name, "addr", linkLocalAddr)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ai *AutoInterface) startDiscoveryListener(iface *net.Interface) error {
|
||||
// Multicast UDP not supported in TinyGo
|
||||
return fmt.Errorf("startDiscoveryListener not supported in TinyGo - requires multicast UDP")
|
||||
addr := &net.UDPAddr{
|
||||
IP: net.ParseIP(ai.mcastDiscoveryAddr),
|
||||
Port: ai.discoveryPort,
|
||||
Zone: iface.Name,
|
||||
}
|
||||
|
||||
conn, err := net.ListenMulticastUDP("udp6", iface, addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := conn.SetReadBuffer(common.NUM_1024); err != nil {
|
||||
debug.Log(debug.DEBUG_ERROR, "Failed to set discovery read buffer", "error", err)
|
||||
}
|
||||
|
||||
ai.Mutex.Lock()
|
||||
ai.discoveryServers[iface.Name] = conn
|
||||
ai.Mutex.Unlock()
|
||||
|
||||
go ai.handleDiscovery(conn, iface.Name)
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Discovery listener started", "interface", iface.Name, "addr", ai.mcastDiscoveryAddr)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ai *AutoInterface) startDataListener(iface *net.Interface) error {
|
||||
// TinyGo doesn't support UDP servers
|
||||
return fmt.Errorf("startDataListener not supported in TinyGo")
|
||||
adoptedIface, exists := ai.adoptedInterfaces[iface.Name]
|
||||
if !exists {
|
||||
return fmt.Errorf("interface not adopted")
|
||||
}
|
||||
|
||||
addr := &net.UDPAddr{
|
||||
IP: net.ParseIP(adoptedIface.linkLocalAddr),
|
||||
Port: ai.dataPort,
|
||||
Zone: iface.Name,
|
||||
}
|
||||
|
||||
conn, err := net.ListenUDP("udp6", addr)
|
||||
if err != nil {
|
||||
debug.Log(debug.DEBUG_ERROR, "Failed to listen on data port", "addr", addr, "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := conn.SetReadBuffer(ai.MTU); err != nil {
|
||||
debug.Log(debug.DEBUG_ERROR, "Failed to set data read buffer", "error", err)
|
||||
}
|
||||
|
||||
ai.Mutex.Lock()
|
||||
ai.interfaceServers[iface.Name] = conn
|
||||
ai.Mutex.Unlock()
|
||||
|
||||
go ai.handleData(conn, iface.Name)
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Data listener started", "interface", iface.Name, "addr", addr)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ai *AutoInterface) handleDiscovery(conn net.Conn, ifaceName string) {
|
||||
// Not used in TinyGo
|
||||
buf := make([]byte, 1024)
|
||||
func (ai *AutoInterface) handleDiscovery(conn *net.UDPConn, ifaceName string) {
|
||||
buf := make([]byte, common.NUM_1024)
|
||||
for {
|
||||
_, err := conn.Read(buf)
|
||||
ai.Mutex.RLock()
|
||||
done := ai.done
|
||||
ai.Mutex.RUnlock()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
n, remoteAddr, err := conn.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
log.Printf("Discovery read error: %v", err)
|
||||
continue
|
||||
if ai.IsOnline() {
|
||||
debug.Log(debug.DEBUG_ERROR, "Discovery read error", "interface", ifaceName, "error", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if n >= len(ai.groupHash) {
|
||||
receivedHash := buf[:len(ai.groupHash)]
|
||||
if bytes.Equal(receivedHash, ai.groupHash) {
|
||||
ai.handlePeerAnnounce(remoteAddr, ifaceName)
|
||||
} else {
|
||||
debug.Log(debug.DEBUG_TRACE, "Received discovery with mismatched group hash", "interface", ifaceName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ai *AutoInterface) handleData(conn net.Conn) {
|
||||
func (ai *AutoInterface) handleData(conn *net.UDPConn, ifaceName string) {
|
||||
buf := make([]byte, ai.GetMTU())
|
||||
for {
|
||||
n, err := conn.Read(buf)
|
||||
ai.Mutex.RLock()
|
||||
done := ai.done
|
||||
ai.Mutex.RUnlock()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
n, _, err := conn.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
if !ai.IsDetached() {
|
||||
log.Printf("Data read error: %v", err)
|
||||
if ai.IsOnline() {
|
||||
debug.Log(debug.DEBUG_ERROR, "Data read error", "interface", ifaceName, "error", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -131,62 +423,186 @@ func (ai *AutoInterface) handleData(conn net.Conn) {
|
||||
}
|
||||
|
||||
func (ai *AutoInterface) handlePeerAnnounce(addr *net.UDPAddr, ifaceName string) {
|
||||
ai.mutex.Lock()
|
||||
defer ai.mutex.Unlock()
|
||||
ai.Mutex.Lock()
|
||||
defer ai.Mutex.Unlock()
|
||||
|
||||
peerAddr := addr.IP.String()
|
||||
peerIP := addr.IP.String()
|
||||
|
||||
for _, localAddr := range ai.linkLocalAddrs {
|
||||
if peerAddr == localAddr {
|
||||
if peerIP == localAddr {
|
||||
ai.multicastEchoes[ifaceName] = time.Now()
|
||||
debug.Log(debug.DEBUG_TRACE, "Received own multicast echo", "interface", ifaceName)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if _, exists := ai.peers[peerAddr]; !exists {
|
||||
ai.peers[peerAddr] = &Peer{
|
||||
peerKey := peerIP + "%" + ifaceName
|
||||
|
||||
if peer, exists := ai.peers[peerKey]; exists {
|
||||
peer.lastHeard = time.Now()
|
||||
debug.Log(debug.DEBUG_TRACE, "Updated peer", "peer", peerIP, "interface", ifaceName)
|
||||
} else {
|
||||
ai.peers[peerKey] = &Peer{
|
||||
ifaceName: ifaceName,
|
||||
lastHeard: time.Now(),
|
||||
addr: addr,
|
||||
}
|
||||
debug.Log(debug.DEBUG_INFO, "Discovered new peer", "peer", peerIP, "interface", ifaceName)
|
||||
}
|
||||
}
|
||||
|
||||
func (ai *AutoInterface) announceLoop() {
|
||||
ticker := time.NewTicker(ai.announceInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if !ai.IsOnline() {
|
||||
return
|
||||
}
|
||||
ai.sendPeerAnnounce()
|
||||
case <-ai.done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ai *AutoInterface) sendPeerAnnounce() {
|
||||
ai.Mutex.RLock()
|
||||
defer ai.Mutex.RUnlock()
|
||||
|
||||
for ifaceName, adoptedIface := range ai.adoptedInterfaces {
|
||||
mcastAddr := &net.UDPAddr{
|
||||
IP: net.ParseIP(ai.mcastDiscoveryAddr),
|
||||
Port: ai.discoveryPort,
|
||||
Zone: ifaceName,
|
||||
}
|
||||
|
||||
if ai.outboundConn == nil {
|
||||
var err error
|
||||
ai.outboundConn, err = net.ListenUDP("udp6", &net.UDPAddr{Port: 0})
|
||||
if err != nil {
|
||||
debug.Log(debug.DEBUG_ERROR, "Failed to create outbound socket", "error", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := ai.outboundConn.WriteToUDP(ai.groupHash, mcastAddr); err != nil {
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Failed to send peer announce", "interface", ifaceName, "error", err)
|
||||
} else {
|
||||
debug.Log(debug.DEBUG_TRACE, "Sent peer announce", "interface", adoptedIface.name)
|
||||
}
|
||||
log.Printf("Added peer %s on %s", peerAddr, ifaceName)
|
||||
} else {
|
||||
ai.peers[peerAddr].lastHeard = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
func (ai *AutoInterface) peerJobs() {
|
||||
ticker := time.NewTicker(PEERING_TIMEOUT)
|
||||
for range ticker.C {
|
||||
ai.mutex.Lock()
|
||||
now := time.Now()
|
||||
ticker := time.NewTicker(ai.peerJobInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for addr, peer := range ai.peers {
|
||||
if now.Sub(peer.lastHeard) > PEERING_TIMEOUT {
|
||||
delete(ai.peers, addr)
|
||||
log.Printf("Removed timed out peer %s", addr)
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if !ai.IsOnline() {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ai.mutex.Unlock()
|
||||
ai.Mutex.Lock()
|
||||
now := time.Now()
|
||||
|
||||
for peerKey, peer := range ai.peers {
|
||||
if now.Sub(peer.lastHeard) > ai.peeringTimeout {
|
||||
delete(ai.peers, peerKey)
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Removed timed out peer", "peer", peerKey)
|
||||
}
|
||||
}
|
||||
|
||||
for ifaceName, echoTime := range ai.multicastEchoes {
|
||||
if now.Sub(echoTime) > ai.mcastEchoTimeout {
|
||||
if _, exists := ai.timedOutInterfaces[ifaceName]; !exists {
|
||||
debug.Log(debug.DEBUG_INFO, "Interface timed out", "interface", ifaceName)
|
||||
ai.timedOutInterfaces[ifaceName] = now
|
||||
}
|
||||
} else {
|
||||
delete(ai.timedOutInterfaces, ifaceName)
|
||||
}
|
||||
}
|
||||
|
||||
ai.Mutex.Unlock()
|
||||
case <-ai.done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ai *AutoInterface) Send(data []byte, address string) error {
|
||||
// TinyGo doesn't support UDP outbound connections for auto-discovery
|
||||
return fmt.Errorf("Send not supported in TinyGo - requires UDP client connections")
|
||||
if !ai.IsOnline() {
|
||||
return fmt.Errorf("interface offline")
|
||||
}
|
||||
|
||||
ai.Mutex.RLock()
|
||||
defer ai.Mutex.RUnlock()
|
||||
|
||||
if len(ai.peers) == 0 {
|
||||
debug.Log(debug.DEBUG_TRACE, "No peers available for sending")
|
||||
return nil
|
||||
}
|
||||
|
||||
if ai.outboundConn == nil {
|
||||
var err error
|
||||
ai.outboundConn, err = net.ListenUDP("udp6", &net.UDPAddr{Port: 0})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create outbound socket: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
sentCount := 0
|
||||
for _, peer := range ai.peers {
|
||||
targetAddr := &net.UDPAddr{
|
||||
IP: peer.addr.IP,
|
||||
Port: ai.dataPort,
|
||||
Zone: peer.ifaceName,
|
||||
}
|
||||
|
||||
if _, err := ai.outboundConn.WriteToUDP(data, targetAddr); err != nil {
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Failed to send to peer", "interface", peer.ifaceName, "error", err)
|
||||
continue
|
||||
}
|
||||
sentCount++
|
||||
}
|
||||
|
||||
if sentCount > 0 {
|
||||
debug.Log(debug.DEBUG_TRACE, "Sent data to peers", "count", sentCount, "bytes", len(data))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ai *AutoInterface) Stop() error {
|
||||
ai.mutex.Lock()
|
||||
defer ai.mutex.Unlock()
|
||||
ai.Mutex.Lock()
|
||||
ai.Online = false
|
||||
ai.IN = false
|
||||
ai.OUT = false
|
||||
|
||||
for _, server := range ai.interfaceServers {
|
||||
server.Close() // #nosec G104
|
||||
}
|
||||
|
||||
for _, server := range ai.discoveryServers {
|
||||
server.Close() // #nosec G104
|
||||
}
|
||||
|
||||
if ai.outboundConn != nil {
|
||||
ai.outboundConn.Close() // #nosec G104
|
||||
}
|
||||
ai.Mutex.Unlock()
|
||||
|
||||
ai.stopOnce.Do(func() {
|
||||
if ai.done != nil {
|
||||
close(ai.done)
|
||||
}
|
||||
})
|
||||
|
||||
debug.Log(debug.DEBUG_INFO, "AutoInterface stopped")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||
)
|
||||
|
||||
func TestNewAutoInterface(t *testing.T) {
|
||||
@@ -44,9 +44,10 @@ func TestNewAutoInterface(t *testing.T) {
|
||||
|
||||
t.Run("CustomConfig", func(t *testing.T) {
|
||||
config := &common.InterfaceConfig{
|
||||
Enabled: true,
|
||||
Port: 12345, // Custom discovery port
|
||||
GroupID: "customGroup",
|
||||
Enabled: true,
|
||||
DiscoveryPort: 12345,
|
||||
DataPort: 54321,
|
||||
GroupID: "customGroup",
|
||||
}
|
||||
ai, err := NewAutoInterface("autoCustom", config)
|
||||
if err != nil {
|
||||
@@ -59,6 +60,9 @@ func TestNewAutoInterface(t *testing.T) {
|
||||
if ai.discoveryPort != 12345 {
|
||||
t.Errorf("discoveryPort = %d; want 12345", ai.discoveryPort)
|
||||
}
|
||||
if ai.dataPort != 54321 {
|
||||
t.Errorf("dataPort = %d; want 54321", ai.dataPort)
|
||||
}
|
||||
if string(ai.groupID) != "customGroup" {
|
||||
t.Errorf("groupID = %s; want customGroup", string(ai.groupID))
|
||||
}
|
||||
@@ -79,9 +83,11 @@ func newMockAutoInterface(name string, config *common.InterfaceConfig) (*mockAut
|
||||
// Initialize maps that would normally be initialized in Start()
|
||||
ai.peers = make(map[string]*Peer)
|
||||
ai.linkLocalAddrs = make([]string, 0)
|
||||
ai.adoptedInterfaces = make(map[string]string)
|
||||
ai.adoptedInterfaces = make(map[string]*AdoptedInterface)
|
||||
ai.interfaceServers = make(map[string]*net.UDPConn)
|
||||
ai.discoveryServers = make(map[string]*net.UDPConn)
|
||||
ai.multicastEchoes = make(map[string]time.Time)
|
||||
ai.timedOutInterfaces = make(map[string]time.Time)
|
||||
|
||||
return &mockAutoInterface{AutoInterface: ai}, nil
|
||||
}
|
||||
@@ -138,14 +144,14 @@ func TestAutoInterfacePeerManagement(t *testing.T) {
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
ai.mutex.Lock()
|
||||
ai.Mutex.Lock()
|
||||
now := time.Now()
|
||||
for addr, peer := range ai.peers {
|
||||
if now.Sub(peer.lastHeard) > testTimeout {
|
||||
delete(ai.peers, addr)
|
||||
}
|
||||
}
|
||||
ai.mutex.Unlock()
|
||||
ai.Mutex.Unlock()
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
@@ -167,27 +173,26 @@ func TestAutoInterfacePeerManagement(t *testing.T) {
|
||||
peer2Addr := &net.UDPAddr{IP: net.ParseIP("fe80::2"), Zone: "eth0"}
|
||||
localAddr := &net.UDPAddr{IP: net.ParseIP("fe80::aaaa"), Zone: "eth0"}
|
||||
|
||||
// Add a simulated local address to avoid adding it as a peer
|
||||
ai.mutex.Lock()
|
||||
ai.Mutex.Lock()
|
||||
ai.linkLocalAddrs = append(ai.linkLocalAddrs, localAddrStr)
|
||||
ai.mutex.Unlock()
|
||||
ai.Mutex.Unlock()
|
||||
|
||||
t.Run("AddPeer1", func(t *testing.T) {
|
||||
ai.mutex.Lock()
|
||||
ai.Mutex.Lock()
|
||||
ai.mockHandlePeerAnnounce(peer1Addr, "eth0")
|
||||
ai.mutex.Unlock()
|
||||
ai.Mutex.Unlock()
|
||||
|
||||
// Give a small amount of time for the peer to be processed
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
ai.mutex.RLock()
|
||||
ai.Mutex.RLock()
|
||||
count := len(ai.peers)
|
||||
peer, exists := ai.peers[peer1AddrStr]
|
||||
var ifaceName string
|
||||
if exists {
|
||||
ifaceName = peer.ifaceName
|
||||
}
|
||||
ai.mutex.RUnlock()
|
||||
ai.Mutex.RUnlock()
|
||||
|
||||
if count != 1 {
|
||||
t.Fatalf("Expected 1 peer, got %d", count)
|
||||
@@ -201,17 +206,17 @@ func TestAutoInterfacePeerManagement(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("AddPeer2", func(t *testing.T) {
|
||||
ai.mutex.Lock()
|
||||
ai.Mutex.Lock()
|
||||
ai.mockHandlePeerAnnounce(peer2Addr, "eth0")
|
||||
ai.mutex.Unlock()
|
||||
ai.Mutex.Unlock()
|
||||
|
||||
// Give a small amount of time for the peer to be processed
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
ai.mutex.RLock()
|
||||
ai.Mutex.RLock()
|
||||
count := len(ai.peers)
|
||||
_, exists := ai.peers[peer2AddrStr]
|
||||
ai.mutex.RUnlock()
|
||||
ai.Mutex.RUnlock()
|
||||
|
||||
if count != 2 {
|
||||
t.Fatalf("Expected 2 peers, got %d", count)
|
||||
@@ -222,16 +227,16 @@ func TestAutoInterfacePeerManagement(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("IgnoreLocalAnnounce", func(t *testing.T) {
|
||||
ai.mutex.Lock()
|
||||
ai.Mutex.Lock()
|
||||
ai.mockHandlePeerAnnounce(localAddr, "eth0")
|
||||
ai.mutex.Unlock()
|
||||
ai.Mutex.Unlock()
|
||||
|
||||
// Give a small amount of time for the peer to be processed
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
ai.mutex.RLock()
|
||||
ai.Mutex.RLock()
|
||||
count := len(ai.peers)
|
||||
ai.mutex.RUnlock()
|
||||
ai.Mutex.RUnlock()
|
||||
|
||||
if count != 2 {
|
||||
t.Fatalf("Expected 2 peers after local announce, got %d", count)
|
||||
@@ -239,32 +244,32 @@ func TestAutoInterfacePeerManagement(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("UpdatePeerTimestamp", func(t *testing.T) {
|
||||
ai.mutex.RLock()
|
||||
ai.Mutex.RLock()
|
||||
peer, exists := ai.peers[peer1AddrStr]
|
||||
var initialTime time.Time
|
||||
if exists {
|
||||
initialTime = peer.lastHeard
|
||||
}
|
||||
ai.mutex.RUnlock()
|
||||
ai.Mutex.RUnlock()
|
||||
|
||||
if !exists {
|
||||
t.Fatalf("Peer %s not found before timestamp update", peer1AddrStr)
|
||||
}
|
||||
|
||||
ai.mutex.Lock()
|
||||
ai.Mutex.Lock()
|
||||
ai.mockHandlePeerAnnounce(peer1Addr, "eth0")
|
||||
ai.mutex.Unlock()
|
||||
ai.Mutex.Unlock()
|
||||
|
||||
// Give a small amount of time for the peer to be processed
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
ai.mutex.RLock()
|
||||
ai.Mutex.RLock()
|
||||
peer, exists = ai.peers[peer1AddrStr]
|
||||
var updatedTime time.Time
|
||||
if exists {
|
||||
updatedTime = peer.lastHeard
|
||||
}
|
||||
ai.mutex.RUnlock()
|
||||
ai.Mutex.RUnlock()
|
||||
|
||||
if !exists {
|
||||
t.Fatalf("Peer %s not found after timestamp update", peer1AddrStr)
|
||||
@@ -279,9 +284,9 @@ func TestAutoInterfacePeerManagement(t *testing.T) {
|
||||
// Wait for peer timeout
|
||||
time.Sleep(testTimeout * 2)
|
||||
|
||||
ai.mutex.RLock()
|
||||
ai.Mutex.RLock()
|
||||
count := len(ai.peers)
|
||||
ai.mutex.RUnlock()
|
||||
ai.Mutex.RUnlock()
|
||||
|
||||
if count != 0 {
|
||||
t.Errorf("Expected all peers to timeout, got %d peers", count)
|
||||
|
||||
97
pkg/interfaces/auto_tinygo.go
Normal file
97
pkg/interfaces/auto_tinygo.go
Normal file
@@ -0,0 +1,97 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
//go:build tinygo
|
||||
// +build tinygo
|
||||
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||
)
|
||||
|
||||
const (
|
||||
HW_MTU = 1196
|
||||
DEFAULT_DISCOVERY_PORT = 29716
|
||||
DEFAULT_DATA_PORT = 42671
|
||||
DEFAULT_GROUP_ID = "reticulum"
|
||||
BITRATE_GUESS = 10 * 1000 * 1000
|
||||
)
|
||||
|
||||
type AutoInterface struct {
|
||||
BaseInterface
|
||||
groupID []byte
|
||||
discoveryPort int
|
||||
dataPort int
|
||||
discoveryScope string
|
||||
peers map[string]*Peer
|
||||
linkLocalAddrs []string
|
||||
adoptedInterfaces map[string]string
|
||||
interfaceServers map[string]net.Conn
|
||||
multicastEchoes map[string]time.Time
|
||||
mutex sync.RWMutex
|
||||
outboundConn net.Conn
|
||||
}
|
||||
|
||||
type Peer struct {
|
||||
ifaceName string
|
||||
lastHeard time.Time
|
||||
conn net.PacketConn
|
||||
}
|
||||
|
||||
func NewAutoInterface(name string, config *common.InterfaceConfig) (*AutoInterface, error) {
|
||||
ai := &AutoInterface{
|
||||
BaseInterface: BaseInterface{
|
||||
Name: name,
|
||||
Mode: common.IF_MODE_FULL,
|
||||
Type: common.IF_TYPE_AUTO,
|
||||
Online: false,
|
||||
Enabled: config.Enabled,
|
||||
Detached: false,
|
||||
IN: true,
|
||||
OUT: false,
|
||||
MTU: HW_MTU,
|
||||
Bitrate: BITRATE_GUESS,
|
||||
},
|
||||
discoveryPort: DEFAULT_DISCOVERY_PORT,
|
||||
dataPort: DEFAULT_DATA_PORT,
|
||||
peers: make(map[string]*Peer),
|
||||
linkLocalAddrs: make([]string, 0),
|
||||
adoptedInterfaces: make(map[string]string),
|
||||
interfaceServers: make(map[string]net.Conn),
|
||||
multicastEchoes: make(map[string]time.Time),
|
||||
}
|
||||
|
||||
if config.Port != 0 {
|
||||
ai.discoveryPort = config.Port
|
||||
}
|
||||
|
||||
if config.GroupID != "" {
|
||||
ai.groupID = []byte(config.GroupID)
|
||||
} else {
|
||||
ai.groupID = []byte("reticulum")
|
||||
}
|
||||
|
||||
return ai, nil
|
||||
}
|
||||
|
||||
func (ai *AutoInterface) Start() error {
|
||||
// TinyGo doesn't support net.Interfaces() or multicast UDP
|
||||
return fmt.Errorf("AutoInterface not supported in TinyGo - requires interface enumeration and multicast UDP")
|
||||
}
|
||||
|
||||
func (ai *AutoInterface) Send(data []byte, address string) error {
|
||||
return fmt.Errorf("Send not supported in TinyGo - requires UDP client connections")
|
||||
}
|
||||
|
||||
func (ai *AutoInterface) Stop() error {
|
||||
ai.Mutex.Lock()
|
||||
defer ai.Mutex.Unlock()
|
||||
ai.Online = false
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -26,17 +28,6 @@ const (
|
||||
TYPE_TCP = 0x02
|
||||
|
||||
PROPAGATION_RATE = 0.02 // 2% of interface bandwidth
|
||||
|
||||
DEBUG_LEVEL = 4 // Default debug level for interface logging
|
||||
|
||||
// Debug levels
|
||||
DEBUG_CRITICAL = 1
|
||||
DEBUG_ERROR = 2
|
||||
DEBUG_INFO = 3
|
||||
DEBUG_VERBOSE = 4
|
||||
DEBUG_TRACE = 5
|
||||
DEBUG_PACKETS = 6
|
||||
DEBUG_ALL = 7
|
||||
)
|
||||
|
||||
type Interface interface {
|
||||
@@ -78,8 +69,9 @@ type BaseInterface struct {
|
||||
TxBytes uint64
|
||||
RxBytes uint64
|
||||
lastTx time.Time
|
||||
lastRx time.Time
|
||||
|
||||
mutex sync.RWMutex
|
||||
Mutex sync.RWMutex
|
||||
packetCallback common.PacketCallback
|
||||
}
|
||||
|
||||
@@ -96,29 +88,30 @@ func NewBaseInterface(name string, ifType common.InterfaceType, enabled bool) Ba
|
||||
MTU: common.DEFAULT_MTU,
|
||||
Bitrate: BITRATE_MINIMUM,
|
||||
lastTx: time.Now(),
|
||||
lastRx: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (i *BaseInterface) SetPacketCallback(callback common.PacketCallback) {
|
||||
i.mutex.Lock()
|
||||
defer i.mutex.Unlock()
|
||||
i.Mutex.Lock()
|
||||
defer i.Mutex.Unlock()
|
||||
i.packetCallback = callback
|
||||
}
|
||||
|
||||
func (i *BaseInterface) GetPacketCallback() common.PacketCallback {
|
||||
i.mutex.RLock()
|
||||
defer i.mutex.RUnlock()
|
||||
i.Mutex.RLock()
|
||||
defer i.Mutex.RUnlock()
|
||||
return i.packetCallback
|
||||
}
|
||||
|
||||
func (i *BaseInterface) ProcessIncoming(data []byte) {
|
||||
i.mutex.Lock()
|
||||
i.Mutex.Lock()
|
||||
i.RxBytes += uint64(len(data))
|
||||
i.mutex.Unlock()
|
||||
i.Mutex.Unlock()
|
||||
|
||||
i.mutex.RLock()
|
||||
i.Mutex.RLock()
|
||||
callback := i.packetCallback
|
||||
i.mutex.RUnlock()
|
||||
i.Mutex.RUnlock()
|
||||
|
||||
if callback != nil {
|
||||
callback(data, i)
|
||||
@@ -127,15 +120,15 @@ func (i *BaseInterface) ProcessIncoming(data []byte) {
|
||||
|
||||
func (i *BaseInterface) ProcessOutgoing(data []byte) error {
|
||||
if !i.Online || i.Detached {
|
||||
log.Printf("[DEBUG-1] Interface %s: Cannot process outgoing packet - interface offline or detached", i.Name)
|
||||
debug.Log(debug.DEBUG_CRITICAL, "Interface cannot process outgoing packet - interface offline or detached", "name", i.Name)
|
||||
return fmt.Errorf("interface offline or detached")
|
||||
}
|
||||
|
||||
i.mutex.Lock()
|
||||
i.Mutex.Lock()
|
||||
i.TxBytes += uint64(len(data))
|
||||
i.mutex.Unlock()
|
||||
i.Mutex.Unlock()
|
||||
|
||||
log.Printf("[DEBUG-%d] Interface %s: Processed outgoing packet of %d bytes, total TX: %d", DEBUG_LEVEL, i.Name, len(data), i.TxBytes)
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Interface processed outgoing packet", "name", i.Name, "bytes", len(data), "total_tx", i.TxBytes)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -145,7 +138,7 @@ func (i *BaseInterface) SendPathRequest(packet []byte) error {
|
||||
}
|
||||
|
||||
frame := make([]byte, 0, len(packet)+1)
|
||||
frame = append(frame, 0x01)
|
||||
frame = append(frame, common.HEX_0x01)
|
||||
frame = append(frame, packet...)
|
||||
|
||||
return i.ProcessOutgoing(frame)
|
||||
@@ -157,7 +150,7 @@ func (i *BaseInterface) SendLinkPacket(dest []byte, data []byte, timestamp time.
|
||||
}
|
||||
|
||||
frame := make([]byte, 0, len(dest)+len(data)+9)
|
||||
frame = append(frame, 0x02)
|
||||
frame = append(frame, common.HEX_0x02)
|
||||
frame = append(frame, dest...)
|
||||
|
||||
ts := make([]byte, 8)
|
||||
@@ -169,35 +162,35 @@ func (i *BaseInterface) SendLinkPacket(dest []byte, data []byte, timestamp time.
|
||||
}
|
||||
|
||||
func (i *BaseInterface) Detach() {
|
||||
i.mutex.Lock()
|
||||
defer i.mutex.Unlock()
|
||||
i.Mutex.Lock()
|
||||
defer i.Mutex.Unlock()
|
||||
i.Detached = true
|
||||
i.Online = false
|
||||
}
|
||||
|
||||
func (i *BaseInterface) IsEnabled() bool {
|
||||
i.mutex.RLock()
|
||||
defer i.mutex.RUnlock()
|
||||
i.Mutex.RLock()
|
||||
defer i.Mutex.RUnlock()
|
||||
return i.Enabled && i.Online && !i.Detached
|
||||
}
|
||||
|
||||
func (i *BaseInterface) Enable() {
|
||||
i.mutex.Lock()
|
||||
defer i.mutex.Unlock()
|
||||
i.Mutex.Lock()
|
||||
defer i.Mutex.Unlock()
|
||||
|
||||
prevState := i.Enabled
|
||||
i.Enabled = true
|
||||
i.Online = true
|
||||
|
||||
log.Printf("[DEBUG-%d] Interface %s: State changed - Enabled: %v->%v, Online: %v->%v", DEBUG_INFO, i.Name, prevState, i.Enabled, !i.Online, i.Online)
|
||||
debug.Log(debug.DEBUG_INFO, "Interface state changed", "name", i.Name, "enabled_prev", prevState, "enabled", i.Enabled, "online_prev", !i.Online, "online", i.Online)
|
||||
}
|
||||
|
||||
func (i *BaseInterface) Disable() {
|
||||
i.mutex.Lock()
|
||||
defer i.mutex.Unlock()
|
||||
i.Mutex.Lock()
|
||||
defer i.Mutex.Unlock()
|
||||
i.Enabled = false
|
||||
i.Online = false
|
||||
log.Printf("[DEBUG-2] Interface %s: Disabled and offline", i.Name)
|
||||
debug.Log(debug.DEBUG_ERROR, "Interface disabled and offline", "name", i.Name)
|
||||
}
|
||||
|
||||
func (i *BaseInterface) GetName() string {
|
||||
@@ -217,14 +210,14 @@ func (i *BaseInterface) GetMTU() int {
|
||||
}
|
||||
|
||||
func (i *BaseInterface) IsOnline() bool {
|
||||
i.mutex.RLock()
|
||||
defer i.mutex.RUnlock()
|
||||
i.Mutex.RLock()
|
||||
defer i.Mutex.RUnlock()
|
||||
return i.Online
|
||||
}
|
||||
|
||||
func (i *BaseInterface) IsDetached() bool {
|
||||
i.mutex.RLock()
|
||||
defer i.mutex.RUnlock()
|
||||
i.Mutex.RLock()
|
||||
defer i.Mutex.RUnlock()
|
||||
return i.Detached
|
||||
}
|
||||
|
||||
@@ -237,11 +230,11 @@ func (i *BaseInterface) Stop() error {
|
||||
}
|
||||
|
||||
func (i *BaseInterface) Send(data []byte, address string) error {
|
||||
log.Printf("[DEBUG-%d] Interface %s: Sending %d bytes to %s", DEBUG_LEVEL, i.Name, len(data), address)
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Interface sending bytes", "name", i.Name, "bytes", len(data), "address", address)
|
||||
|
||||
err := i.ProcessOutgoing(data)
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG-1] Interface %s: Failed to send data: %v", i.Name, err)
|
||||
debug.Log(debug.DEBUG_CRITICAL, "Interface failed to send data", "name", i.Name, "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -254,14 +247,14 @@ func (i *BaseInterface) GetConn() net.Conn {
|
||||
}
|
||||
|
||||
func (i *BaseInterface) GetBandwidthAvailable() bool {
|
||||
i.mutex.RLock()
|
||||
defer i.mutex.RUnlock()
|
||||
i.Mutex.RLock()
|
||||
defer i.Mutex.RUnlock()
|
||||
|
||||
now := time.Now()
|
||||
timeSinceLastTx := now.Sub(i.lastTx)
|
||||
|
||||
if timeSinceLastTx > time.Second {
|
||||
log.Printf("[DEBUG-%d] Interface %s: Bandwidth available (idle for %.2fs)", DEBUG_VERBOSE, i.Name, timeSinceLastTx.Seconds())
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Interface bandwidth available", "name", i.Name, "idle_seconds", timeSinceLastTx.Seconds())
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -270,19 +263,19 @@ func (i *BaseInterface) GetBandwidthAvailable() bool {
|
||||
maxUsage := float64(i.Bitrate) * PROPAGATION_RATE
|
||||
|
||||
available := currentUsage < maxUsage
|
||||
log.Printf("[DEBUG-%d] Interface %s: Bandwidth stats - Current: %.2f bps, Max: %.2f bps, Usage: %.1f%%, Available: %v", DEBUG_VERBOSE, i.Name, currentUsage, maxUsage, (currentUsage/maxUsage)*100, available)
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Interface bandwidth stats", "name", i.Name, "current_bps", currentUsage, "max_bps", maxUsage, "usage_percent", (currentUsage/maxUsage)*100, "available", available)
|
||||
|
||||
return available
|
||||
}
|
||||
|
||||
func (i *BaseInterface) updateBandwidthStats(bytes uint64) {
|
||||
i.mutex.Lock()
|
||||
defer i.mutex.Unlock()
|
||||
i.Mutex.Lock()
|
||||
defer i.Mutex.Unlock()
|
||||
|
||||
i.TxBytes += bytes
|
||||
i.lastTx = time.Now()
|
||||
|
||||
log.Printf("[DEBUG-%d] Interface %s: Updated bandwidth stats - TX bytes: %d, Last TX: %v", DEBUG_LEVEL, i.Name, i.TxBytes, i.lastTx)
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Interface updated bandwidth stats", "name", i.Name, "tx_bytes", i.TxBytes, "last_tx", i.lastTx)
|
||||
}
|
||||
|
||||
type InterceptedInterface struct {
|
||||
@@ -305,7 +298,7 @@ func (i *InterceptedInterface) Send(data []byte, addr string) error {
|
||||
// Call interceptor if provided
|
||||
if i.interceptor != nil && len(data) > 0 {
|
||||
if err := i.interceptor(data, i); err != nil {
|
||||
log.Printf("[DEBUG-2] Failed to intercept outgoing packet: %v", err)
|
||||
debug.Log(debug.DEBUG_ERROR, "Failed to intercept outgoing packet", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||
)
|
||||
|
||||
func TestBaseInterfaceStateChanges(t *testing.T) {
|
||||
@@ -183,7 +183,6 @@ func (m *mockInterface) Send(data []byte, addr string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add other methods to satisfy the Interface interface (can be minimal/panic)
|
||||
func (m *mockInterface) GetType() common.InterfaceType { return common.IF_TYPE_NONE }
|
||||
func (m *mockInterface) GetMode() common.InterfaceMode { return common.IF_MODE_FULL }
|
||||
func (m *mockInterface) ProcessIncoming(data []byte) {}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -21,14 +23,26 @@ const (
|
||||
KISS_TFEND = 0xDC
|
||||
KISS_TFESC = 0xDD
|
||||
|
||||
TCP_USER_TIMEOUT = 24
|
||||
TCP_PROBE_AFTER = 5
|
||||
TCP_PROBE_INTERVAL = 2
|
||||
TCP_PROBES = 12
|
||||
RECONNECT_WAIT = 5
|
||||
INITIAL_TIMEOUT = 5
|
||||
INITIAL_BACKOFF = time.Second
|
||||
MAX_BACKOFF = time.Minute * 5
|
||||
DEFAULT_MTU = 1064
|
||||
BITRATE_GUESS_VAL = 10 * 1000 * 1000
|
||||
RECONNECT_WAIT = 5
|
||||
INITIAL_TIMEOUT = 5
|
||||
INITIAL_BACKOFF = time.Second
|
||||
MAX_BACKOFF = time.Minute * 5
|
||||
|
||||
TCP_USER_TIMEOUT_SEC = 24
|
||||
TCP_PROBE_AFTER_SEC = 5
|
||||
TCP_PROBE_INTERVAL_SEC = 2
|
||||
TCP_PROBES_COUNT = 12
|
||||
TCP_CONNECT_TIMEOUT = 10 * time.Second
|
||||
TCP_MILLISECONDS = 1000
|
||||
|
||||
I2P_USER_TIMEOUT_SEC = 45
|
||||
I2P_PROBE_AFTER_SEC = 10
|
||||
I2P_PROBE_INTERVAL_SEC = 9
|
||||
I2P_PROBES_COUNT = 5
|
||||
|
||||
SO_KEEPALIVE_ENABLE = 1
|
||||
)
|
||||
|
||||
type TCPClientInterface struct {
|
||||
@@ -45,12 +59,8 @@ type TCPClientInterface struct {
|
||||
maxReconnectTries int
|
||||
packetBuffer []byte
|
||||
packetType byte
|
||||
mutex sync.RWMutex
|
||||
enabled bool
|
||||
TxBytes uint64
|
||||
RxBytes uint64
|
||||
lastTx time.Time
|
||||
lastRx time.Time
|
||||
done chan struct{}
|
||||
stopOnce sync.Once
|
||||
}
|
||||
|
||||
func NewTCPClientInterface(name string, targetHost string, targetPort int, kissFraming bool, i2pTunneled bool, enabled bool) (*TCPClientInterface, error) {
|
||||
@@ -61,10 +71,10 @@ func NewTCPClientInterface(name string, targetHost string, targetPort int, kissF
|
||||
kissFraming: kissFraming,
|
||||
i2pTunneled: i2pTunneled,
|
||||
initiator: true,
|
||||
enabled: enabled,
|
||||
maxReconnectTries: TCP_PROBES,
|
||||
maxReconnectTries: RECONNECT_WAIT * TCP_PROBES_COUNT,
|
||||
packetBuffer: make([]byte, 0),
|
||||
neverConnected: true,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
|
||||
if enabled {
|
||||
@@ -82,43 +92,81 @@ func NewTCPClientInterface(name string, targetHost string, targetPort int, kissF
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) Start() error {
|
||||
tc.mutex.Lock()
|
||||
defer tc.mutex.Unlock()
|
||||
|
||||
if !tc.Enabled {
|
||||
return fmt.Errorf("interface not enabled")
|
||||
tc.Mutex.Lock()
|
||||
if !tc.Enabled || tc.Detached {
|
||||
tc.Mutex.Unlock()
|
||||
return fmt.Errorf("interface not enabled or detached")
|
||||
}
|
||||
|
||||
if tc.conn != nil {
|
||||
tc.Online = true
|
||||
go tc.readLoop()
|
||||
tc.Mutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Only recreate done if it's nil or was closed
|
||||
select {
|
||||
case <-tc.done:
|
||||
tc.done = make(chan struct{})
|
||||
tc.stopOnce = sync.Once{}
|
||||
default:
|
||||
if tc.done == nil {
|
||||
tc.done = make(chan struct{})
|
||||
tc.stopOnce = sync.Once{}
|
||||
}
|
||||
}
|
||||
tc.Mutex.Unlock()
|
||||
|
||||
addr := net.JoinHostPort(tc.targetAddr, fmt.Sprintf("%d", tc.targetPort))
|
||||
conn, err := net.Dial("tcp", addr)
|
||||
conn, err := net.DialTimeout("tcp", addr, TCP_CONNECT_TIMEOUT)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tc.Mutex.Lock()
|
||||
tc.conn = conn
|
||||
tc.Mutex.Unlock()
|
||||
|
||||
// Set platform-specific timeouts
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
if err := tc.setTimeoutsLinux(); err != nil {
|
||||
log.Printf("[DEBUG-2] Failed to set Linux TCP timeouts: %v", err)
|
||||
debug.Log(debug.DEBUG_ERROR, "Failed to set Linux TCP timeouts", "error", err)
|
||||
}
|
||||
case "darwin":
|
||||
if err := tc.setTimeoutsOSX(); err != nil {
|
||||
log.Printf("[DEBUG-2] Failed to set OSX TCP timeouts: %v", err)
|
||||
debug.Log(debug.DEBUG_ERROR, "Failed to set OSX TCP timeouts", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
tc.Mutex.Lock()
|
||||
tc.Online = true
|
||||
tc.Mutex.Unlock()
|
||||
|
||||
go tc.readLoop()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) Stop() error {
|
||||
tc.Mutex.Lock()
|
||||
tc.Enabled = false
|
||||
tc.Online = false
|
||||
if tc.conn != nil {
|
||||
_ = tc.conn.Close()
|
||||
tc.conn = nil
|
||||
}
|
||||
tc.Mutex.Unlock()
|
||||
|
||||
tc.stopOnce.Do(func() {
|
||||
if tc.done != nil {
|
||||
close(tc.done)
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) readLoop() {
|
||||
buffer := make([]byte, tc.MTU)
|
||||
inFrame := false
|
||||
@@ -126,10 +174,30 @@ func (tc *TCPClientInterface) readLoop() {
|
||||
dataBuffer := make([]byte, 0)
|
||||
|
||||
for {
|
||||
n, err := tc.conn.Read(buffer)
|
||||
tc.Mutex.RLock()
|
||||
conn := tc.conn
|
||||
done := tc.done
|
||||
tc.Mutex.RUnlock()
|
||||
|
||||
if conn == nil {
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
n, err := conn.Read(buffer)
|
||||
if err != nil {
|
||||
tc.Mutex.Lock()
|
||||
tc.Online = false
|
||||
if tc.initiator && !tc.Detached {
|
||||
detached := tc.Detached
|
||||
initiator := tc.initiator
|
||||
tc.Mutex.Unlock()
|
||||
|
||||
if initiator && !detached {
|
||||
go tc.reconnect()
|
||||
} else {
|
||||
tc.teardown()
|
||||
@@ -137,7 +205,6 @@ func (tc *TCPClientInterface) readLoop() {
|
||||
return
|
||||
}
|
||||
|
||||
// Update RX bytes for raw received data
|
||||
tc.UpdateStats(uint64(n), true) // #nosec G115
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
@@ -169,46 +236,47 @@ func (tc *TCPClientInterface) readLoop() {
|
||||
|
||||
func (tc *TCPClientInterface) handlePacket(data []byte) {
|
||||
if len(data) < 1 {
|
||||
log.Printf("[DEBUG-7] Received invalid packet: empty")
|
||||
debug.Log(debug.DEBUG_ALL, "Received invalid packet: empty")
|
||||
return
|
||||
}
|
||||
|
||||
tc.mutex.Lock()
|
||||
tc.Mutex.Lock()
|
||||
tc.RxBytes += uint64(len(data))
|
||||
lastRx := time.Now()
|
||||
tc.lastRx = lastRx
|
||||
tc.mutex.Unlock()
|
||||
callback := tc.packetCallback
|
||||
tc.Mutex.Unlock()
|
||||
|
||||
log.Printf("[DEBUG-7] Received packet: type=0x%02x, size=%d bytes", data[0], len(data))
|
||||
debug.Log(debug.DEBUG_ALL, "Received packet", "type", fmt.Sprintf("0x%02x", data[0]), "size", len(data))
|
||||
|
||||
// For RNS packets, call the packet callback directly
|
||||
if callback := tc.GetPacketCallback(); callback != nil {
|
||||
log.Printf("[DEBUG-7] Calling packet callback for RNS packet")
|
||||
if callback != nil {
|
||||
debug.Log(debug.DEBUG_ALL, "Calling packet callback for RNS packet")
|
||||
callback(data, tc)
|
||||
} else {
|
||||
log.Printf("[DEBUG-7] No packet callback set for TCP interface")
|
||||
debug.Log(debug.DEBUG_ALL, "No packet callback set for TCP interface")
|
||||
}
|
||||
}
|
||||
|
||||
// Send implements the interface Send method for TCP interface
|
||||
func (tc *TCPClientInterface) Send(data []byte, address string) error {
|
||||
log.Printf("[DEBUG-7] TCP interface %s: Sending %d bytes", tc.Name, len(data))
|
||||
|
||||
debug.Log(debug.DEBUG_ALL, "TCP interface sending bytes", "name", tc.Name, "bytes", len(data))
|
||||
|
||||
if !tc.IsEnabled() || !tc.IsOnline() {
|
||||
return fmt.Errorf("TCP interface %s is not online", tc.Name)
|
||||
}
|
||||
|
||||
// For TCP interface, we need to prepend a packet type byte for announce packets
|
||||
// RNS TCP protocol expects: [packet_type][data]
|
||||
frame := make([]byte, 0, len(data)+1)
|
||||
frame = append(frame, 0x01) // Announce packet type
|
||||
frame = append(frame, data...)
|
||||
|
||||
return tc.ProcessOutgoing(frame)
|
||||
// Send data directly - packet type is already in the first byte of data
|
||||
// TCP interface uses HDLC framing around the raw packet
|
||||
return tc.ProcessOutgoing(data)
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) ProcessOutgoing(data []byte) error {
|
||||
if !tc.Online {
|
||||
tc.Mutex.RLock()
|
||||
online := tc.Online
|
||||
tc.Mutex.RUnlock()
|
||||
|
||||
if !online {
|
||||
return fmt.Errorf("interface offline")
|
||||
}
|
||||
|
||||
@@ -220,13 +288,21 @@ func (tc *TCPClientInterface) ProcessOutgoing(data []byte) error {
|
||||
frame = append([]byte{HDLC_FLAG}, escapeHDLC(data)...)
|
||||
frame = append(frame, HDLC_FLAG)
|
||||
|
||||
// Update TX stats before sending
|
||||
tc.UpdateStats(uint64(len(frame)), false)
|
||||
tc.UpdateStats(uint64(len(frame)), false) // #nosec G115
|
||||
|
||||
log.Printf("[DEBUG-7] TCP interface %s: Writing %d bytes to network", tc.Name, len(frame))
|
||||
_, err := tc.conn.Write(frame)
|
||||
debug.Log(debug.DEBUG_ALL, "TCP interface writing to network", "name", tc.Name, "bytes", len(frame))
|
||||
|
||||
tc.Mutex.RLock()
|
||||
conn := tc.conn
|
||||
tc.Mutex.RUnlock()
|
||||
|
||||
if conn == nil {
|
||||
return fmt.Errorf("connection closed")
|
||||
}
|
||||
|
||||
_, err := conn.Write(frame)
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG-1] TCP interface %s: Write failed: %v", tc.Name, err)
|
||||
debug.Log(debug.DEBUG_CRITICAL, "TCP interface write failed", "name", tc.Name, "error", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -236,7 +312,7 @@ func (tc *TCPClientInterface) teardown() {
|
||||
tc.IN = false
|
||||
tc.OUT = false
|
||||
if tc.conn != nil {
|
||||
tc.conn.Close() // #nosec G104
|
||||
_ = tc.conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,9 +348,9 @@ func (tc *TCPClientInterface) SetPacketCallback(cb common.PacketCallback) {
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) IsEnabled() bool {
|
||||
tc.mutex.RLock()
|
||||
defer tc.mutex.RUnlock()
|
||||
return tc.enabled && tc.Online && !tc.Detached
|
||||
tc.Mutex.RLock()
|
||||
defer tc.Mutex.RUnlock()
|
||||
return tc.Enabled && tc.Online && !tc.Detached
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) GetName() string {
|
||||
@@ -282,31 +358,31 @@ func (tc *TCPClientInterface) GetName() string {
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) GetPacketCallback() common.PacketCallback {
|
||||
tc.mutex.RLock()
|
||||
defer tc.mutex.RUnlock()
|
||||
tc.Mutex.RLock()
|
||||
defer tc.Mutex.RUnlock()
|
||||
return tc.packetCallback
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) IsDetached() bool {
|
||||
tc.mutex.RLock()
|
||||
defer tc.mutex.RUnlock()
|
||||
tc.Mutex.RLock()
|
||||
defer tc.Mutex.RUnlock()
|
||||
return tc.Detached
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) IsOnline() bool {
|
||||
tc.mutex.RLock()
|
||||
defer tc.mutex.RUnlock()
|
||||
tc.Mutex.RLock()
|
||||
defer tc.Mutex.RUnlock()
|
||||
return tc.Online
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) reconnect() {
|
||||
tc.mutex.Lock()
|
||||
tc.Mutex.Lock()
|
||||
if tc.reconnecting {
|
||||
tc.mutex.Unlock()
|
||||
tc.Mutex.Unlock()
|
||||
return
|
||||
}
|
||||
tc.reconnecting = true
|
||||
tc.mutex.Unlock()
|
||||
tc.Mutex.Unlock()
|
||||
|
||||
backoff := time.Second
|
||||
maxBackoff := time.Minute * 5
|
||||
@@ -319,21 +395,19 @@ func (tc *TCPClientInterface) reconnect() {
|
||||
|
||||
conn, err := net.Dial("tcp", addr)
|
||||
if err == nil {
|
||||
tc.mutex.Lock()
|
||||
tc.Mutex.Lock()
|
||||
tc.conn = conn
|
||||
tc.Online = true
|
||||
|
||||
tc.neverConnected = false
|
||||
tc.reconnecting = false
|
||||
tc.mutex.Unlock()
|
||||
tc.Mutex.Unlock()
|
||||
|
||||
go tc.readLoop()
|
||||
return
|
||||
}
|
||||
|
||||
// Log reconnection attempt
|
||||
fmt.Printf("Failed to reconnect to %s (attempt %d/%d): %v\n",
|
||||
addr, retries+1, tc.maxReconnectTries, err)
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Failed to reconnect", "target", net.JoinHostPort(tc.targetAddr, fmt.Sprintf("%d", tc.targetPort)), "attempt", retries+1, "maxTries", tc.maxReconnectTries, "error", err)
|
||||
|
||||
// Wait with exponential backoff
|
||||
time.Sleep(backoff)
|
||||
@@ -347,50 +421,48 @@ func (tc *TCPClientInterface) reconnect() {
|
||||
retries++
|
||||
}
|
||||
|
||||
tc.mutex.Lock()
|
||||
tc.Mutex.Lock()
|
||||
tc.reconnecting = false
|
||||
tc.mutex.Unlock()
|
||||
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)
|
||||
debug.Log(debug.DEBUG_ERROR, "Failed to reconnect after all attempts", "target", net.JoinHostPort(tc.targetAddr, fmt.Sprintf("%d", tc.targetPort)), "maxTries", tc.maxReconnectTries)
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) Enable() {
|
||||
tc.mutex.Lock()
|
||||
defer tc.mutex.Unlock()
|
||||
tc.Mutex.Lock()
|
||||
defer tc.Mutex.Unlock()
|
||||
tc.Online = true
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) Disable() {
|
||||
tc.mutex.Lock()
|
||||
defer tc.mutex.Unlock()
|
||||
tc.Mutex.Lock()
|
||||
defer tc.Mutex.Unlock()
|
||||
tc.Online = false
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) IsConnected() bool {
|
||||
tc.mutex.RLock()
|
||||
defer tc.mutex.RUnlock()
|
||||
tc.Mutex.RLock()
|
||||
defer tc.Mutex.RUnlock()
|
||||
return tc.conn != nil && tc.Online && !tc.reconnecting
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) GetRTT() time.Duration {
|
||||
tc.mutex.RLock()
|
||||
defer tc.mutex.RUnlock()
|
||||
tc.Mutex.RLock()
|
||||
defer tc.Mutex.RUnlock()
|
||||
|
||||
if !tc.IsConnected() {
|
||||
return 0
|
||||
}
|
||||
|
||||
if tcpConn, ok := tc.conn.(*net.TCPConn); ok {
|
||||
var rtt time.Duration = 0
|
||||
var rtt time.Duration
|
||||
if runtime.GOOS == "linux" {
|
||||
if info, err := tcpConn.SyscallConn(); err == nil {
|
||||
if err := info.Control(func(fd uintptr) { // #nosec G104
|
||||
rtt = platformGetRTT(fd)
|
||||
}); err != nil {
|
||||
log.Printf("[DEBUG-2] Error in SyscallConn Control: %v", err)
|
||||
debug.Log(debug.DEBUG_ERROR, "Error in SyscallConn Control", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -401,84 +473,50 @@ func (tc *TCPClientInterface) GetRTT() time.Duration {
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) GetTxBytes() uint64 {
|
||||
tc.mutex.RLock()
|
||||
defer tc.mutex.RUnlock()
|
||||
tc.Mutex.RLock()
|
||||
defer tc.Mutex.RUnlock()
|
||||
return tc.TxBytes
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) GetRxBytes() uint64 {
|
||||
tc.mutex.RLock()
|
||||
defer tc.mutex.RUnlock()
|
||||
tc.Mutex.RLock()
|
||||
defer tc.Mutex.RUnlock()
|
||||
return tc.RxBytes
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) UpdateStats(bytes uint64, isRx bool) {
|
||||
tc.mutex.Lock()
|
||||
defer tc.mutex.Unlock()
|
||||
tc.Mutex.Lock()
|
||||
defer tc.Mutex.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
if isRx {
|
||||
tc.RxBytes += bytes
|
||||
tc.lastRx = now
|
||||
log.Printf("[DEBUG-5] Interface %s RX stats: bytes=%d total=%d last=%v",
|
||||
tc.Name, bytes, tc.RxBytes, tc.lastRx)
|
||||
debug.Log(debug.DEBUG_TRACE, "Interface RX stats", "name", tc.Name, "bytes", bytes, "total", tc.RxBytes, "last", tc.lastRx)
|
||||
} else {
|
||||
tc.TxBytes += bytes
|
||||
tc.lastTx = now
|
||||
log.Printf("[DEBUG-5] Interface %s TX stats: bytes=%d total=%d last=%v",
|
||||
tc.Name, bytes, tc.TxBytes, tc.lastTx)
|
||||
debug.Log(debug.DEBUG_TRACE, "Interface TX stats", "name", tc.Name, "bytes", bytes, "total", tc.TxBytes, "last", tc.lastTx)
|
||||
}
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) GetStats() (tx uint64, rx uint64, lastTx time.Time, lastRx time.Time) {
|
||||
tc.mutex.RLock()
|
||||
defer tc.mutex.RUnlock()
|
||||
tc.Mutex.RLock()
|
||||
defer tc.Mutex.RUnlock()
|
||||
return tc.TxBytes, tc.RxBytes, tc.lastTx, tc.lastRx
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) setTimeoutsLinux() error {
|
||||
tcpConn, ok := tc.conn.(*net.TCPConn)
|
||||
if !ok {
|
||||
return fmt.Errorf("not a TCP connection")
|
||||
}
|
||||
|
||||
if !tc.i2pTunneled {
|
||||
if err := tcpConn.SetKeepAlive(true); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tcpConn.SetKeepAlivePeriod(time.Duration(TCP_PROBE_INTERVAL) * time.Second); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) setTimeoutsOSX() error {
|
||||
tcpConn, ok := tc.conn.(*net.TCPConn)
|
||||
if !ok {
|
||||
return fmt.Errorf("not a TCP connection")
|
||||
}
|
||||
|
||||
if err := tcpConn.SetKeepAlive(true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type TCPServerInterface struct {
|
||||
BaseInterface
|
||||
connections map[string]net.Conn
|
||||
mutex sync.RWMutex
|
||||
bindAddr string
|
||||
bindPort int
|
||||
preferIPv6 bool
|
||||
kissFraming bool
|
||||
i2pTunneled bool
|
||||
packetCallback common.PacketCallback
|
||||
TxBytes uint64
|
||||
RxBytes uint64
|
||||
connections map[string]net.Conn
|
||||
listener net.Listener
|
||||
bindAddr string
|
||||
bindPort int
|
||||
preferIPv6 bool
|
||||
kissFraming bool
|
||||
i2pTunneled bool
|
||||
done chan struct{}
|
||||
stopOnce sync.Once
|
||||
}
|
||||
|
||||
func NewTCPServerInterface(name string, bindAddr string, bindPort int, kissFraming bool, i2pTunneled bool, preferIPv6 bool) (*TCPServerInterface, error) {
|
||||
@@ -489,6 +527,7 @@ func NewTCPServerInterface(name string, bindAddr string, bindPort int, kissFrami
|
||||
Type: common.IF_TYPE_TCP,
|
||||
Online: false,
|
||||
MTU: common.DEFAULT_MTU,
|
||||
Enabled: true,
|
||||
Detached: false,
|
||||
},
|
||||
connections: make(map[string]net.Conn),
|
||||
@@ -497,6 +536,7 @@ func NewTCPServerInterface(name string, bindAddr string, bindPort int, kissFrami
|
||||
preferIPv6: preferIPv6,
|
||||
kissFraming: kissFraming,
|
||||
i2pTunneled: i2pTunneled,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
|
||||
return ts, nil
|
||||
@@ -515,21 +555,21 @@ func (ts *TCPServerInterface) String() string {
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) SetPacketCallback(callback common.PacketCallback) {
|
||||
ts.mutex.Lock()
|
||||
defer ts.mutex.Unlock()
|
||||
ts.Mutex.Lock()
|
||||
defer ts.Mutex.Unlock()
|
||||
ts.packetCallback = callback
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) GetPacketCallback() common.PacketCallback {
|
||||
ts.mutex.RLock()
|
||||
defer ts.mutex.RUnlock()
|
||||
ts.Mutex.RLock()
|
||||
defer ts.Mutex.RUnlock()
|
||||
return ts.packetCallback
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) IsEnabled() bool {
|
||||
ts.mutex.RLock()
|
||||
defer ts.mutex.RUnlock()
|
||||
return ts.BaseInterface.Enabled && ts.BaseInterface.Online && !ts.BaseInterface.Detached
|
||||
ts.Mutex.RLock()
|
||||
defer ts.Mutex.RUnlock()
|
||||
return ts.Enabled && ts.Online && !ts.Detached
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) GetName() string {
|
||||
@@ -537,50 +577,81 @@ func (ts *TCPServerInterface) GetName() string {
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) IsDetached() bool {
|
||||
ts.mutex.RLock()
|
||||
defer ts.mutex.RUnlock()
|
||||
return ts.BaseInterface.Detached
|
||||
ts.Mutex.RLock()
|
||||
defer ts.Mutex.RUnlock()
|
||||
return ts.Detached
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) IsOnline() bool {
|
||||
ts.mutex.RLock()
|
||||
defer ts.mutex.RUnlock()
|
||||
ts.Mutex.RLock()
|
||||
defer ts.Mutex.RUnlock()
|
||||
return ts.Online
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) Enable() {
|
||||
ts.mutex.Lock()
|
||||
defer ts.mutex.Unlock()
|
||||
ts.Mutex.Lock()
|
||||
defer ts.Mutex.Unlock()
|
||||
ts.Online = true
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) Disable() {
|
||||
ts.mutex.Lock()
|
||||
defer ts.mutex.Unlock()
|
||||
ts.Mutex.Lock()
|
||||
defer ts.Mutex.Unlock()
|
||||
ts.Online = false
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) Start() error {
|
||||
ts.mutex.Lock()
|
||||
defer ts.mutex.Unlock()
|
||||
ts.Mutex.Lock()
|
||||
if ts.listener != nil {
|
||||
ts.Mutex.Unlock()
|
||||
return fmt.Errorf("TCP server already started")
|
||||
}
|
||||
// Only recreate done if it's nil or was closed
|
||||
select {
|
||||
case <-ts.done:
|
||||
ts.done = make(chan struct{})
|
||||
ts.stopOnce = sync.Once{}
|
||||
default:
|
||||
if ts.done == nil {
|
||||
ts.done = make(chan struct{})
|
||||
ts.stopOnce = sync.Once{}
|
||||
}
|
||||
}
|
||||
ts.Mutex.Unlock()
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", ts.bindAddr, ts.bindPort)
|
||||
addr := net.JoinHostPort(ts.bindAddr, fmt.Sprintf("%d", ts.bindPort))
|
||||
listener, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start TCP server: %w", err)
|
||||
}
|
||||
|
||||
ts.Mutex.Lock()
|
||||
ts.listener = listener
|
||||
ts.Online = true
|
||||
ts.Mutex.Unlock()
|
||||
|
||||
// Accept connections in a goroutine
|
||||
go func() {
|
||||
for {
|
||||
ts.Mutex.RLock()
|
||||
done := ts.done
|
||||
ts.Mutex.RUnlock()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
if !ts.Online {
|
||||
ts.Mutex.RLock()
|
||||
online := ts.Online
|
||||
ts.Mutex.RUnlock()
|
||||
if !online {
|
||||
return // Normal shutdown
|
||||
}
|
||||
log.Printf("[DEBUG-2] Error accepting connection: %v", err)
|
||||
debug.Log(debug.DEBUG_ERROR, "Error accepting connection", "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -593,60 +664,87 @@ func (ts *TCPServerInterface) Start() error {
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) Stop() error {
|
||||
ts.mutex.Lock()
|
||||
defer ts.mutex.Unlock()
|
||||
|
||||
ts.Mutex.Lock()
|
||||
ts.Online = false
|
||||
if ts.listener != nil {
|
||||
_ = ts.listener.Close()
|
||||
ts.listener = nil
|
||||
}
|
||||
// Close all client connections
|
||||
for addr, conn := range ts.connections {
|
||||
_ = conn.Close()
|
||||
delete(ts.connections, addr)
|
||||
}
|
||||
ts.Mutex.Unlock()
|
||||
|
||||
ts.stopOnce.Do(func() {
|
||||
if ts.done != nil {
|
||||
close(ts.done)
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) GetTxBytes() uint64 {
|
||||
ts.mutex.RLock()
|
||||
defer ts.mutex.RUnlock()
|
||||
ts.Mutex.RLock()
|
||||
defer ts.Mutex.RUnlock()
|
||||
return ts.TxBytes
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) GetRxBytes() uint64 {
|
||||
ts.mutex.RLock()
|
||||
defer ts.mutex.RUnlock()
|
||||
ts.Mutex.RLock()
|
||||
defer ts.Mutex.RUnlock()
|
||||
return ts.RxBytes
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) handleConnection(conn net.Conn) {
|
||||
addr := conn.RemoteAddr().String()
|
||||
ts.mutex.Lock()
|
||||
ts.Mutex.Lock()
|
||||
ts.connections[addr] = conn
|
||||
ts.mutex.Unlock()
|
||||
ts.Mutex.Unlock()
|
||||
|
||||
defer func() {
|
||||
ts.mutex.Lock()
|
||||
ts.Mutex.Lock()
|
||||
delete(ts.connections, addr)
|
||||
ts.mutex.Unlock()
|
||||
conn.Close() // #nosec G104
|
||||
ts.Mutex.Unlock()
|
||||
_ = conn.Close()
|
||||
}()
|
||||
|
||||
buffer := make([]byte, ts.MTU)
|
||||
for {
|
||||
ts.Mutex.RLock()
|
||||
done := ts.done
|
||||
ts.Mutex.RUnlock()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
n, err := conn.Read(buffer)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ts.mutex.Lock()
|
||||
ts.Mutex.Lock()
|
||||
ts.RxBytes += uint64(n) // #nosec G115
|
||||
ts.mutex.Unlock()
|
||||
callback := ts.packetCallback
|
||||
ts.Mutex.Unlock()
|
||||
|
||||
if ts.packetCallback != nil {
|
||||
ts.packetCallback(buffer[:n], ts)
|
||||
if callback != nil {
|
||||
callback(buffer[:n], ts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ts *TCPServerInterface) ProcessOutgoing(data []byte) error {
|
||||
ts.mutex.RLock()
|
||||
defer ts.mutex.RUnlock()
|
||||
ts.Mutex.RLock()
|
||||
online := ts.Online
|
||||
ts.Mutex.RUnlock()
|
||||
|
||||
if !ts.Online {
|
||||
if !online {
|
||||
return fmt.Errorf("interface offline")
|
||||
}
|
||||
|
||||
@@ -659,12 +757,17 @@ func (ts *TCPServerInterface) ProcessOutgoing(data []byte) error {
|
||||
frame = append(frame, HDLC_FLAG)
|
||||
}
|
||||
|
||||
ts.TxBytes += uint64(len(frame))
|
||||
|
||||
ts.Mutex.Lock()
|
||||
ts.TxBytes += uint64(len(frame)) // #nosec G115
|
||||
conns := make([]net.Conn, 0, len(ts.connections))
|
||||
for _, conn := range ts.connections {
|
||||
conns = append(conns, conn)
|
||||
}
|
||||
ts.Mutex.Unlock()
|
||||
|
||||
for _, conn := range conns {
|
||||
if _, err := conn.Write(frame); err != nil {
|
||||
log.Printf("[DEBUG-4] Error writing to connection %s: %v",
|
||||
conn.RemoteAddr(), err)
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Error writing to connection", "address", conn.RemoteAddr(), "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
//go:build !linux || tinygo
|
||||
// +build !linux tinygo
|
||||
|
||||
|
||||
61
pkg/interfaces/tcp_darwin.go
Normal file
61
pkg/interfaces/tcp_darwin.go
Normal file
@@ -0,0 +1,61 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
//go:build darwin
|
||||
// +build darwin
|
||||
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"syscall"
|
||||
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||
)
|
||||
|
||||
func (tc *TCPClientInterface) setTimeoutsLinux() error {
|
||||
return tc.setTimeoutsOSX()
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) setTimeoutsOSX() error {
|
||||
tcpConn, ok := tc.conn.(*net.TCPConn)
|
||||
if !ok {
|
||||
return fmt.Errorf("not a TCP connection")
|
||||
}
|
||||
|
||||
rawConn, err := tcpConn.SyscallConn()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get raw connection: %v", err)
|
||||
}
|
||||
|
||||
var sockoptErr error
|
||||
err = rawConn.Control(func(fd uintptr) {
|
||||
const TCP_KEEPALIVE = 0x10
|
||||
|
||||
var probeAfter int
|
||||
if tc.i2pTunneled {
|
||||
probeAfter = I2P_PROBE_AFTER_SEC
|
||||
} else {
|
||||
probeAfter = TCP_PROBE_AFTER_SEC
|
||||
}
|
||||
|
||||
if err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_KEEPALIVE, SO_KEEPALIVE_ENABLE); err != nil {
|
||||
sockoptErr = fmt.Errorf("failed to enable SO_KEEPALIVE: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, TCP_KEEPALIVE, probeAfter); err != nil {
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Failed to set TCP_KEEPALIVE", "error", err)
|
||||
}
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("control failed: %v", err)
|
||||
}
|
||||
if sockoptErr != nil {
|
||||
return sockoptErr
|
||||
}
|
||||
|
||||
debug.Log(debug.DEBUG_VERBOSE, "TCP keepalive configured (OSX)", "i2p", tc.i2pTunneled)
|
||||
return nil
|
||||
}
|
||||
41
pkg/interfaces/tcp_freebsd.go
Normal file
41
pkg/interfaces/tcp_freebsd.go
Normal file
@@ -0,0 +1,41 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
//go:build freebsd
|
||||
// +build freebsd
|
||||
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||
)
|
||||
|
||||
func (tc *TCPClientInterface) setTimeoutsLinux() error {
|
||||
tcpConn, ok := tc.conn.(*net.TCPConn)
|
||||
if !ok {
|
||||
return fmt.Errorf("not a TCP connection")
|
||||
}
|
||||
|
||||
if err := tcpConn.SetKeepAlive(true); err != nil {
|
||||
return fmt.Errorf("failed to enable keepalive: %v", err)
|
||||
}
|
||||
|
||||
keepalivePeriod := TCP_PROBE_INTERVAL_SEC * time.Second
|
||||
if tc.i2pTunneled {
|
||||
keepalivePeriod = I2P_PROBE_INTERVAL_SEC * time.Second
|
||||
}
|
||||
|
||||
if err := tcpConn.SetKeepAlivePeriod(keepalivePeriod); err != nil {
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Failed to set keepalive period", "error", err)
|
||||
}
|
||||
|
||||
debug.Log(debug.DEBUG_VERBOSE, "TCP keepalive configured (FreeBSD)", "i2p", tc.i2pTunneled)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) setTimeoutsOSX() error {
|
||||
return tc.setTimeoutsLinux()
|
||||
}
|
||||
@@ -1,32 +1,111 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
//go:build linux && !tinygo
|
||||
// +build linux,!tinygo
|
||||
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||
)
|
||||
|
||||
func (tc *TCPClientInterface) setTimeoutsLinux() error {
|
||||
tcpConn, ok := tc.conn.(*net.TCPConn)
|
||||
if !ok {
|
||||
return fmt.Errorf("not a TCP connection")
|
||||
}
|
||||
|
||||
rawConn, err := tcpConn.SyscallConn()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get raw connection: %v", err)
|
||||
}
|
||||
|
||||
var sockoptErr error
|
||||
err = rawConn.Control(func(fd uintptr) {
|
||||
var userTimeout, probeAfter, probeInterval, probeCount int
|
||||
|
||||
if tc.i2pTunneled {
|
||||
userTimeout = I2P_USER_TIMEOUT_SEC * TCP_MILLISECONDS
|
||||
probeAfter = I2P_PROBE_AFTER_SEC
|
||||
probeInterval = I2P_PROBE_INTERVAL_SEC
|
||||
probeCount = I2P_PROBES_COUNT
|
||||
} else {
|
||||
userTimeout = TCP_USER_TIMEOUT_SEC * TCP_MILLISECONDS
|
||||
probeAfter = TCP_PROBE_AFTER_SEC
|
||||
probeInterval = TCP_PROBE_INTERVAL_SEC
|
||||
probeCount = TCP_PROBES_COUNT
|
||||
}
|
||||
|
||||
const TCP_USER_TIMEOUT = 18
|
||||
const TCP_KEEPIDLE = 4
|
||||
const TCP_KEEPINTVL = 5
|
||||
const TCP_KEEPCNT = 6
|
||||
|
||||
if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, TCP_USER_TIMEOUT, userTimeout); err != nil {
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Failed to set TCP_USER_TIMEOUT", "error", err)
|
||||
}
|
||||
|
||||
if err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_KEEPALIVE, SO_KEEPALIVE_ENABLE); err != nil {
|
||||
sockoptErr = fmt.Errorf("failed to enable SO_KEEPALIVE: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, TCP_KEEPIDLE, probeAfter); err != nil {
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Failed to set TCP_KEEPIDLE", "error", err)
|
||||
}
|
||||
|
||||
if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, TCP_KEEPINTVL, probeInterval); err != nil {
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Failed to set TCP_KEEPINTVL", "error", err)
|
||||
}
|
||||
|
||||
if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, TCP_KEEPCNT, probeCount); err != nil {
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Failed to set TCP_KEEPCNT", "error", err)
|
||||
}
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("control failed: %v", err)
|
||||
}
|
||||
if sockoptErr != nil {
|
||||
return sockoptErr
|
||||
}
|
||||
|
||||
debug.Log(debug.DEBUG_VERBOSE, "TCP keepalive configured (Linux)", "i2p", tc.i2pTunneled)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) setTimeoutsOSX() error {
|
||||
return tc.setTimeoutsLinux()
|
||||
}
|
||||
|
||||
func platformGetRTT(fd uintptr) time.Duration {
|
||||
var info syscall.TCPInfo
|
||||
size := uint32(syscall.SizeofTCPInfo)
|
||||
// bearer:disable go_gosec_unsafe_unsafe
|
||||
infoLen := uint32(unsafe.Sizeof(info))
|
||||
|
||||
_, _, err := syscall.Syscall6(
|
||||
const TCP_INFO = 11
|
||||
// #nosec G103
|
||||
_, _, errno := syscall.Syscall6(
|
||||
syscall.SYS_GETSOCKOPT,
|
||||
fd,
|
||||
syscall.SOL_TCP,
|
||||
syscall.TCP_INFO,
|
||||
uintptr(unsafe.Pointer(&info)), // #nosec G103
|
||||
uintptr(unsafe.Pointer(&size)), // #nosec G103
|
||||
syscall.IPPROTO_TCP,
|
||||
TCP_INFO,
|
||||
// bearer:disable go_gosec_unsafe_unsafe
|
||||
uintptr(unsafe.Pointer(&info)),
|
||||
// bearer:disable go_gosec_unsafe_unsafe
|
||||
uintptr(unsafe.Pointer(&infoLen)),
|
||||
0,
|
||||
)
|
||||
|
||||
if err != 0 {
|
||||
if errno != 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// RTT is in microseconds, convert to Duration
|
||||
return time.Duration(info.Rtt) * time.Microsecond
|
||||
}
|
||||
|
||||
41
pkg/interfaces/tcp_netbsd.go
Normal file
41
pkg/interfaces/tcp_netbsd.go
Normal file
@@ -0,0 +1,41 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
//go:build netbsd
|
||||
// +build netbsd
|
||||
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||
)
|
||||
|
||||
func (tc *TCPClientInterface) setTimeoutsLinux() error {
|
||||
tcpConn, ok := tc.conn.(*net.TCPConn)
|
||||
if !ok {
|
||||
return fmt.Errorf("not a TCP connection")
|
||||
}
|
||||
|
||||
if err := tcpConn.SetKeepAlive(true); err != nil {
|
||||
return fmt.Errorf("failed to enable keepalive: %v", err)
|
||||
}
|
||||
|
||||
keepalivePeriod := TCP_PROBE_INTERVAL_SEC * time.Second
|
||||
if tc.i2pTunneled {
|
||||
keepalivePeriod = I2P_PROBE_INTERVAL_SEC * time.Second
|
||||
}
|
||||
|
||||
if err := tcpConn.SetKeepAlivePeriod(keepalivePeriod); err != nil {
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Failed to set keepalive period", "error", err)
|
||||
}
|
||||
|
||||
debug.Log(debug.DEBUG_VERBOSE, "TCP keepalive configured (NetBSD)", "i2p", tc.i2pTunneled)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) setTimeoutsOSX() error {
|
||||
return tc.setTimeoutsLinux()
|
||||
}
|
||||
41
pkg/interfaces/tcp_openbsd.go
Normal file
41
pkg/interfaces/tcp_openbsd.go
Normal file
@@ -0,0 +1,41 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
//go:build openbsd
|
||||
// +build openbsd
|
||||
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||
)
|
||||
|
||||
func (tc *TCPClientInterface) setTimeoutsLinux() error {
|
||||
tcpConn, ok := tc.conn.(*net.TCPConn)
|
||||
if !ok {
|
||||
return fmt.Errorf("not a TCP connection")
|
||||
}
|
||||
|
||||
if err := tcpConn.SetKeepAlive(true); err != nil {
|
||||
return fmt.Errorf("failed to enable keepalive: %v", err)
|
||||
}
|
||||
|
||||
keepalivePeriod := TCP_PROBE_INTERVAL_SEC * time.Second
|
||||
if tc.i2pTunneled {
|
||||
keepalivePeriod = I2P_PROBE_INTERVAL_SEC * time.Second
|
||||
}
|
||||
|
||||
if err := tcpConn.SetKeepAlivePeriod(keepalivePeriod); err != nil {
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Failed to set keepalive period", "error", err)
|
||||
}
|
||||
|
||||
debug.Log(debug.DEBUG_VERBOSE, "TCP keepalive configured (OpenBSD)", "i2p", tc.i2pTunneled)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) setTimeoutsOSX() error {
|
||||
return tc.setTimeoutsLinux()
|
||||
}
|
||||
14
pkg/interfaces/tcp_wasm.go
Normal file
14
pkg/interfaces/tcp_wasm.go
Normal file
@@ -0,0 +1,14 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
//go:build js && wasm
|
||||
// +build js,wasm
|
||||
|
||||
package interfaces
|
||||
|
||||
func (tc *TCPClientInterface) setTimeoutsLinux() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) setTimeoutsOSX() error {
|
||||
return nil
|
||||
}
|
||||
42
pkg/interfaces/tcp_windows.go
Normal file
42
pkg/interfaces/tcp_windows.go
Normal file
@@ -0,0 +1,42 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||
)
|
||||
|
||||
func (tc *TCPClientInterface) setTimeoutsLinux() error {
|
||||
return tc.setTimeoutsWindows()
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) setTimeoutsOSX() error {
|
||||
return tc.setTimeoutsWindows()
|
||||
}
|
||||
|
||||
func (tc *TCPClientInterface) setTimeoutsWindows() error {
|
||||
tcpConn, ok := tc.conn.(*net.TCPConn)
|
||||
if !ok {
|
||||
return fmt.Errorf("not a TCP connection")
|
||||
}
|
||||
|
||||
if err := tcpConn.SetKeepAlive(true); err != nil {
|
||||
return fmt.Errorf("failed to enable keepalive: %v", err)
|
||||
}
|
||||
|
||||
keepalivePeriod := TCP_PROBE_INTERVAL_SEC * time.Second
|
||||
if tc.i2pTunneled {
|
||||
keepalivePeriod = I2P_PROBE_INTERVAL_SEC * time.Second
|
||||
}
|
||||
|
||||
if err := tcpConn.SetKeepAlivePeriod(keepalivePeriod); err != nil {
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Failed to set keepalive period", "error", err)
|
||||
}
|
||||
|
||||
debug.Log(debug.DEBUG_VERBOSE, "TCP keepalive configured (Windows)", "i2p", tc.i2pTunneled)
|
||||
return nil
|
||||
}
|
||||
@@ -1,21 +1,27 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
//go:build !tinygo
|
||||
// +build !tinygo
|
||||
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||
)
|
||||
|
||||
type UDPInterface struct {
|
||||
BaseInterface
|
||||
conn net.Conn
|
||||
conn *net.UDPConn
|
||||
addr *net.UDPAddr
|
||||
targetAddr *net.UDPAddr
|
||||
mutex sync.RWMutex
|
||||
readBuffer []byte
|
||||
done chan struct{}
|
||||
stopOnce sync.Once
|
||||
}
|
||||
|
||||
func NewUDPInterface(name string, addr string, target string, enabled bool) (*UDPInterface, error) {
|
||||
@@ -36,66 +42,52 @@ func NewUDPInterface(name string, addr string, target string, enabled bool) (*UD
|
||||
BaseInterface: NewBaseInterface(name, common.IF_TYPE_UDP, enabled),
|
||||
addr: udpAddr,
|
||||
targetAddr: targetAddr,
|
||||
readBuffer: make([]byte, common.DEFAULT_MTU),
|
||||
readBuffer: make([]byte, common.NUM_1064),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
|
||||
ui.MTU = common.NUM_1064
|
||||
|
||||
return ui, nil
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) GetName() string {
|
||||
return ui.Name
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) GetType() common.InterfaceType {
|
||||
return ui.Type
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) GetMode() common.InterfaceMode {
|
||||
return ui.Mode
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) IsOnline() bool {
|
||||
ui.mutex.RLock()
|
||||
defer ui.mutex.RUnlock()
|
||||
return ui.Online
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) IsDetached() bool {
|
||||
ui.mutex.RLock()
|
||||
defer ui.mutex.RUnlock()
|
||||
return ui.Detached
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) Detach() {
|
||||
ui.mutex.Lock()
|
||||
defer ui.mutex.Unlock()
|
||||
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 {
|
||||
// TinyGo doesn't support UDP sending
|
||||
return fmt.Errorf("UDPInterface Send not supported in TinyGo - requires UDP client functionality")
|
||||
}
|
||||
debug.Log(debug.DEBUG_ALL, "UDP interface sending bytes", "name", ui.Name, "bytes", len(data))
|
||||
|
||||
func (ui *UDPInterface) SetPacketCallback(callback common.PacketCallback) {
|
||||
ui.mutex.Lock()
|
||||
defer ui.mutex.Unlock()
|
||||
ui.packetCallback = callback
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) GetPacketCallback() common.PacketCallback {
|
||||
ui.mutex.RLock()
|
||||
defer ui.mutex.RUnlock()
|
||||
return ui.packetCallback
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) ProcessIncoming(data []byte) {
|
||||
if callback := ui.GetPacketCallback(); callback != nil {
|
||||
callback(data, ui)
|
||||
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 {
|
||||
@@ -108,9 +100,9 @@ func (ui *UDPInterface) ProcessOutgoing(data []byte) error {
|
||||
return fmt.Errorf("UDP write failed: %v", err)
|
||||
}
|
||||
|
||||
ui.mutex.Lock()
|
||||
ui.Mutex.Lock()
|
||||
ui.TxBytes += uint64(len(data))
|
||||
ui.mutex.Unlock()
|
||||
ui.Mutex.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -119,82 +111,102 @@ func (ui *UDPInterface) GetConn() net.Conn {
|
||||
return ui.conn
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) GetTxBytes() uint64 {
|
||||
ui.mutex.RLock()
|
||||
defer ui.mutex.RUnlock()
|
||||
return ui.TxBytes
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) GetRxBytes() uint64 {
|
||||
ui.mutex.RLock()
|
||||
defer ui.mutex.RUnlock()
|
||||
return ui.RxBytes
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) GetMTU() int {
|
||||
return ui.MTU
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) GetBitrate() int {
|
||||
return int(ui.Bitrate)
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) Enable() {
|
||||
ui.mutex.Lock()
|
||||
defer ui.mutex.Unlock()
|
||||
ui.Online = true
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) Disable() {
|
||||
ui.mutex.Lock()
|
||||
defer ui.mutex.Unlock()
|
||||
ui.Online = false
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) Start() error {
|
||||
// TinyGo doesn't support UDP servers, only clients
|
||||
return fmt.Errorf("UDPInterface not supported in TinyGo - UDP server functionality requires net.ListenUDP")
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) readLoop() {
|
||||
// This method is not used in TinyGo since UDP servers are not supported
|
||||
buffer := make([]byte, common.DEFAULT_MTU)
|
||||
for ui.IsOnline() && !ui.IsDetached() {
|
||||
n, err := ui.conn.Read(buffer)
|
||||
if err != nil {
|
||||
if ui.IsOnline() {
|
||||
log.Printf("Error reading from UDP interface %s: %v", ui.Name, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if ui.packetCallback != nil {
|
||||
ui.packetCallback(buffer[:n], ui)
|
||||
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, ui.MTU)
|
||||
buffer := make([]byte, common.NUM_1064)
|
||||
for {
|
||||
n, _, err := ui.conn.ReadFromUDP(buffer)
|
||||
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 {
|
||||
if ui.Online {
|
||||
log.Printf("Error reading from UDP interface %s: %v", ui.Name, err)
|
||||
ui.Stop() // Consider if stopping is the right action or just log and continue
|
||||
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
|
||||
}
|
||||
if ui.packetCallback != nil {
|
||||
ui.packetCallback(buffer[:n], ui)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
func (ui *UDPInterface) IsEnabled() bool {
|
||||
ui.mutex.RLock()
|
||||
defer ui.mutex.RUnlock()
|
||||
return ui.Enabled && ui.Online && !ui.Detached
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package interfaces
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||
)
|
||||
|
||||
func TestNewUDPInterface(t *testing.T) {
|
||||
@@ -25,11 +25,6 @@ func TestNewUDPInterface(t *testing.T) {
|
||||
if ui.GetType() != common.IF_TYPE_UDP {
|
||||
t.Errorf("GetType() = %v; want %v", ui.GetType(), common.IF_TYPE_UDP)
|
||||
}
|
||||
if ui.addr.String() != validAddr && ui.addr.Port == 0 { // Check if address resolved, port 0 is special
|
||||
// Allow OS-assigned port if 0 was specified
|
||||
} else if ui.addr.String() != validAddr {
|
||||
// t.Errorf("Resolved addr = %s; want %s", ui.addr.String(), validAddr) //This check is flaky with port 0
|
||||
}
|
||||
if ui.targetAddr.String() != validTarget {
|
||||
t.Errorf("Resolved targetAddr = %s; want %s", ui.targetAddr.String(), validTarget)
|
||||
}
|
||||
@@ -71,7 +66,6 @@ func TestNewUDPInterface(t *testing.T) {
|
||||
|
||||
func TestUDPInterfaceState(t *testing.T) {
|
||||
// Basic state tests are covered by BaseInterface tests
|
||||
// Add specific UDP ones if needed, e.g., involving the conn
|
||||
addr := "127.0.0.1:0"
|
||||
ui, _ := NewUDPInterface("udpState", addr, "", true)
|
||||
|
||||
|
||||
69
pkg/interfaces/udp_tinygo.go
Normal file
69
pkg/interfaces/udp_tinygo.go
Normal file
@@ -0,0 +1,69 @@
|
||||
// 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"
|
||||
)
|
||||
|
||||
type UDPInterface struct {
|
||||
BaseInterface
|
||||
conn net.Conn
|
||||
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) Start() error {
|
||||
// TinyGo doesn't support UDP servers, only clients
|
||||
return fmt.Errorf("UDPInterface not supported in TinyGo - UDP server functionality requires net.ListenUDP")
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) Send(data []byte, addr string) error {
|
||||
// TinyGo doesn't support UDP sending
|
||||
return fmt.Errorf("UDPInterface Send not supported in TinyGo - requires UDP client functionality")
|
||||
}
|
||||
|
||||
func (ui *UDPInterface) Stop() error {
|
||||
ui.Mutex.Lock()
|
||||
defer ui.Mutex.Unlock()
|
||||
ui.Online = false
|
||||
return nil
|
||||
}
|
||||
|
||||
714
pkg/interfaces/websocket_native.go
Normal file
714
pkg/interfaces/websocket_native.go
Normal file
@@ -0,0 +1,714 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
//go:build !js
|
||||
// +build !js
|
||||
|
||||
// WebSocketInterface is a native implementation of the WebSocket interface.
|
||||
// It is used to connect to the WebSocket server and send/receive data.
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/rand"
|
||||
// bearer:disable go_gosec_blocklist_sha1
|
||||
"crypto/sha1" // #nosec G505
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||
)
|
||||
|
||||
const (
|
||||
wsGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
||||
|
||||
WS_BUFFER_SIZE = 4096
|
||||
WS_MTU = 1064
|
||||
WS_BITRATE = 10000000
|
||||
WS_HTTPS_PORT = 443
|
||||
WS_HTTP_PORT = 80
|
||||
WS_VERSION = "13"
|
||||
WS_CONNECT_TIMEOUT = 10 * time.Second
|
||||
WS_RECONNECT_DELAY = 2 * time.Second
|
||||
WS_KEY_SIZE = 16
|
||||
WS_MASK_KEY_SIZE = 4
|
||||
WS_HEADER_SIZE = 2
|
||||
WS_PAYLOAD_LEN_16BIT = 126
|
||||
WS_PAYLOAD_LEN_64BIT = 127
|
||||
WS_MAX_PAYLOAD_16BIT = 65536
|
||||
WS_FRAME_HEADER_FIN = 0x80
|
||||
WS_FRAME_HEADER_OPCODE = 0x0F
|
||||
WS_FRAME_HEADER_MASKED = 0x80
|
||||
WS_FRAME_HEADER_LEN = 0x7F
|
||||
WS_OPCODE_CONTINUATION = 0x00
|
||||
WS_OPCODE_TEXT = 0x01
|
||||
WS_OPCODE_BINARY = 0x02
|
||||
WS_OPCODE_CLOSE = 0x08
|
||||
WS_OPCODE_PING = 0x09
|
||||
WS_OPCODE_PONG = 0x0A
|
||||
)
|
||||
|
||||
type WebSocketInterface struct {
|
||||
BaseInterface
|
||||
wsURL string
|
||||
conn net.Conn
|
||||
reader *bufio.Reader
|
||||
connected bool
|
||||
messageQueue [][]byte
|
||||
readBuffer []byte
|
||||
writeBuffer []byte
|
||||
done chan struct{}
|
||||
stopOnce sync.Once
|
||||
}
|
||||
|
||||
func NewWebSocketInterface(name string, wsURL string, enabled bool) (*WebSocketInterface, error) {
|
||||
debug.Log(debug.DEBUG_VERBOSE, "NewWebSocketInterface called", "name", name, "url", wsURL, "enabled", enabled)
|
||||
ws := &WebSocketInterface{
|
||||
BaseInterface: NewBaseInterface(name, common.IF_TYPE_UDP, enabled),
|
||||
wsURL: wsURL,
|
||||
messageQueue: make([][]byte, 0),
|
||||
readBuffer: make([]byte, WS_BUFFER_SIZE),
|
||||
writeBuffer: make([]byte, WS_BUFFER_SIZE),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
|
||||
ws.MTU = WS_MTU
|
||||
ws.Bitrate = WS_BITRATE
|
||||
|
||||
debug.Log(debug.DEBUG_VERBOSE, "WebSocket interface initialized", "name", name, "mtu", ws.MTU, "bitrate", ws.Bitrate)
|
||||
return ws, nil
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) GetName() string {
|
||||
return wsi.Name
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) GetType() common.InterfaceType {
|
||||
return wsi.Type
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) GetMode() common.InterfaceMode {
|
||||
return wsi.Mode
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) IsOnline() bool {
|
||||
wsi.Mutex.RLock()
|
||||
defer wsi.Mutex.RUnlock()
|
||||
return wsi.Online && wsi.connected
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) IsDetached() bool {
|
||||
wsi.Mutex.RLock()
|
||||
defer wsi.Mutex.RUnlock()
|
||||
return wsi.Detached
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) Detach() {
|
||||
wsi.Mutex.Lock()
|
||||
defer wsi.Mutex.Unlock()
|
||||
wsi.Detached = true
|
||||
wsi.Online = false
|
||||
wsi.closeWebSocketLocked()
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) Enable() {
|
||||
wsi.Mutex.Lock()
|
||||
defer wsi.Mutex.Unlock()
|
||||
wsi.Enabled = true
|
||||
wsi.Online = true
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) Disable() {
|
||||
wsi.Mutex.Lock()
|
||||
defer wsi.Mutex.Unlock()
|
||||
wsi.Enabled = false
|
||||
wsi.closeWebSocketLocked()
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) Start() error {
|
||||
wsi.Mutex.Lock()
|
||||
if !wsi.Enabled || wsi.Detached {
|
||||
wsi.Mutex.Unlock()
|
||||
debug.Log(debug.DEBUG_INFO, "WebSocket interface not enabled or detached", "name", wsi.Name)
|
||||
return fmt.Errorf("interface not enabled or detached")
|
||||
}
|
||||
if wsi.conn != nil {
|
||||
wsi.Mutex.Unlock()
|
||||
debug.Log(debug.DEBUG_INFO, "WebSocket already started", "name", wsi.Name)
|
||||
return fmt.Errorf("WebSocket already started")
|
||||
}
|
||||
// Only recreate done if it's nil or was closed
|
||||
select {
|
||||
case <-wsi.done:
|
||||
wsi.done = make(chan struct{})
|
||||
wsi.stopOnce = sync.Once{}
|
||||
default:
|
||||
if wsi.done == nil {
|
||||
wsi.done = make(chan struct{})
|
||||
wsi.stopOnce = sync.Once{}
|
||||
}
|
||||
}
|
||||
wsi.Mutex.Unlock()
|
||||
|
||||
debug.Log(debug.DEBUG_INFO, "Starting WebSocket connection", "name", wsi.Name, "url", wsi.wsURL)
|
||||
|
||||
u, err := url.Parse(wsi.wsURL)
|
||||
if err != nil {
|
||||
debug.Log(debug.DEBUG_ERROR, "Invalid WebSocket URL", "name", wsi.Name, "url", wsi.wsURL, "error", err)
|
||||
return fmt.Errorf("invalid WebSocket URL: %v", err)
|
||||
}
|
||||
|
||||
var conn net.Conn
|
||||
var host string
|
||||
|
||||
if u.Scheme == "wss" {
|
||||
host = u.Host
|
||||
if !strings.Contains(host, ":") {
|
||||
host += fmt.Sprintf(":%d", WS_HTTPS_PORT)
|
||||
}
|
||||
tcpConn, err := net.DialTimeout("tcp", host, WS_CONNECT_TIMEOUT)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect: %v", err)
|
||||
}
|
||||
tlsConn := tls.Client(tcpConn, &tls.Config{
|
||||
ServerName: u.Hostname(),
|
||||
InsecureSkipVerify: false,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
})
|
||||
if err := tlsConn.Handshake(); err != nil {
|
||||
_ = tcpConn.Close()
|
||||
debug.Log(debug.DEBUG_ERROR, "TLS handshake failed", "name", wsi.Name, "host", host, "error", err)
|
||||
return fmt.Errorf("TLS handshake failed: %v", err)
|
||||
}
|
||||
conn = tlsConn
|
||||
} else if u.Scheme == "ws" {
|
||||
host = u.Host
|
||||
if !strings.Contains(host, ":") {
|
||||
host += fmt.Sprintf(":%d", WS_HTTP_PORT)
|
||||
}
|
||||
debug.Log(debug.DEBUG_VERBOSE, "Connecting to WebSocket server", "name", wsi.Name, "host", host)
|
||||
tcpConn, err := net.DialTimeout("tcp", host, WS_CONNECT_TIMEOUT)
|
||||
if err != nil {
|
||||
debug.Log(debug.DEBUG_ERROR, "Failed to connect to WebSocket server", "name", wsi.Name, "host", host, "error", err)
|
||||
return fmt.Errorf("failed to connect: %v", err)
|
||||
}
|
||||
conn = tcpConn
|
||||
} else {
|
||||
debug.Log(debug.DEBUG_ERROR, "Unsupported WebSocket scheme", "name", wsi.Name, "scheme", u.Scheme)
|
||||
return fmt.Errorf("unsupported scheme: %s (use ws:// or wss://)", u.Scheme)
|
||||
}
|
||||
|
||||
key, err := generateWebSocketKey()
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return fmt.Errorf("failed to generate key: %v", err)
|
||||
}
|
||||
|
||||
path := u.Path
|
||||
if path == "" {
|
||||
path = "/"
|
||||
}
|
||||
if u.RawQuery != "" {
|
||||
path += "?" + u.RawQuery
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", path, nil)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return fmt.Errorf("failed to create request: %v", err)
|
||||
}
|
||||
|
||||
req.Host = u.Host
|
||||
req.Header.Set("Upgrade", "websocket")
|
||||
req.Header.Set("Connection", "Upgrade")
|
||||
req.Header.Set("Sec-WebSocket-Key", key)
|
||||
req.Header.Set("Sec-WebSocket-Version", WS_VERSION)
|
||||
req.Header.Set("User-Agent", "Reticulum-Go/1.0")
|
||||
|
||||
if err := req.Write(conn); err != nil {
|
||||
_ = conn.Close()
|
||||
return fmt.Errorf("failed to send handshake: %v", err)
|
||||
}
|
||||
|
||||
resp, err := http.ReadResponse(bufio.NewReader(conn), req)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return fmt.Errorf("failed to read handshake response: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusSwitchingProtocols {
|
||||
_ = conn.Close()
|
||||
debug.Log(debug.DEBUG_ERROR, "WebSocket handshake failed", "name", wsi.Name, "status", resp.StatusCode)
|
||||
return fmt.Errorf("handshake failed: status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
if strings.ToLower(resp.Header.Get("Upgrade")) != "websocket" {
|
||||
_ = conn.Close()
|
||||
return fmt.Errorf("invalid upgrade header")
|
||||
}
|
||||
|
||||
accept := resp.Header.Get("Sec-WebSocket-Accept")
|
||||
expectedAccept := computeAcceptKey(key)
|
||||
if accept != expectedAccept {
|
||||
_ = conn.Close()
|
||||
return fmt.Errorf("invalid accept key")
|
||||
}
|
||||
|
||||
wsi.Mutex.Lock()
|
||||
wsi.conn = conn
|
||||
wsi.reader = bufio.NewReader(conn)
|
||||
wsi.connected = true
|
||||
wsi.Online = true
|
||||
|
||||
debug.Log(debug.DEBUG_INFO, "WebSocket connected", "name", wsi.Name, "url", wsi.wsURL)
|
||||
|
||||
queue := make([][]byte, len(wsi.messageQueue))
|
||||
copy(queue, wsi.messageQueue)
|
||||
wsi.messageQueue = wsi.messageQueue[:0]
|
||||
wsi.Mutex.Unlock() // Unlock after copying queue, before I/O
|
||||
|
||||
for _, msg := range queue {
|
||||
_ = wsi.sendWebSocketMessage(msg)
|
||||
}
|
||||
|
||||
go wsi.readLoop()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) Stop() error {
|
||||
wsi.Mutex.Lock()
|
||||
defer wsi.Mutex.Unlock()
|
||||
|
||||
wsi.Enabled = false
|
||||
wsi.Online = false
|
||||
|
||||
wsi.stopOnce.Do(func() {
|
||||
if wsi.done != nil {
|
||||
close(wsi.done)
|
||||
}
|
||||
})
|
||||
|
||||
wsi.closeWebSocketLocked()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) closeWebSocket() {
|
||||
wsi.Mutex.Lock()
|
||||
defer wsi.Mutex.Unlock()
|
||||
wsi.closeWebSocketLocked()
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) closeWebSocketLocked() {
|
||||
if wsi.conn != nil {
|
||||
wsi.sendCloseFrameLocked()
|
||||
_ = wsi.conn.Close()
|
||||
wsi.conn = nil
|
||||
wsi.reader = nil
|
||||
}
|
||||
wsi.connected = false
|
||||
wsi.Online = false
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) readLoop() {
|
||||
for {
|
||||
wsi.Mutex.RLock()
|
||||
conn := wsi.conn
|
||||
reader := wsi.reader
|
||||
done := wsi.done
|
||||
wsi.Mutex.RUnlock()
|
||||
|
||||
if conn == nil || reader == nil {
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
data, err := wsi.readFrame()
|
||||
if err != nil {
|
||||
wsi.Mutex.Lock()
|
||||
wsi.connected = false
|
||||
wsi.Online = false
|
||||
if wsi.conn != nil {
|
||||
_ = wsi.conn.Close()
|
||||
wsi.conn = nil
|
||||
wsi.reader = nil
|
||||
}
|
||||
wsi.Mutex.Unlock()
|
||||
|
||||
debug.Log(debug.DEBUG_INFO, "WebSocket closed", "name", wsi.Name, "error", err)
|
||||
|
||||
time.Sleep(WS_RECONNECT_DELAY)
|
||||
|
||||
wsi.Mutex.RLock()
|
||||
stillEnabled := wsi.Enabled && !wsi.Detached
|
||||
wsi.Mutex.RUnlock()
|
||||
|
||||
if stillEnabled {
|
||||
go wsi.Start()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if len(data) > 0 {
|
||||
wsi.Mutex.Lock()
|
||||
wsi.RxBytes += uint64(len(data))
|
||||
wsi.Mutex.Unlock()
|
||||
|
||||
wsi.ProcessIncoming(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) readFrame() ([]byte, error) {
|
||||
wsi.Mutex.RLock()
|
||||
reader := wsi.reader
|
||||
wsi.Mutex.RUnlock()
|
||||
|
||||
if reader == nil {
|
||||
return nil, io.EOF
|
||||
}
|
||||
|
||||
header := make([]byte, WS_HEADER_SIZE)
|
||||
if _, err := io.ReadFull(reader, header); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fin := (header[0] & WS_FRAME_HEADER_FIN) != 0
|
||||
opcode := header[0] & WS_FRAME_HEADER_OPCODE
|
||||
masked := (header[1] & WS_FRAME_HEADER_MASKED) != 0
|
||||
payloadLen := int(header[1] & WS_FRAME_HEADER_LEN)
|
||||
|
||||
if opcode == WS_OPCODE_CLOSE {
|
||||
return nil, io.EOF
|
||||
}
|
||||
|
||||
if opcode == WS_OPCODE_PING {
|
||||
return wsi.handlePingFrame(reader, payloadLen, masked)
|
||||
}
|
||||
|
||||
if opcode == WS_OPCODE_PONG {
|
||||
return wsi.handlePongFrame(reader, payloadLen, masked)
|
||||
}
|
||||
|
||||
if opcode != WS_OPCODE_BINARY {
|
||||
return nil, fmt.Errorf("unsupported opcode: %d", opcode)
|
||||
}
|
||||
|
||||
if payloadLen == WS_PAYLOAD_LEN_16BIT {
|
||||
lenBytes := make([]byte, 2)
|
||||
if _, err := io.ReadFull(reader, lenBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payloadLen = int(binary.BigEndian.Uint16(lenBytes))
|
||||
} else if payloadLen == WS_PAYLOAD_LEN_64BIT {
|
||||
lenBytes := make([]byte, 8)
|
||||
if _, err := io.ReadFull(reader, lenBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
val := binary.BigEndian.Uint64(lenBytes)
|
||||
if val > uint64(math.MaxInt) {
|
||||
return nil, fmt.Errorf("payload length exceeds maximum integer value")
|
||||
}
|
||||
payloadLen = int(val) // #nosec G115
|
||||
}
|
||||
|
||||
maskKey := make([]byte, WS_MASK_KEY_SIZE)
|
||||
if masked {
|
||||
if _, err := io.ReadFull(reader, maskKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
payload := make([]byte, payloadLen)
|
||||
if _, err := io.ReadFull(reader, payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if masked {
|
||||
for i := 0; i < payloadLen; i++ {
|
||||
payload[i] ^= maskKey[i%WS_MASK_KEY_SIZE]
|
||||
}
|
||||
}
|
||||
|
||||
if !fin {
|
||||
nextFrame, err := wsi.readFrame()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return append(payload, nextFrame...), nil
|
||||
}
|
||||
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) Send(data []byte, addr string) error {
|
||||
wsi.Mutex.RLock()
|
||||
enabled := wsi.Enabled
|
||||
detached := wsi.Detached
|
||||
connected := wsi.connected
|
||||
wsi.Mutex.RUnlock()
|
||||
|
||||
if !enabled || detached {
|
||||
debug.Log(debug.DEBUG_VERBOSE, "WebSocket interface not enabled or detached, dropping packet", "name", wsi.Name, "bytes", len(data))
|
||||
return fmt.Errorf("interface not enabled")
|
||||
}
|
||||
|
||||
wsi.Mutex.Lock()
|
||||
wsi.TxBytes += uint64(len(data))
|
||||
wsi.Mutex.Unlock()
|
||||
|
||||
if !connected {
|
||||
debug.Log(debug.DEBUG_VERBOSE, "WebSocket not connected, queuing packet", "name", wsi.Name, "bytes", len(data), "queue_size", len(wsi.messageQueue))
|
||||
wsi.Mutex.Lock()
|
||||
wsi.messageQueue = append(wsi.messageQueue, data)
|
||||
wsi.Mutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
packetType := "unknown"
|
||||
if len(data) > 0 {
|
||||
switch data[0] {
|
||||
case 0x01:
|
||||
packetType = "announce"
|
||||
case 0x02:
|
||||
packetType = "link"
|
||||
default:
|
||||
packetType = fmt.Sprintf("0x%02x", data[0])
|
||||
}
|
||||
}
|
||||
debug.Log(debug.DEBUG_INFO, "Sending packet over WebSocket", "name", wsi.Name, "bytes", len(data), "packet_type", packetType)
|
||||
return wsi.sendWebSocketMessage(data)
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) sendWebSocketMessage(data []byte) error {
|
||||
wsi.Mutex.RLock()
|
||||
conn := wsi.conn
|
||||
wsi.Mutex.RUnlock()
|
||||
|
||||
if conn == nil {
|
||||
return fmt.Errorf("WebSocket not initialized")
|
||||
}
|
||||
|
||||
frame := wsi.createFrame(data, WS_OPCODE_BINARY, true)
|
||||
wsi.Mutex.Lock()
|
||||
_, err := conn.Write(frame)
|
||||
wsi.Mutex.Unlock()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send: %v", err)
|
||||
}
|
||||
|
||||
debug.Log(debug.DEBUG_INFO, "WebSocket sent packet successfully", "name", wsi.Name, "bytes", len(data), "frame_bytes", len(frame))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) sendCloseFrame() {
|
||||
wsi.Mutex.RLock()
|
||||
defer wsi.Mutex.RUnlock()
|
||||
wsi.sendCloseFrameLocked()
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) sendCloseFrameLocked() {
|
||||
conn := wsi.conn
|
||||
if conn == nil {
|
||||
return
|
||||
}
|
||||
|
||||
frame := wsi.createFrame(nil, WS_OPCODE_CLOSE, true)
|
||||
_, _ = conn.Write(frame)
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) handlePingFrame(reader *bufio.Reader, payloadLen int, masked bool) ([]byte, error) {
|
||||
if payloadLen == WS_PAYLOAD_LEN_16BIT {
|
||||
lenBytes := make([]byte, 2)
|
||||
if _, err := io.ReadFull(reader, lenBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payloadLen = int(binary.BigEndian.Uint16(lenBytes))
|
||||
} else if payloadLen == WS_PAYLOAD_LEN_64BIT {
|
||||
lenBytes := make([]byte, 8)
|
||||
if _, err := io.ReadFull(reader, lenBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
val := binary.BigEndian.Uint64(lenBytes)
|
||||
if val > uint64(math.MaxInt) {
|
||||
return nil, fmt.Errorf("payload length exceeds maximum integer value")
|
||||
}
|
||||
payloadLen = int(val) // #nosec G115
|
||||
}
|
||||
|
||||
maskKey := make([]byte, WS_MASK_KEY_SIZE)
|
||||
if masked {
|
||||
if _, err := io.ReadFull(reader, maskKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
payload := make([]byte, payloadLen)
|
||||
if payloadLen > 0 {
|
||||
if _, err := io.ReadFull(reader, payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if masked {
|
||||
for i := 0; i < payloadLen; i++ {
|
||||
payload[i] ^= maskKey[i%WS_MASK_KEY_SIZE]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wsi.sendPongFrame(payload)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) handlePongFrame(reader *bufio.Reader, payloadLen int, masked bool) ([]byte, error) {
|
||||
if payloadLen == WS_PAYLOAD_LEN_16BIT {
|
||||
lenBytes := make([]byte, 2)
|
||||
if _, err := io.ReadFull(reader, lenBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payloadLen = int(binary.BigEndian.Uint16(lenBytes))
|
||||
} else if payloadLen == WS_PAYLOAD_LEN_64BIT {
|
||||
lenBytes := make([]byte, 8)
|
||||
if _, err := io.ReadFull(reader, lenBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
val := binary.BigEndian.Uint64(lenBytes)
|
||||
if val > uint64(math.MaxInt) {
|
||||
return nil, fmt.Errorf("payload length exceeds maximum integer value")
|
||||
}
|
||||
payloadLen = int(val) // #nosec G115
|
||||
}
|
||||
|
||||
maskKey := make([]byte, WS_MASK_KEY_SIZE)
|
||||
if masked {
|
||||
if _, err := io.ReadFull(reader, maskKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if payloadLen > 0 {
|
||||
payload := make([]byte, payloadLen)
|
||||
if _, err := io.ReadFull(reader, payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) sendPongFrame(data []byte) {
|
||||
wsi.Mutex.RLock()
|
||||
conn := wsi.conn
|
||||
wsi.Mutex.RUnlock()
|
||||
|
||||
if conn == nil {
|
||||
return
|
||||
}
|
||||
|
||||
frame := wsi.createFrame(data, WS_OPCODE_PONG, true)
|
||||
wsi.Mutex.Lock()
|
||||
_, _ = conn.Write(frame)
|
||||
wsi.Mutex.Unlock()
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) createFrame(data []byte, opcode byte, fin bool) []byte {
|
||||
payloadLen := len(data)
|
||||
frame := make([]byte, WS_HEADER_SIZE)
|
||||
|
||||
if fin {
|
||||
frame[0] |= WS_FRAME_HEADER_FIN
|
||||
}
|
||||
frame[0] |= opcode
|
||||
|
||||
if payloadLen < WS_PAYLOAD_LEN_16BIT {
|
||||
frame[1] = byte(payloadLen)
|
||||
frame = append(frame, data...)
|
||||
} else if payloadLen < WS_MAX_PAYLOAD_16BIT {
|
||||
frame[1] = WS_PAYLOAD_LEN_16BIT // #nosec G602
|
||||
lenBytes := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(lenBytes, uint16(payloadLen)) // #nosec G115
|
||||
frame = append(frame, lenBytes...)
|
||||
frame = append(frame, data...)
|
||||
} else {
|
||||
frame[1] = WS_PAYLOAD_LEN_64BIT // #nosec G602
|
||||
lenBytes := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(lenBytes, uint64(payloadLen)) // #nosec G115
|
||||
frame = append(frame, lenBytes...)
|
||||
frame = append(frame, data...)
|
||||
}
|
||||
|
||||
return frame
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) ProcessOutgoing(data []byte) error {
|
||||
return wsi.Send(data, "")
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) GetConn() net.Conn {
|
||||
wsi.Mutex.RLock()
|
||||
defer wsi.Mutex.RUnlock()
|
||||
return wsi.conn
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) GetMTU() int {
|
||||
return wsi.MTU
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) IsEnabled() bool {
|
||||
wsi.Mutex.RLock()
|
||||
defer wsi.Mutex.RUnlock()
|
||||
return wsi.Enabled && wsi.Online && !wsi.Detached
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) SendPathRequest(packet []byte) error {
|
||||
return wsi.Send(packet, "")
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) SendLinkPacket(dest []byte, data []byte, timestamp time.Time) error {
|
||||
frame := make([]byte, 0, len(dest)+len(data)+9)
|
||||
frame = append(frame, WS_OPCODE_BINARY)
|
||||
frame = append(frame, dest...)
|
||||
ts := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(ts, uint64(timestamp.Unix())) // #nosec G115
|
||||
frame = append(frame, ts...)
|
||||
frame = append(frame, data...)
|
||||
return wsi.Send(frame, "")
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) GetBandwidthAvailable() bool {
|
||||
return wsi.BaseInterface.GetBandwidthAvailable()
|
||||
}
|
||||
|
||||
func generateWebSocketKey() (string, error) {
|
||||
key := make([]byte, WS_KEY_SIZE)
|
||||
if _, err := rand.Read(key); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(key), nil
|
||||
}
|
||||
|
||||
func computeAcceptKey(key string) string {
|
||||
// bearer:disable go_gosec_crypto_weak_crypto
|
||||
h := sha1.New() // #nosec G401
|
||||
h.Write([]byte(key))
|
||||
h.Write([]byte(wsGUID))
|
||||
// bearer:disable go_lang_weak_hash_sha1
|
||||
return base64.StdEncoding.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
280
pkg/interfaces/websocket_native_test.go
Normal file
280
pkg/interfaces/websocket_native_test.go
Normal file
@@ -0,0 +1,280 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||
)
|
||||
|
||||
func TestWebSocketGUID(t *testing.T) {
|
||||
if wsGUID != "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" {
|
||||
t.Errorf("wsGUID mismatch: expected RFC 6455 GUID, got %s", wsGUID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateWebSocketKey(t *testing.T) {
|
||||
key1, err := generateWebSocketKey()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate key: %v", err)
|
||||
}
|
||||
|
||||
key2, err := generateWebSocketKey()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate key: %v", err)
|
||||
}
|
||||
|
||||
if key1 == key2 {
|
||||
t.Error("Generated keys should be unique")
|
||||
}
|
||||
|
||||
if len(key1) != 24 {
|
||||
t.Errorf("Expected base64-encoded key length 24, got %d", len(key1))
|
||||
}
|
||||
}
|
||||
|
||||
func TestComputeAcceptKey(t *testing.T) {
|
||||
testKey := "dGhlIHNhbXBsZSBub25jZQ=="
|
||||
expectedAccept := "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="
|
||||
|
||||
accept := computeAcceptKey(testKey)
|
||||
if accept != expectedAccept {
|
||||
t.Errorf("Accept key mismatch: expected %s, got %s", expectedAccept, accept)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewWebSocketInterface(t *testing.T) {
|
||||
ws, err := NewWebSocketInterface("test", "wss://socket.quad4.io/ws", true)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create WebSocket interface: %v", err)
|
||||
}
|
||||
|
||||
if ws.GetName() != "test" {
|
||||
t.Errorf("Expected name 'test', got %s", ws.GetName())
|
||||
}
|
||||
|
||||
if ws.GetType() != common.IF_TYPE_UDP {
|
||||
t.Errorf("Expected type IF_TYPE_UDP, got %v", ws.GetType())
|
||||
}
|
||||
|
||||
if ws.GetMTU() != 1064 {
|
||||
t.Errorf("Expected MTU 1064, got %d", ws.GetMTU())
|
||||
}
|
||||
|
||||
if ws.IsOnline() {
|
||||
t.Error("Interface should not be online before Start()")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebSocketConnection(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping network test in short mode")
|
||||
}
|
||||
|
||||
ws, err := NewWebSocketInterface("test", "wss://socket.quad4.io/ws", true)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create WebSocket interface: %v", err)
|
||||
}
|
||||
|
||||
ws.SetPacketCallback(func(data []byte, ni common.NetworkInterface) {
|
||||
t.Logf("Received packet: %d bytes", len(data))
|
||||
})
|
||||
|
||||
err = ws.Start()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start WebSocket: %v", err)
|
||||
}
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
if !ws.IsOnline() {
|
||||
t.Error("WebSocket should be online after Start()")
|
||||
}
|
||||
|
||||
testData := []byte{0x01, 0x02, 0x03, 0x04}
|
||||
err = ws.Send(testData, "")
|
||||
if err != nil {
|
||||
t.Errorf("Failed to send data: %v", err)
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
if err := ws.Stop(); err != nil {
|
||||
t.Errorf("Failed to stop WebSocket: %v", err)
|
||||
}
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
if ws.IsOnline() {
|
||||
t.Error("WebSocket should be offline after Stop()")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebSocketReconnection(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping network test in short mode")
|
||||
}
|
||||
|
||||
ws, err := NewWebSocketInterface("test", "wss://socket.quad4.io/ws", true)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create WebSocket interface: %v", err)
|
||||
}
|
||||
|
||||
err = ws.Start()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start WebSocket: %v", err)
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
if !ws.IsOnline() {
|
||||
t.Error("WebSocket should be online")
|
||||
}
|
||||
|
||||
conn := ws.GetConn()
|
||||
if conn == nil {
|
||||
t.Error("GetConn() should return a connection")
|
||||
}
|
||||
|
||||
conn.Close()
|
||||
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
if ws.IsOnline() {
|
||||
t.Log("WebSocket reconnected successfully")
|
||||
}
|
||||
|
||||
if err := ws.Stop(); err != nil {
|
||||
t.Errorf("Failed to stop WebSocket: %v", err)
|
||||
}
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
func TestWebSocketMessageQueue(t *testing.T) {
|
||||
ws, err := NewWebSocketInterface("test", "wss://socket.quad4.io/ws", true)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create WebSocket interface: %v", err)
|
||||
}
|
||||
|
||||
ws.Enable()
|
||||
|
||||
testData := []byte{0x01, 0x02, 0x03}
|
||||
err = ws.Send(testData, "")
|
||||
if err != nil {
|
||||
t.Errorf("Send should queue message when offline, got error: %v", err)
|
||||
}
|
||||
|
||||
if testing.Short() {
|
||||
return
|
||||
}
|
||||
|
||||
err = ws.Start()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start WebSocket: %v", err)
|
||||
}
|
||||
|
||||
// Wait for interface to be online (up to 10 seconds)
|
||||
for i := 0; i < 100; i++ {
|
||||
if ws.IsOnline() {
|
||||
break
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
if !ws.IsOnline() {
|
||||
t.Error("WebSocket should be online")
|
||||
}
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
if err := ws.Stop(); err != nil {
|
||||
t.Errorf("Failed to stop WebSocket: %v", err)
|
||||
}
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
func TestWebSocketFrameEncoding(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping frame encoding test in short mode")
|
||||
}
|
||||
|
||||
ws, err := NewWebSocketInterface("test", "wss://socket.quad4.io/ws", true)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create WebSocket interface: %v", err)
|
||||
}
|
||||
|
||||
err = ws.Start()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start WebSocket: %v", err)
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
data []byte
|
||||
}{
|
||||
{"small frame", []byte{0x01, 0x02, 0x03}},
|
||||
{"medium frame", make([]byte, 200)},
|
||||
{"large frame", make([]byte, 1000)},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := ws.Send(tc.data, "")
|
||||
if err != nil {
|
||||
t.Errorf("Failed to send %s: %v", tc.name, err)
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
})
|
||||
}
|
||||
|
||||
if err := ws.Stop(); err != nil {
|
||||
t.Errorf("Failed to stop WebSocket: %v", err)
|
||||
}
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
func TestWebSocketEnableDisable(t *testing.T) {
|
||||
ws, err := NewWebSocketInterface("test", "wss://socket.quad4.io/ws", false)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create WebSocket interface: %v", err)
|
||||
}
|
||||
|
||||
if ws.IsEnabled() {
|
||||
t.Error("Interface should not be enabled initially")
|
||||
}
|
||||
|
||||
ws.Enable()
|
||||
if !ws.IsEnabled() {
|
||||
t.Error("Interface should be enabled after Enable()")
|
||||
}
|
||||
|
||||
ws.Disable()
|
||||
if ws.IsEnabled() {
|
||||
t.Error("Interface should not be enabled after Disable()")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebSocketDetach(t *testing.T) {
|
||||
ws, err := NewWebSocketInterface("test", "wss://socket.quad4.io/ws", true)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create WebSocket interface: %v", err)
|
||||
}
|
||||
|
||||
if ws.IsDetached() {
|
||||
t.Error("Interface should not be detached initially")
|
||||
}
|
||||
|
||||
ws.Detach()
|
||||
if !ws.IsDetached() {
|
||||
t.Error("Interface should be detached after Detach()")
|
||||
}
|
||||
|
||||
if ws.IsOnline() {
|
||||
t.Error("Interface should be offline after Detach()")
|
||||
}
|
||||
}
|
||||
253
pkg/interfaces/websocket_wasm.go
Normal file
253
pkg/interfaces/websocket_wasm.go
Normal file
@@ -0,0 +1,253 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||
//go:build js && wasm
|
||||
// +build js,wasm
|
||||
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"syscall/js"
|
||||
"time"
|
||||
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||
)
|
||||
|
||||
const (
|
||||
WS_MTU = 1064
|
||||
WS_BITRATE = 10000000
|
||||
WS_RECONNECT_DELAY = 2 * time.Second
|
||||
)
|
||||
|
||||
type WebSocketInterface struct {
|
||||
BaseInterface
|
||||
wsURL string
|
||||
ws js.Value
|
||||
connected bool
|
||||
messageQueue [][]byte
|
||||
}
|
||||
|
||||
func NewWebSocketInterface(name string, wsURL string, enabled bool) (*WebSocketInterface, error) {
|
||||
ws := &WebSocketInterface{
|
||||
BaseInterface: NewBaseInterface(name, common.IF_TYPE_UDP, enabled),
|
||||
wsURL: wsURL,
|
||||
messageQueue: make([][]byte, 0),
|
||||
}
|
||||
|
||||
ws.MTU = WS_MTU
|
||||
ws.Bitrate = WS_BITRATE
|
||||
|
||||
return ws, nil
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) GetName() string {
|
||||
return wsi.Name
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) GetType() common.InterfaceType {
|
||||
return wsi.Type
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) GetMode() common.InterfaceMode {
|
||||
return wsi.Mode
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) IsOnline() bool {
|
||||
wsi.Mutex.RLock()
|
||||
defer wsi.Mutex.RUnlock()
|
||||
return wsi.Online && wsi.connected
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) IsDetached() bool {
|
||||
wsi.Mutex.RLock()
|
||||
defer wsi.Mutex.RUnlock()
|
||||
return wsi.Detached
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) Detach() {
|
||||
wsi.Mutex.Lock()
|
||||
defer wsi.Mutex.Unlock()
|
||||
wsi.Detached = true
|
||||
wsi.Online = false
|
||||
wsi.closeWebSocket()
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) Enable() {
|
||||
wsi.Mutex.Lock()
|
||||
defer wsi.Mutex.Unlock()
|
||||
wsi.Enabled = true
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) Disable() {
|
||||
wsi.Mutex.Lock()
|
||||
defer wsi.Mutex.Unlock()
|
||||
wsi.Enabled = false
|
||||
wsi.closeWebSocket()
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) Start() error {
|
||||
wsi.Mutex.Lock()
|
||||
defer wsi.Mutex.Unlock()
|
||||
|
||||
if wsi.ws.Truthy() {
|
||||
return fmt.Errorf("WebSocket already started")
|
||||
}
|
||||
|
||||
ws := js.Global().Get("WebSocket").New(wsi.wsURL)
|
||||
ws.Set("binaryType", "arraybuffer")
|
||||
|
||||
ws.Set("onopen", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
wsi.Mutex.Lock()
|
||||
wsi.connected = true
|
||||
wsi.Online = true
|
||||
wsi.Mutex.Unlock()
|
||||
|
||||
debug.Log(debug.DEBUG_INFO, "WebSocket connected", "name", wsi.Name, "url", wsi.wsURL)
|
||||
|
||||
wsi.Mutex.Lock()
|
||||
queue := make([][]byte, len(wsi.messageQueue))
|
||||
copy(queue, wsi.messageQueue)
|
||||
wsi.messageQueue = wsi.messageQueue[:0]
|
||||
wsi.Mutex.Unlock()
|
||||
|
||||
for _, msg := range queue {
|
||||
wsi.sendWebSocketMessage(msg)
|
||||
}
|
||||
|
||||
return nil
|
||||
}))
|
||||
|
||||
ws.Set("onmessage", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
if len(args) < 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
event := args[0]
|
||||
data := event.Get("data")
|
||||
|
||||
var packet []byte
|
||||
if data.Type() == js.TypeString {
|
||||
packet = []byte(data.String())
|
||||
} else if data.Type() == js.TypeObject {
|
||||
array := js.Global().Get("Uint8Array").New(data)
|
||||
length := array.Get("length").Int()
|
||||
packet = make([]byte, length)
|
||||
js.CopyBytesToGo(packet, array)
|
||||
} else {
|
||||
debug.Log(debug.DEBUG_ERROR, "Unknown WebSocket message type", "type", data.Type().String())
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(packet) < 1 {
|
||||
debug.Log(debug.DEBUG_ERROR, "WebSocket message empty")
|
||||
return nil
|
||||
}
|
||||
|
||||
wsi.Mutex.Lock()
|
||||
wsi.RxBytes += uint64(len(packet))
|
||||
wsi.Mutex.Unlock()
|
||||
|
||||
wsi.ProcessIncoming(packet)
|
||||
|
||||
return nil
|
||||
}))
|
||||
|
||||
ws.Set("onerror", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
debug.Log(debug.DEBUG_ERROR, "WebSocket error", "name", wsi.Name)
|
||||
return nil
|
||||
}))
|
||||
|
||||
ws.Set("onclose", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
wsi.Mutex.Lock()
|
||||
wsi.connected = false
|
||||
wsi.Online = false
|
||||
wsi.Mutex.Unlock()
|
||||
|
||||
debug.Log(debug.DEBUG_INFO, "WebSocket closed", "name", wsi.Name)
|
||||
|
||||
if wsi.Enabled && !wsi.Detached {
|
||||
time.Sleep(WS_RECONNECT_DELAY)
|
||||
go wsi.Start()
|
||||
}
|
||||
|
||||
return nil
|
||||
}))
|
||||
|
||||
wsi.ws = ws
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) Stop() error {
|
||||
wsi.Mutex.Lock()
|
||||
defer wsi.Mutex.Unlock()
|
||||
wsi.Enabled = false
|
||||
wsi.closeWebSocket()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) closeWebSocket() {
|
||||
if wsi.ws.Truthy() {
|
||||
wsi.ws.Call("close")
|
||||
wsi.ws = js.Value{}
|
||||
}
|
||||
wsi.connected = false
|
||||
wsi.Online = false
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) Send(data []byte, addr string) error {
|
||||
if !wsi.IsEnabled() {
|
||||
return fmt.Errorf("interface not enabled")
|
||||
}
|
||||
|
||||
wsi.Mutex.Lock()
|
||||
wsi.TxBytes += uint64(len(data))
|
||||
wsi.Mutex.Unlock()
|
||||
|
||||
if !wsi.connected {
|
||||
wsi.Mutex.Lock()
|
||||
wsi.messageQueue = append(wsi.messageQueue, data)
|
||||
wsi.Mutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
return wsi.sendWebSocketMessage(data)
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) sendWebSocketMessage(data []byte) error {
|
||||
if !wsi.ws.Truthy() {
|
||||
return fmt.Errorf("WebSocket not initialized")
|
||||
}
|
||||
|
||||
if wsi.ws.Get("readyState").Int() != 1 {
|
||||
return fmt.Errorf("WebSocket not open")
|
||||
}
|
||||
|
||||
array := js.Global().Get("Uint8Array").New(len(data))
|
||||
js.CopyBytesToJS(array, data)
|
||||
|
||||
wsi.ws.Call("send", array)
|
||||
|
||||
debug.Log(debug.DEBUG_VERBOSE, "WebSocket sent packet", "name", wsi.Name, "bytes", len(data))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) ProcessOutgoing(data []byte) error {
|
||||
return wsi.Send(data, "")
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) GetConn() net.Conn {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) GetMTU() int {
|
||||
return wsi.MTU
|
||||
}
|
||||
|
||||
func (wsi *WebSocketInterface) IsEnabled() bool {
|
||||
wsi.Mutex.RLock()
|
||||
defer wsi.Mutex.RUnlock()
|
||||
return wsi.Enabled && wsi.Online && !wsi.Detached
|
||||
}
|
||||
Reference in New Issue
Block a user