Reticulum-Go/pkg/node/reload.go

257 lines
7.5 KiB
Go

// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2024-2026 Quad4.io
package node
import (
"errors"
"fmt"
"quad4/reticulum-go/pkg/common"
"quad4/reticulum-go/pkg/debug"
"quad4/reticulum-go/pkg/interfaces"
)
func sliceEqual(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
func floatEqual(a, b float64) bool {
const eps = 1e-9
d := a - b
if d < 0 {
d = -d
}
return d < eps
}
func interfaceConfigsEqualForReload(a, b *common.InterfaceConfig) bool {
if a == nil && b == nil {
return true
}
if a == nil || b == nil {
return false
}
return a.Type == b.Type &&
a.Enabled == b.Enabled &&
a.Address == b.Address &&
a.TargetHost == b.TargetHost &&
a.TargetPort == b.TargetPort &&
a.TargetAddress == b.TargetAddress &&
a.Port == b.Port &&
a.KISSFraming == b.KISSFraming &&
a.I2PTunneled == b.I2PTunneled &&
a.I2PConnectable == b.I2PConnectable &&
a.I2PSAMAddress == b.I2PSAMAddress &&
a.PreferIPv6 == b.PreferIPv6 &&
a.MaxReconnTries == b.MaxReconnTries &&
a.Bitrate == b.Bitrate &&
a.MTU == b.MTU &&
sliceEqual(a.I2PPeers, b.I2PPeers) &&
sliceEqual(a.Devices, b.Devices) &&
sliceEqual(a.IgnoredDevices, b.IgnoredDevices) &&
a.GroupID == b.GroupID &&
a.DiscoveryScope == b.DiscoveryScope &&
a.DiscoveryPort == b.DiscoveryPort &&
a.DataPort == b.DataPort &&
a.MulticastAddrType == b.MulticastAddrType &&
a.Interface == b.Interface &&
floatEqual(a.AnnounceCap, b.AnnounceCap) &&
floatEqual(a.AnnounceRateTarget, b.AnnounceRateTarget) &&
a.AnnounceRateGrace == b.AnnounceRateGrace &&
floatEqual(a.AnnounceRatePenalty, b.AnnounceRatePenalty) &&
a.IngressControl == b.IngressControl &&
a.IngressControlSet == b.IngressControlSet &&
a.ICNewTime == b.ICNewTime &&
floatEqual(a.ICBurstFreqNew, b.ICBurstFreqNew) &&
floatEqual(a.ICBurstFreq, b.ICBurstFreq) &&
a.ICMaxHeldAnnounces == b.ICMaxHeldAnnounces &&
a.ICBurstHold == b.ICBurstHold &&
a.ICBurstPenalty == b.ICBurstPenalty &&
a.ICHeldReleaseInterval == b.ICHeldReleaseInterval &&
floatEqual(a.ICPRBurstFreqNew, b.ICPRBurstFreqNew) &&
floatEqual(a.ICPRBurstFreq, b.ICPRBurstFreq) &&
floatEqual(a.ECPRFreq, b.ECPRFreq) &&
a.EgressControl == b.EgressControl &&
a.EgressControlSet == b.EgressControlSet &&
a.NetworkName == b.NetworkName &&
a.Passphrase == b.Passphrase &&
a.IFACSize == b.IFACSize &&
a.IFACNetname == b.IFACNetname &&
a.IFACNetkey == b.IFACNetkey &&
a.PublishIFAC == b.PublishIFAC &&
a.Command == b.Command &&
a.RespawnDelay == b.RespawnDelay &&
a.SharedInstanceType == b.SharedInstanceType &&
a.InstanceName == b.InstanceName &&
a.CertFile == b.CertFile &&
a.KeyFile == b.KeyFile &&
a.PeerKey == b.PeerKey &&
a.SNI == b.SNI &&
a.Mode == b.Mode &&
a.RecursivePRs == b.RecursivePRs &&
a.AnnouncesFromInternal == b.AnnouncesFromInternal &&
a.AnnouncesFromInternalSet == b.AnnouncesFromInternalSet &&
a.AnnouncesToInternal == b.AnnouncesToInternal &&
a.AnnouncesToInternalSet == b.AnnouncesToInternalSet &&
a.Gravity == b.Gravity &&
a.GravitySet == b.GravitySet &&
a.Outgoing == b.Outgoing &&
a.OutgoingSet == b.OutgoingSet &&
a.Device == b.Device &&
a.Speed == b.Speed &&
a.DataBits == b.DataBits &&
a.Parity == b.Parity &&
a.StopBits == b.StopBits &&
a.RTSCTS == b.RTSCTS &&
a.DSRDTR == b.DSRDTR &&
a.XONXOFF == b.XONXOFF &&
a.SerialFrameIdleMs == b.SerialFrameIdleMs &&
a.Path == b.Path &&
a.TransportMode == b.TransportMode &&
a.Domain == b.Domain &&
a.ResolveIntervalSec == b.ResolveIntervalSec &&
a.ContextID == b.ContextID &&
a.LongPollSec == b.LongPollSec &&
a.Discoverable == b.Discoverable &&
a.DiscoveryName == b.DiscoveryName &&
a.ReachableOn == b.ReachableOn &&
a.DiscoveryAnnounceIntervalSec == b.DiscoveryAnnounceIntervalSec &&
a.DiscoveryStampValue == b.DiscoveryStampValue &&
a.DiscoveryEncrypt == b.DiscoveryEncrypt &&
a.DiscoveryLocationCmd == b.DiscoveryLocationCmd &&
floatEqual(a.DiscoveryLatitude, b.DiscoveryLatitude) &&
floatEqual(a.DiscoveryLongitude, b.DiscoveryLongitude) &&
floatEqual(a.DiscoveryHeight, b.DiscoveryHeight) &&
a.HasDiscoveryGeo == b.HasDiscoveryGeo &&
a.ControlHost == b.ControlHost &&
a.ControlPort == b.ControlPort &&
a.MTUOverhead == b.MTUOverhead &&
a.AutoFragmentation == b.AutoFragmentation &&
a.AutoFragSet == b.AutoFragSet &&
a.ShortFrames == b.ShortFrames &&
a.ShortMTU == b.ShortMTU &&
a.HandshakeX2 == b.HandshakeX2 &&
a.ProofX2 == b.ProofX2 &&
a.AutoBitrate == b.AutoBitrate &&
a.AutoBitrateSet == b.AutoBitrateSet &&
a.CSMAOverhead == b.CSMAOverhead &&
a.CSMAOverheadSet == b.CSMAOverheadSet &&
floatEqual(a.TimeoutMargin, b.TimeoutMargin) &&
a.FrequencyHz == b.FrequencyHz &&
a.SampleRate == b.SampleRate &&
a.Bandwidth == b.Bandwidth &&
floatEqual(a.RXGain, b.RXGain) &&
floatEqual(a.TXGain, b.TXGain) &&
a.Modem == b.Modem &&
a.SerialNum == b.SerialNum
}
func (n *Node) tearDownInterface(iface interfaces.Interface) {
if iface == nil {
return
}
name := iface.GetName()
n.transport.UnregisterInterface(name)
if buf, ok := n.buffers[name]; ok {
_ = buf.Close()
delete(n.buffers, name)
}
if ch, ok := n.channels[name]; ok {
_ = ch.Close()
delete(n.channels, name)
}
_ = iface.Stop()
}
// ReloadInterfaces reconciles network interfaces against newCfg without restarting transport.
func (n *Node) ReloadInterfaces(newCfg *common.ReticulumConfig) error {
if newCfg == nil {
return errors.New("nil config")
}
if n.sharedInstance != nil && !n.sharedInstance.OwnsNetworkInterfaces() {
n.config = newCfg
n.transport.SetReticulumConfig(newCfg)
return nil
}
if n.transport == nil {
return errors.New("nil transport")
}
n.reloadMu.Lock()
defer n.reloadMu.Unlock()
oldCfg := n.config
oldByName := make(map[string]interfaces.Interface, len(n.interfaces))
for _, x := range n.interfaces {
oldByName[x.GetName()] = x
}
for name, oldI := range oldByName {
ic, inNew := newCfg.Interfaces[name]
if !inNew || !ic.Enabled {
n.tearDownInterface(oldI)
delete(oldByName, name)
continue
}
if !interfaceConfigsEqualForReload(oldCfg.Interfaces[name], ic) {
n.tearDownInterface(oldI)
delete(oldByName, name)
}
}
var next []interfaces.Interface
for name, ic := range newCfg.Interfaces {
if !ic.Enabled {
continue
}
if oldI, ok := oldByName[name]; ok {
next = append(next, oldI)
continue
}
niface, err := interfaces.NewFromConfigWithContext(name, ic, n.fromConfigContext())
if err != nil {
if newCfg.PanicOnInterfaceErr {
return fmt.Errorf("interface %s: %w", name, err)
}
debug.Log(debug.DebugCritical, "ReloadInterfaces: skip interface", "name", name, "error", err)
continue
}
if err := niface.Start(); err != nil {
if newCfg.PanicOnInterfaceErr {
return fmt.Errorf("start %s: %w", name, err)
}
debug.Log(debug.DebugCritical, "ReloadInterfaces: start failed", "name", name, "error", err)
continue
}
ni, ok := niface.(common.NetworkInterface)
if !ok {
_ = niface.Stop()
return fmt.Errorf("interface %s does not implement common.NetworkInterface", name)
}
if err := n.transport.ReplaceInterface(name, ni); err != nil {
_ = niface.Stop()
if newCfg.PanicOnInterfaceErr {
return err
}
debug.Log(debug.DebugCritical, "ReloadInterfaces: ReplaceInterface failed", "name", name, "error", err)
continue
}
n.handleInterface(ni)
n.wireConnectivityHooks(niface)
next = append(next, niface)
}
n.interfaces = next
n.config = newCfg
n.transport.SetReticulumConfig(newCfg)
return nil
}