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