Reticulum-Go/pkg/interfaces/backbone.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)
}