mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-30 19:46:15 +00:00
node: extract lifecycle supervisor FSM, migrate substrate fields (behavior-neutral)
Introduce a sans-IO lifecycle supervisor: a synchronous step(event) ->
[action] state machine (SupervisorFsm) that authors the substrate-child
spawn and teardown order, plus an owner struct (Supervisor) holding the
substrate runtime fields and embedding the FSM. The substrate-lifecycle
fields leave Node's flat list into the owner - state, packet_tx, the TUN
reader/writer handles + shutdown fd + channels, the DNS task + identity
channel, the Nostr/LAN rendezvous drivers, and the encrypt/decrypt worker
pools; the dataplane keeps packet_rx.
start()/stop() become the driver executing the FSM's SpawnChild/StopChild
actions: same children, same order, same warn/debug-and-continue on
optional failures, same logs, same NodeState transitions. Behavior- and
wire-neutral. The only determinism change is that transports now tear
down in ascending-id order, previously nondeterministic HashMap iteration.
The bounded Draining phase and the Running{Full|Degraded} health split
land as separate follow-on commits.
This commit is contained in:
@@ -172,7 +172,7 @@ impl Node {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let cache_key = (packet.transport_id, header.receiver_idx.as_u32());
|
||||
if let Some(workers) = self.decrypt_workers.as_ref().cloned()
|
||||
if let Some(workers) = self.supervisor.decrypt_workers.as_ref().cloned()
|
||||
&& self.decrypt_registered_sessions.contains(&cache_key)
|
||||
{
|
||||
let job = crate::node::decrypt_worker::DecryptJob {
|
||||
@@ -451,7 +451,7 @@ impl Node {
|
||||
/// black-hole the session.
|
||||
#[cfg(unix)]
|
||||
pub(in crate::node) fn register_decrypt_worker_session(&mut self, node_addr: &crate::NodeAddr) {
|
||||
let Some(workers) = self.decrypt_workers.as_ref().cloned() else {
|
||||
let Some(workers) = self.supervisor.decrypt_workers.as_ref().cloned() else {
|
||||
return;
|
||||
};
|
||||
let (cache_key, state) = {
|
||||
@@ -492,7 +492,7 @@ impl Node {
|
||||
&mut self,
|
||||
cache_key: (crate::transport::TransportId, u32),
|
||||
) {
|
||||
if let Some(workers) = self.decrypt_workers.as_ref() {
|
||||
if let Some(workers) = self.supervisor.decrypt_workers.as_ref() {
|
||||
workers.unregister_session(cache_key);
|
||||
}
|
||||
self.decrypt_registered_sessions.remove(&cache_key);
|
||||
|
||||
@@ -47,7 +47,7 @@ impl Node {
|
||||
// Take the TUN outbound receiver, or create a dummy channel that never
|
||||
// produces messages (when TUN is disabled). Holding the sender prevents
|
||||
// the channel from closing.
|
||||
let (mut tun_outbound_rx, _tun_guard) = match self.tun_outbound_rx.take() {
|
||||
let (mut tun_outbound_rx, _tun_guard) = match self.supervisor.tun_outbound_rx.take() {
|
||||
Some(rx) => (rx, None),
|
||||
None => {
|
||||
let (tx, rx) = tokio::sync::mpsc::channel(1);
|
||||
@@ -57,7 +57,7 @@ impl Node {
|
||||
|
||||
// Take the DNS identity receiver, or create a dummy channel (when DNS
|
||||
// is disabled). Same pattern as TUN outbound.
|
||||
let (mut dns_identity_rx, _dns_guard) = match self.dns_identity_rx.take() {
|
||||
let (mut dns_identity_rx, _dns_guard) = match self.supervisor.dns_identity_rx.take() {
|
||||
Some(rx) => (rx, None),
|
||||
None => {
|
||||
let (tx, rx) = tokio::sync::mpsc::channel(1);
|
||||
@@ -320,9 +320,11 @@ impl Node {
|
||||
// discovery-layer cooldown to the long protocol-mismatch
|
||||
// window and emit a single WARN per fresh observation.
|
||||
if self
|
||||
.supervisor
|
||||
.nostr_rendezvous
|
||||
.is_bootstrap_transport(&packet.transport_id)
|
||||
&& let Some(npub) = self
|
||||
.supervisor
|
||||
.nostr_rendezvous
|
||||
.bootstrap_transport_npub(&packet.transport_id)
|
||||
.cloned()
|
||||
|
||||
@@ -374,7 +374,7 @@ impl Node {
|
||||
mark_ipv6_ecn_ce(&mut packet);
|
||||
self.metrics().congestion.ce_received.inc();
|
||||
}
|
||||
if let Some(tun_tx) = &self.tun_tx {
|
||||
if let Some(tun_tx) = &self.supervisor.tun_tx {
|
||||
if let Err(e) = tun_tx.send(packet) {
|
||||
debug!(error = %e, "Failed to deliver decompressed IPv6 packet to TUN");
|
||||
}
|
||||
@@ -1832,7 +1832,7 @@ impl Node {
|
||||
send: PipelinedSend<'_>,
|
||||
) -> Result<bool, NodeError> {
|
||||
let dest_addr = send.dest_addr;
|
||||
let Some(workers) = self.encrypt_workers.as_ref().cloned() else {
|
||||
let Some(workers) = self.supervisor.encrypt_workers.as_ref().cloned() else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
@@ -2448,7 +2448,7 @@ impl Node {
|
||||
let our_ipv6 = FipsAddress::from_node_addr(self.node_addr()).to_ipv6();
|
||||
if let Some(response) =
|
||||
build_dest_unreachable(original_packet, DestUnreachableCode::NoRoute, our_ipv6)
|
||||
&& let Some(tun_tx) = &self.tun_tx
|
||||
&& let Some(tun_tx) = &self.supervisor.tun_tx
|
||||
{
|
||||
let _ = tun_tx.send(response);
|
||||
}
|
||||
@@ -2483,7 +2483,7 @@ impl Node {
|
||||
// causes a PMTUD blackhole when both src and ICMP-src are local.
|
||||
let dest_addr = Ipv6Addr::from(<[u8; 16]>::try_from(&original_packet[24..40]).unwrap());
|
||||
if let Some(response) = build_packet_too_big(original_packet, mtu, dest_addr)
|
||||
&& let Some(tun_tx) = &self.tun_tx
|
||||
&& let Some(tun_tx) = &self.supervisor.tun_tx
|
||||
{
|
||||
debug!(
|
||||
original_src = %src_addr,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,658 @@
|
||||
//! Node lifecycle supervisor — sans-IO core (Milestone-1 Step 1a).
|
||||
//!
|
||||
//! A synchronous `step(event) -> Vec<Action>` finite-state machine over the
|
||||
//! fixed set of substrate children. It owns the *decision* of what to bring up
|
||||
//! and tear down and in what order; the async driver in [`super`]
|
||||
//! (`start()`/`stop()`) performs the actual I/O each [`Action`] names and reports
|
||||
//! results back as [`Event`]s. The core reads no clock, performs no I/O, and
|
||||
//! holds no runtime handles — time enters only as inputs (a future `Tick`/
|
||||
//! `DrainDeadlineElapsed`, added with the `Draining` phase) — so it is
|
||||
//! unit-testable with synthetic sequences and survives a later thread-boundary
|
||||
//! move (design doc §6 Core 1, §8 "cores are sans-IO").
|
||||
//!
|
||||
//! ## Scope: the behavior-neutral rewrite
|
||||
//!
|
||||
//! This is the first of the three Step-1a commits and is strictly
|
||||
//! behavior-preserving. The machine mirrors today's `start()`/`stop()` exactly:
|
||||
//!
|
||||
//! - every configured child is spawned in the current order, and optional
|
||||
//! failures are warn/debug-and-continue (there is no `Degraded` yet — a
|
||||
//! failed child simply drains from `pending` and start still reaches
|
||||
//! `Running`, as today an even-zero-transport node does);
|
||||
//! - teardown runs in today's order and, faithfully, does **not** stop the
|
||||
//! encrypt/decrypt worker pools (they are spawned in `start()` but never torn
|
||||
//! down in `stop()`);
|
||||
//! - the machine authors only the `SpawnChild`/`StopChild` *ordering*. The
|
||||
//! `self.state` field writes stay verbatim in the driver at their current
|
||||
//! positions, so the published `NodeState` transitions are byte-for-byte
|
||||
//! unchanged. FSM-owned published state (`PublishState`) is introduced with
|
||||
//! the `Draining`/`Degraded` additions, which need it.
|
||||
//!
|
||||
//! The `Draining` phase and the `Running{Full|Degraded}` health split (design
|
||||
//! doc §6/§9.1) land as the two subsequent, separately-flagged commits and
|
||||
//! extend the [`SupState`], [`Event`], and [`Action`] enums below.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
use std::thread::JoinHandle;
|
||||
|
||||
use crate::node::NodeState;
|
||||
use crate::transport::{PacketTx, TransportId};
|
||||
use crate::upper::tun::{TunOutboundRx, TunTx};
|
||||
|
||||
/// A supervised substrate child (design doc §6 Core 1).
|
||||
///
|
||||
/// Each transport is an individual child keyed by its id so the later
|
||||
/// required-vs-optional health policy can reason about partial N-of-M bring-up.
|
||||
/// The TUN device is a compound unit at the driver (a reader thread plus a
|
||||
/// writer thread); the supervisor tracks it as the single `Tun` child, and the
|
||||
/// driver joins both threads when it executes `StopChild(Tun)`.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub(crate) enum Child {
|
||||
/// A transport instance (UDP / TCP / Ethernet), keyed by its runtime id.
|
||||
Transport(TransportId),
|
||||
/// The off-task FMP-encrypt + UDP-send worker pool (`#[cfg(unix)]`).
|
||||
EncryptWorkers,
|
||||
/// The off-task FMP-decrypt worker pool (`#[cfg(unix)]`).
|
||||
DecryptWorkers,
|
||||
/// Nostr overlay rendezvous/discovery.
|
||||
Nostr,
|
||||
/// LAN mDNS / DNS-SD rendezvous.
|
||||
Mdns,
|
||||
/// The TUN device (reader + writer threads).
|
||||
Tun,
|
||||
/// The `.fips` DNS responder task.
|
||||
Dns,
|
||||
}
|
||||
|
||||
/// An input to the supervisor. Results of executing [`Action`]s are fed back as
|
||||
/// `SubstrateUp` / `SubstrateFailed` / `ChildStopped`.
|
||||
///
|
||||
/// Only the events the behavior-neutral rewrite needs are present; `Tick`,
|
||||
/// `ChildExited`, and `DrainDeadlineElapsed` (design doc §6) arrive with the
|
||||
/// `Draining` / `Degraded` commits.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum Event {
|
||||
/// Begin bring-up. `transports` are the ids the driver has already created
|
||||
/// (their ids are allocated at creation), in creation order; the booleans
|
||||
/// mark which singleton children are configured. Valid from `Created` or
|
||||
/// `Stopped`.
|
||||
Start {
|
||||
/// Created transport ids, in the order they must be started.
|
||||
transports: Vec<TransportId>,
|
||||
/// The `#[cfg(unix)]` encrypt worker pool is configured.
|
||||
encrypt_workers: bool,
|
||||
/// The `#[cfg(unix)]` decrypt worker pool is configured.
|
||||
decrypt_workers: bool,
|
||||
/// Nostr overlay discovery is enabled.
|
||||
nostr: bool,
|
||||
/// LAN mDNS discovery is enabled.
|
||||
mdns: bool,
|
||||
/// The TUN device is enabled.
|
||||
tun: bool,
|
||||
/// The DNS responder is enabled.
|
||||
dns: bool,
|
||||
},
|
||||
/// A child the driver was asked to spawn came up.
|
||||
SubstrateUp {
|
||||
/// The child that started successfully.
|
||||
child: Child,
|
||||
},
|
||||
/// A child the driver was asked to spawn failed to start. In the
|
||||
/// behavior-neutral rewrite this is warn/debug-and-continue: the child
|
||||
/// drains from `pending` and never joins the up-set (matching today), and
|
||||
/// start still proceeds to `Running`.
|
||||
SubstrateFailed {
|
||||
/// The child that failed to start.
|
||||
child: Child,
|
||||
},
|
||||
/// Begin teardown. Valid from `Running`.
|
||||
Stop,
|
||||
/// A child the driver was asked to stop has finished stopping.
|
||||
ChildStopped {
|
||||
/// The child that has been torn down.
|
||||
child: Child,
|
||||
},
|
||||
}
|
||||
|
||||
/// An effect the driver must perform. The core never performs I/O itself.
|
||||
///
|
||||
/// `PublishState` (design doc §6) is intentionally absent from the
|
||||
/// behavior-neutral rewrite: the driver keeps its verbatim `self.state` writes,
|
||||
/// so no published-state action is needed until `Draining`/`Degraded`.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum Action {
|
||||
/// Bring up this child (the driver performs the spawn / start I/O and
|
||||
/// reports `SubstrateUp` or `SubstrateFailed`).
|
||||
SpawnChild(Child),
|
||||
/// Tear down this child (the driver performs the stop / join I/O and
|
||||
/// reports `ChildStopped`).
|
||||
StopChild(Child),
|
||||
}
|
||||
|
||||
/// Internal supervisor state (design doc §6). Richer than the published
|
||||
/// [`NodeState`](crate::node::NodeState): `Starting`/`Stopping` carry the set of
|
||||
/// children still resolving. `Draining{deadline}`, `Running{Full|Degraded}`, and
|
||||
/// `Failed{reason}` are added by the later flagged commits.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum SupState {
|
||||
/// Constructed but not started.
|
||||
Created,
|
||||
/// Bringing children up; `pending` is the set not yet resolved.
|
||||
Starting {
|
||||
/// Children asked to spawn that have not yet reported up-or-failed.
|
||||
pending: HashSet<Child>,
|
||||
},
|
||||
/// All children resolved; node operational.
|
||||
Running,
|
||||
/// Tearing children down; `pending` is the set not yet stopped.
|
||||
Stopping {
|
||||
/// Children asked to stop that have not yet reported stopped.
|
||||
pending: HashSet<Child>,
|
||||
},
|
||||
/// Fully torn down.
|
||||
Stopped,
|
||||
}
|
||||
|
||||
/// The lifecycle supervisor FSM.
|
||||
///
|
||||
/// Construct with [`SupervisorFsm::new`], feed [`Event`]s via [`SupervisorFsm::step`],
|
||||
/// and execute the returned [`Action`]s. See the module docs for the
|
||||
/// behavior-neutral scope.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct SupervisorFsm {
|
||||
state: SupState,
|
||||
/// Children currently up (present). Drives the teardown plan.
|
||||
up: HashSet<Child>,
|
||||
}
|
||||
|
||||
impl SupervisorFsm {
|
||||
/// A fresh supervisor in `Created`.
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
state: SupState::Created,
|
||||
up: HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A supervisor seeded directly into `Running` with a known up-set.
|
||||
///
|
||||
/// The teardown driver (`stop()`) reconstructs the up-set from observed
|
||||
/// runtime presence (`dns_task.is_some()`, transports keys, etc.) rather
|
||||
/// than relying on a live machine persisted across start/stop, so that
|
||||
/// teardown ordering is authored here regardless of how the node reached
|
||||
/// `Running`. Feeding `Event::Stop` then yields the ordered `StopChild`
|
||||
/// plan over exactly the present children.
|
||||
pub(crate) fn running_with(up: impl IntoIterator<Item = Child>) -> Self {
|
||||
Self {
|
||||
state: SupState::Running,
|
||||
up: up.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Current internal state (for the driver's bookkeeping and for tests).
|
||||
#[cfg(test)]
|
||||
pub(crate) fn state(&self) -> &SupState {
|
||||
&self.state
|
||||
}
|
||||
|
||||
/// Advance the machine by one event, returning the effects to perform.
|
||||
pub(crate) fn step(&mut self, event: Event) -> Vec<Action> {
|
||||
match event {
|
||||
Event::Start {
|
||||
transports,
|
||||
encrypt_workers,
|
||||
decrypt_workers,
|
||||
nostr,
|
||||
mdns,
|
||||
tun,
|
||||
dns,
|
||||
} => self.on_start(
|
||||
transports,
|
||||
encrypt_workers,
|
||||
decrypt_workers,
|
||||
nostr,
|
||||
mdns,
|
||||
tun,
|
||||
dns,
|
||||
),
|
||||
Event::SubstrateUp { child } => self.on_substrate_up(child),
|
||||
Event::SubstrateFailed { child } => self.on_substrate_failed(child),
|
||||
Event::Stop => self.on_stop(),
|
||||
Event::ChildStopped { child } => self.on_child_stopped(child),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn on_start(
|
||||
&mut self,
|
||||
transports: Vec<TransportId>,
|
||||
encrypt_workers: bool,
|
||||
decrypt_workers: bool,
|
||||
nostr: bool,
|
||||
mdns: bool,
|
||||
tun: bool,
|
||||
dns: bool,
|
||||
) -> Vec<Action> {
|
||||
// Only meaningful from a not-running state (the driver also guards on
|
||||
// `can_start`). Ignore otherwise.
|
||||
if !matches!(self.state, SupState::Created | SupState::Stopped) {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
// Canonical spawn order, mirroring today's `start()`:
|
||||
// transports (creation order) → encrypt → decrypt → nostr → mdns →
|
||||
// tun → dns. (The driver performs the peer-connect between mdns and
|
||||
// tun; it is not a supervised child.)
|
||||
let mut order: Vec<Child> = transports.into_iter().map(Child::Transport).collect();
|
||||
if encrypt_workers {
|
||||
order.push(Child::EncryptWorkers);
|
||||
}
|
||||
if decrypt_workers {
|
||||
order.push(Child::DecryptWorkers);
|
||||
}
|
||||
if nostr {
|
||||
order.push(Child::Nostr);
|
||||
}
|
||||
if mdns {
|
||||
order.push(Child::Mdns);
|
||||
}
|
||||
if tun {
|
||||
order.push(Child::Tun);
|
||||
}
|
||||
if dns {
|
||||
order.push(Child::Dns);
|
||||
}
|
||||
|
||||
self.up.clear();
|
||||
|
||||
// A node with no children at all still reaches `Running` (today: even
|
||||
// zero started transports proceeds to `Running`).
|
||||
if order.is_empty() {
|
||||
self.state = SupState::Running;
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
self.state = SupState::Starting {
|
||||
pending: order.iter().copied().collect(),
|
||||
};
|
||||
order.into_iter().map(Action::SpawnChild).collect()
|
||||
}
|
||||
|
||||
fn on_substrate_up(&mut self, child: Child) -> Vec<Action> {
|
||||
if let SupState::Starting { pending } = &mut self.state {
|
||||
pending.remove(&child);
|
||||
self.up.insert(child);
|
||||
if pending.is_empty() {
|
||||
self.state = SupState::Running;
|
||||
}
|
||||
}
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn on_substrate_failed(&mut self, child: Child) -> Vec<Action> {
|
||||
// Behavior-neutral: warn/continue. The child drains from `pending` and
|
||||
// does not join the up-set; start still reaches `Running`.
|
||||
if let SupState::Starting { pending } = &mut self.state {
|
||||
pending.remove(&child);
|
||||
if pending.is_empty() {
|
||||
self.state = SupState::Running;
|
||||
}
|
||||
}
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn on_stop(&mut self) -> Vec<Action> {
|
||||
if !matches!(self.state, SupState::Running) {
|
||||
return Vec::new();
|
||||
}
|
||||
let order = self.teardown_order();
|
||||
self.state = SupState::Stopping {
|
||||
pending: order.iter().copied().collect(),
|
||||
};
|
||||
order.into_iter().map(Action::StopChild).collect()
|
||||
}
|
||||
|
||||
fn on_child_stopped(&mut self, child: Child) -> Vec<Action> {
|
||||
if let SupState::Stopping { pending } = &mut self.state {
|
||||
pending.remove(&child);
|
||||
self.up.remove(&child);
|
||||
if pending.is_empty() {
|
||||
self.state = SupState::Stopped;
|
||||
}
|
||||
}
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
/// Teardown order over the up-set, mirroring today's `stop()`:
|
||||
/// dns → nostr → mdns → transports (ascending id) → tun.
|
||||
///
|
||||
/// The encrypt/decrypt worker pools are deliberately excluded: today's
|
||||
/// `stop()` spawns them in `start()` but never tears them down. Transports
|
||||
/// are ordered by ascending id for determinism (today's `stop()` iterates
|
||||
/// them in nondeterministic `HashMap` order, so this is neutral).
|
||||
fn teardown_order(&self) -> Vec<Child> {
|
||||
let mut order = Vec::new();
|
||||
if self.up.contains(&Child::Dns) {
|
||||
order.push(Child::Dns);
|
||||
}
|
||||
if self.up.contains(&Child::Nostr) {
|
||||
order.push(Child::Nostr);
|
||||
}
|
||||
if self.up.contains(&Child::Mdns) {
|
||||
order.push(Child::Mdns);
|
||||
}
|
||||
let mut transports: Vec<TransportId> = self
|
||||
.up
|
||||
.iter()
|
||||
.filter_map(|c| match c {
|
||||
Child::Transport(id) => Some(*id),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
transports.sort_by_key(|id| id.as_u32());
|
||||
order.extend(transports.into_iter().map(Child::Transport));
|
||||
if self.up.contains(&Child::Tun) {
|
||||
order.push(Child::Tun);
|
||||
}
|
||||
order
|
||||
}
|
||||
}
|
||||
|
||||
/// Owner of the node's lifecycle-managed substrate handles plus the sans-IO
|
||||
/// [`SupervisorFsm`] that authors their spawn/teardown ordering.
|
||||
///
|
||||
/// The fields moved here off `Node` are exactly the children the supervisor
|
||||
/// governs — the packet-send channel, the TUN reader/writer plumbing, the DNS
|
||||
/// responder task, the Nostr/LAN rendezvous drivers, and (on unix) the
|
||||
/// encrypt/decrypt worker pools — together with the published `NodeState`.
|
||||
/// This is a pure relocation: the driver (`start()`/`stop()`) reaches each
|
||||
/// field through `self.supervisor.*`, and the initializers are the same ones
|
||||
/// `Node::new` used.
|
||||
pub(crate) struct Supervisor {
|
||||
/// Node operational state (the published `NodeState`; the driver keeps its
|
||||
/// verbatim writes here at their current positions).
|
||||
pub(in crate::node) state: NodeState,
|
||||
|
||||
/// Packet sender for transports.
|
||||
pub(in crate::node) packet_tx: Option<PacketTx>,
|
||||
|
||||
/// TUN packet sender channel.
|
||||
pub(in crate::node) tun_tx: Option<TunTx>,
|
||||
/// Receiver for outbound packets from the TUN reader.
|
||||
pub(in crate::node) tun_outbound_rx: Option<TunOutboundRx>,
|
||||
/// TUN reader thread handle.
|
||||
pub(in crate::node) tun_reader_handle: Option<JoinHandle<()>>,
|
||||
/// TUN writer thread handle.
|
||||
pub(in crate::node) tun_writer_handle: Option<JoinHandle<()>>,
|
||||
/// Shutdown pipe: writing to this fd unblocks the TUN reader thread on macOS.
|
||||
/// On Linux, deleting the interface via netlink serves the same purpose.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(in crate::node) tun_shutdown_fd: Option<std::os::unix::io::RawFd>,
|
||||
|
||||
/// Receiver for resolved identities from the DNS responder.
|
||||
pub(in crate::node) dns_identity_rx: Option<crate::upper::dns::DnsIdentityRx>,
|
||||
/// DNS responder task handle.
|
||||
pub(in crate::node) dns_task: Option<tokio::task::JoinHandle<()>>,
|
||||
|
||||
/// Node-side driver state for the Nostr overlay peer-rendezvous
|
||||
/// subsystem: the engine handle, its startup timestamp, the one-shot
|
||||
/// startup-sweep latch, and the per-peer bootstrap-transport bookkeeping
|
||||
/// adopted from NAT-traversal handoffs.
|
||||
pub(in crate::node) nostr_rendezvous: crate::nostr::RendezvousDriver,
|
||||
/// mDNS / DNS-SD responder + browser for local-link peer discovery.
|
||||
/// Identity is unverified at this layer — the Noise XX handshake
|
||||
/// initiated against an mDNS-observed endpoint is what proves the
|
||||
/// peer holds the matching private key.
|
||||
pub(in crate::node) lan_rendezvous: Option<Arc<crate::mdns::LanRendezvous>>,
|
||||
|
||||
/// Off-task FMP-encrypt + UDP-send worker pool. Unix-only —
|
||||
/// the worker issues direct sendmmsg(2) / sendmsg+UDP_GSO calls
|
||||
/// on raw fds via `AsRawFd`. None on Windows or when the worker
|
||||
/// pool failed to spawn.
|
||||
#[cfg(unix)]
|
||||
pub(crate) encrypt_workers: Option<crate::node::encrypt_worker::EncryptWorkerPool>,
|
||||
|
||||
/// Off-task FMP decrypt worker pool — receiver-side mirror of
|
||||
/// `encrypt_workers`. Workers are shards: each owns its session
|
||||
/// state directly in a thread-local `HashMap` (no `RwLock`,
|
||||
/// no `Mutex` per packet). Hash-by-cache-key dispatch.
|
||||
#[cfg(unix)]
|
||||
pub(crate) decrypt_workers: Option<crate::node::decrypt_worker::DecryptWorkerPool>,
|
||||
|
||||
/// The sans-IO lifecycle FSM authoring spawn/teardown ordering.
|
||||
pub(in crate::node) fsm: SupervisorFsm,
|
||||
}
|
||||
|
||||
impl Supervisor {
|
||||
/// A fresh supervisor with all handles empty and the FSM in `Created`,
|
||||
/// matching the field initializers `Node::new` previously used.
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
state: NodeState::Created,
|
||||
packet_tx: None,
|
||||
tun_tx: None,
|
||||
tun_outbound_rx: None,
|
||||
tun_reader_handle: None,
|
||||
tun_writer_handle: None,
|
||||
#[cfg(target_os = "macos")]
|
||||
tun_shutdown_fd: None,
|
||||
dns_identity_rx: None,
|
||||
dns_task: None,
|
||||
nostr_rendezvous: crate::nostr::RendezvousDriver::default(),
|
||||
lan_rendezvous: None,
|
||||
#[cfg(unix)]
|
||||
encrypt_workers: None,
|
||||
#[cfg(unix)]
|
||||
decrypt_workers: None,
|
||||
fsm: SupervisorFsm::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn tid(n: u32) -> TransportId {
|
||||
TransportId::new(n)
|
||||
}
|
||||
|
||||
fn start_full() -> Event {
|
||||
Event::Start {
|
||||
transports: vec![tid(1), tid(2)],
|
||||
encrypt_workers: true,
|
||||
decrypt_workers: true,
|
||||
nostr: true,
|
||||
mdns: true,
|
||||
tun: true,
|
||||
dns: true,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn start_emits_spawn_in_canonical_order() {
|
||||
let mut s = SupervisorFsm::new();
|
||||
let actions = s.step(start_full());
|
||||
assert_eq!(
|
||||
actions,
|
||||
vec![
|
||||
Action::SpawnChild(Child::Transport(tid(1))),
|
||||
Action::SpawnChild(Child::Transport(tid(2))),
|
||||
Action::SpawnChild(Child::EncryptWorkers),
|
||||
Action::SpawnChild(Child::DecryptWorkers),
|
||||
Action::SpawnChild(Child::Nostr),
|
||||
Action::SpawnChild(Child::Mdns),
|
||||
Action::SpawnChild(Child::Tun),
|
||||
Action::SpawnChild(Child::Dns),
|
||||
]
|
||||
);
|
||||
assert!(matches!(s.state(), SupState::Starting { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_children_up_reaches_running() {
|
||||
let mut s = SupervisorFsm::new();
|
||||
let spawns = s.step(start_full());
|
||||
for a in spawns {
|
||||
let child = match a {
|
||||
Action::SpawnChild(c) => c,
|
||||
_ => panic!("unexpected action"),
|
||||
};
|
||||
assert_eq!(s.step(Event::SubstrateUp { child }), vec![]);
|
||||
}
|
||||
assert_eq!(s.state(), &SupState::Running);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_child_still_reaches_running_and_is_not_up() {
|
||||
// Behavior-neutral: a failed optional child does not block Running and
|
||||
// is excluded from teardown (never joined the up-set).
|
||||
let mut s = SupervisorFsm::new();
|
||||
s.step(start_full());
|
||||
for child in [
|
||||
Child::Transport(tid(1)),
|
||||
Child::Transport(tid(2)),
|
||||
Child::EncryptWorkers,
|
||||
Child::DecryptWorkers,
|
||||
Child::Nostr,
|
||||
] {
|
||||
s.step(Event::SubstrateUp { child });
|
||||
}
|
||||
// mdns fails, tun+dns come up
|
||||
s.step(Event::SubstrateFailed { child: Child::Mdns });
|
||||
s.step(Event::SubstrateUp { child: Child::Tun });
|
||||
s.step(Event::SubstrateUp { child: Child::Dns });
|
||||
assert_eq!(s.state(), &SupState::Running);
|
||||
|
||||
let stops = s.step(Event::Stop);
|
||||
// mdns must not appear in teardown; workers excluded by design.
|
||||
assert_eq!(
|
||||
stops,
|
||||
vec![
|
||||
Action::StopChild(Child::Dns),
|
||||
Action::StopChild(Child::Nostr),
|
||||
Action::StopChild(Child::Transport(tid(1))),
|
||||
Action::StopChild(Child::Transport(tid(2))),
|
||||
Action::StopChild(Child::Tun),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_children_reaches_running_immediately() {
|
||||
let mut s = SupervisorFsm::new();
|
||||
let actions = s.step(Event::Start {
|
||||
transports: vec![],
|
||||
encrypt_workers: false,
|
||||
decrypt_workers: false,
|
||||
nostr: false,
|
||||
mdns: false,
|
||||
tun: false,
|
||||
dns: false,
|
||||
});
|
||||
assert_eq!(actions, vec![]);
|
||||
assert_eq!(s.state(), &SupState::Running);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stop_teardown_order_excludes_workers() {
|
||||
let mut s = SupervisorFsm::new();
|
||||
s.step(start_full());
|
||||
for child in [
|
||||
Child::Transport(tid(2)),
|
||||
Child::Transport(tid(1)),
|
||||
Child::EncryptWorkers,
|
||||
Child::DecryptWorkers,
|
||||
Child::Nostr,
|
||||
Child::Mdns,
|
||||
Child::Tun,
|
||||
Child::Dns,
|
||||
] {
|
||||
s.step(Event::SubstrateUp { child });
|
||||
}
|
||||
let stops = s.step(Event::Stop);
|
||||
assert_eq!(
|
||||
stops,
|
||||
vec![
|
||||
Action::StopChild(Child::Dns),
|
||||
Action::StopChild(Child::Nostr),
|
||||
Action::StopChild(Child::Mdns),
|
||||
// transports ascending by id regardless of up-report order
|
||||
Action::StopChild(Child::Transport(tid(1))),
|
||||
Action::StopChild(Child::Transport(tid(2))),
|
||||
Action::StopChild(Child::Tun),
|
||||
]
|
||||
);
|
||||
assert!(matches!(s.state(), SupState::Stopping { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_children_stopped_reaches_stopped() {
|
||||
let mut s = SupervisorFsm::new();
|
||||
s.step(start_full());
|
||||
// Every spawned child reports an outcome: five come up, three fail
|
||||
// (warn/continue). `pending` drains fully, so the node still reaches
|
||||
// `Running` — as it does today.
|
||||
for child in [
|
||||
Child::Transport(tid(1)),
|
||||
Child::EncryptWorkers,
|
||||
Child::Nostr,
|
||||
Child::Tun,
|
||||
Child::Dns,
|
||||
] {
|
||||
s.step(Event::SubstrateUp { child });
|
||||
}
|
||||
for child in [Child::Transport(tid(2)), Child::DecryptWorkers, Child::Mdns] {
|
||||
s.step(Event::SubstrateFailed { child });
|
||||
}
|
||||
assert_eq!(s.state(), &SupState::Running);
|
||||
|
||||
// Only the children that came up are torn down; the failed ones never
|
||||
// joined the up-set.
|
||||
let stops = s.step(Event::Stop);
|
||||
assert_eq!(
|
||||
stops,
|
||||
vec![
|
||||
Action::StopChild(Child::Dns),
|
||||
Action::StopChild(Child::Nostr),
|
||||
Action::StopChild(Child::Transport(tid(1))),
|
||||
Action::StopChild(Child::Tun),
|
||||
]
|
||||
);
|
||||
for a in stops {
|
||||
let child = match a {
|
||||
Action::StopChild(c) => c,
|
||||
_ => panic!("unexpected action"),
|
||||
};
|
||||
assert_eq!(s.step(Event::ChildStopped { child }), vec![]);
|
||||
}
|
||||
assert_eq!(s.state(), &SupState::Stopped);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn late_substrate_up_in_running_is_inert() {
|
||||
let mut s = SupervisorFsm::new();
|
||||
s.step(Event::Start {
|
||||
transports: vec![tid(1)],
|
||||
encrypt_workers: false,
|
||||
decrypt_workers: false,
|
||||
nostr: false,
|
||||
mdns: false,
|
||||
tun: false,
|
||||
dns: false,
|
||||
});
|
||||
s.step(Event::SubstrateUp {
|
||||
child: Child::Transport(tid(1)),
|
||||
});
|
||||
assert_eq!(s.state(), &SupState::Running);
|
||||
// A stray event in Running produces nothing and does not change state.
|
||||
assert_eq!(
|
||||
s.step(Event::SubstrateUp {
|
||||
child: Child::Nostr
|
||||
}),
|
||||
vec![]
|
||||
);
|
||||
assert_eq!(s.state(), &SupState::Running);
|
||||
}
|
||||
}
|
||||
+24
-91
@@ -61,14 +61,13 @@ use crate::transport::{
|
||||
};
|
||||
use crate::upper::hosts::HostMap;
|
||||
use crate::upper::icmp_rate_limit::IcmpRateLimiter;
|
||||
use crate::upper::tun::{TunError, TunOutboundRx, TunState, TunTx};
|
||||
use crate::upper::tun::{TunError, TunState, TunTx};
|
||||
use crate::utils::index::IndexAllocator;
|
||||
use crate::{Config, ConfigError, Identity, IdentityError, NodeAddr, PeerIdentity, TreeCoordinate};
|
||||
use rand::Rng;
|
||||
use std::collections::{BTreeSet, HashMap, VecDeque};
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
use std::thread::JoinHandle;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Errors related to node operations.
|
||||
@@ -271,9 +270,13 @@ pub struct Node {
|
||||
/// `update_peers`; readers reach it through the accessors.
|
||||
context: Arc<context::NodeContext>,
|
||||
|
||||
// === State ===
|
||||
/// Node operational state.
|
||||
state: NodeState,
|
||||
// === Lifecycle Supervisor ===
|
||||
/// Owner of the lifecycle-managed substrate handles (packet-send channel,
|
||||
/// TUN plumbing, DNS task, Nostr/LAN rendezvous, encrypt/decrypt worker
|
||||
/// pools) plus the published `NodeState` and the sans-IO supervisor FSM
|
||||
/// that authors their spawn/teardown ordering. Reached via
|
||||
/// `self.supervisor.*`.
|
||||
supervisor: lifecycle::supervisor::Supervisor,
|
||||
|
||||
// === Spanning Tree ===
|
||||
/// Local spanning tree state.
|
||||
@@ -304,8 +307,6 @@ pub struct Node {
|
||||
addr_to_link: HashMap<AddrKey, LinkId>,
|
||||
|
||||
// === Packet Channel ===
|
||||
/// Packet sender for transports.
|
||||
packet_tx: Option<PacketTx>,
|
||||
/// Packet receiver (for event loop).
|
||||
packet_rx: Option<PacketRx>,
|
||||
|
||||
@@ -382,24 +383,6 @@ pub struct Node {
|
||||
tun_state: TunState,
|
||||
/// TUN interface name (for cleanup).
|
||||
tun_name: Option<String>,
|
||||
/// TUN packet sender channel.
|
||||
tun_tx: Option<TunTx>,
|
||||
/// Receiver for outbound packets from the TUN reader.
|
||||
tun_outbound_rx: Option<TunOutboundRx>,
|
||||
/// TUN reader thread handle.
|
||||
tun_reader_handle: Option<JoinHandle<()>>,
|
||||
/// TUN writer thread handle.
|
||||
tun_writer_handle: Option<JoinHandle<()>>,
|
||||
/// Shutdown pipe: writing to this fd unblocks the TUN reader thread on macOS.
|
||||
/// On Linux, deleting the interface via netlink serves the same purpose.
|
||||
#[cfg(target_os = "macos")]
|
||||
tun_shutdown_fd: Option<std::os::unix::io::RawFd>,
|
||||
|
||||
// === DNS Responder ===
|
||||
/// Receiver for resolved identities from the DNS responder.
|
||||
dns_identity_rx: Option<crate::upper::dns::DnsIdentityRx>,
|
||||
/// DNS responder task handle.
|
||||
dns_task: Option<tokio::task::JoinHandle<()>>,
|
||||
|
||||
// === Index-Based Session Dispatch ===
|
||||
/// Allocator for session indices.
|
||||
@@ -444,17 +427,6 @@ pub struct Node {
|
||||
/// are exhausted.
|
||||
retry_pending: HashMap<NodeAddr, retry::RetryState>,
|
||||
|
||||
/// Node-side driver state for the Nostr overlay peer-rendezvous
|
||||
/// subsystem: the engine handle, its startup timestamp, the one-shot
|
||||
/// startup-sweep latch, and the per-peer bootstrap-transport bookkeeping
|
||||
/// adopted from NAT-traversal handoffs.
|
||||
nostr_rendezvous: crate::nostr::RendezvousDriver,
|
||||
/// mDNS / DNS-SD responder + browser for local-link peer discovery.
|
||||
/// Identity is unverified at this layer — the Noise XX handshake
|
||||
/// initiated against an mDNS-observed endpoint is what proves the
|
||||
/// peer holds the matching private key.
|
||||
lan_rendezvous: Option<Arc<crate::mdns::LanRendezvous>>,
|
||||
|
||||
// === Periodic Parent Re-evaluation ===
|
||||
/// Timestamp of last periodic parent re-evaluation (for pacing).
|
||||
last_parent_reeval: Option<std::time::Instant>,
|
||||
@@ -490,20 +462,6 @@ pub struct Node {
|
||||
/// published through a lock-free snapshot for the display path.
|
||||
host_map: reloadable::HostMapReloadable,
|
||||
|
||||
/// Off-task FMP-encrypt + UDP-send worker pool. Unix-only —
|
||||
/// the worker issues direct sendmmsg(2) / sendmsg+UDP_GSO calls
|
||||
/// on raw fds via `AsRawFd`. None on Windows or when the worker
|
||||
/// pool failed to spawn.
|
||||
#[cfg(unix)]
|
||||
pub(crate) encrypt_workers: Option<encrypt_worker::EncryptWorkerPool>,
|
||||
|
||||
/// Off-task FMP decrypt worker pool — receiver-side mirror of
|
||||
/// `encrypt_workers`. Workers are shards: each owns its session
|
||||
/// state directly in a thread-local `HashMap` (no `RwLock`,
|
||||
/// no `Mutex` per packet). Hash-by-cache-key dispatch.
|
||||
#[cfg(unix)]
|
||||
pub(crate) decrypt_workers: Option<decrypt_worker::DecryptWorkerPool>,
|
||||
|
||||
/// Sessions whose recv cipher + replay window have been handed
|
||||
/// off to a decrypt shard worker. Lookup gate on the hot receive
|
||||
/// path: if the cache-key is in here, dispatch to worker; else
|
||||
@@ -612,7 +570,7 @@ impl Node {
|
||||
|
||||
Ok(Self {
|
||||
context,
|
||||
state: NodeState::Created,
|
||||
supervisor: lifecycle::supervisor::Supervisor::new(),
|
||||
tree_state,
|
||||
bloom_state,
|
||||
coord_cache,
|
||||
@@ -620,7 +578,6 @@ impl Node {
|
||||
transport_drops: HashMap::new(),
|
||||
links: HashMap::new(),
|
||||
addr_to_link: HashMap::new(),
|
||||
packet_tx: None,
|
||||
packet_rx: None,
|
||||
connections: HashMap::new(),
|
||||
peers: HashMap::new(),
|
||||
@@ -643,14 +600,6 @@ impl Node {
|
||||
)),
|
||||
tun_state,
|
||||
tun_name: None,
|
||||
tun_tx: None,
|
||||
tun_outbound_rx: None,
|
||||
tun_reader_handle: None,
|
||||
tun_writer_handle: None,
|
||||
#[cfg(target_os = "macos")]
|
||||
tun_shutdown_fd: None,
|
||||
dns_identity_rx: None,
|
||||
dns_task: None,
|
||||
index_allocator: IndexAllocator::new(),
|
||||
peers_by_index: HashMap::new(),
|
||||
pending_outbound: HashMap::new(),
|
||||
@@ -669,8 +618,6 @@ impl Node {
|
||||
),
|
||||
pending_connects: Vec::new(),
|
||||
retry_pending: HashMap::new(),
|
||||
nostr_rendezvous: crate::nostr::RendezvousDriver::default(),
|
||||
lan_rendezvous: None,
|
||||
last_parent_reeval: None,
|
||||
last_congestion_log: None,
|
||||
estimated_mesh_size: None,
|
||||
@@ -681,10 +628,6 @@ impl Node {
|
||||
host_map,
|
||||
path_mtu_lookup: Arc::new(std::sync::RwLock::new(HashMap::new())),
|
||||
#[cfg(unix)]
|
||||
encrypt_workers: None,
|
||||
#[cfg(unix)]
|
||||
decrypt_workers: None,
|
||||
#[cfg(unix)]
|
||||
decrypt_registered_sessions: std::collections::HashSet::new(),
|
||||
#[cfg(unix)]
|
||||
decrypt_fallback_rx: Some(decrypt_fallback_rx),
|
||||
@@ -773,7 +716,7 @@ impl Node {
|
||||
|
||||
Ok(Self {
|
||||
context,
|
||||
state: NodeState::Created,
|
||||
supervisor: lifecycle::supervisor::Supervisor::new(),
|
||||
tree_state,
|
||||
bloom_state,
|
||||
coord_cache,
|
||||
@@ -781,7 +724,6 @@ impl Node {
|
||||
transport_drops: HashMap::new(),
|
||||
links: HashMap::new(),
|
||||
addr_to_link: HashMap::new(),
|
||||
packet_tx: None,
|
||||
packet_rx: None,
|
||||
connections: HashMap::new(),
|
||||
peers: HashMap::new(),
|
||||
@@ -804,14 +746,6 @@ impl Node {
|
||||
)),
|
||||
tun_state,
|
||||
tun_name: None,
|
||||
tun_tx: None,
|
||||
tun_outbound_rx: None,
|
||||
tun_reader_handle: None,
|
||||
tun_writer_handle: None,
|
||||
#[cfg(target_os = "macos")]
|
||||
tun_shutdown_fd: None,
|
||||
dns_identity_rx: None,
|
||||
dns_task: None,
|
||||
index_allocator: IndexAllocator::new(),
|
||||
peers_by_index: HashMap::new(),
|
||||
pending_outbound: HashMap::new(),
|
||||
@@ -827,8 +761,6 @@ impl Node {
|
||||
lookup: Lookup::new(LookupBackoff::new(), LookupForwardRateLimiter::new()),
|
||||
pending_connects: Vec::new(),
|
||||
retry_pending: HashMap::new(),
|
||||
nostr_rendezvous: crate::nostr::RendezvousDriver::default(),
|
||||
lan_rendezvous: None,
|
||||
last_parent_reeval: None,
|
||||
last_congestion_log: None,
|
||||
estimated_mesh_size: None,
|
||||
@@ -839,10 +771,6 @@ impl Node {
|
||||
host_map,
|
||||
path_mtu_lookup: Arc::new(std::sync::RwLock::new(HashMap::new())),
|
||||
#[cfg(unix)]
|
||||
encrypt_workers: None,
|
||||
#[cfg(unix)]
|
||||
decrypt_workers: None,
|
||||
#[cfg(unix)]
|
||||
decrypt_registered_sessions: std::collections::HashSet::new(),
|
||||
#[cfg(unix)]
|
||||
decrypt_fallback_rx: Some(decrypt_fallback_rx),
|
||||
@@ -1210,7 +1138,7 @@ impl Node {
|
||||
|
||||
/// Get the node state.
|
||||
pub fn state(&self) -> NodeState {
|
||||
self.state
|
||||
self.supervisor.state
|
||||
}
|
||||
|
||||
/// Get the node uptime.
|
||||
@@ -1220,7 +1148,7 @@ impl Node {
|
||||
|
||||
/// Check if node is operational.
|
||||
pub fn is_running(&self) -> bool {
|
||||
self.state.is_operational()
|
||||
self.supervisor.state.is_operational()
|
||||
}
|
||||
|
||||
/// Check if this is a leaf-only node.
|
||||
@@ -1520,7 +1448,7 @@ impl Node {
|
||||
let snapshot = crate::control::snapshot::StatsSnapshot {
|
||||
history: std::sync::Arc::new(self.stats_history.clone()),
|
||||
estimated_mesh_size: self.estimated_mesh_size,
|
||||
state: self.state,
|
||||
state: self.supervisor.state,
|
||||
tun_state: self.tun_state,
|
||||
tun_name: self.tun_name.clone(),
|
||||
effective_ipv6_mtu: self.effective_ipv6_mtu(),
|
||||
@@ -2269,7 +2197,11 @@ impl Node {
|
||||
}
|
||||
|
||||
pub(crate) fn cleanup_bootstrap_transport_if_unused(&mut self, transport_id: TransportId) {
|
||||
if !self.nostr_rendezvous.is_bootstrap_transport(&transport_id) {
|
||||
if !self
|
||||
.supervisor
|
||||
.nostr_rendezvous
|
||||
.is_bootstrap_transport(&transport_id)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2299,7 +2231,8 @@ impl Node {
|
||||
"bootstrap transport has no remaining references; dropping"
|
||||
);
|
||||
|
||||
self.nostr_rendezvous
|
||||
self.supervisor
|
||||
.nostr_rendezvous
|
||||
.remove_bootstrap_transport(&transport_id);
|
||||
self.transport_drops.remove(&transport_id);
|
||||
self.transports.remove(&transport_id);
|
||||
@@ -2376,7 +2309,7 @@ impl Node {
|
||||
/// Used by control queries (`show_peers` per-peer Nostr-traversal
|
||||
/// state) to read failure-state without taking shared ownership.
|
||||
pub fn nostr_rendezvous_handle(&self) -> Option<&crate::nostr::NostrRendezvous> {
|
||||
self.nostr_rendezvous.engine()
|
||||
self.supervisor.nostr_rendezvous.engine()
|
||||
}
|
||||
|
||||
/// Iterate over all peer node IDs.
|
||||
@@ -2669,7 +2602,7 @@ impl Node {
|
||||
///
|
||||
/// Returns None if TUN is not active or the node hasn't been started.
|
||||
pub fn tun_tx(&self) -> Option<&TunTx> {
|
||||
self.tun_tx.as_ref()
|
||||
self.supervisor.tun_tx.as_ref()
|
||||
}
|
||||
|
||||
// === Sending ===
|
||||
@@ -2765,7 +2698,7 @@ impl Node {
|
||||
{
|
||||
let send_cipher_opt = session.send_cipher_clone();
|
||||
if let Some(fmp_cipher) = send_cipher_opt
|
||||
&& let Some(workers) = self.encrypt_workers.as_ref().cloned()
|
||||
&& let Some(workers) = self.supervisor.encrypt_workers.as_ref().cloned()
|
||||
&& let Some(transport) = self.transports.get(&transport_id)
|
||||
&& let TransportHandle::Udp(udp) = transport
|
||||
&& let Some(socket) = udp.async_socket()
|
||||
@@ -2999,7 +2932,7 @@ impl fmt::Debug for Node {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Node")
|
||||
.field("node_addr", self.node_addr())
|
||||
.field("state", &self.state)
|
||||
.field("state", &self.supervisor.state)
|
||||
.field("is_leaf_only", &self.is_leaf_only())
|
||||
.field("connections", &self.connection_count())
|
||||
.field("peers", &self.peer_count())
|
||||
|
||||
+2
-2
@@ -282,7 +282,7 @@ impl Node {
|
||||
// evicts if the relay has nothing, otherwise leaves it. Cheap
|
||||
// (one Filter fetch with 2s timeout) and bounded by the retry
|
||||
// backoff cadence.
|
||||
if let Some(bootstrap) = self.nostr_rendezvous.engine_arc() {
|
||||
if let Some(bootstrap) = self.supervisor.nostr_rendezvous.engine_arc() {
|
||||
let _ = bootstrap
|
||||
.refetch_advert_for_stale_check(&peer_config.npub)
|
||||
.await;
|
||||
@@ -318,7 +318,7 @@ impl Node {
|
||||
// entry expires. Force a re-fetch so the next retry tick
|
||||
// picks up fresh endpoints.
|
||||
if matches!(e, NodeError::NoTransportForType(_))
|
||||
&& let Some(bootstrap) = self.nostr_rendezvous.engine_arc()
|
||||
&& let Some(bootstrap) = self.supervisor.nostr_rendezvous.engine_arc()
|
||||
{
|
||||
let npub = peer_config.npub.clone();
|
||||
tokio::spawn(async move {
|
||||
|
||||
+18
-18
@@ -24,17 +24,17 @@ async fn test_adopted_udp_traversal_completes_handshake() {
|
||||
let (packet_tx_a, packet_rx_a) = packet_channel(64);
|
||||
let (packet_tx_b, packet_rx_b) = packet_channel(64);
|
||||
|
||||
node_a.packet_tx = Some(packet_tx_a.clone());
|
||||
node_a.supervisor.packet_tx = Some(packet_tx_a.clone());
|
||||
node_a.packet_rx = Some(packet_rx_a);
|
||||
node_a.state = NodeState::Running;
|
||||
node_a.supervisor.state = NodeState::Running;
|
||||
|
||||
let mut transport_b = UdpTransport::new(transport_id_b, None, udp_config, packet_tx_b.clone());
|
||||
transport_b.start_async().await.unwrap();
|
||||
|
||||
let addr_b = transport_b.local_addr().unwrap();
|
||||
node_b.packet_tx = Some(packet_tx_b.clone());
|
||||
node_b.supervisor.packet_tx = Some(packet_tx_b.clone());
|
||||
node_b.packet_rx = Some(packet_rx_b);
|
||||
node_b.state = NodeState::Running;
|
||||
node_b.supervisor.state = NodeState::Running;
|
||||
node_b
|
||||
.transports
|
||||
.insert(transport_id_b, TransportHandle::Udp(transport_b));
|
||||
@@ -91,9 +91,9 @@ async fn test_adopted_udp_traversal_completes_handshake() {
|
||||
async fn test_failed_adopted_traversal_cleans_up_transport() {
|
||||
let mut node = make_node();
|
||||
let (packet_tx, packet_rx) = packet_channel(64);
|
||||
node.packet_tx = Some(packet_tx);
|
||||
node.supervisor.packet_tx = Some(packet_tx);
|
||||
node.packet_rx = Some(packet_rx);
|
||||
node.state = NodeState::Running;
|
||||
node.supervisor.state = NodeState::Running;
|
||||
node.index_allocator = IndexAllocator::with_max_attempts(0);
|
||||
|
||||
let peer = make_node();
|
||||
@@ -121,9 +121,9 @@ async fn test_failed_adopted_traversal_cleans_up_transport() {
|
||||
async fn test_adopted_traversal_skips_already_connected_peer() {
|
||||
let mut node = make_node();
|
||||
let (packet_tx, packet_rx) = packet_channel(64);
|
||||
node.packet_tx = Some(packet_tx);
|
||||
node.supervisor.packet_tx = Some(packet_tx);
|
||||
node.packet_rx = Some(packet_rx);
|
||||
node.state = NodeState::Running;
|
||||
node.supervisor.state = NodeState::Running;
|
||||
|
||||
let transport_id = TransportId::new(1);
|
||||
let link_id = LinkId::new(1);
|
||||
@@ -180,17 +180,17 @@ async fn test_third_peer_can_handshake_via_adopted_transport_socket() {
|
||||
let (packet_tx_b, packet_rx_b) = packet_channel(64);
|
||||
let (packet_tx_c, packet_rx_c) = packet_channel(64);
|
||||
|
||||
node_a.packet_tx = Some(packet_tx_a.clone());
|
||||
node_a.supervisor.packet_tx = Some(packet_tx_a.clone());
|
||||
node_a.packet_rx = Some(packet_rx_a);
|
||||
node_a.state = NodeState::Running;
|
||||
node_a.supervisor.state = NodeState::Running;
|
||||
|
||||
node_b.packet_tx = Some(packet_tx_b.clone());
|
||||
node_b.supervisor.packet_tx = Some(packet_tx_b.clone());
|
||||
node_b.packet_rx = Some(packet_rx_b);
|
||||
node_b.state = NodeState::Running;
|
||||
node_b.supervisor.state = NodeState::Running;
|
||||
|
||||
node_c.packet_tx = Some(packet_tx_c.clone());
|
||||
node_c.supervisor.packet_tx = Some(packet_tx_c.clone());
|
||||
node_c.packet_rx = Some(packet_rx_c);
|
||||
node_c.state = NodeState::Running;
|
||||
node_c.supervisor.state = NodeState::Running;
|
||||
|
||||
let mut transport_a = UdpTransport::new(transport_id_a, None, udp_config.clone(), packet_tx_a);
|
||||
transport_a.start_async().await.unwrap();
|
||||
@@ -300,9 +300,9 @@ async fn test_adopted_udp_inherits_mtu_from_single_primary_config() {
|
||||
let mut node = make_node_with(config);
|
||||
|
||||
let (packet_tx, packet_rx) = packet_channel(64);
|
||||
node.packet_tx = Some(packet_tx);
|
||||
node.supervisor.packet_tx = Some(packet_tx);
|
||||
node.packet_rx = Some(packet_rx);
|
||||
node.state = NodeState::Running;
|
||||
node.supervisor.state = NodeState::Running;
|
||||
|
||||
let peer = make_node();
|
||||
let adopted_socket = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
|
||||
@@ -350,9 +350,9 @@ async fn test_adopted_udp_inherits_mtu_from_named_primary_config() {
|
||||
let mut node = make_node_with(config);
|
||||
|
||||
let (packet_tx, packet_rx) = packet_channel(64);
|
||||
node.packet_tx = Some(packet_tx);
|
||||
node.supervisor.packet_tx = Some(packet_tx);
|
||||
node.packet_rx = Some(packet_rx);
|
||||
node.state = NodeState::Running;
|
||||
node.supervisor.state = NodeState::Running;
|
||||
|
||||
let peer = make_node();
|
||||
let adopted_socket = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
|
||||
|
||||
@@ -1095,7 +1095,7 @@ async fn test_check_pending_lookups_default_sequence_unreachable() {
|
||||
|
||||
// Inject a TUN sender so `send_icmpv6_dest_unreachable` is observable.
|
||||
let (tun_tx, tun_rx) = mpsc::channel::<Vec<u8>>();
|
||||
node.tun_tx = Some(tun_tx);
|
||||
node.supervisor.tun_tx = Some(tun_tx);
|
||||
|
||||
// Build a target identity (the unreachable destination).
|
||||
let target_identity = Identity::generate();
|
||||
|
||||
@@ -285,8 +285,8 @@ async fn test_run_rx_loop_handshake() {
|
||||
node_b.packet_rx = Some(packet_rx_b);
|
||||
|
||||
// Set node state to Running (transports need to be operational)
|
||||
node_a.state = NodeState::Running;
|
||||
node_b.state = NodeState::Running;
|
||||
node_a.supervisor.state = NodeState::Running;
|
||||
node_b.supervisor.state = NodeState::Running;
|
||||
|
||||
// === Phase 1: Node A initiates handshake to Node B ===
|
||||
|
||||
|
||||
+10
-10
@@ -657,7 +657,7 @@ async fn test_session_100_nodes() {
|
||||
let mut tun_receivers: Vec<mpsc::Receiver<Vec<u8>>> = Vec::with_capacity(NUM_NODES);
|
||||
for tn in nodes.iter_mut() {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
tn.node.tun_tx = Some(tx);
|
||||
tn.node.supervisor.tun_tx = Some(tx);
|
||||
tun_receivers.push(rx);
|
||||
}
|
||||
|
||||
@@ -1073,7 +1073,7 @@ async fn test_tun_outbound_established_session() {
|
||||
|
||||
// Install TUN receiver on Node 1
|
||||
let (tun_tx, tun_rx) = std::sync::mpsc::channel();
|
||||
nodes[1].node.tun_tx = Some(tun_tx);
|
||||
nodes[1].node.supervisor.tun_tx = Some(tun_tx);
|
||||
|
||||
// Build and inject an IPv6 packet
|
||||
let test_payload = b"data-plane-test-12345";
|
||||
@@ -1117,7 +1117,7 @@ async fn test_tun_outbound_triggers_session_initiation() {
|
||||
|
||||
// Install TUN receiver on Node 1
|
||||
let (tun_tx, tun_rx) = std::sync::mpsc::channel();
|
||||
nodes[1].node.tun_tx = Some(tun_tx);
|
||||
nodes[1].node.supervisor.tun_tx = Some(tun_tx);
|
||||
|
||||
// Build and inject an IPv6 packet (identity cache populated at peer promotion)
|
||||
let test_payload = b"trigger-session-test";
|
||||
@@ -1170,7 +1170,7 @@ async fn test_tun_outbound_unknown_destination() {
|
||||
|
||||
// Install TUN receiver on Node 0 (for ICMPv6 response)
|
||||
let (tun_tx, tun_rx) = std::sync::mpsc::channel();
|
||||
nodes[0].node.tun_tx = Some(tun_tx);
|
||||
nodes[0].node.supervisor.tun_tx = Some(tun_tx);
|
||||
|
||||
let src_fips = crate::FipsAddress::from_node_addr(nodes[0].node.node_addr());
|
||||
|
||||
@@ -1222,7 +1222,7 @@ async fn test_tun_outbound_3node_forwarded() {
|
||||
|
||||
// Install TUN receiver on Node 2
|
||||
let (tun_tx, tun_rx) = std::sync::mpsc::channel();
|
||||
nodes[2].node.tun_tx = Some(tun_tx);
|
||||
nodes[2].node.supervisor.tun_tx = Some(tun_tx);
|
||||
|
||||
// Build and inject an IPv6 packet (triggers session initiation to Node 2)
|
||||
let test_payload = b"forwarded-data-plane";
|
||||
@@ -1267,7 +1267,7 @@ async fn test_tun_outbound_pending_queue_flush() {
|
||||
|
||||
// Install TUN receiver on Node 1
|
||||
let (tun_tx, tun_rx) = std::sync::mpsc::channel();
|
||||
nodes[1].node.tun_tx = Some(tun_tx);
|
||||
nodes[1].node.supervisor.tun_tx = Some(tun_tx);
|
||||
|
||||
// Send 5 packets before any session exists
|
||||
let mut packets = Vec::new();
|
||||
@@ -1918,7 +1918,7 @@ async fn test_tun_outbound_path_mtu_generates_ptb() {
|
||||
|
||||
// Install TUN receiver on source node to capture ICMPv6 PTB
|
||||
let (tun_tx, tun_rx) = std::sync::mpsc::channel();
|
||||
nodes[0].node.tun_tx = Some(tun_tx);
|
||||
nodes[0].node.supervisor.tun_tx = Some(tun_tx);
|
||||
|
||||
// Build an IPv6 packet that fits local MTU but exceeds path MTU
|
||||
let reduced_ipv6_mtu = crate::upper::icmp::effective_ipv6_mtu(reduced_mtu) as usize;
|
||||
@@ -1975,7 +1975,7 @@ async fn test_tun_outbound_path_mtu_generates_ptb() {
|
||||
|
||||
// Verify a packet that fits within path MTU passes through (no PTB)
|
||||
let (tun_tx2, tun_rx2) = std::sync::mpsc::channel();
|
||||
nodes[0].node.tun_tx = Some(tun_tx2);
|
||||
nodes[0].node.supervisor.tun_tx = Some(tun_tx2);
|
||||
let fitting_payload = vec![0u8; reduced_ipv6_mtu - 41]; // fits within path MTU
|
||||
let fitting_packet = build_ipv6_packet(&src_fips, &dst_fips, &fitting_payload);
|
||||
assert!(fitting_packet.len() <= reduced_ipv6_mtu);
|
||||
@@ -2114,7 +2114,7 @@ async fn test_multihop_pmtud_heterogeneous_mtu() {
|
||||
// should check PathMtuState and generate ICMPv6 PTB on TUN instead
|
||||
// of forwarding.
|
||||
let (tun_tx2, tun_rx2) = std::sync::mpsc::channel();
|
||||
nodes[0].node.tun_tx = Some(tun_tx2);
|
||||
nodes[0].node.supervisor.tun_tx = Some(tun_tx2);
|
||||
|
||||
nodes[0].node.handle_tun_outbound(ipv6_packet.clone()).await;
|
||||
|
||||
@@ -2158,7 +2158,7 @@ async fn test_multihop_pmtud_heterogeneous_mtu() {
|
||||
|
||||
// Verify a fitting packet still passes through without PTB
|
||||
let (tun_tx3, tun_rx3) = std::sync::mpsc::channel();
|
||||
nodes[0].node.tun_tx = Some(tun_tx3);
|
||||
nodes[0].node.supervisor.tun_tx = Some(tun_tx3);
|
||||
|
||||
let fitting_payload = vec![0xCDu8; 600 - 40]; // 600-byte IPv6 packet, well within 694
|
||||
let fitting_packet = build_ipv6_packet(&src_fips, &dst_fips, &fitting_payload);
|
||||
|
||||
+17
-11
@@ -56,7 +56,7 @@ async fn test_nat_bootstrap_failure_falls_back_to_direct_udp_address() {
|
||||
let peer_identity = Identity::generate();
|
||||
let mut node = make_node();
|
||||
let (packet_tx, packet_rx) = packet_channel(64);
|
||||
node.packet_tx = Some(packet_tx.clone());
|
||||
node.supervisor.packet_tx = Some(packet_tx.clone());
|
||||
node.packet_rx = Some(packet_rx);
|
||||
|
||||
let transport_id = TransportId::new(1);
|
||||
@@ -102,7 +102,7 @@ async fn test_try_peer_addresses_races_all_concrete_udp_candidates() {
|
||||
let peer_identity = Identity::generate();
|
||||
let mut node = make_node();
|
||||
let (packet_tx, packet_rx) = packet_channel(64);
|
||||
node.packet_tx = Some(packet_tx.clone());
|
||||
node.supervisor.packet_tx = Some(packet_tx.clone());
|
||||
node.packet_rx = Some(packet_rx);
|
||||
|
||||
let transport_id = TransportId::new(1);
|
||||
@@ -1050,7 +1050,7 @@ async fn update_peers_races_new_alternative_without_dropping_active_peer() {
|
||||
config.peers = vec![old_peer.clone()];
|
||||
let mut node = make_node_with(config);
|
||||
let (packet_tx, packet_rx) = packet_channel(64);
|
||||
node.packet_tx = Some(packet_tx.clone());
|
||||
node.supervisor.packet_tx = Some(packet_tx.clone());
|
||||
node.packet_rx = Some(packet_rx);
|
||||
|
||||
let transport_id = TransportId::new(1);
|
||||
@@ -1130,7 +1130,9 @@ async fn test_nostr_traversal_failure_skips_connected_peer() {
|
||||
peer_config: crate::config::PeerConfig::new(peer_identity.npub(), "udp", "127.0.0.1:9"),
|
||||
reason: "stale traversal failure".to_string(),
|
||||
});
|
||||
node.nostr_rendezvous.set_engine(bootstrap.clone());
|
||||
node.supervisor
|
||||
.nostr_rendezvous
|
||||
.set_engine(bootstrap.clone());
|
||||
|
||||
node.poll_nostr_rendezvous().await;
|
||||
|
||||
@@ -1170,7 +1172,9 @@ async fn test_nostr_traversal_established_skips_connected_peer() {
|
||||
socket,
|
||||
),
|
||||
});
|
||||
node.nostr_rendezvous.set_engine(bootstrap.clone());
|
||||
node.supervisor
|
||||
.nostr_rendezvous
|
||||
.set_engine(bootstrap.clone());
|
||||
|
||||
node.poll_nostr_rendezvous().await;
|
||||
|
||||
@@ -1424,7 +1428,7 @@ async fn test_transport_mtu_returns_min_across_operational() {
|
||||
// iteration order. This is the core ISSUE-2026-0011 regression test.
|
||||
let mut node = make_node();
|
||||
let (packet_tx, packet_rx) = packet_channel(64);
|
||||
node.packet_tx = Some(packet_tx);
|
||||
node.supervisor.packet_tx = Some(packet_tx);
|
||||
node.packet_rx = Some(packet_rx);
|
||||
|
||||
let udp1 = make_udp_transport_with_mtu(1, 1497).await;
|
||||
@@ -1461,7 +1465,7 @@ async fn test_transport_mtu_min_with_single_operational() {
|
||||
// operational.
|
||||
let mut node = make_node();
|
||||
let (packet_tx, packet_rx) = packet_channel(64);
|
||||
node.packet_tx = Some(packet_tx);
|
||||
node.supervisor.packet_tx = Some(packet_tx);
|
||||
node.packet_rx = Some(packet_rx);
|
||||
|
||||
let udp = make_udp_transport_with_mtu(1, 1452).await;
|
||||
@@ -1484,7 +1488,7 @@ async fn test_transport_mtu_min_with_single_operational() {
|
||||
async fn test_seed_path_mtu_inserts_when_empty() {
|
||||
let mut node = make_node();
|
||||
let (packet_tx, packet_rx) = packet_channel(64);
|
||||
node.packet_tx = Some(packet_tx);
|
||||
node.supervisor.packet_tx = Some(packet_tx);
|
||||
node.packet_rx = Some(packet_rx);
|
||||
|
||||
let udp = make_udp_transport_with_mtu(1, 1452).await;
|
||||
@@ -1517,7 +1521,7 @@ async fn test_seed_path_mtu_inserts_when_empty() {
|
||||
async fn test_seed_path_mtu_keeps_tighter_existing_value() {
|
||||
let mut node = make_node();
|
||||
let (packet_tx, packet_rx) = packet_channel(64);
|
||||
node.packet_tx = Some(packet_tx);
|
||||
node.supervisor.packet_tx = Some(packet_tx);
|
||||
node.packet_rx = Some(packet_rx);
|
||||
|
||||
let udp = make_udp_transport_with_mtu(1, 1452).await;
|
||||
@@ -1557,7 +1561,7 @@ async fn test_seed_path_mtu_keeps_tighter_existing_value() {
|
||||
async fn test_seed_path_mtu_tightens_looser_existing_value() {
|
||||
let mut node = make_node();
|
||||
let (packet_tx, packet_rx) = packet_channel(64);
|
||||
node.packet_tx = Some(packet_tx);
|
||||
node.supervisor.packet_tx = Some(packet_tx);
|
||||
node.packet_rx = Some(packet_rx);
|
||||
|
||||
let udp = make_udp_transport_with_mtu(1, 1280).await;
|
||||
@@ -1727,7 +1731,9 @@ async fn poll_nostr_rendezvous_established_gated_at_capacity() {
|
||||
socket,
|
||||
),
|
||||
});
|
||||
node.nostr_rendezvous.set_engine(bootstrap.clone());
|
||||
node.supervisor
|
||||
.nostr_rendezvous
|
||||
.set_engine(bootstrap.clone());
|
||||
|
||||
let before_peers = node.peer_count();
|
||||
let before_links = node.link_count();
|
||||
|
||||
Reference in New Issue
Block a user