Merge main into tinygo and fix conflicts

This commit is contained in:
2025-12-31 22:53:35 -06:00
114 changed files with 14183 additions and 2213 deletions

View File

@@ -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
}

View File

@@ -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)

View 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
}

View File

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

View File

@@ -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) {}

View File

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

View File

@@ -1,3 +1,5 @@
// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
//go:build !linux || tinygo
// +build !linux tinygo

View 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
}

View 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()
}

View File

@@ -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
}

View 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()
}

View 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()
}

View 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
}

View 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
}

View File

@@ -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
}

View File

@@ -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)

View 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
}

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

View 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()")
}
}

View 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
}