mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-31 12:06:15 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9121925d4b | ||
|
|
006c3314ad | ||
|
|
e8270970ee | ||
|
|
527514689f | ||
|
|
0aed417dbd | ||
|
|
3ae4ac725b |
@@ -8,6 +8,7 @@
|
||||
|
||||
pub mod lan;
|
||||
pub mod nostr;
|
||||
pub mod platform;
|
||||
|
||||
use crate::config::UdpConfig;
|
||||
use crate::{NodeAddr, TransportId};
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
//! Platform-pushed peer discovery.
|
||||
//!
|
||||
//! A generic seam for an embedding platform (e.g. an Android app layer that
|
||||
//! runs its own radio discovery, such as Wi-Fi Aware) to push "peer `npub` is
|
||||
//! reachable at `addr` over transport type `T`" events into a running node —
|
||||
//! the transport-agnostic generalization of the LAN mDNS drain
|
||||
//! (`poll_lan_discovery`), which delivers the same shape but is hardwired to
|
||||
//! UDP transports.
|
||||
//!
|
||||
//! The queue is a process-global, like the Android BLE bridge injection seam
|
||||
//! (`set_android_ble_bridge`): the embedder pushes without holding a `Node`
|
||||
//! handle, and the node drains once per tick in `poll_platform_discovery`.
|
||||
//! Events pushed while no node is running are retained up to [`QUEUE_CAP`]
|
||||
//! (oldest dropped first) so a push racing a node rebuild is not lost.
|
||||
//! With more than one node in a process, whichever drains first consumes
|
||||
//! the events (same caveat as the BLE bridge) — intended for the
|
||||
//! single-node embedding case.
|
||||
//!
|
||||
//! The pushed npub is only a routing hint: the Noise IK handshake is the
|
||||
//! authentication, exactly as with mDNS adverts — a spoofed push fails the
|
||||
//! IK exchange and is dropped.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::Mutex;
|
||||
|
||||
/// Maximum retained events while undrained. Beyond this the oldest event is
|
||||
/// dropped: platform pushes are periodic (radio discovery re-fires), so a
|
||||
/// dropped event is re-learned, while an unbounded queue would grow forever
|
||||
/// if the node is stopped.
|
||||
const QUEUE_CAP: usize = 256;
|
||||
|
||||
/// A peer reachability event pushed by the embedding platform.
|
||||
///
|
||||
/// Addresses and identities are strings at this seam (it is crossed from
|
||||
/// JNI); they are parsed and validated at drain time, where a bad value is
|
||||
/// logged and skipped rather than surfaced to the pusher.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum PlatformPeerEvent {
|
||||
/// The platform established reachability: dial `addr` on an operational
|
||||
/// transport whose type name matches `transport_type`. For `udp` the
|
||||
/// selection is family-aware — an IPv6 target picks an IPv6-capable
|
||||
/// socket, never a wildcard IPv4 one. For IPv6 link-local addresses the
|
||||
/// scope must be a numeric ifindex (`"[fe80::x%3]:4870"`) —
|
||||
/// interface-name scopes do not parse.
|
||||
Available {
|
||||
npub: String,
|
||||
addr: String,
|
||||
transport_type: String,
|
||||
},
|
||||
/// The platform observed the link go away (e.g. the Wi-Fi Aware data
|
||||
/// path was lost). The node closes any pooled connection it holds for
|
||||
/// the peer's current address on that transport so a dead socket is
|
||||
/// not re-used; reconnection is left to the ordinary machinery.
|
||||
Lost {
|
||||
npub: String,
|
||||
transport_type: String,
|
||||
},
|
||||
}
|
||||
|
||||
static QUEUE: Mutex<VecDeque<PlatformPeerEvent>> = Mutex::new(VecDeque::new());
|
||||
|
||||
fn push(event: PlatformPeerEvent) {
|
||||
let mut queue = QUEUE.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if queue.len() >= QUEUE_CAP {
|
||||
queue.pop_front();
|
||||
}
|
||||
queue.push_back(event);
|
||||
}
|
||||
|
||||
/// Push "peer is reachable at `addr` over `transport_type`".
|
||||
pub fn platform_peer_available(npub: &str, addr: &str, transport_type: &str) {
|
||||
push(PlatformPeerEvent::Available {
|
||||
npub: npub.to_string(),
|
||||
addr: addr.to_string(),
|
||||
transport_type: transport_type.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Push "the platform-managed link to peer went away".
|
||||
pub fn platform_peer_lost(npub: &str, transport_type: &str) {
|
||||
push(PlatformPeerEvent::Lost {
|
||||
npub: npub.to_string(),
|
||||
transport_type: transport_type.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Drain all queued events. Called by the node once per tick.
|
||||
pub fn drain_platform_peer_events() -> Vec<PlatformPeerEvent> {
|
||||
let mut queue = QUEUE.lock().unwrap_or_else(|e| e.into_inner());
|
||||
queue.drain(..).collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The queue is a process-global, so tests touching it must not
|
||||
/// interleave across test threads.
|
||||
static TEST_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
#[test]
|
||||
fn push_drain_roundtrip() {
|
||||
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
drain_platform_peer_events();
|
||||
platform_peer_available("npub1abc", "[fe80::1%3]:4870", "tcp");
|
||||
platform_peer_lost("npub1abc", "tcp");
|
||||
let events = drain_platform_peer_events();
|
||||
assert_eq!(events.len(), 2);
|
||||
assert_eq!(
|
||||
events[0],
|
||||
PlatformPeerEvent::Available {
|
||||
npub: "npub1abc".into(),
|
||||
addr: "[fe80::1%3]:4870".into(),
|
||||
transport_type: "tcp".into(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
events[1],
|
||||
PlatformPeerEvent::Lost {
|
||||
npub: "npub1abc".into(),
|
||||
transport_type: "tcp".into(),
|
||||
}
|
||||
);
|
||||
assert!(drain_platform_peer_events().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn queue_caps_by_dropping_oldest() {
|
||||
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
drain_platform_peer_events();
|
||||
for i in 0..(QUEUE_CAP + 10) {
|
||||
platform_peer_available(&format!("npub{i}"), "addr", "tcp");
|
||||
}
|
||||
let events = drain_platform_peer_events();
|
||||
assert_eq!(events.len(), QUEUE_CAP);
|
||||
match &events[0] {
|
||||
PlatformPeerEvent::Available { npub, .. } => assert_eq!(npub, "npub10"),
|
||||
other => panic!("unexpected event: {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -439,14 +439,30 @@ impl Node {
|
||||
let origin_coords = self.tree_state().my_coords().clone();
|
||||
let request = LookupRequest::generate(*target, origin, origin_coords, ttl, 0);
|
||||
|
||||
// Send only to tree peers whose bloom filter contains the target
|
||||
let peer_addrs: Vec<NodeAddr> = self
|
||||
// Prefer tree peers whose bloom filter contains the target.
|
||||
let mut peer_addrs: Vec<NodeAddr> = self
|
||||
.peers
|
||||
.iter()
|
||||
.filter(|(addr, peer)| self.is_tree_peer(addr) && peer.may_reach(target))
|
||||
.map(|(addr, _)| *addr)
|
||||
.collect();
|
||||
|
||||
// Edge fallback: if no tree peer advertises the target — always the case
|
||||
// for a *directly connected* target, since a neighbour is never in another
|
||||
// peer's bloom — flood the request to EVERY peer we can send to, not just
|
||||
// tree peers. Crucially this includes the target itself when it's a direct
|
||||
// (non-tree) neighbour: a node answers a lookup for its own address, so the
|
||||
// querier learns its coordinates and can route to it. Without this a
|
||||
// mesh-edge node can't reach its own neighbours.
|
||||
if peer_addrs.is_empty() {
|
||||
peer_addrs = self
|
||||
.peers
|
||||
.iter()
|
||||
.filter(|(_, peer)| peer.can_send())
|
||||
.map(|(addr, _)| *addr)
|
||||
.collect();
|
||||
}
|
||||
|
||||
let peer_count = peer_addrs.len();
|
||||
|
||||
debug!(
|
||||
@@ -509,14 +525,22 @@ impl Node {
|
||||
return;
|
||||
}
|
||||
|
||||
// Bloom filter pre-check: if no peer's filter contains the target,
|
||||
// it's not in the mesh — skip the lookup and record as failure.
|
||||
// Bloom filter pre-check: normally, if no peer's filter contains the
|
||||
// target we skip. But a *directly connected* peer is never in any other
|
||||
// peer's bloom (you reach it directly, not through them), so a mesh-edge
|
||||
// node — one whose only peers are direct links (BLE / Wi-Fi Aware / LAN),
|
||||
// with sparse blooms — could never discover coordinates for its own
|
||||
// neighbours and would fail to route to them. Only suppress on a bloom
|
||||
// miss when we have several peers whose blooms we can actually trust;
|
||||
// otherwise fall through and flood the lookup (bounded by TTL + the
|
||||
// `sent == 0` guard below).
|
||||
let reachable = self.peers.values().any(|peer| peer.may_reach(dest));
|
||||
if !reachable {
|
||||
if !reachable && self.peers.len() > 2 {
|
||||
self.metrics().discovery.req_bloom_miss.inc();
|
||||
self.discovery_backoff.record_failure(dest);
|
||||
debug!(
|
||||
target_node = %self.peer_display_name(dest),
|
||||
peers = self.peers.len(),
|
||||
"Discovery skipped, target not in any peer bloom filter"
|
||||
);
|
||||
return;
|
||||
|
||||
@@ -263,6 +263,11 @@ impl Node {
|
||||
let ce_flag = header.flags & FLAG_CE != 0;
|
||||
let sp_flag = header.flags & FLAG_SP != 0;
|
||||
|
||||
// Transport-preference cutover: gate roaming so a faster transport
|
||||
// (Wi-Fi Aware / UDP) isn't dragged back to a slower one (BLE) by a
|
||||
// stray packet. Computed before the peer borrow (needs `self`).
|
||||
let roam_pref = self.transport_preference(packet.transport_id);
|
||||
let roam_hysteresis = self.roam_hysteresis_ms();
|
||||
if let Some(peer) = self.peers.get_mut(&node_addr) {
|
||||
if let Some(mmp) = peer.mmp_mut() {
|
||||
mmp.receiver.record_recv(
|
||||
@@ -274,7 +279,13 @@ impl Node {
|
||||
);
|
||||
let _spin_rtt = mmp.spin_bit.rx_observe(sp_flag, header.counter, now);
|
||||
}
|
||||
peer.set_current_addr(packet.transport_id, packet.remote_addr.clone());
|
||||
peer.roam_current_addr(
|
||||
packet.transport_id,
|
||||
packet.remote_addr.clone(),
|
||||
roam_pref,
|
||||
packet.timestamp_ms,
|
||||
roam_hysteresis,
|
||||
);
|
||||
peer.link_stats_mut()
|
||||
.record_recv(packet.data.len(), packet.timestamp_ms);
|
||||
peer.touch(packet.timestamp_ms);
|
||||
@@ -356,10 +367,18 @@ impl Node {
|
||||
return;
|
||||
};
|
||||
let now = Instant::now();
|
||||
let roam_pref = self.transport_preference(transport_id);
|
||||
let roam_hysteresis = self.roam_hysteresis_ms();
|
||||
let mut address_changed = false;
|
||||
if let Some(peer) = self.peers.get_mut(node_addr) {
|
||||
peer.reset_decrypt_failures();
|
||||
address_changed = peer.set_current_addr(transport_id, remote_addr.clone());
|
||||
address_changed = peer.roam_current_addr(
|
||||
transport_id,
|
||||
remote_addr.clone(),
|
||||
roam_pref,
|
||||
packet_timestamp_ms,
|
||||
roam_hysteresis,
|
||||
);
|
||||
peer.link_stats_mut()
|
||||
.record_recv(packet_len, packet_timestamp_ms);
|
||||
peer.touch(packet_timestamp_ms);
|
||||
|
||||
@@ -264,6 +264,7 @@ impl Node {
|
||||
self.poll_pending_connects().await;
|
||||
self.poll_nostr_discovery().await;
|
||||
self.poll_lan_discovery().await;
|
||||
self.poll_platform_discovery().await;
|
||||
self.resend_pending_handshakes(now_ms).await;
|
||||
self.resend_pending_rekeys(now_ms).await;
|
||||
self.resend_pending_session_handshakes(now_ms).await;
|
||||
|
||||
@@ -2140,6 +2140,15 @@ impl Node {
|
||||
}
|
||||
if let Err(e) = self.send_ipv6_packet(&dest_addr, &ipv6_packet).await {
|
||||
debug!(dest = %self.peer_display_name(&dest_addr), error = %e, "Failed to send TUN packet via session");
|
||||
// An established session can still have no route when its
|
||||
// coordinates were never learned or went stale — e.g. a
|
||||
// platform-pushed peer (Wi-Fi Aware / LAN) whose Noise session
|
||||
// came up before tree discovery. Unlike session initiation
|
||||
// (below), this path didn't trigger a lookup, so the route
|
||||
// never warmed and every packet was silently dropped. Kick
|
||||
// discovery (rate-limited internally); the app's retransmit
|
||||
// then succeeds once coordinates arrive.
|
||||
self.maybe_initiate_lookup(&dest_addr).await;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
+182
-27
@@ -367,6 +367,27 @@ impl Node {
|
||||
.min_by_key(|(id, _)| id.as_u32())
|
||||
}
|
||||
|
||||
/// Resolve a discovered `(transport_type, addr)` pair to an operational
|
||||
/// transport. For `udp` addresses the socket family matters — a wildcard
|
||||
/// IPv4 socket cannot send to an IPv6 link-local target (e.g. a Wi-Fi
|
||||
/// Aware data path) — so when the address parses as a socket address the
|
||||
/// selection is family-aware and skips bootstrap-adopted sockets. All
|
||||
/// other transport types match by name alone.
|
||||
fn find_transport_for_discovered_addr(
|
||||
&self,
|
||||
transport_type: &str,
|
||||
addr: &str,
|
||||
) -> Option<TransportId> {
|
||||
if transport_type == "udp"
|
||||
&& let Ok(remote_addr) = addr.parse::<SocketAddr>()
|
||||
{
|
||||
return self
|
||||
.find_udp_transport_for_remote_addr(remote_addr)
|
||||
.map(|(id, _)| id);
|
||||
}
|
||||
self.find_transport_for_type(transport_type)
|
||||
}
|
||||
|
||||
/// Initiate a connection to a peer on a specific transport and address.
|
||||
///
|
||||
/// For connectionless transports (UDP, Ethernet): allocates a link, starts
|
||||
@@ -599,6 +620,18 @@ impl Node {
|
||||
let remote_addr = peer.addr;
|
||||
|
||||
if self.peers.contains_key(&node_addr) {
|
||||
// Don't re-probe an alternate path that is strictly worse
|
||||
// than the one the peer is already on: a peer settled on a
|
||||
// faster transport (Wi-Fi Aware / UDP) must not be pulled
|
||||
// back to BLE by BLE rediscovery. Equal-or-better candidates
|
||||
// still refresh, so BLE → Aware upgrades proceed.
|
||||
if let Some(cur) = self.peers.get(&node_addr).and_then(|p| p.transport_id()) {
|
||||
if self.transport_preference(cur)
|
||||
> self.transport_preference(candidate_transport_id)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let transport_name = transport.transport_type().name;
|
||||
let candidate = PeerAddress::new(transport_name, remote_addr.to_string());
|
||||
if self.active_peer_candidate_is_fresh_enough_to_skip(
|
||||
@@ -956,6 +989,139 @@ impl Node {
|
||||
}
|
||||
}
|
||||
|
||||
/// Drain platform-pushed peers and initiate Noise IK handshakes.
|
||||
///
|
||||
/// The transport-agnostic sibling of `poll_lan_discovery`: an embedding
|
||||
/// platform (e.g. the Android Wi-Fi Aware radio) pushes
|
||||
/// `(npub, addr, transport type)` events into the process-global queue
|
||||
/// (`crate::discovery::platform`) and this drains them once per tick.
|
||||
/// As with mDNS, the pushed npub is only a hint — the IK handshake is
|
||||
/// the authentication.
|
||||
///
|
||||
/// Unlike the LAN drain, an event for an already-active peer starts an
|
||||
/// alternate-path handshake (gated by the same freshness/in-flight
|
||||
/// checks as `poll_transport_discovery`), so a platform push can move a
|
||||
/// peer onto a faster transport — the BLE→Wi-Fi-Aware cutover.
|
||||
pub(super) async fn poll_platform_discovery(&mut self) {
|
||||
let events = crate::discovery::platform::drain_platform_peer_events();
|
||||
if events.is_empty() {
|
||||
return;
|
||||
}
|
||||
let mut connect_budget = self.discovery_connect_budget();
|
||||
for event in events {
|
||||
match event {
|
||||
crate::discovery::platform::PlatformPeerEvent::Available {
|
||||
npub,
|
||||
addr,
|
||||
transport_type,
|
||||
} => {
|
||||
let Some(transport_id) =
|
||||
self.find_transport_for_discovered_addr(&transport_type, &addr)
|
||||
else {
|
||||
debug!(
|
||||
npub = %npub,
|
||||
transport_type = %transport_type,
|
||||
addr = %addr,
|
||||
"platform: skip pushed peer with no compatible operational transport"
|
||||
);
|
||||
continue;
|
||||
};
|
||||
let identity = match crate::PeerIdentity::from_npub(&npub) {
|
||||
Ok(id) => id,
|
||||
Err(err) => {
|
||||
debug!(npub = %npub, error = %err, "platform: skip bad npub");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let peer_node_addr = *identity.node_addr();
|
||||
if peer_node_addr == *self.identity().node_addr() {
|
||||
continue;
|
||||
}
|
||||
let remote_addr = crate::transport::TransportAddr::from_string(&addr);
|
||||
|
||||
if self.peers.contains_key(&peer_node_addr) {
|
||||
// Active peer: this is a path upgrade, not a first
|
||||
// contact — apply the alternate-path gates.
|
||||
let candidate = PeerAddress::new(&transport_type, addr.clone());
|
||||
if self.active_peer_candidate_is_fresh_enough_to_skip(
|
||||
&peer_node_addr,
|
||||
std::slice::from_ref(&candidate),
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if self.is_connecting_to_peer_on_path(
|
||||
&peer_node_addr,
|
||||
transport_id,
|
||||
&remote_addr,
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if connect_budget == 0 {
|
||||
debug!(npub = %npub, "platform: connect budget exhausted");
|
||||
continue;
|
||||
}
|
||||
connect_budget = connect_budget.saturating_sub(1);
|
||||
info!(
|
||||
peer = %self.peer_display_name(&peer_node_addr),
|
||||
transport_id = %transport_id,
|
||||
remote_addr = %remote_addr,
|
||||
"platform: initiating handshake to pushed peer"
|
||||
);
|
||||
if let Err(err) = self
|
||||
.initiate_connection(transport_id, remote_addr, identity)
|
||||
.await
|
||||
{
|
||||
debug!(
|
||||
npub = %npub,
|
||||
error = %err,
|
||||
"platform: failed to initiate connection to pushed peer"
|
||||
);
|
||||
}
|
||||
}
|
||||
crate::discovery::platform::PlatformPeerEvent::Lost {
|
||||
npub,
|
||||
transport_type,
|
||||
} => {
|
||||
let Ok(identity) = crate::PeerIdentity::from_npub(&npub) else {
|
||||
continue;
|
||||
};
|
||||
let peer_node_addr = *identity.node_addr();
|
||||
let Some(peer) = self.peers.get(&peer_node_addr) else {
|
||||
continue;
|
||||
};
|
||||
// Only act if the peer currently sits on the named
|
||||
// transport type: close the pooled connection so the
|
||||
// dead socket is not re-used. Reconnection (including
|
||||
// falling back to another transport) is the ordinary
|
||||
// machinery's job.
|
||||
let (Some(transport_id), Some(current_addr)) =
|
||||
(peer.transport_id(), peer.current_addr().cloned())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let on_named_transport = self
|
||||
.transports
|
||||
.get(&transport_id)
|
||||
.map(|t| t.transport_type().name == transport_type)
|
||||
.unwrap_or(false);
|
||||
if !on_named_transport {
|
||||
continue;
|
||||
}
|
||||
info!(
|
||||
peer = %self.peer_display_name(&peer_node_addr),
|
||||
transport_id = %transport_id,
|
||||
remote_addr = %current_addr,
|
||||
"platform: closing connection for lost pushed peer"
|
||||
);
|
||||
if let Some(transport) = self.transports.get(&transport_id) {
|
||||
transport.close_connection(¤t_addr).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Poll pending transport connects and initiate handshakes for ready ones.
|
||||
///
|
||||
/// Called from the tick handler. For each pending connect, queries the
|
||||
@@ -1076,6 +1242,12 @@ impl Node {
|
||||
|
||||
match handle.start().await {
|
||||
Ok(()) => {
|
||||
#[cfg(unix)]
|
||||
if transport_type == "udp"
|
||||
&& let (Some(tx), Some(fd)) = (&self.udp_bind_tx, handle.raw_fd())
|
||||
{
|
||||
let _ = tx.send(fd);
|
||||
}
|
||||
self.transports.insert(transport_id, handle);
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -1769,34 +1941,17 @@ impl Node {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
let tid = if addr.transport == "udp"
|
||||
&& let Ok(remote_socket_addr) = addr.addr.parse::<SocketAddr>()
|
||||
{
|
||||
match self.find_udp_transport_for_remote_addr(remote_socket_addr) {
|
||||
Some((id, _)) => id,
|
||||
None => {
|
||||
debug!(
|
||||
transport = %addr.transport,
|
||||
addr = %addr.addr,
|
||||
"No compatible operational UDP transport for address"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
match self.find_transport_for_discovered_addr(&addr.transport, &addr.addr) {
|
||||
Some(tid) => (tid, TransportAddr::from_string(&addr.addr)),
|
||||
None => {
|
||||
debug!(
|
||||
transport = %addr.transport,
|
||||
addr = %addr.addr,
|
||||
"No compatible operational transport for address"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
match self.find_transport_for_type(&addr.transport) {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
debug!(
|
||||
transport = %addr.transport,
|
||||
addr = %addr.addr,
|
||||
"No operational transport for address type"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
};
|
||||
(tid, TransportAddr::from_string(&addr.addr))
|
||||
}
|
||||
};
|
||||
|
||||
if self.is_connecting_to_peer_on_path(&peer_node_addr, transport_id, &remote_addr) {
|
||||
|
||||
+109
-23
@@ -440,6 +440,13 @@ pub struct Node {
|
||||
/// DNS responder task handle.
|
||||
dns_task: Option<tokio::task::JoinHandle<()>>,
|
||||
|
||||
// === App-owned UDP socket binding ===
|
||||
/// Fires once with the UDP transport's raw fd right after it starts, so an
|
||||
/// embedder can apply host-specific socket options (e.g. pinning it to one
|
||||
/// of several OS networks). See [`Self::enable_app_owned_udp_fd`].
|
||||
#[cfg(unix)]
|
||||
udp_bind_tx: Option<std::sync::mpsc::Sender<std::os::unix::io::RawFd>>,
|
||||
|
||||
// === Index-Based Session Dispatch ===
|
||||
/// Allocator for session indices.
|
||||
index_allocator: IndexAllocator,
|
||||
@@ -699,6 +706,8 @@ impl Node {
|
||||
tun_shutdown_fd: None,
|
||||
dns_identity_rx: None,
|
||||
dns_task: None,
|
||||
#[cfg(unix)]
|
||||
udp_bind_tx: None,
|
||||
index_allocator: IndexAllocator::new(),
|
||||
peers_by_index: HashMap::new(),
|
||||
pending_outbound: HashMap::new(),
|
||||
@@ -860,6 +869,8 @@ impl Node {
|
||||
tun_shutdown_fd: None,
|
||||
dns_identity_rx: None,
|
||||
dns_task: None,
|
||||
#[cfg(unix)]
|
||||
udp_bind_tx: None,
|
||||
index_allocator: IndexAllocator::new(),
|
||||
peers_by_index: HashMap::new(),
|
||||
pending_outbound: HashMap::new(),
|
||||
@@ -1054,29 +1065,33 @@ impl Node {
|
||||
.iter()
|
||||
.map(|(name, config)| (name.map(|s| s.to_string()), config.clone()))
|
||||
.collect();
|
||||
match crate::transport::ble::android_io::android_ble_bridge() {
|
||||
Some(bridge) => {
|
||||
for (name, ble_config) in ble_instances {
|
||||
let transport_id = self.allocate_transport_id();
|
||||
let io = crate::transport::ble::android_io::AndroidIo::new(
|
||||
std::sync::Arc::clone(&bridge),
|
||||
);
|
||||
let mut ble = crate::transport::ble::BleTransport::new(
|
||||
transport_id,
|
||||
name,
|
||||
ble_config,
|
||||
io,
|
||||
packet_tx.clone(),
|
||||
);
|
||||
ble.set_local_pubkey(self.identity().pubkey().serialize());
|
||||
transports.push(TransportHandle::Ble(ble));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if !ble_instances.is_empty() {
|
||||
tracing::warn!("BLE configured but no Android radio bridge injected");
|
||||
}
|
||||
}
|
||||
// Built whether or not a radio bridge exists yet, and without
|
||||
// capturing the one that does: `AndroidIo` resolves the process-wide
|
||||
// bridge per operation. The radio is owned by an Android foreground
|
||||
// service whose lifetime is independent of the node's — it can start
|
||||
// after the node, and it mints a fresh bridge every time it starts.
|
||||
// Binding either fact into the transport at construction meant the
|
||||
// only way to pick up a radio was to stop and rebuild the node,
|
||||
// which drops every peer and session. Operations attempted while no
|
||||
// radio is present fail as transport errors and recover on their own
|
||||
// once one appears.
|
||||
if !ble_instances.is_empty()
|
||||
&& crate::transport::ble::android_io::android_ble_bridge().is_none()
|
||||
{
|
||||
tracing::info!("BLE configured; waiting for the Android radio bridge");
|
||||
}
|
||||
for (name, ble_config) in ble_instances {
|
||||
let transport_id = self.allocate_transport_id();
|
||||
let io = crate::transport::ble::android_io::AndroidIo::from_global();
|
||||
let mut ble = crate::transport::ble::BleTransport::new(
|
||||
transport_id,
|
||||
name,
|
||||
ble_config,
|
||||
io,
|
||||
packet_tx.clone(),
|
||||
);
|
||||
ble.set_local_pubkey(self.identity().pubkey().serialize());
|
||||
transports.push(TransportHandle::Ble(ble));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1093,6 +1108,29 @@ impl Node {
|
||||
.map(|(id, _)| *id)
|
||||
}
|
||||
|
||||
/// Link preference of a transport instance by id (higher = preferred),
|
||||
/// from its transport type. See [`crate::transport::transport_link_preference`].
|
||||
/// Unknown ids get 0 so any real transport outranks a stale/removed one.
|
||||
pub(crate) fn transport_preference(&self, id: TransportId) -> u8 {
|
||||
self.transports
|
||||
.get(&id)
|
||||
.map(|h| crate::transport::transport_link_preference(h.transport_type().name))
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// How long a preferred transport must be silent before a lower-preference
|
||||
/// transport may take over the peer's link (the roaming cutover hysteresis).
|
||||
/// ~2 heartbeat intervals so a single missed heartbeat can't cause a
|
||||
/// spurious downgrade; floored so a tiny configured interval can't flap.
|
||||
pub(crate) fn roam_hysteresis_ms(&self) -> u64 {
|
||||
(self
|
||||
.config()
|
||||
.node
|
||||
.heartbeat_interval_secs
|
||||
.saturating_mul(2_000))
|
||||
.max(15_000)
|
||||
}
|
||||
|
||||
/// Resolve an Ethernet peer address ("interface/mac") to a transport ID
|
||||
/// and binary TransportAddr.
|
||||
///
|
||||
@@ -2887,6 +2925,54 @@ impl Node {
|
||||
(outbound_tx, tun_rx)
|
||||
}
|
||||
|
||||
/// Set up an **app-owned DNS resolver**: an embedder that answers `.fips`
|
||||
/// queries itself (e.g. the Android VpnService packet pump, which has no
|
||||
/// system DNS socket) returns each resolved identity through the sender this
|
||||
/// returns. The `run_rx_loop` consumes them and calls
|
||||
/// [`Self::register_identity`] — the same identity-cache population (and hence
|
||||
/// route warming) the built-in [`crate::upper::dns::run_dns_responder`] does.
|
||||
///
|
||||
/// Without this the embedder's answers resolve the AAAA but leave the node's
|
||||
/// identity cache empty, so the first outbound packet to a freshly-resolved
|
||||
/// `<npub>.fips` has no cached pubkey to open a session with and is dropped.
|
||||
///
|
||||
/// Call after [`Node::new`] and **before** [`Self::start`], like
|
||||
/// [`Self::enable_app_owned_tun`].
|
||||
pub fn enable_app_owned_dns(&mut self) -> crate::upper::dns::DnsIdentityTx {
|
||||
let size = self.config().node.buffers.tun_channel.max(1);
|
||||
let (identity_tx, identity_rx) = tokio::sync::mpsc::channel(size);
|
||||
self.dns_identity_rx = Some(identity_rx);
|
||||
identity_tx
|
||||
}
|
||||
|
||||
/// Set up an **app-owned UDP socket binding**: the returned receiver gets
|
||||
/// the UDP transport's raw socket fd once, right after the transport opens
|
||||
/// inside [`Self::start`], so an embedder can apply a socket option FIPS
|
||||
/// itself has no way to choose — in particular pinning the socket to one
|
||||
/// of several networks the host OS offers.
|
||||
///
|
||||
/// FIPS opens a single wildcard UDP socket and picks the egress path per
|
||||
/// destination address, which assumes the OS routes by destination alone.
|
||||
/// Some hosts don't: where the OS binds each socket to one "network" and
|
||||
/// steers replies by that association, a peer reachable only over a
|
||||
/// secondary network (a link-local address on an interface that is not
|
||||
/// the default route) can receive our handshake and have its reply
|
||||
/// discarded before it reaches the socket. The fd is the only handle that
|
||||
/// lets the embedder correct this, and it is otherwise private to the
|
||||
/// transport.
|
||||
///
|
||||
/// Call after [`Node::new`] and before [`Self::start`], like
|
||||
/// [`Self::enable_app_owned_tun`]. Nothing is ever sent if no UDP
|
||||
/// transport is configured or it fails to start.
|
||||
#[cfg(unix)]
|
||||
pub fn enable_app_owned_udp_fd(
|
||||
&mut self,
|
||||
) -> std::sync::mpsc::Receiver<std::os::unix::io::RawFd> {
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
self.udp_bind_tx = Some(tx);
|
||||
rx
|
||||
}
|
||||
|
||||
// === Sending ===
|
||||
|
||||
/// Encrypt and send a link-layer message to an authenticated peer.
|
||||
|
||||
@@ -99,6 +99,14 @@ pub struct ActivePeer {
|
||||
transport_id: Option<TransportId>,
|
||||
/// Current transport address (for roaming support).
|
||||
current_addr: Option<TransportAddr>,
|
||||
/// Link preference of the current transport (higher = preferred). Gates
|
||||
/// roaming so a fast transport (e.g. Wi-Fi Aware / UDP) is not dragged back
|
||||
/// onto a slow one (BLE) by a stray packet — see `roam_current_addr`.
|
||||
current_transport_preference: u8,
|
||||
/// When the current transport last delivered an authenticated packet (Unix
|
||||
/// ms). Lets the roam gate detect that the preferred transport has gone
|
||||
/// silent, so a lower-preference transport may take over.
|
||||
current_transport_last_recv_ms: u64,
|
||||
|
||||
// === Spanning Tree ===
|
||||
/// Their latest parent declaration.
|
||||
@@ -232,6 +240,8 @@ impl ActivePeer {
|
||||
their_index: None,
|
||||
transport_id: None,
|
||||
current_addr: None,
|
||||
current_transport_preference: 0,
|
||||
current_transport_last_recv_ms: 0,
|
||||
declaration: None,
|
||||
ancestry: None,
|
||||
tree_announce_min_interval_ms: 500,
|
||||
@@ -318,6 +328,11 @@ impl ActivePeer {
|
||||
their_index: Some(their_index),
|
||||
transport_id: Some(transport_id),
|
||||
current_addr: Some(current_addr),
|
||||
// Preference 0 until the first authenticated data packet sets the
|
||||
// real value (via roam_current_addr); last-recv starts fresh at
|
||||
// authentication so the preferred link isn't seen as stale.
|
||||
current_transport_preference: 0,
|
||||
current_transport_last_recv_ms: authenticated_at,
|
||||
declaration: None,
|
||||
ancestry: None,
|
||||
tree_announce_min_interval_ms: 500,
|
||||
@@ -535,6 +550,45 @@ impl ActivePeer {
|
||||
changed
|
||||
}
|
||||
|
||||
/// Roam to `(transport_id, addr)` under the transport-preference cutover
|
||||
/// policy, returning whether `(transport_id, addr)` actually changed.
|
||||
///
|
||||
/// The new path is adopted when it is the current transport (ordinary
|
||||
/// address roaming, e.g. BLE MAC rotation), when its `preference` is `>=`
|
||||
/// the current transport's (eager upgrade to a faster link — BLE →
|
||||
/// Wi-Fi Aware), or when the current, higher-preference transport has gone
|
||||
/// silent for at least `hysteresis_ms` (the preferred link died — fall
|
||||
/// back). A lower-preference packet arriving on a still-live preferred
|
||||
/// transport is ignored for roaming, so e.g. a BLE keepalive cannot drag an
|
||||
/// active Wi-Fi Aware session back onto BLE. Equal preferences reduce to
|
||||
/// plain last-authenticated-packet-wins roaming.
|
||||
pub fn roam_current_addr(
|
||||
&mut self,
|
||||
transport_id: TransportId,
|
||||
addr: TransportAddr,
|
||||
preference: u8,
|
||||
now_ms: u64,
|
||||
hysteresis_ms: u64,
|
||||
) -> bool {
|
||||
let same_transport = self.transport_id == Some(transport_id);
|
||||
let adopt = self.transport_id.is_none()
|
||||
|| same_transport
|
||||
|| preference >= self.current_transport_preference
|
||||
|| now_ms.saturating_sub(self.current_transport_last_recv_ms) >= hysteresis_ms;
|
||||
if !adopt {
|
||||
// Stay on the preferred transport; do not refresh its last-recv,
|
||||
// so it keeps aging toward the hysteresis fallback if it is dead.
|
||||
return false;
|
||||
}
|
||||
let changed =
|
||||
self.transport_id != Some(transport_id) || self.current_addr.as_ref() != Some(&addr);
|
||||
self.transport_id = Some(transport_id);
|
||||
self.current_addr = Some(addr);
|
||||
self.current_transport_preference = preference;
|
||||
self.current_transport_last_recv_ms = now_ms;
|
||||
changed
|
||||
}
|
||||
|
||||
// === Handshake Resend ===
|
||||
|
||||
/// Store wire-format msg2 for resend on duplicate msg1.
|
||||
@@ -1245,6 +1299,40 @@ mod tests {
|
||||
assert!(!peer.can_send());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roam_transport_preference_cutover() {
|
||||
let mut peer = ActivePeer::new(make_peer_identity(), LinkId::new(1), 1000);
|
||||
let ble = TransportId::new(1);
|
||||
let udp = TransportId::new(2);
|
||||
let ble_addr = TransportAddr::from_string("ble0/AA:BB");
|
||||
let udp_addr = TransportAddr::from_string("[fe80::1%3]:4870");
|
||||
const BLE_PREF: u8 = 50;
|
||||
const UDP_PREF: u8 = 100;
|
||||
const HYST: u64 = 20_000;
|
||||
|
||||
// First packet (no current transport): adopted.
|
||||
assert!(peer.roam_current_addr(ble, ble_addr.clone(), BLE_PREF, 1000, HYST));
|
||||
assert_eq!(peer.transport_id(), Some(ble));
|
||||
|
||||
// Higher-preference transport: eager upgrade BLE -> Aware/UDP.
|
||||
assert!(peer.roam_current_addr(udp, udp_addr.clone(), UDP_PREF, 1100, HYST));
|
||||
assert_eq!(peer.transport_id(), Some(udp));
|
||||
|
||||
// Lower-preference keepalive while UDP is fresh: ignored, stays on UDP.
|
||||
assert!(!peer.roam_current_addr(ble, ble_addr.clone(), BLE_PREF, 1200, HYST));
|
||||
assert_eq!(peer.transport_id(), Some(udp));
|
||||
|
||||
// UDP silent past the hysteresis window: a BLE packet now wins (fall
|
||||
// back). Last UDP recv was 1100; this arrives at 1100 + HYST + 1.
|
||||
assert!(peer.roam_current_addr(ble, ble_addr.clone(), BLE_PREF, 1100 + HYST + 1, HYST));
|
||||
assert_eq!(peer.transport_id(), Some(ble));
|
||||
|
||||
// Same-transport address roaming is always allowed.
|
||||
let ble_addr2 = TransportAddr::from_string("ble0/CC:DD");
|
||||
assert!(peer.roam_current_addr(ble, ble_addr2.clone(), BLE_PREF, 1100 + HYST + 2, HYST));
|
||||
assert_eq!(peer.current_addr(), Some(&ble_addr2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tree_position() {
|
||||
let identity = make_peer_identity();
|
||||
|
||||
@@ -387,12 +387,44 @@ pub fn android_ble_bridge() -> Option<Arc<AndroidBleBridge>> {
|
||||
|
||||
/// macOS/Android-style external-radio backend over [`AndroidBleBridge`].
|
||||
pub struct AndroidIo {
|
||||
bridge: Arc<AndroidBleBridge>,
|
||||
/// Bridge to use when one was supplied explicitly (tests). In the app this
|
||||
/// is `None` and every operation resolves [`android_ble_bridge`] instead.
|
||||
///
|
||||
/// Resolving per call is what lets the radio be replaced without rebuilding
|
||||
/// the node. The Android service mints a fresh bridge each time it starts
|
||||
/// (a new `BleRadio`, a new JNI handle); if this type captured that `Arc` at
|
||||
/// construction, a running node would keep driving the dead one, and the
|
||||
/// only way to adopt the new radio would be to stop and restart the node —
|
||||
/// dropping every peer and every session with it.
|
||||
bridge: Option<Arc<AndroidBleBridge>>,
|
||||
}
|
||||
|
||||
impl AndroidIo {
|
||||
/// Resolve the bridge from the process-wide slot on each operation.
|
||||
pub fn from_global() -> Self {
|
||||
Self { bridge: None }
|
||||
}
|
||||
|
||||
/// Pin a specific bridge, for tests that drive one directly.
|
||||
pub fn new(bridge: Arc<AndroidBleBridge>) -> Self {
|
||||
Self { bridge }
|
||||
Self {
|
||||
bridge: Some(bridge),
|
||||
}
|
||||
}
|
||||
|
||||
/// The bridge for this operation. `None` once the radio has gone away —
|
||||
/// callers surface that as a transport error rather than panicking, since
|
||||
/// it is reachable simply by toggling Bluetooth off mid-operation.
|
||||
fn bridge(&self) -> Option<Arc<AndroidBleBridge>> {
|
||||
match &self.bridge {
|
||||
Some(b) => Some(Arc::clone(b)),
|
||||
None => android_ble_bridge(),
|
||||
}
|
||||
}
|
||||
|
||||
fn require_bridge(&self) -> Result<Arc<AndroidBleBridge>, TransportError> {
|
||||
self.bridge()
|
||||
.ok_or_else(|| TransportError::Io(std::io::Error::other("BLE radio not available")))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -533,42 +565,40 @@ impl BleIo for AndroidIo {
|
||||
type Scanner = AndroidScanner;
|
||||
|
||||
async fn listen(&self, _psm: u16) -> Result<AndroidAcceptor, TransportError> {
|
||||
let bridge = self.require_bridge()?;
|
||||
// Android assigns the listener PSM; the `psm` arg from FIPS is ignored.
|
||||
let os_psm = self.bridge.radio.listen();
|
||||
let os_psm = bridge.radio.listen();
|
||||
if os_psm != 0 {
|
||||
self.bridge.local_psm.store(os_psm, Ordering::Relaxed);
|
||||
bridge.local_psm.store(os_psm, Ordering::Relaxed);
|
||||
}
|
||||
let rx = self
|
||||
.bridge
|
||||
let rx = bridge
|
||||
.accept_rx
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.take();
|
||||
Ok(AndroidAcceptor {
|
||||
rx,
|
||||
radio: Arc::clone(&self.bridge.radio),
|
||||
radio: Arc::clone(&bridge.radio),
|
||||
})
|
||||
}
|
||||
|
||||
async fn connect(&self, addr: &BleAddr, psm: u16) -> Result<AndroidStream, TransportError> {
|
||||
let bridge = self.require_bridge()?;
|
||||
// Substitute the learned per-peer PSM for this address, if known.
|
||||
let dial_psm = self.bridge.psm_map.resolve(addr, psm);
|
||||
let connect_id = self.bridge.next_id.fetch_add(1, Ordering::Relaxed);
|
||||
let dial_psm = bridge.psm_map.resolve(addr, psm);
|
||||
let connect_id = bridge.next_id.fetch_add(1, Ordering::Relaxed);
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.bridge
|
||||
bridge
|
||||
.connects
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.insert(connect_id, tx);
|
||||
self.bridge.radio.connect(connect_id, addr, dial_psm);
|
||||
bridge.radio.connect(connect_id, addr, dial_psm);
|
||||
// FIPS already wraps connect() in a timeout, so we just await the result.
|
||||
match rx.await {
|
||||
Ok(ep) => Ok(AndroidStream::from_endpoints(
|
||||
ep,
|
||||
Arc::clone(&self.bridge.radio),
|
||||
)),
|
||||
Ok(ep) => Ok(AndroidStream::from_endpoints(ep, Arc::clone(&bridge.radio))),
|
||||
Err(_) => {
|
||||
self.bridge
|
||||
bridge
|
||||
.connects
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
@@ -581,24 +611,26 @@ impl BleIo for AndroidIo {
|
||||
}
|
||||
|
||||
async fn start_advertising(&self) -> Result<(), TransportError> {
|
||||
self.bridge
|
||||
let bridge = self.require_bridge()?;
|
||||
bridge
|
||||
.radio
|
||||
.start_advertising(self.bridge.local_psm.load(Ordering::Relaxed));
|
||||
.start_advertising(bridge.local_psm.load(Ordering::Relaxed));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn stop_advertising(&self) -> Result<(), TransportError> {
|
||||
self.bridge.radio.stop_advertising();
|
||||
let bridge = self.require_bridge()?;
|
||||
bridge.radio.stop_advertising();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn start_scanning(&self) -> Result<AndroidScanner, TransportError> {
|
||||
let bridge = self.require_bridge()?;
|
||||
// Re-learn PSMs / adverts each scan cycle (addresses rotate with MAC).
|
||||
self.bridge.psm_map.clear();
|
||||
self.bridge.clear_adverts();
|
||||
self.bridge.radio.start_scanning();
|
||||
let rx = self
|
||||
.bridge
|
||||
bridge.psm_map.clear();
|
||||
bridge.clear_adverts();
|
||||
bridge.radio.start_scanning();
|
||||
let rx = bridge
|
||||
.scan_rx
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
|
||||
@@ -280,6 +280,27 @@ impl fmt::Display for TransportType {
|
||||
}
|
||||
}
|
||||
|
||||
/// Link preference for a transport type — higher is preferred for carrying a
|
||||
/// peer's traffic when that peer is reachable over more than one transport at
|
||||
/// once. Used by the roaming cutover (`ActivePeer::roam_current_addr`) so a
|
||||
/// peer on a fast transport (Wi-Fi Aware / UDP) is not dragged onto a slow
|
||||
/// one (BLE) by a stray packet. Equal preferences reduce to plain
|
||||
/// last-authenticated-packet-wins roaming, so single-transport deployments are
|
||||
/// unaffected.
|
||||
pub fn transport_link_preference(name: &str) -> u8 {
|
||||
match name {
|
||||
// High-bandwidth IP transports (incl. the Wi-Fi Aware data path, which
|
||||
// rides the UDP transport).
|
||||
"wifi" | "udp" | "tcp" => 100,
|
||||
"ethernet" => 90,
|
||||
// Always-on but low-bandwidth control link.
|
||||
"ble" => 50,
|
||||
// Anonymity overlays: high latency.
|
||||
"tor" | "nym" => 40,
|
||||
_ => 80,
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Transport State
|
||||
// ============================================================================
|
||||
@@ -1074,6 +1095,24 @@ impl TransportHandle {
|
||||
}
|
||||
}
|
||||
|
||||
/// Raw fd of the bound socket (UDP only, returns None for other
|
||||
/// transports). See [`crate::transport::udp::UdpTransport::raw_fd`].
|
||||
#[cfg(unix)]
|
||||
pub fn raw_fd(&self) -> Option<std::os::unix::io::RawFd> {
|
||||
match self {
|
||||
TransportHandle::Udp(t) => t.raw_fd(),
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
TransportHandle::Ethernet(_) => None,
|
||||
TransportHandle::Tcp(_) => None,
|
||||
TransportHandle::Tor(_) => None,
|
||||
TransportHandle::Nym(_) => None,
|
||||
#[cfg(ble_available)]
|
||||
TransportHandle::Ble(_) => None,
|
||||
#[cfg(test)]
|
||||
TransportHandle::Loopback(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the interface name (Ethernet only, returns None for other transports).
|
||||
pub fn interface_name(&self) -> Option<&str> {
|
||||
match self {
|
||||
|
||||
@@ -89,6 +89,17 @@ impl UdpTransport {
|
||||
self.local_addr
|
||||
}
|
||||
|
||||
/// Raw fd of the bound socket, so an embedder can apply socket options
|
||||
/// this transport does not manage itself — notably binding it to one of
|
||||
/// several networks a host OS offers, when destination-based routing alone
|
||||
/// does not reach a peer (see [`crate::Node::enable_app_owned_udp_fd`]).
|
||||
/// `None` before start.
|
||||
#[cfg(unix)]
|
||||
pub fn raw_fd(&self) -> Option<std::os::unix::io::RawFd> {
|
||||
use std::os::unix::io::AsRawFd;
|
||||
self.socket.as_ref().map(|s| s.as_raw_fd())
|
||||
}
|
||||
|
||||
/// Configured recv buffer size — used when opening per-peer
|
||||
/// `ConnectedPeerSocket`s so they get the same buffer ceiling as
|
||||
/// the wildcard listen socket.
|
||||
|
||||
@@ -606,7 +606,17 @@ mod platform {
|
||||
unsafe { &*(storage as *const _ as *const libc::sockaddr_in6) };
|
||||
let ip = std::net::Ipv6Addr::from(addr.sin6_addr.s6_addr);
|
||||
let port = u16::from_be(addr.sin6_port);
|
||||
Ok(SocketAddr::from((ip, port)))
|
||||
// Preserve sin6_scope_id: a link-local source (fe80::/10) is
|
||||
// only routable together with its interface scope, so dropping
|
||||
// it here breaks replies to a link-local peer (e.g. a Wi-Fi
|
||||
// Aware NDP interface). Non-link-local sources carry scope 0,
|
||||
// for which SocketAddrV6 is identical to the unscoped form.
|
||||
Ok(SocketAddr::V6(std::net::SocketAddrV6::new(
|
||||
ip,
|
||||
port,
|
||||
0,
|
||||
addr.sin6_scope_id,
|
||||
)))
|
||||
}
|
||||
family => Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
|
||||
Reference in New Issue
Block a user