mirror of
https://github.com/Quad4-Software/Reticulum-Go
synced 2026-08-29 23:48:44 -04:00
411 lines
9.7 KiB
Go
411 lines
9.7 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
// Copyright (c) 2024-2026 Quad4.io
|
|
|
|
//go:build !tinygo
|
|
|
|
package interfaces
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
|
|
"quad4/reticulum-go/pkg/backbone"
|
|
"quad4/reticulum-go/pkg/common"
|
|
"quad4/reticulum-go/pkg/debug"
|
|
)
|
|
|
|
const (
|
|
defaultBlockFastFlapping = true
|
|
defaultFastFlapThresholdSec = 20.0
|
|
defaultFastFlapGrace = 5
|
|
defaultFastFlapExpirySec = 12 * 60 * 60
|
|
)
|
|
|
|
// fastFlapEntry tracks short-lived reconnects from one remote IP.
|
|
// Fields: started, lastFlap, flaps.
|
|
type fastFlapEntry struct {
|
|
started time.Time
|
|
lastFlap time.Time
|
|
flaps int
|
|
}
|
|
|
|
// BackboneInterface is a high-throughput TCP server using the process-wide I/O hub.
|
|
// Each accepted client becomes a spawned BackboneClientInterface, matching reference
|
|
// Reticulum's epoll backbone model.
|
|
type BackboneInterface struct {
|
|
BaseInterface
|
|
listener net.Listener
|
|
bindAddr string
|
|
bindPort int
|
|
preferIPv6 bool
|
|
hub *backbone.Hub
|
|
spawned []*BackboneClientInterface
|
|
spawnMu sync.Mutex
|
|
spawnHook func(*BackboneClientInterface)
|
|
callback common.PacketCallback
|
|
done chan struct{}
|
|
stopOnce sync.Once
|
|
|
|
blockFastFlapping bool
|
|
fastFlapThreshold time.Duration
|
|
fastFlapGrace int
|
|
fastFlapExpiry time.Duration
|
|
fastFlapMu sync.Mutex
|
|
fastFlapping map[string]*fastFlapEntry
|
|
}
|
|
|
|
// NewBackboneInterface binds a local TCP listener using cfg.Address/cfg.Port or cfg.Interface.
|
|
func NewBackboneInterface(name string, cfg *common.InterfaceConfig, hub *backbone.Hub, spawn func(*BackboneClientInterface)) (*BackboneInterface, error) {
|
|
if hub == nil {
|
|
hub = backbone.Get()
|
|
}
|
|
if hub == nil {
|
|
return nil, fmt.Errorf("backbone I/O hub not initialised")
|
|
}
|
|
|
|
bindAddr := cfg.Address
|
|
bindPort := cfg.Port
|
|
|
|
if cfg.Interface != "" {
|
|
iface, err := net.InterfaceByName(cfg.Interface)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("find interface %q: %w", cfg.Interface, err)
|
|
}
|
|
addrs, err := iface.Addrs()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list addresses for %q: %w", cfg.Interface, err)
|
|
}
|
|
for _, addr := range addrs {
|
|
ipnet, ok := addr.(*net.IPNet)
|
|
if !ok {
|
|
continue
|
|
}
|
|
ip := ipnet.IP
|
|
if cfg.PreferIPv6 {
|
|
if ip.To4() == nil {
|
|
bindAddr = ip.String()
|
|
break
|
|
}
|
|
} else {
|
|
if ip.To4() != nil {
|
|
bindAddr = ip.String()
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if bindAddr == "" && len(addrs) > 0 {
|
|
if ipnet, ok := addrs[0].(*net.IPNet); ok {
|
|
bindAddr = ipnet.IP.String()
|
|
}
|
|
}
|
|
}
|
|
|
|
if bindPort <= 0 {
|
|
return nil, fmt.Errorf("no port for BackboneInterface %q", name)
|
|
}
|
|
|
|
blockFlap := defaultBlockFastFlapping
|
|
if cfg != nil && cfg.BlockFastFlappingSet {
|
|
blockFlap = cfg.BlockFastFlapping
|
|
}
|
|
thresholdSec := defaultFastFlapThresholdSec
|
|
if cfg != nil && cfg.FastFlappingThreshold > 0 {
|
|
thresholdSec = cfg.FastFlappingThreshold
|
|
}
|
|
grace := defaultFastFlapGrace
|
|
if cfg != nil && cfg.FastFlappingGrace > 0 {
|
|
grace = cfg.FastFlappingGrace
|
|
}
|
|
expirySec := float64(defaultFastFlapExpirySec)
|
|
if cfg != nil && cfg.FastFlappingBlockTimeMin > 0 {
|
|
expirySec = cfg.FastFlappingBlockTimeMin * 60
|
|
}
|
|
|
|
bi := &BackboneInterface{
|
|
BaseInterface: NewBaseInterface(name, common.IFTypeBackbone, cfg.Enabled),
|
|
bindAddr: bindAddr,
|
|
bindPort: bindPort,
|
|
preferIPv6: cfg.PreferIPv6,
|
|
hub: hub,
|
|
spawnHook: spawn,
|
|
done: make(chan struct{}),
|
|
blockFastFlapping: blockFlap,
|
|
fastFlapThreshold: time.Duration(thresholdSec * float64(time.Second)),
|
|
fastFlapGrace: grace,
|
|
fastFlapExpiry: time.Duration(expirySec * float64(time.Second)),
|
|
fastFlapping: make(map[string]*fastFlapEntry),
|
|
}
|
|
bi.MTU = backboneHWMTU
|
|
bi.Bitrate = backboneServerBitrateGuess
|
|
bi.In = true
|
|
bi.Out = false
|
|
return bi, nil
|
|
}
|
|
|
|
func (bi *BackboneInterface) String() string {
|
|
addr := bi.bindAddr
|
|
if addr == "" {
|
|
if bi.preferIPv6 {
|
|
addr = "[::0]"
|
|
} else {
|
|
addr = "0.0.0.0"
|
|
}
|
|
}
|
|
return fmt.Sprintf("BackboneInterface[%s/%s:%d]", bi.Name, addr, bi.bindPort)
|
|
}
|
|
|
|
func (bi *BackboneInterface) Start() error {
|
|
bi.Mutex.Lock()
|
|
if bi.listener != nil {
|
|
bi.Mutex.Unlock()
|
|
return nil
|
|
}
|
|
select {
|
|
case <-bi.done:
|
|
bi.done = make(chan struct{})
|
|
bi.stopOnce = sync.Once{}
|
|
default:
|
|
}
|
|
bi.Mutex.Unlock()
|
|
|
|
addr := net.JoinHostPort(bi.bindAddr, fmt.Sprintf("%d", bi.bindPort))
|
|
ln, err := net.Listen("tcp", addr)
|
|
if err != nil {
|
|
return fmt.Errorf("backbone listen: %w", common.WrapListenError(err))
|
|
}
|
|
|
|
bi.Mutex.Lock()
|
|
bi.listener = ln
|
|
bi.Online = true
|
|
bi.Mutex.Unlock()
|
|
|
|
if err := bi.hub.RegisterListener(ln, bi.acceptConn); err != nil {
|
|
_ = ln.Close()
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (bi *BackboneInterface) Stop() error {
|
|
bi.Mutex.Lock()
|
|
bi.Online = false
|
|
if bi.listener != nil {
|
|
_ = bi.listener.Close()
|
|
bi.listener = nil
|
|
}
|
|
bi.Mutex.Unlock()
|
|
|
|
bi.spawnMu.Lock()
|
|
spawned := append([]*BackboneClientInterface(nil), bi.spawned...)
|
|
bi.spawned = nil
|
|
bi.spawnMu.Unlock()
|
|
|
|
for _, c := range spawned {
|
|
_ = c.Stop()
|
|
}
|
|
|
|
bi.stopOnce.Do(func() {
|
|
close(bi.done)
|
|
})
|
|
return nil
|
|
}
|
|
|
|
func (bi *BackboneInterface) acceptConn(conn net.Conn) {
|
|
select {
|
|
case <-bi.done:
|
|
_ = conn.Close()
|
|
return
|
|
default:
|
|
}
|
|
|
|
remoteIP := peerIP(conn)
|
|
if bi.isFastFlappingBlocked(remoteIP) {
|
|
debug.Log(debug.DebugVerbose, "Ignoring incoming connection from fast-flapping IP", "ip", remoteIP)
|
|
_ = conn.Close()
|
|
return
|
|
}
|
|
|
|
client := newSpawnedBackboneClient(bi, conn)
|
|
bi.spawnMu.Lock()
|
|
select {
|
|
case <-bi.done:
|
|
bi.spawnMu.Unlock()
|
|
_ = conn.Close()
|
|
return
|
|
default:
|
|
}
|
|
if bi.isFastFlappingBlocked(remoteIP) {
|
|
bi.spawnMu.Unlock()
|
|
debug.Log(debug.DebugVerbose, "Ignoring incoming connection from fast-flapping IP", "ip", remoteIP)
|
|
_ = conn.Close()
|
|
return
|
|
}
|
|
bi.spawned = append(bi.spawned, client)
|
|
cb := bi.callback
|
|
hook := bi.spawnHook
|
|
bi.spawnMu.Unlock()
|
|
|
|
if cb != nil {
|
|
client.SetPacketCallback(cb)
|
|
}
|
|
if hook != nil {
|
|
hook(client)
|
|
}
|
|
if err := client.attachStream(); err != nil {
|
|
debug.Log(debug.DebugError, "backbone spawn attach failed", "error", err)
|
|
_ = client.Stop()
|
|
}
|
|
}
|
|
|
|
func peerIP(conn net.Conn) string {
|
|
if conn == nil || conn.RemoteAddr() == nil {
|
|
return ""
|
|
}
|
|
host, _, err := net.SplitHostPort(conn.RemoteAddr().String())
|
|
if err != nil {
|
|
return conn.RemoteAddr().String()
|
|
}
|
|
return host
|
|
}
|
|
|
|
func (bi *BackboneInterface) isFastFlappingBlocked(remoteIP string) bool {
|
|
if !bi.blockFastFlapping || remoteIP == "" {
|
|
return false
|
|
}
|
|
now := time.Now()
|
|
bi.fastFlapMu.Lock()
|
|
defer bi.fastFlapMu.Unlock()
|
|
bi.expireFastFlapsLocked(now)
|
|
ffe, ok := bi.fastFlapping[remoteIP]
|
|
if !ok {
|
|
return false
|
|
}
|
|
return ffe.flaps > bi.fastFlapGrace
|
|
}
|
|
|
|
// BlockedIPCount returns how many remote IPs are currently blocked for flapping.
|
|
func (bi *BackboneInterface) BlockedIPCount() int {
|
|
if !bi.blockFastFlapping {
|
|
return 0
|
|
}
|
|
now := time.Now()
|
|
bi.fastFlapMu.Lock()
|
|
defer bi.fastFlapMu.Unlock()
|
|
bi.expireFastFlapsLocked(now)
|
|
count := 0
|
|
for _, ffe := range bi.fastFlapping {
|
|
if ffe.flaps > bi.fastFlapGrace {
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
// BlockedIPs returns remote IPs currently blocked for fast-flapping.
|
|
// Unlike Python's blocked_ip_list (which returns every tracked flap entry),
|
|
// Go only exports IPs that have exceeded the grace threshold.
|
|
func (bi *BackboneInterface) BlockedIPs() []string {
|
|
if !bi.blockFastFlapping {
|
|
return nil
|
|
}
|
|
now := time.Now()
|
|
bi.fastFlapMu.Lock()
|
|
defer bi.fastFlapMu.Unlock()
|
|
bi.expireFastFlapsLocked(now)
|
|
out := make([]string, 0)
|
|
for ip, ffe := range bi.fastFlapping {
|
|
if ffe.flaps > bi.fastFlapGrace {
|
|
out = append(out, ip)
|
|
}
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
func (bi *BackboneInterface) expireFastFlapsLocked(now time.Time) {
|
|
for ip, ffe := range bi.fastFlapping {
|
|
if now.Sub(ffe.lastFlap) > bi.fastFlapExpiry {
|
|
delete(bi.fastFlapping, ip)
|
|
debug.Log(debug.DebugVerbose, "Fast-flapping block expired", "ip", ip)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (bi *BackboneInterface) recordFastFlap(remoteIP string, connectedFor time.Duration) {
|
|
if !bi.blockFastFlapping || remoteIP == "" {
|
|
return
|
|
}
|
|
if connectedFor >= bi.fastFlapThreshold {
|
|
return
|
|
}
|
|
now := time.Now()
|
|
bi.fastFlapMu.Lock()
|
|
defer bi.fastFlapMu.Unlock()
|
|
ffe := bi.fastFlapping[remoteIP]
|
|
if ffe == nil {
|
|
ffe = &fastFlapEntry{started: now}
|
|
bi.fastFlapping[remoteIP] = ffe
|
|
}
|
|
ffe.lastFlap = now
|
|
ffe.flaps++
|
|
if ffe.flaps > bi.fastFlapGrace {
|
|
debug.Log(debug.DebugError, "Ignoring further connections due to fast-flapping",
|
|
"ip", remoteIP, "flaps", ffe.flaps, "connected_for", connectedFor)
|
|
} else {
|
|
debug.Log(debug.DebugVerbose, "Backbone client fast flapping",
|
|
"ip", remoteIP, "flaps", ffe.flaps, "connected_for", connectedFor)
|
|
}
|
|
}
|
|
|
|
func (bi *BackboneInterface) removeSpawned(client *BackboneClientInterface) {
|
|
bi.spawnMu.Lock()
|
|
defer bi.spawnMu.Unlock()
|
|
for i, c := range bi.spawned {
|
|
if c == client {
|
|
bi.spawned = append(bi.spawned[:i], bi.spawned[i+1:]...)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (bi *BackboneInterface) SetPacketCallback(cb common.PacketCallback) {
|
|
bi.Mutex.Lock()
|
|
bi.callback = cb
|
|
bi.packetCallback = cb
|
|
bi.Mutex.Unlock()
|
|
bi.spawnMu.Lock()
|
|
for _, c := range bi.spawned {
|
|
c.SetPacketCallback(cb)
|
|
}
|
|
bi.spawnMu.Unlock()
|
|
}
|
|
|
|
func (bi *BackboneInterface) ProcessOutgoing([]byte) error {
|
|
return nil
|
|
}
|
|
|
|
func (bi *BackboneInterface) Send(data []byte, address string) error {
|
|
if err := common.RejectReceiveOnly(bi); err != nil {
|
|
return err
|
|
}
|
|
bi.spawnMu.Lock()
|
|
spawned := append([]*BackboneClientInterface(nil), bi.spawned...)
|
|
bi.spawnMu.Unlock()
|
|
for _, c := range spawned {
|
|
if err := c.Send(data, address); err != nil {
|
|
debug.Log(debug.DebugVerbose, "backbone fan-out send error", "client", c.Name, "error", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (bi *BackboneInterface) GetConn() net.Conn { return nil }
|
|
|
|
func (bi *BackboneInterface) Clients() int {
|
|
bi.spawnMu.Lock()
|
|
defer bi.spawnMu.Unlock()
|
|
return len(bi.spawned)
|
|
}
|