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