mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-09-03 01:25:37 +00:00
feat(mobile): add embedded runtime and managed network updates (#2532)
* feat(mobile): add embedded iOS runtime API Add a thin panic-safe C ABI crate for embedding no-TUN instances on iOS. Expose lifecycle, status, JSON-RPC, string ownership, and error handling. Build device and simulator XCFramework static libraries on macOS. Add exact named-instance deletion to the iOS and Android wrappers. Cover wrapper lifecycle and the port-forward patch flow on host targets. * fix(gateway): recover TCP port-forward listeners Release an unusable TCP port-forward listener after an accept failure. Retry binding until the forward is cancelled. Keep the old listener released while rebinding so mobile sockets can recover. Expose opt-in iOS diagnostics for listener and connection events. Trace configuration removal and adapter shutdown. Add tests for recovery, release-before-rebind, and cancellation. * feat(web): persist incremental managed config patches Add a revision-CAS PATCH contract for managed configs while keeping the existing Full PUT path for compatibility and recovery. Apply Full and Patch mutations with their revision in one SQLite transaction. Reject ownership conflicts and invalidate revisions on alternate web-owned writes. Document limits, failure semantics, rollout order, and verification. Cover delta updates, conflicts, idempotency, and transaction rollback. * feat(web): apply managed config patches to live sessions Carry Patch fences and touched instance IDs into live sessions. Reconcile only those instances when the applied revision matches the Patch base. Fall back to Full reconciliation for gaps and restarts. Invalidate the applied revision around every direct runtime mutation. Fence revision advancement with the runtime cache epoch so stale reconcile rounds cannot overwrite a newer invalidation. Require deletion responses to confirm each requested instance before advancing the revision. Raise the managed PUT and PATCH body limit to 32 MiB and return typed conflicts for publisher recovery. * fix(core): retry transient accepted TCP errors Keep TCP tunnel listeners alive when an accepted socket fails during upgrade with a retryable connection-state error. Share the retryable I/O classifier with the socket listener. Cover a rejected connection followed by success and propagation of permanent errors. * feat(core): add internal Peer Relay edge projection Derive the local advertised OSPF row from physical adjacency and transport-authenticated credential relay coverage. Keep full local adjacency only in the temporary SPF snapshot so direct destinations retain a fallback route. Leave Peer Relay disabled at the public configuration seam. A follow-up change can expose the preference without coupling route projection to credential reauthorization. feat(config): expose Peer Relay routing preference Add prefer_peer_relay to public protobuf, TOML, management patch, and hosted runtime surfaces. Read the preference from live peer context so runtime config updates take effect. Refresh authenticated peer metadata when the option is enabled. Cover dynamic enable and disable in a five-node, dual-admin credential topology, including forwarded relay coverage and local fallback.
This commit is contained in:
@@ -153,6 +153,7 @@ pub fn network_config_from_toml(config: &TomlConfig) -> NetworkConfig {
|
||||
result.disable_udp_hole_punching = Some(flags.disable_udp_hole_punching);
|
||||
result.disable_upnp = Some(flags.disable_upnp);
|
||||
result.disable_relay_data = Some(flags.disable_relay_data);
|
||||
result.prefer_peer_relay = Some(flags.prefer_peer_relay);
|
||||
result.enable_udp_broadcast_relay = Some(flags.enable_udp_broadcast_relay);
|
||||
result.disable_sym_hole_punching = Some(flags.disable_sym_hole_punching);
|
||||
result.enable_magic_dns = Some(flags.accept_dns);
|
||||
|
||||
@@ -473,6 +473,10 @@ impl NetworkConfigExt for NetworkConfig {
|
||||
flags.disable_relay_data = disable_relay_data;
|
||||
}
|
||||
|
||||
if let Some(prefer_peer_relay) = self.prefer_peer_relay {
|
||||
flags.prefer_peer_relay = prefer_peer_relay;
|
||||
}
|
||||
|
||||
if let Some(enable_udp_broadcast_relay) = self.enable_udp_broadcast_relay {
|
||||
flags.enable_udp_broadcast_relay = enable_udp_broadcast_relay;
|
||||
}
|
||||
@@ -672,6 +676,7 @@ impl NetworkConfigExt for NetworkConfig {
|
||||
result.disable_udp_hole_punching = Some(flags.disable_udp_hole_punching);
|
||||
result.disable_upnp = Some(flags.disable_upnp);
|
||||
result.disable_relay_data = Some(flags.disable_relay_data);
|
||||
result.prefer_peer_relay = Some(flags.prefer_peer_relay);
|
||||
result.enable_udp_broadcast_relay = Some(flags.enable_udp_broadcast_relay);
|
||||
result.disable_sym_hole_punching = Some(flags.disable_sym_hole_punching);
|
||||
result.enable_magic_dns = Some(flags.accept_dns);
|
||||
@@ -775,6 +780,24 @@ mod tests {
|
||||
assert_eq!(output.managed_credentials[0].reusable, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn peer_relay_preference_round_trips_independently() {
|
||||
let input = NetworkConfig {
|
||||
disable_relay_data: Some(false),
|
||||
prefer_peer_relay: Some(true),
|
||||
..standalone_config()
|
||||
};
|
||||
|
||||
let config = input.gen_config().unwrap();
|
||||
let flags = config.get_flags();
|
||||
assert!(!flags.disable_relay_data);
|
||||
assert!(flags.prefer_peer_relay);
|
||||
|
||||
let output = NetworkConfig::new_from_config(&config).unwrap();
|
||||
assert_eq!(output.disable_relay_data, Some(false));
|
||||
assert_eq!(output.prefer_peer_relay, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_enabled_vpn_portal_config_reports_migration_error() {
|
||||
let error = NetworkConfig {
|
||||
|
||||
@@ -69,6 +69,7 @@ pub fn gen_default_flags() -> Flags {
|
||||
instance_recv_bps_limit: u64::MAX,
|
||||
disable_upnp: false,
|
||||
disable_relay_data: false,
|
||||
prefer_peer_relay: false,
|
||||
enable_udp_broadcast_relay: false,
|
||||
socket_mark: None,
|
||||
}
|
||||
@@ -162,6 +163,7 @@ define_flags_diff! {
|
||||
need_p2p,
|
||||
disable_upnp,
|
||||
disable_relay_data,
|
||||
prefer_peer_relay,
|
||||
enable_udp_broadcast_relay,
|
||||
socket_mark,
|
||||
],
|
||||
@@ -1488,6 +1490,7 @@ socket_mark = 66
|
||||
flags.bind_device = false;
|
||||
flags.enable_ipv6 = false;
|
||||
flags.relay_network_whitelist = "".to_string();
|
||||
flags.prefer_peer_relay = true;
|
||||
flags.mtu = 0;
|
||||
flags.foreign_relay_bps_limit = u64::MAX - 1;
|
||||
flags.instance_recv_bps_limit = u64::MAX - 2;
|
||||
@@ -1521,6 +1524,7 @@ socket_mark = 66
|
||||
assert!(!reloaded_flags.bind_device);
|
||||
assert!(!reloaded_flags.enable_ipv6);
|
||||
assert_eq!(reloaded_flags.relay_network_whitelist, "");
|
||||
assert!(reloaded_flags.prefer_peer_relay);
|
||||
assert_eq!(reloaded_flags.mtu, 0);
|
||||
assert_eq!(reloaded_flags.foreign_relay_bps_limit, u64::MAX - 1);
|
||||
assert_eq!(reloaded_flags.instance_recv_bps_limit, u64::MAX - 2);
|
||||
|
||||
@@ -16,6 +16,7 @@ use crate::{
|
||||
tcp::{
|
||||
TcpBindOptions, TcpListenOptions, TcpSocketListener, TcpSocketPurpose,
|
||||
VirtualTcpListenerFactory, VirtualTcpSocket, VirtualTcpSocketFactory,
|
||||
is_retryable_tcp_io_error,
|
||||
},
|
||||
udp::{
|
||||
UdpBindOptions, UdpSession, UdpSessionAcceptKind, UdpSessionListenRequest,
|
||||
@@ -165,9 +166,20 @@ where
|
||||
}
|
||||
|
||||
async fn accept(&mut self) -> anyhow::Result<Self::Accepted> {
|
||||
let local_url = self.inner.local_url();
|
||||
let socket = self.inner.accept().await?;
|
||||
Ok(upgrade_accepted_tcp_with_local_url(socket, local_url)?)
|
||||
loop {
|
||||
let local_url = self.inner.local_url();
|
||||
let socket = self.inner.accept().await?;
|
||||
match upgrade_accepted_tcp_with_local_url(socket, local_url) {
|
||||
Ok(tunnel) => return Ok(tunnel),
|
||||
Err(error) if is_retryable_accepted_tcp_error(&error) => {
|
||||
tracing::warn!(
|
||||
?error,
|
||||
"accepted tcp connection failed with retryable error"
|
||||
);
|
||||
}
|
||||
Err(error) => return Err(error.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn local_url(&self) -> Url {
|
||||
@@ -406,6 +418,14 @@ where
|
||||
TcpTunnelUpgrader::new(info).upgrade(socket)
|
||||
}
|
||||
|
||||
fn is_retryable_accepted_tcp_error(error: &TunnelError) -> bool {
|
||||
matches!(
|
||||
error,
|
||||
TunnelError::IOError(error)
|
||||
if is_retryable_tcp_io_error(error)
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn upgrade_accepted_byte_stream<S>(
|
||||
socket: S,
|
||||
local_url: Url,
|
||||
@@ -493,8 +513,10 @@ fn socket_url(scheme: &str, addr: SocketAddr) -> Url {
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
io,
|
||||
pin::Pin,
|
||||
sync::Mutex,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
@@ -503,6 +525,7 @@ pub(crate) mod tests {
|
||||
|
||||
use crate::{
|
||||
packet::ZCPacket,
|
||||
socket::tcp::VirtualTcpListener,
|
||||
socket::udp::{UdpSessionKind, VirtualUdpSocket},
|
||||
};
|
||||
|
||||
@@ -527,6 +550,7 @@ pub(crate) mod tests {
|
||||
stream: DuplexStream,
|
||||
local_addr: SocketAddr,
|
||||
peer_addr: SocketAddr,
|
||||
peer_addr_error: Option<io::ErrorKind>,
|
||||
transport_label: Option<&'static str>,
|
||||
}
|
||||
|
||||
@@ -545,6 +569,7 @@ pub(crate) mod tests {
|
||||
stream,
|
||||
local_addr,
|
||||
peer_addr,
|
||||
peer_addr_error: None,
|
||||
transport_label: None,
|
||||
}
|
||||
}
|
||||
@@ -553,6 +578,11 @@ pub(crate) mod tests {
|
||||
self.transport_label = Some(transport_label);
|
||||
self
|
||||
}
|
||||
|
||||
fn with_peer_addr_error(mut self, kind: io::ErrorKind) -> Self {
|
||||
self.peer_addr_error = Some(kind);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncRead for MockTcpSocket {
|
||||
@@ -589,6 +619,9 @@ pub(crate) mod tests {
|
||||
}
|
||||
|
||||
fn peer_addr(&self) -> io::Result<SocketAddr> {
|
||||
if let Some(kind) = self.peer_addr_error {
|
||||
return Err(io::Error::new(kind, "mock peer address failure"));
|
||||
}
|
||||
Ok(self.peer_addr)
|
||||
}
|
||||
|
||||
@@ -597,6 +630,56 @@ pub(crate) mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
struct MockTcpListener {
|
||||
local_addr: SocketAddr,
|
||||
accepts: Mutex<VecDeque<io::Result<(MockTcpSocket, SocketAddr)>>>,
|
||||
}
|
||||
|
||||
impl MockTcpListener {
|
||||
fn new(
|
||||
local_addr: SocketAddr,
|
||||
accepts: Vec<io::Result<(MockTcpSocket, SocketAddr)>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
local_addr,
|
||||
accepts: Mutex::new(accepts.into_iter().collect()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl VirtualTcpListener for MockTcpListener {
|
||||
type Socket = MockTcpSocket;
|
||||
|
||||
fn local_addr(&self) -> io::Result<SocketAddr> {
|
||||
Ok(self.local_addr)
|
||||
}
|
||||
|
||||
async fn accept(&self) -> io::Result<(Self::Socket, SocketAddr)> {
|
||||
let result = { self.accepts.lock().unwrap().pop_front() };
|
||||
match result {
|
||||
Some(result) => result,
|
||||
None => std::future::pending().await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MockTcpListenerFactory {
|
||||
listener: Arc<MockTcpListener>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl VirtualTcpListenerFactory for MockTcpListenerFactory {
|
||||
type Listener = MockTcpListener;
|
||||
|
||||
async fn bind_tcp(
|
||||
&self,
|
||||
_options: TcpListenOptions,
|
||||
) -> anyhow::Result<Arc<Self::Listener>> {
|
||||
Ok(self.listener.clone())
|
||||
}
|
||||
}
|
||||
|
||||
struct MockUdpSocket {
|
||||
local_addr: SocketAddr,
|
||||
}
|
||||
@@ -686,6 +769,57 @@ pub(crate) mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tcp_tunnel_listener_rejects_transient_peer_addr_error_and_continues() {
|
||||
let local_addr: SocketAddr = "127.0.0.1:11013".parse().unwrap();
|
||||
let rejected_peer_addr: SocketAddr = "127.0.0.1:21013".parse().unwrap();
|
||||
let accepted_peer_addr: SocketAddr = "127.0.0.1:21014".parse().unwrap();
|
||||
let listener = Arc::new(MockTcpListener::new(
|
||||
local_addr,
|
||||
vec![
|
||||
Ok((
|
||||
MockTcpSocket::new(local_addr, rejected_peer_addr)
|
||||
.with_peer_addr_error(io::ErrorKind::NotConnected),
|
||||
rejected_peer_addr,
|
||||
)),
|
||||
Ok((
|
||||
MockTcpSocket::new(local_addr, accepted_peer_addr),
|
||||
accepted_peer_addr,
|
||||
)),
|
||||
],
|
||||
));
|
||||
let factory = Arc::new(MockTcpListenerFactory { listener });
|
||||
let mut tunnel_listener = TcpTunnelListener::new(local_addr, factory);
|
||||
|
||||
tunnel_listener.listen().await.unwrap();
|
||||
let tunnel = tunnel_listener.accept().await.unwrap();
|
||||
let info = tunnel.info().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
info.remote_addr.unwrap().url,
|
||||
format!("tcp://{accepted_peer_addr}")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tcp_tunnel_listener_propagates_non_retryable_peer_addr_error() {
|
||||
let local_addr: SocketAddr = "127.0.0.1:11013".parse().unwrap();
|
||||
let peer_addr: SocketAddr = "127.0.0.1:21013".parse().unwrap();
|
||||
let listener = Arc::new(MockTcpListener::new(
|
||||
local_addr,
|
||||
vec![Ok((
|
||||
MockTcpSocket::new(local_addr, peer_addr)
|
||||
.with_peer_addr_error(io::ErrorKind::Other),
|
||||
peer_addr,
|
||||
))],
|
||||
));
|
||||
let factory = Arc::new(MockTcpListenerFactory { listener });
|
||||
let mut tunnel_listener = TcpTunnelListener::new(local_addr, factory);
|
||||
|
||||
tunnel_listener.listen().await.unwrap();
|
||||
assert!(tunnel_listener.accept().await.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn faketcp_upgrader_rejects_socket_without_host_transport_label() {
|
||||
let local_addr: SocketAddr = "192.0.2.1:10000".parse().unwrap();
|
||||
|
||||
@@ -41,6 +41,7 @@ use crate::{
|
||||
};
|
||||
|
||||
const MAX_UDP_PAYLOAD_SIZE: usize = 65_507;
|
||||
const TCP_PORT_FORWARD_REBIND_DELAY: Duration = Duration::from_millis(100);
|
||||
// A remote flow owns roughly 320 KiB of smoltcp and response buffers.
|
||||
// Keep the per-instance worst case near 80 MiB instead of allowing a
|
||||
// source-port flood to grow the WASM heap without bound.
|
||||
@@ -193,13 +194,17 @@ where
|
||||
}
|
||||
|
||||
self.cancel_tokens.retain(|current, _| {
|
||||
cfgs.iter().any(|next| {
|
||||
let keep = cfgs.iter().any(|next| {
|
||||
if next.dst_addr.ip().is_unspecified() {
|
||||
current.bind_addr == next.bind_addr && current.proto == next.proto
|
||||
} else {
|
||||
current == next
|
||||
}
|
||||
})
|
||||
});
|
||||
if !keep {
|
||||
tracing::info!(?current, "port-forward removed by runtime config reload");
|
||||
}
|
||||
keep
|
||||
});
|
||||
self.udp_clients
|
||||
.retain(|key, _| self.cancel_tokens.contains_key(&key.forward));
|
||||
@@ -238,7 +243,9 @@ where
|
||||
.bind
|
||||
.clone()
|
||||
.with_context(self.socket_context.clone());
|
||||
let listener = self.host.bind_tcp(options.with_bind(bind)).await?;
|
||||
let options = options.with_bind(bind);
|
||||
let listener = self.host.bind_tcp(options.clone()).await?;
|
||||
tracing::info!(?bind_addr, ?dst_addr, "TCP port-forward listener bound");
|
||||
let cancel = CancellationToken::new();
|
||||
self.cancel_tokens
|
||||
.insert(cfg.clone(), cancel.clone().drop_guard());
|
||||
@@ -249,21 +256,28 @@ where
|
||||
Arc::downgrade(&connections),
|
||||
"TCP port-forward connections",
|
||||
));
|
||||
let host = self.host.clone();
|
||||
self.tasks.lock().unwrap().spawn(async move {
|
||||
let mut listener = Some(listener);
|
||||
loop {
|
||||
let (incoming, source_addr) = select! {
|
||||
biased;
|
||||
_ = cancel.cancelled() => break,
|
||||
result = listener.accept() => match result {
|
||||
Ok(accepted) => accepted,
|
||||
Err(error) => {
|
||||
tracing::error!(?error, ?bind_addr, "port-forward accept failed");
|
||||
continue;
|
||||
}
|
||||
},
|
||||
let Some((incoming, source_addr)) =
|
||||
accept_tcp_port_forward(&host, &options, &mut listener, &cancel).await
|
||||
else {
|
||||
break;
|
||||
};
|
||||
tracing::info!(
|
||||
?bind_addr,
|
||||
?source_addr,
|
||||
?dst_addr,
|
||||
"port-forward accepted local connection"
|
||||
);
|
||||
let data_plane = data_plane.clone();
|
||||
connections.lock().unwrap().spawn(async move {
|
||||
tracing::info!(
|
||||
?source_addr,
|
||||
?dst_addr,
|
||||
"port-forward data-plane connect started"
|
||||
);
|
||||
let options = DataPlaneTcpConnectOptions::gateway(
|
||||
Duration::from_secs(10),
|
||||
TcpSocketPurpose::PortForward,
|
||||
@@ -276,9 +290,15 @@ where
|
||||
return;
|
||||
}
|
||||
};
|
||||
tracing::info!(?source_addr, ?dst_addr, "port-forward data-plane connected");
|
||||
copy_tcp(incoming, outgoing, dst_addr).await;
|
||||
});
|
||||
}
|
||||
tracing::info!(
|
||||
?bind_addr,
|
||||
?dst_addr,
|
||||
"TCP port-forward listener task stopped"
|
||||
);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
@@ -430,6 +450,10 @@ where
|
||||
|
||||
async fn stop_inner(&self) {
|
||||
self.started.store(false, Ordering::Release);
|
||||
tracing::info!(
|
||||
forward_count = self.cancel_tokens.len(),
|
||||
"port-forward adapter stopping"
|
||||
);
|
||||
self.cancel_tokens.clear();
|
||||
self.udp_response_tasks.clear();
|
||||
self.udp_clients.clear();
|
||||
@@ -447,6 +471,69 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
async fn accept_tcp_port_forward<H>(
|
||||
host: &Arc<H>,
|
||||
options: &TcpListenOptions,
|
||||
listener: &mut Option<Arc<H::Listener>>,
|
||||
cancel: &CancellationToken,
|
||||
) -> Option<(<H::Listener as VirtualTcpListener>::Socket, SocketAddr)>
|
||||
where
|
||||
H: VirtualTcpListenerFactory,
|
||||
{
|
||||
loop {
|
||||
let current = listener
|
||||
.take()
|
||||
.expect("TCP port-forward listener must be bound before accepting");
|
||||
let accepted = select! {
|
||||
biased;
|
||||
_ = cancel.cancelled() => {
|
||||
tracing::info!(?options, "port-forward listener cancelled while accepting");
|
||||
return None;
|
||||
},
|
||||
result = current.accept() => result,
|
||||
};
|
||||
match accepted {
|
||||
Ok(accepted) => {
|
||||
listener.replace(current);
|
||||
return Some(accepted);
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!(?error, ?options, "port-forward accept failed; rebinding");
|
||||
}
|
||||
}
|
||||
drop(current);
|
||||
|
||||
loop {
|
||||
select! {
|
||||
biased;
|
||||
_ = cancel.cancelled() => {
|
||||
tracing::info!(?options, "port-forward listener cancelled during rebind delay");
|
||||
return None;
|
||||
},
|
||||
_ = crate::foundation::time::sleep(TCP_PORT_FORWARD_REBIND_DELAY) => {}
|
||||
}
|
||||
let rebound = select! {
|
||||
biased;
|
||||
_ = cancel.cancelled() => {
|
||||
tracing::info!(?options, "port-forward listener cancelled while rebinding");
|
||||
return None;
|
||||
},
|
||||
result = host.bind_tcp(options.clone()) => result,
|
||||
};
|
||||
match rebound {
|
||||
Ok(rebound) => {
|
||||
tracing::info!(?options, "port-forward listener rebound");
|
||||
listener.replace(rebound);
|
||||
break;
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!(?error, ?options, "port-forward listener rebind failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn reserve_udp_client_slot<H>(
|
||||
cancel: &CancellationToken,
|
||||
admission: &Arc<Mutex<()>>,
|
||||
@@ -536,8 +623,105 @@ where
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{io, sync::atomic::AtomicUsize};
|
||||
|
||||
use super::*;
|
||||
use crate::host::testkit::{TestHost, TestUdpSocket};
|
||||
use crate::host::testkit::{TestHost, TestTcpSocket, TestUdpSocket};
|
||||
|
||||
struct RecoveringTcpListener {
|
||||
accept_error: bool,
|
||||
active: Arc<AtomicUsize>,
|
||||
address: SocketAddr,
|
||||
}
|
||||
|
||||
impl Drop for RecoveringTcpListener {
|
||||
fn drop(&mut self) {
|
||||
self.active.fetch_sub(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl VirtualTcpListener for RecoveringTcpListener {
|
||||
type Socket = TestTcpSocket;
|
||||
|
||||
fn local_addr(&self) -> io::Result<SocketAddr> {
|
||||
Ok(self.address)
|
||||
}
|
||||
|
||||
async fn accept(&self) -> io::Result<(Self::Socket, SocketAddr)> {
|
||||
if self.accept_error {
|
||||
return Err(io::Error::other("listener is no longer usable"));
|
||||
}
|
||||
let (socket, _) = tokio::io::duplex(64);
|
||||
Ok((TestTcpSocket(socket), "127.0.0.1:40000".parse().unwrap()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct RecoveringTcpHost {
|
||||
binds: AtomicUsize,
|
||||
active: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl VirtualTcpListenerFactory for RecoveringTcpHost {
|
||||
type Listener = RecoveringTcpListener;
|
||||
|
||||
async fn bind_tcp(&self, options: TcpListenOptions) -> anyhow::Result<Arc<Self::Listener>> {
|
||||
let bind_index = self.binds.fetch_add(1, Ordering::Relaxed);
|
||||
if bind_index > 0 && self.active.load(Ordering::Relaxed) != 0 {
|
||||
anyhow::bail!("previous listener is still active");
|
||||
}
|
||||
self.active.fetch_add(1, Ordering::Relaxed);
|
||||
Ok(Arc::new(RecoveringTcpListener {
|
||||
accept_error: bind_index == 0,
|
||||
active: self.active.clone(),
|
||||
address: options.bind.local_addr.unwrap(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tcp_listener_rebinds_after_accept_error() {
|
||||
let host = Arc::new(RecoveringTcpHost::default());
|
||||
let options = TcpListenOptions::port_forward("127.0.0.1:5202".parse().unwrap());
|
||||
let mut listener = Some(host.bind_tcp(options.clone()).await.unwrap());
|
||||
|
||||
let accepted = tokio::time::timeout(
|
||||
Duration::from_secs(1),
|
||||
accept_tcp_port_forward(&host, &options, &mut listener, &CancellationToken::new()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(accepted.is_some());
|
||||
assert_eq!(host.binds.load(Ordering::Relaxed), 2);
|
||||
assert_eq!(host.active.load(Ordering::Relaxed), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tcp_listener_rebind_stops_when_cancelled() {
|
||||
let host = Arc::new(RecoveringTcpHost::default());
|
||||
let options = TcpListenOptions::port_forward("127.0.0.1:5202".parse().unwrap());
|
||||
let mut listener = Some(host.bind_tcp(options.clone()).await.unwrap());
|
||||
let cancel = CancellationToken::new();
|
||||
let cancel_after_error = cancel.clone();
|
||||
tokio::spawn(async move {
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
cancel_after_error.cancel();
|
||||
});
|
||||
|
||||
let accepted = tokio::time::timeout(
|
||||
Duration::from_secs(1),
|
||||
accept_tcp_port_forward(&host, &options, &mut listener, &cancel),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(accepted.is_none());
|
||||
assert_eq!(host.binds.load(Ordering::Relaxed), 1);
|
||||
assert_eq!(host.active.load(Ordering::Relaxed), 0);
|
||||
}
|
||||
|
||||
fn udp_client_key(port: u16) -> UdpClientKey {
|
||||
UdpClientKey {
|
||||
|
||||
@@ -96,6 +96,11 @@ where
|
||||
flags.disable_relay_data = disable_relay_data;
|
||||
candidate.set_flags(flags);
|
||||
}
|
||||
if let Some(prefer_peer_relay) = patch.prefer_peer_relay {
|
||||
let mut flags = candidate.get_flags();
|
||||
flags.prefer_peer_relay = prefer_peer_relay;
|
||||
candidate.set_flags(flags);
|
||||
}
|
||||
if let Some(enabled) = patch.ipv6_public_addr_provider {
|
||||
candidate.set_ipv6_public_addr_provider(enabled);
|
||||
provider_config_changed = true;
|
||||
|
||||
@@ -881,11 +881,12 @@ impl Default for RouteConnInfo {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct InterfacePeerSnapshot {
|
||||
generation: u64,
|
||||
peers: BTreeSet<PeerId>,
|
||||
identity_types: BTreeMap<PeerId, Option<PeerIdentityType>>,
|
||||
public_keys: BTreeMap<PeerId, Option<Vec<u8>>>,
|
||||
}
|
||||
|
||||
// constructed with all infos synced from all peers.
|
||||
@@ -904,7 +905,7 @@ struct SyncedRouteInfo {
|
||||
|
||||
// Aggregated trusted credential pubkeys from all admin nodes
|
||||
// Maps pubkey bytes -> TrustedCredentialPubkey
|
||||
trusted_credential_pubkeys: DashMap<Vec<u8>, TrustedCredentialPubkey>,
|
||||
trusted_credential_pubkeys: RwLock<HashMap<Vec<u8>, TrustedCredentialPubkey>>,
|
||||
// Tracks the currently accepted peer for non-reusable credentials.
|
||||
// Maps credential pubkey bytes -> peer_id.
|
||||
non_reusable_credential_owners: DashMap<Vec<u8>, PeerId>,
|
||||
@@ -1060,17 +1061,9 @@ impl SyncedRouteInfo {
|
||||
&self,
|
||||
all_trusted: &HashMap<Vec<u8>, TrustedCredentialPubkey>,
|
||||
) -> HashSet<Vec<u8>> {
|
||||
let prev_trusted = self
|
||||
.trusted_credential_pubkeys
|
||||
.iter()
|
||||
.map(|entry| entry.key().clone())
|
||||
.collect();
|
||||
|
||||
self.trusted_credential_pubkeys.clear();
|
||||
for (pubkey, credential) in all_trusted {
|
||||
self.trusted_credential_pubkeys
|
||||
.insert(pubkey.clone(), credential.clone());
|
||||
}
|
||||
let mut trusted = self.trusted_credential_pubkeys.write();
|
||||
let prev_trusted = trusted.keys().cloned().collect();
|
||||
*trusted = all_trusted.clone();
|
||||
|
||||
prev_trusted
|
||||
}
|
||||
@@ -1881,8 +1874,9 @@ impl SyncedRouteInfo {
|
||||
return None;
|
||||
}
|
||||
self.trusted_credential_pubkeys
|
||||
.read()
|
||||
.get(peer_pubkey)
|
||||
.map(|r| r.value().clone())
|
||||
.cloned()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2374,6 +2368,7 @@ struct PeerRouteServiceImpl {
|
||||
cached_interface_peer_snapshot: std::sync::Mutex<Arc<InterfacePeerSnapshot>>,
|
||||
interface_peers_generation: AtomicU64,
|
||||
applied_interface_peers_generation: AtomicU64,
|
||||
applied_interface_peers: std::sync::Mutex<BTreeSet<PeerId>>,
|
||||
|
||||
last_update_my_foreign_network: AtomicCell<Option<Instant>>,
|
||||
|
||||
@@ -2432,7 +2427,7 @@ impl PeerRouteServiceImpl {
|
||||
group_trust_map: DashMap::new(),
|
||||
group_trust_map_cache: DashMap::new(),
|
||||
group_trust_update_lock: parking_lot::Mutex::new(()),
|
||||
trusted_credential_pubkeys: DashMap::new(),
|
||||
trusted_credential_pubkeys: RwLock::new(HashMap::new()),
|
||||
non_reusable_credential_owners: DashMap::new(),
|
||||
suppressed_non_reusable_credential_peers: DashMap::new(),
|
||||
version: AtomicVersion::new(),
|
||||
@@ -2446,6 +2441,7 @@ impl PeerRouteServiceImpl {
|
||||
)),
|
||||
interface_peers_generation: AtomicU64::new(1),
|
||||
applied_interface_peers_generation: AtomicU64::new(0),
|
||||
applied_interface_peers: std::sync::Mutex::new(BTreeSet::new()),
|
||||
|
||||
last_update_my_foreign_network: AtomicCell::new(None),
|
||||
|
||||
@@ -2462,6 +2458,10 @@ impl PeerRouteServiceImpl {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn peer_relay_projection_enabled(&self) -> bool {
|
||||
self.context.flags().prefer_peer_relay && !self.is_credential_node()
|
||||
}
|
||||
|
||||
fn set_public_ipv6_service(&self, service: Weak<PublicIpv6Service>) {
|
||||
*self.public_ipv6_service.lock().unwrap() = service;
|
||||
}
|
||||
@@ -2532,14 +2532,19 @@ impl PeerRouteServiceImpl {
|
||||
|
||||
let peers: BTreeSet<_> = interface.list_peers().await.into_iter().collect();
|
||||
let mut identity_types = BTreeMap::new();
|
||||
let mut public_keys = BTreeMap::new();
|
||||
for peer_id in peers.iter().copied() {
|
||||
identity_types.insert(peer_id, interface.get_peer_identity_type(peer_id).await);
|
||||
if self.peer_relay_projection_enabled() {
|
||||
public_keys.insert(peer_id, interface.get_peer_public_key(peer_id).await);
|
||||
}
|
||||
}
|
||||
|
||||
InterfacePeerSnapshot {
|
||||
generation: 0,
|
||||
peers,
|
||||
identity_types,
|
||||
public_keys,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2564,11 +2569,6 @@ impl PeerRouteServiceImpl {
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_peers_from_interface_snapshot(&self) -> (u64, BTreeSet<PeerId>) {
|
||||
let snapshot = self.interface_peer_snapshot().await;
|
||||
(snapshot.generation, snapshot.peers.clone())
|
||||
}
|
||||
|
||||
async fn get_peer_identity_type_from_interface(
|
||||
&self,
|
||||
peer_id: PeerId,
|
||||
@@ -2588,6 +2588,11 @@ impl PeerRouteServiceImpl {
|
||||
}
|
||||
|
||||
async fn get_peer_public_key_from_interface(&self, peer_id: PeerId) -> Option<Vec<u8>> {
|
||||
let snapshot = self.interface_peer_snapshot().await;
|
||||
if let Some(public_key) = snapshot.public_keys.get(&peer_id) {
|
||||
return public_key.clone();
|
||||
}
|
||||
|
||||
self.interface
|
||||
.lock()
|
||||
.await
|
||||
@@ -2597,6 +2602,105 @@ impl PeerRouteServiceImpl {
|
||||
.await
|
||||
}
|
||||
|
||||
fn derive_advertised_connected_peers(
|
||||
&self,
|
||||
snapshot: &InterfacePeerSnapshot,
|
||||
) -> BTreeSet<PeerId> {
|
||||
if !self.peer_relay_projection_enabled() {
|
||||
return snapshot.peers.clone();
|
||||
}
|
||||
|
||||
let is_credential_peer = |peer_id: PeerId| {
|
||||
matches!(
|
||||
snapshot.identity_types.get(&peer_id),
|
||||
Some(Some(PeerIdentityType::Credential))
|
||||
)
|
||||
};
|
||||
|
||||
let eligible_relays: BTreeSet<_> = {
|
||||
let peer_infos = self.synced_route_info.peer_infos.read();
|
||||
snapshot
|
||||
.peers
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|peer_id| is_credential_peer(*peer_id))
|
||||
.filter(|peer_id| {
|
||||
!self
|
||||
.synced_route_info
|
||||
.suppressed_non_reusable_credential_peers
|
||||
.contains_key(peer_id)
|
||||
})
|
||||
.filter(|peer_id| {
|
||||
let Some(Some(public_key)) = snapshot.public_keys.get(peer_id) else {
|
||||
return false;
|
||||
};
|
||||
if peer_infos
|
||||
.get(peer_id)
|
||||
.and_then(|info| info.feature_flag)
|
||||
.is_some_and(|flags| flags.avoid_relay_data)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
self.synced_route_info
|
||||
.get_credential_info_by_pubkey(public_key)
|
||||
.is_some_and(|credential| credential.allow_relay)
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
if eligible_relays.is_empty() {
|
||||
return snapshot.peers.clone();
|
||||
}
|
||||
|
||||
let conn_map = self.synced_route_info.conn_map.read();
|
||||
let mut covered_targets = BTreeSet::new();
|
||||
for relay_peer_id in &eligible_relays {
|
||||
let Some(relay_conn_info) = conn_map.get(relay_peer_id) else {
|
||||
continue;
|
||||
};
|
||||
for target_peer_id in &relay_conn_info.connected_peers {
|
||||
if snapshot.peers.contains(target_peer_id)
|
||||
&& is_credential_peer(*target_peer_id)
|
||||
&& !eligible_relays.contains(target_peer_id)
|
||||
&& !self
|
||||
.synced_route_info
|
||||
.suppressed_non_reusable_credential_peers
|
||||
.contains_key(target_peer_id)
|
||||
{
|
||||
covered_targets.insert(*target_peer_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
snapshot
|
||||
.peers
|
||||
.difference(&covered_targets)
|
||||
.copied()
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn reconcile_my_conn_info(
|
||||
&self,
|
||||
snapshot: &InterfacePeerSnapshot,
|
||||
interface_snapshot_changed: bool,
|
||||
) -> bool {
|
||||
let advertised_peers = self.derive_advertised_connected_peers(snapshot);
|
||||
if self
|
||||
.synced_route_info
|
||||
.update_my_conn_info(self.my_peer_id, advertised_peers)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if interface_snapshot_changed && self.peer_relay_projection_enabled() {
|
||||
self.synced_route_info.version.inc();
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn update_my_peer_info(&self) -> bool {
|
||||
self.synced_route_info.update_my_peer_info(
|
||||
self.my_peer_id,
|
||||
@@ -2620,17 +2724,28 @@ impl PeerRouteServiceImpl {
|
||||
.as_ref()
|
||||
.map(|x| x.need_periodic_requery_peers())
|
||||
.unwrap_or(false);
|
||||
if !need_periodic_requery {
|
||||
return false;
|
||||
let snapshot = self.cached_interface_peer_snapshot.lock().unwrap().clone();
|
||||
let need_peer_relay_metadata = self.peer_relay_projection_enabled()
|
||||
&& snapshot.public_keys.len() != snapshot.peers.len();
|
||||
if !need_periodic_requery && !need_peer_relay_metadata {
|
||||
return self.reconcile_my_conn_info(&snapshot, false);
|
||||
}
|
||||
|
||||
self.mark_interface_peers_dirty();
|
||||
}
|
||||
|
||||
let (generation, connected_peers) = self.list_peers_from_interface_snapshot().await;
|
||||
let updated = self
|
||||
.synced_route_info
|
||||
.update_my_conn_info(self.my_peer_id, connected_peers);
|
||||
let snapshot = self.interface_peer_snapshot().await;
|
||||
let generation = snapshot.generation;
|
||||
let interface_snapshot_changed = {
|
||||
let mut applied_peers = self.applied_interface_peers.lock().unwrap();
|
||||
if *applied_peers == snapshot.peers {
|
||||
false
|
||||
} else {
|
||||
*applied_peers = snapshot.peers.clone();
|
||||
true
|
||||
}
|
||||
};
|
||||
let updated = self.reconcile_my_conn_info(&snapshot, interface_snapshot_changed);
|
||||
self.applied_interface_peers_generation
|
||||
.store(generation, Ordering::Release);
|
||||
updated
|
||||
@@ -2663,6 +2778,28 @@ impl PeerRouteServiceImpl {
|
||||
.update_my_foreign_network(self.my_peer_id, foreign_networks)
|
||||
}
|
||||
|
||||
fn local_route_snapshot(&self) -> OspfRouteSnapshot {
|
||||
let mut snapshot = self.synced_route_info.route_snapshot();
|
||||
if !self.peer_relay_projection_enabled() {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
let local_connected_peers = self
|
||||
.cached_interface_peer_snapshot
|
||||
.lock()
|
||||
.unwrap()
|
||||
.peers
|
||||
.clone();
|
||||
if let Some(self_row) = snapshot
|
||||
.conn_map
|
||||
.iter_mut()
|
||||
.find(|row| row.peer_id == self.my_peer_id)
|
||||
{
|
||||
self_row.connected_peers = local_connected_peers;
|
||||
}
|
||||
snapshot
|
||||
}
|
||||
|
||||
fn update_route_table(&self) {
|
||||
self.cost_calculator
|
||||
.write()
|
||||
@@ -2672,7 +2809,7 @@ impl PeerRouteServiceImpl {
|
||||
.begin_update();
|
||||
|
||||
let calc_locked = self.cost_calculator.read().unwrap();
|
||||
let route_snapshot = self.synced_route_info.route_snapshot();
|
||||
let route_snapshot = self.local_route_snapshot();
|
||||
|
||||
self.route_table.build_from_snapshot(
|
||||
self.my_peer_id,
|
||||
@@ -4538,11 +4675,34 @@ mod tests {
|
||||
get_peer_identity_type_calls: Arc<AtomicU32>,
|
||||
}
|
||||
|
||||
struct PeriodicRequeryInterface {
|
||||
peers: Vec<PeerId>,
|
||||
list_peers_calls: Arc<AtomicU32>,
|
||||
}
|
||||
|
||||
struct BlockingInterface {
|
||||
entered: Arc<Notify>,
|
||||
release: Arc<Notify>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct TogglePeerRelayContext {
|
||||
enabled: AtomicBool,
|
||||
}
|
||||
|
||||
impl PeerContext for TogglePeerRelayContext {
|
||||
fn network_identity(&self) -> CoreNetworkIdentity {
|
||||
CoreNetworkIdentity::default()
|
||||
}
|
||||
|
||||
fn flags(&self) -> crate::proto::common::FlagsInConfig {
|
||||
crate::proto::common::FlagsInConfig {
|
||||
prefer_peer_relay: self.enabled.load(Ordering::Relaxed),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl RouteInterface for BlockingInterface {
|
||||
async fn list_peers(&self) -> Vec<PeerId> {
|
||||
@@ -4623,15 +4783,127 @@ mod tests {
|
||||
.flatten()
|
||||
}
|
||||
|
||||
async fn get_peer_public_key(&self, peer_id: PeerId) -> Option<Vec<u8>> {
|
||||
Some(vec![peer_id as u8; 32])
|
||||
}
|
||||
|
||||
fn my_peer_id(&self) -> PeerId {
|
||||
self.my_peer_id
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl RouteInterface for PeriodicRequeryInterface {
|
||||
async fn list_peers(&self) -> Vec<PeerId> {
|
||||
self.list_peers_calls.fetch_add(1, Ordering::Relaxed);
|
||||
self.peers.clone()
|
||||
}
|
||||
|
||||
fn my_peer_id(&self) -> PeerId {
|
||||
1
|
||||
}
|
||||
|
||||
fn need_periodic_requery_peers(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
async fn get_peer_identity_type(&self, _peer_id: PeerId) -> Option<PeerIdentityType> {
|
||||
Some(PeerIdentityType::Admin)
|
||||
}
|
||||
|
||||
async fn get_peer_public_key(&self, peer_id: PeerId) -> Option<Vec<u8>> {
|
||||
Some(vec![peer_id as u8; 32])
|
||||
}
|
||||
}
|
||||
|
||||
fn test_service_impl(my_peer_id: PeerId) -> PeerRouteServiceImpl {
|
||||
PeerRouteServiceImpl::new(my_peer_id, Arc::new(NoopPeerContext::default()))
|
||||
}
|
||||
|
||||
fn test_peer_relay_service_impl(my_peer_id: PeerId) -> PeerRouteServiceImpl {
|
||||
let flags = crate::proto::common::FlagsInConfig {
|
||||
prefer_peer_relay: true,
|
||||
..Default::default()
|
||||
};
|
||||
PeerRouteServiceImpl::new(
|
||||
my_peer_id,
|
||||
Arc::new(NoopPeerContext::default().with_flags(flags)),
|
||||
)
|
||||
}
|
||||
|
||||
fn interface_peer_snapshot(
|
||||
peers: impl IntoIterator<Item = (PeerId, PeerIdentityType, Option<Vec<u8>>)>,
|
||||
) -> InterfacePeerSnapshot {
|
||||
let peers: Vec<_> = peers.into_iter().collect();
|
||||
InterfacePeerSnapshot {
|
||||
generation: 1,
|
||||
peers: peers.iter().map(|(peer_id, _, _)| *peer_id).collect(),
|
||||
identity_types: peers
|
||||
.iter()
|
||||
.map(|(peer_id, identity, _)| (*peer_id, Some(*identity)))
|
||||
.collect(),
|
||||
public_keys: peers
|
||||
.into_iter()
|
||||
.map(|(peer_id, _, public_key)| (peer_id, public_key))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn install_peer_info(
|
||||
service_impl: &PeerRouteServiceImpl,
|
||||
peer_id: PeerId,
|
||||
advertised_public_key: Vec<u8>,
|
||||
avoid_relay_data: bool,
|
||||
) {
|
||||
let feature_flag = crate::proto::common::PeerFeatureFlag {
|
||||
avoid_relay_data,
|
||||
..Default::default()
|
||||
};
|
||||
service_impl.synced_route_info.peer_infos.write().insert(
|
||||
peer_id,
|
||||
RoutePeerInfo {
|
||||
peer_id,
|
||||
version: 1,
|
||||
feature_flag: Some(feature_flag),
|
||||
noise_static_pubkey: advertised_public_key,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn install_credential_grant(
|
||||
service_impl: &PeerRouteServiceImpl,
|
||||
public_key: Vec<u8>,
|
||||
allow_relay: bool,
|
||||
) {
|
||||
service_impl
|
||||
.synced_route_info
|
||||
.trusted_credential_pubkeys
|
||||
.write()
|
||||
.insert(
|
||||
public_key,
|
||||
TrustedCredentialPubkey {
|
||||
allow_relay,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn install_conn_row(
|
||||
service_impl: &PeerRouteServiceImpl,
|
||||
peer_id: PeerId,
|
||||
connected_peers: impl IntoIterator<Item = PeerId>,
|
||||
) {
|
||||
service_impl.synced_route_info.conn_map.write().insert(
|
||||
peer_id,
|
||||
RouteConnInfo {
|
||||
connected_peers: connected_peers.into_iter().collect(),
|
||||
version: 1.into(),
|
||||
last_update: SystemTime::now(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async fn test_route_with_admin_peer(
|
||||
context: ArcPeerContext,
|
||||
) -> (Arc<PeerRoute>, Arc<PeerRpcManager>) {
|
||||
@@ -4676,6 +4948,307 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trusted_credential_replacement_is_atomic_for_readers() {
|
||||
const CREDENTIAL_COUNT: u32 = 16_384;
|
||||
const REPLACEMENT_COUNT: usize = 16;
|
||||
|
||||
let service_impl = test_service_impl(1);
|
||||
let credentials: HashMap<_, _> = (0..CREDENTIAL_COUNT)
|
||||
.map(|id| {
|
||||
(
|
||||
id.to_le_bytes().to_vec(),
|
||||
TrustedCredentialPubkey {
|
||||
allow_relay: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
service_impl
|
||||
.synced_route_info
|
||||
.replace_trusted_credential_pubkeys(&credentials);
|
||||
|
||||
let keys: Vec<_> = credentials.keys().cloned().collect();
|
||||
let start = std::sync::Barrier::new(2);
|
||||
|
||||
std::thread::scope(|scope| {
|
||||
let writer = scope.spawn(|| {
|
||||
start.wait();
|
||||
for _ in 0..REPLACEMENT_COUNT {
|
||||
service_impl
|
||||
.synced_route_info
|
||||
.replace_trusted_credential_pubkeys(&credentials);
|
||||
}
|
||||
});
|
||||
|
||||
start.wait();
|
||||
let mut snapshots_read = 0;
|
||||
while !writer.is_finished() {
|
||||
assert!(keys.iter().all(|key| {
|
||||
service_impl
|
||||
.synced_route_info
|
||||
.get_credential_info_by_pubkey(key)
|
||||
.is_some()
|
||||
}));
|
||||
snapshots_read += 1;
|
||||
}
|
||||
writer.join().unwrap();
|
||||
assert!(snapshots_read > 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn peer_relay_projection_suppresses_only_covered_credential_leaves() {
|
||||
let service_impl = test_peer_relay_service_impl(1);
|
||||
let relay_key = vec![2; 32];
|
||||
let leaf_a_key = vec![3; 32];
|
||||
let leaf_b_key = vec![4; 32];
|
||||
let snapshot = interface_peer_snapshot([
|
||||
(2, PeerIdentityType::Credential, Some(relay_key.clone())),
|
||||
(3, PeerIdentityType::Credential, Some(leaf_a_key.clone())),
|
||||
(4, PeerIdentityType::Credential, Some(leaf_b_key.clone())),
|
||||
(5, PeerIdentityType::Admin, None),
|
||||
]);
|
||||
|
||||
for peer_id in 1..=5 {
|
||||
install_peer_info(&service_impl, peer_id, vec![peer_id as u8; 32], false);
|
||||
}
|
||||
install_credential_grant(&service_impl, relay_key, true);
|
||||
install_credential_grant(&service_impl, leaf_a_key, false);
|
||||
install_credential_grant(&service_impl, leaf_b_key, false);
|
||||
install_conn_row(&service_impl, 2, [3, 4]);
|
||||
|
||||
assert!(service_impl.reconcile_my_conn_info(&snapshot, false));
|
||||
assert_eq!(
|
||||
service_impl
|
||||
.synced_route_info
|
||||
.get_connected_peers::<BTreeSet<_>>(1),
|
||||
Some(BTreeSet::from([2, 5]))
|
||||
);
|
||||
|
||||
*service_impl.cached_interface_peer_snapshot.lock().unwrap() = Arc::new(snapshot.clone());
|
||||
let local_snapshot = service_impl.local_route_snapshot();
|
||||
assert_eq!(
|
||||
local_snapshot
|
||||
.conn_map
|
||||
.iter()
|
||||
.find(|row| row.peer_id == 1)
|
||||
.unwrap()
|
||||
.connected_peers,
|
||||
snapshot.peers
|
||||
);
|
||||
|
||||
service_impl.update_route_table();
|
||||
assert_eq!(
|
||||
service_impl
|
||||
.route_table
|
||||
.get_next_hop(3)
|
||||
.unwrap()
|
||||
.next_hop_peer_id,
|
||||
3
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn peer_relay_projection_uses_authenticated_public_key() {
|
||||
let service_impl = test_peer_relay_service_impl(1);
|
||||
let authenticated_key = vec![2; 32];
|
||||
let forged_relay_key = vec![9; 32];
|
||||
let snapshot = interface_peer_snapshot([
|
||||
(
|
||||
2,
|
||||
PeerIdentityType::Credential,
|
||||
Some(authenticated_key.clone()),
|
||||
),
|
||||
(4, PeerIdentityType::Credential, Some(vec![4; 32])),
|
||||
]);
|
||||
install_peer_info(&service_impl, 2, forged_relay_key.clone(), false);
|
||||
install_credential_grant(&service_impl, authenticated_key, false);
|
||||
install_credential_grant(&service_impl, forged_relay_key, true);
|
||||
install_conn_row(&service_impl, 2, [4]);
|
||||
|
||||
assert_eq!(
|
||||
service_impl.derive_advertised_connected_peers(&snapshot),
|
||||
snapshot.peers
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn peer_relay_projection_is_disabled_by_default() {
|
||||
let service_impl = test_service_impl(1);
|
||||
let snapshot = interface_peer_snapshot([
|
||||
(2, PeerIdentityType::Credential, Some(vec![2; 32])),
|
||||
(4, PeerIdentityType::Credential, Some(vec![4; 32])),
|
||||
]);
|
||||
install_peer_info(&service_impl, 2, vec![2; 32], false);
|
||||
install_credential_grant(&service_impl, vec![2; 32], true);
|
||||
install_conn_row(&service_impl, 2, [4]);
|
||||
|
||||
assert_eq!(
|
||||
service_impl.derive_advertised_connected_peers(&snapshot),
|
||||
snapshot.peers
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn peer_relay_projection_restores_edges_after_last_coverage_disappears() {
|
||||
let service_impl = test_peer_relay_service_impl(1);
|
||||
let snapshot = interface_peer_snapshot([
|
||||
(2, PeerIdentityType::Credential, Some(vec![2; 32])),
|
||||
(3, PeerIdentityType::Credential, Some(vec![3; 32])),
|
||||
(4, PeerIdentityType::Credential, Some(vec![4; 32])),
|
||||
(5, PeerIdentityType::Credential, Some(vec![5; 32])),
|
||||
]);
|
||||
for relay_peer_id in [2, 3] {
|
||||
install_peer_info(
|
||||
&service_impl,
|
||||
relay_peer_id,
|
||||
vec![relay_peer_id as u8; 32],
|
||||
false,
|
||||
);
|
||||
install_credential_grant(&service_impl, vec![relay_peer_id as u8; 32], true);
|
||||
}
|
||||
install_conn_row(&service_impl, 2, [4]);
|
||||
install_conn_row(&service_impl, 3, [4, 5]);
|
||||
|
||||
assert!(service_impl.reconcile_my_conn_info(&snapshot, false));
|
||||
let first_version = service_impl
|
||||
.synced_route_info
|
||||
.conn_map
|
||||
.read()
|
||||
.get(&1)
|
||||
.unwrap()
|
||||
.version
|
||||
.get();
|
||||
assert_eq!(
|
||||
service_impl
|
||||
.synced_route_info
|
||||
.get_connected_peers::<BTreeSet<_>>(1),
|
||||
Some(BTreeSet::from([2, 3]))
|
||||
);
|
||||
|
||||
service_impl.synced_route_info.conn_map.write().remove(&2);
|
||||
assert!(!service_impl.reconcile_my_conn_info(&snapshot, false));
|
||||
|
||||
service_impl.synced_route_info.conn_map.write().remove(&3);
|
||||
assert!(service_impl.reconcile_my_conn_info(&snapshot, false));
|
||||
let self_row = service_impl.synced_route_info.conn_map.read();
|
||||
let self_row = self_row.get(&1).unwrap();
|
||||
assert_eq!(self_row.connected_peers, snapshot.peers);
|
||||
assert_eq!(self_row.version.get(), first_version + 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn peer_relay_projection_ignores_ineligible_relays() {
|
||||
let service_impl = test_peer_relay_service_impl(1);
|
||||
let snapshot = interface_peer_snapshot([
|
||||
(2, PeerIdentityType::Credential, Some(vec![2; 32])),
|
||||
(3, PeerIdentityType::Credential, Some(vec![3; 32])),
|
||||
(4, PeerIdentityType::Admin, Some(vec![4; 32])),
|
||||
(5, PeerIdentityType::Credential, Some(vec![5; 32])),
|
||||
(6, PeerIdentityType::Credential, Some(vec![6; 32])),
|
||||
]);
|
||||
install_peer_info(&service_impl, 2, vec![2; 32], false);
|
||||
install_peer_info(&service_impl, 3, vec![3; 32], true);
|
||||
install_peer_info(&service_impl, 4, vec![4; 32], false);
|
||||
install_peer_info(&service_impl, 5, vec![5; 32], false);
|
||||
for peer_id in [2, 3, 4, 5] {
|
||||
install_credential_grant(&service_impl, vec![peer_id as u8; 32], peer_id != 2);
|
||||
install_conn_row(&service_impl, peer_id, [6]);
|
||||
}
|
||||
service_impl
|
||||
.synced_route_info
|
||||
.suppressed_non_reusable_credential_peers
|
||||
.insert(5, ());
|
||||
|
||||
assert_eq!(
|
||||
service_impl.derive_advertised_connected_peers(&snapshot),
|
||||
snapshot.peers
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn peer_relay_projection_refreshes_local_topology_when_advertisement_is_unchanged() {
|
||||
let service_impl = test_peer_relay_service_impl(1);
|
||||
install_peer_info(&service_impl, 2, vec![2; 32], false);
|
||||
install_credential_grant(&service_impl, vec![2; 32], true);
|
||||
install_conn_row(&service_impl, 2, [3, 4]);
|
||||
let first = interface_peer_snapshot([
|
||||
(2, PeerIdentityType::Credential, Some(vec![2; 32])),
|
||||
(3, PeerIdentityType::Credential, Some(vec![3; 32])),
|
||||
]);
|
||||
assert!(service_impl.reconcile_my_conn_info(&first, false));
|
||||
let self_version = service_impl
|
||||
.synced_route_info
|
||||
.conn_map
|
||||
.read()
|
||||
.get(&1)
|
||||
.unwrap()
|
||||
.version
|
||||
.get();
|
||||
let route_version = service_impl.synced_route_info.version.get();
|
||||
|
||||
let second = interface_peer_snapshot([
|
||||
(2, PeerIdentityType::Credential, Some(vec![2; 32])),
|
||||
(4, PeerIdentityType::Credential, Some(vec![4; 32])),
|
||||
]);
|
||||
assert!(service_impl.reconcile_my_conn_info(&second, true));
|
||||
assert_eq!(
|
||||
service_impl
|
||||
.synced_route_info
|
||||
.conn_map
|
||||
.read()
|
||||
.get(&1)
|
||||
.unwrap()
|
||||
.version
|
||||
.get(),
|
||||
self_version
|
||||
);
|
||||
assert_eq!(
|
||||
service_impl.synced_route_info.version.get(),
|
||||
route_version + 1
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn peer_relay_projection_reconciles_forwarded_row_without_interface_change() {
|
||||
let service_impl = test_peer_relay_service_impl(1);
|
||||
let peers = Arc::new(Mutex::new(vec![2, 4]));
|
||||
let list_peers_calls = Arc::new(AtomicU32::new(0));
|
||||
*service_impl.interface.lock().await = Some(Box::new(CountingInterface {
|
||||
my_peer_id: 1,
|
||||
peers,
|
||||
peer_identity_types: Arc::new(Mutex::new(HashMap::from([
|
||||
(2, Some(PeerIdentityType::Credential)),
|
||||
(4, Some(PeerIdentityType::Credential)),
|
||||
]))),
|
||||
list_peers_calls: list_peers_calls.clone(),
|
||||
get_peer_identity_type_calls: Arc::new(AtomicU32::new(0)),
|
||||
}));
|
||||
install_peer_info(&service_impl, 2, vec![2; 32], false);
|
||||
install_credential_grant(&service_impl, vec![2; 32], true);
|
||||
|
||||
assert!(service_impl.update_my_conn_info().await);
|
||||
assert_eq!(list_peers_calls.load(Ordering::Relaxed), 1);
|
||||
assert_eq!(
|
||||
service_impl
|
||||
.synced_route_info
|
||||
.get_connected_peers::<BTreeSet<_>>(1),
|
||||
Some(BTreeSet::from([2, 4]))
|
||||
);
|
||||
|
||||
install_conn_row(&service_impl, 2, [4]);
|
||||
assert!(service_impl.update_my_conn_info().await);
|
||||
assert_eq!(list_peers_calls.load(Ordering::Relaxed), 1);
|
||||
assert_eq!(
|
||||
service_impl
|
||||
.synced_route_info
|
||||
.get_connected_peers::<BTreeSet<_>>(1),
|
||||
Some(BTreeSet::from([2]))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn interface_peer_cache_refreshes_only_when_marked_dirty() {
|
||||
let service_impl = test_service_impl(1);
|
||||
@@ -4741,6 +5314,64 @@ mod tests {
|
||||
assert_eq!(get_peer_identity_type_calls.load(Ordering::Relaxed), 4);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn periodic_requery_without_peer_change_keeps_route_version_stable() {
|
||||
let service_impl = test_peer_relay_service_impl(1);
|
||||
let list_peers_calls = Arc::new(AtomicU32::new(0));
|
||||
*service_impl.interface.lock().await = Some(Box::new(PeriodicRequeryInterface {
|
||||
peers: vec![2],
|
||||
list_peers_calls: list_peers_calls.clone(),
|
||||
}));
|
||||
|
||||
assert!(service_impl.update_my_conn_info().await);
|
||||
let route_version = service_impl.synced_route_info.version.get();
|
||||
|
||||
assert!(!service_impl.update_my_conn_info().await);
|
||||
assert_eq!(list_peers_calls.load(Ordering::Relaxed), 2);
|
||||
assert_eq!(service_impl.synced_route_info.version.get(), route_version);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn enabling_peer_relay_refreshes_authenticated_interface_metadata() {
|
||||
let context = Arc::new(TogglePeerRelayContext::default());
|
||||
let service_impl = PeerRouteServiceImpl::new(1, context.clone());
|
||||
let peer_identity_types = Arc::new(Mutex::new(HashMap::from([
|
||||
(2, Some(PeerIdentityType::Credential)),
|
||||
(3, Some(PeerIdentityType::Credential)),
|
||||
])));
|
||||
let list_peers_calls = Arc::new(AtomicU32::new(0));
|
||||
*service_impl.interface.lock().await = Some(Box::new(CountingInterface {
|
||||
my_peer_id: 1,
|
||||
peers: Arc::new(Mutex::new(vec![2, 3])),
|
||||
peer_identity_types,
|
||||
list_peers_calls: list_peers_calls.clone(),
|
||||
get_peer_identity_type_calls: Arc::new(AtomicU32::new(0)),
|
||||
}));
|
||||
install_peer_info(&service_impl, 2, vec![2; 32], false);
|
||||
install_credential_grant(&service_impl, vec![2; 32], true);
|
||||
install_conn_row(&service_impl, 2, [3]);
|
||||
|
||||
assert!(service_impl.update_my_conn_info().await);
|
||||
assert_eq!(list_peers_calls.load(Ordering::Relaxed), 1);
|
||||
assert_eq!(
|
||||
service_impl
|
||||
.synced_route_info
|
||||
.get_connected_peers::<BTreeSet<_>>(1),
|
||||
Some(BTreeSet::from([2, 3]))
|
||||
);
|
||||
|
||||
context.enabled.store(true, Ordering::Relaxed);
|
||||
|
||||
assert!(service_impl.update_my_conn_info().await);
|
||||
assert_eq!(list_peers_calls.load(Ordering::Relaxed), 2);
|
||||
assert_eq!(
|
||||
service_impl
|
||||
.synced_route_info
|
||||
.get_connected_peers::<BTreeSet<_>>(1),
|
||||
Some(BTreeSet::from([2]))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_peer_identity_type_reuses_snapshot_until_topology_changes() {
|
||||
let service_impl = test_service_impl(1);
|
||||
|
||||
@@ -76,6 +76,11 @@ impl NoopPeerContext {
|
||||
self.secure_mode = Some(secure_mode);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_flags(mut self, flags: FlagsInConfig) -> Self {
|
||||
self.flags = flags;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for NoopPeerContext {
|
||||
|
||||
@@ -361,7 +361,7 @@ where
|
||||
let listener = self.listener()?;
|
||||
match listener.accept().await {
|
||||
Ok((socket, _)) => return Ok(socket),
|
||||
Err(error) if is_retryable_tcp_accept_error(&error) => {
|
||||
Err(error) if is_retryable_tcp_io_error(&error) => {
|
||||
tracing::warn!(?error, "tcp accept failed with retryable error");
|
||||
}
|
||||
Err(error) => {
|
||||
@@ -377,7 +377,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn is_retryable_tcp_accept_error(error: &io::Error) -> bool {
|
||||
pub(crate) fn is_retryable_tcp_io_error(error: &io::Error) -> bool {
|
||||
use io::ErrorKind::*;
|
||||
matches!(
|
||||
error.kind(),
|
||||
|
||||
@@ -126,6 +126,7 @@ fn hosted_network_config(config: &NetworkConfig) -> NetworkConfig {
|
||||
instance_recv_bps_limit: config.instance_recv_bps_limit,
|
||||
disable_upnp: config.disable_upnp,
|
||||
disable_relay_data: config.disable_relay_data,
|
||||
prefer_peer_relay: config.prefer_peer_relay,
|
||||
enable_udp_broadcast_relay: config.enable_udp_broadcast_relay,
|
||||
managed_credentials: config.managed_credentials.clone(),
|
||||
peers,
|
||||
@@ -425,6 +426,7 @@ mod tests {
|
||||
}),
|
||||
enable_private_mode: Some(true),
|
||||
disable_relay_data: Some(true),
|
||||
prefer_peer_relay: Some(true),
|
||||
proxy_cidrs: vec!["10.88.0.0/24".to_owned()],
|
||||
managed_credentials: vec![ManagedCredentialConfig {
|
||||
credential_id: "managed".to_owned(),
|
||||
@@ -455,6 +457,7 @@ mod tests {
|
||||
assert_eq!(hosted.secure_mode, original.secure_mode);
|
||||
assert_eq!(hosted.enable_private_mode, Some(true));
|
||||
assert_eq!(hosted.disable_relay_data, Some(true));
|
||||
assert_eq!(hosted.prefer_peer_relay, Some(true));
|
||||
assert_eq!(hosted.proxy_cidrs, original.proxy_cidrs);
|
||||
assert_eq!(hosted.managed_credentials, original.managed_credentials);
|
||||
assert_eq!(hosted.port_forwards, original.port_forwards);
|
||||
|
||||
Reference in New Issue
Block a user