2026-07-07 02:38:40 -05:00
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
// Copyright (c) 2024-2026 Quad4.io
|
|
|
|
|
|
|
|
|
|
package node
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/hex"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"sync"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"quad4/reticulum-go/pkg/backbone"
|
|
|
|
|
"quad4/reticulum-go/pkg/buffer"
|
|
|
|
|
"quad4/reticulum-go/pkg/channel"
|
|
|
|
|
"quad4/reticulum-go/pkg/common"
|
|
|
|
|
"quad4/reticulum-go/pkg/debug"
|
2026-07-07 03:20:50 -05:00
|
|
|
"quad4/reticulum-go/pkg/discovery"
|
2026-07-07 02:38:40 -05:00
|
|
|
"quad4/reticulum-go/pkg/interfaces"
|
|
|
|
|
"quad4/reticulum-go/pkg/link"
|
2026-08-13 11:39:52 -05:00
|
|
|
"quad4/reticulum-go/pkg/sandbox"
|
2026-07-07 02:38:40 -05:00
|
|
|
"quad4/reticulum-go/pkg/sharedinstance"
|
|
|
|
|
"quad4/reticulum-go/pkg/transport"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// PauseMode controls how OnNetworkLost affects interfaces.
|
|
|
|
|
type PauseMode int
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
PauseModeDisable PauseMode = iota
|
|
|
|
|
PauseModeStop
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// LinkReconnectOptions configures optional automatic link re-establishment.
|
|
|
|
|
type LinkReconnectOptions struct {
|
|
|
|
|
MaxAttempts int
|
|
|
|
|
Backoff time.Duration
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Node orchestrates transport, interfaces, and network lifecycle for embedders.
|
|
|
|
|
type Node struct {
|
|
|
|
|
config *common.ReticulumConfig
|
|
|
|
|
transport *transport.Transport
|
|
|
|
|
sharedInstance *sharedinstance.Instance
|
|
|
|
|
interfaces []interfaces.Interface
|
|
|
|
|
channels map[string]*channel.Channel
|
|
|
|
|
buffers map[string]*buffer.Buffer
|
|
|
|
|
reloadMu sync.Mutex
|
|
|
|
|
|
|
|
|
|
lastNetworkDown time.Time
|
|
|
|
|
networkPaused bool
|
|
|
|
|
pauseMode PauseMode
|
|
|
|
|
watchedDests map[string][]byte
|
|
|
|
|
watchMu sync.RWMutex
|
|
|
|
|
linkMgr *linkManager
|
2026-07-07 03:20:50 -05:00
|
|
|
discovery *discovery.InterfaceDiscovery
|
2026-07-17 07:46:38 -05:00
|
|
|
announcer *discovery.InterfaceAnnouncer
|
2026-07-07 03:20:50 -05:00
|
|
|
}
|
|
|
|
|
|
2026-07-17 07:46:38 -05:00
|
|
|
// StartInterfaceDiscovery enables rnstransport interface discovery listening
|
|
|
|
|
// and starts the InterfaceAnnouncer when discoverable interfaces are configured.
|
2026-07-07 03:20:50 -05:00
|
|
|
func (n *Node) StartInterfaceDiscovery() {
|
2026-07-17 07:46:38 -05:00
|
|
|
if n == nil || n.transport == nil || n.config == nil {
|
2026-07-07 03:20:50 -05:00
|
|
|
return
|
|
|
|
|
}
|
2026-07-17 07:46:38 -05:00
|
|
|
listen := n.config.DiscoverInterfaces || discovery.HasDiscoverableInterfaces(n.config)
|
|
|
|
|
if !listen {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if n.discovery == nil {
|
2026-07-27 06:42:14 -05:00
|
|
|
isBH := func(h []byte) bool {
|
|
|
|
|
tab := n.transport.BlackholeTable()
|
|
|
|
|
return tab != nil && tab.Has(h)
|
|
|
|
|
}
|
|
|
|
|
n.discovery = discovery.NewInterfaceDiscoveryWithBlackhole(n.transport, discovery.DefaultStampValue, nil, isBH)
|
2026-07-17 07:46:38 -05:00
|
|
|
n.discovery.Start()
|
|
|
|
|
}
|
|
|
|
|
if n.announcer != nil || !discovery.HasDiscoverableInterfaces(n.config) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
id := n.transport.NetworkIdentity()
|
|
|
|
|
if id == nil {
|
|
|
|
|
id = n.transport.TransportIdentity()
|
|
|
|
|
}
|
|
|
|
|
if id == nil {
|
|
|
|
|
debug.Log(debug.DebugError, "Interface discovery announcer skipped: no identity")
|
2026-07-07 03:20:50 -05:00
|
|
|
return
|
|
|
|
|
}
|
2026-07-17 07:46:38 -05:00
|
|
|
ann, err := discovery.NewInterfaceAnnouncer(n.transport, n.config, id)
|
|
|
|
|
if err != nil {
|
|
|
|
|
debug.Log(debug.DebugError, "Interface discovery announcer failed", "error", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
n.announcer = ann
|
|
|
|
|
n.announcer.Start()
|
2026-07-07 02:38:40 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// New constructs a Node from configuration without starting it.
|
|
|
|
|
func New(cfg *common.ReticulumConfig) (*Node, error) {
|
|
|
|
|
if cfg == nil {
|
|
|
|
|
cfg = common.DefaultConfig()
|
|
|
|
|
}
|
2026-08-13 11:39:52 -05:00
|
|
|
sandbox.SetExecRlimits(cfg.SandboxExecRlimits)
|
2026-07-07 02:38:40 -05:00
|
|
|
if _, err := backbone.Init(backbone.ParseBackend(cfg.BackboneIO)); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("backbone I/O hub: %w", err)
|
|
|
|
|
}
|
|
|
|
|
t := transport.NewTransport(cfg)
|
|
|
|
|
n := &Node{
|
|
|
|
|
config: cfg,
|
|
|
|
|
transport: t,
|
|
|
|
|
interfaces: make([]interfaces.Interface, 0),
|
|
|
|
|
channels: make(map[string]*channel.Channel),
|
|
|
|
|
buffers: make(map[string]*buffer.Buffer),
|
|
|
|
|
pauseMode: PauseModeDisable,
|
|
|
|
|
watchedDests: make(map[string][]byte),
|
|
|
|
|
}
|
|
|
|
|
ctx := n.fromConfigContext()
|
|
|
|
|
for name, ifaceConfig := range cfg.Interfaces {
|
|
|
|
|
if !ifaceConfig.Enabled {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
iface, err := interfaces.NewFromConfigWithContext(name, ifaceConfig, ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if cfg.PanicOnInterfaceErr {
|
2026-07-16 11:38:13 -05:00
|
|
|
return nil, fmt.Errorf("failed to create interface %s: %w", name, err)
|
2026-07-07 02:38:40 -05:00
|
|
|
}
|
|
|
|
|
debug.Log(debug.DebugCritical, "Error creating interface", "name", name, "error", err)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
n.interfaces = append(n.interfaces, iface)
|
|
|
|
|
}
|
|
|
|
|
return n, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Transport returns the underlying transport.
|
|
|
|
|
func (n *Node) Transport() *transport.Transport {
|
|
|
|
|
return n.transport
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Config returns the active configuration.
|
|
|
|
|
func (n *Node) Config() *common.ReticulumConfig {
|
|
|
|
|
return n.config
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Interfaces returns the configured interface list.
|
|
|
|
|
func (n *Node) Interfaces() []interfaces.Interface {
|
|
|
|
|
return n.interfaces
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Start starts transport and network interfaces.
|
|
|
|
|
func (n *Node) Start() error {
|
|
|
|
|
if err := n.transport.Start(); err != nil {
|
|
|
|
|
return fmt.Errorf("failed to start transport: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if err := n.transport.InitializePathRequestHandler(); err != nil {
|
|
|
|
|
return fmt.Errorf("path request handler: %w", err)
|
|
|
|
|
}
|
2026-07-09 14:45:25 -05:00
|
|
|
if err := n.transport.InitializeProbeDestination(); err != nil {
|
|
|
|
|
return fmt.Errorf("probe destination: %w", err)
|
|
|
|
|
}
|
2026-07-07 02:38:40 -05:00
|
|
|
hooks := sharedinstance.Hooks{
|
|
|
|
|
RegisterInterface: n.transport.RegisterInterface,
|
|
|
|
|
HandleInterface: n.handleInterface,
|
|
|
|
|
}
|
|
|
|
|
inst, err := sharedinstance.Attach(n.config, n.transport, hooks)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("shared instance: %w", err)
|
|
|
|
|
}
|
|
|
|
|
n.sharedInstance = inst
|
2026-07-16 11:16:37 -05:00
|
|
|
if err := n.transport.InitializeNetworkIdentity(); err != nil {
|
|
|
|
|
return fmt.Errorf("network identity: %w", err)
|
|
|
|
|
}
|
2026-07-07 02:38:40 -05:00
|
|
|
if !inst.OwnsNetworkInterfaces() {
|
2026-07-17 17:43:57 -05:00
|
|
|
debug.Log(debug.DebugInfo, "Using existing local shared Reticulum instance, skipping configured network interfaces")
|
2026-07-07 02:38:40 -05:00
|
|
|
return nil
|
|
|
|
|
}
|
2026-08-13 01:47:41 -05:00
|
|
|
if err := n.startInterfaces(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if err := n.transport.InitializeRemoteManagement(); err != nil {
|
|
|
|
|
return fmt.Errorf("remote management: %w", err)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
2026-07-07 02:38:40 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (n *Node) startInterfaces() error {
|
|
|
|
|
type result struct {
|
|
|
|
|
iface interfaces.Interface
|
|
|
|
|
err error
|
|
|
|
|
}
|
|
|
|
|
results := make(chan result, len(n.interfaces))
|
|
|
|
|
for _, iface := range n.interfaces {
|
|
|
|
|
go func(iface interfaces.Interface) {
|
|
|
|
|
results <- result{iface: iface, err: iface.Start()}
|
|
|
|
|
}(iface)
|
|
|
|
|
}
|
|
|
|
|
started := make([]interfaces.Interface, 0, len(n.interfaces))
|
|
|
|
|
for range len(n.interfaces) {
|
|
|
|
|
res := <-results
|
|
|
|
|
if res.err != nil {
|
|
|
|
|
if n.config.PanicOnInterfaceErr {
|
2026-07-16 11:38:13 -05:00
|
|
|
return fmt.Errorf("failed to start interface %s: %w", res.iface.GetName(), res.err)
|
2026-07-07 02:38:40 -05:00
|
|
|
}
|
|
|
|
|
debug.Log(debug.DebugCritical, "Error starting interface", "name", res.iface.GetName(), "error", res.err)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
started = append(started, res.iface)
|
|
|
|
|
}
|
|
|
|
|
for _, iface := range started {
|
|
|
|
|
ni, ok := iface.(common.NetworkInterface)
|
|
|
|
|
if !ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if err := n.transport.RegisterInterface(iface.GetName(), ni); err != nil {
|
|
|
|
|
debug.Log(debug.DebugCritical, "Failed to register interface", "name", iface.GetName(), "error", err)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
n.handleInterface(ni)
|
|
|
|
|
n.wireConnectivityHooks(iface)
|
2026-07-07 11:46:45 -05:00
|
|
|
if lc, ok := iface.(*interfaces.LocalClientInterface); ok && lc.IsSharedInstanceClient() {
|
|
|
|
|
n.transport.SetConnectedToSharedInstance(true)
|
|
|
|
|
if n.config != nil {
|
|
|
|
|
n.config.ConnectedToSharedInstance = true
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-07 02:38:40 -05:00
|
|
|
}
|
|
|
|
|
n.interfaces = started
|
2026-07-07 03:30:34 -05:00
|
|
|
if n.config != nil && n.config.WatchInterfaces {
|
2026-07-07 02:38:40 -05:00
|
|
|
n.startInterfaceMonitor()
|
|
|
|
|
}
|
2026-07-07 03:20:50 -05:00
|
|
|
n.StartInterfaceDiscovery()
|
2026-07-07 02:38:40 -05:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Stop shuts down interfaces and transport.
|
|
|
|
|
func (n *Node) Stop() error {
|
|
|
|
|
n.reloadMu.Lock()
|
|
|
|
|
defer n.reloadMu.Unlock()
|
2026-07-17 07:46:38 -05:00
|
|
|
if n.announcer != nil {
|
|
|
|
|
n.announcer.Stop()
|
|
|
|
|
n.announcer = nil
|
|
|
|
|
}
|
|
|
|
|
if n.discovery != nil {
|
|
|
|
|
n.discovery.Stop()
|
|
|
|
|
n.discovery = nil
|
|
|
|
|
}
|
2026-07-07 02:38:40 -05:00
|
|
|
if n.sharedInstance != nil {
|
|
|
|
|
n.sharedInstance.Close()
|
|
|
|
|
n.sharedInstance = nil
|
|
|
|
|
}
|
|
|
|
|
for _, buf := range n.buffers {
|
|
|
|
|
_ = buf.Close()
|
|
|
|
|
}
|
|
|
|
|
for _, ch := range n.channels {
|
|
|
|
|
_ = ch.Close()
|
|
|
|
|
}
|
|
|
|
|
for _, iface := range n.interfaces {
|
|
|
|
|
_ = iface.Stop()
|
|
|
|
|
}
|
|
|
|
|
if n.transport != nil {
|
|
|
|
|
if err := n.transport.Close(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
backbone.Shutdown()
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// WatchDestination registers a destination hash for path refresh on wake.
|
|
|
|
|
func (n *Node) WatchDestination(hash []byte) error {
|
|
|
|
|
if len(hash) != 16 {
|
|
|
|
|
return errors.New("destination hash must be 16 bytes")
|
|
|
|
|
}
|
|
|
|
|
key := hex.EncodeToString(hash)
|
|
|
|
|
n.watchMu.Lock()
|
|
|
|
|
n.watchedDests[key] = append([]byte(nil), hash...)
|
|
|
|
|
n.watchMu.Unlock()
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// EnableLinkAutoReconnect enables automatic link re-establishment.
|
|
|
|
|
func (n *Node) EnableLinkAutoReconnect(opts LinkReconnectOptions) {
|
|
|
|
|
if n.linkMgr == nil {
|
|
|
|
|
n.linkMgr = newLinkManager(n.transport, opts)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-25 14:43:24 -05:00
|
|
|
func defaultGravityFromConfig(cfg *common.ReticulumConfig) int {
|
|
|
|
|
if cfg != nil && cfg.DefaultGravitySet {
|
|
|
|
|
return cfg.DefaultGravity
|
|
|
|
|
}
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-07 02:38:40 -05:00
|
|
|
func (n *Node) fromConfigContext() *interfaces.FromConfigContext {
|
|
|
|
|
homeDir, err := os.UserHomeDir()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2026-07-16 11:16:37 -05:00
|
|
|
configDir := filepath.Join(homeDir, ".reticulum-go")
|
|
|
|
|
if n.config != nil && n.config.ConfigPath != "" {
|
|
|
|
|
configDir = filepath.Dir(n.config.ConfigPath)
|
|
|
|
|
}
|
|
|
|
|
storage := filepath.Join(configDir, "storage")
|
2026-07-07 02:38:40 -05:00
|
|
|
return &interfaces.FromConfigContext{
|
|
|
|
|
I2PStoragePath: storage,
|
2026-07-16 11:16:37 -05:00
|
|
|
ConfigDir: configDir,
|
2026-07-07 02:38:40 -05:00
|
|
|
TransportID: n.transport.TransportIdentityHash(),
|
2026-07-07 03:20:50 -05:00
|
|
|
WatchInterfaces: n.config != nil && n.config.WatchInterfaces,
|
2026-07-07 02:38:40 -05:00
|
|
|
DiscoverInterfaces: n.config != nil && n.config.DiscoverInterfaces,
|
|
|
|
|
PanicOnInterfaceError: n.config != nil && n.config.PanicOnInterfaceErr,
|
2026-07-25 14:43:24 -05:00
|
|
|
DefaultGravity: defaultGravityFromConfig(n.config),
|
2026-07-07 02:38:40 -05:00
|
|
|
BackboneHub: backbone.Get(),
|
|
|
|
|
SpawnBackbone: func(client *interfaces.BackboneClientInterface) {
|
|
|
|
|
if err := n.transport.RegisterInterface(client.GetName(), client); err != nil {
|
|
|
|
|
debug.Log(debug.DebugCritical, "Failed to register spawned backbone client", "error", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
n.handleInterface(client)
|
|
|
|
|
},
|
2026-07-07 11:46:45 -05:00
|
|
|
SpawnLocal: func(client *interfaces.LocalClientInterface) {
|
|
|
|
|
if err := n.transport.RegisterInterface(client.GetName(), client); err != nil {
|
|
|
|
|
debug.Log(debug.DebugCritical, "Failed to register spawned local client", "error", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
n.handleInterface(client)
|
|
|
|
|
},
|
2026-07-07 02:38:40 -05:00
|
|
|
RegisterPeer: func(name string, peer common.NetworkInterface) error {
|
|
|
|
|
return n.transport.RegisterInterface(name, peer)
|
|
|
|
|
},
|
|
|
|
|
UnregisterPeer: func(name string) {
|
|
|
|
|
n.transport.UnregisterInterface(name)
|
|
|
|
|
n.unregisterInterfaceBuffers(name)
|
|
|
|
|
},
|
|
|
|
|
SetupPeer: n.handleInterface,
|
|
|
|
|
SynthesizeTunnel: func(peer interfaces.TunnelPeer) {
|
|
|
|
|
_ = n.transport.SynthesizeTunnel(peer)
|
|
|
|
|
},
|
|
|
|
|
VoidTunnel: func(peer interfaces.TunnelPeer) {
|
|
|
|
|
n.transport.VoidTunnel(peer)
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (n *Node) wireConnectivityHooks(iface interfaces.Interface) {
|
|
|
|
|
notifier, ok := iface.(interfaces.ConnectivityNotifier)
|
|
|
|
|
if !ok {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
notifier.SetConnectivityHooks(
|
|
|
|
|
func() { debug.Log(debug.DebugVerbose, "Interface connectivity down", "name", iface.GetName()) },
|
|
|
|
|
func() { debug.Log(debug.DebugVerbose, "Interface connectivity up", "name", iface.GetName()) },
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (n *Node) ownsInterfaces() bool {
|
|
|
|
|
return n.sharedInstance == nil || n.sharedInstance.OwnsNetworkInterfaces()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RegisterLink registers a link for automatic re-establishment when enabled.
|
|
|
|
|
func (n *Node) RegisterLink(l *link.Link) {
|
|
|
|
|
if n.linkMgr != nil {
|
|
|
|
|
n.linkMgr.Register(l)
|
|
|
|
|
}
|
|
|
|
|
}
|