Reticulum-Go/pkg/node/autoconnect.go

403 lines
10 KiB
Go

// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2024-2026 Quad4.io
package node
import (
"bytes"
"fmt"
"path/filepath"
"time"
"quad4/reticulum-go/pkg/common"
"quad4/reticulum-go/pkg/debug"
"quad4/reticulum-go/pkg/discovery"
"quad4/reticulum-go/pkg/interfaces"
)
const autoconnectMonitorInterval = 5 * time.Second
const autoconnectDetachAfter = 12 * time.Second
type autoconnectEntry struct {
iface interfaces.Interface
hash []byte
downSince time.Time
}
func (n *Node) discoveryStorageDir() string {
if n == nil || n.config == nil || n.config.UseInMemoryStorage() {
return ""
}
if n.config.ConfigPath == "" {
return ""
}
return filepath.Join(filepath.Dir(n.config.ConfigPath), "storage")
}
func (n *Node) onInterfaceDiscovered(info *discovery.ReceivedAnnounceInfo) {
if n == nil || info == nil || n.config == nil {
return
}
_ = discovery.PersistDiscoveredInterface(n.discoveryStorageDir(), info)
if n.config.AutoconnectDiscoveredInterfaces <= 0 {
return
}
n.autoconnect(info)
}
func (n *Node) autoconnectCount() int {
if n == nil {
return 0
}
n.acMu.Lock()
defer n.acMu.Unlock()
return len(n.acEntries)
}
func (n *Node) autoconnectCandidateIfaces() []interfaces.Interface {
n.reloadMu.Lock()
candidates := append([]interfaces.Interface(nil), n.interfaces...)
n.reloadMu.Unlock()
for _, iface := range candidates {
if lister, ok := iface.(interfaces.I2PSpawnedLister); ok {
candidates = append(candidates, lister.ListSpawnedPeers()...)
}
}
return candidates
}
func (n *Node) autoconnectExists(info *discovery.ReceivedAnnounceInfo) bool {
eh := discovery.EndpointHash(info)
n.acMu.Lock()
for _, e := range n.acEntries {
if len(e.hash) > 0 && string(e.hash) == string(eh) {
n.acMu.Unlock()
return true
}
}
n.acMu.Unlock()
host := info.Info.ReachableOn
port := info.Info.Port
hasPort := info.Info.HasPort
for _, iface := range n.autoconnectCandidateIfaces() {
if interfaces.MatchesDiscoveredEndpoint(iface, eh, host, port, hasPort) {
return true
}
}
return false
}
func (n *Node) autoconnectPeerConfig() *common.InterfaceConfig {
cfg := &common.InterfaceConfig{
Enabled: true,
Bitrate: 5_000_000,
}
if n.config.AutoconnectInterfaceGravitySet {
cfg.Gravity = n.config.AutoconnectInterfaceGravity
cfg.GravitySet = true
}
if n.config.AutoconnectInterfaceMode != "" {
cfg.Mode = n.config.AutoconnectInterfaceMode
} else if n.config.EnableTransport {
cfg.Mode = "gateway"
}
if n.config.AutoconnectAnnouncesToInternalSet {
cfg.AnnouncesToInternal = n.config.AutoconnectAnnouncesToInternal
cfg.AnnouncesToInternalSet = true
}
return cfg
}
func (n *Node) autoconnect(info *discovery.ReceivedAnnounceInfo) {
if n == nil || n.config == nil || info == nil {
return
}
limit := n.config.AutoconnectDiscoveredInterfaces
if limit <= 0 {
return
}
if n.autoconnectCount() >= limit {
return
}
ifaceType := info.Info.Type
if _, ok := discovery.AutoconnectTypes[ifaceType]; !ok {
return
}
if discovery.IsYggIPv6(info.Info.ReachableOn) {
return
}
if n.autoconnectExists(info) {
debug.Log(debug.DebugVerbose, "Discovered interface already exists, not auto-connecting",
"type", ifaceType, "name", info.Info.Name)
return
}
name := autoconnectInterfaceName(info)
eh := discovery.EndpointHash(info)
peerCfg := n.autoconnectPeerConfig()
peerCfg.IFACNetname = info.Info.IFACNetname
peerCfg.IFACNetkey = info.Info.IFACNetkey
switch ifaceType {
case "I2PInterface":
if n.autoconnectI2P(info, name, eh, peerCfg) {
return
}
case "TCPServerInterface":
n.autoconnectTCPClient(info, name, eh, peerCfg)
case "BackboneInterface":
n.autoconnectBackboneClient(info, name, eh, peerCfg)
}
}
func autoconnectInterfaceName(info *discovery.ReceivedAnnounceInfo) string {
if info == nil {
return "Discovered interface"
}
base := info.Info.Name
if base == "" {
base = "Discovered " + info.Info.Type
}
spec := info.Info.ReachableOn
if info.Info.HasPort {
spec = fmt.Sprintf("%s:%d", spec, info.Info.Port)
}
if spec == "" {
return base
}
return fmt.Sprintf("%s (%s)", base, spec)
}
func (n *Node) autoconnectBackboneClient(info *discovery.ReceivedAnnounceInfo, name string, eh []byte, peerCfg *common.InterfaceConfig) {
peerCfg.Type = "BackboneClientInterface"
peerCfg.TargetHost = info.Info.ReachableOn
peerCfg.TargetPort = int(info.Info.Port)
created, err := interfaces.NewFromConfigWithContext(name, peerCfg, n.fromConfigContext())
if err != nil {
debug.Log(debug.DebugError, "Autoconnect create failed", "error", err)
return
}
client, ok := created.(*interfaces.BackboneClientInterface)
if !ok {
debug.Log(debug.DebugError, "Autoconnect unexpected interface type", "got", fmt.Sprintf("%T", created))
_ = created.Stop()
return
}
client.AutoconnectHash = append([]byte(nil), eh...)
client.AutoconnectSource = append([]byte(nil), info.RemoteIdentity...)
if err := client.Start(); err != nil {
debug.Log(debug.DebugError, "Autoconnect start failed", "error", err)
return
}
if err := n.transport.RegisterInterface(client.GetName(), client); err != nil {
debug.Log(debug.DebugError, "Autoconnect register failed", "error", err)
_ = client.Stop()
return
}
if !n.trackAutoconnect(client, eh, info.Info.Type, name, info.Info.ReachableOn, info.Info.Port) {
n.transport.UnregisterInterface(client.GetName())
_ = client.Stop()
return
}
n.reloadMu.Lock()
n.interfaces = append(n.interfaces, client)
n.reloadMu.Unlock()
}
func (n *Node) autoconnectTCPClient(info *discovery.ReceivedAnnounceInfo, name string, eh []byte, peerCfg *common.InterfaceConfig) {
peerCfg.Type = "TCPClientInterface"
peerCfg.TargetHost = info.Info.ReachableOn
peerCfg.TargetPort = int(info.Info.Port)
created, err := interfaces.NewFromConfigWithContext(name, peerCfg, n.fromConfigContext())
if err != nil {
debug.Log(debug.DebugError, "Autoconnect create failed", "error", err)
return
}
client, ok := created.(*interfaces.TCPClientInterface)
if !ok {
debug.Log(debug.DebugError, "Autoconnect unexpected interface type", "got", fmt.Sprintf("%T", created))
_ = created.Stop()
return
}
client.AutoconnectHash = append([]byte(nil), eh...)
client.AutoconnectSource = append([]byte(nil), info.RemoteIdentity...)
if err := client.Start(); err != nil {
debug.Log(debug.DebugError, "Autoconnect start failed", "error", err)
return
}
if err := n.transport.RegisterInterface(client.GetName(), client); err != nil {
debug.Log(debug.DebugError, "Autoconnect register failed", "error", err)
_ = client.Stop()
return
}
if !n.trackAutoconnect(client, eh, info.Info.Type, name, info.Info.ReachableOn, info.Info.Port) {
n.transport.UnregisterInterface(client.GetName())
_ = client.Stop()
return
}
n.reloadMu.Lock()
n.interfaces = append(n.interfaces, client)
n.reloadMu.Unlock()
}
func (n *Node) trackAutoconnect(iface interfaces.Interface, eh []byte, ifaceType, name, host string, port int64) bool {
if n == nil || n.config == nil || iface == nil {
return false
}
limit := n.config.AutoconnectDiscoveredInterfaces
n.acMu.Lock()
if len(n.acEntries) >= limit {
n.acMu.Unlock()
return false
}
for _, e := range n.acEntries {
if len(eh) > 0 && len(e.hash) > 0 && bytes.Equal(e.hash, eh) {
n.acMu.Unlock()
return false
}
}
n.acEntries = append(n.acEntries, &autoconnectEntry{iface: iface, hash: eh})
n.acMu.Unlock()
n.handleInterface(iface)
n.wireConnectivityHooks(iface)
n.ensureAutoconnectMonitor()
debug.Log(debug.DebugInfo, "Auto-connecting discovered interface",
"type", ifaceType, "name", name, "host", host, "port", port)
return true
}
func (n *Node) drainAutoconnectEntries() {
if n == nil {
return
}
n.acMu.Lock()
entries := append([]*autoconnectEntry(nil), n.acEntries...)
n.acEntries = nil
n.acMu.Unlock()
for _, e := range entries {
n.teardownAutoconnect(e)
}
}
func (n *Node) reconnectPersistedAutoconnect() {
if n == nil || n.config == nil || n.config.AutoconnectDiscoveredInterfaces <= 0 {
return
}
list, err := discovery.LoadPersistedInterfaces(n.discoveryStorageDir())
if err != nil {
debug.Log(debug.DebugVerbose, "Load persisted discovery interfaces failed", "error", err)
return
}
for _, info := range list {
if !info.Info.Transport {
continue
}
if n.autoconnectCount() >= n.config.AutoconnectDiscoveredInterfaces {
break
}
n.autoconnect(info)
}
}
func (n *Node) ensureAutoconnectMonitor() {
n.acMu.Lock()
defer n.acMu.Unlock()
if n.acMonitorRunning {
return
}
n.acMonitorRunning = true
n.acMonitorStop = make(chan struct{})
go n.autoconnectMonitorLoop(n.acMonitorStop)
}
func (n *Node) stopAutoconnectMonitor() {
n.acMu.Lock()
stop := n.acMonitorStop
n.acMonitorRunning = false
n.acMonitorStop = nil
n.acMu.Unlock()
if stop != nil {
close(stop)
}
}
func (n *Node) autoconnectMonitorLoop(stop <-chan struct{}) {
ticker := time.NewTicker(autoconnectMonitorInterval)
defer ticker.Stop()
for {
select {
case <-stop:
return
case <-ticker.C:
n.autoconnectMonitorTick()
}
}
}
func (n *Node) autoconnectMonitorTick() {
n.acMu.Lock()
now := time.Now()
var detach []*autoconnectEntry
for _, e := range n.acEntries {
if e.iface == nil {
continue
}
if e.iface.IsOnline() {
e.downSince = time.Time{}
continue
}
if e.downSince.IsZero() {
e.downSince = now
continue
}
if now.Sub(e.downSince) >= autoconnectDetachAfter {
detach = append(detach, e)
}
}
n.acMu.Unlock()
for _, e := range detach {
n.teardownAutoconnect(e)
}
}
func (n *Node) teardownAutoconnect(e *autoconnectEntry) {
if e == nil || e.iface == nil {
return
}
name := e.iface.GetName()
debug.Log(debug.DebugVerbose, "Tearing down auto-connected interface", "name", name)
if hook, ok := e.iface.(interface{ DetachAutoconnectFromParent() }); ok {
hook.DetachAutoconnectFromParent()
}
_ = e.iface.Stop()
n.transport.UnregisterInterface(name)
n.unregisterInterfaceBuffers(name)
n.reloadMu.Lock()
filtered := n.interfaces[:0]
for _, iface := range n.interfaces {
if iface != e.iface {
filtered = append(filtered, iface)
}
}
n.interfaces = filtered
n.reloadMu.Unlock()
n.acMu.Lock()
kept := n.acEntries[:0]
for _, cur := range n.acEntries {
if cur != e {
kept = append(kept, cur)
}
}
n.acEntries = kept
n.acMu.Unlock()
}