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).
1467///
1468/// A fresh writer is constructed for every connection: the initial connect in
1469/// [`start_client`] and each reconnect in [`ClientLink::reconnect_loop`]. Its
1470/// [`TransmitBuffer`] and [`EgressController`] therefore always start empty and
1471/// unstalled, and the stalled writer is dropped together with its socket on
1472/// teardown, so no egress drain/stall state can survive into the next
1473/// connection. This structurally covers upstream's `BackboneInterface._dp_ec_reset`
1474/// (rgit `f0875ac4`), which resets the same counters on a persistent Python
1475/// initiator instance.
1476struct BackboneClientWriter {
1477    stream: TcpStream,
1478    transmit_buffer: TransmitBuffer,
1479    egress_control: EgressControl,
1480}
1481
1482impl Writer for BackboneClientWriter {
1483    fn send_frame_confirmed(&mut self, data: &[u8]) -> io::Result<()> {
1484        if !self
1485            .transmit_buffer
1486            .append_with_limit(hdlc::frame(data), Some(EGRESS_HIGH_WATERMARK))
1487        {
1488            return Err(io::Error::new(
1489                io::ErrorKind::InvalidInput,
1490                "confirmed frame exceeds backbone buffer",
1491            ));
1492        }
1493        self.transmit_buffer.flush();
1494        while self.transmit_buffer.sendable_bytes() > 0 {
1495            if self.transmit_buffer.drain_to(&mut self.stream)? == 0 {
1496                return Err(io::Error::new(
1497                    io::ErrorKind::WriteZero,
1498                    "backbone writer made no progress",
1499                ));
1500            }
1501        }
1502        Ok(())
1503    }
1504    fn send_frame(&mut self, data: &[u8]) -> io::Result<()> {
1505        self.send_frames(&[crate::interface::FrameBytes::Owned(data.to_vec())])
1506    }
1507
1508    fn send_frames(&mut self, frames: &[crate::interface::FrameBytes]) -> io::Result<()> {
1509        for frame in frames {
1510            let encoded = hdlc::frame(frame);
1511            let encoded_len = encoded.len();
1512            if !self
1513                .transmit_buffer
1514                .append_with_limit(encoded, Some(EGRESS_HIGH_WATERMARK))
1515            {
1516                self.egress_control.record_drop(encoded_len);
1517            }
1518        }
1519        self.transmit_buffer.flush();
1520        while self.transmit_buffer.sendable_bytes() > 0 {
1521            if self.transmit_buffer.drain_to(&mut self.stream)? == 0 {
1522                return Err(io::Error::new(
1523                    io::ErrorKind::WriteZero,
1524                    "backbone client writer made no progress",
1525                ));
1526            }
1527        }
1528        Ok(())
1529    }
1530
1531    fn egress_control(&self) -> Option<EgressControl> {
1532        Some(self.egress_control.clone())
1533    }
1534
1535    fn loop_socket(&self) -> Option<std::os::fd::RawFd> {
1536        Some(self.stream.as_raw_fd())
1537    }
1538
1539    fn loop_egress(&mut self) -> Option<LoopEgress> {
1540        Some(LoopEgress::budget(self.egress_control.clone()))
1541    }
1542}
1543
1544/// Try to connect to the target host:port with timeout.
1545fn try_connect_client(
1546    config: &BackboneClientConfig,
1547    underlay_mark: Option<u32>,
1548) -> io::Result<TcpStream> {
1549    let runtime = lock_or_recover(&config.runtime, "backbone runtime").clone();
1550    let addr_str = format!("{}:{}", config.target_host, config.target_port);
1551    let addr = addr_str
1552        .to_socket_addrs()?
1553        .next()
1554        .ok_or_else(|| io::Error::new(io::ErrorKind::AddrNotAvailable, "no addresses resolved"))?;
1555
1556    #[cfg(target_os = "linux")]
1557    let stream = if underlay_mark.is_some() {
1558        super::connect_tcp_with_options(&addr, None, underlay_mark, runtime.connect_timeout)?
1559    } else {
1560        TcpStream::connect_timeout(&addr, runtime.connect_timeout)?
1561    };
1562    #[cfg(not(target_os = "linux"))]
1563    let stream = TcpStream::connect_timeout(&addr, runtime.connect_timeout)?;
1564    stream.set_nodelay(true)?;
1565    set_tcp_keepalive(&stream).ok();
1566
1567    // Prevent SIGPIPE on macOS when writing to broken pipes
1568    #[cfg(target_os = "macos")]
1569    {
1570        let sock = SockRef::from(&stream);
1571        sock.set_nosigpipe(true).ok();
1572    }
1573
1574    Ok(stream)
1575}
1576
1577/// Connect and start the reader thread. Returns the writer for the driver.
1578pub fn start_client(config: BackboneClientConfig, tx: EventSender) -> io::Result<Box<dyn Writer>> {
1579    start_client_with_ifac(config, tx, 0, None, ListenerControl::new())
1580}
1581
1582pub(crate) fn start_client_with_ifac(
1583    config: BackboneClientConfig,
1584    tx: EventSender,
1585    ifac_size: usize,
1586    underlay_mark: Option<u32>,
1587    control: ListenerControl,
1588) -> io::Result<Box<dyn Writer>> {
1589    let stream = try_connect_client(&config, underlay_mark)?;
1590    let writer_stream = stream.try_clone()?;
1591
1592    let id = config.interface_id;
1593    log::info!(
1594        "[{}] backbone client connected to {}:{}",
1595        config.name,
1596        config.target_host,
1597        config.target_port
1598    );
1599
1600    // Initial connect: writer is None because it's returned directly to the caller
1601    let _ = tx.send(Event::InterfaceUp(id, None, None));
1602
1603    let link = Arc::new(ClientLink {
1604        config,
1605        tx,
1606        ifac_size,
1607        underlay_mark,
1608        control,
1609    });
1610    if let Err(error) = link.serve(stream) {
1611        let _ = link.tx.send(Event::InterfaceDown(id));
1612        return Err(error);
1613    }
1614
1615    Ok(Box::new(BackboneClientWriter {
1616        stream: writer_stream,
1617        transmit_buffer: TransmitBuffer::new(),
1618        egress_control: EgressControl::new(EGRESS_HIGH_WATERMARK),
1619    }))
1620}
1621
1622/// What a client connection needs across reconnects.
1623struct ClientLink {
1624    config: BackboneClientConfig,
1625    tx: EventSender,
1626    ifac_size: usize,
1627    underlay_mark: Option<u32>,
1628    control: ListenerControl,
1629}
1630
1631impl ClientLink {
1632    /// Read `stream` on the driver thread from now on.
1633    fn serve(self: &Arc<Self>, stream: TcpStream) -> io::Result<()> {
1634        let source = ClientSource {
1635            stream,
1636            decoder: hdlc::Decoder::reticulum(HW_MTU, self.ifac_size),
1637            link: Arc::clone(self),
1638        };
1639        self.tx.register_source(
1640            crate::interface::loop_writer::read_key(self.config.interface_id),
1641            Box::new(source),
1642        )
1643    }
1644
1645    /// Report the connection down and reconnect on a thread of its own,
1646    /// which exits once the new connection is served on the driver (or
1647    /// reconnecting gives up or is stopped).
1648    fn connection_lost(self: &Arc<Self>) {
1649        let id = self.config.interface_id;
1650        let _ = self.tx.push_from_loop([Event::InterfaceDown(id)]);
1651        let link = Arc::clone(self);
1652        let spawned = thread::Builder::new()
1653            .name(format!("bb-reconn-{}", id.0))
1654            .spawn(move || link.reconnect_loop());
1655        if let Err(error) = spawned {
1656            log::error!(
1657                "[{}] cannot start reconnecting, giving up: {}",
1658                self.config.name,
1659                error
1660            );
1661        }
1662    }
1663
1664    fn reconnect_loop(self: Arc<Self>) {
1665        loop {
1666            let Some(stream) =
1667                client_reconnect(&self.config, &self.tx, self.underlay_mark, &self.control)
1668            else {
1669                if !self.control.should_stop() {
1670                    log::error!("[{}] reconnection failed, giving up", self.config.name);
1671                }
1672                return;
1673            };
1674            match self.serve(stream) {
1675                Ok(()) => return,
1676                Err(error) => {
1677                    log::warn!(
1678                        "[{}] cannot read the new connection: {}",
1679                        self.config.name,
1680                        error
1681                    );
1682                    let _ = self.tx.send(Event::InterfaceDown(self.config.interface_id));
1683                }
1684            }
1685        }
1686    }
1687}
1688
1689/// The connected client socket, read on the driver thread.
1690struct ClientSource {
1691    stream: TcpStream,
1692    decoder: hdlc::Decoder,
1693    link: Arc<ClientLink>,
1694}
1695
1696impl LoopSource for ClientSource {
1697    fn fd(&self) -> std::os::fd::RawFd {
1698        self.stream.as_raw_fd()
1699    }
1700
1701    fn stopped(&self) -> bool {
1702        self.link.control.should_stop()
1703    }
1704
1705    fn on_ready(&mut self, cx: &mut LoopContext<'_>) -> SourceStatus {
1706        let link = &self.link;
1707        if link.control.should_stop() {
1708            return SourceStatus::Remove;
1709        }
1710        let name = &link.config.name;
1711        match read_stream_frames(
1712            self.stream.as_raw_fd(),
1713            &mut self.decoder,
1714            cx,
1715            &link.tx,
1716            link.config.interface_id,
1717            name,
1718            |_, _| {},
1719        ) {
1720            StreamRead::Rearm => SourceStatus::Rearm,
1721            StreamRead::Suspend => SourceStatus::Suspend,
1722            StreamRead::Closed => {
1723                log::warn!("[{}] connection closed", name);
1724                link.connection_lost();
1725                SourceStatus::Remove
1726            }
1727            StreamRead::Failed(error) => {
1728                log::warn!("[{}] read error: {}", name, error);
1729                link.connection_lost();
1730                SourceStatus::Remove
1731            }
1732            StreamRead::DriverGone => SourceStatus::Remove,
1733        }
1734    }
1735}
1736
1737/// Maximum backoff multiplier: `base_delay * 2^MAX_BACKOFF_SHIFT`.
1738/// With a 5 s base this caps at 5 × 2^6 = 320 s ≈ 5 min.
1739const MAX_BACKOFF_SHIFT: u32 = 6;
1740
1741/// Attempt to reconnect with exponential backoff and jitter.
1742/// Returns the new reader stream on success.
1743/// Sends the new writer to the driver via InterfaceUp event.
1744fn client_reconnect(
1745    config: &BackboneClientConfig,
1746    tx: &EventSender,
1747    underlay_mark: Option<u32>,
1748    control: &ListenerControl,
1749) -> Option<TcpStream> {
1750    let mut attempts = 0u32;
1751    loop {
1752        let runtime = lock_or_recover(&config.runtime, "backbone runtime").clone();
1753
1754        let shift = attempts.min(MAX_BACKOFF_SHIFT);
1755        let backoff = runtime.reconnect_wait * 2u32.pow(shift);
1756        // Add ±25 % jitter to avoid thundering-herd reconnects.
1757        let jitter_range = backoff / 4;
1758        let jitter = if jitter_range.as_nanos() > 0 {
1759            let offset = Duration::from_nanos(
1760                (std::hash::RandomState::new().build_hasher().finish()
1761                    % jitter_range.as_nanos() as u64)
1762                    * 2,
1763            );
1764            if offset > jitter_range {
1765                backoff + (offset - jitter_range)
1766            } else {
1767                backoff - (jitter_range - offset)
1768            }
1769        } else {
1770            backoff
1771        };
1772        if !wait_while_active(control, jitter) {
1773            return None;
1774        }
1775
1776        attempts += 1;
1777
1778        if let Some(max) = runtime.max_reconnect_tries {
1779            if attempts > max {
1780                let _ = tx.send(Event::InterfaceDown(config.interface_id));
1781                return None;
1782            }
1783        }
1784
1785        log::info!(
1786            "[{}] reconnect attempt {} (backoff {:.1}s) ...",
1787            config.name,
1788            attempts,
1789            jitter.as_secs_f64(),
1790        );
1791
1792        match try_connect_client(config, underlay_mark) {
1793            Ok(new_stream) => {
1794                if control.should_stop() {
1795                    return None;
1796                }
1797                let writer_stream = match new_stream.try_clone() {
1798                    Ok(s) => s,
1799                    Err(e) => {
1800                        log::warn!("[{}] failed to clone stream: {}", config.name, e);
1801                        continue;
1802                    }
1803                };
1804                log::info!(
1805                    "[{}] reconnected after {} attempt(s)",
1806                    config.name,
1807                    attempts
1808                );
1809                let new_writer: Box<dyn Writer> = Box::new(BackboneClientWriter {
1810                    stream: writer_stream,
1811                    transmit_buffer: TransmitBuffer::new(),
1812                    egress_control: EgressControl::new(EGRESS_HIGH_WATERMARK),
1813                });
1814                let _ = tx.send(Event::InterfaceUp(
1815                    config.interface_id,
1816                    Some(new_writer),
1817                    None,
1818                ));
1819                return Some(new_stream);
1820            }
1821            Err(e) => {
1822                log::warn!("[{}] reconnect failed: {}", config.name, e);
1823            }
1824        }
1825    }
1826}
1827
1828// ---------------------------------------------------------------------------
1829// Factory
1830// ---------------------------------------------------------------------------
1831
1832/// Internal enum used by [`BackboneInterfaceFactory`] to carry either a
1833/// server or client config through the opaque `InterfaceConfigData` channel.
1834#[derive(Clone)]
1835pub(crate) enum BackboneMode {
1836    Server(BackboneConfig),
1837    Client(BackboneClientConfig, u8),
1838}
1839
1840/// Factory for `BackboneInterface`.
1841///
1842/// If the config params contain `"remote"` or `"target_host"` the interface
1843/// is started in client mode; otherwise it is started as a TCP listener
1844/// (server mode).
1845pub struct BackboneInterfaceFactory;
1846
1847/// Compatibility name used by Reticulum 1.3.8 for a forced client-mode
1848/// Backbone interface.
1849pub struct BackboneClientInterfaceFactory;
1850
1851impl InterfaceFactory for BackboneClientInterfaceFactory {
1852    fn type_name(&self) -> &str {
1853        "BackboneClientInterface"
1854    }
1855
1856    fn parse_config(
1857        &self,
1858        name: &str,
1859        id: InterfaceId,
1860        params: &HashMap<String, String>,
1861    ) -> Result<Box<dyn InterfaceConfigData>, String> {
1862        if !params.contains_key("remote") && !params.contains_key("target_host") {
1863            return Err("BackboneClientInterface requires 'remote' or 'target_host'".into());
1864        }
1865        BackboneInterfaceFactory.parse_config(name, id, params)
1866    }
1867
1868    fn start(
1869        &self,
1870        config: Box<dyn InterfaceConfigData>,
1871        ctx: StartContext,
1872    ) -> io::Result<StartResult> {
1873        BackboneInterfaceFactory.start(config, ctx)
1874    }
1875}
1876
1877fn parse_positive_duration_secs(params: &HashMap<String, String>, key: &str) -> Option<Duration> {
1878    params
1879        .get(key)
1880        .and_then(|v| v.parse::<f64>().ok())
1881        .filter(|v| *v > 0.0)
1882        .and_then(|v| Duration::try_from_secs_f64(v).ok())
1883        .filter(|duration| Instant::now().checked_add(*duration).is_some())
1884}
1885
1886fn parse_nonnegative_duration(
1887    params: &HashMap<String, String>,
1888    key: &str,
1889    multiplier: f64,
1890    default: Duration,
1891) -> Result<Duration, String> {
1892    let Some(raw) = params.get(key) else {
1893        return Ok(default);
1894    };
1895    let value = raw
1896        .parse::<f64>()
1897        .map_err(|_| format!("invalid {key} '{raw}' (expected a non-negative number)"))?;
1898    let seconds = value * multiplier;
1899    if !value.is_finite() || value < 0.0 || !seconds.is_finite() || seconds > u64::MAX as f64 {
1900        return Err(format!(
1901            "invalid {key} '{raw}' (expected a finite non-negative number)"
1902        ));
1903    }
1904    let duration = Duration::from_secs_f64(seconds);
1905    if Instant::now().checked_add(duration).is_none() {
1906        return Err(format!("invalid {key} '{raw}' (duration is too large)"));
1907    }
1908    Ok(duration)
1909}
1910
1911fn parse_fast_flap_config(
1912    params: &HashMap<String, String>,
1913) -> Result<BackboneFastFlapConfig, String> {
1914    let defaults = BackboneFastFlapConfig::default();
1915    let enabled = match params.get("block_fast_flapping") {
1916        Some(raw) => crate::config::parse_bool_pub(raw)
1917            .ok_or_else(|| format!("invalid block_fast_flapping '{raw}' (expected yes or no)"))?,
1918        None => defaults.enabled,
1919    };
1920    let connection_threshold = parse_nonnegative_duration(
1921        params,
1922        "fast_flapping_threshold",
1923        1.0,
1924        defaults.connection_threshold,
1925    )?;
1926    let grace = match params.get("fast_flapping_grace") {
1927        Some(raw) => raw.parse::<u64>().map_err(|_| {
1928            format!("invalid fast_flapping_grace '{raw}' (expected a non-negative integer)")
1929        })?,
1930        None => defaults.grace,
1931    };
1932    let block_duration = parse_nonnegative_duration(
1933        params,
1934        "fast_flapping_block_time",
1935        60.0,
1936        defaults.block_duration,
1937    )?;
1938
1939    Ok(BackboneFastFlapConfig {
1940        enabled,
1941        connection_threshold,
1942        grace,
1943        block_duration,
1944    })
1945}
1946
1947impl InterfaceFactory for BackboneInterfaceFactory {
1948    fn type_name(&self) -> &str {
1949        "BackboneInterface"
1950    }
1951
1952    fn parse_config(
1953        &self,
1954        name: &str,
1955        id: InterfaceId,
1956        params: &HashMap<String, String>,
1957    ) -> Result<Box<dyn InterfaceConfigData>, String> {
1958        if let Some(target_host) = params.get("remote").or_else(|| params.get("target_host")) {
1959            // Client mode
1960            let target_host = target_host.clone();
1961            let target_port = params
1962                .get("target_port")
1963                .or_else(|| params.get("port"))
1964                .and_then(|v| v.parse().ok())
1965                .unwrap_or(4242);
1966            let transport_identity = params.get("transport_identity").cloned();
1967            let priority = match params.get("priority") {
1968                Some(value) => value.parse::<u8>().map_err(|_| {
1969                    format!(
1970                        "invalid Backbone peer priority '{}' (expected 0..100)",
1971                        value
1972                    )
1973                })?,
1974                None => crate::driver::BACKBONE_PEER_POOL_CONFIGURED_DEFAULT_PRIORITY,
1975            };
1976            if priority > 100 {
1977                return Err(format!(
1978                    "invalid Backbone peer priority '{}' (expected 0..100)",
1979                    priority
1980                ));
1981            }
1982            Ok(Box::new(BackboneMode::Client(
1983                BackboneClientConfig {
1984                    name: name.to_string(),
1985                    target_host,
1986                    target_port,
1987                    interface_id: id,
1988                    transport_identity,
1989                    ..BackboneClientConfig::default()
1990                },
1991                priority,
1992            )))
1993        } else {
1994            // Server mode
1995            let listen_ip = params
1996                .get("listen_ip")
1997                .or_else(|| params.get("device"))
1998                .cloned()
1999                .unwrap_or_else(|| "0.0.0.0".into());
2000            let listen_port = params
2001                .get("listen_port")
2002                .or_else(|| params.get("port"))
2003                .and_then(|v| v.parse().ok())
2004                .unwrap_or(4242);
2005            let max_connections = params.get("max_connections").and_then(|v| v.parse().ok());
2006            let idle_timeout = parse_positive_duration_secs(params, "idle_timeout");
2007            let write_stall_timeout = parse_positive_duration_secs(params, "write_stall_timeout");
2008            let abuse = BackboneAbuseConfig {
2009                max_penalty_duration: parse_positive_duration_secs(params, "max_penalty_duration"),
2010            };
2011            let fast_flap = parse_fast_flap_config(params)?;
2012            let mut config = BackboneConfig {
2013                name: name.to_string(),
2014                listen_ip,
2015                listen_port,
2016                interface_id: id,
2017                mode: constants::MODE_FULL,
2018                gravity: 0,
2019                recursive_prs: false,
2020                announces_from_internal: true,
2021                announces_to_internal: None,
2022                max_connections,
2023                idle_timeout,
2024                write_stall_timeout,
2025                abuse,
2026                fast_flap,
2027                fast_flap_state: shared_fast_flap_monitor(),
2028                ingress_control: IngressControlConfig::enabled(),
2029                runtime: Arc::new(Mutex::new(BackboneServerRuntime {
2030                    max_connections: None,
2031                    idle_timeout: None,
2032                    write_stall_timeout: None,
2033                    abuse: BackboneAbuseConfig::default(),
2034                })),
2035                peer_state: Arc::new(Mutex::new(BackbonePeerMonitor::new())),
2036            };
2037            let startup = BackboneServerRuntime::from_config(&config);
2038            config.runtime = Arc::new(Mutex::new(startup));
2039            Ok(Box::new(BackboneMode::Server(config)))
2040        }
2041    }
2042
2043    fn start(
2044        &self,
2045        config: Box<dyn InterfaceConfigData>,
2046        ctx: StartContext,
2047    ) -> io::Result<StartResult> {
2048        let mode = *config.into_any().downcast::<BackboneMode>().map_err(|_| {
2049            io::Error::new(
2050                io::ErrorKind::InvalidData,
2051                "wrong config type for BackboneInterface",
2052            )
2053        })?;
2054
2055        match mode {
2056            BackboneMode::Client(cfg, _) => {
2057                let id = cfg.interface_id;
2058                let name = cfg.name.clone();
2059                let info = InterfaceInfo {
2060                    id,
2061                    name,
2062                    mode: ctx.mode,
2063                    gravity: ctx.gravity,
2064                    recursive_prs: ctx.recursive_prs,
2065                    announces_from_internal: ctx.announces_from_internal,
2066                    announces_to_internal: ctx.announces_to_internal,
2067                    out_capable: true,
2068                    in_capable: true,
2069                    bitrate: Some(CLIENT_BITRATE_GUESS),
2070                    airtime_profile: None,
2071                    announce_rate_target: None,
2072                    announce_rate_grace: 0,
2073                    announce_rate_penalty: 0.0,
2074                    announce_cap: constants::ANNOUNCE_CAP,
2075                    is_local_client: false,
2076                    wants_tunnel: false,
2077                    tunnel_id: None,
2078                    mtu: HW_MTU as u32,
2079                    ingress_control: ctx.ingress_control,
2080                    ia_freq: 0.0,
2081                    ip_freq: 0.0,
2082                    op_freq: 0.0,
2083                    op_samples: 0,
2084                    started: crate::time::now(),
2085                };
2086                let ifac_size = ctx.ifac.as_ref().map(|ifac| ifac.size).unwrap_or(0);
2087                let control = ListenerControl::new();
2088                let writer = start_client_with_ifac(
2089                    cfg,
2090                    ctx.tx,
2091                    ifac_size,
2092                    ctx.underlay_mark,
2093                    control.clone(),
2094                )?;
2095                Ok(StartResult::Simple {
2096                    control: Some(control),
2097                    id,
2098                    info,
2099                    writer,
2100                    interface_type_name: "BackboneInterface".to_string(),
2101                })
2102            }
2103            BackboneMode::Server(mut cfg) => {
2104                cfg.ingress_control = ctx.ingress_control;
2105                cfg.mode = ctx.mode;
2106                cfg.gravity = ctx.gravity;
2107                cfg.recursive_prs = ctx.recursive_prs;
2108                cfg.announces_from_internal = ctx.announces_from_internal;
2109                cfg.announces_to_internal = ctx.announces_to_internal;
2110                let parent_id = cfg.interface_id;
2111                start_with_template(
2112                    cfg,
2113                    ctx.tx,
2114                    ctx.next_dynamic_id,
2115                    Some(DynamicInterfaceTemplate {
2116                        parent_id,
2117                        interface_type: "BackboneInterfacePeer".into(),
2118                        ifac: ctx.ifac,
2119                        mode: ctx.mode,
2120                        gravity: ctx.gravity,
2121                        recursive_prs: ctx.recursive_prs,
2122                        announces_from_internal: ctx.announces_from_internal,
2123                        announces_to_internal: ctx.announces_to_internal,
2124                    }),
2125                    ctx.underlay_mark,
2126                )?;
2127                Ok(StartResult::Listener { control: None })
2128            }
2129        }
2130    }
2131}
2132
2133pub(crate) fn runtime_handle_from_mode(mode: &BackboneMode) -> Option<BackboneRuntimeConfigHandle> {
2134    match mode {
2135        BackboneMode::Server(config) => Some(BackboneRuntimeConfigHandle {
2136            interface_name: config.name.clone(),
2137            runtime: Arc::clone(&config.runtime),
2138            startup: BackboneServerRuntime::from_config(config),
2139        }),
2140        BackboneMode::Client(_, _) => None,
2141    }
2142}
2143
2144pub(crate) fn peer_state_handle_from_mode(mode: &BackboneMode) -> Option<BackbonePeerStateHandle> {
2145    match mode {
2146        BackboneMode::Server(config) => Some(BackbonePeerStateHandle {
2147            interface_id: config.interface_id,
2148            interface_name: config.name.clone(),
2149            mode: config.mode,
2150            gravity: config.gravity,
2151            announces_to_internal: config.announces_to_internal,
2152            peer_state: Arc::clone(&config.peer_state),
2153            fast_flap: config.fast_flap,
2154            fast_flap_state: Arc::clone(&config.fast_flap_state),
2155        }),
2156        BackboneMode::Client(_, _) => None,
2157    }
2158}
2159
2160pub(crate) fn client_runtime_handle_from_mode(
2161    mode: &BackboneMode,
2162) -> Option<BackboneClientRuntimeConfigHandle> {
2163    match mode {
2164        BackboneMode::Client(config, _) => Some(BackboneClientRuntimeConfigHandle {
2165            interface_name: config.name.clone(),
2166            runtime: Arc::clone(&config.runtime),
2167            startup: BackboneClientRuntime::from_config(config),
2168        }),
2169        BackboneMode::Server(_) => None,
2170    }
2171}
2172
2173pub(crate) fn client_config_from_mode(mode: &BackboneMode) -> Option<BackboneClientConfig> {
2174    match mode {
2175        BackboneMode::Client(config, _) => Some(config.clone()),
2176        BackboneMode::Server(_) => None,
2177    }
2178}
2179
2180pub(crate) fn client_priority_from_mode(mode: &BackboneMode) -> Option<u8> {
2181    match mode {
2182        BackboneMode::Client(_, priority) => Some(*priority),
2183        BackboneMode::Server(_) => None,
2184    }
2185}
2186
2187#[cfg(test)]
2188mod tests {
2189    use super::*;
2190    use std::io::Read;
2191    use std::sync::mpsc;
2192    use std::time::Duration;
2193
2194    fn find_free_port() -> u16 {
2195        crate::test_support::port()
2196    }
2197
2198    /// Serve the receiver's sockets and timers for `duration` without
2199    /// taking any event, as a driver busy with other work would.
2200    fn serve_for(rx: &crate::event::EventReceiver, duration: Duration) {
2201        let deadline = Instant::now() + duration;
2202        let mut ready = polling::Events::new();
2203        while Instant::now() < deadline {
2204            ready.clear();
2205            rx.poll_ready(&mut ready).unwrap();
2206            rx.serve_sources(&ready);
2207            thread::sleep(Duration::from_millis(2));
2208        }
2209    }
2210
2211    fn test_fast_flap_config() -> BackboneFastFlapConfig {
2212        BackboneFastFlapConfig {
2213            enabled: true,
2214            connection_threshold: Duration::from_secs(20),
2215            grace: 5,
2216            block_duration: Duration::from_secs(12 * 60 * 60),
2217        }
2218    }
2219
2220    #[test]
2221    fn fast_flap_defaults_match_upstream() {
2222        assert_eq!(BackboneFastFlapConfig::default(), test_fast_flap_config());
2223    }
2224
2225    #[test]
2226    fn sixth_short_connection_blocks_but_fifth_does_not() {
2227        let config = test_fast_flap_config();
2228        let mut monitor = BackboneFastFlapMonitor::new();
2229        let peer: IpAddr = "192.0.2.10".parse().unwrap();
2230        let start = Instant::now();
2231
2232        for flap in 1..=5 {
2233            let record = monitor
2234                .record_disconnect(
2235                    peer,
2236                    Duration::from_secs(19),
2237                    &config,
2238                    start + Duration::from_secs(flap),
2239                )
2240                .unwrap();
2241            assert_eq!(record.flap_count, flap);
2242            assert!(!record.blocked);
2243            assert!(!monitor.is_blocked(peer, &config, start + Duration::from_secs(flap)));
2244        }
2245
2246        let sixth = monitor
2247            .record_disconnect(
2248                peer,
2249                Duration::from_secs(1),
2250                &config,
2251                start + Duration::from_secs(6),
2252            )
2253            .unwrap();
2254        assert_eq!(sixth.flap_count, 6);
2255        assert!(sixth.blocked);
2256        assert!(sixth.newly_blocked);
2257        assert!(monitor.is_blocked(peer, &config, start + Duration::from_secs(6)));
2258    }
2259
2260    #[test]
2261    fn every_short_connection_counts_even_after_receiving_data() {
2262        let config = test_fast_flap_config();
2263        let mut monitor = BackboneFastFlapMonitor::new();
2264        let peer: IpAddr = "192.0.2.11".parse().unwrap();
2265        let now = Instant::now();
2266
2267        let record = monitor
2268            .record_disconnect(peer, Duration::from_secs(2), &config, now)
2269            .unwrap();
2270        assert_eq!(record.flap_count, 1);
2271        assert_eq!(monitor.flap_count(peer), 1);
2272    }
2273
2274    #[test]
2275    fn threshold_boundary_and_long_connections_are_not_counted() {
2276        let config = test_fast_flap_config();
2277        let mut monitor = BackboneFastFlapMonitor::new();
2278        let peer: IpAddr = "192.0.2.12".parse().unwrap();
2279        let now = Instant::now();
2280
2281        assert!(monitor
2282            .record_disconnect(peer, Duration::from_secs(20), &config, now)
2283            .is_none());
2284        assert!(monitor
2285            .record_disconnect(peer, Duration::from_secs(21), &config, now)
2286            .is_none());
2287        assert_eq!(monitor.flap_count(peer), 0);
2288    }
2289
2290    #[test]
2291    fn disabled_fast_flapping_neither_tracks_nor_blocks() {
2292        let mut config = test_fast_flap_config();
2293        config.enabled = false;
2294        let mut monitor = BackboneFastFlapMonitor::new();
2295        let peer: IpAddr = "192.0.2.13".parse().unwrap();
2296        let now = Instant::now();
2297
2298        for offset in 0..10 {
2299            assert!(monitor
2300                .record_disconnect(
2301                    peer,
2302                    Duration::from_secs(1),
2303                    &config,
2304                    now + Duration::from_secs(offset),
2305                )
2306                .is_none());
2307        }
2308        assert_eq!(monitor.flap_count(peer), 0);
2309        assert!(!monitor.is_blocked(peer, &config, now + Duration::from_secs(10)));
2310        assert_eq!(monitor.blocked_ip_count(&config, now), 0);
2311    }
2312
2313    #[test]
2314    fn flap_blocks_expire_strictly_after_configured_duration() {
2315        let mut config = test_fast_flap_config();
2316        config.grace = 0;
2317        config.block_duration = Duration::from_secs(60);
2318        let mut monitor = BackboneFastFlapMonitor::new();
2319        let peer: IpAddr = "192.0.2.14".parse().unwrap();
2320        let now = Instant::now();
2321
2322        monitor
2323            .record_disconnect(peer, Duration::from_secs(1), &config, now)
2324            .unwrap();
2325        assert!(monitor.is_blocked(peer, &config, now + Duration::from_secs(60)));
2326        assert!(!monitor.is_blocked(
2327            peer,
2328            &config,
2329            now + Duration::from_secs(60) + Duration::from_nanos(1)
2330        ));
2331        assert_eq!(monitor.flap_count(peer), 0);
2332    }
2333
2334    #[test]
2335    fn fast_flap_history_is_isolated_by_ip() {
2336        let mut config = test_fast_flap_config();
2337        config.grace = 1;
2338        let mut monitor = BackboneFastFlapMonitor::new();
2339        let first: IpAddr = "192.0.2.15".parse().unwrap();
2340        let second: IpAddr = "192.0.2.16".parse().unwrap();
2341        let now = Instant::now();
2342
2343        for offset in 0..2 {
2344            monitor
2345                .record_disconnect(
2346                    first,
2347                    Duration::from_secs(1),
2348                    &config,
2349                    now + Duration::from_secs(offset),
2350                )
2351                .unwrap();
2352        }
2353        monitor
2354            .record_disconnect(second, Duration::from_secs(1), &config, now)
2355            .unwrap();
2356
2357        assert!(monitor.is_blocked(first, &config, now + Duration::from_secs(2)));
2358        assert!(!monitor.is_blocked(second, &config, now + Duration::from_secs(2)));
2359        assert_eq!(
2360            monitor.blocked_ip_count(&config, now + Duration::from_secs(2)),
2361            1
2362        );
2363        assert_eq!(
2364            monitor.blocked_ip_list(&config, now + Duration::from_secs(2)),
2365            vec![first],
2366            "tracked peers within the grace allowance must not appear as blocked"
2367        );
2368    }
2369
2370    #[test]
2371    fn blocked_ip_list_is_sorted_current_and_empty_when_disabled() {
2372        let mut config = test_fast_flap_config();
2373        config.grace = 0;
2374        config.block_duration = Duration::from_secs(60);
2375        let mut monitor = BackboneFastFlapMonitor::new();
2376        let first: IpAddr = "203.0.113.20".parse().unwrap();
2377        let second: IpAddr = "192.0.2.10".parse().unwrap();
2378        let now = Instant::now();
2379        monitor
2380            .record_disconnect(first, Duration::from_secs(1), &config, now)
2381            .unwrap();
2382        monitor
2383            .record_disconnect(second, Duration::from_secs(1), &config, now)
2384            .unwrap();
2385
2386        assert_eq!(monitor.blocked_ip_list(&config, now), vec![second, first]);
2387        assert!(monitor
2388            .blocked_ip_list(&config, now + Duration::from_secs(61))
2389            .is_empty());
2390        config.enabled = false;
2391        assert!(monitor.blocked_ip_list(&config, now).is_empty());
2392    }
2393
2394    #[test]
2395    fn peer_monitor_snapshot_clears_expired_blacklist_state() {
2396        let peer: IpAddr = "192.0.2.17".parse().unwrap();
2397        let mut monitor = BackbonePeerMonitor::new();
2398        monitor.blacklist(peer, Duration::from_secs(60), "temporary".into());
2399        let mut peers = HashMap::new();
2400        monitor.sync_into(&mut peers);
2401        let state = peers.get_mut(&peer).unwrap();
2402        state.blacklisted_until = None;
2403        state.blacklist_reason = None;
2404
2405        monitor.upsert_snapshot(&peers);
2406
2407        let entry = monitor.list("test").into_iter().next().unwrap();
2408        assert!(entry.blacklisted_remaining_secs.is_none());
2409        assert!(entry.blacklist_reason.is_none());
2410    }
2411
2412    fn recv_non_peer_event(
2413        rx: &crate::event::EventReceiver,
2414        timeout: Duration,
2415    ) -> Result<Event, mpsc::RecvTimeoutError> {
2416        let deadline = Instant::now() + timeout;
2417        loop {
2418            let remaining = deadline.saturating_duration_since(Instant::now());
2419            if remaining.is_zero() {
2420                return Err(mpsc::RecvTimeoutError::Timeout);
2421            }
2422            let event = rx.recv_timeout(remaining)?;
2423            match event {
2424                Event::BackbonePeerConnected { .. }
2425                | Event::BackbonePeerDisconnected { .. }
2426                | Event::BackbonePeerIdleTimeout { .. }
2427                | Event::BackbonePeerWriteStall { .. }
2428                | Event::BackbonePeerPenalty { .. } => continue,
2429                other => return Ok(other),
2430            }
2431        }
2432    }
2433
2434    fn make_server_config(
2435        port: u16,
2436        interface_id: u64,
2437        max_connections: Option<usize>,
2438        idle_timeout: Option<Duration>,
2439        write_stall_timeout: Option<Duration>,
2440        abuse: BackboneAbuseConfig,
2441    ) -> BackboneConfig {
2442        let mut config = BackboneConfig {
2443            name: "test-backbone".into(),
2444            listen_ip: "127.0.0.1".into(),
2445            listen_port: port,
2446            interface_id: InterfaceId(interface_id),
2447            mode: constants::MODE_FULL,
2448            gravity: 0,
2449            recursive_prs: false,
2450            announces_from_internal: true,
2451            announces_to_internal: None,
2452            max_connections,
2453            idle_timeout,
2454            write_stall_timeout,
2455            abuse,
2456            fast_flap: BackboneFastFlapConfig::default(),
2457            fast_flap_state: Arc::new(Mutex::new(BackboneFastFlapMonitor::new())),
2458            ingress_control: IngressControlConfig::enabled(),
2459            runtime: Arc::new(Mutex::new(BackboneServerRuntime {
2460                max_connections: None,
2461                idle_timeout: None,
2462                write_stall_timeout: None,
2463                abuse: BackboneAbuseConfig::default(),
2464            })),
2465            peer_state: Arc::new(Mutex::new(BackbonePeerMonitor::new())),
2466        };
2467        let startup = BackboneServerRuntime::from_config(&config);
2468        config.runtime = Arc::new(Mutex::new(startup));
2469        config
2470    }
2471
2472    #[test]
2473    fn backbone_accept_connection() {
2474        let port = find_free_port();
2475        let (tx, rx) = crate::event::channel();
2476        let next_id = Arc::new(AtomicU64::new(8000));
2477
2478        let config = make_server_config(port, 80, None, None, None, BackboneAbuseConfig::default());
2479
2480        start(config, tx, next_id).unwrap();
2481        thread::sleep(Duration::from_millis(50));
2482
2483        let _client = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
2484
2485        let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
2486        match event {
2487            Event::InterfaceUp(id, writer, info) => {
2488                assert_eq!(id, InterfaceId(8000));
2489                assert!(writer.is_some());
2490                assert!(info.is_some());
2491                let info = info.unwrap();
2492                assert!(info.out_capable);
2493                assert!(info.in_capable);
2494                assert_eq!(info.bitrate, Some(SERVER_BITRATE_GUESS));
2495                assert_eq!(info.mtu, SERVER_PEER_MTU);
2496            }
2497            other => panic!("expected InterfaceUp, got {:?}", other),
2498        }
2499    }
2500
2501    #[test]
2502    fn backbone_class_defaults_preserve_server_and_client_distinction() {
2503        assert_eq!(SERVER_BITRATE_GUESS, 100_000_000);
2504        assert_eq!(SERVER_PEER_MTU, 32_768);
2505        assert_eq!(CLIENT_BITRATE_GUESS, 100_000_000);
2506        assert_eq!(HW_MTU, 32_768);
2507    }
2508
2509    #[test]
2510    fn auto_mtu_thresholds_include_exact_bitrate_boundaries() {
2511        for (bitrate, mtu) in [
2512            (1_000_000_000, Some(524_288)),
2513            (750_000_000, Some(262_144)),
2514            (400_000_000, Some(131_072)),
2515            (200_000_000, Some(65_536)),
2516            (100_000_000, Some(32_768)),
2517            (10_000_000, Some(16_384)),
2518            (5_000_000, Some(8_192)),
2519            (2_000_000, Some(4_096)),
2520            (1_000_000, Some(2_048)),
2521            (62_500, Some(1_024)),
2522            (62_499, None),
2523        ] {
2524            assert_eq!(auto_mtu_for_bitrate(bitrate), mtu, "bitrate {bitrate}");
2525        }
2526    }
2527
2528    #[test]
2529    fn backbone_accepted_connection_inherits_server_mode() {
2530        let port = find_free_port();
2531        let (tx, rx) = crate::event::channel();
2532        let next_id = Arc::new(AtomicU64::new(8050));
2533
2534        let mut config =
2535            make_server_config(port, 85, None, None, None, BackboneAbuseConfig::default());
2536        config.mode = constants::MODE_GATEWAY;
2537
2538        start(config, tx, next_id).unwrap();
2539        thread::sleep(Duration::from_millis(50));
2540
2541        let _client = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
2542
2543        let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
2544        match event {
2545            Event::InterfaceUp(_, _, Some(info)) => {
2546                assert_eq!(info.mode, constants::MODE_GATEWAY);
2547            }
2548            other => panic!("expected InterfaceUp with info, got {:?}", other),
2549        }
2550    }
2551
2552    #[test]
2553    fn dataplane_ingress_thresholds_match_upstream_defaults() {
2554        assert_eq!(
2555            ingress_thresholds(1024),
2556            IngressThresholds {
2557                high: 921,
2558                immediate: 921,
2559                mid: 696,
2560                low: 102,
2561            }
2562        );
2563        assert_eq!(
2564            ingress_thresholds(8),
2565            IngressThresholds {
2566                high: 7,
2567                immediate: 128,
2568                mid: 5,
2569                low: 0,
2570            }
2571        );
2572    }
2573
2574    #[test]
2575    fn dataplane_ingress_selects_the_largest_packet_producer() {
2576        assert_eq!(
2577            busiest_ingress_key([(7, 12), (8, 41), (9, 3)].into_iter()),
2578            Some(8)
2579        );
2580        assert_eq!(busiest_ingress_key([(7, 0), (8, 0)].into_iter()), None);
2581    }
2582
2583    #[test]
2584    fn backbone_gates_the_largest_producer_before_data_queue_overflow() {
2585        let port = find_free_port();
2586        let capacities = crate::event::InboundQueueCapacities {
2587            data: 200,
2588            announce: 8,
2589            path_request: 8,
2590            ingress_limited: 8,
2591        };
2592        let (tx, rx) = crate::event::channel_with_queue_capacities(16, capacities);
2593        let observer = tx.clone();
2594        let next_id = Arc::new(AtomicU64::new(8075));
2595        let config = make_server_config(port, 80, None, None, None, BackboneAbuseConfig::default());
2596
2597        start(config, tx, next_id).unwrap();
2598        thread::sleep(Duration::from_millis(50));
2599        let mut client = TcpStream::connect(format!("127.0.0.1:{port}")).unwrap();
2600        let _ = recv_non_peer_event(&rx, Duration::from_secs(1)).unwrap();
2601
2602        let payload = vec![0u8; 32];
2603        let framed = hdlc::frame(&payload);
2604        let mut burst = Vec::with_capacity(framed.len() * 180);
2605        for _ in 0..180 {
2606            burst.extend_from_slice(&framed);
2607        }
2608        client.write_all(&burst).unwrap();
2609
2610        let deadline = Instant::now() + Duration::from_secs(2);
2611        while observer.inbound_queue_snapshot().heights[0] < 180 && Instant::now() < deadline {
2612            serve_for(&rx, Duration::from_millis(5));
2613        }
2614        serve_for(&rx, Duration::from_millis(100));
2615        let gated = observer.inbound_queue_snapshot();
2616        assert!(
2617            gated.heights[0] >= 180,
2618            "DATA queue never reached the high watermark"
2619        );
2620        assert_eq!(gated.dropped[0], 0);
2621
2622        client.write_all(&hdlc::frame(&[1u8; 32])).unwrap();
2623        serve_for(&rx, Duration::from_millis(300));
2624        let after_marker = observer.inbound_queue_snapshot();
2625        assert_eq!(after_marker.heights[0], gated.heights[0]);
2626        assert_eq!(
2627            after_marker.dropped[0], gated.dropped[0],
2628            "the producer remained readable and overflowed the DATA queue"
2629        );
2630
2631        for _ in 0..gated.heights[0] {
2632            let event = recv_non_peer_event(&rx, Duration::from_secs(1)).unwrap();
2633            assert!(matches!(event, Event::Frame { .. }));
2634        }
2635        let released = recv_non_peer_event(&rx, Duration::from_secs(5)).unwrap();
2636        assert!(
2637            matches!(released, Event::Frame { data, .. } if data == vec![1u8; 32]),
2638            "held producer was not released after DATA pressure recovered"
2639        );
2640    }
2641
2642    #[test]
2643    fn backbone_receive_frame() {
2644        let port = find_free_port();
2645        let (tx, rx) = crate::event::channel();
2646        let next_id = Arc::new(AtomicU64::new(8100));
2647
2648        let config = make_server_config(port, 81, None, None, None, BackboneAbuseConfig::default());
2649
2650        start(config, tx, next_id).unwrap();
2651        thread::sleep(Duration::from_millis(50));
2652
2653        let mut client = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
2654
2655        // Drain InterfaceUp
2656        let _ = recv_non_peer_event(&rx, Duration::from_secs(1)).unwrap();
2657
2658        // The exact Reticulum header minimum must be dropped without
2659        // preventing recovery of the next valid frame.
2660        client
2661            .write_all(&hdlc::frame(&[0x11; rns_core::constants::HEADER_MINSIZE]))
2662            .unwrap();
2663        let payload: Vec<u8> = (0..32).collect();
2664        client.write_all(&hdlc::frame(&payload)).unwrap();
2665
2666        let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
2667        match event {
2668            Event::Frame {
2669                interface_id,
2670                data,
2671                rssi: _,
2672                snr: _,
2673            } => {
2674                assert_eq!(interface_id, InterfaceId(8100));
2675                assert_eq!(data, payload);
2676            }
2677            other => panic!("expected Frame, got {:?}", other),
2678        }
2679    }
2680
2681    #[test]
2682    fn backbone_send_to_client() {
2683        let port = find_free_port();
2684        let (tx, rx) = crate::event::channel();
2685        let next_id = Arc::new(AtomicU64::new(8200));
2686
2687        let config = make_server_config(port, 82, None, None, None, BackboneAbuseConfig::default());
2688
2689        start(config, tx, next_id).unwrap();
2690        thread::sleep(Duration::from_millis(50));
2691
2692        let mut client = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
2693        client
2694            .set_read_timeout(Some(Duration::from_secs(2)))
2695            .unwrap();
2696
2697        // Get writer from InterfaceUp
2698        let event = recv_non_peer_event(&rx, Duration::from_secs(1)).unwrap();
2699        let mut writer = match event {
2700            Event::InterfaceUp(_, Some(w), _) => w,
2701            other => panic!("expected InterfaceUp with writer, got {:?}", other),
2702        };
2703
2704        // Send frame via writer
2705        let payload: Vec<u8> = (0..24).collect();
2706        writer.send_frame(&payload).unwrap();
2707
2708        // Read from client
2709        let mut buf = [0u8; 256];
2710        let n = client.read(&mut buf).unwrap();
2711        let expected = hdlc::frame(&payload);
2712        assert_eq!(&buf[..n], &expected[..]);
2713    }
2714
2715    #[test]
2716    fn backbone_write_backpressure_does_not_starve_reads() {
2717        let port = find_free_port();
2718        let (tx, rx) = crate::event::channel();
2719        let next_id = Arc::new(AtomicU64::new(8250));
2720        let config = make_server_config(port, 82, None, None, None, BackboneAbuseConfig::default());
2721
2722        start(config, tx, next_id).unwrap();
2723        thread::sleep(Duration::from_millis(50));
2724
2725        let mut client = TcpStream::connect(format!("127.0.0.1:{port}")).unwrap();
2726        SockRef::from(&client).set_recv_buffer_size(4096).unwrap();
2727        let event = recv_non_peer_event(&rx, Duration::from_secs(1)).unwrap();
2728        let mut writer = match event {
2729            Event::InterfaceUp(_, Some(writer), _) => writer,
2730            other => panic!("expected InterfaceUp with writer, got {other:?}"),
2731        };
2732
2733        let outbound = vec![0x55; 60 * 1024];
2734        let deadline = Instant::now() + Duration::from_secs(3);
2735        let mut backpressured = false;
2736        while Instant::now() < deadline {
2737            match writer.send_frame(&outbound) {
2738                Ok(()) => {}
2739                Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
2740                    backpressured = true;
2741                    break;
2742                }
2743                Err(error) => panic!("unexpected backbone write error: {error}"),
2744            }
2745        }
2746        assert!(
2747            backpressured,
2748            "failed to create outbound write backpressure"
2749        );
2750
2751        let inbound: Vec<u8> = (0..32).collect();
2752        client.write_all(&hdlc::frame(&inbound)).unwrap();
2753        let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
2754        match event {
2755            Event::Frame { data, .. } => assert_eq!(data, inbound),
2756            other => panic!("inbound frame was starved by outbound backpressure: {other:?}"),
2757        }
2758    }
2759
2760    #[test]
2761    fn backbone_multiple_clients() {
2762        let port = find_free_port();
2763        let (tx, rx) = crate::event::channel();
2764        let next_id = Arc::new(AtomicU64::new(8300));
2765
2766        let config = make_server_config(port, 83, None, None, None, BackboneAbuseConfig::default());
2767
2768        start(config, tx, next_id).unwrap();
2769        thread::sleep(Duration::from_millis(50));
2770
2771        let _client1 = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
2772        let _client2 = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
2773
2774        let mut ids = Vec::new();
2775        for _ in 0..2 {
2776            let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
2777            match event {
2778                Event::InterfaceUp(id, _, _) => ids.push(id),
2779                other => panic!("expected InterfaceUp, got {:?}", other),
2780            }
2781        }
2782
2783        assert_eq!(ids.len(), 2);
2784        assert_ne!(ids[0], ids[1]);
2785    }
2786
2787    #[test]
2788    fn backbone_client_disconnect() {
2789        let port = find_free_port();
2790        let (tx, rx) = crate::event::channel();
2791        let next_id = Arc::new(AtomicU64::new(8400));
2792
2793        let config = make_server_config(port, 84, None, None, None, BackboneAbuseConfig::default());
2794
2795        start(config, tx, next_id).unwrap();
2796        thread::sleep(Duration::from_millis(50));
2797
2798        let client = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
2799
2800        // Drain InterfaceUp
2801        let _ = recv_non_peer_event(&rx, Duration::from_secs(1)).unwrap();
2802
2803        // Disconnect
2804        drop(client);
2805
2806        // Should receive InterfaceDown
2807        let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
2808        assert!(
2809            matches!(event, Event::InterfaceDown(InterfaceId(8400))),
2810            "expected InterfaceDown(8400), got {:?}",
2811            event
2812        );
2813    }
2814
2815    #[test]
2816    fn backbone_epoll_multiplexing() {
2817        let port = find_free_port();
2818        let (tx, rx) = crate::event::channel();
2819        let next_id = Arc::new(AtomicU64::new(8500));
2820
2821        let config = make_server_config(port, 85, None, None, None, BackboneAbuseConfig::default());
2822
2823        start(config, tx, next_id).unwrap();
2824        thread::sleep(Duration::from_millis(50));
2825
2826        let mut client1 = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
2827        let mut client2 = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
2828
2829        // Drain both InterfaceUp events
2830        let _ = recv_non_peer_event(&rx, Duration::from_secs(1)).unwrap();
2831        let _ = recv_non_peer_event(&rx, Duration::from_secs(1)).unwrap();
2832
2833        // Both clients send data simultaneously
2834        let payload1: Vec<u8> = (0..24).collect();
2835        let payload2: Vec<u8> = (100..130).collect();
2836        client1.write_all(&hdlc::frame(&payload1)).unwrap();
2837        client2.write_all(&hdlc::frame(&payload2)).unwrap();
2838
2839        // Should receive both Frame events
2840        let mut received = Vec::new();
2841        for _ in 0..2 {
2842            let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
2843            match event {
2844                Event::Frame { data, .. } => received.push(data),
2845                other => panic!("expected Frame, got {:?}", other),
2846            }
2847        }
2848        assert!(received.contains(&payload1));
2849        assert!(received.contains(&payload2));
2850    }
2851
2852    #[test]
2853    fn backbone_bind_port() {
2854        let port = find_free_port();
2855        let (tx, _rx) = crate::event::channel();
2856        let next_id = Arc::new(AtomicU64::new(8600));
2857
2858        let config = make_server_config(port, 86, None, None, None, BackboneAbuseConfig::default());
2859
2860        // Should not error
2861        start(config, tx, next_id).unwrap();
2862    }
2863
2864    #[test]
2865    fn backbone_hdlc_fragmented() {
2866        let port = find_free_port();
2867        let (tx, rx) = crate::event::channel();
2868        let next_id = Arc::new(AtomicU64::new(8700));
2869
2870        let config = make_server_config(port, 87, None, None, None, BackboneAbuseConfig::default());
2871
2872        start(config, tx, next_id).unwrap();
2873        thread::sleep(Duration::from_millis(50));
2874
2875        let mut client = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
2876        client.set_nodelay(true).unwrap();
2877
2878        // Drain InterfaceUp
2879        let _ = recv_non_peer_event(&rx, Duration::from_secs(1)).unwrap();
2880
2881        // Send HDLC frame in two fragments
2882        let payload: Vec<u8> = (0..32).collect();
2883        let framed = hdlc::frame(&payload);
2884        let mid = framed.len() / 2;
2885
2886        client.write_all(&framed[..mid]).unwrap();
2887        thread::sleep(Duration::from_millis(50));
2888        client.write_all(&framed[mid..]).unwrap();
2889
2890        // Should receive reassembled frame
2891        let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
2892        match event {
2893            Event::Frame { data, .. } => {
2894                assert_eq!(data, payload);
2895            }
2896            other => panic!("expected Frame, got {:?}", other),
2897        }
2898    }
2899
2900    // -----------------------------------------------------------------------
2901    // Client mode tests
2902    // -----------------------------------------------------------------------
2903
2904    fn make_client_config(port: u16, id: u64) -> BackboneClientConfig {
2905        BackboneClientConfig {
2906            name: format!("test-bb-client-{}", port),
2907            target_host: "127.0.0.1".into(),
2908            target_port: port,
2909            interface_id: InterfaceId(id),
2910            reconnect_wait: Duration::from_millis(100),
2911            max_reconnect_tries: Some(2),
2912            connect_timeout: Duration::from_secs(2),
2913            transport_identity: None,
2914            runtime: Arc::new(Mutex::new(BackboneClientRuntime {
2915                reconnect_wait: Duration::from_millis(100),
2916                max_reconnect_tries: Some(2),
2917                connect_timeout: Duration::from_secs(2),
2918            })),
2919        }
2920    }
2921
2922    #[test]
2923    fn backbone_client_connect() {
2924        let port = find_free_port();
2925        let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
2926        let (tx, rx) = crate::event::channel();
2927
2928        let config = make_client_config(port, 9000);
2929        let _writer = start_client(config, tx).unwrap();
2930
2931        let _server_stream = listener.accept().unwrap();
2932
2933        let event = rx.recv_timeout(Duration::from_secs(2)).unwrap();
2934        assert!(matches!(event, Event::InterfaceUp(InterfaceId(9000), _, _)));
2935    }
2936
2937    #[test]
2938    fn backbone_client_receive_frame() {
2939        let port = find_free_port();
2940        let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
2941        let (tx, rx) = crate::event::channel();
2942
2943        let config = make_client_config(port, 9100);
2944        let _writer = start_client(config, tx).unwrap();
2945
2946        let (mut server_stream, _) = listener.accept().unwrap();
2947
2948        // Drain InterfaceUp
2949        let _ = rx.recv_timeout(Duration::from_secs(1)).unwrap();
2950
2951        // Client mode applies the same strict lower bound and recovers.
2952        server_stream
2953            .write_all(&hdlc::frame(&[0x11; rns_core::constants::HEADER_MINSIZE]))
2954            .unwrap();
2955        let payload: Vec<u8> = (0..32).collect();
2956        server_stream.write_all(&hdlc::frame(&payload)).unwrap();
2957
2958        let event = rx.recv_timeout(Duration::from_secs(2)).unwrap();
2959        match event {
2960            Event::Frame {
2961                interface_id,
2962                data,
2963                rssi: _,
2964                snr: _,
2965            } => {
2966                assert_eq!(interface_id, InterfaceId(9100));
2967                assert_eq!(data, payload);
2968            }
2969            other => panic!("expected Frame, got {:?}", other),
2970        }
2971    }
2972
2973    #[test]
2974    fn backbone_client_send_frame() {
2975        let port = find_free_port();
2976        let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
2977        let (tx, _rx) = crate::event::channel();
2978
2979        let config = make_client_config(port, 9200);
2980        let mut writer = start_client(config, tx).unwrap();
2981
2982        let (mut server_stream, _) = listener.accept().unwrap();
2983        server_stream
2984            .set_read_timeout(Some(Duration::from_secs(2)))
2985            .unwrap();
2986
2987        let payload: Vec<u8> = (0..24).collect();
2988        writer.send_frame(&payload).unwrap();
2989
2990        let mut buf = [0u8; 256];
2991        let n = server_stream.read(&mut buf).unwrap();
2992        let expected = hdlc::frame(&payload);
2993        assert_eq!(&buf[..n], &expected[..]);
2994    }
2995
2996    #[test]
2997    fn backbone_max_connections_rejects_excess() {
2998        let port = find_free_port();
2999        let (tx, rx) = crate::event::channel();
3000        let next_id = Arc::new(AtomicU64::new(8800));
3001
3002        let config = make_server_config(
3003            port,
3004            88,
3005            Some(2),
3006            None,
3007            None,
3008            BackboneAbuseConfig::default(),
3009        );
3010
3011        start(config, tx, next_id).unwrap();
3012        thread::sleep(Duration::from_millis(50));
3013
3014        // Connect two clients (at limit)
3015        let _client1 = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
3016        let _client2 = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
3017
3018        // Drain both InterfaceUp events
3019        for _ in 0..2 {
3020            let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
3021            assert!(matches!(event, Event::InterfaceUp(_, _, _)));
3022        }
3023
3024        // Third connection should be accepted at TCP level but immediately dropped
3025        let client3 = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
3026        client3
3027            .set_read_timeout(Some(Duration::from_millis(500)))
3028            .unwrap();
3029
3030        // Give server time to reject
3031        thread::sleep(Duration::from_millis(100));
3032
3033        // Should NOT receive a third InterfaceUp
3034        let result = recv_non_peer_event(&rx, Duration::from_millis(500));
3035        assert!(
3036            result.is_err(),
3037            "expected no InterfaceUp for rejected connection, got {:?}",
3038            result
3039        );
3040    }
3041
3042    #[test]
3043    fn backbone_max_connections_allows_after_disconnect() {
3044        let port = find_free_port();
3045        let (tx, rx) = crate::event::channel();
3046        let next_id = Arc::new(AtomicU64::new(8900));
3047
3048        let config = make_server_config(
3049            port,
3050            89,
3051            Some(1),
3052            None,
3053            None,
3054            BackboneAbuseConfig::default(),
3055        );
3056
3057        start(config, tx, next_id).unwrap();
3058        thread::sleep(Duration::from_millis(50));
3059
3060        // Connect first client
3061        let client1 = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
3062        let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
3063        assert!(matches!(event, Event::InterfaceUp(_, _, _)));
3064
3065        // Disconnect first client
3066        drop(client1);
3067
3068        // Wait for InterfaceDown
3069        let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
3070        assert!(matches!(event, Event::InterfaceDown(_)));
3071
3072        // Now a new connection should be accepted
3073        let _client2 = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
3074        let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
3075        assert!(
3076            matches!(event, Event::InterfaceUp(_, _, _)),
3077            "expected InterfaceUp after slot freed, got {:?}",
3078            event
3079        );
3080    }
3081
3082    #[test]
3083    fn backbone_client_reconnect() {
3084        let port = find_free_port();
3085        let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
3086        listener.set_nonblocking(false).unwrap();
3087        let (tx, rx) = crate::event::channel();
3088
3089        let config = make_client_config(port, 9300);
3090        let _writer = start_client(config, tx).unwrap();
3091
3092        // Accept first connection and immediately close it
3093        let (server_stream, _) = listener.accept().unwrap();
3094
3095        // Drain InterfaceUp
3096        let _ = rx.recv_timeout(Duration::from_secs(1)).unwrap();
3097
3098        drop(server_stream);
3099
3100        // Should get InterfaceDown
3101        let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
3102        assert!(matches!(event, Event::InterfaceDown(InterfaceId(9300))));
3103
3104        // Accept the reconnection
3105        let _server_stream2 = listener.accept().unwrap();
3106
3107        // Should get InterfaceUp again
3108        let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
3109        assert!(matches!(event, Event::InterfaceUp(InterfaceId(9300), _, _)));
3110    }
3111
3112    #[test]
3113    fn a_reconnected_backbone_client_is_read_on_the_receiving_thread() {
3114        let port = find_free_port();
3115        let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
3116        let (tx, rx) = crate::event::channel();
3117        let _writer = start_client(make_client_config(port, 9310), tx).unwrap();
3118        let (first, _) = listener.accept().unwrap();
3119        assert!(matches!(
3120            rx.recv_timeout(Duration::from_secs(1)).unwrap(),
3121            Event::InterfaceUp(InterfaceId(9310), None, _)
3122        ));
3123        drop(first);
3124        assert!(matches!(
3125            recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap(),
3126            Event::InterfaceDown(InterfaceId(9310))
3127        ));
3128        let (mut second, _) = listener.accept().unwrap();
3129        assert!(matches!(
3130            recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap(),
3131            Event::InterfaceUp(InterfaceId(9310), Some(_), _)
3132        ));
3133        let payload: Vec<u8> = (0..32).collect();
3134        second.write_all(&hdlc::frame(&payload)).unwrap();
3135        match recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap() {
3136            Event::Frame { data, .. } => assert_eq!(data, payload),
3137            other => panic!("expected Frame, got {:?}", other),
3138        }
3139        // The reconnect thread exits once the new socket is served here.
3140        let reconnecting = || {
3141            std::fs::read_dir("/proc/self/task")
3142                .unwrap()
3143                .filter_map(|task| std::fs::read_to_string(task.ok()?.path().join("comm")).ok())
3144                .any(|name| name.trim_end() == "bb-reconn-9310")
3145        };
3146        let deadline = Instant::now() + Duration::from_secs(2);
3147        while reconnecting() && Instant::now() < deadline {
3148            thread::sleep(Duration::from_millis(10));
3149        }
3150        assert!(!reconnecting());
3151    }
3152
3153    #[test]
3154    fn backbone_client_stop_prevents_reconnect() {
3155        let port = find_free_port();
3156        let listener = TcpListener::bind(("127.0.0.1", port)).unwrap();
3157        let (tx, rx) = crate::event::channel();
3158
3159        let config = make_client_config(port, 9350);
3160        let control = ListenerControl::new();
3161        let _writer = start_client_with_ifac(config, tx, 0, None, control.clone()).unwrap();
3162
3163        let (server_stream, _) = listener.accept().unwrap();
3164        // Drain the initial InterfaceUp from the successful connect.
3165        let _ = recv_non_peer_event(&rx, Duration::from_secs(1)).unwrap();
3166
3167        control.request_stop();
3168        drop(server_stream);
3169
3170        // A detached client must not emit InterfaceDown or reconnect.
3171        if let Ok(event) = recv_non_peer_event(&rx, Duration::from_millis(700)) {
3172            panic!("unexpected event after stop: {:?}", event);
3173        }
3174    }
3175
3176    #[test]
3177    fn backbone_idle_timeout_disconnects_silent_client() {
3178        let port = find_free_port();
3179        let (tx, rx) = crate::event::channel();
3180        let next_id = Arc::new(AtomicU64::new(9400));
3181
3182        let config = make_server_config(
3183            port,
3184            94,
3185            None,
3186            Some(Duration::from_millis(150)),
3187            None,
3188            BackboneAbuseConfig::default(),
3189        );
3190
3191        start(config, tx, next_id).unwrap();
3192        thread::sleep(Duration::from_millis(50));
3193
3194        let _client = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
3195
3196        let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
3197        let client_id = match event {
3198            Event::InterfaceUp(id, _, _) => id,
3199            other => panic!("expected InterfaceUp, got {:?}", other),
3200        };
3201
3202        let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
3203        assert!(matches!(event, Event::InterfaceDown(id) if id == client_id));
3204    }
3205
3206    #[test]
3207    fn backbone_idle_timeout_ignores_client_after_data() {
3208        let port = find_free_port();
3209        let (tx, rx) = crate::event::channel();
3210        let next_id = Arc::new(AtomicU64::new(9500));
3211
3212        let config = make_server_config(
3213            port,
3214            95,
3215            None,
3216            Some(Duration::from_millis(200)),
3217            None,
3218            BackboneAbuseConfig::default(),
3219        );
3220
3221        start(config, tx, next_id).unwrap();
3222        thread::sleep(Duration::from_millis(50));
3223
3224        let mut client = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
3225
3226        let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
3227        let client_id = match event {
3228            Event::InterfaceUp(id, _, _) => id,
3229            other => panic!("expected InterfaceUp, got {:?}", other),
3230        };
3231
3232        client.write_all(&hdlc::frame(&[1u8; 24])).unwrap();
3233
3234        let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
3235        match event {
3236            Event::Frame {
3237                interface_id,
3238                data,
3239                rssi: _,
3240                snr: _,
3241            } => {
3242                assert_eq!(interface_id, client_id);
3243                assert_eq!(data, vec![1u8; 24]);
3244            }
3245            other => panic!("expected Frame, got {:?}", other),
3246        }
3247
3248        let result = recv_non_peer_event(&rx, Duration::from_millis(500));
3249        assert!(
3250            result.is_err(),
3251            "expected no InterfaceDown after client sent data, got {:?}",
3252            result
3253        );
3254    }
3255
3256    #[test]
3257    fn backbone_runtime_idle_timeout_updates_live() {
3258        let port = find_free_port();
3259        let (tx, rx) = crate::event::channel();
3260        let next_id = Arc::new(AtomicU64::new(9650));
3261
3262        let config = make_server_config(port, 97, None, None, None, BackboneAbuseConfig::default());
3263        let runtime = Arc::clone(&config.runtime);
3264
3265        start(config, tx, next_id).unwrap();
3266        thread::sleep(Duration::from_millis(50));
3267
3268        let _client = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
3269        let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
3270        let client_id = match event {
3271            Event::InterfaceUp(id, _, _) => id,
3272            other => panic!("expected InterfaceUp, got {:?}", other),
3273        };
3274
3275        {
3276            let mut runtime = runtime.lock().unwrap();
3277            runtime.idle_timeout = Some(Duration::from_millis(150));
3278        }
3279
3280        let event = recv_non_peer_event(&rx, Duration::from_secs(4)).unwrap();
3281        assert!(matches!(event, Event::InterfaceDown(id) if id == client_id));
3282    }
3283
3284    #[test]
3285    fn backbone_write_stall_timeout_disconnects_unwritable_client() {
3286        let port = find_free_port();
3287        let (tx, rx) = crate::event::channel();
3288        let next_id = Arc::new(AtomicU64::new(9660));
3289
3290        let config = make_server_config(
3291            port,
3292            98,
3293            None,
3294            None,
3295            Some(Duration::from_millis(50)),
3296            BackboneAbuseConfig::default(),
3297        );
3298
3299        start(config, tx, next_id).unwrap();
3300        thread::sleep(Duration::from_millis(50));
3301
3302        let client = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
3303        client
3304            .set_read_timeout(Some(Duration::from_millis(100)))
3305            .unwrap();
3306        let sock = SockRef::from(&client);
3307        sock.set_recv_buffer_size(4096).ok();
3308
3309        let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
3310        let (client_id, mut writer) = match event {
3311            Event::InterfaceUp(id, Some(writer), _) => (id, writer),
3312            other => panic!("expected InterfaceUp with writer, got {:?}", other),
3313        };
3314
3315        let payload = vec![0x55; 512 * 1024];
3316        let deadline = Instant::now() + Duration::from_secs(3);
3317        let mut stalled = false;
3318        while Instant::now() < deadline {
3319            match writer.send_frame(&payload) {
3320                Ok(()) => {}
3321                Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
3322                    thread::sleep(Duration::from_millis(10));
3323                }
3324                Err(ref e) if e.kind() == io::ErrorKind::TimedOut => {
3325                    stalled = true;
3326                    break;
3327                }
3328                Err(e) => panic!("unexpected send error: {}", e),
3329            }
3330        }
3331
3332        assert!(stalled, "expected writer to time out on persistent stall");
3333        let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
3334        assert!(matches!(event, Event::InterfaceDown(id) if id == client_id));
3335    }
3336
3337    #[test]
3338    fn shutting_down_backbone_writer_closes_the_client_socket() {
3339        let port = find_free_port();
3340        let (tx, rx) = crate::event::channel();
3341        let next_id = Arc::new(AtomicU64::new(9670));
3342        let config = make_server_config(port, 99, None, None, None, BackboneAbuseConfig::default());
3343
3344        start(config, tx, next_id).unwrap();
3345        thread::sleep(Duration::from_millis(50));
3346
3347        let mut client = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
3348        client
3349            .set_read_timeout(Some(Duration::from_secs(2)))
3350            .unwrap();
3351        let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
3352        let mut writer = match event {
3353            Event::InterfaceUp(_, Some(writer), _) => writer,
3354            other => panic!("expected InterfaceUp with writer, got {:?}", other),
3355        };
3356
3357        writer.shutdown();
3358
3359        let mut byte = [0u8; 1];
3360        assert_eq!(client.read(&mut byte).unwrap(), 0);
3361    }
3362
3363    /// Drain events matching a predicate, return the first match.
3364    fn wait_for<F>(
3365        rx: &crate::event::EventReceiver,
3366        timeout: Duration,
3367        mut pred: F,
3368    ) -> Option<Event>
3369    where
3370        F: FnMut(&Event) -> bool,
3371    {
3372        let deadline = Instant::now() + timeout;
3373        loop {
3374            let remaining = deadline.saturating_duration_since(Instant::now());
3375            if remaining.is_zero() {
3376                return None;
3377            }
3378            match rx.recv_timeout(remaining) {
3379                Ok(event) if pred(&event) => return Some(event),
3380                Ok(_) => continue,
3381                Err(_) => return None,
3382            }
3383        }
3384    }
3385
3386    #[test]
3387    fn backbone_write_stall_emits_peer_events() {
3388        let port = find_free_port();
3389        let (tx, rx) = crate::event::channel();
3390        let next_id = Arc::new(AtomicU64::new(9700));
3391
3392        let config = make_server_config(
3393            port,
3394            97,
3395            None,
3396            None,
3397            Some(Duration::from_millis(50)), // 50ms stall timeout
3398            BackboneAbuseConfig::default(),
3399        );
3400
3401        start(config, tx, next_id).unwrap();
3402        thread::sleep(Duration::from_millis(50));
3403
3404        // Connect a client that won't read (will cause write stall)
3405        let client = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
3406        client
3407            .set_read_timeout(Some(Duration::from_millis(100)))
3408            .unwrap();
3409        let sock = SockRef::from(&client);
3410        sock.set_recv_buffer_size(4096).ok();
3411
3412        // Wait for InterfaceUp and grab writer
3413        let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
3414        let mut writer = match event {
3415            Event::InterfaceUp(_, Some(w), _) => w,
3416            other => panic!("expected InterfaceUp with writer, got {:?}", other),
3417        };
3418
3419        // Flood until stall
3420        let payload = vec![0x55; 512 * 1024];
3421        let deadline = Instant::now() + Duration::from_secs(3);
3422        while Instant::now() < deadline {
3423            match writer.send_frame(&payload) {
3424                Ok(()) | Err(_) => {
3425                    if Instant::now() + Duration::from_millis(10) > deadline {
3426                        break;
3427                    }
3428                    thread::sleep(Duration::from_millis(10));
3429                }
3430            }
3431        }
3432
3433        // Should see BackbonePeerWriteStall event
3434        let stall_event = wait_for(&rx, Duration::from_secs(3), |e| {
3435            matches!(e, Event::BackbonePeerWriteStall { .. })
3436        });
3437        assert!(
3438            stall_event.is_some(),
3439            "expected BackbonePeerWriteStall event"
3440        );
3441    }
3442
3443    #[test]
3444    fn backbone_blacklisted_peer_rejected_on_connect() {
3445        let port = find_free_port();
3446        let (tx, rx) = crate::event::channel();
3447        let next_id = Arc::new(AtomicU64::new(9800));
3448
3449        let config = make_server_config(port, 98, None, None, None, BackboneAbuseConfig::default());
3450        let peer_state = config.peer_state.clone();
3451
3452        start(config, tx.clone(), next_id).unwrap();
3453        thread::sleep(Duration::from_millis(50));
3454
3455        // First connection should succeed
3456        let client1 = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
3457        let event = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
3458        assert!(
3459            matches!(event, Event::InterfaceUp(_, _, _)),
3460            "first connection should succeed"
3461        );
3462        drop(client1);
3463
3464        // Drain disconnect events
3465        thread::sleep(Duration::from_millis(100));
3466        while rx.try_recv().is_ok() {}
3467
3468        // Blacklist 127.0.0.1 via the peer monitor
3469        peer_state.lock().unwrap().blacklist(
3470            "127.0.0.1".parse().unwrap(),
3471            Duration::from_secs(60),
3472            "test blacklist".into(),
3473        );
3474
3475        // Second connection from same IP should be rejected (no InterfaceUp)
3476        let _client2 = TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
3477        // Give poll loop time to reject
3478        thread::sleep(Duration::from_millis(200));
3479
3480        // Should NOT get an InterfaceUp — connection should have been rejected
3481        let event = rx.try_recv();
3482        if let Ok(Event::InterfaceUp(_, _, _)) = event {
3483            panic!("blacklisted peer should not get InterfaceUp")
3484        }
3485    }
3486
3487    #[test]
3488    fn backbone_six_fast_flaps_reject_before_allocating_seventh_interface() {
3489        let port = find_free_port();
3490        let (tx, rx) = crate::event::channel();
3491        let next_id = Arc::new(AtomicU64::new(9900));
3492        let mut config =
3493            make_server_config(port, 99, None, None, None, BackboneAbuseConfig::default());
3494        config.fast_flap = test_fast_flap_config();
3495        let fast_flap_state = Arc::clone(&config.fast_flap_state);
3496        let peer_state = Arc::clone(&config.peer_state);
3497
3498        start(config, tx, Arc::clone(&next_id)).unwrap();
3499        thread::sleep(Duration::from_millis(50));
3500
3501        for flap in 0..6 {
3502            let mut client = TcpStream::connect(format!("127.0.0.1:{port}")).unwrap();
3503            let up = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
3504            assert!(matches!(up, Event::InterfaceUp(_, _, _)));
3505
3506            // Receiving traffic must not exempt a short-lived connection.
3507            if flap == 0 {
3508                let payload = vec![0x41; 32];
3509                client.write_all(&hdlc::frame(&payload)).unwrap();
3510                let frame = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
3511                assert!(matches!(frame, Event::Frame { data, .. } if data == payload));
3512            }
3513
3514            drop(client);
3515            let down = recv_non_peer_event(&rx, Duration::from_secs(2)).unwrap();
3516            assert!(matches!(down, Event::InterfaceDown(_)));
3517        }
3518
3519        let peer_ip: IpAddr = "127.0.0.1".parse().unwrap();
3520        assert!(
3521            lock_or_recover(&fast_flap_state, "test fast-flap state").is_blocked(
3522                peer_ip,
3523                &test_fast_flap_config(),
3524                Instant::now(),
3525            )
3526        );
3527        assert_eq!(next_id.load(Ordering::Relaxed), 9906);
3528        let entries = lock_or_recover(&peer_state, "test peer state").list("test-backbone");
3529        let entry = entries
3530            .iter()
3531            .find(|entry| entry.peer_ip == peer_ip)
3532            .expect("fast-flapping peer state");
3533        assert_eq!(entry.blacklist_reason.as_deref(), Some("fast flapping"));
3534        assert!(entry.blacklisted_remaining_secs.is_some());
3535
3536        let _rejected = TcpStream::connect(format!("127.0.0.1:{port}")).unwrap();
3537        serve_for(&rx, Duration::from_millis(200));
3538        assert_eq!(
3539            next_id.load(Ordering::Relaxed),
3540            9906,
3541            "rejection must happen before dynamic interface allocation"
3542        );
3543        while let Ok(event) = rx.try_recv() {
3544            assert!(
3545                !matches!(event, Event::InterfaceUp(_, _, _)),
3546                "blocked peer must not register a dynamic interface"
3547            );
3548        }
3549        let entries = lock_or_recover(&peer_state, "test peer state").list("test-backbone");
3550        assert_eq!(
3551            entries
3552                .iter()
3553                .find(|entry| entry.peer_ip == peer_ip)
3554                .unwrap()
3555                .reject_count,
3556            1
3557        );
3558    }
3559
3560    #[test]
3561    fn backbone_parse_config_reads_abuse_settings() {
3562        let factory = BackboneInterfaceFactory;
3563        let mut params = HashMap::new();
3564        params.insert("listen_ip".into(), "127.0.0.1".into());
3565        params.insert("listen_port".into(), "4242".into());
3566        params.insert("idle_timeout".into(), "15".into());
3567        params.insert("write_stall_timeout".into(), "45".into());
3568        params.insert("max_penalty_duration".into(), "3600".into());
3569
3570        let config = factory
3571            .parse_config("test-backbone", InterfaceId(97), &params)
3572            .unwrap();
3573        let mode = *config.into_any().downcast::<BackboneMode>().unwrap();
3574
3575        match mode {
3576            BackboneMode::Server(config) => {
3577                assert_eq!(config.listen_ip, "127.0.0.1");
3578                assert_eq!(config.listen_port, 4242);
3579                assert_eq!(config.idle_timeout, Some(Duration::from_secs(15)));
3580                assert_eq!(config.write_stall_timeout, Some(Duration::from_secs(45)));
3581                assert_eq!(
3582                    config.abuse.max_penalty_duration,
3583                    Some(Duration::from_secs(3600))
3584                );
3585            }
3586            BackboneMode::Client(_, _) => panic!("expected server config"),
3587        }
3588    }
3589
3590    #[test]
3591    fn backbone_parse_config_reads_fast_flapping_settings() {
3592        let factory = BackboneInterfaceFactory;
3593        let mut params = HashMap::new();
3594        params.insert("listen_ip".into(), "127.0.0.1".into());
3595        params.insert("listen_port".into(), "4242".into());
3596        params.insert("block_fast_flapping".into(), "no".into());
3597        params.insert("fast_flapping_threshold".into(), "2.5".into());
3598        params.insert("fast_flapping_grace".into(), "9".into());
3599        params.insert("fast_flapping_block_time".into(), "1.5".into());
3600
3601        let config = factory
3602            .parse_config("test-backbone", InterfaceId(93), &params)
3603            .unwrap();
3604        let mode = *config.into_any().downcast::<BackboneMode>().unwrap();
3605        let BackboneMode::Server(config) = mode else {
3606            panic!("expected server config");
3607        };
3608
3609        assert!(!config.fast_flap.enabled);
3610        assert_eq!(
3611            config.fast_flap.connection_threshold,
3612            Duration::from_secs_f64(2.5)
3613        );
3614        assert_eq!(config.fast_flap.grace, 9);
3615        assert_eq!(config.fast_flap.block_duration, Duration::from_secs(90));
3616    }
3617
3618    #[test]
3619    fn backbone_parse_config_rejects_invalid_fast_flapping_settings() {
3620        let factory = BackboneInterfaceFactory;
3621        for (key, value) in [
3622            ("block_fast_flapping", "perhaps"),
3623            ("fast_flapping_threshold", "-1"),
3624            ("fast_flapping_threshold", "NaN"),
3625            ("fast_flapping_grace", "-1"),
3626            ("fast_flapping_block_time", "infinite"),
3627        ] {
3628            let mut params = HashMap::new();
3629            params.insert("listen_port".into(), "4242".into());
3630            params.insert(key.into(), value.into());
3631
3632            let err = match factory.parse_config("test-backbone", InterfaceId(92), &params) {
3633                Ok(_) => panic!("{key}={value} should be rejected"),
3634                Err(err) => err,
3635            };
3636            assert!(err.contains(key), "unexpected error: {err}");
3637        }
3638    }
3639
3640    #[test]
3641    fn parsed_backbone_listeners_share_fast_flap_history() {
3642        let factory = BackboneInterfaceFactory;
3643        let mut params = HashMap::new();
3644        params.insert("listen_port".into(), "4242".into());
3645
3646        let first = factory
3647            .parse_config("first", InterfaceId(90), &params)
3648            .unwrap();
3649        let second = factory
3650            .parse_config("second", InterfaceId(91), &params)
3651            .unwrap();
3652        let BackboneMode::Server(first) = *first.into_any().downcast::<BackboneMode>().unwrap()
3653        else {
3654            panic!("expected server config");
3655        };
3656        let BackboneMode::Server(second) = *second.into_any().downcast::<BackboneMode>().unwrap()
3657        else {
3658            panic!("expected server config");
3659        };
3660
3661        assert!(Arc::ptr_eq(&first.fast_flap_state, &second.fast_flap_state));
3662    }
3663
3664    #[test]
3665    fn backbone_parse_config_reads_client_priority() {
3666        let factory = BackboneInterfaceFactory;
3667        let mut params = HashMap::new();
3668        params.insert("remote".into(), "example.com".into());
3669        params.insert("target_port".into(), "4242".into());
3670        params.insert("priority".into(), "87".into());
3671
3672        let config = factory
3673            .parse_config("test-backbone-client", InterfaceId(96), &params)
3674            .unwrap();
3675        let mode = *config.into_any().downcast::<BackboneMode>().unwrap();
3676
3677        match mode {
3678            BackboneMode::Client(_, priority) => assert_eq!(priority, 87),
3679            BackboneMode::Server(_) => panic!("expected client config"),
3680        }
3681    }
3682
3683    #[test]
3684    fn backbone_parse_config_defaults_client_priority() {
3685        let factory = BackboneInterfaceFactory;
3686        let mut params = HashMap::new();
3687        params.insert("remote".into(), "example.com".into());
3688
3689        let config = factory
3690            .parse_config("test-backbone-client", InterfaceId(95), &params)
3691            .unwrap();
3692        let mode = *config.into_any().downcast::<BackboneMode>().unwrap();
3693
3694        match mode {
3695            BackboneMode::Client(_, priority) => assert_eq!(priority, 60),
3696            BackboneMode::Server(_) => panic!("expected client config"),
3697        }
3698    }
3699
3700    #[test]
3701    fn backbone_parse_config_rejects_invalid_client_priority() {
3702        let factory = BackboneInterfaceFactory;
3703        for value in ["-1", "101", "fast"] {
3704            let mut params = HashMap::new();
3705            params.insert("remote".into(), "example.com".into());
3706            params.insert("priority".into(), value.into());
3707
3708            let err = match factory.parse_config("test-backbone-client", InterfaceId(94), &params) {
3709                Ok(_) => panic!("priority {value} should be rejected"),
3710                Err(err) => err,
3711            };
3712            assert!(
3713                err.contains("priority"),
3714                "unexpected error for {value}: {err}"
3715            );
3716        }
3717    }
3718
3719    #[test]
3720    fn positive_duration_param_rejects_values_that_are_not_durations() {
3721        for raw in ["inf", "-inf", "NaN", "1e300", "-1", "0"] {
3722            let params = HashMap::from([("idle_timeout".to_string(), raw.to_string())]);
3723            assert_eq!(
3724                parse_positive_duration_secs(&params, "idle_timeout"),
3725                None,
3726                "value {raw} should be ignored"
3727            );
3728        }
3729        let params = HashMap::from([("idle_timeout".to_string(), "2.5".to_string())]);
3730        assert_eq!(
3731            parse_positive_duration_secs(&params, "idle_timeout"),
3732            Some(Duration::from_millis(2500))
3733        );
3734    }
3735}