Summary
Under Model B (RNS/LXMF node in the Network Extension), enabling N TCP relays only brings up the first one, and editing the relay set does not apply live to the background node. This was consciously deferred during the Model-B landing; filing it so it isn't lost.
This is the one genuine background-connectivity capability gap that the audit of the older tunnel branch (PR #57, feat/enable-tunnel-flip-flag, commit 79beb50 "multi-TCP tunnel — extension manages a connection per entity") surfaced that Model B (feat/model-b-background-ne, PR #90) does not yet cover. The other #57 hardening items were either ported into #90 or are MOOT/COVERED under the in-NE-node architecture.
Current behavior
NEReticulumNode.loadTCPRelayConfig() is scoped to a single relay — start() builds one hardcoded interface id: "ne-tcp-relay" (NEReticulumNode.swift:356), and the on-connect announce hook is gated guard interfaceId == "ne-tcp-relay" (:408). The SCOPE comment at :339 says only the first tcpClient relay is wired live.
InterfaceManagementViewModel.swift:376-380 records the deferral explicitly: "Multi-TCP reconciliation with main's per-entity tcpInterfaces/tcpEndpoints tracking is deferred to the dual-backend landing."
- There is no TCP-interface config-change Darwin observer in the NE (it only observes
rnodeConfigChanged and propagationConfigChanged), and AppServices.applyInterfaceChanges() (:1869) posts no relay-config Darwin notification — so adding/removing/editing a relay in the app never reaches the running background node until the NE is fully restarted.
Impact
- A user who configures multiple community/relay servers gets only the first as a live background path; the rest are silent until a restart, and never if they're added after launch.
- Architecture-independent — this is background-node capability, not tunnel plumbing, so it stays relevant under Model B.
Proposed implementation
(from the PR #57 → Model-B gap audit; the per-entity socket plumbing through SharedFrameQueue/PacketTunnelProvider/TunnelManager/ExtensionFrameReader is not needed — the NE node owns the sockets directly)
NEReticulumNode.loadTCPRelayConfig() — return all enabled tcpClient endpoints, not the first match.
NEReticulumNode.start() (~:353-373) — register one reticulum-swift TCPInterface per enabled entity, keyed by entity.id (drop the hardcoded "ne-tcp-relay" singleton; generalize the on-connect announce hook at :408 to fire per-interface).
- New TCP-interface config-change Darwin observer in the NE — mirror
registerRNodeConfigObserver (NEReticulumNode.swift:~500): diff the App-Group relay set and hot add/remove/restart per entity.
AppServices.applyInterfaceChanges() (:1869) — post that new Darwin notification so app-side relay edits apply to the background node live.
Related deferred item (lower priority)
PR #57's be663f2 also persisted the background-transport enabled flag via a SharedDefaultsConstants.tunnelEnabledKey and auto-restarted the tunnel from it at AppServices.initialize(), so users wouldn't have to re-enable every launch. Model B defines no tunnelEnabledKey and has no auto-restart-from-saved-pref path. Lower priority because Model B's tunnel is intended to be always-on (it is the node), but worth tracking alongside this if/when the tunnel enable/disable lifecycle is revisited. (PR #90 did port the disable half — TunnelManager.disable() now actually clears on-demand.)
Found via the PR #57 hardening-gap audit while landing PR #90 (Model-B background NE).
Summary
Under Model B (RNS/LXMF node in the Network Extension), enabling N TCP relays only brings up the first one, and editing the relay set does not apply live to the background node. This was consciously deferred during the Model-B landing; filing it so it isn't lost.
This is the one genuine background-connectivity capability gap that the audit of the older tunnel branch (PR #57,
feat/enable-tunnel-flip-flag, commit79beb50"multi-TCP tunnel — extension manages a connection per entity") surfaced that Model B (feat/model-b-background-ne, PR #90) does not yet cover. The other #57 hardening items were either ported into #90 or are MOOT/COVERED under the in-NE-node architecture.Current behavior
NEReticulumNode.loadTCPRelayConfig()is scoped to a single relay —start()builds one hardcoded interfaceid: "ne-tcp-relay"(NEReticulumNode.swift:356), and the on-connect announce hook is gatedguard interfaceId == "ne-tcp-relay"(:408). The SCOPE comment at:339says only the firsttcpClientrelay is wired live.InterfaceManagementViewModel.swift:376-380records the deferral explicitly: "Multi-TCP reconciliation with main's per-entity tcpInterfaces/tcpEndpoints tracking is deferred to the dual-backend landing."rnodeConfigChangedandpropagationConfigChanged), andAppServices.applyInterfaceChanges()(:1869) posts no relay-config Darwin notification — so adding/removing/editing a relay in the app never reaches the running background node until the NE is fully restarted.Impact
Proposed implementation
(from the PR #57 → Model-B gap audit; the per-entity socket plumbing through
SharedFrameQueue/PacketTunnelProvider/TunnelManager/ExtensionFrameReaderis not needed — the NE node owns the sockets directly)NEReticulumNode.loadTCPRelayConfig()— return all enabledtcpClientendpoints, not the first match.NEReticulumNode.start()(~:353-373) — register one reticulum-swiftTCPInterfaceper enabled entity, keyed byentity.id(drop the hardcoded"ne-tcp-relay"singleton; generalize the on-connect announce hook at:408to fire per-interface).registerRNodeConfigObserver(NEReticulumNode.swift:~500): diff the App-Group relay set and hot add/remove/restart per entity.AppServices.applyInterfaceChanges()(:1869) — post that new Darwin notification so app-side relay edits apply to the background node live.Related deferred item (lower priority)
PR #57's
be663f2also persisted the background-transport enabled flag via aSharedDefaultsConstants.tunnelEnabledKeyand auto-restarted the tunnel from it atAppServices.initialize(), so users wouldn't have to re-enable every launch. Model B defines notunnelEnabledKeyand has no auto-restart-from-saved-pref path. Lower priority because Model B's tunnel is intended to be always-on (it is the node), but worth tracking alongside this if/when the tunnel enable/disable lifecycle is revisited. (PR #90 did port the disable half —TunnelManager.disable()now actually clears on-demand.)Found via the PR #57 hardening-gap audit while landing PR #90 (Model-B background NE).