mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-09 00:04:54 +00:00
Merge branch 'master' into next
This commit is contained in:
+110
@@ -85,6 +85,46 @@ with v0.2.x peers.
|
||||
|
||||
### Added
|
||||
|
||||
#### Platform Support
|
||||
|
||||
- Windows platform support: wintun TUN device, TCP control socket on
|
||||
`localhost:21210` (in place of the Unix domain socket), Windows
|
||||
Service lifecycle (`--install-service`, `--uninstall-service`,
|
||||
`--service`), ZIP packaging with PowerShell install/uninstall scripts,
|
||||
and CI build/test matrix entry
|
||||
([#45](https://github.com/jmcorgan/fips/pull/45))
|
||||
- macOS platform support: native `utun` TUN interface management, raw
|
||||
Ethernet transport via BPF, `.pkg` packaging with launchd plist and
|
||||
uninstall script, x86_64 cross-compile from arm64, and CI build/unit
|
||||
test jobs
|
||||
- `gateway` Cargo feature flag gates the optional Linux-only
|
||||
`rustables` dependency so macOS and Windows builds never pull in
|
||||
nftables bindings
|
||||
|
||||
#### Outbound LAN Gateway
|
||||
|
||||
- New `fips-gateway` binary that lets unmodified LAN hosts reach FIPS
|
||||
mesh destinations via DNS-allocated virtual IPs and kernel nftables
|
||||
NAT. Virtual-IP pool (`fd01::/112` by default) with state-machine
|
||||
lifecycle and TTL-based reclamation; conntrack-backed session
|
||||
tracking; proxy NDP on the LAN interface; control socket at
|
||||
`/run/fips/gateway.sock` with `show_gateway` and `show_mappings`;
|
||||
fipstop Gateway tab with pool gauge and mappings table; design doc
|
||||
at `docs/design/fips-gateway.md`; integration test harness
|
||||
- Gateway packaging: systemd service unit with `After=fips.service`,
|
||||
Debian and AUR package entries, OpenWrt procd init with dnsmasq
|
||||
forwarding, proxy NDP, RA route advertisements, and IPv6 forwarding
|
||||
sysctls. Gateway enabled by default on OpenWrt
|
||||
|
||||
#### Examples
|
||||
|
||||
- macOS WireGuard sidecar: run FIPS in a local Docker container and
|
||||
route `.fips` traffic from the macOS host through a WireGuard tunnel
|
||||
to the container's `fips0` interface. Only traffic destined for
|
||||
`fd00::/8` transits the sidecar; regular internet traffic continues
|
||||
to use the host network
|
||||
([#51](https://github.com/jmcorgan/fips/pull/51))
|
||||
|
||||
#### Bluetooth Transport
|
||||
|
||||
- Bluetooth Low Energy (BLE) L2CAP Connection-Oriented Channel transport
|
||||
@@ -96,6 +136,31 @@ with v0.2.x peers.
|
||||
- Cross-probe tie-breaker using deterministic NodeAddr comparison
|
||||
- Connection pool with configurable capacity and eviction
|
||||
|
||||
#### DNS
|
||||
|
||||
- Multi-backend `.fips` DNS configuration: a detection script
|
||||
configures whichever resolver is available, in priority order:
|
||||
systemd dns-delegate (systemd >= 258), systemd-resolved via
|
||||
`resolvectl`, standalone dnsmasq, NetworkManager with the dnsmasq
|
||||
plugin. Teardown reads the recorded backend from
|
||||
`/run/fips/dns-backend` and reverses only what was applied
|
||||
([#58](https://github.com/jmcorgan/fips/pull/58),
|
||||
fixes [#52](https://github.com/jmcorgan/fips/issues/52))
|
||||
|
||||
#### Operator Configuration
|
||||
|
||||
- `node.log_level` config field (case-insensitive, default `info`)
|
||||
replaces the hardcoded `RUST_LOG=info` previously baked into
|
||||
systemd units and the OpenWrt procd init script. The daemon now
|
||||
loads config before initializing tracing so the configured level
|
||||
takes effect; `RUST_LOG` still overrides when set
|
||||
|
||||
#### Documentation
|
||||
|
||||
- Pre-implementation proposal for NAT traversal using Nostr relays
|
||||
as the signaling channel and STUN for reflexive address discovery
|
||||
(`docs/proposals/`)
|
||||
|
||||
#### Packaging and Deployment
|
||||
|
||||
- Linux release artifact workflow: builds x86_64 and aarch64 tarballs
|
||||
@@ -106,6 +171,20 @@ with v0.2.x peers.
|
||||
([#21](https://github.com/jmcorgan/fips/pull/21),
|
||||
[@dskvr](https://github.com/dskvr))
|
||||
|
||||
### Changed
|
||||
|
||||
- MMP link-layer report intervals retuned for constrained transports:
|
||||
steady-state floor raised from 100ms to 1000ms, ceiling from 2000ms
|
||||
to 5000ms. Cold-start uses a 200ms floor for the first 5 SRTT samples
|
||||
before switching to steady-state. Reduces BLE overhead ~10× while
|
||||
keeping reports well above the EWMA convergence threshold.
|
||||
Session-layer intervals unchanged
|
||||
- 35 info-level log messages demoted to debug (handshake
|
||||
cross-connection mechanics, periodic MMP telemetry, TUN/transport
|
||||
shutdown, retry scheduling). Info output now focuses on
|
||||
operator-relevant state changes: lifecycle events, peer promotions,
|
||||
session establishment, parent switches, transport start/stop
|
||||
|
||||
### Fixed
|
||||
|
||||
- Control socket path detection in fipsctl and fipstop now checks for
|
||||
@@ -118,6 +197,37 @@ with v0.2.x peers.
|
||||
unavailable on OpenWrt targets
|
||||
- IPv6 routing policy rule added at TUN setup to protect `fd00::/8`
|
||||
from interception by Tailscale's table 52 default route
|
||||
- Bloom filter routing no longer swallows traffic when no bloom
|
||||
candidate is strictly closer than the current node. `find_next_hop`
|
||||
now falls through to greedy tree routing in that case instead of
|
||||
returning `NoRoute`, which previously caused dropped packets in
|
||||
topologies where the tree parent was closer but not a bloom
|
||||
candidate
|
||||
- Auto-connect peers now reconnect after a graceful `Disconnect`
|
||||
notification from the remote side. `handle_disconnect` previously
|
||||
removed the peer without scheduling a reconnect, orphaning the
|
||||
entry on a clean upstream shutdown; the other removal paths
|
||||
(link-dead, decrypt failure, peer restart) already scheduled
|
||||
reconnect ([#60](https://github.com/jmcorgan/fips/issues/60),
|
||||
reported by [@SwapMarket](https://github.com/SwapMarket))
|
||||
- `fipsctl connect` now rejects FIPS mesh (`fd00::/8`) addresses for
|
||||
`udp`, `tcp`, and `ethernet` transports with a clear error message
|
||||
instead of echoing success while the daemon silently failed the
|
||||
bind with `EAFNOSUPPORT`
|
||||
([#61](https://github.com/jmcorgan/fips/issues/61),
|
||||
reported by [@SwapMarket](https://github.com/SwapMarket))
|
||||
- Rekey msg1 on non-accepting transports (e.g. UDP holepunch) was
|
||||
rejected at the top of `handle_msg1()`, which broke rekey handshakes
|
||||
on established links and produced repeated "dual rekey initiation"
|
||||
log floods. The gate now only blocks truly new inbound handshakes
|
||||
from unknown addresses; rekey and restart msg1s for established
|
||||
peers are processed normally
|
||||
([#47](https://github.com/jmcorgan/fips/issues/47),
|
||||
[#49](https://github.com/jmcorgan/fips/pull/49))
|
||||
- `fipstop` now uses `ratatui::try_init()` instead of `ratatui::init()`,
|
||||
so terminal initialization failures (e.g. Docker on macOS Sequoia,
|
||||
or environments without a usable tty) produce a clean error message
|
||||
instead of a hard crash
|
||||
|
||||
## [0.2.0] - 2026-03-22
|
||||
|
||||
|
||||
@@ -45,8 +45,12 @@ endpoints.
|
||||
forward secrecy and protocol negotiation in the handshake
|
||||
- **Nostr-native identity** — secp256k1 keypairs as node addresses, no
|
||||
registration or central authority
|
||||
- **IPv6 adaptation** — TUN interface maps npubs to fd00::/8 addresses for
|
||||
unmodified IP applications; static hostname mapping (`/etc/fips/hosts`)
|
||||
- **IPv6 adaptation** — TUN interface maps npubs to fd00::/8 addresses
|
||||
for unmodified IP applications; built-in `.fips` DNS resolver with
|
||||
optional static hostname mapping (`/etc/fips/hosts`)
|
||||
- **Outbound LAN gateway** — optional `fips-gateway` daemon lets
|
||||
unmodified LAN hosts reach `.fips` destinations via a
|
||||
DNS-allocated virtual IP pool and kernel nftables NAT
|
||||
- **Metrics Measurement Protocol** — per-link RTT, loss, jitter, and goodput
|
||||
measurement with mesh size estimation
|
||||
- **ECN congestion signaling** — hop-by-hop CE flag relay with RFC 3168 IPv6
|
||||
@@ -69,18 +73,21 @@ supported (see transport matrix below).
|
||||
|
||||
### Transport support by platform
|
||||
|
||||
| Transport | Linux | macOS | Windows |
|
||||
|-----------|:-----:|:-----:|:-------:|
|
||||
| UDP | ✅ | ✅ | ❌ |
|
||||
| TCP | ✅ | ✅ | ❌ |
|
||||
| Ethernet | ✅ | ✅ | ❌ |
|
||||
| Tor | ✅ | ✅ | ❌ |
|
||||
| BLE | ✅ | ❌ | ❌ |
|
||||
| Transport | Linux | macOS | Windows | OpenWrt |
|
||||
|-----------|:-----:|:-----:|:-------:|:-------:|
|
||||
| UDP | ✅ | ✅ | ✅ | ✅ |
|
||||
| TCP | ✅ | ✅ | ✅ | ✅ |
|
||||
| Ethernet | ✅ | ✅ | ❌ | ✅ |
|
||||
| Tor | ✅ | ✅ | ✅ | ✅ |
|
||||
| BLE | ✅ | ❌ | ❌ | ❌ |
|
||||
|
||||
On **Linux**, the BLE transport requires BlueZ and libdbus. On
|
||||
Debian/Ubuntu: `sudo apt install bluez libdbus-1-dev`. Then build with
|
||||
BLE enabled: `cargo build --release --features ble`.
|
||||
|
||||
On **OpenWrt**, BLE is disabled because libdbus is not available on
|
||||
the target. All other transports work and ship in the default ipk.
|
||||
|
||||
## Installation
|
||||
|
||||
After building, choose one of the following methods to install.
|
||||
@@ -262,7 +269,11 @@ for the full reference.
|
||||
FIPS includes a DNS resolver (enabled by default, port 5354) that maps
|
||||
`.fips` names to fd00::/8 IPv6 addresses.
|
||||
|
||||
**Linux** (systemd-resolved):
|
||||
**Linux**: The `.deb` package auto-detects and configures whichever
|
||||
resolver is present (systemd dns-delegate, systemd-resolved, dnsmasq,
|
||||
or NetworkManager with dnsmasq); no manual setup is needed. For
|
||||
manual or tarball installs, point your resolver at `127.0.0.1:5354`
|
||||
for the `fips` domain — e.g., with systemd-resolved:
|
||||
|
||||
```bash
|
||||
sudo resolvectl dns fips0 127.0.0.1:5354
|
||||
@@ -323,13 +334,20 @@ including static topology tests and stochastic chaos simulation.
|
||||
|
||||
## Examples
|
||||
|
||||
- [examples/wireguard-sidecar-macos/](examples/wireguard-sidecar-macos/) -
|
||||
Run a local WireGuard sidecar on macOS so `.fips` traffic can reach the mesh
|
||||
through Docker.
|
||||
|
||||
The macOS WireGuard sidecar only forwards FIPS IPv6 traffic destined for
|
||||
`fd00::/8` from `wg0` to `fips0`. Regular internet traffic does not transit the
|
||||
sidecar and continues to use the host network normally.
|
||||
- [examples/sidecar-nostr-relay/](examples/sidecar-nostr-relay/) —
|
||||
Run a [strfry](https://github.com/hoytech/strfry) Nostr relay
|
||||
reachable exclusively over the FIPS mesh. The relay container shares
|
||||
the FIPS sidecar's network namespace and is isolated from the host
|
||||
network.
|
||||
- [examples/k8s-sidecar/](examples/k8s-sidecar/) — Run FIPS as a
|
||||
Kubernetes Pod sidecar. The sidecar creates `fips0` in the Pod's
|
||||
shared network namespace so every other container in the Pod gets
|
||||
mesh access without modification.
|
||||
- [examples/wireguard-sidecar-macos/](examples/wireguard-sidecar-macos/) —
|
||||
Reach the FIPS mesh from a macOS host through a local Docker
|
||||
container over a WireGuard tunnel. Only traffic destined for
|
||||
`fd00::/8` transits the sidecar; regular internet traffic continues
|
||||
to use the host network.
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -346,8 +364,9 @@ If you want to contribute, start with:
|
||||
## Project Structure
|
||||
|
||||
```text
|
||||
src/ Rust source (library + fips/fipsctl/fipstop binaries)
|
||||
packaging/ Debian, systemd tarball, and shared packaging files
|
||||
src/ Rust source (library + fips/fipsctl/fipstop/fips-gateway binaries)
|
||||
packaging/ Debian, macOS .pkg, Windows ZIP, OpenWrt ipk, AUR, systemd tarball
|
||||
examples/ Deployment examples (Nostr relay, K8s sidecar, macOS WireGuard)
|
||||
docs/design/ Protocol design specifications
|
||||
testing/ Docker-based integration test harnesses
|
||||
```
|
||||
@@ -364,14 +383,17 @@ Ethernet, Tor, and Bluetooth (BLE) with a small live mesh of deployed nodes.
|
||||
- Noise XX encryption at both layers with protocol negotiation
|
||||
- Periodic Noise rekey with hitless cutover for forward secrecy (FMP + FSP)
|
||||
- Persistent node identity with key file management
|
||||
- IPv6 TUN adapter with DNS resolution of `.fips` names
|
||||
- IPv6 TUN adapter with built-in `.fips` DNS resolver and multi-backend
|
||||
auto-configuration (systemd dns-delegate, systemd-resolved, dnsmasq,
|
||||
NetworkManager)
|
||||
- Static hostname mapping (`/etc/fips/hosts`) with auto-reload
|
||||
- Per-link metrics (RTT, loss, jitter, goodput) and mesh size estimation
|
||||
- ECN congestion signaling (hop-by-hop CE relay, IPv6 CE marking, kernel drop detection)
|
||||
- UDP, TCP, Ethernet, Tor, and BLE transports (BLE via L2CAP CoC with per-link MTU negotiation)
|
||||
- Outbound LAN gateway for unmodified hosts via DNS-allocated virtual IPs and nftables NAT
|
||||
- Runtime inspection and peer management via `fipsctl` and `fipstop`
|
||||
- Reproducible builds with toolchain pinning and SOURCE_DATE_EPOCH
|
||||
- Linux (Debian, systemd tarball, OpenWrt, AUR) and macOS packaging
|
||||
- Linux (Debian, systemd tarball, OpenWrt, AUR), macOS (`.pkg`), and Windows (ZIP, service) packaging
|
||||
- Docker-based integration and chaos testing
|
||||
|
||||
### Near-term priorities
|
||||
|
||||
@@ -12,6 +12,7 @@ use fips::upper::hosts::HostMap;
|
||||
use fips::version;
|
||||
use fips::{Identity, encode_nsec};
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::net::{Ipv6Addr, SocketAddrV6};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -249,6 +250,50 @@ fn default_key_dir() -> PathBuf {
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if `address` is an IPv6 literal in `fd00::/8` (FIPS mesh ULA range).
|
||||
///
|
||||
/// Handles three common syntaxes:
|
||||
/// - bare IPv6: `fd9d:...`
|
||||
/// - bracketed + port: `[fd9d:...]:2121`
|
||||
/// - bare IPv6 + port: `fd9d:...:2121` (ambiguous; accepted if tail is numeric)
|
||||
fn is_fips_mesh_address(address: &str) -> bool {
|
||||
let is_ula = |a: &Ipv6Addr| a.octets()[0] == 0xfd;
|
||||
|
||||
if let Ok(a) = address.parse::<Ipv6Addr>() {
|
||||
return is_ula(&a);
|
||||
}
|
||||
if let Ok(sa) = address.parse::<SocketAddrV6>() {
|
||||
return is_ula(sa.ip());
|
||||
}
|
||||
if let Some((host, port)) = address.rsplit_once(':')
|
||||
&& port.chars().all(|c| c.is_ascii_digit())
|
||||
&& !port.is_empty()
|
||||
{
|
||||
let host = host.trim_start_matches('[').trim_end_matches(']');
|
||||
if let Ok(a) = host.parse::<Ipv6Addr>() {
|
||||
return is_ula(&a);
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Reject `fd00::/8` addresses for transports that expect a reachable network endpoint.
|
||||
///
|
||||
/// FIPS mesh ULAs are derived from npubs and only make sense as destinations
|
||||
/// inside an already-established mesh — they are not valid udp/tcp/ethernet
|
||||
/// transport endpoints. Without this check the CLI echoes success while the
|
||||
/// daemon rejects the bind with EAFNOSUPPORT (issue #61).
|
||||
fn validate_connect_address(address: &str, transport: &str) -> Result<(), String> {
|
||||
let checked = matches!(transport, "udp" | "tcp" | "ethernet");
|
||||
if checked && is_fips_mesh_address(address) {
|
||||
return Err(format!(
|
||||
"'{address}' is a FIPS mesh address (fd00::/8), not a reachable {transport} endpoint.\n\
|
||||
Provide the peer's routable IP/hostname and port (e.g., '192.0.2.1:2121' or 'peer.example.com:2121')."
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resolve a peer identifier to an npub.
|
||||
///
|
||||
/// If the identifier starts with "npub1", it's returned as-is.
|
||||
@@ -328,6 +373,10 @@ fn main() {
|
||||
address,
|
||||
transport,
|
||||
} => {
|
||||
if let Err(e) = validate_connect_address(address, transport) {
|
||||
eprintln!("error: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
let npub = resolve_peer(peer);
|
||||
build_command(
|
||||
"connect",
|
||||
@@ -353,3 +402,62 @@ fn main() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn detects_bare_ula_literal() {
|
||||
assert!(is_fips_mesh_address("fd9d:abcd::1"));
|
||||
assert!(is_fips_mesh_address("fd00::"));
|
||||
assert!(is_fips_mesh_address(
|
||||
"fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_bracketed_ula_with_port() {
|
||||
assert!(is_fips_mesh_address("[fd9d:abcd::1]:2121"));
|
||||
assert!(is_fips_mesh_address("[fd00::1]:8443"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_bare_ula_with_port() {
|
||||
assert!(is_fips_mesh_address("fd9d:abcd::1:2121"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_non_ula_ipv6() {
|
||||
// fc00::/7 other half (fcXX:) is also ULA but not fd00::/8 — we only
|
||||
// block the fd-prefixed half that FIPS actually uses.
|
||||
assert!(!is_fips_mesh_address("fc00::1"));
|
||||
assert!(!is_fips_mesh_address("::1"));
|
||||
assert!(!is_fips_mesh_address("2001:db8::1"));
|
||||
assert!(!is_fips_mesh_address("[2001:db8::1]:2121"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_ipv4_and_hostnames() {
|
||||
assert!(!is_fips_mesh_address("192.0.2.1:2121"));
|
||||
assert!(!is_fips_mesh_address("peer.example.com:2121"));
|
||||
assert!(!is_fips_mesh_address("coinos.pro:2121"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validates_only_target_transports() {
|
||||
assert!(validate_connect_address("fd9d::1:2121", "udp").is_err());
|
||||
assert!(validate_connect_address("fd9d::1:2121", "tcp").is_err());
|
||||
assert!(validate_connect_address("fd9d::1:2121", "ethernet").is_err());
|
||||
// Other transports are not inspected — they may legitimately accept
|
||||
// non-IP endpoints (tor onion, etc.).
|
||||
assert!(validate_connect_address("fd9d::1:2121", "tor").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allows_valid_endpoints() {
|
||||
assert!(validate_connect_address("192.0.2.1:2121", "udp").is_ok());
|
||||
assert!(validate_connect_address("peer.example.com:2121", "tcp").is_ok());
|
||||
assert!(validate_connect_address("[2001:db8::1]:2121", "udp").is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,8 +71,12 @@ impl Node {
|
||||
/// Handle a Disconnect notification from a peer.
|
||||
///
|
||||
/// The peer is signaling an orderly departure. We immediately remove
|
||||
/// them from all state rather than waiting for timeout detection.
|
||||
fn handle_disconnect(&mut self, from: &NodeAddr, payload: &[u8]) {
|
||||
/// them from all state rather than waiting for timeout detection, and
|
||||
/// schedule a reconnect if the peer is configured as auto-connect.
|
||||
/// Without this, a graceful upstream shutdown orphans auto-connect
|
||||
/// entries — other removal paths (link-dead, decrypt failure, peer
|
||||
/// restart) all schedule reconnect.
|
||||
pub(in crate::node) fn handle_disconnect(&mut self, from: &NodeAddr, payload: &[u8]) {
|
||||
let disconnect = match crate::protocol::Disconnect::decode(payload) {
|
||||
Ok(msg) => msg,
|
||||
Err(e) => {
|
||||
@@ -87,7 +91,13 @@ impl Node {
|
||||
"Peer sent disconnect notification"
|
||||
);
|
||||
|
||||
let addr = *from;
|
||||
self.remove_active_peer(from);
|
||||
let now_ms = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as u64)
|
||||
.unwrap_or(0);
|
||||
self.schedule_reconnect(addr, now_ms);
|
||||
}
|
||||
|
||||
/// Remove an active peer and clean up all associated state.
|
||||
|
||||
@@ -804,6 +804,43 @@ fn test_schedule_reconnect_fresh_state() {
|
||||
assert_eq!(state.retry_after_ms, 1_000 + expected_delay);
|
||||
}
|
||||
|
||||
/// Test that a graceful Disconnect from an auto-connect peer schedules reconnect.
|
||||
///
|
||||
/// Regression test for issue #60: `handle_disconnect` previously called
|
||||
/// `remove_active_peer` without `schedule_reconnect`, orphaning auto-connect
|
||||
/// entries on a clean upstream shutdown. Other peer-removal paths (link-dead,
|
||||
/// decrypt failure, peer restart) all schedule reconnect.
|
||||
#[test]
|
||||
fn test_disconnect_schedules_reconnect() {
|
||||
use crate::protocol::{Disconnect, DisconnectReason};
|
||||
|
||||
let peer_identity = Identity::generate();
|
||||
let peer_npub = peer_identity.npub();
|
||||
let peer_node_addr = *PeerIdentity::from_npub(&peer_npub).unwrap().node_addr();
|
||||
|
||||
let mut config = Config::new();
|
||||
config.peers.push(crate::config::PeerConfig::new(
|
||||
peer_npub,
|
||||
"udp",
|
||||
"10.0.0.2:2121",
|
||||
));
|
||||
|
||||
let mut node = Node::new(config).unwrap();
|
||||
|
||||
let payload = Disconnect::new(DisconnectReason::Shutdown).encode();
|
||||
node.handle_disconnect(&peer_node_addr, &payload);
|
||||
|
||||
let state = node
|
||||
.retry_pending
|
||||
.get(&peer_node_addr)
|
||||
.expect("handle_disconnect should schedule reconnect for auto-connect peer");
|
||||
assert!(state.reconnect, "Entry should be marked as reconnect");
|
||||
assert_eq!(
|
||||
state.retry_count, 0,
|
||||
"Fresh reconnect after disconnect should start at count=0"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that promote_connection clears retry_pending.
|
||||
#[test]
|
||||
fn test_promote_clears_retry_pending() {
|
||||
|
||||
Reference in New Issue
Block a user