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