Skip to main content

rns_net/interface/
backbone.rs

1//! Backbone TCP mesh interface using cross-platform polling.
2//!
3//! Server mode: listens on a TCP port, accepts peer connections, spawns
4//! dynamic per-peer interfaces. Uses a single poll thread to multiplex
5//! all client sockets. HDLC framing for packet boundaries.
6//!
7//! Client mode: connects to a remote backbone server, single TCP connection
8//! with HDLC framing. Reconnects on disconnect.
9//!
10//! Matches Python `BackboneInterface.py`.
11
12use std::collections::HashMap;
13use std::hash::{BuildHasher, Hasher};
14use std::io;
15#[cfg(test)]
16use std::io::Write;
17use std::net::{IpAddr, Shutdown, TcpListener, TcpStream, ToSocketAddrs};
18use std::os::unix::io::AsRawFd;
19use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
20use std::sync::{Arc, Mutex, OnceLock};
21use std::thread;
22use std::time::{Duration, Instant};
23
24use polling::Event as PollEvent;
25use socket2::{SockRef, TcpKeepalive};
26
27use rns_core::constants;
28use rns_core::transport::types::{IngressControlConfig, InterfaceId, InterfaceInfo};
29
30use crate::event::{Event, EventSender};
31use crate::event::{LoopContext, LoopSource, SourceStatus};
32use crate::hdlc;
33use crate::interface::loop_writer::LoopEgress;
34use crate::interface::tcp::{read_stream_frames, StreamRead};
35use crate::interface::transmit_buffer::{
36    EgressController, TransmitBuffer, EGRESS_DEAD_TIME, EGRESS_HIGH_WATERMARK,
37};
38use crate::interface::{
39    lock_or_recover, wait_while_active, DynamicInterfaceTemplate, EgressControl,
40    InterfaceConfigData, InterfaceFactory, ListenerControl, StartContext, StartResult, Writer,
41};
42use crate::BackbonePeerStateEntry;
43
44/// Upstream listener bitrate estimate used for accepted peers.
45pub(crate) const SERVER_BITRATE_GUESS: u64 = 100_000_000;
46/// Standalone Backbone client bitrate estimate.
47pub(crate) const CLIENT_BITRATE_GUESS: u64 = 100_000_000;
48
49const fn auto_mtu_for_bitrate(bitrate: u64) -> Option<u32> {
50    if bitrate >= 1_000_000_000 {
51        Some(524_288)
52    } else if bitrate >= 750_000_000 {
53        Some(262_144)
54    } else if bitrate >= 400_000_000 {
55        Some(131_072)
56    } else if bitrate >= 200_000_000 {
57        Some(65_536)
58    } else if bitrate >= 100_000_000 {
59        Some(32_768)
60    } else if bitrate >= 10_000_000 {
61        Some(16_384)
62    } else if bitrate >= 5_000_000 {
63        Some(8_192)
64    } else if bitrate >= 2_000_000 {
65        Some(4_096)
66    } else if bitrate >= 1_000_000 {
67        Some(2_048)
68    } else if bitrate >= 62_500 {
69        Some(1_024)
70    } else {
71        None
72    }
73}
74
75/// Effective frame size selected by upstream autoconfiguration at 100 Mbps.
76pub(crate) const HW_MTU: usize = match auto_mtu_for_bitrate(CLIENT_BITRATE_GUESS) {
77    Some(mtu) => mtu as usize,
78    None => 1_048_576,
79};
80/// MTU selected for listener-accepted peers at the listener bitrate estimate.
81pub(crate) const SERVER_PEER_MTU: u32 = match auto_mtu_for_bitrate(SERVER_BITRATE_GUESS) {
82    Some(mtu) => mtu,
83    None => 1_048_576,
84};
85
86const DP_IC_HIGH_WM_PCT: usize = 90;
87const DP_IC_MID_WM_PCT: usize = 68;
88const DP_IC_LOW_WM_PCT: usize = 10;
89const DP_IC_INTERVAL: Duration = Duration::from_millis(250);
90const DP_IC_RCVBUF: usize = 32_768;
91const DP_IC_IF_HEADROOM: usize = 32;
92const DP_IC_PENALTY: f64 = 1.5;
93
94/// Configuration for a backbone interface.
95#[derive(Debug, Clone)]
96pub struct BackboneConfig {
97    pub name: String,
98    pub listen_ip: String,
99    pub listen_port: u16,
100    pub interface_id: InterfaceId,
101    pub mode: u8,
102    pub gravity: i64,
103    pub recursive_prs: bool,
104    pub announces_from_internal: bool,
105    pub announces_to_internal: Option<bool>,
106    pub max_connections: Option<usize>,
107    pub idle_timeout: Option<Duration>,
108    pub write_stall_timeout: Option<Duration>,
109    pub abuse: BackboneAbuseConfig,
110    pub fast_flap: BackboneFastFlapConfig,
111    pub fast_flap_state: Arc<Mutex<BackboneFastFlapMonitor>>,
112    pub ingress_control: IngressControlConfig,
113    pub runtime: Arc<Mutex<BackboneServerRuntime>>,
114    pub peer_state: Arc<Mutex<BackbonePeerMonitor>>,
115}
116
117/// Configurable behavior-based abuse detection for inbound peers.
118#[derive(Debug, Clone, Default)]
119pub struct BackboneAbuseConfig {
120    pub max_penalty_duration: Option<Duration>,
121}
122
123/// Upstream-compatible automatic blocking for repeatedly short-lived inbound
124/// Backbone connections.
125#[derive(Debug, Clone, Copy, Eq, PartialEq)]
126pub struct BackboneFastFlapConfig {
127    pub enabled: bool,
128    pub connection_threshold: Duration,
129    pub grace: u64,
130    pub block_duration: Duration,
131}
132
133impl Default for BackboneFastFlapConfig {
134    fn default() -> Self {
135        Self {
136            enabled: true,
137            connection_threshold: Duration::from_secs(20),
138            grace: 5,
139            block_duration: Duration::from_secs(12 * 60 * 60),
140        }
141    }
142}
143
144#[derive(Debug, Clone)]
145struct FastFlapPeerState {
146    first_flap_at: Instant,
147    last_flap_at: Instant,
148    flap_count: u64,
149}
150
151/// Result of recording one short-lived connection.
152#[derive(Debug, Clone, Copy, Eq, PartialEq)]
153pub struct FastFlapRecord {
154    pub flap_count: u64,
155    pub blocked: bool,
156    pub newly_blocked: bool,
157    pub blocked_until: Option<Instant>,
158    pub elapsed_since_first: Duration,
159}
160
161/// Process-wide fast-flap history. Policy remains per listener, while the IP
162/// history is shared like upstream's class-level `fast_flapping` table.
163#[derive(Debug, Default)]
164pub struct BackboneFastFlapMonitor {
165    peers: HashMap<IpAddr, FastFlapPeerState>,
166}
167
168impl BackboneFastFlapMonitor {
169    pub fn new() -> Self {
170        Self::default()
171    }
172
173    pub fn record_disconnect(
174        &mut self,
175        peer_ip: IpAddr,
176        connected_for: Duration,
177        config: &BackboneFastFlapConfig,
178        now: Instant,
179    ) -> Option<FastFlapRecord> {
180        if !config.enabled || connected_for >= config.connection_threshold {
181            return None;
182        }
183
184        self.expire_peer(peer_ip, config, now);
185        let state = self.peers.entry(peer_ip).or_insert(FastFlapPeerState {
186            first_flap_at: now,
187            last_flap_at: now,
188            flap_count: 0,
189        });
190        let was_blocked = state.flap_count > config.grace;
191        state.last_flap_at = now;
192        state.flap_count = state.flap_count.saturating_add(1);
193        let blocked = state.flap_count > config.grace;
194
195        Some(FastFlapRecord {
196            flap_count: state.flap_count,
197            blocked,
198            newly_blocked: blocked && !was_blocked,
199            blocked_until: blocked
200                .then(|| now.checked_add(config.block_duration))
201                .flatten(),
202            elapsed_since_first: now.saturating_duration_since(state.first_flap_at),
203        })
204    }
205
206    pub fn is_blocked(
207        &mut self,
208        peer_ip: IpAddr,
209        config: &BackboneFastFlapConfig,
210        now: Instant,
211    ) -> bool {
212        self.blocked_until(peer_ip, config, now).is_some()
213    }
214
215    pub fn blocked_until(
216        &mut self,
217        peer_ip: IpAddr,
218        config: &BackboneFastFlapConfig,
219        now: Instant,
220    ) -> Option<Instant> {
221        if !config.enabled {
222            return None;
223        }
224        self.expire_peer(peer_ip, config, now);
225        self.peers
226            .get(&peer_ip)
227            .filter(|state| state.flap_count > config.grace)
228            .and_then(|state| state.last_flap_at.checked_add(config.block_duration))
229    }
230
231    pub fn blocked_ip_count(&mut self, config: &BackboneFastFlapConfig, now: Instant) -> usize {
232        if !config.enabled {
233            return 0;
234        }
235        self.expire_all(config, now);
236        self.peers
237            .values()
238            .filter(|state| state.flap_count > config.grace)
239            .count()
240    }
241
242    pub fn blocked_ip_list(
243        &mut self,
244        config: &BackboneFastFlapConfig,
245        now: Instant,
246    ) -> Vec<IpAddr> {
247        if !config.enabled {
248            return Vec::new();
249        }
250        self.expire_all(config, now);
251        let mut blocked: Vec<_> = self
252            .peers
253            .iter()
254            .filter(|(_, state)| state.flap_count > config.grace)
255            .map(|(peer_ip, _)| *peer_ip)
256            .collect();
257        blocked.sort_unstable();
258        blocked
259    }
260
261    pub fn flap_count(&self, peer_ip: IpAddr) -> u64 {
262        self.peers
263            .get(&peer_ip)
264            .map(|state| state.flap_count)
265            .unwrap_or(0)
266    }
267
268    pub fn clear(&mut self, peer_ip: IpAddr) -> bool {
269        self.peers.remove(&peer_ip).is_some()
270    }
271
272    fn expire_peer(&mut self, peer_ip: IpAddr, config: &BackboneFastFlapConfig, now: Instant) {
273        let expired = self.peers.get(&peer_ip).is_some_and(|state| {
274            now.saturating_duration_since(state.last_flap_at) > config.block_duration
275        });
276        if expired {
277            self.peers.remove(&peer_ip);
278        }
279    }
280
281    fn expire_all(&mut self, config: &BackboneFastFlapConfig, now: Instant) {
282        self.peers.retain(|_, state| {
283            now.saturating_duration_since(state.last_flap_at) <= config.block_duration
284        });
285    }
286}
287
288fn shared_fast_flap_monitor() -> Arc<Mutex<BackboneFastFlapMonitor>> {
289    static MONITOR: OnceLock<Arc<Mutex<BackboneFastFlapMonitor>>> = OnceLock::new();
290    Arc::clone(MONITOR.get_or_init(|| Arc::new(Mutex::new(BackboneFastFlapMonitor::new()))))
291}
292
293/// Live runtime state for a backbone server interface.
294#[derive(Debug, Clone)]
295pub struct BackboneServerRuntime {
296    pub max_connections: Option<usize>,
297    pub idle_timeout: Option<Duration>,
298    pub write_stall_timeout: Option<Duration>,
299    pub abuse: BackboneAbuseConfig,
300}
301
302impl BackboneServerRuntime {
303    pub fn from_config(config: &BackboneConfig) -> Self {
304        Self {
305            max_connections: config.max_connections,
306            idle_timeout: config.idle_timeout,
307            write_stall_timeout: config.write_stall_timeout,
308            abuse: config.abuse.clone(),
309        }
310    }
311}
312
313#[derive(Debug, Clone)]
314pub struct BackboneRuntimeConfigHandle {
315    pub interface_name: String,
316    pub runtime: Arc<Mutex<BackboneServerRuntime>>,
317    pub startup: BackboneServerRuntime,
318}
319
320#[derive(Debug, Clone)]
321pub struct BackbonePeerStateHandle {
322    pub interface_id: InterfaceId,
323    pub interface_name: String,
324    pub mode: u8,
325    pub gravity: i64,
326    pub announces_to_internal: Option<bool>,
327    pub peer_state: Arc<Mutex<BackbonePeerMonitor>>,
328    pub fast_flap: BackboneFastFlapConfig,
329    pub fast_flap_state: Arc<Mutex<BackboneFastFlapMonitor>>,
330}
331
332impl Default for BackboneConfig {
333    fn default() -> Self {
334        let mut config = BackboneConfig {
335            name: String::new(),
336            listen_ip: "0.0.0.0".into(),
337            listen_port: 0,
338            interface_id: InterfaceId(0),
339            mode: constants::MODE_FULL,
340            gravity: 0,
341            recursive_prs: false,
342            announces_from_internal: true,
343            announces_to_internal: None,
344            max_connections: None,
345            idle_timeout: None,
346            write_stall_timeout: None,
347            abuse: BackboneAbuseConfig::default(),
348            fast_flap: BackboneFastFlapConfig::default(),
349            fast_flap_state: shared_fast_flap_monitor(),
350            ingress_control: IngressControlConfig::enabled(),
351            runtime: Arc::new(Mutex::new(BackboneServerRuntime {
352                max_connections: None,
353                idle_timeout: None,
354                write_stall_timeout: None,
355                abuse: BackboneAbuseConfig::default(),
356            })),
357            peer_state: Arc::new(Mutex::new(BackbonePeerMonitor::new())),
358        };
359        let startup = BackboneServerRuntime::from_config(&config);
360        config.runtime = Arc::new(Mutex::new(startup));
361        config
362    }
363}
364
365/// Writer that sends HDLC-framed data over a cloned TCP stream (server mode).
366///
367/// The server poller owns only readable interest on the original stream. This
368/// independent writer therefore never enables or re-arms writable poll events;
369/// an already-pending buffer is retried directly before accepting another
370/// frame.
371struct BackboneWriter {
372    stream: TcpStream,
373    runtime: Arc<Mutex<BackboneServerRuntime>>,
374    interface_name: String,
375    interface_id: InterfaceId,
376    event_tx: EventSender,
377    transmit_buffer: TransmitBuffer,
378    egress_controller: EgressController,
379    egress_control: EgressControl,
380    egress_started: Instant,
381    stall_started: Option<Instant>,
382    disconnect_notified: bool,
383    write_stall_flag: Arc<AtomicBool>,
384}
385
386impl Writer for BackboneWriter {
387    fn send_frame_confirmed(&mut self, data: &[u8]) -> io::Result<()> {
388        // The async writer has already reserved this frame's byte budget.
389        // Once admitted, retain it even if the egress gate subsequently closes.
390        if !self
391            .transmit_buffer
392            .append_with_limit(hdlc::frame(data), Some(EGRESS_HIGH_WATERMARK))
393        {
394            return Err(io::Error::new(
395                io::ErrorKind::InvalidInput,
396                "confirmed frame exceeds backbone buffer",
397            ));
398        }
399        self.transmit_buffer.flush();
400        let timeout = lock_or_recover(&self.runtime, "backbone runtime").write_stall_timeout;
401        self.drain_transmit(timeout)
402    }
403    fn send_frame(&mut self, data: &[u8]) -> io::Result<()> {
404        self.send_frames(&[crate::interface::FrameBytes::Owned(data.to_vec())])
405    }
406
407    fn send_frames(&mut self, frames: &[crate::interface::FrameBytes]) -> io::Result<()> {
408        let write_stall_timeout =
409            lock_or_recover(&self.runtime, "backbone runtime").write_stall_timeout;
410        if self.transmit_buffer.buffered_bytes() > 0 {
411            self.drain_transmit(write_stall_timeout)?;
412            if self.transmit_buffer.buffered_bytes() > 0 {
413                return Err(io::Error::new(
414                    io::ErrorKind::WouldBlock,
415                    "backbone writer still stalled",
416                ));
417            }
418        }
419
420        let elapsed = self.egress_started.elapsed();
421        let _ = self.egress_controller.evaluate(
422            elapsed,
423            self.transmit_buffer.buffered_bytes(),
424            self.transmit_buffer.sendable_bytes(),
425            self.transmit_buffer.total_sent_bytes(),
426        );
427        self.egress_control
428            .set_stalled(self.egress_controller.stalled());
429        for frame in frames {
430            let encoded = hdlc::frame(frame);
431            let encoded_len = encoded.len();
432            if self.egress_control.stalled()
433                || !self
434                    .transmit_buffer
435                    .append_with_limit(encoded, Some(EGRESS_HIGH_WATERMARK))
436            {
437                self.egress_control.record_drop(encoded_len);
438                log::debug!(
439                    "[{}:{}] egress control dropped outbound frame of {} bytes",
440                    self.interface_name,
441                    self.interface_id.0,
442                    encoded_len
443                );
444            }
445        }
446        self.transmit_buffer.flush();
447        self.drain_transmit(write_stall_timeout)
448    }
449
450    fn flush(&mut self) -> io::Result<()> {
451        let timeout = lock_or_recover(&self.runtime, "backbone runtime").write_stall_timeout;
452        self.drain_transmit(timeout)
453    }
454
455    fn egress_control(&self) -> Option<EgressControl> {
456        Some(self.egress_control.clone())
457    }
458
459    fn loop_socket(&self) -> Option<std::os::fd::RawFd> {
460        Some(self.stream.as_raw_fd())
461    }
462
463    fn loop_egress(&mut self) -> Option<LoopEgress> {
464        let runtime = Arc::clone(&self.runtime);
465        Some(
466            LoopEgress::budget(self.egress_control.clone()).with_stall_watch(
467                move || lock_or_recover(&runtime, "backbone runtime").write_stall_timeout,
468                Arc::clone(&self.write_stall_flag),
469            ),
470        )
471    }
472
473    fn shutdown(&mut self) {
474        let _ = self.stream.shutdown(Shutdown::Both);
475    }
476}
477
478impl BackboneWriter {
479    fn drain_transmit(&mut self, write_stall_timeout: Option<Duration>) -> io::Result<()> {
480        if self.transmit_buffer.buffered_bytes() == 0 {
481            return Ok(());
482        }
483
484        let written = self.transmit_buffer.drain_to(&mut self.stream)?;
485        if written > 0 {
486            self.stall_started = None;
487        }
488        let elapsed = self.egress_started.elapsed();
489        let dead = self.egress_controller.evaluate(
490            elapsed,
491            self.transmit_buffer.buffered_bytes(),
492            self.transmit_buffer.sendable_bytes(),
493            self.transmit_buffer.total_sent_bytes(),
494        );
495        self.egress_control
496            .set_stalled(self.egress_controller.stalled());
497        if dead {
498            return Err(self.disconnect_for_write_stall(EGRESS_DEAD_TIME));
499        }
500        if self.transmit_buffer.buffered_bytes() == 0 {
501            return Ok(());
502        }
503
504        let now = Instant::now();
505        let started = self.stall_started.get_or_insert(now);
506        let timeout = write_stall_timeout.unwrap_or(EGRESS_DEAD_TIME);
507        if now.duration_since(*started) >= timeout {
508            return Err(self.disconnect_for_write_stall(timeout));
509        }
510        Err(io::Error::new(
511            io::ErrorKind::WouldBlock,
512            "backbone writer would block",
513        ))
514    }
515
516    fn disconnect_for_write_stall(&mut self, timeout: Duration) -> io::Error {
517        if !self.disconnect_notified {
518            log::warn!(
519                "[{}] backbone client {} disconnected due to write stall timeout ({:?})",
520                self.interface_name,
521                self.interface_id.0,
522                timeout
523            );
524            self.write_stall_flag.store(true, Ordering::Relaxed);
525            let _ = self.stream.shutdown(Shutdown::Both);
526            let _ = self.event_tx.send(Event::InterfaceDown(self.interface_id));
527            self.disconnect_notified = true;
528        }
529        io::Error::new(
530            io::ErrorKind::TimedOut,
531            format!("backbone writer stalled for {:?}", timeout),
532        )
533    }
534}
535
536/// Start a backbone interface. Binds the TCP listener, which is served
537/// with its peers on the receiving (driver) thread.
538pub fn start(config: BackboneConfig, tx: EventSender, next_id: Arc<AtomicU64>) -> io::Result<()> {
539    start_with_template(config, tx, next_id, None, None)
540}
541
542fn start_with_template(
543    config: BackboneConfig,
544    tx: EventSender,
545    next_id: Arc<AtomicU64>,
546    dynamic_template: Option<DynamicInterfaceTemplate>,
547    underlay_mark: Option<u32>,
548) -> io::Result<()> {
549    let addr = format!("{}:{}", config.listen_ip, config.listen_port);
550    let listener = TcpListener::bind(&addr)?;
551    super::apply_underlay_mark(listener.as_raw_fd(), underlay_mark)?;
552    listener.set_nonblocking(true)?;
553
554    log::info!(
555        "[{}] backbone server listening on {}",
556        config.name,
557        listener
558            .local_addr()
559            .unwrap_or_else(|_| std::net::SocketAddr::from(([0, 0, 0, 0], config.listen_port)))
560    );
561
562    let ifac_size = dynamic_template
563        .as_ref()
564        .and_then(|template| template.ifac.as_ref())
565        .map(|ifac| ifac.size)
566        .unwrap_or(0);
567    // Queue capacities are finalized before interfaces start. Derive the
568    // ingress controller's watermarks once, before any listener can read.
569    let ingress_thresholds = ingress_thresholds(tx.inbound_queue_snapshot().capacities[0]);
570    let server = Arc::new(Server {
571        name: config.name.clone(),
572        server_interface_id: config.interface_id,
573        tx: tx.clone(),
574        next_id,
575        runtime: Arc::clone(&config.runtime),
576        peer_state: Arc::clone(&config.peer_state),
577        fast_flap: config.fast_flap,
578        fast_flap_state: Arc::clone(&config.fast_flap_state),
579        ingress_control: config.ingress_control,
580        accepted_peer_mode: config.mode,
581        accepted_peer_gravity: config.gravity,
582        accepted_peer_recursive_prs: config.recursive_prs,
583        accepted_peer_announces_from_internal: config.announces_from_internal,
584        accepted_peer_announces_to_internal: config.announces_to_internal,
585        dynamic_template,
586        ifac_size,
587        underlay_mark,
588        ingress_thresholds,
589        state: Mutex::new(ServerState {
590            clients: HashMap::new(),
591            peers: HashMap::new(),
592            ingress_snapshot_at: Instant::now(),
593        }),
594    });
595    tx.register_source(
596        crate::interface::loop_writer::read_key(config.interface_id),
597        Box::new(ListenerSource {
598            listener,
599            server,
600            next_interval: Instant::now() + DP_IC_INTERVAL,
601        }),
602    )
603}
604
605/// Connections accepted per readiness before other sources get a turn.
606const ACCEPT_BURST: usize = 64;
607
608/// A listener's configuration and the state its peers share. The listener
609/// and all its peers are served on the same thread, so `state` is never
610/// contended.
611struct Server {
612    name: String,
613    server_interface_id: InterfaceId,
614    tx: EventSender,
615    next_id: Arc<AtomicU64>,
616    runtime: Arc<Mutex<BackboneServerRuntime>>,
617    peer_state: Arc<Mutex<BackbonePeerMonitor>>,
618    fast_flap: BackboneFastFlapConfig,
619    fast_flap_state: Arc<Mutex<BackboneFastFlapMonitor>>,
620    ingress_control: IngressControlConfig,
621    accepted_peer_mode: u8,
622    accepted_peer_gravity: i64,
623    accepted_peer_recursive_prs: bool,
624    accepted_peer_announces_from_internal: bool,
625    accepted_peer_announces_to_internal: Option<bool>,
626    dynamic_template: Option<DynamicInterfaceTemplate>,
627    ifac_size: usize,
628    underlay_mark: Option<u32>,
629    ingress_thresholds: IngressThresholds,
630    state: Mutex<ServerState>,
631}
632
633struct ServerState {
634    /// Connected peers by poller key.
635    clients: HashMap<usize, ClientState>,
636    peers: HashMap<IpAddr, PeerBehaviorState>,
637    ingress_snapshot_at: Instant,
638}
639
640/// Per-client tracking state.
641struct ClientState {
642    id: InterfaceId,
643    peer_ip: IpAddr,
644    peer_port: u16,
645    /// The peer's socket, owned by its source, which removes this entry
646    /// before the socket is closed.
647    fd: std::os::fd::RawFd,
648    connected_at: Instant,
649    has_received_data: bool,
650    write_stall_flag: Arc<AtomicBool>,
651    /// The idle timeout shut the socket down; the peer source reports it
652    /// once it reads the end of the stream.
653    idle_timed_out: bool,
654    dp_ingress_bytes: u64,
655    dp_ingress_packets: u64,
656    dp_ingress_hold: Option<Instant>,
657    dp_ingress_gated: bool,
658    dp_ingress_throttle_count: u64,
659}
660
661impl Server {
662    fn lock(&self) -> std::sync::MutexGuard<'_, ServerState> {
663        lock_or_recover(&self.state, "backbone server state")
664    }
665
666    /// Take blacklist changes made through management and expire what is
667    /// due, then publish the merged view.
668    fn sync_peer_state(&self, state: &mut ServerState) {
669        let mut monitor = lock_or_recover(&self.peer_state, "backbone peer state");
670        monitor.sync_into(&mut state.peers);
671        cleanup_peer_state(&mut state.peers);
672        monitor.upsert_snapshot(&state.peers);
673    }
674
675    /// Watch a peer for readability again, or stop watching it.
676    fn set_readable(&self, key: usize, fd: std::os::fd::RawFd, readable: bool) {
677        // SAFETY: `fd` belongs to a registered peer source (see `ClientState`).
678        let source = unsafe { std::os::fd::BorrowedFd::borrow_raw(fd) };
679        let interest = if readable {
680            PollEvent::readable(key)
681        } else {
682            PollEvent::none(key)
683        };
684        if let Err(error) = self.tx.poller().modify(source, interest) {
685            log::warn!(
686                "[{}] cannot update peer read interest: {}",
687                self.name,
688                error
689            );
690        }
691    }
692
693    /// Stop reading the peer that produced the most packets this sample
694    /// for a hold proportional to the sample span. Returns its key.
695    fn throttle_busiest_ingress(&self, state: &mut ServerState) -> Option<usize> {
696        let key = busiest_ingress_key(
697            state
698                .clients
699                .iter()
700                .map(|(key, client)| (*key, client.dp_ingress_packets)),
701        )?;
702        let allocation_divisor = state.clients.len().max(DP_IC_IF_HEADROOM) as f64 * DP_IC_PENALTY;
703        let sample_span = state.ingress_snapshot_at.elapsed();
704        let client = state.clients.get_mut(&key)?;
705        if client.dp_ingress_gated {
706            return None;
707        }
708        let hold = Duration::from_secs_f64(
709            (sample_span.as_secs_f64().max(0.001) * allocation_divisor).max(0.001),
710        );
711        client.dp_ingress_gated = true;
712        client.dp_ingress_hold = Some(Instant::now() + hold);
713        client.dp_ingress_throttle_count = client.dp_ingress_throttle_count.saturating_add(1);
714        log::info!(
715            "ingress throttled on BackboneInterface/{} for {:.3}s",
716            client.id.0,
717            hold.as_secs_f64(),
718        );
719        Some(key)
720    }
721
722    /// Resume reading one gated peer whose hold has passed.
723    fn release_one_ingress(&self, state: &mut ServerState, now: Instant) {
724        let Some((key, client)) = state.clients.iter_mut().find(|(_, client)| {
725            client.dp_ingress_gated
726                && client
727                    .dp_ingress_hold
728                    .is_none_or(|hold_until| now >= hold_until)
729        }) else {
730            return;
731        };
732        client.dp_ingress_gated = false;
733        client.dp_ingress_hold = None;
734        log::info!(
735            "released ingress throttle on BackboneInterface/{}",
736            client.id.0
737        );
738        self.set_readable(*key, client.fd, true);
739    }
740
741    /// The listener's periodic work: ingress throttling and idle timeouts.
742    fn interval(&self) {
743        let mut state = self.lock();
744        let state = &mut *state;
745        self.sync_peer_state(state);
746        let now = Instant::now();
747        if now.duration_since(state.ingress_snapshot_at) >= DP_IC_INTERVAL {
748            let height = self.tx.inbound_queue_snapshot().heights[0];
749            if height > self.ingress_thresholds.mid {
750                if let Some(key) = self.throttle_busiest_ingress(state) {
751                    self.set_readable(key, state.clients[&key].fd, false);
752                }
753            } else if height < self.ingress_thresholds.low {
754                self.release_one_ingress(state, now);
755            }
756            for client in state.clients.values_mut() {
757                client.dp_ingress_bytes = 0;
758                client.dp_ingress_packets = 0;
759            }
760            state.ingress_snapshot_at = now;
761        }
762
763        let idle_timeout = lock_or_recover(&self.runtime, "backbone runtime").idle_timeout;
764        if let Some(timeout) = idle_timeout {
765            for (key, client) in state.clients.iter_mut() {
766                if client.has_received_data
767                    || client.idle_timed_out
768                    || now.duration_since(client.connected_at) < timeout
769                {
770                    continue;
771                }
772                // The peer source reports the disconnect when it reads the
773                // end of the stream, gated or not.
774                client.idle_timed_out = true;
775                // SAFETY: the descriptor is open (see `ClientState`).
776                unsafe { libc::shutdown(client.fd, libc::SHUT_RDWR) };
777                self.set_readable(*key, client.fd, true);
778            }
779        }
780    }
781
782    /// Accept one connection; returns `false` once the driver is gone.
783    fn admit(
784        self: &Arc<Self>,
785        state: &mut ServerState,
786        stream: TcpStream,
787        peer_addr: std::net::SocketAddr,
788        cx: &mut LoopContext<'_>,
789    ) -> bool {
790        let name = &self.name;
791        if let Err(error) = super::apply_underlay_mark(stream.as_raw_fd(), self.underlay_mark) {
792            log::error!(
793                "[{}] failed to mark accepted underlay socket: {}",
794                name,
795                error
796            );
797            return true;
798        }
799        let peer_ip = peer_addr.ip();
800        let peer_port = peer_addr.port();
801
802        let fast_flap_blocked_until = lock_or_recover(
803            &self.fast_flap_state,
804            "backbone fast-flap state",
805        )
806        .blocked_until(peer_ip, &self.fast_flap, Instant::now());
807        if is_ip_blacklisted(&mut state.peers, peer_ip) || fast_flap_blocked_until.is_some() {
808            let peer = state
809                .peers
810                .entry(peer_ip)
811                .or_insert_with(PeerBehaviorState::new);
812            if let Some(blocked_until) = fast_flap_blocked_until {
813                peer.blacklisted_until = Some(blocked_until);
814                peer.blacklist_reason = Some("fast flapping".into());
815            }
816            peer.reject_count = peer.reject_count.saturating_add(1);
817            lock_or_recover(&self.peer_state, "backbone peer state").upsert_snapshot(&state.peers);
818            log::trace!(target: crate::logging::PATHING_LOG_TARGET,
819                "[{}] rejecting blocked peer {}",
820                name,
821                peer_addr,
822            );
823            return true;
824        }
825
826        let max_connections = lock_or_recover(&self.runtime, "backbone runtime").max_connections;
827        if let Some(max) = max_connections {
828            if state.clients.len() >= max {
829                log::warn!(
830                    "[{}] max connections ({}) reached, rejecting {}",
831                    name,
832                    max,
833                    peer_addr
834                );
835                return true;
836            }
837        }
838
839        stream.set_nonblocking(true).ok();
840        stream.set_nodelay(true).ok();
841        set_tcp_keepalive(&stream).ok();
842        SockRef::from(&stream)
843            .set_recv_buffer_size(DP_IC_RCVBUF)
844            .ok();
845
846        // Prevent SIGPIPE on macOS when writing to broken pipes
847        #[cfg(target_os = "macos")]
848        {
849            let sock = SockRef::from(&stream);
850            sock.set_nosigpipe(true).ok();
851        }
852
853        // Create writer via try_clone (cross-platform dup)
854        let writer_stream = match stream.try_clone() {
855            Ok(s) => s,
856            Err(e) => {
857                log::warn!("[{}] failed to clone client stream: {}", name, e);
858                return true;
859            }
860        };
861        let client_id = InterfaceId(self.next_id.fetch_add(1, Ordering::Relaxed));
862        let key = crate::interface::loop_writer::read_key(client_id);
863
864        log::trace!(target: crate::logging::PATHING_LOG_TARGET,
865            "[{}] backbone client connected: {} → id {}",
866            name,
867            peer_addr,
868            client_id.0
869        );
870
871        let write_stall_flag = Arc::new(AtomicBool::new(false));
872        let writer: Box<dyn Writer> = Box::new(BackboneWriter {
873            stream: writer_stream,
874            runtime: Arc::clone(&self.runtime),
875            interface_name: name.clone(),
876            interface_id: client_id,
877            event_tx: self.tx.clone(),
878            transmit_buffer: TransmitBuffer::new(),
879            egress_controller: EgressController::default(),
880            egress_control: EgressControl::new(EGRESS_HIGH_WATERMARK),
881            egress_started: Instant::now(),
882            stall_started: None,
883            disconnect_notified: false,
884            write_stall_flag: Arc::clone(&write_stall_flag),
885        });
886
887        state.clients.insert(
888            key,
889            ClientState {
890                id: client_id,
891                peer_ip,
892                peer_port,
893                fd: stream.as_raw_fd(),
894                connected_at: Instant::now(),
895                has_received_data: false,
896                write_stall_flag,
897                idle_timed_out: false,
898                dp_ingress_bytes: 0,
899                dp_ingress_packets: 0,
900                dp_ingress_hold: None,
901                dp_ingress_gated: false,
902                dp_ingress_throttle_count: 0,
903            },
904        );
905        state
906            .peers
907            .entry(peer_ip)
908            .or_insert_with(PeerBehaviorState::new)
909            .connected_count += 1;
910        lock_or_recover(&self.peer_state, "backbone peer state").upsert_snapshot(&state.peers);
911
912        let info = InterfaceInfo {
913            id: client_id,
914            name: format!("BackboneInterface/{}", client_id.0),
915            mode: self.accepted_peer_mode,
916            gravity: self.accepted_peer_gravity,
917            recursive_prs: self.accepted_peer_recursive_prs,
918            announces_from_internal: self.accepted_peer_announces_from_internal,
919            announces_to_internal: self.accepted_peer_announces_to_internal,
920            out_capable: true,
921            in_capable: true,
922            bitrate: Some(SERVER_BITRATE_GUESS),
923            airtime_profile: None,
924            announce_rate_target: None,
925            announce_rate_grace: 0,
926            announce_rate_penalty: 0.0,
927            announce_cap: constants::ANNOUNCE_CAP,
928            is_local_client: false,
929            wants_tunnel: false,
930            tunnel_id: None,
931            mtu: SERVER_PEER_MTU,
932            ia_freq: 0.0,
933            ip_freq: 0.0,
934            op_freq: 0.0,
935            op_samples: 0,
936            started: 0.0,
937            ingress_control: self.ingress_control,
938        };
939        let up = if let Some(template) = &self.dynamic_template {
940            Event::DynamicInterfaceUp {
941                id: client_id,
942                writer,
943                registration: Box::new(template.registration(info)),
944            }
945        } else {
946            Event::InterfaceUp(client_id, Some(writer), Some(Box::new(info)))
947        };
948        let connected = Event::BackbonePeerConnected {
949            server_interface_id: self.server_interface_id,
950            peer_interface_id: client_id,
951            peer_ip,
952            peer_port,
953        };
954        if self.tx.push_from_loop([connected, up]).is_err() {
955            state.clients.remove(&key);
956            return false;
957        }
958        cx.add_source(
959            key,
960            Box::new(PeerSource {
961                stream,
962                decoder: hdlc::Decoder::reticulum(HW_MTU, self.ifac_size),
963                key,
964                id: client_id,
965                server: Arc::clone(self),
966            }),
967        );
968        true
969    }
970}
971
972#[derive(Clone, Copy, Debug, Eq, PartialEq)]
973struct IngressThresholds {
974    high: usize,
975    immediate: usize,
976    mid: usize,
977    low: usize,
978}
979
980fn ingress_thresholds(data_capacity: usize) -> IngressThresholds {
981    // Keep initialization silent; only actual throttle/release transitions are
982    // operational events. Upstream removed its accidental critical HWM log.
983    let high = ((DP_IC_HIGH_WM_PCT * data_capacity) / 100).max(4);
984    IngressThresholds {
985        high,
986        immediate: high.max(128),
987        mid: ((DP_IC_MID_WM_PCT * data_capacity) / 100).max(2),
988        low: (DP_IC_LOW_WM_PCT * data_capacity) / 100,
989    }
990}
991
992fn busiest_ingress_key(producers: impl Iterator<Item = (usize, u64)>) -> Option<usize> {
993    producers
994        .filter(|(_, packets)| *packets > 0)
995        .max_by_key(|(_, packets)| *packets)
996        .map(|(key, _)| key)
997}
998
999#[derive(Debug, Clone)]
1000struct PeerBehaviorState {
1001    blacklisted_until: Option<Instant>,
1002    blacklist_reason: Option<String>,
1003    reject_count: u64,
1004    connected_count: usize,
1005}
1006
1007impl PeerBehaviorState {
1008    fn new() -> Self {
1009        Self {
1010            blacklisted_until: None,
1011            blacklist_reason: None,
1012            reject_count: 0,
1013            connected_count: 0,
1014        }
1015    }
1016}
1017
1018#[derive(Debug, Clone, Default)]
1019pub struct BackbonePeerMonitor {
1020    peers: HashMap<IpAddr, PeerBehaviorState>,
1021}
1022
1023impl BackbonePeerMonitor {
1024    pub fn new() -> Self {
1025        Self {
1026            peers: HashMap::new(),
1027        }
1028    }
1029
1030    fn upsert_snapshot(&mut self, peers: &HashMap<IpAddr, PeerBehaviorState>) {
1031        let mut merged = self.peers.clone();
1032
1033        for (peer_ip, state) in peers {
1034            let entry = merged
1035                .entry(*peer_ip)
1036                .or_insert_with(PeerBehaviorState::new);
1037            entry.connected_count = state.connected_count;
1038            entry.reject_count = state.reject_count;
1039            entry.blacklisted_until = state.blacklisted_until;
1040            entry.blacklist_reason = state.blacklist_reason.clone();
1041        }
1042
1043        merged.retain(|peer_ip, state| {
1044            peers.contains_key(peer_ip)
1045                || state.blacklisted_until.is_some()
1046                || state.reject_count > 0
1047        });
1048        self.peers = merged;
1049    }
1050
1051    fn sync_into(&self, peers: &mut HashMap<IpAddr, PeerBehaviorState>) {
1052        for (peer_ip, state) in &self.peers {
1053            let entry = peers.entry(*peer_ip).or_insert_with(PeerBehaviorState::new);
1054            entry.blacklisted_until = state.blacklisted_until;
1055            entry.blacklist_reason = state.blacklist_reason.clone();
1056            entry.reject_count = state.reject_count;
1057        }
1058
1059        peers.retain(|peer_ip, state| {
1060            if state.connected_count > 0 {
1061                return true;
1062            }
1063            self.peers.contains_key(peer_ip)
1064        });
1065    }
1066
1067    pub fn list(&self, interface_name: &str) -> Vec<BackbonePeerStateEntry> {
1068        let now = Instant::now();
1069        let mut entries: Vec<BackbonePeerStateEntry> = self
1070            .peers
1071            .iter()
1072            .map(|(peer_ip, state)| BackbonePeerStateEntry {
1073                interface_name: interface_name.to_string(),
1074                peer_ip: *peer_ip,
1075                connected_count: state.connected_count,
1076                blacklisted_remaining_secs: state
1077                    .blacklisted_until
1078                    .and_then(|until| (until > now).then(|| (until - now).as_secs_f64())),
1079                blacklist_reason: state.blacklist_reason.clone(),
1080                reject_count: state.reject_count,
1081            })
1082            .collect();
1083        entries.sort_by_key(|a| a.peer_ip);
1084        entries
1085    }
1086
1087    pub fn clear(&mut self, peer_ip: IpAddr) -> bool {
1088        self.peers.remove(&peer_ip).is_some()
1089    }
1090
1091    pub fn blacklist(&mut self, peer_ip: IpAddr, duration: Duration, reason: String) -> bool {
1092        let state = self
1093            .peers
1094            .entry(peer_ip)
1095            .or_insert_with(PeerBehaviorState::new);
1096        state.blacklisted_until = Some(Instant::now() + duration);
1097        state.blacklist_reason = Some(reason);
1098        true
1099    }
1100
1101    #[cfg(test)]
1102    pub fn seed_entry(&mut self, entry: BackbonePeerStateEntry) {
1103        let mut state = PeerBehaviorState::new();
1104        state.connected_count = entry.connected_count;
1105        state.reject_count = entry.reject_count;
1106        state.blacklist_reason = entry.blacklist_reason;
1107        if let Some(remaining) = entry.blacklisted_remaining_secs {
1108            state.blacklisted_until = Some(Instant::now() + Duration::from_secs_f64(remaining));
1109        }
1110        self.peers.insert(entry.peer_ip, state);
1111    }
1112}
1113
1114#[derive(Clone, Copy)]
1115enum DisconnectReason {
1116    RemoteClosed,
1117    IdleTimeout,
1118    WriteStall,
1119}
1120
1121/// The listening socket, served on the driver thread: accepts peers and
1122/// runs the server's periodic work from its timer.
1123struct ListenerSource {
1124    listener: TcpListener,
1125    server: Arc<Server>,
1126    next_interval: Instant,
1127}
1128
1129impl LoopSource for ListenerSource {
1130    fn fd(&self) -> std::os::fd::RawFd {
1131        self.listener.as_raw_fd()
1132    }
1133
1134    fn on_ready(&mut self, cx: &mut LoopContext<'_>) -> SourceStatus {
1135        let server = &self.server;
1136        let mut state = server.lock();
1137        server.sync_peer_state(&mut state);
1138        for _ in 0..ACCEPT_BURST {
1139            match self.listener.accept() {
1140                Ok((stream, peer_addr)) => {
1141                    if !server.admit(&mut state, stream, peer_addr, cx) {
1142                        return SourceStatus::Remove;
1143                    }
1144                }
1145                Err(error) => match super::tcp_server::accept_error_step(&error) {
1146                    super::tcp_server::AcceptStep::Drained => return SourceStatus::Rearm,
1147                    super::tcp_server::AcceptStep::Next => {}
1148                    super::tcp_server::AcceptStep::Pause => {
1149                        log::warn!("[{}] accept error, pausing accepts: {}", server.name, error);
1150                        return SourceStatus::Pause;
1151                    }
1152                },
1153            }
1154        }
1155        SourceStatus::Rearm
1156    }
1157
1158    fn deadline(&self) -> Option<Instant> {
1159        Some(self.next_interval)
1160    }
1161
1162    fn on_timer(&mut self, _cx: &mut LoopContext<'_>) -> SourceStatus {
1163        self.server.interval();
1164        self.next_interval = Instant::now() + DP_IC_INTERVAL;
1165        SourceStatus::Rearm
1166    }
1167}
1168
1169/// One accepted peer, read on the driver thread.
1170struct PeerSource {
1171    stream: TcpStream,
1172    decoder: hdlc::Decoder,
1173    key: usize,
1174    id: InterfaceId,
1175    server: Arc<Server>,
1176}
1177
1178impl LoopSource for PeerSource {
1179    fn fd(&self) -> std::os::fd::RawFd {
1180        self.stream.as_raw_fd()
1181    }
1182
1183    fn on_ready(&mut self, cx: &mut LoopContext<'_>) -> SourceStatus {
1184        let server = Arc::clone(&self.server);
1185        if server
1186            .lock()
1187            .clients
1188            .get(&self.key)
1189            .is_some_and(|client| client.dp_ingress_gated)
1190        {
1191            // Throttled: the server re-arms it when the hold ends.
1192            return SourceStatus::Hold;
1193        }
1194        let (mut bytes, mut frames) = (0u64, 0u64);
1195        let read = read_stream_frames(
1196            self.stream.as_raw_fd(),
1197            &mut self.decoder,
1198            cx,
1199            &server.tx,
1200            self.id,
1201            &server.name,
1202            |read_bytes, read_frames| {
1203                bytes += read_bytes as u64;
1204                frames += read_frames as u64;
1205            },
1206        );
1207        let mut state = server.lock();
1208        if bytes > 0 {
1209            if let Some(client) = state.clients.get_mut(&self.key) {
1210                client.has_received_data = true;
1211                client.dp_ingress_bytes = client.dp_ingress_bytes.saturating_add(bytes);
1212                client.dp_ingress_packets = client.dp_ingress_packets.saturating_add(frames);
1213            }
1214        }
1215        let status = match read {
1216            StreamRead::Rearm => SourceStatus::Rearm,
1217            StreamRead::Suspend => SourceStatus::Suspend,
1218            StreamRead::Closed | StreamRead::Failed(_) => {
1219                let reason = match state.clients.get(&self.key) {
1220                    Some(client) if client.idle_timed_out => DisconnectReason::IdleTimeout,
1221                    Some(client) if client.write_stall_flag.load(Ordering::Relaxed) => {
1222                        DisconnectReason::WriteStall
1223                    }
1224                    _ => DisconnectReason::RemoteClosed,
1225                };
1226                self.disconnect(&mut state, reason);
1227                return SourceStatus::Remove;
1228            }
1229            StreamRead::DriverGone => return SourceStatus::Remove,
1230        };
1231        if bytes > 0
1232            && server.tx.inbound_queue_snapshot().heights[0] >= server.ingress_thresholds.immediate
1233        {
1234            if let Some(key) = server.throttle_busiest_ingress(&mut state) {
1235                if key == self.key {
1236                    return SourceStatus::Hold;
1237                }
1238                server.set_readable(key, state.clients[&key].fd, false);
1239            }
1240        }
1241        status
1242    }
1243
1244    fn on_removed(&mut self) {
1245        // Closed without a disconnect (the receiver went away): forget it so
1246        // nothing touches the descriptor once it is closed.
1247        self.server.lock().clients.remove(&self.key);
1248    }
1249
1250    fn on_abandoned(&mut self) {
1251        // The driver was told about this peer; it will never be read.
1252        let server = Arc::clone(&self.server);
1253        let mut state = server.lock();
1254        self.disconnect(&mut state, DisconnectReason::RemoteClosed);
1255    }
1256}
1257
1258impl PeerSource {
1259    fn disconnect(&self, state: &mut ServerState, reason: DisconnectReason) {
1260        let server = &self.server;
1261        let name = &server.name;
1262        let Some(client) = state.clients.remove(&self.key) else {
1263            return;
1264        };
1265        let client_id = client.id;
1266
1267        match reason {
1268            DisconnectReason::RemoteClosed => {
1269                log::trace!(target: crate::logging::PATHING_LOG_TARGET,
1270                    "[{}] backbone client {} disconnected",
1271                    name,
1272                    client_id.0,
1273                );
1274            }
1275            DisconnectReason::IdleTimeout => {
1276                log::info!(
1277                    "[{}] backbone client {} disconnected due to idle timeout",
1278                    name,
1279                    client_id.0
1280                );
1281            }
1282            DisconnectReason::WriteStall => {
1283                // Already logged by the writer that detected the stall.
1284            }
1285        }
1286
1287        let connected_for = client.connected_at.elapsed();
1288        if let Some(record) = lock_or_recover(&server.fast_flap_state, "backbone fast-flap state")
1289            .record_disconnect(
1290                client.peer_ip,
1291                connected_for,
1292                &server.fast_flap,
1293                Instant::now(),
1294            )
1295        {
1296            let frequency = if record.elapsed_since_first.is_zero() {
1297                None
1298            } else {
1299                Some(record.flap_count as f64 / record.elapsed_since_first.as_secs_f64())
1300            };
1301            log::debug!(
1302                "[{}] peer {} fast flapped after {:.3}s ({} flaps{})",
1303                name,
1304                client.peer_ip,
1305                connected_for.as_secs_f64(),
1306                record.flap_count,
1307                frequency
1308                    .map(|value| format!(", {:.3} Hz", value))
1309                    .unwrap_or_default(),
1310            );
1311            if record.blocked {
1312                let peer = state
1313                    .peers
1314                    .entry(client.peer_ip)
1315                    .or_insert_with(PeerBehaviorState::new);
1316                peer.blacklisted_until = record.blocked_until;
1317                peer.blacklist_reason = Some("fast flapping".into());
1318                if record.newly_blocked {
1319                    log::warn!(
1320                        "[{}] blocking further connections from {} due to fast flapping",
1321                        name,
1322                        client.peer_ip,
1323                    );
1324                }
1325            }
1326        }
1327        let mut events = vec![Event::BackbonePeerDisconnected {
1328            server_interface_id: server.server_interface_id,
1329            peer_interface_id: client.id,
1330            peer_ip: client.peer_ip,
1331            peer_port: client.peer_port,
1332            connected_for,
1333            had_received_data: client.has_received_data,
1334        }];
1335        match reason {
1336            DisconnectReason::IdleTimeout => events.push(Event::BackbonePeerIdleTimeout {
1337                server_interface_id: server.server_interface_id,
1338                peer_interface_id: client.id,
1339                peer_ip: client.peer_ip,
1340                peer_port: client.peer_port,
1341                connected_for,
1342            }),
1343            DisconnectReason::WriteStall => events.push(Event::BackbonePeerWriteStall {
1344                server_interface_id: server.server_interface_id,
1345                peer_interface_id: client.id,
1346                peer_ip: client.peer_ip,
1347                peer_port: client.peer_port,
1348                connected_for,
1349            }),
1350            DisconnectReason::RemoteClosed => {}
1351        }
1352
1353        if let Some(peer) = state.peers.get_mut(&client.peer_ip) {
1354            peer.connected_count = peer.connected_count.saturating_sub(1);
1355        }
1356        lock_or_recover(&server.peer_state, "backbone peer state").upsert_snapshot(&state.peers);
1357        // The writer already reported a write stall's interface down.
1358        if !matches!(reason, DisconnectReason::WriteStall) {
1359            events.push(Event::InterfaceDown(client_id));
1360        }
1361        let _ = server.tx.push_from_loop(events);
1362    }
1363}
1364
1365fn cleanup_peer_state(peers: &mut HashMap<IpAddr, PeerBehaviorState>) {
1366    let now = Instant::now();
1367    peers.retain(|_, state| {
1368        if matches!(state.blacklisted_until, Some(until) if now >= until) {
1369            state.blacklisted_until = None;
1370            state.blacklist_reason = None;
1371        }
1372        state.blacklisted_until.is_some() || state.connected_count > 0 || state.reject_count > 0
1373    });
1374}
1375
1376fn is_ip_blacklisted(peers: &mut HashMap<IpAddr, PeerBehaviorState>, peer_ip: IpAddr) -> bool {
1377    let now = Instant::now();
1378    if let Some(state) = peers.get_mut(&peer_ip) {
1379        if let Some(until) = state.blacklisted_until {
1380            if now < until {
1381                return true;
1382            }
1383            state.blacklisted_until = None;
1384        }
1385    }
1386    false
1387}
1388
1389fn set_tcp_keepalive(stream: &TcpStream) -> io::Result<()> {
1390    let sock = SockRef::from(stream);
1391    let mut keepalive = TcpKeepalive::new()
1392        .with_time(Duration::from_secs(5))
1393        .with_interval(Duration::from_secs(2));
1394    #[cfg(any(target_os = "linux", target_os = "macos"))]
1395    {
1396        keepalive = keepalive.with_retries(12);
1397    }
1398    sock.set_tcp_keepalive(&keepalive)
1399}
1400
1401// ---------------------------------------------------------------------------
1402// Client mode
1403// ---------------------------------------------------------------------------
1404
1405/// Configuration for a backbone client interface.
1406#[derive(Debug, Clone)]
1407pub struct BackboneClientConfig {
1408    pub name: String,
1409    pub target_host: String,
1410    pub target_port: u16,
1411    pub interface_id: InterfaceId,
1412    pub reconnect_wait: Duration,
1413    pub max_reconnect_tries: Option<u32>,
1414    pub connect_timeout: Duration,
1415    pub transport_identity: Option<String>,
1416    pub runtime: Arc<Mutex<BackboneClientRuntime>>,
1417}
1418
1419#[derive(Debug, Clone)]
1420pub struct BackboneClientRuntime {
1421    pub reconnect_wait: Duration,
1422    pub max_reconnect_tries: Option<u32>,
1423    pub connect_timeout: Duration,
1424}
1425
1426impl BackboneClientRuntime {
1427    pub fn from_config(config: &BackboneClientConfig) -> Self {
1428        Self {
1429            reconnect_wait: config.reconnect_wait,
1430            max_reconnect_tries: config.max_reconnect_tries,
1431            connect_timeout: config.connect_timeout,
1432        }
1433    }
1434}
1435
1436#[derive(Debug, Clone)]
1437pub struct BackboneClientRuntimeConfigHandle {
1438    pub interface_name: String,
1439    pub runtime: Arc<Mutex<BackboneClientRuntime>>,
1440    pub startup: BackboneClientRuntime,
1441}
1442
1443impl Default for BackboneClientConfig {
1444    fn default() -> Self {
1445        let mut config = BackboneClientConfig {
1446            name: String::new(),
1447            target_host: "127.0.0.1".into(),
1448            target_port: 4242,
1449            interface_id: InterfaceId(0),
1450            reconnect_wait: Duration::from_secs(5),
1451            max_reconnect_tries: None,
1452            connect_timeout: Duration::from_secs(5),
1453            transport_identity: None,
1454            runtime: Arc::new(Mutex::new(BackboneClientRuntime {
1455                reconnect_wait: Duration::from_secs(5),
1456                max_reconnect_tries: None,
1457                connect_timeout: Duration::from_secs(5),
1458            })),
1459        };
1460        let startup = BackboneClientRuntime::from_config(&config);
1461        config.runtime = Arc::new(Mutex::new(startup));
1462        config
1463    }
1464}
1465
1466/// Writer that sends HDLC-framed data over a TCP stream (client mode).
1467struct BackboneClientWriter {
1468    stream: TcpStream,
1469    transmit_buffer: TransmitBuffer,
1470    egress_control: EgressControl,
1471}
1472
1473impl Writer for BackboneClientWriter {
1474    fn send_frame_confirmed(&mut self, data: &[u8]) -> io::Result<()> {
1475        if !self
1476            .transmit_buffer
1477            .append_with_limit(hdlc::frame(data), Some(EGRESS_HIGH_WATERMARK))
1478        {
1479            return Err(io::Error::new(
1480                io::ErrorKind::InvalidInput,
1481                "confirmed frame exceeds backbone buffer",
1482            ));
1483        }
1484        self.transmit_buffer.flush();
1485        while self.transmit_buffer.sendable_bytes() > 0 {
1486            if self.transmit_buffer.drain_to(&mut self.stream)? == 0 {
1487                return Err(io::Error::new(
1488                    io::ErrorKind::WriteZero,
1489                    "backbone writer made no progress",
1490                ));
1491            }
1492        }
1493        Ok(())
1494    }
1495    fn send_frame(&mut self, data: &[u8]) -> io::Result<()> {
1496        self.send_frames(&[crate::interface::FrameBytes::Owned(data.to_vec())])
1497    }
1498
1499    fn send_frames(&mut self, frames: &[crate::interface::FrameBytes]) -> io::Result<()> {
1500        for frame in frames {
1501            let encoded = hdlc::frame(frame);
1502            let encoded_len = encoded.len();
1503            if !self
1504                .transmit_buffer
1505                .append_with_limit(encoded, Some(EGRESS_HIGH_WATERMARK))
1506            {
1507                self.egress_control.record_drop(encoded_len);
1508            }
1509        }
1510        self.transmit_buffer.flush();
1511        while self.transmit_buffer.sendable_bytes() > 0 {
1512            if self.transmit_buffer.drain_to(&mut self.stream)? == 0 {
1513                return Err(io::Error::new(
1514                    io::ErrorKind::WriteZero,
1515                    "backbone client writer made no progress",
1516                ));
1517            }
1518        }
1519        Ok(())
1520    }
1521
1522    fn egress_control(&self) -> Option<EgressControl> {
1523        Some(self.egress_control.clone())
1524    }
1525
1526    fn loop_socket(&self) -> Option<std::os::fd::RawFd> {
1527        Some(self.stream.as_raw_fd())
1528    }
1529
1530    fn loop_egress(&mut self) -> Option<LoopEgress> {
1531        Some(LoopEgress::budget(self.egress_control.clone()))
1532    }
1533}
1534
1535/// Try to connect to the target host:port with timeout.
1536fn try_connect_client(
1537    config: &BackboneClientConfig,
1538    underlay_mark: Option<u32>,
1539) -> io::Result<TcpStream> {
1540    let runtime = lock_or_recover(&config.runtime, "backbone runtime").clone();
1541    let addr_str = format!("{}:{}", config.target_host, config.target_port);
1542    let addr = addr_str
1543        .to_socket_addrs()?
1544        .next()
1545        .ok_or_else(|| io::Error::new(io::ErrorKind::AddrNotAvailable, "no addresses resolved"))?;
1546
1547    #[cfg(target_os = "linux")]
1548    let stream = if underlay_mark.is_some() {
1549        super::connect_tcp_with_options(&addr, None, underlay_mark, runtime.connect_timeout)?
1550    } else {
1551        TcpStream::connect_timeout(&addr, runtime.connect_timeout)?
1552    };
1553    #[cfg(not(target_os = "linux"))]
1554    let stream = TcpStream::connect_timeout(&addr, runtime.connect_timeout)?;
1555    stream.set_nodelay(true)?;
1556    set_tcp_keepalive(&stream).ok();
1557
1558    // Prevent SIGPIPE on macOS when writing to broken pipes
1559    #[cfg(target_os = "macos")]
1560    {
1561        let sock = SockRef::from(&stream);
1562        sock.set_nosigpipe(true).ok();
1563    }
1564
1565    Ok(stream)
1566}
1567
1568/// Connect and start the reader thread. Returns the writer for the driver.
1569pub fn start_client(config: BackboneClientConfig, tx: EventSender) -> io::Result<Box<dyn Writer>> {
1570    start_client_with_ifac(config, tx, 0, None, ListenerControl::new())
1571}
1572
1573pub(crate) fn start_client_with_ifac(
1574    config: BackboneClientConfig,
1575    tx: EventSender,
1576    ifac_size: usize,
1577    underlay_mark: Option<u32>,
1578    control: ListenerControl,
1579) -> io::Result<Box<dyn Writer>> {
1580    let stream = try_connect_client(&config, underlay_mark)?;
1581    let writer_stream = stream.try_clone()?;
1582
1583    let id = config.interface_id;
1584    log::info!(
1585        "[{}] backbone client connected to {}:{}",
1586        config.name,
1587        config.target_host,
1588        config.target_port
1589    );
1590
1591    // Initial connect: writer is None because it's returned directly to the caller
1592    let _ = tx.send(Event::InterfaceUp(id, None, None));
1593
1594    let link = Arc::new(ClientLink {
1595        config,
1596        tx,
1597        ifac_size,
1598        underlay_mark,
1599        control,
1600    });
1601    if let Err(error) = link.serve(stream) {
1602        let _ = link.tx.send(Event::InterfaceDown(id));
1603        return Err(error);
1604    }
1605
1606    Ok(Box::new(BackboneClientWriter {
1607        stream: writer_stream,
1608        transmit_buffer: TransmitBuffer::new(),
1609        egress_control: EgressControl::new(EGRESS_HIGH_WATERMARK),
1610    }))
1611}
1612
1613/// What a client connection needs across reconnects.
1614struct ClientLink {
1615    config: BackboneClientConfig,
1616    tx: EventSender,
1617    ifac_size: usize,
1618    underlay_mark: Option<u32>,
1619    control: ListenerControl,
1620}
1621
1622impl ClientLink {
1623    /// Read `stream` on the driver thread from now on.
1624    fn serve(self: &Arc<Self>, stream: TcpStream) -> io::Result<()> {
1625        let source = ClientSource {
1626            stream,
1627            decoder: hdlc::Decoder::reticulum(HW_MTU, self.ifac_size),
1628            link: Arc::clone(self),
1629        };
1630        self.tx.register_source(
1631            crate::interface::loop_writer::read_key(self.config.interface_id),
1632            Box::new(source),
1633        )
1634    }
1635
1636    /// Report the connection down and reconnect on a thread of its own,
1637    /// which exits once the new connection is served on the driver (or
1638    /// reconnecting gives up or is stopped).
1639    fn connection_lost(self: &Arc<Self>) {
1640        let id = self.config.interface_id;
1641        let _ = self.tx.push_from_loop([Event::InterfaceDown(id)]);
1642        let link = Arc::clone(self);
1643        let spawned = thread::Builder::new()
1644            .name(format!("bb-reconn-{}", id.0))
1645            .spawn(move || link.reconnect_loop());
1646        if let Err(error) = spawned {
1647            log::error!(
1648                "[{}] cannot start reconnecting, giving up: {}",
1649                self.config.name,
1650                error
1651            );
1652        }
1653    }
1654
1655    fn reconnect_loop(self: Arc<Self>) {
1656        loop {
1657            let Some(stream) =
1658                client_reconnect(&self.config, &self.tx, self.underlay_mark, &self.control)
1659            else {
1660                if !self.control.should_stop() {
1661                    log::error!("[{}] reconnection failed, giving up", self.config.name);
1662                }
1663                return;
1664            };
1665            match self.serve(stream) {
1666                Ok(()) => return,
1667                Err(error) => {
1668                    log::warn!(
1669                        "[{}] cannot read the new connection: {}",
1670                        self.config.name,
1671                        error
1672                    );
1673                    let _ = self.tx.send(Event::InterfaceDown(self.config.interface_id));
1674                }
1675            }
1676        }
1677    }
1678}
1679
1680/// The connected client socket, read on the driver thread.
1681struct ClientSource {
1682    stream: TcpStream,
1683    decoder: hdlc::Decoder,
1684    link: Arc<ClientLink>,
1685}
1686
1687impl LoopSource for ClientSource {
1688    fn fd(&self) -> std::os::fd::RawFd {
1689        self.stream.as_raw_fd()
1690    }
1691
1692    fn stopped(&self) -> bool {
1693        self.link.control.should_stop()
1694    }
1695
1696    fn on_ready(&mut self, cx: &mut LoopContext<'_>) -> SourceStatus {
1697        let link = &self.link;
1698        if link.control.should_stop() {
1699            return SourceStatus::Remove;
1700        }
1701        let name = &link.config.name;
1702        match read_stream_frames(
1703            self.stream.as_raw_fd(),
1704            &mut self.decoder,
1705            cx,
1706            &link.tx,
1707            link.config.interface_id,
1708            name,
1709            |_, _| {},
1710        ) {
1711            StreamRead::Rearm => SourceStatus::Rearm,
1712            StreamRead::Suspend => SourceStatus::Suspend,
1713            StreamRead::Closed => {
1714                log::warn!("[{}] connection closed", name);
1715                link.connection_lost();
1716                SourceStatus::Remove
1717            }
1718            StreamRead::Failed(error) => {
1719                log::warn!("[{}] read error: {}", name, error);
1720                link.connection_lost();
1721                SourceStatus::Remove
1722            }
1723            StreamRead::DriverGone => SourceStatus::Remove,
1724        }
1725    }
1726}
1727
1728/// Maximum backoff multiplier: `base_delay * 2^MAX_BACKOFF_SHIFT`.
1729/// With a 5 s base this caps at 5 × 2^6 = 320 s ≈ 5 min.
1730const MAX_BACKOFF_SHIFT: u32 = 6;
1731
1732/// Attempt to reconnect with exponential backoff and jitter.
1733/// Returns the new reader stream on success.
1734/// Sends the new writer to the driver via InterfaceUp event.
1735fn client_reconnect(
1736    config: &BackboneClientConfig,
1737    tx: &EventSender,
1738    underlay_mark: Option<u32>,
1739    control: &ListenerControl,
1740) -> Option<TcpStream> {
1741    let mut attempts = 0u32;
1742    loop {
1743        let runtime = lock_or_recover(&config.runtime, "backbone runtime").clone();
1744
1745        let shift = attempts.min(MAX_BACKOFF_SHIFT);
1746        let backoff = runtime.reconnect_wait * 2u32.pow(shift);
1747        // Add ±25 % jitter to avoid thundering-herd reconnects.
1748        let jitter_range = backoff / 4;
1749        let jitter = if jitter_range.as_nanos() > 0 {
1750            let offset = Duration::from_nanos(
1751                (std::hash::RandomState::new().build_hasher().finish()
1752                    % jitter_range.as_nanos() as u64)
1753                    * 2,
1754            );
1755            if offset > jitter_range {
1756                backoff + (offset - jitter_range)
1757            } else {
1758                backoff - (jitter_range - offset)
1759            }
1760        } else {
1761            backoff
1762        };
1763        if !wait_while_active(control, jitter) {
1764            return None;
1765        }
1766
1767        attempts += 1;
1768
1769        if let Some(max) = runtime.max_reconnect_tries {
1770            if attempts > max {
1771                let _ = tx.send(Event::InterfaceDown(config.interface_id));
1772                return None;
1773            }
1774        }
1775
1776        log::info!(
1777            "[{}] reconnect attempt {} (backoff {:.1}s) ...",
1778            config.name,
1779            attempts,
1780            jitter.as_secs_f64(),
1781        );
1782
1783        match try_connect_client(config, underlay_mark) {
1784            Ok(new_stream) => {
1785                if control.should_stop() {
1786                    return None;
1787                }
1788                let writer_stream = match new_stream.try_clone() {
1789                    Ok(s) => s,
1790                    Err(e) => {
1791                        log::warn!("[{}] failed to clone stream: {}", config.name, e);
1792                        continue;
1793                    }
1794                };
1795                log::info!(
1796                    "[{}] reconnected after {} attempt(s)",
1797                    config.name,
1798                    attempts
1799                );
1800                let new_writer: Box<dyn Writer> = Box::new(BackboneClientWriter {
1801                    stream: writer_stream,
1802                    transmit_buffer: TransmitBuffer::new(),
1803                    egress_control: EgressControl::new(EGRESS_HIGH_WATERMARK),
1804                });
1805                let _ = tx.send(Event::InterfaceUp(
1806                    config.interface_id,
1807                    Some(new_writer),
1808                    None,
1809                ));
1810                return Some(new_stream);
1811            }
1812            Err(e) => {
1813                log::warn!("[{}] reconnect failed: {}", config.name, e);
1814            }
1815        }
1816    }
1817}
1818
1819// ---------------------------------------------------------------------------
1820// Factory
1821// ---------------------------------------------------------------------------
1822
1823/// Internal enum used by [`BackboneInterfaceFactory`] to carry either a
1824/// server or client config through the opaque `InterfaceConfigData` channel.
1825#[derive(Clone)]
1826pub(crate) enum BackboneMode {
1827    Server(BackboneConfig),
1828    Client(BackboneClientConfig, u8),
1829}
1830
1831/// Factory for `BackboneInterface`.
1832///
1833/// If the config params contain `"remote"` or `"target_host"` the interface
1834/// is started in client mode; otherwise it is started as a TCP listener
1835/// (server mode).
1836pub struct BackboneInterfaceFactory;
1837
1838/// Compatibility name used by Reticulum 1.3.8 for a forced client-mode
1839/// Backbone interface.
1840pub struct BackboneClientInterfaceFactory;
1841
1842impl InterfaceFactory for BackboneClientInterfaceFactory {
1843    fn type_name(&self) -> &str {
1844        "BackboneClientInterface"
1845    }
1846
1847    fn parse_config(
1848        &self,
1849        name: &str,
1850        id: InterfaceId,
1851        params: &HashMap<String, String>,
1852    ) -> Result<Box<dyn InterfaceConfigData>, String> {
1853        if !params.contains_key("remote") && !params.contains_key("target_host") {
1854            return Err("BackboneClientInterface requires 'remote' or 'target_host'".into());
1855        }
1856        BackboneInterfaceFactory.parse_config(name, id, params)
1857    }
1858
1859    fn start(
1860        &self,
1861        config: Box<dyn InterfaceConfigData>,
1862        ctx: StartContext,
1863    ) -> io::Result<StartResult> {
1864        BackboneInterfaceFactory.start(config, ctx)
1865    }
1866}
1867
1868fn parse_positive_duration_secs(params: &HashMap<String, String>, key: &str) -> Option<Duration> {
1869    params
1870        .get(key)
1871        .and_then(|v| v.parse::<f64>().ok())
1872        .filter(|v| *v > 0.0)
1873        .and_then(|v| Duration::try_from_secs_f64(v).ok())
1874        .filter(|duration| Instant::now().checked_add(*duration).is_some())
1875}
1876
1877fn parse_nonnegative_duration(
1878    params: &HashMap<String, String>,
1879    key: &str,
1880    multiplier: f64,
1881    default: Duration,
1882) -> Result<Duration, String> {
1883    let Some(raw) = params.get(key) else {
1884        return Ok(default);
1885    };
1886    let value = raw
1887        .parse::<f64>()
1888        .map_err(|_| format!("invalid {key} '{raw}' (expected a non-negative number)"))?;
1889    let seconds = value * multiplier;
1890    if !value.is_finite() || value < 0.0 || !seconds.is_finite() || seconds > u64::MAX as f64 {
1891        return Err(format!(
1892            "invalid {key} '{raw}' (expected a finite non-negative number)"
1893        ));
1894    }
1895    let duration = Duration::from_secs_f64(seconds);
1896    if Instant::now().checked_add(duration).is_none() {
1897        return Err(format!("invalid {key} '{raw}' (duration is too large)"));
1898    }
1899    Ok(duration)
1900}
1901
1902fn parse_fast_flap_config(
1903    params: &HashMap<String, String>,
1904) -> Result<BackboneFastFlapConfig, String> {
1905    let defaults = BackboneFastFlapConfig::default();
1906    let enabled = match params.get("block_fast_flapping") {
1907        Some(raw) => crate::config::parse_bool_pub(raw)
1908            .ok_or_else(|| format!("invalid block_fast_flapping '{raw}' (expected yes or no)"))?,
1909        None => defaults.enabled,
1910    };
1911    let connection_threshold = parse_nonnegative_duration(
1912        params,
1913        "fast_flapping_threshold",
1914        1.0,
1915        defaults.connection_threshold,
1916    )?;
1917    let grace = match params.get("fast_flapping_grace") {
1918        Some(raw) => raw.parse::<u64>().map_err(|_| {
1919            format!("invalid fast_flapping_grace '{raw}' (expected a non-negative integer)")
1920        })?,
1921        None => defaults.grace,
1922    };
1923    let block_duration = parse_nonnegative_duration(
1924        params,
1925        "fast_flapping_block_time",
1926        60.0,
1927        defaults.block_duration,
1928    )?;
1929
1930    Ok(BackboneFastFlapConfig {
1931        enabled,
1932        connection_threshold,
1933        grace,
1934        block_duration,
1935    })
1936}
1937
1938impl InterfaceFactory for BackboneInterfaceFactory {
1939    fn type_name(&self) -> &str {
1940        "BackboneInterface"
1941    }
1942
1943    fn parse_config(
1944        &self,
1945        name: &str,
1946        id: InterfaceId,
1947        params: &HashMap<String, String>,
1948    ) -> Result<Box<dyn InterfaceConfigData>, String> {
1949        if let Some(target_host) = params.get("remote").or_else(|| params.get("target_host")) {
1950            // Client mode
1951            let target_host = target_host.clone();
1952            let target_port = params
1953                .get("target_port")
1954                .or_else(|| params.get("port"))
1955                .and_then(|v| v.parse().ok())
1956                .unwrap_or(4242);
1957            let transport_identity = params.get("transport_identity").cloned();
1958            let priority = match params.get("priority") {
1959                Some(value) => value.parse::<u8>().map_err(|_| {
1960                    format!(
1961                        "invalid Backbone peer priority '{}' (expected 0..100)",
1962                        value
1963                    )
1964                })?,
1965                None => crate::driver::BACKBONE_PEER_POOL_CONFIGURED_DEFAULT_PRIORITY,
1966            };
1967            if priority > 100 {
1968                return Err(format!(
1969                    "invalid Backbone peer priority '{}' (expected 0..100)",
1970                    priority
1971                ));
1972            }
1973            Ok(Box::new(BackboneMode::Client(
1974                BackboneClientConfig {
1975                    name: name.to_string(),
1976                    target_host,
1977                    target_port,
1978                    interface_id: id,
1979                    transport_identity,
1980                    ..BackboneClientConfig::default()
1981                },
1982                priority,
1983            )))
1984        } else {
1985            // Server mode
1986            let listen_ip = params
1987                .get("listen_ip")
1988                .or_else(|| params.get("device"))
1989                .cloned()
1990                .unwrap_or_else(|| "0.0.0.0".into());
1991            let listen_port = params
1992                .get("listen_port")
1993                .or_else(|| params.get("port"))
1994                .and_then(|v| v.parse().ok())
1995                .unwrap_or(4242);
1996            let max_connections = params.get("max_connections").and_then(|v| v.parse().ok());
1997            let idle_timeout = parse_positive_duration_secs(params, "idle_timeout");
1998            let write_stall_timeout = parse_positive_duration_secs(params, "write_stall_timeout");
1999            let abuse = BackboneAbuseConfig {
2000                max_penalty_duration: parse_positive_duration_secs(params, "max_penalty_duration"),
2001            };
2002            let fast_flap = parse_fast_flap_config(params)?;
2003            let mut config = BackboneConfig {
2004                name: name.to_string(),
2005                listen_ip,
2006                listen_port,
2007                interface_id: id,
2008                mode: constants::MODE_FULL,
2009                gravity: 0,
2010                recursive_prs: false,
2011                announces_from_internal: true,
2012                announces_to_internal: None,
2013                max_connections,
2014                idle_timeout,
2015                write_stall_timeout,
2016                abuse,
2017                fast_flap,
2018                fast_flap_state: shared_fast_flap_monitor(),
2019                ingress_control: IngressControlConfig::enabled(),
2020                runtime: Arc::new(Mutex::new(BackboneServerRuntime {
2021                    max_connections: None,
2022                    idle_timeout: None,
2023                    write_stall_timeout: None,
2024                    abuse: BackboneAbuseConfig::default(),
2025                })),
2026                peer_state: Arc::new(Mutex::new(BackbonePeerMonitor::new())),
2027            };
2028            let startup = BackboneServerRuntime::from_config(&config);
2029            config.runtime = Arc::new(Mutex::new(startup));
2030            Ok(Box::new(BackboneMode::Server(config)))
2031        }
2032    }
2033
2034    fn start(
2035        &self,
2036        config: Box<dyn InterfaceConfigData>,
2037        ctx: StartContext,
2038    ) -> io::Result<StartResult> {
2039        let mode = *config.into_any().downcast::<BackboneMode>().map_err(|_| {
2040            io::Error::new(
2041                io::ErrorKind::InvalidData,
2042                "wrong config type for BackboneInterface",
2043            )
2044        })?;
2045
2046        match mode {
2047            BackboneMode::Client(cfg, _) => {
2048                let id = cfg.interface_id;
2049                let name = cfg.name.clone();
2050                let info = InterfaceInfo {
2051                    id,
2052                    name,
2053                    mode: ctx.mode,
2054                    gravity: ctx.gravity,
2055                    recursive_prs: ctx.recursive_prs,
2056                    announces_from_internal: ctx.announces_from_internal,
2057                    announces_to_internal: ctx.announces_to_internal,
2058                    out_capable: true,
2059                    in_capable: true,
2060                    bitrate: Some(CLIENT_BITRATE_GUESS),
2061                    airtime_profile: None,
2062                    announce_rate_target: None,
2063                    announce_rate_grace: 0,
2064                    announce_rate_penalty: 0.0,
2065                    announce_cap: constants::ANNOUNCE_CAP,
2066                    is_local_client: false,
2067                    wants_tunnel: false,
2068                    tunnel_id: None,
2069                    mtu: HW_MTU as u32,
2070                    ingress_control: ctx.ingress_control,
2071                    ia_freq: 0.0,
2072                    ip_freq: 0.0,
2073                    op_freq: 0.0,
2074                    op_samples: 0,
2075                    started: crate::time::now(),
2076                };
2077                let ifac_size = ctx.ifac.as_ref().map(|ifac| ifac.size).unwrap_or(0);
2078                let control = ListenerControl::new();
2079                let writer = start_client_with_ifac(
2080                    cfg,
2081                    ctx.tx,
2082                    ifac_size,
2083                    ctx.underlay_mark,
2084                    control.clone(),
2085                )?;
2086                Ok(StartResult::Simple {
2087                    control: Some(control),
2088                    id,
2089                    info,
2090                    writer,
2091                    interface_type_name: "BackboneInterface".to_string(),
2092                })
2093            }
2094            BackboneMode::Server(mut cfg) => {
2095                cfg.ingress_control = ctx.ingress_control;
2096                cfg.mode = ctx.mode;
2097                cfg.gravity = ctx.gravity;
2098                cfg.recursive_prs = ctx.recursive_prs;
2099                cfg.announces_from_internal = ctx.announces_from_internal;
2100                cfg.announces_to_internal = ctx.announces_to_internal;
2101                let parent_id = cfg.interface_id;
2102                start_with_template(
2103                    cfg,
2104                    ctx.tx,
2105                    ctx.next_dynamic_id,
2106                    Some(DynamicInterfaceTemplate {
2107                        parent_id,
2108                        interface_type: "BackboneInterfacePeer".into(),
2109                        ifac: ctx.ifac,
2110                        mode: ctx.mode,
2111                        gravity: ctx.gravity,
2112                        recursive_prs: ctx.recursive_prs,
2113                        announces_from_internal: ctx.announces_from_internal,
2114                        announces_to_internal: ctx.announces_to_internal,
2115                    }),
2116                    ctx.underlay_mark,
2117                )?;
2118                Ok(StartResult::Listener { control: None })
2119            }
2120        }
2121    }
2122}
2123
2124pub(crate) fn runtime_handle_from_mode(mode: &BackboneMode) -> Option<BackboneRuntimeConfigHandle> {
2125    match mode {
2126        BackboneMode::Server(config) => Some(BackboneRuntimeConfigHandle {
2127            interface_name: config.name.clone(),
2128            runtime: Arc::clone(&config.runtime),
2129            startup: BackboneServerRuntime::from_config(config),
2130        }),
2131        BackboneMode::Client(_, _) => None,
2132    }
2133}
2134
2135pub(crate) fn peer_state_handle_from_mode(mode: &BackboneMode) -> Option<BackbonePeerStateHandle> {
2136    match mode {
2137        BackboneMode::Server(config) => Some(BackbonePeerStateHandle {
2138            interface_id: config.interface_id,
2139            interface_name: config.name.clone(),
2140            mode: config.mode,
2141            gravity: config.gravity,
2142            announces_to_internal: config.announces_to_internal,
2143            peer_state: Arc::clone(&config.peer_state),
2144            fast_flap: config.fast_flap,
2145            fast_flap_state: Arc::clone(&config.fast_flap_state),
2146        }),
2147        BackboneMode::Client(_, _) => None,
2148    }
2149}
2150
2151pub(crate) fn client_runtime_handle_from_mode(
2152    mode: &BackboneMode,
2153) -> Option<BackboneClientRuntimeConfigHandle> {
2154    match mode {
2155        BackboneMode::Client(config, _) => Some(BackboneClientRuntimeConfigHandle {
2156            interface_name: config.name.clone(),
2157            runtime: Arc::clone(&config.runtime),
2158            startup: BackboneClientRuntime::from_config(config),
2159        }),
2160        BackboneMode::Server(_) => None,
2161    }
2162}
2163
2164pub(crate) fn client_config_from_mode(mode: &BackboneMode) -> Option<BackboneClientConfig> {
2165    match mode {
2166        BackboneMode::Client(config, _) => Some(config.clone()),
2167        BackboneMode::Server(_) => None,
2168    }
2169}
2170
2171pub(crate) fn client_priority_from_mode(mode: &BackboneMode) -> Option<u8> {
2172    match mode {
2173        BackboneMode::Client(_, priority) => Some(*priority),
2174        BackboneMode::Server(_) => None,
2175    }
2176}
2177
2178#[cfg(test)]
2179mod tests {
2180    use super::*;
2181    use std::io::Read;
2182    use std::sync::mpsc;
2183    use std::time::Duration;
2184
2185    fn find_free_port() -> u16 {
2186        crate::test_support::port()
2187    }
2188
2189    /// Serve the receiver's sockets and timers for `duration` without
2190    /// taking any event, as a driver busy with other work would.
2191    fn serve_for(rx: &crate::event::EventReceiver, duration: Duration) {
2192        let deadline = Instant::now() + duration;
2193        let mut ready = polling::Events::new();
2194        while Instant::now() < deadline {
2195            ready.clear();
2196            rx.poll_ready(&mut ready).unwrap();
2197            rx.serve_sources(&ready);
2198            thread::sleep(Duration::from_millis(2));
2199        }
2200    }
2201
2202    fn test_fast_flap_config() -> BackboneFastFlapConfig {
2203        BackboneFastFlapConfig {
2204            enabled: true,
2205            connection_threshold: Duration::from_secs(20),
2206            grace: 5,
2207            block_duration: Duration::from_secs(12 * 60 * 60),
2208        }
2209    }
2210
2211    #[test]
2212    fn fast_flap_defaults_match_upstream() {
2213        assert_eq!(BackboneFastFlapConfig::default(), test_fast_flap_config());
2214    }
2215
2216    #[test]
2217    fn sixth_short_connection_blocks_but_fifth_does_not() {
2218        let config = test_fast_flap_config();
2219        let mut monitor = BackboneFastFlapMonitor::new();
2220        let peer: IpAddr = "192.0.2.10".parse().unwrap();
2221        let start = Instant::now();
2222
2223        for flap in 1..=5 {
2224            let record = monitor
2225                .record_disconnect(
2226                    peer,
2227                    Duration::from_secs(19),
2228                    &config,
2229                    start + Duration::from_secs(flap),
2230                )
2231                .unwrap();
2232            assert_eq!(record.flap_count, flap);
2233            assert!(!record.blocked);
2234            assert!(!monitor.is_blocked(peer, &config, start + Duration::from_secs(flap)));
2235        }
2236
2237        let sixth = monitor
2238            .record_disconnect(
2239                peer,
2240                Duration::from_secs(1),
2241                &config,
2242                start + Duration::from_secs(6),
2243            )
2244            .unwrap();
2245        assert_eq!(sixth.flap_count, 6);
2246        assert!(sixth.blocked);
2247        assert!(sixth.newly_blocked);
2248        assert!(monitor.is_blocked(peer, &config, start + Duration::from_secs(6)));
2249    }
2250
2251    #[test]
2252    fn every_short_connection_counts_even_after_receiving_data() {
2253        let config = test_fast_flap_config();
2254        let mut monitor = BackboneFastFlapMonitor::new();
2255        let peer: IpAddr = "192.0.2.11".parse().unwrap();
2256        let now = Instant::now();
2257
2258        let record = monitor
2259            .record_disconnect(peer, Duration::from_secs(2), &config, now)
2260            .unwrap();
2261        assert_eq!(record.flap_count, 1);
2262        assert_eq!(monitor.flap_count(peer), 1);
2263    }
2264
2265    #[test]
2266    fn threshold_boundary_and_long_connections_are_not_counted() {
2267        let config = test_fast_flap_config();
2268        let mut monitor = BackboneFastFlapMonitor::new();
2269        let peer: IpAddr = "192.0.2.12".parse().unwrap();
2270        let now = Instant::now();
2271
2272        assert!(monitor
2273            .record_disconnect(peer, Duration::from_secs(20), &config, now)
2274            .is_none());
2275        assert!(monitor
2276            .record_disconnect(peer, Duration::from_secs(21), &config, now)
2277            .is_none());
2278        assert_eq!(monitor.flap_count(peer), 0);
2279    }
2280
2281    #[test]
2282    fn disabled_fast_flapping_neither_tracks_nor_blocks() {
2283        let mut config = test_fast_flap_config();
2284        config.enabled = false;
2285        let mut monitor = BackboneFastFlapMonitor::new();
2286        let peer: IpAddr = "192.0.2.13".parse().unwrap();
2287        let now = Instant::now();
2288
2289        for offset in 0..10 {
2290            assert!(monitor
2291                .record_disconnect(
2292                    peer,
2293                    Duration::from_secs(1),
2294                    &config,
2295                    now + Duration::from_secs(offset),
2296                )
2297                .is_none());
2298        }
2299        assert_eq!(monitor.flap_count(peer), 0);
2300        assert!(!monitor.is_blocked(peer, &config, now + Duration::from_secs(10)));
2301        assert_eq!(monitor.blocked_ip_count(&config, now), 0);
2302    }
2303
2304    #[test]
2305    fn flap_blocks_expire_strictly_after_configured_duration() {
2306        let mut config = test_fast_flap_config();
2307        config.grace = 0;
2308        config.block_duration = Duration::from_secs(60);
2309        let mut monitor = BackboneFastFlapMonitor::new();
2310        let peer: IpAddr = "192.0.2.14".parse().unwrap();
2311        let now = Instant::now();
2312
2313        monitor
2314            .record_disconnect(peer, Duration::from_secs(1), &config, now)
2315            .unwrap();
2316        assert!(monitor.is_blocked(peer, &config, now + Duration::from_secs(60)));
2317        assert!(!monitor.is_blocked(
2318            peer,
2319            &config,
2320            now + Duration::from_secs(60) + Duration::from_nanos(1)
2321        ));
2322        assert_eq!(monitor.flap_count(peer), 0);
2323    }
2324
2325    #[test]
2326    fn fast_flap_history_is_isolated_by_ip() {
2327        let mut config = test_fast_flap_config();
2328        config.grace = 1;
2329        let mut monitor = BackboneFastFlapMonitor::new();
2330        let first: IpAddr = "192.0.2.15".parse().unwrap();
2331        let second: IpAddr = "192.0.2.16".parse().unwrap();
2332        let now = Instant::now();
2333
2334        for offset in 0..2 {
2335            monitor
2336                .record_disconnect(
2337                    first,
2338                    Duration::from_secs(1),
2339                    &config,
2340                    now + Duration::from_secs(offset),
2341                )
2342                .unwrap();
2343        }
2344        monitor
2345            .record_disconnect(second, Duration::from_secs(1), &config, now)
2346            .unwrap();
2347
2348        assert!(monitor.is_blocked(first, &config, now + Duration::from_secs(2)));
2349        assert!(!monitor.is_blocked(second, &config, now + Duration::from_secs(2)));
2350        assert_eq!(
2351            monitor.blocked_ip_count(&config, now + Duration::from_secs(2)),
2352            1
2353        );
2354        assert_eq!(
2355            monitor.blocked_ip_list(&config, now + Duration::from_secs(2)),
2356            vec![first],
2357            "tracked peers within the grace allowance must not appear as blocked"
2358        );
2359    }
2360
2361    #[test]
2362    fn blocked_ip_list_is_sorted_current_and_empty_when_disabled() {
2363        let mut config = test_fast_flap_config();
2364        config.grace = 0;
2365        config.block_duration = Duration::from_secs(60);
2366        let mut monitor = BackboneFastFlapMonitor::new();
2367        let first: IpAddr = "203.0.113.20".parse().unwrap();
2368        let second: IpAddr = "192.0.2.10".parse().unwrap();
2369        let now = Instant::now();
2370        monitor
2371            .record_disconnect(first, Duration::from_secs(1), &config, now)
2372            .unwrap();
2373        monitor
2374            .record_disconnect(second, Duration::from_secs(1), &config, now)
2375            .unwrap();
2376
2377        assert_eq!(monitor.blocked_ip_list(&config, now), vec![second, first]);
2378        assert!(monitor
2379            .blocked_ip_list(&config, now + Duration::from_secs(61))
2380            .is_empty());
2381        config.enabled = false;
2382        assert!(monitor.blocked_ip_list(&config, now).is_empty());
2383    }
2384
2385    #[test]
2386    fn peer_monitor_snapshot_clears_expired_blacklist_state() {
2387        let peer: IpAddr = "192.0.2.17".parse().unwrap();
2388        let mut monitor = BackbonePeerMonitor::new();
2389        monitor.blacklist(peer, Duration::from_secs(60), "temporary".into());
2390        let mut peers = HashMap::new();
2391        monitor.sync_into(&mut peers);
2392        let state = peers.get_mut(&peer).unwrap();
2393        state.blacklisted_until = None;
2394        state.blacklist_reason = None;
2395
2396        monitor.upsert_snapshot(&peers);
2397
2398        let entry = monitor.list("test").into_iter().next().unwrap();
2399        assert!(entry.blacklisted_remaining_secs.is_none());
2400        assert!(entry.blacklist_reason.is_none());
2401    }
2402
2403    fn recv_non_peer_event(
2404        rx: &crate::event::EventReceiver,
2405        timeout: Duration,
2406    ) -> Result<Event, mpsc::RecvTimeoutError> {
2407        let deadline = Instant::now() + timeout;
2408        loop {
2409            let remaining = deadline.saturating_duration_since(Instant::now());
2410            if remaining.is_zero() {
2411                return Err(mpsc::RecvTimeoutError::Timeout);
2412            }
2413            let event = rx.recv_timeout(remaining)?;
2414            match event {
2415                Event::BackbonePeerConnected { .. }
2416                | Event::BackbonePeerDisconnected { .. }
2417                | Event::BackbonePeerIdleTimeout { .. }
2418                | Event::BackbonePeerWriteStall { .. }
2419                | Event::BackbonePeerPenalty { .. } => continue,
2420                other => return Ok(other),
2421            }
2422        }
2423    }
2424
2425    fn make_server_config(
2426        port: u16,
2427        interface_id: u64,
2428        max_connections: Option<usize>,
2429        idle_timeout: Option<Duration>,
2430        write_stall_timeout: Option<Duration>,
2431        abuse: BackboneAbuseConfig,
2432    ) -> BackboneConfig {
2433        let mut config = BackboneConfig {
2434            name: "test-backbone".into(),
2435            listen_ip: "127.0.0.1".into(),
2436            listen_port: port,
2437            interface_id: InterfaceId(interface_id),
2438            mode: constants::MODE_FULL,
2439            gravity: 0,
2440            recursive_prs: false,
2441            announces_from_internal: true,
2442            announces_to_internal: None,
2443            max_connections,
2444            idle_timeout,
2445            write_stall_timeout,
2446            abuse,
2447            fast_flap: BackboneFastFlapConfig::default(),
2448            fast_flap_state: Arc::new(Mutex::new(BackboneFastFlapMonitor::new())),
2449            ingress_control: IngressControlConfig::enabled(),
2450            runtime: Arc::new(Mutex::new(BackboneServerRuntime {
2451                max_connections: None,
2452                idle_timeout: None,
2453                write_stall_timeout: None,
2454                abuse: BackboneAbuseConfig::default(),
2455            })),
2456            peer_state: Arc::new(Mutex::new(BackbonePeerMonitor::new())),
2457        };
2458        let startup = BackboneServerRuntime::from_config(&config);
2459        config.runtime = Arc::new(Mutex::new(startup));
2460        config
2461    }
2462
2463    #[test]
2464    fn backbone_accept_connection() {
2465        let port = find_free_port();
2466        let (tx, rx) = crate::event::channel();
2467        let next_id = Arc::new(AtomicU64::new(8000));
2468
2469        let config = make_server_config(port, 80, None, None, None, BackboneAbuseConfig::default());
2470
2471        start(config, tx, next_id).unwrap();
2472        thread::sleep(Duration::from_millis(50));
2473
2474        let _client = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
2475
2476        let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
2477        match event {
2478            Event::InterfaceUp(id, writer, info) => {
2479                assert_eq!(id, InterfaceId(8000));
2480                assert!(writer.is_some());
2481                assert!(info.is_some());
2482                let info = info.unwrap();
2483                assert!(info.out_capable);
2484                assert!(info.in_capable);
2485                assert_eq!(info.bitrate, Some(SERVER_BITRATE_GUESS));
2486                assert_eq!(info.mtu, SERVER_PEER_MTU);
2487            }
2488            other => panic!("expected InterfaceUp, got {:?}", other),
2489        }
2490    }
2491
2492    #[test]
2493    fn backbone_class_defaults_preserve_server_and_client_distinction() {
2494        assert_eq!(SERVER_BITRATE_GUESS, 100_000_000);
2495        assert_eq!(SERVER_PEER_MTU, 32_768);
2496        assert_eq!(CLIENT_BITRATE_GUESS, 100_000_000);
2497        assert_eq!(HW_MTU, 32_768);
2498    }
2499
2500    #[test]
2501    fn auto_mtu_thresholds_include_exact_bitrate_boundaries() {
2502        for (bitrate, mtu) in [
2503            (1_000_000_000, Some(524_288)),
2504            (750_000_000, Some(262_144)),
2505            (400_000_000, Some(131_072)),
2506            (200_000_000, Some(65_536)),
2507            (100_000_000, Some(32_768)),
2508            (10_000_000, Some(16_384)),
2509            (5_000_000, Some(8_192)),
2510            (2_000_000, Some(4_096)),
2511            (1_000_000, Some(2_048)),
2512            (62_500, Some(1_024)),
2513            (62_499, None),
2514        ] {
2515            assert_eq!(auto_mtu_for_bitrate(bitrate), mtu, "bitrate {bitrate}");
2516        }
2517    }
2518
2519    #[test]
2520    fn backbone_accepted_connection_inherits_server_mode() {
2521        let port = find_free_port();
2522        let (tx, rx) = crate::event::channel();
2523        let next_id = Arc::new(AtomicU64::new(8050));
2524
2525        let mut config =
2526            make_server_config(port, 85, None, None, None, BackboneAbuseConfig::default());
2527        config.mode = constants::MODE_GATEWAY;
2528
2529        start(config, tx, next_id).unwrap();
2530        thread::sleep(Duration::from_millis(50));
2531
2532        let _client = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
2533
2534        let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
2535        match event {
2536            Event::InterfaceUp(_, _, Some(info)) => {
2537                assert_eq!(info.mode, constants::MODE_GATEWAY);
2538            }
2539            other => panic!("expected InterfaceUp with info, got {:?}", other),
2540        }
2541    }
2542
2543    #[test]
2544    fn dataplane_ingress_thresholds_match_upstream_defaults() {
2545        assert_eq!(
2546            ingress_thresholds(1024),
2547            IngressThresholds {
2548                high: 921,
2549                immediate: 921,
2550                mid: 696,
2551                low: 102,
2552            }
2553        );
2554        assert_eq!(
2555            ingress_thresholds(8),
2556            IngressThresholds {
2557                high: 7,
2558                immediate: 128,
2559                mid: 5,
2560                low: 0,
2561            }
2562        );
2563    }
2564
2565    #[test]
2566    fn dataplane_ingress_selects_the_largest_packet_producer() {
2567        assert_eq!(
2568            busiest_ingress_key([(7, 12), (8, 41), (9, 3)].into_iter()),
2569            Some(8)
2570        );
2571        assert_eq!(busiest_ingress_key([(7, 0), (8, 0)].into_iter()), None);
2572    }
2573
2574    #[test]
2575    fn backbone_gates_the_largest_producer_before_data_queue_overflow() {
2576        let port = find_free_port();
2577        let capacities = crate::event::InboundQueueCapacities {
2578            data: 200,
2579            announce: 8,
2580            path_request: 8,
2581            ingress_limited: 8,
2582        };
2583        let (tx, rx) = crate::event::channel_with_queue_capacities(16, capacities);
2584        let observer = tx.clone();
2585        let next_id = Arc::new(AtomicU64::new(8075));
2586        let config = make_server_config(port, 80, None, None, None, BackboneAbuseConfig::default());
2587
2588        start(config, tx, next_id).unwrap();
2589        thread::sleep(Duration::from_millis(50));
2590        let mut client = TcpStream::connect(format!("127.0.0.1:{port}")).unwrap();
2591        let _ = recv_non_peer_event(&rx, Duration::from_secs(1)).unwrap();
2592
2593        let payload = vec![0u8; 32];
2594        let framed = hdlc::frame(&payload);
2595        let mut burst = Vec::with_capacity(framed.len() * 180);
2596        for _ in 0..180 {
2597            burst.extend_from_slice(&framed);
2598        }
2599        client.write_all(&burst).unwrap();
2600
2601        let deadline = Instant::now() + Duration::from_secs(2);
2602        while observer.inbound_queue_snapshot().heights[0] < 180 && Instant::now() < deadline {
2603            serve_for(&rx, Duration::from_millis(5));
2604        }
2605        serve_for(&rx, Duration::from_millis(100));
2606        let gated = observer.inbound_queue_snapshot();
2607        assert!(
2608            gated.heights[0] >= 180,
2609            "DATA queue never reached the high watermark"
2610        );
2611        assert_eq!(gated.dropped[0], 0);
2612
2613        client.write_all(&hdlc::frame(&[1u8; 32])).unwrap();
2614        serve_for(&rx, Duration::from_millis(300));
2615        let after_marker = observer.inbound_queue_snapshot();
2616        assert_eq!(after_marker.heights[0], gated.heights[0]);
2617        assert_eq!(
2618            after_marker.dropped[0], gated.dropped[0],
2619            "the producer remained readable and overflowed the DATA queue"
2620        );
2621
2622        for _ in 0..gated.heights[0] {
2623            let event = recv_non_peer_event(&rx, Duration::from_secs(1)).unwrap();
2624            assert!(matches!(event, Event::Frame { .. }));
2625        }
2626        let released = recv_non_peer_event(&rx, Duration::from_secs(5)).unwrap();
2627        assert!(
2628            matches!(released, Event::Frame { data, .. } if data == vec![1u8; 32]),
2629            "held producer was not released after DATA pressure recovered"
2630        );
2631    }
2632
2633    #[test]
2634    fn backbone_receive_frame() {
2635        let port = find_free_port();
2636        let (tx, rx) = crate::event::channel();
2637        let next_id = Arc::new(AtomicU64::new(8100));
2638
2639        let config = make_server_config(port, 81, None, None, None, BackboneAbuseConfig::default());
2640
2641        start(config, tx, next_id).unwrap();
2642        thread::sleep(Duration::from_millis(50));
2643
2644        let mut client = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
2645
2646        // Drain InterfaceUp
2647        let _ = recv_non_peer_event(&rx, Duration::from_secs(1)).unwrap();
2648
2649        // The exact Reticulum header minimum must be dropped without
2650        // preventing recovery of the next valid frame.
2651        client
2652            .write_all(&hdlc::frame(&[0x11; rns_core::constants::HEADER_MINSIZE]))
2653            .unwrap();
2654        let payload: Vec<u8> = (0..32).collect();
2655        client.write_all(&hdlc::frame(&payload)).unwrap();
2656
2657        let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
2658        match event {
2659            Event::Frame {
2660                interface_id,
2661                data,
2662                rssi: _,
2663                snr: _,
2664            } => {
2665                assert_eq!(interface_id, InterfaceId(8100));
2666                assert_eq!(data, payload);
2667            }
2668            other => panic!("expected Frame, got {:?}", other),
2669        }
2670    }
2671
2672    #[test]
2673    fn backbone_send_to_client() {
2674        let port = find_free_port();
2675        let (tx, rx) = crate::event::channel();
2676        let next_id = Arc::new(AtomicU64::new(8200));
2677
2678        let config = make_server_config(port, 82, None, None, None, BackboneAbuseConfig::default());
2679
2680        start(config, tx, next_id).unwrap();
2681        thread::sleep(Duration::from_millis(50));
2682
2683        let mut client = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
2684        client
2685            .set_read_timeout(Some(Duration::from_secs(2)))
2686            .unwrap();
2687
2688        // Get writer from InterfaceUp
2689        let event = recv_non_peer_event(&rx, Duration::from_secs(1)).unwrap();
2690        let mut writer = match event {
2691            Event::InterfaceUp(_, Some(w), _) => w,
2692            other => panic!("expected InterfaceUp with writer, got {:?}", other),
2693        };
2694
2695        // Send frame via writer
2696        let payload: Vec<u8> = (0..24).collect();
2697        writer.send_frame(&payload).unwrap();
2698
2699        // Read from client
2700        let mut buf = [0u8; 256];
2701        let n = client.read(&mut buf).unwrap();
2702        let expected = hdlc::frame(&payload);
2703        assert_eq!(&buf[..n], &expected[..]);
2704    }
2705
2706    #[test]
2707    fn backbone_write_backpressure_does_not_starve_reads() {
2708        let port = find_free_port();
2709        let (tx, rx) = crate::event::channel();
2710        let next_id = Arc::new(AtomicU64::new(8250));
2711        let config = make_server_config(port, 82, None, None, None, BackboneAbuseConfig::default());
2712
2713        start(config, tx, next_id).unwrap();
2714        thread::sleep(Duration::from_millis(50));
2715
2716        let mut client = TcpStream::connect(format!("127.0.0.1:{port}")).unwrap();
2717        SockRef::from(&client).set_recv_buffer_size(4096).unwrap();
2718        let event = recv_non_peer_event(&rx, Duration::from_secs(1)).unwrap();
2719        let mut writer = match event {
2720            Event::InterfaceUp(_, Some(writer), _) => writer,
2721            other => panic!("expected InterfaceUp with writer, got {other:?}"),
2722        };
2723
2724        let outbound = vec![0x55; 60 * 1024];
2725        let deadline = Instant::now() + Duration::from_secs(3);
2726        let mut backpressured = false;
2727        while Instant::now() < deadline {
2728            match writer.send_frame(&outbound) {
2729                Ok(()) => {}
2730                Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
2731                    backpressured = true;
2732                    break;
2733                }
2734                Err(error) => panic!("unexpected backbone write error: {error}"),
2735            }
2736        }
2737        assert!(
2738            backpressured,
2739            "failed to create outbound write backpressure"
2740        );
2741
2742        let inbound: Vec<u8> = (0..32).collect();
2743        client.write_all(&hdlc::frame(&inbound)).unwrap();
2744        let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
2745        match event {
2746            Event::Frame { data, .. } => assert_eq!(data, inbound),
2747            other => panic!("inbound frame was starved by outbound backpressure: {other:?}"),
2748        }
2749    }
2750
2751    #[test]
2752    fn backbone_multiple_clients() {
2753        let port = find_free_port();
2754        let (tx, rx) = crate::event::channel();
2755        let next_id = Arc::new(AtomicU64::new(8300));
2756
2757        let config = make_server_config(port, 83, None, None, None, BackboneAbuseConfig::default());
2758
2759        start(config, tx, next_id).unwrap();
2760        thread::sleep(Duration::from_millis(50));
2761
2762        let _client1 = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
2763        let _client2 = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
2764
2765        let mut ids = Vec::new();
2766        for _ in 0..2 {
2767            let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
2768            match event {
2769                Event::InterfaceUp(id, _, _) => ids.push(id),
2770                other => panic!("expected InterfaceUp, got {:?}", other),
2771            }
2772        }
2773
2774        assert_eq!(ids.len(), 2);
2775        assert_ne!(ids[0], ids[1]);
2776    }
2777
2778    #[test]
2779    fn backbone_client_disconnect() {
2780        let port = find_free_port();
2781        let (tx, rx) = crate::event::channel();
2782        let next_id = Arc::new(AtomicU64::new(8400));
2783
2784        let config = make_server_config(port, 84, None, None, None, BackboneAbuseConfig::default());
2785
2786        start(config, tx, next_id).unwrap();
2787        thread::sleep(Duration::from_millis(50));
2788
2789        let client = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
2790
2791        // Drain InterfaceUp
2792        let _ = recv_non_peer_event(&rx, Duration::from_secs(1)).unwrap();
2793
2794        // Disconnect
2795        drop(client);
2796
2797        // Should receive InterfaceDown
2798        let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
2799        assert!(
2800            matches!(event, Event::InterfaceDown(InterfaceId(8400))),
2801            "expected InterfaceDown(8400), got {:?}",
2802            event
2803        );
2804    }
2805
2806    #[test]
2807    fn backbone_epoll_multiplexing() {
2808        let port = find_free_port();
2809        let (tx, rx) = crate::event::channel();
2810        let next_id = Arc::new(AtomicU64::new(8500));
2811
2812        let config = make_server_config(port, 85, None, None, None, BackboneAbuseConfig::default());
2813
2814        start(config, tx, next_id).unwrap();
2815        thread::sleep(Duration::from_millis(50));
2816
2817        let mut client1 = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
2818        let mut client2 = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
2819
2820        // Drain both InterfaceUp events
2821        let _ = recv_non_peer_event(&rx, Duration::from_secs(1)).unwrap();
2822        let _ = recv_non_peer_event(&rx, Duration::from_secs(1)).unwrap();
2823
2824        // Both clients send data simultaneously
2825        let payload1: Vec<u8> = (0..24).collect();
2826        let payload2: Vec<u8> = (100..130).collect();
2827        client1.write_all(&hdlc::frame(&payload1)).unwrap();
2828        client2.write_all(&hdlc::frame(&payload2)).unwrap();
2829
2830        // Should receive both Frame events
2831        let mut received = Vec::new();
2832        for _ in 0..2 {
2833            let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
2834            match event {
2835                Event::Frame { data, .. } => received.push(data),
2836                other => panic!("expected Frame, got {:?}", other),
2837            }
2838        }
2839        assert!(received.contains(&payload1));
2840        assert!(received.contains(&payload2));
2841    }
2842
2843    #[test]
2844    fn backbone_bind_port() {
2845        let port = find_free_port();
2846        let (tx, _rx) = crate::event::channel();
2847        let next_id = Arc::new(AtomicU64::new(8600));
2848
2849        let config = make_server_config(port, 86, None, None, None, BackboneAbuseConfig::default());
2850
2851        // Should not error
2852        start(config, tx, next_id).unwrap();
2853    }
2854
2855    #[test]
2856    fn backbone_hdlc_fragmented() {
2857        let port = find_free_port();
2858        let (tx, rx) = crate::event::channel();
2859        let next_id = Arc::new(AtomicU64::new(8700));
2860
2861        let config = make_server_config(port, 87, None, None, None, BackboneAbuseConfig::default());
2862
2863        start(config, tx, next_id).unwrap();
2864        thread::sleep(Duration::from_millis(50));
2865
2866        let mut client = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
2867        client.set_nodelay(true).unwrap();
2868
2869        // Drain InterfaceUp
2870        let _ = recv_non_peer_event(&rx, Duration::from_secs(1)).unwrap();
2871
2872        // Send HDLC frame in two fragments
2873        let payload: Vec<u8> = (0..32).collect();
2874        let framed = hdlc::frame(&payload);
2875        let mid = framed.len() / 2;
2876
2877        client.write_all(&framed[..mid]).unwrap();
2878        thread::sleep(Duration::from_millis(50));
2879        client.write_all(&framed[mid..]).unwrap();
2880
2881        // Should receive reassembled frame
2882        let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
2883        match event {
2884            Event::Frame { data, .. } => {
2885                assert_eq!(data, payload);
2886            }
2887            other => panic!("expected Frame, got {:?}", other),
2888        }
2889    }
2890
2891    // -----------------------------------------------------------------------
2892    // Client mode tests
2893    // -----------------------------------------------------------------------
2894
2895    fn make_client_config(port: u16, id: u64) -> BackboneClientConfig {
2896        BackboneClientConfig {
2897            name: format!("test-bb-client-{}", port),
2898            target_host: "127.0.0.1".into(),
2899            target_port: port,
2900            interface_id: InterfaceId(id),
2901            reconnect_wait: Duration::from_millis(100),
2902            max_reconnect_tries: Some(2),
2903            connect_timeout: Duration::from_secs(2),
2904            transport_identity: None,
2905            runtime: Arc::new(Mutex::new(BackboneClientRuntime {
2906                reconnect_wait: Duration::from_millis(100),
2907                max_reconnect_tries: Some(2),
2908                connect_timeout: Duration::from_secs(2),
2909            })),
2910        }
2911    }
2912
2913    #[test]
2914    fn backbone_client_connect() {
2915        let port = find_free_port();
2916        let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
2917        let (tx, rx) = crate::event::channel();
2918
2919        let config = make_client_config(port, 9000);
2920        let _writer = start_client(config, tx).unwrap();
2921
2922        let _server_stream = listener.accept().unwrap();
2923
2924        let event = rx.recv_timeout(Duration::from_secs(2)).unwrap();
2925        assert!(matches!(event, Event::InterfaceUp(InterfaceId(9000), _, _)));
2926    }
2927
2928    #[test]
2929    fn backbone_client_receive_frame() {
2930        let port = find_free_port();
2931        let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
2932        let (tx, rx) = crate::event::channel();
2933
2934        let config = make_client_config(port, 9100);
2935        let _writer = start_client(config, tx).unwrap();
2936
2937        let (mut server_stream, _) = listener.accept().unwrap();
2938
2939        // Drain InterfaceUp
2940        let _ = rx.recv_timeout(Duration::from_secs(1)).unwrap();
2941
2942        // Client mode applies the same strict lower bound and recovers.
2943        server_stream
2944            .write_all(&hdlc::frame(&[0x11; rns_core::constants::HEADER_MINSIZE]))
2945            .unwrap();
2946        let payload: Vec<u8> = (0..32).collect();
2947        server_stream.write_all(&hdlc::frame(&payload)).unwrap();
2948
2949        let event = rx.recv_timeout(Duration::from_secs(2)).unwrap();
2950        match event {
2951            Event::Frame {
2952                interface_id,
2953                data,
2954                rssi: _,
2955                snr: _,
2956            } => {
2957                assert_eq!(interface_id, InterfaceId(9100));
2958                assert_eq!(data, payload);
2959            }
2960            other => panic!("expected Frame, got {:?}", other),
2961        }
2962    }
2963
2964    #[test]
2965    fn backbone_client_send_frame() {
2966        let port = find_free_port();
2967        let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
2968        let (tx, _rx) = crate::event::channel();
2969
2970        let config = make_client_config(port, 9200);
2971        let mut writer = start_client(config, tx).unwrap();
2972
2973        let (mut server_stream, _) = listener.accept().unwrap();
2974        server_stream
2975            .set_read_timeout(Some(Duration::from_secs(2)))
2976            .unwrap();
2977
2978        let payload: Vec<u8> = (0..24).collect();
2979        writer.send_frame(&payload).unwrap();
2980
2981        let mut buf = [0u8; 256];
2982        let n = server_stream.read(&mut buf).unwrap();
2983        let expected = hdlc::frame(&payload);
2984        assert_eq!(&buf[..n], &expected[..]);
2985    }
2986
2987    #[test]
2988    fn backbone_max_connections_rejects_excess() {
2989        let port = find_free_port();
2990        let (tx, rx) = crate::event::channel();
2991        let next_id = Arc::new(AtomicU64::new(8800));
2992
2993        let config = make_server_config(
2994            port,
2995            88,
2996            Some(2),
2997            None,
2998            None,
2999            BackboneAbuseConfig::default(),
3000        );
3001
3002        start(config, tx, next_id).unwrap();
3003        thread::sleep(Duration::from_millis(50));
3004
3005        // Connect two clients (at limit)
3006        let _client1 = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
3007        let _client2 = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
3008
3009        // Drain both InterfaceUp events
3010        for _ in 0..2 {
3011            let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
3012            assert!(matches!(event, Event::InterfaceUp(_, _, _)));
3013        }
3014
3015        // Third connection should be accepted at TCP level but immediately dropped
3016        let client3 = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
3017        client3
3018            .set_read_timeout(Some(Duration::from_millis(500)))
3019            .unwrap();
3020
3021        // Give server time to reject
3022        thread::sleep(Duration::from_millis(100));
3023
3024        // Should NOT receive a third InterfaceUp
3025        let result = recv_non_peer_event(&rx, Duration::from_millis(500));
3026        assert!(
3027            result.is_err(),
3028            "expected no InterfaceUp for rejected connection, got {:?}",
3029            result
3030        );
3031    }
3032
3033    #[test]
3034    fn backbone_max_connections_allows_after_disconnect() {
3035        let port = find_free_port();
3036        let (tx, rx) = crate::event::channel();
3037        let next_id = Arc::new(AtomicU64::new(8900));
3038
3039        let config = make_server_config(
3040            port,
3041            89,
3042            Some(1),
3043            None,
3044            None,
3045            BackboneAbuseConfig::default(),
3046        );
3047
3048        start(config, tx, next_id).unwrap();
3049        thread::sleep(Duration::from_millis(50));
3050
3051        // Connect first client
3052        let client1 = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
3053        let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
3054        assert!(matches!(event, Event::InterfaceUp(_, _, _)));
3055
3056        // Disconnect first client
3057        drop(client1);
3058
3059        // Wait for InterfaceDown
3060        let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
3061        assert!(matches!(event, Event::InterfaceDown(_)));
3062
3063        // Now a new connection should be accepted
3064        let _client2 = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
3065        let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
3066        assert!(
3067            matches!(event, Event::InterfaceUp(_, _, _)),
3068            "expected InterfaceUp after slot freed, got {:?}",
3069            event
3070        );
3071    }
3072
3073    #[test]
3074    fn backbone_client_reconnect() {
3075        let port = find_free_port();
3076        let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
3077        listener.set_nonblocking(false).unwrap();
3078        let (tx, rx) = crate::event::channel();
3079
3080        let config = make_client_config(port, 9300);
3081        let _writer = start_client(config, tx).unwrap();
3082
3083        // Accept first connection and immediately close it
3084        let (server_stream, _) = listener.accept().unwrap();
3085
3086        // Drain InterfaceUp
3087        let _ = rx.recv_timeout(Duration::from_secs(1)).unwrap();
3088
3089        drop(server_stream);
3090
3091        // Should get InterfaceDown
3092        let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
3093        assert!(matches!(event, Event::InterfaceDown(InterfaceId(9300))));
3094
3095        // Accept the reconnection
3096        let _server_stream2 = listener.accept().unwrap();
3097
3098        // Should get InterfaceUp again
3099        let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
3100        assert!(matches!(event, Event::InterfaceUp(InterfaceId(9300), _, _)));
3101    }
3102
3103    #[test]
3104    fn a_reconnected_backbone_client_is_read_on_the_receiving_thread() {
3105        let port = find_free_port();
3106        let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
3107        let (tx, rx) = crate::event::channel();
3108        let _writer = start_client(make_client_config(port, 9310), tx).unwrap();
3109        let (first, _) = listener.accept().unwrap();
3110        assert!(matches!(
3111            rx.recv_timeout(Duration::from_secs(1)).unwrap(),
3112            Event::InterfaceUp(InterfaceId(9310), None, _)
3113        ));
3114        drop(first);
3115        assert!(matches!(
3116            recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap(),
3117            Event::InterfaceDown(InterfaceId(9310))
3118        ));
3119        let (mut second, _) = listener.accept().unwrap();
3120        assert!(matches!(
3121            recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap(),
3122            Event::InterfaceUp(InterfaceId(9310), Some(_), _)
3123        ));
3124        let payload: Vec<u8> = (0..32).collect();
3125        second.write_all(&hdlc::frame(&payload)).unwrap();
3126        match recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap() {
3127            Event::Frame { data, .. } => assert_eq!(data, payload),
3128            other => panic!("expected Frame, got {:?}", other),
3129        }
3130        // The reconnect thread exits once the new socket is served here.
3131        let reconnecting = || {
3132            std::fs::read_dir("/proc/self/task")
3133                .unwrap()
3134                .filter_map(|task| std::fs::read_to_string(task.ok()?.path().join("comm")).ok())
3135                .any(|name| name.trim_end() == "bb-reconn-9310")
3136        };
3137        let deadline = Instant::now() + Duration::from_secs(2);
3138        while reconnecting() && Instant::now() < deadline {
3139            thread::sleep(Duration::from_millis(10));
3140        }
3141        assert!(!reconnecting());
3142    }
3143
3144    #[test]
3145    fn backbone_client_stop_prevents_reconnect() {
3146        let port = find_free_port();
3147        let listener = TcpListener::bind(("127.0.0.1", port)).unwrap();
3148        let (tx, rx) = crate::event::channel();
3149
3150        let config = make_client_config(port, 9350);
3151        let control = ListenerControl::new();
3152        let _writer = start_client_with_ifac(config, tx, 0, None, control.clone()).unwrap();
3153
3154        let (server_stream, _) = listener.accept().unwrap();
3155        // Drain the initial InterfaceUp from the successful connect.
3156        let _ = recv_non_peer_event(&rx, Duration::from_secs(1)).unwrap();
3157
3158        control.request_stop();
3159        drop(server_stream);
3160
3161        // A detached client must not emit InterfaceDown or reconnect.
3162        if let Ok(event) = recv_non_peer_event(&rx, Duration::from_millis(700)) {
3163            panic!("unexpected event after stop: {:?}", event);
3164        }
3165    }
3166
3167    #[test]
3168    fn backbone_idle_timeout_disconnects_silent_client() {
3169        let port = find_free_port();
3170        let (tx, rx) = crate::event::channel();
3171        let next_id = Arc::new(AtomicU64::new(9400));
3172
3173        let config = make_server_config(
3174            port,
3175            94,
3176            None,
3177            Some(Duration::from_millis(150)),
3178            None,
3179            BackboneAbuseConfig::default(),
3180        );
3181
3182        start(config, tx, next_id).unwrap();
3183        thread::sleep(Duration::from_millis(50));
3184
3185        let _client = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
3186
3187        let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
3188        let client_id = match event {
3189            Event::InterfaceUp(id, _, _) => id,
3190            other => panic!("expected InterfaceUp, got {:?}", other),
3191        };
3192
3193        let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
3194        assert!(matches!(event, Event::InterfaceDown(id) if id == client_id));
3195    }
3196
3197    #[test]
3198    fn backbone_idle_timeout_ignores_client_after_data() {
3199        let port = find_free_port();
3200        let (tx, rx) = crate::event::channel();
3201        let next_id = Arc::new(AtomicU64::new(9500));
3202
3203        let config = make_server_config(
3204            port,
3205            95,
3206            None,
3207            Some(Duration::from_millis(200)),
3208            None,
3209            BackboneAbuseConfig::default(),
3210        );
3211
3212        start(config, tx, next_id).unwrap();
3213        thread::sleep(Duration::from_millis(50));
3214
3215        let mut client = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
3216
3217        let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
3218        let client_id = match event {
3219            Event::InterfaceUp(id, _, _) => id,
3220            other => panic!("expected InterfaceUp, got {:?}", other),
3221        };
3222
3223        client.write_all(&hdlc::frame(&[1u8; 24])).unwrap();
3224
3225        let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
3226        match event {
3227            Event::Frame {
3228                interface_id,
3229                data,
3230                rssi: _,
3231                snr: _,
3232            } => {
3233                assert_eq!(interface_id, client_id);
3234                assert_eq!(data, vec![1u8; 24]);
3235            }
3236            other => panic!("expected Frame, got {:?}", other),
3237        }
3238
3239        let result = recv_non_peer_event(&rx, Duration::from_millis(500));
3240        assert!(
3241            result.is_err(),
3242            "expected no InterfaceDown after client sent data, got {:?}",
3243            result
3244        );
3245    }
3246
3247    #[test]
3248    fn backbone_runtime_idle_timeout_updates_live() {
3249        let port = find_free_port();
3250        let (tx, rx) = crate::event::channel();
3251        let next_id = Arc::new(AtomicU64::new(9650));
3252
3253        let config = make_server_config(port, 97, None, None, None, BackboneAbuseConfig::default());
3254        let runtime = Arc::clone(&config.runtime);
3255
3256        start(config, tx, next_id).unwrap();
3257        thread::sleep(Duration::from_millis(50));
3258
3259        let _client = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
3260        let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
3261        let client_id = match event {
3262            Event::InterfaceUp(id, _, _) => id,
3263            other => panic!("expected InterfaceUp, got {:?}", other),
3264        };
3265
3266        {
3267            let mut runtime = runtime.lock().unwrap();
3268            runtime.idle_timeout = Some(Duration::from_millis(150));
3269        }
3270
3271        let event = recv_non_peer_event(&rx, Duration::from_secs(4)).unwrap();
3272        assert!(matches!(event, Event::InterfaceDown(id) if id == client_id));
3273    }
3274
3275    #[test]
3276    fn backbone_write_stall_timeout_disconnects_unwritable_client() {
3277        let port = find_free_port();
3278        let (tx, rx) = crate::event::channel();
3279        let next_id = Arc::new(AtomicU64::new(9660));
3280
3281        let config = make_server_config(
3282            port,
3283            98,
3284            None,
3285            None,
3286            Some(Duration::from_millis(50)),
3287            BackboneAbuseConfig::default(),
3288        );
3289
3290        start(config, tx, next_id).unwrap();
3291        thread::sleep(Duration::from_millis(50));
3292
3293        let client = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
3294        client
3295            .set_read_timeout(Some(Duration::from_millis(100)))
3296            .unwrap();
3297        let sock = SockRef::from(&client);
3298        sock.set_recv_buffer_size(4096).ok();
3299
3300        let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
3301        let (client_id, mut writer) = match event {
3302            Event::InterfaceUp(id, Some(writer), _) => (id, writer),
3303            other => panic!("expected InterfaceUp with writer, got {:?}", other),
3304        };
3305
3306        let payload = vec![0x55; 512 * 1024];
3307        let deadline = Instant::now() + Duration::from_secs(3);
3308        let mut stalled = false;
3309        while Instant::now() < deadline {
3310            match writer.send_frame(&payload) {
3311                Ok(()) => {}
3312                Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
3313                    thread::sleep(Duration::from_millis(10));
3314                }
3315                Err(ref e) if e.kind() == io::ErrorKind::TimedOut => {
3316                    stalled = true;
3317                    break;
3318                }
3319                Err(e) => panic!("unexpected send error: {}", e),
3320            }
3321        }
3322
3323        assert!(stalled, "expected writer to time out on persistent stall");
3324        let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
3325        assert!(matches!(event, Event::InterfaceDown(id) if id == client_id));
3326    }
3327
3328    #[test]
3329    fn shutting_down_backbone_writer_closes_the_client_socket() {
3330        let port = find_free_port();
3331        let (tx, rx) = crate::event::channel();
3332        let next_id = Arc::new(AtomicU64::new(9670));
3333        let config = make_server_config(port, 99, None, None, None, BackboneAbuseConfig::default());
3334
3335        start(config, tx, next_id).unwrap();
3336        thread::sleep(Duration::from_millis(50));
3337
3338        let mut client = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
3339        client
3340            .set_read_timeout(Some(Duration::from_secs(2)))
3341            .unwrap();
3342        let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
3343        let mut writer = match event {
3344            Event::InterfaceUp(_, Some(writer), _) => writer,
3345            other => panic!("expected InterfaceUp with writer, got {:?}", other),
3346        };
3347
3348        writer.shutdown();
3349
3350        let mut byte = [0u8; 1];
3351        assert_eq!(client.read(&mut byte).unwrap(), 0);
3352    }
3353
3354    /// Drain events matching a predicate, return the first match.
3355    fn wait_for<F>(
3356        rx: &crate::event::EventReceiver,
3357        timeout: Duration,
3358        mut pred: F,
3359    ) -> Option<Event>
3360    where
3361        F: FnMut(&Event) -> bool,
3362    {
3363        let deadline = Instant::now() + timeout;
3364        loop {
3365            let remaining = deadline.saturating_duration_since(Instant::now());
3366            if remaining.is_zero() {
3367                return None;
3368            }
3369            match rx.recv_timeout(remaining) {
3370                Ok(event) if pred(&event) => return Some(event),
3371                Ok(_) => continue,
3372                Err(_) => return None,
3373            }
3374        }
3375    }
3376
3377    #[test]
3378    fn backbone_write_stall_emits_peer_events() {
3379        let port = find_free_port();
3380        let (tx, rx) = crate::event::channel();
3381        let next_id = Arc::new(AtomicU64::new(9700));
3382
3383        let config = make_server_config(
3384            port,
3385            97,
3386            None,
3387            None,
3388            Some(Duration::from_millis(50)), // 50ms stall timeout
3389            BackboneAbuseConfig::default(),
3390        );
3391
3392        start(config, tx, next_id).unwrap();
3393        thread::sleep(Duration::from_millis(50));
3394
3395        // Connect a client that won't read (will cause write stall)
3396        let client = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
3397        client
3398            .set_read_timeout(Some(Duration::from_millis(100)))
3399            .unwrap();
3400        let sock = SockRef::from(&client);
3401        sock.set_recv_buffer_size(4096).ok();
3402
3403        // Wait for InterfaceUp and grab writer
3404        let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
3405        let mut writer = match event {
3406            Event::InterfaceUp(_, Some(w), _) => w,
3407            other => panic!("expected InterfaceUp with writer, got {:?}", other),
3408        };
3409
3410        // Flood until stall
3411        let payload = vec![0x55; 512 * 1024];
3412        let deadline = Instant::now() + Duration::from_secs(3);
3413        while Instant::now() < deadline {
3414            match writer.send_frame(&payload) {
3415                Ok(()) | Err(_) => {
3416                    if Instant::now() + Duration::from_millis(10) > deadline {
3417                        break;
3418                    }
3419                    thread::sleep(Duration::from_millis(10));
3420                }
3421            }
3422        }
3423
3424        // Should see BackbonePeerWriteStall event
3425        let stall_event = wait_for(&rx, Duration::from_secs(3), |e| {
3426            matches!(e, Event::BackbonePeerWriteStall { .. })
3427        });
3428        assert!(
3429            stall_event.is_some(),
3430            "expected BackbonePeerWriteStall event"
3431        );
3432    }
3433
3434    #[test]
3435    fn backbone_blacklisted_peer_rejected_on_connect() {
3436        let port = find_free_port();
3437        let (tx, rx) = crate::event::channel();
3438        let next_id = Arc::new(AtomicU64::new(9800));
3439
3440        let config = make_server_config(port, 98, None, None, None, BackboneAbuseConfig::default());
3441        let peer_state = config.peer_state.clone();
3442
3443        start(config, tx.clone(), next_id).unwrap();
3444        thread::sleep(Duration::from_millis(50));
3445
3446        // First connection should succeed
3447        let client1 = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
3448        let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
3449        assert!(
3450            matches!(event, Event::InterfaceUp(_, _, _)),
3451            "first connection should succeed"
3452        );
3453        drop(client1);
3454
3455        // Drain disconnect events
3456        thread::sleep(Duration::from_millis(100));
3457        while rx.try_recv().is_ok() {}
3458
3459        // Blacklist 127.0.0.1 via the peer monitor
3460        peer_state.lock().unwrap().blacklist(
3461            "127.0.0.1".parse().unwrap(),
3462            Duration::from_secs(60),
3463            "test blacklist".into(),
3464        );
3465
3466        // Second connection from same IP should be rejected (no InterfaceUp)
3467        let _client2 = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
3468        // Give poll loop time to reject
3469        thread::sleep(Duration::from_millis(200));
3470
3471        // Should NOT get an InterfaceUp — connection should have been rejected
3472        let event = rx.try_recv();
3473        if let Ok(Event::InterfaceUp(_, _, _)) = event {
3474            panic!("blacklisted peer should not get InterfaceUp")
3475        }
3476    }
3477
3478    #[test]
3479    fn backbone_six_fast_flaps_reject_before_allocating_seventh_interface() {
3480        let port = find_free_port();
3481        let (tx, rx) = crate::event::channel();
3482        let next_id = Arc::new(AtomicU64::new(9900));
3483        let mut config =
3484            make_server_config(port, 99, None, None, None, BackboneAbuseConfig::default());
3485        config.fast_flap = test_fast_flap_config();
3486        let fast_flap_state = Arc::clone(&config.fast_flap_state);
3487        let peer_state = Arc::clone(&config.peer_state);
3488
3489        start(config, tx, Arc::clone(&next_id)).unwrap();
3490        thread::sleep(Duration::from_millis(50));
3491
3492        for flap in 0..6 {
3493            let mut client = TcpStream::connect(format!("127.0.0.1:{port}")).unwrap();
3494            let up = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
3495            assert!(matches!(up, Event::InterfaceUp(_, _, _)));
3496
3497            // Receiving traffic must not exempt a short-lived connection.
3498            if flap == 0 {
3499                let payload = vec![0x41; 32];
3500                client.write_all(&hdlc::frame(&payload)).unwrap();
3501                let frame = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
3502                assert!(matches!(frame, Event::Frame { data, .. } if data == payload));
3503            }
3504
3505            drop(client);
3506            let down = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
3507            assert!(matches!(down, Event::InterfaceDown(_)));
3508        }
3509
3510        let peer_ip: IpAddr = "127.0.0.1".parse().unwrap();
3511        assert!(
3512            lock_or_recover(&fast_flap_state, "test fast-flap state").is_blocked(
3513                peer_ip,
3514                &test_fast_flap_config(),
3515                Instant::now(),
3516            )
3517        );
3518        assert_eq!(next_id.load(Ordering::Relaxed), 9906);
3519        let entries = lock_or_recover(&peer_state, "test peer state").list("test-backbone");
3520        let entry = entries
3521            .iter()
3522            .find(|entry| entry.peer_ip == peer_ip)
3523            .expect("fast-flapping peer state");
3524        assert_eq!(entry.blacklist_reason.as_deref(), Some("fast flapping"));
3525        assert!(entry.blacklisted_remaining_secs.is_some());
3526
3527        let _rejected = TcpStream::connect(format!("127.0.0.1:{port}")).unwrap();
3528        serve_for(&rx, Duration::from_millis(200));
3529        assert_eq!(
3530            next_id.load(Ordering::Relaxed),
3531            9906,
3532            "rejection must happen before dynamic interface allocation"
3533        );
3534        while let Ok(event) = rx.try_recv() {
3535            assert!(
3536                !matches!(event, Event::InterfaceUp(_, _, _)),
3537                "blocked peer must not register a dynamic interface"
3538            );
3539        }
3540        let entries = lock_or_recover(&peer_state, "test peer state").list("test-backbone");
3541        assert_eq!(
3542            entries
3543                .iter()
3544                .find(|entry| entry.peer_ip == peer_ip)
3545                .unwrap()
3546                .reject_count,
3547            1
3548        );
3549    }
3550
3551    #[test]
3552    fn backbone_parse_config_reads_abuse_settings() {
3553        let factory = BackboneInterfaceFactory;
3554        let mut params = HashMap::new();
3555        params.insert("listen_ip".into(), "127.0.0.1".into());
3556        params.insert("listen_port".into(), "4242".into());
3557        params.insert("idle_timeout".into(), "15".into());
3558        params.insert("write_stall_timeout".into(), "45".into());
3559        params.insert("max_penalty_duration".into(), "3600".into());
3560
3561        let config = factory
3562            .parse_config("test-backbone", InterfaceId(97), &params)
3563            .unwrap();
3564        let mode = *config.into_any().downcast::<BackboneMode>().unwrap();
3565
3566        match mode {
3567            BackboneMode::Server(config) => {
3568                assert_eq!(config.listen_ip, "127.0.0.1");
3569                assert_eq!(config.listen_port, 4242);
3570                assert_eq!(config.idle_timeout, Some(Duration::from_secs(15)));
3571                assert_eq!(config.write_stall_timeout, Some(Duration::from_secs(45)));
3572                assert_eq!(
3573                    config.abuse.max_penalty_duration,
3574                    Some(Duration::from_secs(3600))
3575                );
3576            }
3577            BackboneMode::Client(_, _) => panic!("expected server config"),
3578        }
3579    }
3580
3581    #[test]
3582    fn backbone_parse_config_reads_fast_flapping_settings() {
3583        let factory = BackboneInterfaceFactory;
3584        let mut params = HashMap::new();
3585        params.insert("listen_ip".into(), "127.0.0.1".into());
3586        params.insert("listen_port".into(), "4242".into());
3587        params.insert("block_fast_flapping".into(), "no".into());
3588        params.insert("fast_flapping_threshold".into(), "2.5".into());
3589        params.insert("fast_flapping_grace".into(), "9".into());
3590        params.insert("fast_flapping_block_time".into(), "1.5".into());
3591
3592        let config = factory
3593            .parse_config("test-backbone", InterfaceId(93), &params)
3594            .unwrap();
3595        let mode = *config.into_any().downcast::<BackboneMode>().unwrap();
3596        let BackboneMode::Server(config) = mode else {
3597            panic!("expected server config");
3598        };
3599
3600        assert!(!config.fast_flap.enabled);
3601        assert_eq!(
3602            config.fast_flap.connection_threshold,
3603            Duration::from_secs_f64(2.5)
3604        );
3605        assert_eq!(config.fast_flap.grace, 9);
3606        assert_eq!(config.fast_flap.block_duration, Duration::from_secs(90));
3607    }
3608
3609    #[test]
3610    fn backbone_parse_config_rejects_invalid_fast_flapping_settings() {
3611        let factory = BackboneInterfaceFactory;
3612        for (key, value) in [
3613            ("block_fast_flapping", "perhaps"),
3614            ("fast_flapping_threshold", "-1"),
3615            ("fast_flapping_threshold", "NaN"),
3616            ("fast_flapping_grace", "-1"),
3617            ("fast_flapping_block_time", "infinite"),
3618        ] {
3619            let mut params = HashMap::new();
3620            params.insert("listen_port".into(), "4242".into());
3621            params.insert(key.into(), value.into());
3622
3623            let err = match factory.parse_config("test-backbone", InterfaceId(92), &params) {
3624                Ok(_) => panic!("{key}={value} should be rejected"),
3625                Err(err) => err,
3626            };
3627            assert!(err.contains(key), "unexpected error: {err}");
3628        }
3629    }
3630
3631    #[test]
3632    fn parsed_backbone_listeners_share_fast_flap_history() {
3633        let factory = BackboneInterfaceFactory;
3634        let mut params = HashMap::new();
3635        params.insert("listen_port".into(), "4242".into());
3636
3637        let first = factory
3638            .parse_config("first", InterfaceId(90), &params)
3639            .unwrap();
3640        let second = factory
3641            .parse_config("second", InterfaceId(91), &params)
3642            .unwrap();
3643        let BackboneMode::Server(first) = *first.into_any().downcast::<BackboneMode>().unwrap()
3644        else {
3645            panic!("expected server config");
3646        };
3647        let BackboneMode::Server(second) = *second.into_any().downcast::<BackboneMode>().unwrap()
3648        else {
3649            panic!("expected server config");
3650        };
3651
3652        assert!(Arc::ptr_eq(&first.fast_flap_state, &second.fast_flap_state));
3653    }
3654
3655    #[test]
3656    fn backbone_parse_config_reads_client_priority() {
3657        let factory = BackboneInterfaceFactory;
3658        let mut params = HashMap::new();
3659        params.insert("remote".into(), "example.com".into());
3660        params.insert("target_port".into(), "4242".into());
3661        params.insert("priority".into(), "87".into());
3662
3663        let config = factory
3664            .parse_config("test-backbone-client", InterfaceId(96), &params)
3665            .unwrap();
3666        let mode = *config.into_any().downcast::<BackboneMode>().unwrap();
3667
3668        match mode {
3669            BackboneMode::Client(_, priority) => assert_eq!(priority, 87),
3670            BackboneMode::Server(_) => panic!("expected client config"),
3671        }
3672    }
3673
3674    #[test]
3675    fn backbone_parse_config_defaults_client_priority() {
3676        let factory = BackboneInterfaceFactory;
3677        let mut params = HashMap::new();
3678        params.insert("remote".into(), "example.com".into());
3679
3680        let config = factory
3681            .parse_config("test-backbone-client", InterfaceId(95), &params)
3682            .unwrap();
3683        let mode = *config.into_any().downcast::<BackboneMode>().unwrap();
3684
3685        match mode {
3686            BackboneMode::Client(_, priority) => assert_eq!(priority, 60),
3687            BackboneMode::Server(_) => panic!("expected client config"),
3688        }
3689    }
3690
3691    #[test]
3692    fn backbone_parse_config_rejects_invalid_client_priority() {
3693        let factory = BackboneInterfaceFactory;
3694        for value in ["-1", "101", "fast"] {
3695            let mut params = HashMap::new();
3696            params.insert("remote".into(), "example.com".into());
3697            params.insert("priority".into(), value.into());
3698
3699            let err = match factory.parse_config("test-backbone-client", InterfaceId(94), &params) {
3700                Ok(_) => panic!("priority {value} should be rejected"),
3701                Err(err) => err,
3702            };
3703            assert!(
3704                err.contains("priority"),
3705                "unexpected error for {value}: {err}"
3706            );
3707        }
3708    }
3709
3710    #[test]
3711    fn positive_duration_param_rejects_values_that_are_not_durations() {
3712        for raw in ["inf", "-inf", "NaN", "1e300", "-1", "0"] {
3713            let params = HashMap::from([("idle_timeout".to_string(), raw.to_string())]);
3714            assert_eq!(
3715                parse_positive_duration_secs(&params, "idle_timeout"),
3716                None,
3717                "value {raw} should be ignored"
3718            );
3719        }
3720        let params = HashMap::from([("idle_timeout".to_string(), "2.5".to_string())]);
3721        assert_eq!(
3722            parse_positive_duration_secs(&params, "idle_timeout"),
3723            Some(Duration::from_millis(2500))
3724        );
3725    }
3726}