682 lines
16 KiB
Go
682 lines
16 KiB
Go
// SPDX-License-Identifier: 0BSD
|
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
|
package interfaces
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"net"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"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 = 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"
|
|
|
|
MULTI_IF_DEQUE_LEN = 48
|
|
MULTI_IF_DEQUE_TTL = 750 * time.Millisecond
|
|
)
|
|
|
|
type DequeEntry struct {
|
|
hash [32]byte
|
|
timestamp time.Time
|
|
}
|
|
|
|
type AutoInterface struct {
|
|
BaseInterface
|
|
groupID []byte
|
|
groupHash []byte
|
|
discoveryPort int
|
|
dataPort int
|
|
discoveryScope string
|
|
multicastAddrType string
|
|
mcastDiscoveryAddr 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
|
|
mifDeque []DequeEntry
|
|
done chan struct{}
|
|
stopOnce sync.Once
|
|
}
|
|
|
|
type AdoptedInterface struct {
|
|
name string
|
|
linkLocalAddr string
|
|
index int
|
|
}
|
|
|
|
type Peer struct {
|
|
ifaceName string
|
|
lastHeard time.Time
|
|
addr *net.UDPAddr
|
|
}
|
|
|
|
func descopeLinkLocal(addr string) string {
|
|
// Drop scope specifier expressed as %ifname (macOS)
|
|
if i := strings.Index(addr, "%"); i != -1 {
|
|
addr = addr[:i]
|
|
}
|
|
|
|
// Drop embedded scope specifier (NetBSD, OpenBSD)
|
|
// Python: re.sub(r"fe80:[0-9a-f]*::","fe80::", link_local_addr)
|
|
if strings.HasPrefix(addr, "fe80:") {
|
|
parts := strings.Split(addr, ":")
|
|
// Check for fe80:[scope]::...
|
|
if len(parts) >= 3 && parts[2] == "" && parts[1] != "" {
|
|
return "fe80::" + strings.Join(parts[3:], ":")
|
|
}
|
|
}
|
|
return addr
|
|
}
|
|
|
|
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
|
|
if config.MulticastAddrType != "" {
|
|
multicastAddrType = normalizeMulticastType(config.MulticastAddrType)
|
|
}
|
|
|
|
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))
|
|
|
|
// Python-compatible multicast address generation
|
|
// gt = "0:" + "{:02x}".format(g[3]+(g[2]<<8)) + ":" + ...
|
|
gt := "0"
|
|
for i := 1; i <= 6; i++ {
|
|
gt += fmt.Sprintf(":%02x%02x", groupHash[i*2], groupHash[i*2+1])
|
|
}
|
|
mcastAddr := fmt.Sprintf("ff%s%s:%s", multicastAddrType, discoveryScope, gt)
|
|
|
|
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,
|
|
},
|
|
groupID: []byte(groupID),
|
|
groupHash: groupHash[:],
|
|
discoveryPort: discoveryPort,
|
|
dataPort: dataPort,
|
|
discoveryScope: discoveryScope,
|
|
multicastAddrType: multicastAddrType,
|
|
mcastDiscoveryAddr: mcastAddr,
|
|
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,
|
|
mifDeque: make([]DequeEntry, 0, MULTI_IF_DEQUE_LEN),
|
|
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 {
|
|
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 {
|
|
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 = descopeLinkLocal(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 {
|
|
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 {
|
|
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.UDPConn, ifaceName string) {
|
|
buf := make([]byte, common.NUM_1024)
|
|
for {
|
|
ai.Mutex.RLock()
|
|
done := ai.done
|
|
ai.Mutex.RUnlock()
|
|
|
|
select {
|
|
case <-done:
|
|
return
|
|
default:
|
|
}
|
|
|
|
n, remoteAddr, err := conn.ReadFromUDP(buf)
|
|
if err != nil {
|
|
if ai.IsOnline() {
|
|
debug.Log(debug.DEBUG_ERROR, "Discovery read error", "interface", ifaceName, "error", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Python: discovery_token = RNS.Identity.full_hash(self.group_id+ipv6_src[0].encode("utf-8"))
|
|
peerIP := descopeLinkLocal(remoteAddr.IP.String())
|
|
tokenSource := append(ai.groupID, []byte(peerIP)...)
|
|
expectedHash := sha256.Sum256(tokenSource)
|
|
|
|
if n >= len(expectedHash) {
|
|
receivedHash := buf[:len(expectedHash)]
|
|
if bytes.Equal(receivedHash, expectedHash[:]) {
|
|
ai.handlePeerAnnounce(remoteAddr, ifaceName)
|
|
} else {
|
|
debug.Log(debug.DEBUG_TRACE, "Received discovery with mismatched group hash", "interface", ifaceName, "peer", peerIP)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (ai *AutoInterface) handleData(conn *net.UDPConn, ifaceName string) {
|
|
buf := make([]byte, ai.GetMTU())
|
|
for {
|
|
ai.Mutex.RLock()
|
|
done := ai.done
|
|
ai.Mutex.RUnlock()
|
|
|
|
select {
|
|
case <-done:
|
|
return
|
|
default:
|
|
}
|
|
|
|
n, remoteAddr, err := conn.ReadFromUDP(buf)
|
|
if err != nil {
|
|
if ai.IsOnline() {
|
|
debug.Log(debug.DEBUG_ERROR, "Data read error", "interface", ifaceName, "error", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
data := buf[:n]
|
|
dataHash := sha256.Sum256(data)
|
|
now := time.Now()
|
|
|
|
ai.Mutex.Lock()
|
|
// Check for duplicate in mifDeque
|
|
isDuplicate := false
|
|
for i := 0; i < len(ai.mifDeque); i++ {
|
|
if ai.mifDeque[i].hash == dataHash && now.Sub(ai.mifDeque[i].timestamp) < MULTI_IF_DEQUE_TTL {
|
|
isDuplicate = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if isDuplicate {
|
|
ai.Mutex.Unlock()
|
|
continue
|
|
}
|
|
|
|
// Add to deque
|
|
ai.mifDeque = append(ai.mifDeque, DequeEntry{hash: dataHash, timestamp: now})
|
|
if len(ai.mifDeque) > MULTI_IF_DEQUE_LEN {
|
|
ai.mifDeque = ai.mifDeque[1:]
|
|
}
|
|
|
|
// Refresh peer if known
|
|
peerIP := descopeLinkLocal(remoteAddr.IP.String())
|
|
peerKey := peerIP + "%" + ifaceName
|
|
if peer, exists := ai.peers[peerKey]; exists {
|
|
peer.lastHeard = now
|
|
}
|
|
ai.Mutex.Unlock()
|
|
|
|
if callback := ai.GetPacketCallback(); callback != nil {
|
|
callback(data, ai)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (ai *AutoInterface) handlePeerAnnounce(addr *net.UDPAddr, ifaceName string) {
|
|
ai.Mutex.Lock()
|
|
defer ai.Mutex.Unlock()
|
|
|
|
peerIP := addr.IP.String()
|
|
|
|
for _, localAddr := range ai.linkLocalAddrs {
|
|
if peerIP == localAddr {
|
|
ai.multicastEchoes[ifaceName] = time.Now()
|
|
debug.Log(debug.DEBUG_TRACE, "Received own multicast echo", "interface", ifaceName)
|
|
return
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
// Python: discovery_token = RNS.Identity.full_hash(self.group_id+link_local_address.encode("utf-8"))
|
|
tokenSource := append(ai.groupID, []byte(adoptedIface.linkLocalAddr)...)
|
|
token := sha256.Sum256(tokenSource)
|
|
|
|
if _, err := ai.outboundConn.WriteToUDP(token[:], 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)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (ai *AutoInterface) peerJobs() {
|
|
ticker := time.NewTicker(ai.peerJobInterval)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
if !ai.IsOnline() {
|
|
return
|
|
}
|
|
|
|
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 {
|
|
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()
|
|
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
|
|
}
|