Compare commits

..
Author SHA1 Message Date
KKRainbow a24cc0a1bb fix(socket): keep port forwards usable after interface recreation
Bind TCP and UDP port-forward listeners to their configured address
instead of implicitly pinning them to the current network interface.
An interface recreated with the same name and IP gets a new ifindex,
leaving device-bound listeners unable to receive traffic while accept
continues waiting. Keep explicit device-binding options unchanged.

Add TCP and UDP three-node regressions that recreate a TUN interface
without restarting the forwarding instance, plus device-option checks.

Fixes #2714.
2026-10-21 00:39:23 +08:00
87 changed files with 802 additions and 4498 deletions
+1 -2
View File
@@ -16,10 +16,9 @@ test-group = 'subnet-proxy'
# These tests modify or inspect host-wide interfaces, routes, or BPF state.
# They must exclude all other tests, including the isolated network fixtures.
# Foreign-network listener discovery also enumerates the default namespace.
# Run exclusive tests first so the parallel pool does not repeatedly drain.
[[profile.default.overrides]]
filter = 'test(tests::upnp_test) | test(common::ifcfg::netlink::tests) | test(instance::public_ipv6_provider::platform::tests) | test(instance::dns_server::tests) | test(socket::fake_tcp::netfilter::linux_bpf::tests) | test(tests::three_node::foreign_network_advertises_listeners_for_direct_connect)'
filter = 'test(tests::upnp_test) | test(common::ifcfg::netlink::tests) | test(instance::public_ipv6_provider::platform::tests) | test(instance::dns_server::tests) | test(socket::fake_tcp::netfilter::linux_bpf::tests)'
threads-required = 'num-test-threads'
priority = 100
+1 -1
View File
@@ -94,7 +94,7 @@ jobs:
- name: Setup Android SDK
uses: android-actions/setup-android@v3
with:
cmdline-tools-version: '11076708'
cmdline-tools-version: 12.0
packages: 'build-tools;34.0.0 ndk;26.0.10792818 platform-tools platforms;android-34 '
- name: Setup Android Environment
Generated
+30 -31
View File
@@ -216,7 +216,7 @@ version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -227,7 +227,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -2365,7 +2365,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users 0.5.3",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -2685,7 +2685,6 @@ dependencies = [
"ring",
"rustls",
"rustls-pki-types",
"rustls-platform-verifier",
"serde",
"serde_json",
"sha2 0.10.9",
@@ -2702,7 +2701,7 @@ dependencies = [
"url",
"uuid",
"wasm-bindgen",
"webpki-roots 1.0.11",
"webpki-roots 1.0.9",
"wildmatch",
"x25519-dalek 2.0.1",
"zerocopy 0.7.35",
@@ -3220,7 +3219,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -5508,7 +5507,7 @@ dependencies = [
"png 0.18.1",
"serde",
"thiserror 2.0.20",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -5910,7 +5909,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -6477,7 +6476,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
dependencies = [
"libc",
"windows-sys 0.45.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -7467,7 +7466,7 @@ dependencies = [
"once_cell",
"socket2",
"tracing",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -8059,14 +8058,14 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.12.1",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
name = "rustls"
version = "0.23.46"
version = "0.23.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf"
checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
dependencies = [
"aws-lc-rs",
"once_cell",
@@ -8117,7 +8116,7 @@ dependencies = [
"security-framework",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -8128,9 +8127,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]]
name = "rustls-webpki"
version = "0.103.16"
version = "0.103.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281"
checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
dependencies = [
"aws-lc-rs",
"ring",
@@ -8970,7 +8969,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
dependencies = [
"libc",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -9902,10 +9901,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom 0.3.4",
"getrandom 0.4.3",
"once_cell",
"rustix 1.1.5",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -9924,7 +9923,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874"
dependencies = [
"rustix 1.1.5",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -10190,7 +10189,7 @@ dependencies = [
"tokio",
"tokio-rustls",
"tokio-util",
"webpki-roots 1.0.11",
"webpki-roots 1.0.9",
]
[[package]]
@@ -10608,7 +10607,7 @@ dependencies = [
"png 0.18.1",
"serde",
"thiserror 2.0.20",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -10648,7 +10647,7 @@ checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
[[package]]
name = "tun-easytier"
version = "1.1.1"
source = "git+https://github.com/EasyTier/rust-tun?rev=ac0c674c900dbdcf0b3fe9f5f6134c11579d1f7c#ac0c674c900dbdcf0b3fe9f5f6134c11579d1f7c"
source = "git+https://github.com/EasyTier/rust-tun?rev=5ba6be073e5b486b68c12f80fac5cfb50601f024#5ba6be073e5b486b68c12f80fac5cfb50601f024"
dependencies = [
"bytes",
"cfg-if",
@@ -10715,7 +10714,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
dependencies = [
"memoffset",
"tempfile",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -11222,9 +11221,9 @@ dependencies = [
[[package]]
name = "webpki-root-certs"
version = "1.0.11"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e49d0e65abc00c142e8f9428da991da9f1a250c39be97459901eb12b9b49f62a"
checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b"
dependencies = [
"rustls-pki-types",
]
@@ -11235,14 +11234,14 @@ version = "0.26.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
dependencies = [
"webpki-roots 1.0.11",
"webpki-roots 1.0.9",
]
[[package]]
name = "webpki-roots"
version = "1.0.11"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5f280bbb2b287bc8e7de7e3852945a0ff50d1bbcb5ed42b6c7a568c546df171"
checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
dependencies = [
"rustls-pki-types",
]
@@ -11361,7 +11360,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.48.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -11986,7 +11985,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10"
dependencies = [
"cfg-if",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
+7 -12
View File
@@ -364,18 +364,13 @@ A core instance:
`CoreProcessRuntime`, plus optional protocol and platform capabilities. The
bundle carries capabilities, not preconstructed portable managers.
By default, the standard native factory owns one continuously driven Tokio
executor per instance. `multi_thread` selects a multi-thread or current-thread
runtime; `multi_thread_count` sets the worker count with the historical minimum
of two. The `easytier-core --shared-runtime` option instead shares one
multi-thread executor across the factory and all its instances. Its worker count
defaults to the available CPU count and can be set with the process-level
`--multi-thread-count` option, with a minimum of one; per-instance runtime flags
are ignored in this mode. Construction, start/stop, and FFI data-plane tasks use
the selected executor, independent of the process management runtime. Dropping
the last factory or instance Host owner shuts down the executor without blocking
a worker on its own termination. Compact and WASI Hosts retain their externally
supplied execution model.
The standard native factory owns one continuously driven Tokio executor per
instance. `multi_thread` selects a multi-thread or current-thread runtime;
`multi_thread_count` sets the worker count with the historical minimum of two.
Construction, start/stop, and FFI data-plane tasks use that executor, independent
of the process management runtime. Dropping its Host owner shuts down the
executor without blocking a worker on its own termination. Compact and WASI
Hosts retain their externally supplied execution model.
Each Module owns partial-start cleanup for its internal resources.
`CoreInstance` has one outer cancellation and recovery path for the complete
+1 -8
View File
@@ -55,7 +55,6 @@ rand.workspace = true
quanta.workspace = true
ring = { version = "0.17", optional = true }
rustls = { workspace = true, features = ["ring", "std", "tls12"], optional = true }
rustls-platform-verifier = { version = "0.7", optional = true }
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
sha2.workspace = true
@@ -110,10 +109,7 @@ browser-config = ["config-write", "easytier-proto/api", "easytier-proto/json-rpc
chacha20 = ["dep:chacha20poly1305"]
openssl-crypto = ["dep:openssl"]
ring-crypto = ["dep:ring"]
# The WASI host performs AEAD for the guest (easytier_host.crypto_aead_seal/open),
# so the guest must not link a software backend: ring would emit env.ring_core_* imports
# that the host module cannot provide.
wasi-crypto-offload = []
wasi-crypto-offload = ["ring-crypto"]
config-write = []
endpoint-discovery = [
"dep:http-body-util",
@@ -125,8 +121,6 @@ endpoint-discovery = [
"dep:tokio-rustls",
"dep:webpki-roots",
]
# Opt in to platform trust for HTTPS discovery; excluded from default features.
system-certificates = ["endpoint-discovery", "dep:rustls-platform-verifier"]
dhcp-ipv4 = []
dhcp-ipv6 = []
public-ipv6-provider = []
@@ -146,7 +140,6 @@ management-rpc = ["easytier-proto/api"]
proxy-cidr-monitor = []
rich-config-errors = ["dep:ariadne"]
tcp-hole-punch = []
smoltcp = ["proxy-packet"]
proxy-packet = [
"wrapped-transport",
"dep:smoltcp",
-33
View File
@@ -87,7 +87,6 @@ pub(crate) fn set_network_flags(result: &mut NetworkConfig, flags: FlagsPatch) {
result.enable_udp_broadcast_relay = flags.enable_udp_broadcast_relay;
result.disable_sym_hole_punching = flags.disable_sym_hole_punching;
result.enable_magic_dns = flags.accept_dns;
result.tld_dns_zone = flags.tld_dns_zone;
result.mtu = flags.mtu.map(|mtu| mtu as i32);
result.data_compress_algo = flags.data_compress_algo;
result.encryption_algorithm = flags.encryption_algorithm;
@@ -514,7 +513,6 @@ impl NetworkConfigExt for NetworkConfig {
}
}),
accept_dns: self.enable_magic_dns,
tld_dns_zone: self.tld_dns_zone.clone(),
mtu: self
.mtu
.map(u32::try_from)
@@ -931,21 +929,6 @@ mod tests {
);
}
#[test]
fn magic_dns_zone_round_trips_missing_default_and_custom_values() {
for zone in [None, Some("et.net."), Some("nas.internal.")] {
let input = NetworkConfig {
tld_dns_zone: zone.map(str::to_owned),
..standalone_config()
};
let config = input.gen_config().unwrap();
let config = TomlConfigLoader::new_from_str(&config.dump()).unwrap();
assert_eq!(config.get_flags().tld_dns_zone, zone.unwrap_or("et.net."));
let output = NetworkConfig::new_from_config(&config).unwrap();
assert_eq!(output.tld_dns_zone.as_deref(), zone);
}
}
#[test]
fn vpn_portal_api_config_round_trips_through_toml_model() {
let input = NetworkConfig {
@@ -1062,22 +1045,6 @@ mod tests {
assert!(config.get_vpn_portal_config().is_none());
}
#[cfg(feature = "browser-config")]
#[test]
fn browser_merge_updates_and_clears_magic_dns_zone() {
let original = "[flags]\ntld_dns_zone = \"old.internal.\"\n";
for zone in [None, Some("et.net."), Some("nas.internal.")] {
let input = NetworkConfig {
tld_dns_zone: zone.map(str::to_owned),
..standalone_config()
};
let merged = merge_network_config_toml(original, &input).unwrap();
let output = TomlConfigLoader::new_from_str(&merged).unwrap();
assert_eq!(output.get_flags().tld_dns_zone, zone.unwrap_or("et.net."));
assert_eq!(output.get_flags_patch().tld_dns_zone.as_deref(), zone);
}
}
#[cfg(feature = "browser-config")]
#[test]
fn browser_merge_reconciles_renamed_flags() {
@@ -723,7 +723,6 @@ mod tests {
));
let handler = DirectConnectorRpcHandler::new_for_foreign_network_with_stun(
host,
Arc::new(crate::connectivity::NoLocalListeners),
SocketContext::default().with_socket_mark(Some(7)),
Some(Arc::new(FixedStunProvider)),
);
@@ -745,14 +744,6 @@ mod tests {
response.public_ipv4,
Some("198.51.100.7".parse::<std::net::Ipv4Addr>().unwrap().into())
);
assert_eq!(
response
.listeners
.into_iter()
.map(Url::from)
.collect::<Vec<_>>(),
vec!["tcp://192.0.2.1:11010".parse::<Url>().unwrap()]
);
}
fn snapshot() -> HostConnectorEnvironmentSnapshot {
+4 -15
View File
@@ -18,7 +18,7 @@ use crate::{
connectivity::hole_punch::policy::{should_background_p2p_with_peer, should_try_p2p_with_peer},
connectivity::stun::{StunInfoProvider, StunSocketMapper},
connectivity::{
LocalListenerUrls,
LocalListenerUrls, NoLocalListeners,
protocol::{
ClientProtocolUpgrader, ProtocolTransport, protocol_transport, protocol_uses_udp,
},
@@ -915,10 +915,10 @@ where
{
pub fn new_for_foreign_network_with_stun(
host: Arc<H>,
running_listeners: Arc<dyn LocalListenerUrls>,
socket_context: SocketContext,
stun: Option<Arc<dyn StunInfoProvider>>,
) -> Self {
let running_listeners = Arc::new(NoLocalListeners);
Self {
host,
peer_manager: None,
@@ -997,23 +997,14 @@ where
{
host: Arc<H>,
stun: Arc<dyn StunInfoProvider>,
running_listeners: Arc<dyn LocalListenerUrls>,
}
impl<H> ForeignDirectConnectorRpcRegistrar<H>
where
H: DirectConnectorHost,
{
pub fn new(
host: Arc<H>,
stun: Arc<dyn StunInfoProvider>,
running_listeners: Arc<dyn LocalListenerUrls>,
) -> Self {
Self {
host,
stun,
running_listeners,
}
pub fn new(host: Arc<H>, stun: Arc<dyn StunInfoProvider>) -> Self {
Self { host, stun }
}
}
@@ -1031,7 +1022,6 @@ where
GeneratedDirectConnectorRpcServer::new(
DirectConnectorRpcHandler::new_for_foreign_network_with_stun(
self.host.clone(),
self.running_listeners.clone(),
socket_context,
Some(self.stun.clone()),
),
@@ -1211,7 +1201,6 @@ fn is_public_ipv4(ip: Ipv4Addr) -> bool {
#[cfg(test)]
mod tests {
use super::*;
use crate::connectivity::NoLocalListeners;
impl<H> DirectConnectorRpcHandler<H>
where
@@ -41,11 +41,12 @@ const DST_PORT_OFFSET: u16 = 20;
const REMOTE_WAIT_TIME_MS: u64 = 5000;
pub fn apply_peer_easy_sym_port_offset(base_port: u16, peer_is_incremental: bool) -> u16 {
if peer_is_incremental {
base_port.saturating_add(DST_PORT_OFFSET)
let port = if peer_is_incremental {
(base_port as u32).saturating_add(DST_PORT_OFFSET as u32)
} else {
base_port.saturating_sub(DST_PORT_OFFSET).max(1)
}
(base_port as u32).saturating_sub(DST_PORT_OFFSET as u32)
};
port as u16
}
#[tracing::instrument(skip(runtime, signaling), fields(dst_peer_id), err)]
@@ -212,19 +213,24 @@ where
}
async fn prepare_udp_array(&self) -> anyhow::Result<Arc<UdpSocketArray<R>>> {
let rlocked = self.udp_array.read().await;
if let Some(udp_array) = rlocked.clone() {
return Ok(udp_array);
}
drop(rlocked);
let mut wlocked = self.udp_array.write().await;
let udp_array = wlocked
.get_or_insert_with(|| {
Arc::new(UdpSocketArray::new_with_context(
UDP_ARRAY_SIZE_FOR_HARD_SYM,
self.runtime.clone(),
self.runtime.socket_context(),
))
})
.clone();
// Successful candidates leave the array even if transport admission fails.
// Refill only those slots so other candidates retain their NAT mappings.
if let Some(udp_array) = wlocked.clone() {
return Ok(udp_array);
}
let udp_array = Arc::new(UdpSocketArray::new_with_context(
UDP_ARRAY_SIZE_FOR_HARD_SYM,
self.runtime.clone(),
self.runtime.socket_context(),
));
udp_array.start().await?;
wlocked.replace(udp_array.clone());
Ok(udp_array)
}
@@ -507,10 +513,11 @@ where
SendPunchPacketBothEasySym {
transaction_id: tid,
public_ip: my_public_ip,
dst_port_num: apply_peer_easy_sym_port_offset(
cur_mapped_addr.port(),
me_is_incremental,
) as u32,
dst_port_num: if me_is_incremental {
cur_mapped_addr.port().saturating_add(DST_PORT_OFFSET)
} else {
cur_mapped_addr.port().saturating_sub(DST_PORT_OFFSET)
} as u32,
udp_socket_count: UDP_ARRAY_SIZE_FOR_BOTH_EASY_SYM as u32,
wait_time_ms: REMOTE_WAIT_TIME_MS as u32,
},
@@ -527,7 +534,6 @@ where
.ok_or(anyhow::anyhow!("remote_mapped_addr is required"))?;
let now = Instant::now();
let punch_packet = new_hole_punch_packet(tid, HOLE_PUNCH_PACKET_BODY_LEN).into_bytes();
remote_mapped_addr.set_port(apply_peer_easy_sym_port_offset(
remote_mapped_addr.port(),
peer_is_incremental,
@@ -540,7 +546,10 @@ where
while now.elapsed().as_millis() < (REMOTE_WAIT_TIME_MS + 1000).into() {
udp_array
.send_with_all(&punch_packet, remote_mapped_addr)
.send_with_all(
&new_hole_punch_packet(tid, HOLE_PUNCH_PACKET_BODY_LEN).into_bytes(),
remote_mapped_addr,
)
.await?;
crate::foundation::time::sleep(std::time::Duration::from_millis(100)).await;
@@ -562,25 +571,17 @@ where
);
for _ in 0..2 {
// The peer may still be receiving raw HolePunch packets. Keep
// probing this candidate until it switches to a session listener.
send_from_local(socket.socket.as_ref(), &punch_packet, remote_mapped_addr).await?;
let connect = self
match self
.runtime
.connect_with_socket(socket.socket.clone(), remote_mapped_addr);
tokio::pin!(connect);
loop {
tokio::select! {
result = &mut connect => {
match result {
Ok(socket) => return Ok(Some(socket)),
Err(e) => tracing::error!(?e, "failed to connect with socket"),
}
break;
}
_ = crate::foundation::time::sleep(std::time::Duration::from_millis(200)) => {
send_from_local(socket.socket.as_ref(), &punch_packet, remote_mapped_addr).await?;
}
.connect_with_socket(socket.socket.clone(), remote_mapped_addr)
.await
{
Ok(socket) => {
return Ok(Some(socket));
}
Err(e) => {
tracing::error!(?e, "failed to connect with socket");
continue;
}
}
}
@@ -625,7 +626,6 @@ mod tests {
incoming_notify: tokio::sync::Notify,
sent: std::sync::Mutex<Vec<(Vec<u8>, SocketAddr)>>,
drop_syn_packets: AtomicUsize,
required_punch_packets: AtomicUsize,
}
#[async_trait]
@@ -636,20 +636,7 @@ mod tests {
async fn send_to(&self, data: &[u8], addr: SocketAddr) -> io::Result<usize> {
self.sent.lock().unwrap().push((data.to_vec(), addr));
if crate::packet::hole_punch_packet_tid(data, HOLE_PUNCH_PACKET_BODY_LEN).is_some()
&& self.required_punch_packets.load(Ordering::Relaxed) != 0
{
self.required_punch_packets.fetch_sub(1, Ordering::Relaxed);
self.incoming
.lock()
.unwrap()
.push_back((data.to_vec(), addr));
self.incoming_notify.notify_one();
}
if data.len() == 16 && data[4] == crate::packet::UdpPacketType::Syn as u8 {
if self.required_punch_packets.load(Ordering::Relaxed) != 0 {
return Ok(data.len());
}
if self.drop_syn_packets.load(Ordering::Relaxed) != 0 {
self.drop_syn_packets.fetch_sub(1, Ordering::Relaxed);
return Ok(data.len());
@@ -685,7 +672,6 @@ mod tests {
bind_options: tokio::sync::Mutex<Vec<UdpBindOptions>>,
sockets: std::sync::Mutex<Vec<std::sync::Weak<MockSocket>>>,
drop_syn_packets: usize,
required_punch_packets: usize,
}
impl MockRuntime {
@@ -696,7 +682,6 @@ mod tests {
bind_options: tokio::sync::Mutex::new(Vec::new()),
sockets: std::sync::Mutex::new(Vec::new()),
drop_syn_packets: 0,
required_punch_packets: 0,
}
}
}
@@ -714,7 +699,6 @@ mod tests {
incoming_notify: tokio::sync::Notify::new(),
sent: std::sync::Mutex::new(Vec::new()),
drop_syn_packets: AtomicUsize::new(self.drop_syn_packets),
required_punch_packets: AtomicUsize::new(self.required_punch_packets),
});
self.sockets.lock().unwrap().push(Arc::downgrade(&socket));
Ok(socket)
@@ -1073,18 +1057,6 @@ mod tests {
let hard_requests = signaling.hard_requests.lock().await;
assert_eq!(hard_requests.len(), 1);
assert_eq!(last_port_idx, 42);
drop(hard_requests);
drop(easy_requests);
client
.do_hole_punching(2, 4, &mut last_port_idx, NatType::SymmetricEasyInc.into())
.await
.unwrap();
assert_eq!(stun.port_mapping_count.load(Ordering::Relaxed), 2);
assert_eq!(
runtime.bind_count.load(Ordering::Relaxed),
UDP_ARRAY_SIZE_FOR_HARD_SYM
);
}
#[tokio::test]
@@ -1119,104 +1091,9 @@ mod tests {
}
#[test]
fn both_easy_sym_port_offset_stays_in_valid_port_range() {
for (base, incremental, expected) in [
(10000, true, 10020),
(10000, false, 9980),
(65516, true, 65535),
(65530, true, 65535),
(65535, true, 65535),
(20, false, 1),
(10, false, 1),
(1, false, 1),
] {
assert_eq!(apply_peer_easy_sym_port_offset(base, incremental), expected);
}
}
#[tokio::test]
async fn sym_to_cone_replenishes_candidates_after_connected_socket_is_dropped() {
let runtime = Arc::new(MockRuntime::new());
let client = UdpSymToConePunchClient::new(
runtime.clone(),
Arc::new(RecordingSignaling::new(0)),
Arc::new(MockStunInfoProvider::default()),
);
let array = client.prepare_udp_array().await.unwrap();
let remote = SocketAddr::from(([203, 0, 113, 1], 30000));
for tid in 0..=UDP_ARRAY_SIZE_FOR_HARD_SYM as u32 {
array.add_interest_tid(tid);
let socket = runtime
.sockets
.lock()
.unwrap()
.last()
.unwrap()
.upgrade()
.unwrap();
socket.incoming.lock().unwrap().push_back((
new_hole_punch_packet(tid, HOLE_PUNCH_PACKET_BODY_LEN)
.into_bytes()
.to_vec(),
remote,
));
socket.incoming_notify.notify_one();
drop(socket);
let punched = tokio::time::timeout(std::time::Duration::from_secs(1), async {
loop {
if let Some(socket) = array.try_fetch_punched_socket(tid) {
break socket;
}
tokio::task::yield_now().await;
}
})
.await
.unwrap();
array.remove_interest_tid(tid);
let connected = runtime
.connect_with_socket(punched.socket, remote)
.await
.unwrap();
// Model a successful UDP handshake followed by failed peer admission.
drop(connected);
assert!(Arc::ptr_eq(
&array,
&client.prepare_udp_array().await.unwrap()
));
assert_eq!(
runtime.bind_count.load(Ordering::Relaxed),
UDP_ARRAY_SIZE_FOR_HARD_SYM + tid as usize + 1,
);
}
}
#[tokio::test]
async fn both_easy_sym_keeps_probing_hit_until_remote_listener_is_ready() {
let mut runtime = MockRuntime::new();
// Initial probes produce a reverse hit, but the remote raw receiver
// needs more probes before it can accept SYN as a session listener.
runtime.required_punch_packets = 6;
let runtime = Arc::new(runtime);
let client = UdpBothEasySymPunchClient::new(
runtime,
Arc::new(RecordingSignaling::new(0)),
Arc::new(MockStunInfoProvider::default()),
);
let result = tokio::time::timeout(
std::time::Duration::from_secs(2),
client.do_hole_punching(
2,
NatType::SymmetricEasyInc.into(),
NatType::SymmetricEasyInc.into(),
&mut false,
),
)
.await
.unwrap()
.unwrap();
assert!(result.is_some());
fn both_easy_sym_port_offset_preserves_old_proto_cast_semantics() {
assert_eq!(apply_peer_easy_sym_port_offset(65530, true), 14);
assert_eq!(apply_peer_easy_sym_port_offset(10, false), 0);
}
#[tokio::test]
@@ -163,25 +163,19 @@ where
}
let base_port_num = request.base_port_num;
anyhow::ensure!(
(1..=u16::MAX as u32).contains(&base_port_num),
"send_punch_packet_easy_sym invalid base port"
);
let max_port_num = request.max_port_num.max(1);
let port_start = if request.is_incremental {
base_port_num.saturating_add(1)
} else {
base_port_num.saturating_sub(max_port_num).max(1)
base_port_num.saturating_sub(max_port_num)
};
let port_end = if request.is_incremental {
base_port_num
.saturating_add(max_port_num)
.min(u16::MAX as u32)
base_port_num.saturating_add(max_port_num)
} else {
base_port_num.saturating_sub(1)
};
if port_end < port_start {
if port_end <= port_start {
anyhow::bail!("send_punch_packet_easy_sym invalid port range");
}
@@ -956,6 +950,7 @@ where
if let Some(s) = udp_array.try_fetch_punched_socket(transaction_id) {
tracing::info!(?s, ?transaction_id, "got punched socket in both easy sym");
assert!(Arc::strong_count(&s.socket) == 1);
let Some(port) = s.socket.local_addr().ok().map(|addr| addr.port()) else {
tracing::warn!("failed to get local addr from punched socket");
continue;
@@ -1651,88 +1646,6 @@ mod tests {
assert_eq!(sent.len(), 1);
}
#[tokio::test]
async fn predictable_sender_uses_valid_boundary_ports() {
for (base_port_num, max_port_num, is_incremental, expected_ports) in [
(10000, 4, true, vec![10001, 10002, 10003, 10004]),
(10000, 4, false, vec![9996, 9997, 9998, 9999]),
(65534, 50, true, vec![65535]),
(2, 50, false, vec![1]),
(65535, 50, true, vec![]),
(1, 50, false, vec![]),
] {
let public_ips = vec![Ipv4Addr::new(198, 51, 100, 1)];
let listener = listener(10007, Vec::new());
let socket = listener.socket.clone();
let listener_mapped_addr = listener.mapped_addr;
let server = UdpHolePunchServer::new(
Arc::new(MockRuntime::new(vec![listener])),
mock_stun(),
Arc::new(MockSink::default()),
UdpSymPunchLock::default(),
);
server.common.select_listener(false, false).await.unwrap();
let result = server
.send_punch_packet_easy_sym_inner(SendPunchPacketEasySym {
listener_mapped_addr,
public_ips: public_ips.clone(),
transaction_id: 9,
base_port_num,
max_port_num,
is_incremental,
})
.await;
assert_eq!(result.is_ok(), !expected_ports.is_empty());
let sent = socket.sent.lock().await;
assert_eq!(sent.len(), expected_ports.len() * public_ips.len() * 3 * 2);
for ip in &public_ips {
for port in &expected_ports {
assert_eq!(
sent.iter()
.filter(|(_, addr)| *addr == SocketAddr::from((*ip, *port)))
.count(),
6
);
}
}
}
}
#[tokio::test]
async fn predictable_sender_keeps_packet_budget_with_multiple_public_ips() {
let listener = listener(10007, Vec::new());
let socket = listener.socket.clone();
let listener_mapped_addr = listener.mapped_addr;
let server = UdpHolePunchServer::new(
Arc::new(MockRuntime::new(vec![listener])),
mock_stun(),
Arc::new(MockSink::default()),
UdpSymPunchLock::default(),
);
server.common.select_listener(false, false).await.unwrap();
let public_ips = vec![
Ipv4Addr::new(198, 51, 100, 1),
Ipv4Addr::new(198, 51, 100, 2),
];
server
.send_punch_packet_easy_sym_inner(SendPunchPacketEasySym {
listener_mapped_addr,
public_ips: public_ips.clone(),
transaction_id: 9,
base_port_num: 10000,
max_port_num: 50,
is_incremental: true,
})
.await
.unwrap();
let sent = socket.sent.lock().await;
assert_eq!(sent.len(), 300);
for ip in public_ips {
assert!(sent.iter().any(|(_, addr)| addr.ip() == ip));
}
}
#[tokio::test]
async fn symmetric_packet_sender_returns_next_port_index() {
let socket = Arc::new(MockSocket {
@@ -4,7 +4,7 @@ use std::{
sync::{Arc, Mutex},
};
use dashmap::DashMap;
use dashmap::{DashMap, DashSet};
use tokio::task::JoinSet;
use tracing::{Instrument, Level, instrument};
@@ -42,6 +42,7 @@ where
socket_context: SocketContext,
tasks: Arc<Mutex<JoinSet<()>>>,
interest_tids: Arc<DashSet<u32>>,
tid_to_socket: Arc<DashMap<u32, Vec<PunchedUdpSocket<R::Socket>>>>,
}
@@ -67,6 +68,7 @@ where
socket_context,
tasks,
interest_tids: Arc::new(DashSet::new()),
tid_to_socket: Arc::new(DashMap::new()),
}
}
@@ -78,11 +80,12 @@ where
pub async fn add_new_socket(&self, socket: Arc<R::Socket>) -> anyhow::Result<()> {
let socket_map = self.sockets.clone();
let local_addr = socket.local_addr()?;
let interest_tids = self.interest_tids.clone();
let tid_to_socket = self.tid_to_socket.clone();
socket_map.insert(local_addr, socket.clone());
self.tasks.lock().unwrap().spawn(
async move {
let socket_map_guard = RemoveSocketOnDrop {
let _socket_map_guard = RemoveSocketOnDrop {
sockets: socket_map,
local_addr,
};
@@ -103,18 +106,17 @@ where
tracing::debug!(?addr, ?tid, "got udp hole punch packet");
if let Some(mut punched) = tid_to_socket.get_mut(&tid) {
if interest_tids.contains(&tid) {
tracing::info!(?addr, ?tid, "got hole punching packet with interest tid");
// Stop receiving and remove the candidate before handing it off.
// The consumer may immediately reuse the socket or rebind its port.
drop(socket_map_guard);
tracing::debug!(?local_addr, "udp socket recv loop end");
punched.push(PunchedUdpSocket {
socket,
tid,
remote_addr: addr,
});
return;
tid_to_socket
.entry(tid)
.or_default()
.push(PunchedUdpSocket {
socket: socket.clone(),
tid,
remote_addr: addr,
});
break;
}
}
tracing::debug!(?local_addr, "udp socket recv loop end");
@@ -168,7 +170,7 @@ where
}
pub fn add_interest_tid(&self, tid: u32) {
self.tid_to_socket.entry(tid).or_default();
self.interest_tids.insert(tid);
}
pub fn add_intreast_tid(&self, tid: u32) {
@@ -176,6 +178,7 @@ where
}
pub fn remove_interest_tid(&self, tid: u32) {
self.interest_tids.remove(&tid);
self.tid_to_socket.remove(&tid);
}
@@ -193,6 +196,7 @@ where
.field("sockets", &self.sockets.len())
.field("max_socket_count", &self.max_socket_count)
.field("started", &self.started())
.field("interest_tids", &self.interest_tids.len())
.field("tid_to_socket", &self.tid_to_socket.len())
.finish()
}
@@ -319,9 +323,6 @@ mod tests {
assert_eq!(punched.tid, tid);
assert_eq!(punched.remote_addr, remote_addr);
assert!(Arc::ptr_eq(&punched.socket, &socket));
assert!(array.sockets.is_empty());
drop(socket);
assert_eq!(Arc::strong_count(&punched.socket), 1);
return;
}
tokio::task::yield_now().await;
@@ -330,27 +331,6 @@ mod tests {
panic!("punched socket was not recorded");
}
#[tokio::test]
async fn removing_interest_drops_hits_and_ignores_late_packets() {
let array = UdpSocketArray::new(0, Arc::new(MockFactory::new()));
let tid = 7;
array.add_interest_tid(tid);
array.remove_interest_tid(tid);
let socket = Arc::new(MockSocket::new(
SocketAddr::from(([127, 0, 0, 1], 20000)),
vec![(
new_hole_punch_packet(tid, HOLE_PUNCH_PACKET_BODY_LEN)
.into_bytes()
.to_vec(),
SocketAddr::from(([10, 0, 0, 1], 1234)),
)],
));
array.add_new_socket(socket).await.unwrap();
tokio::task::yield_now().await;
assert!(array.tid_to_socket.is_empty());
assert_eq!(array.sockets.len(), 1);
}
#[tokio::test]
async fn send_with_all_sends_three_packets_per_socket() {
let runtime = Arc::new(MockFactory::new());
@@ -7,8 +7,6 @@ use hyper::{Request, header};
use hyper_util::rt::TokioIo;
use rand::{Rng as _, seq::SliceRandom};
use rustls::pki_types::ServerName;
#[cfg(feature = "system-certificates")]
use rustls_platform_verifier::BuilderVerifierExt as _;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio_rustls::TlsConnector;
use tokio_util::task::AbortOnDropHandle;
@@ -186,6 +184,9 @@ where
if request.url.scheme() == "https" {
let server_name = tls_server_name(&request.url)?;
let root_store = rustls::RootCertStore {
roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(),
};
// A host application can enable another rustls backend through Cargo
// feature unification. Select ring explicitly instead of requiring a
// process-wide provider to have been initialized first (#2607).
@@ -193,16 +194,9 @@ where
rustls::crypto::ring::default_provider(),
))
.with_safe_default_protocol_versions()
.context("selecting HTTPS protocol versions failed")?;
#[cfg(feature = "system-certificates")]
let tls_config = tls_config
.with_platform_verifier()
.context("loading platform TLS verifier failed")?;
#[cfg(not(feature = "system-certificates"))]
let tls_config = tls_config.with_root_certificates(rustls::RootCertStore {
roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(),
});
let tls_config = tls_config.with_no_client_auth();
.context("selecting HTTPS protocol versions failed")?
.with_root_certificates(root_store)
.with_no_client_auth();
let stream = TlsConnector::from(Arc::new(tls_config))
.connect(server_name, socket)
.await
+6 -41
View File
@@ -12,11 +12,6 @@ use crate::foundation::time;
pub(crate) trait ByteLimiter: Send + Sync {
fn try_consume(&self, bytes: u64) -> bool;
/// Bytes charged by this limiter so far.
fn metered_bytes(&self) -> u64 {
0
}
}
impl ByteLimiter for () {
@@ -42,7 +37,6 @@ struct BucketState {
available_tokens: u64,
last_refill: Instant,
refill_remainder: u64,
metered_bytes: u64,
}
#[derive(Clone, Copy)]
@@ -84,7 +78,6 @@ impl TokenBucket {
available_tokens: config.capacity,
last_refill: Instant::now(),
refill_remainder: 0,
metered_bytes: 0,
}),
config,
stop_notifier: Notify::new(),
@@ -139,30 +132,15 @@ impl TokenBucket {
let tokens = tokens.min(self.config.capacity);
let mut state = self.state.lock();
let now = Instant::now();
self.try_consume_locked(&mut state, tokens, now)
}
/// Consume from an already locked state at an explicit instant.
///
/// Production takes the lock first and then reads the clock, so tests can
/// pass the same instant twice to observe the bucket without any refill.
fn try_consume_locked(&self, state: &mut BucketState, tokens: u64, now: Instant) -> bool {
self.refill(state, now);
self.refill(&mut state, Instant::now());
if state.available_tokens < tokens {
return false;
}
state.available_tokens -= tokens;
state.metered_bytes = state.metered_bytes.saturating_add(tokens);
true
}
/// Total bytes charged by this bucket so far.
pub fn metered_bytes(&self) -> u64 {
self.state.lock().metered_bytes
}
/// Consume tokens, sleeping until they become available.
pub async fn consume(&self, tokens: u64) {
let mut remaining = tokens;
@@ -188,7 +166,6 @@ impl TokenBucket {
self.refill(&mut state, Instant::now());
if state.available_tokens >= tokens {
state.available_tokens -= tokens;
state.metered_bytes = state.metered_bytes.saturating_add(tokens);
return;
}
@@ -218,10 +195,6 @@ impl ByteLimiter for TokenBucket {
fn try_consume(&self, bytes: u64) -> bool {
TokenBucket::try_consume(self, bytes)
}
fn metered_bytes(&self) -> u64 {
TokenBucket::metered_bytes(self)
}
}
pub struct TokenBucketManager {
@@ -322,14 +295,10 @@ mod tests {
#[tokio::test]
async fn test_initial_state() {
let bucket = TokenBucket::new(1000, 1000);
// Refill is time based; reuse one instant so no token can appear
// between the two calls regardless of scheduling.
let now = Instant::now();
let mut state = bucket.state.lock();
// Should have full capacity initially
assert!(bucket.try_consume_locked(&mut state, 1000, now));
assert!(!bucket.try_consume_locked(&mut state, 1, now)); // Should be empty now
assert!(bucket.try_consume(1000));
assert!(!bucket.try_consume(1)); // Should be empty now
}
/// Test token consumption behavior
@@ -337,18 +306,14 @@ mod tests {
async fn test_consumption() {
let bucket = TokenBucket::new(1500, 1000);
// Same instant for every call: no refill can happen between them.
let now = Instant::now();
let mut state = bucket.state.lock();
// First packet should succeed
assert!(bucket.try_consume_locked(&mut state, 1000, now));
assert!(bucket.try_consume(1000));
// Second packet should fail (only 500 left)
assert!(!bucket.try_consume_locked(&mut state, 600, now));
assert!(!bucket.try_consume(600));
// Should be able to take remaining tokens
assert!(bucket.try_consume_locked(&mut state, 500, now));
assert!(bucket.try_consume(500));
}
#[tokio::test]
+4 -16
View File
@@ -176,19 +176,7 @@ const TCP_ENTRY: FlowKind = FlowKind::Tcp;
const TCP_LISTEN_ENTRY: FlowKind = FlowKind::TcpListen;
type FlowSet = Arc<FlowTable<FlowData>>;
pub(super) enum DataPlaneTcpIo {
Smoltcp(crate::gateway::smoltcp::TcpStream),
External(Box<dyn TcpProxyStream>),
}
impl DataPlaneTcpIo {
fn as_mut(&mut self) -> &mut dyn TcpProxyStream {
match self {
Self::Smoltcp(stream) => stream,
Self::External(stream) => &mut **stream,
}
}
}
type DataPlaneTcpIo = Box<dyn TcpProxyStream>;
pub(crate) struct DataPlaneRuntime<H>
where
@@ -700,7 +688,7 @@ where
generation.ensure_open()?;
Ok(DataPlaneTcpStream::new(
DataPlaneTcpIo::Smoltcp(stream),
Box::new(stream),
local_addr,
Some(data_plane_ref),
DataPlaneTcpStreamRoute::Outbound { _flow: flow },
@@ -722,7 +710,7 @@ where
.await?;
let local_addr = socket.local_addr().map_err(DataPlaneError::from)?;
Ok(DataPlaneTcpStream::new(
DataPlaneTcpIo::External(Box::new(socket)),
Box::new(socket),
local_addr,
None,
DataPlaneTcpStreamRoute::External,
@@ -760,7 +748,7 @@ where
)))
.await?;
Ok(DataPlaneTcpStream::new(
DataPlaneTcpIo::External(stream),
stream,
source_addr,
None,
DataPlaneTcpStreamRoute::External,
+7 -22
View File
@@ -25,7 +25,7 @@ pub(super) enum DataPlaneTcpStreamRoute {
/// A TCP stream created by the data-plane API.
pub struct DataPlaneTcpStream {
stream: Option<DataPlaneTcpIo>,
pub(super) stream: DataPlaneTcpIo,
pub(super) local_addr: SocketAddr,
pub(super) _route: DataPlaneTcpStreamRoute,
pub(super) _data_plane_lease: Option<DataPlaneLease>,
@@ -53,7 +53,7 @@ impl DataPlaneTcpStream {
generation: DataPlaneIoGuard,
) -> Self {
Self {
stream: Some(stream),
stream,
local_addr,
_data_plane_lease: data_plane_lease,
_route: route,
@@ -68,21 +68,6 @@ impl DataPlaneTcpStream {
}
}
impl Drop for DataPlaneTcpStream {
fn drop(&mut self) {
if let Some(DataPlaneTcpIo::Smoltcp(stream)) = self.stream.take() {
if self.generation.is_closed() {
return;
}
// The application is done, but FIN retransmissions and the peer's
// FIN still need this flow and the network stack until TCP closes.
let route = std::mem::replace(&mut self._route, DataPlaneTcpStreamRoute::External);
let lease = self._data_plane_lease.take();
stream.close((route, lease));
}
}
}
impl DataPlaneTcpListener {
pub fn local_addr(&self) -> SocketAddr {
self.local_addr
@@ -114,7 +99,7 @@ impl DataPlaneTcpListener {
FlowData::DataPlaneRoute,
);
let accepted = DataPlaneTcpStream::new(
DataPlaneTcpIo::Smoltcp(stream),
Box::new(stream),
local_addr,
Some(self.data_plane_lease.clone()),
DataPlaneTcpStreamRoute::Accepted { _flow: flow },
@@ -134,7 +119,7 @@ impl AsyncRead for DataPlaneTcpStream {
if this.generation.ensure_open().is_err() || this.read_closed.as_mut().poll(cx).is_ready() {
return Poll::Ready(Err(this.generation.closed_io_error()));
}
Pin::new(this.stream.as_mut().unwrap().as_mut()).poll_read(cx, buf)
Pin::new(&mut this.stream).poll_read(cx, buf)
}
}
@@ -149,7 +134,7 @@ impl AsyncWrite for DataPlaneTcpStream {
{
return Poll::Ready(Err(this.generation.closed_io_error()));
}
Pin::new(this.stream.as_mut().unwrap().as_mut()).poll_write(cx, buf)
Pin::new(&mut this.stream).poll_write(cx, buf)
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), std::io::Error>> {
@@ -158,7 +143,7 @@ impl AsyncWrite for DataPlaneTcpStream {
{
return Poll::Ready(Err(this.generation.closed_io_error()));
}
Pin::new(this.stream.as_mut().unwrap().as_mut()).poll_flush(cx)
Pin::new(&mut this.stream).poll_flush(cx)
}
fn poll_shutdown(
@@ -170,6 +155,6 @@ impl AsyncWrite for DataPlaneTcpStream {
{
return Poll::Ready(Err(this.generation.closed_io_error()));
}
Pin::new(this.stream.as_mut().unwrap().as_mut()).poll_shutdown(cx)
Pin::new(&mut this.stream).poll_shutdown(cx)
}
}
+2 -22
View File
@@ -542,29 +542,9 @@ async fn listener_and_accepted_stream_own_independent_flow_lifetimes() {
assert_eq!(&buf, b"after-listener-drop");
drop(client);
assert_eq!(a.gateway.entries.count(), 1);
assert_eq!(server.read(&mut buf).await.unwrap(), 0);
// The closed application no longer reads, but TCP must keep draining
// incoming data so the peer's queued data and FIN can finish sending.
tokio::time::timeout(timeout, async {
server.write_all(&vec![0; 1024 * 1024]).await.unwrap();
server.shutdown().await.unwrap();
})
.await
.unwrap();
assert_eq!(a.gateway.entries.count(), 0);
drop(server);
tokio::time::timeout(Duration::from_secs(15), async {
while a.gateway.entries.count() != 0
|| b.gateway.entries.count() != 0
|| a.gateway.net.lock().await.is_some()
|| b.gateway.net.lock().await.is_some()
{
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await
.expect("TCP teardown must release the last flow and stack leases");
assert_eq!(b.gateway.entries.count(), 0);
stop_data_plane_pair(&a, &b).await;
}
@@ -11,7 +11,6 @@ use tokio::{
sync::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel},
task::JoinSet,
};
use tokio_util::sync::CancellationToken;
use crate::{
packet::ZCPacket,
@@ -27,6 +26,23 @@ use super::{
traits::{IcmpProxyRuntime, IcmpProxySocket, ProxyRuntimeError},
};
async fn start_icmp_runtime<R: IcmpProxyRuntime + 'static>(
runtime: &R,
no_tun: bool,
) -> Result<Option<Arc<R::Socket>>, ProxyRuntimeError> {
match runtime.start_icmp().await {
Ok(socket) => Ok(Some(socket)),
Err(err) => {
runtime.stop_icmp();
if !no_tun {
return Err(err);
}
tracing::warn!(?err, "start ICMP runtime failed without TUN");
Ok(None)
}
}
}
pub struct IcmpProxyService<R: IcmpProxyRuntime + 'static> {
peer_manager: Arc<PeerManagerCore>,
runtime: Arc<R>,
@@ -36,8 +52,6 @@ pub struct IcmpProxyService<R: IcmpProxyRuntime + 'static> {
pipeline_guard: std::sync::Mutex<Option<PipelineRegistrationGuard>>,
tasks: std::sync::Mutex<JoinSet<()>>,
socket: std::sync::Mutex<Option<Arc<R::Socket>>>,
socket_init: tokio::sync::Mutex<()>,
stopped: CancellationToken,
runtime_started: AtomicBool,
started: AtomicBool,
}
@@ -59,8 +73,6 @@ impl<R: IcmpProxyRuntime + 'static> IcmpProxyService<R> {
pipeline_guard: std::sync::Mutex::new(None),
tasks: std::sync::Mutex::new(JoinSet::new()),
socket: std::sync::Mutex::new(None),
socket_init: tokio::sync::Mutex::new(()),
stopped: CancellationToken::new(),
runtime_started: AtomicBool::new(false),
started: AtomicBool::new(false),
})
@@ -72,6 +84,17 @@ impl<R: IcmpProxyRuntime + 'static> IcmpProxyService<R> {
}
let snapshot = self.runtime.proxy_runtime_snapshot();
let socket = match start_icmp_runtime(self.runtime.as_ref(), snapshot.no_tun).await {
Ok(socket) => socket,
Err(err) => {
self.started.store(false, Ordering::Release);
return Err(err);
}
};
let runtime_started = socket.is_some();
self.runtime_started
.store(runtime_started, Ordering::Release);
self.socket.lock().unwrap().clone_from(&socket);
if let Some(mut response_rx) = self.response_rx.lock().unwrap().take() {
let peer_manager = self.peer_manager.clone();
@@ -90,6 +113,33 @@ impl<R: IcmpProxyRuntime + 'static> IcmpProxyService<R> {
});
}
if let Some(socket) = socket {
let service = Arc::downgrade(self);
self.tasks.lock().unwrap().spawn(async move {
loop {
let recv_result = socket.recv().await;
let (peer_ip, mut packet) = match recv_result {
Ok(packet) => packet,
Err(err) => {
tracing::error!(?err, "receive ICMP packet failed");
continue;
}
};
if packet.is_empty() {
tracing::error!("received empty ICMP packet");
break;
}
let IpAddr::V4(peer_ip) = peer_ip else {
continue;
};
let Some(service) = service.upgrade() else {
break;
};
service.handle_socket_response(peer_ip, &mut packet);
}
});
}
let service = Arc::downgrade(self);
self.tasks.lock().unwrap().spawn(async move {
loop {
@@ -126,13 +176,9 @@ impl<R: IcmpProxyRuntime + 'static> IcmpProxyService<R> {
}
pub fn stop(&self) {
// Serialize shutdown with publication of a newly opened socket and its
// receive task. Opening itself must not hold this synchronous lock.
let mut socket = self.socket.lock().unwrap();
if !self.started.swap(false, Ordering::AcqRel) {
return;
}
self.stopped.cancel();
if let Some(guard) = self.pipeline_guard.lock().unwrap().take() {
guard.close();
}
@@ -140,61 +186,7 @@ impl<R: IcmpProxyRuntime + 'static> IcmpProxyService<R> {
if self.runtime_started.swap(false, Ordering::AcqRel) {
self.runtime.stop_icmp();
}
socket.take();
}
async fn socket(self: &Arc<Self>) -> Result<Arc<R::Socket>, ProxyRuntimeError> {
let _init = self.socket_init.lock().await;
if !self.started.load(Ordering::Acquire) || self.stopped.is_cancelled() {
return Err(std::io::Error::from(std::io::ErrorKind::NotConnected).into());
}
if let Some(socket) = self.socket.lock().unwrap().clone() {
return Ok(socket);
}
// Roll back partial resources on errors, caller cancellation or stop.
let rollback = guarden::guard! { self.runtime.stop_icmp(); };
let socket = tokio::select! {
biased;
_ = self.stopped.cancelled() => {
return Err(std::io::Error::from(std::io::ErrorKind::NotConnected).into());
}
socket = self.runtime.start_icmp() => socket?,
};
let mut current = self.socket.lock().unwrap();
if !self.started.load(Ordering::Acquire) {
return Err(std::io::Error::from(std::io::ErrorKind::NotConnected).into());
}
let service = Arc::downgrade(self);
let receiver = socket.clone();
self.tasks.lock().unwrap().spawn(async move {
loop {
let recv_result = receiver.recv().await;
let (peer_ip, mut packet) = match recv_result {
Ok(packet) => packet,
Err(err) => {
tracing::error!(?err, "receive ICMP packet failed");
continue;
}
};
if packet.is_empty() {
tracing::error!("received empty ICMP packet");
break;
}
let IpAddr::V4(peer_ip) = peer_ip else {
continue;
};
let Some(service) = service.upgrade() else {
break;
};
service.handle_socket_response(peer_ip, &mut packet);
}
});
*current = Some(socket.clone());
self.runtime_started.store(true, Ordering::Release);
rollback.defuse();
Ok(socket)
self.socket.lock().unwrap().take();
}
async fn handle_peer_packet(self: Arc<Self>, packet: ZCPacket) -> Option<ZCPacket> {
@@ -212,13 +204,14 @@ impl<R: IcmpProxyRuntime + 'static> IcmpProxyService<R> {
destination,
packet: request,
} => {
match self.socket().await {
Ok(socket) => {
let socket = self.socket.lock().unwrap().clone();
match socket {
Some(socket) => {
if let Err(err) = socket.send(destination, &request).await {
tracing::error!(?err, "send ICMP packet through runtime failed");
}
}
Err(err) => tracing::error!(?err, "start ICMP runtime failed"),
None => tracing::error!("send ICMP packet without a runtime socket"),
}
None
}
@@ -266,59 +259,25 @@ impl<R: IcmpProxyRuntime + 'static> PeerPacketFilter for IcmpProxyServiceFilter<
#[cfg(test)]
mod tests {
use std::sync::atomic::AtomicUsize;
use smoltcp::wire::{Icmpv4Message, Icmpv4Packet, IpProtocol, Ipv4Packet};
use tokio::sync::Semaphore;
use super::*;
use crate::{
config::peers::PeerRuntimeSnapshot,
gateway::proxy::{
cidr_table::{ProxyCidrRule, ProxyCidrSnapshot},
traits::{ProxyRuntimeInfo, ProxyRuntimeSnapshot},
use std::{
net::{IpAddr, Ipv4Addr},
sync::{
Arc,
atomic::{AtomicUsize, Ordering},
},
packet::PacketType,
peers::peer_manager::PortablePeerManagerConfig,
};
#[derive(Default)]
struct TestSocket {
sent: std::sync::Mutex<Vec<Ipv4Addr>>,
receives: AtomicUsize,
}
#[async_trait::async_trait]
impl IcmpProxySocket for TestSocket {
async fn send(
&self,
destination: Ipv4Addr,
_packet: &[u8],
) -> Result<(), ProxyRuntimeError> {
self.sent.lock().unwrap().push(destination);
Ok(())
}
async fn recv(&self) -> Result<(IpAddr, Vec<u8>), ProxyRuntimeError> {
self.receives.fetch_add(1, Ordering::AcqRel);
std::future::pending().await
}
}
use super::*;
use crate::gateway::proxy::traits::{IcmpProxySocket, ProxyRuntimeInfo, ProxyRuntimeSnapshot};
#[derive(Default)]
struct TestRuntime {
snapshot: ProxyRuntimeSnapshot,
socket: Arc<TestSocket>,
starts: AtomicUsize,
stops: AtomicUsize,
active: AtomicBool,
fail: AtomicBool,
start_gate: Option<Semaphore>,
struct PartialStartRuntime {
stop_count: AtomicUsize,
}
impl ProxyRuntimeInfo for TestRuntime {
impl ProxyRuntimeInfo for PartialStartRuntime {
fn proxy_runtime_snapshot(&self) -> ProxyRuntimeSnapshot {
self.snapshot
ProxyRuntimeSnapshot::default()
}
fn is_ip_local_virtual_ip(&self, _ip: &IpAddr) -> bool {
@@ -326,252 +285,53 @@ mod tests {
}
}
struct NoopIcmpSocket;
#[async_trait::async_trait]
impl IcmpProxyRuntime for TestRuntime {
type Socket = TestSocket;
impl IcmpProxySocket for NoopIcmpSocket {
async fn send(
&self,
_destination: Ipv4Addr,
_packet: &[u8],
) -> Result<(), ProxyRuntimeError> {
Ok(())
}
async fn recv(&self) -> Result<(IpAddr, Vec<u8>), ProxyRuntimeError> {
Err(std::io::Error::other("unused test socket").into())
}
}
#[async_trait::async_trait]
impl IcmpProxyRuntime for PartialStartRuntime {
type Socket = NoopIcmpSocket;
async fn start_icmp(&self) -> Result<Arc<Self::Socket>, ProxyRuntimeError> {
self.starts.fetch_add(1, Ordering::AcqRel);
self.active.store(true, Ordering::Release);
if let Some(gate) = &self.start_gate {
gate.acquire().await.unwrap().forget();
}
tokio::task::yield_now().await;
if self.fail.load(Ordering::Acquire) {
return Err(std::io::Error::other("partial start").into());
}
Ok(self.socket.clone())
Err(std::io::Error::other("partial start").into())
}
fn stop_icmp(&self) {
self.stops.fetch_add(1, Ordering::AcqRel);
self.active.store(false, Ordering::Release);
self.stop_count.fetch_add(1, Ordering::AcqRel);
}
}
fn service(
runtime: Arc<TestRuntime>,
table: Arc<ProxyCidrTable>,
) -> Arc<IcmpProxyService<TestRuntime>> {
let (packet_tx, _packet_rx) = crate::host::packet::host_packet_channel();
let peer_manager = Arc::new(
PeerManagerCore::new_portable_for_test(
PortablePeerManagerConfig::new(PeerRuntimeSnapshot::default().runtime),
packet_tx,
)
.unwrap(),
);
IcmpProxyService::new(peer_manager, runtime, table, Duration::from_secs(10))
}
#[tokio::test]
async fn failed_runtime_start_rolls_back_partial_resources() {
let runtime = PartialStartRuntime::default();
fn runtime(no_tun: bool, enable_exit_node: bool) -> Arc<TestRuntime> {
Arc::new(TestRuntime {
snapshot: ProxyRuntimeSnapshot {
virtual_ipv4: Some("10.0.0.1".parse().unwrap()),
no_tun,
enable_exit_node,
..Default::default()
},
..Default::default()
})
}
fn echo_request(destination: &str) -> ZCPacket {
let mut bytes = [0_u8; 28];
let mut request = Icmpv4Packet::new_unchecked(&mut bytes[20..]);
request.set_msg_type(Icmpv4Message::EchoRequest);
request.set_echo_ident(7);
request.set_echo_seq_no(11);
request.fill_checksum();
let mut ipv4 = Ipv4Packet::new_unchecked(&mut bytes[..]);
ipv4.set_version(4);
ipv4.set_header_len(20);
ipv4.set_total_len(28);
ipv4.set_hop_limit(64);
ipv4.set_next_header(IpProtocol::Icmp);
ipv4.set_src_addr("10.0.0.2".parse().unwrap());
ipv4.set_dst_addr(destination.parse().unwrap());
ipv4.fill_checksum();
let mut packet = ZCPacket::new_with_payload(&bytes);
packet.fill_peer_manager_hdr(101, 202, PacketType::Data as u8);
packet
assert!(start_icmp_runtime(&runtime, false).await.is_err());
assert_eq!(runtime.stop_count.load(Ordering::Acquire), 1);
}
#[tokio::test]
async fn no_tun_local_echo_does_not_open_socket() {
let runtime = runtime(true, false);
let service = service(runtime.clone(), Arc::new(ProxyCidrTable::new()));
let mut responses = service.response_rx.lock().unwrap().take().unwrap();
service.start().await.unwrap();
assert!(
service
.clone()
.handle_peer_packet(echo_request("10.0.0.1"))
.await
.is_none()
);
let response = responses.try_recv().unwrap();
let ipv4 = Ipv4Packet::new_checked(response.payload()).unwrap();
let icmp = Icmpv4Packet::new_checked(ipv4.payload()).unwrap();
assert_eq!(icmp.msg_type(), Icmpv4Message::EchoReply);
assert_eq!(runtime.starts.load(Ordering::Acquire), 0);
assert_eq!(runtime.socket.receives.load(Ordering::Acquire), 0);
service.stop();
assert_eq!(runtime.stops.load(Ordering::Acquire), 0);
}
async fn no_tun_suppresses_start_error_after_rollback() {
let runtime = PartialStartRuntime::default();
#[tokio::test]
async fn dynamic_proxy_networks_open_once_and_follow_updated_rules() {
let runtime = runtime(false, false);
let table = Arc::new(ProxyCidrTable::new());
let service = service(runtime.clone(), table.clone());
service.start().await.unwrap();
assert!(
service
.clone()
.handle_peer_packet(echo_request("192.0.2.42"))
.await
.is_some()
);
assert_eq!(runtime.starts.load(Ordering::Acquire), 0);
table.update_snapshot(ProxyCidrSnapshot {
rules: vec![ProxyCidrRule {
cidr: "127.0.0.0/24".parse().unwrap(),
mapped_cidr: Some("192.0.2.0/24".parse().unwrap()),
}],
});
// The first concurrent requests must share both the socket and receiver.
futures::future::join_all((0..16).map(|_| {
service
.clone()
.handle_peer_packet(echo_request("192.0.2.42"))
}))
.await;
tokio::task::yield_now().await;
assert_eq!(runtime.starts.load(Ordering::Acquire), 1);
assert_eq!(runtime.socket.receives.load(Ordering::Acquire), 1);
assert_eq!(
*runtime.socket.sent.lock().unwrap(),
vec!["127.0.0.42".parse::<Ipv4Addr>().unwrap(); 16]
);
table.update_snapshot(ProxyCidrSnapshot::default());
assert!(
service
.clone()
.handle_peer_packet(echo_request("192.0.2.42"))
.await
.is_some()
);
assert_eq!(runtime.socket.sent.lock().unwrap().len(), 16);
// Keep the socket for in-flight replies and subsequent network additions.
assert_eq!(runtime.stops.load(Ordering::Acquire), 0);
table.update_snapshot(ProxyCidrSnapshot {
rules: vec![ProxyCidrRule {
cidr: "192.0.2.0/24".parse().unwrap(),
mapped_cidr: None,
}],
});
assert!(
service
.clone()
.handle_peer_packet(echo_request("192.0.2.42"))
.await
.is_none()
);
assert_eq!(runtime.starts.load(Ordering::Acquire), 1);
assert_eq!(
runtime.socket.sent.lock().unwrap().last(),
Some(&"192.0.2.42".parse().unwrap())
);
service.stop();
service.stop();
assert_eq!(runtime.stops.load(Ordering::Acquire), 1);
assert!(!runtime.active.load(Ordering::Acquire));
assert!(service.socket().await.is_err());
}
#[tokio::test]
async fn exit_node_opens_socket_without_proxy_networks() {
let runtime = runtime(false, true);
let service = service(runtime.clone(), Arc::new(ProxyCidrTable::new()));
service.start().await.unwrap();
assert_eq!(runtime.starts.load(Ordering::Acquire), 0);
let mut packet = echo_request("192.0.2.42");
packet
.mut_peer_manager_header()
.unwrap()
.set_exit_node(true);
assert!(service.clone().handle_peer_packet(packet).await.is_none());
assert_eq!(runtime.starts.load(Ordering::Acquire), 1);
assert_eq!(
*runtime.socket.sent.lock().unwrap(),
["192.0.2.42".parse::<Ipv4Addr>().unwrap()]
);
}
#[tokio::test]
async fn failed_runtime_start_rolls_back_and_can_retry() {
let runtime = runtime(true, false);
runtime.fail.store(true, Ordering::Release);
let service = service(runtime.clone(), Arc::new(ProxyCidrTable::new()));
let mut responses = service.response_rx.lock().unwrap().take().unwrap();
service.start().await.unwrap();
assert!(service.socket().await.is_err());
assert_eq!(runtime.stops.load(Ordering::Acquire), 1);
assert!(!runtime.active.load(Ordering::Acquire));
assert!(
service
.clone()
.handle_peer_packet(echo_request("10.0.0.1"))
.await
.is_none()
);
assert!(responses.try_recv().is_ok());
assert_eq!(runtime.starts.load(Ordering::Acquire), 1);
runtime.fail.store(false, Ordering::Release);
assert!(service.socket().await.is_ok());
service.stop();
assert_eq!(runtime.stops.load(Ordering::Acquire), 2);
}
#[tokio::test]
async fn stop_cancels_pending_socket_start() {
let runtime = Arc::new(TestRuntime {
start_gate: Some(Semaphore::new(0)),
..Default::default()
});
let service = service(runtime.clone(), Arc::new(ProxyCidrTable::new()));
service.start().await.unwrap();
let mut opening = Box::pin(service.socket());
assert!(futures::poll!(&mut opening).is_pending());
assert!(runtime.active.load(Ordering::Acquire));
service.stop();
assert!(opening.await.is_err());
assert!(!runtime.active.load(Ordering::Acquire));
assert_eq!(runtime.stops.load(Ordering::Acquire), 1);
assert_eq!(runtime.socket.receives.load(Ordering::Acquire), 0);
assert!(service.socket().await.is_err());
assert_eq!(runtime.starts.load(Ordering::Acquire), 1);
}
#[tokio::test]
async fn cancelled_caller_rolls_back_socket_start() {
let runtime = Arc::new(TestRuntime {
start_gate: Some(Semaphore::new(0)),
..Default::default()
});
let service = service(runtime.clone(), Arc::new(ProxyCidrTable::new()));
service.start().await.unwrap();
let mut opening = Box::pin(service.socket());
assert!(futures::poll!(&mut opening).is_pending());
drop(opening);
assert!(!runtime.active.load(Ordering::Acquire));
assert_eq!(runtime.stops.load(Ordering::Acquire), 1);
runtime.start_gate.as_ref().unwrap().add_permits(1);
assert!(service.socket().await.is_ok());
service.stop();
assert_eq!(runtime.stops.load(Ordering::Acquire), 2);
let socket = start_icmp_runtime(&runtime, true).await.unwrap();
assert!(socket.is_none());
if socket.is_some() {
runtime.stop_icmp();
}
assert_eq!(runtime.stop_count.load(Ordering::Acquire), 1);
}
}
+5 -5
View File
@@ -379,12 +379,12 @@ where
return Ok(());
}
// Runtime updates can be cancelled while a host operation is pending.
// Roll back the startup flags and resources so a later update can retry.
let rollback = guarden::guard! { self.stop_started(); };
self.runtime.latch_smoltcp();
self.tcp_started.store(true, Ordering::Release);
self.tcp.start(true).await?;
if let Err(error) = self.tcp.start(true).await {
self.stop_started();
return Err(error);
}
if let Some(icmp) = &self.icmp {
self.icmp_started.store(true, Ordering::Release);
@@ -398,6 +398,7 @@ where
.proxy
.icmp_failure_is_fatal
{
self.stop_started();
return Err(error);
}
tracing::warn!(?error, "optional ICMP proxy runtime failed to start");
@@ -406,7 +407,6 @@ where
self.udp_started.store(true, Ordering::Release);
self.udp.start().await;
rollback.defuse();
Ok(())
}
+1 -1
View File
@@ -3,5 +3,5 @@ mod tokio_smoltcp;
pub(super) use stack::{SmolTcpStack, output_dst_ip};
pub(super) use tokio_smoltcp::{
BufferSize, Net, NetConfig, TcpListener, TcpStream, UdpSocket, channel_device,
BufferSize, Net, NetConfig, TcpListener, UdpSocket, channel_device,
};
@@ -24,10 +24,7 @@ use std::{
sync::Arc,
task::{Context as TaskContext, Poll, Waker},
};
use tokio::{
pin, select,
sync::{Notify, mpsc},
};
use tokio::{pin, select, sync::Notify};
use crate::foundation::time::sleep;
@@ -53,18 +50,11 @@ struct TcpListenerRegistry {
type SharedTcpListenerRegistry = Arc<Mutex<TcpListenerRegistry>>;
struct ClosingTcpSocket {
handle: OwnedSocketHandle,
deadline: Instant,
_keep_alive: Box<dyn Send>,
}
pub(crate) struct Reactor {
notify: Arc<Notify>,
iface: BufferInterface,
socket_allocator: SocketAlloctor,
tcp_listeners: SharedTcpListenerRegistry,
closing_tcp: mpsc::UnboundedSender<ClosingTcpSocket>,
}
async fn receive(
@@ -213,14 +203,12 @@ fn update_tcp_listeners(
wakers
}
#[allow(clippy::too_many_arguments)]
async fn run(
mut async_iface: impl super::device::AsyncDevice,
iface: BufferInterface,
mut device: BufferDevice,
socket_allocator: SocketAlloctor,
tcp_listeners: SharedTcpListenerRegistry,
mut closing_tcp: mpsc::UnboundedReceiver<ClosingTcpSocket>,
notify: Arc<Notify>,
stopper: Arc<Notify>,
) -> io::Result<()> {
@@ -231,36 +219,21 @@ async fn run(
.max_burst_size
.unwrap_or(MAX_BURST_SIZE);
let mut recv_buf = VecDeque::with_capacity(max_burst_size);
let mut closing = Vec::<ClosingTcpSocket>::new();
let mut closed = Vec::new();
pin!(timer);
loop {
let packets = device.take_send_queue();
async_iface.send_all(&mut iter(packets).map(Ok)).await?;
// Keep routing and stack leases until the last protocol output has
// left the device queue. Drop handles outside the socket-set lock.
closed.clear();
while let Ok(socket) = closing_tcp.try_recv() {
closing.push(socket);
}
if recv_buf.is_empty() && device.need_wait() {
let start = Instant::now();
let mut deadline = {
let deadline = {
iface
.lock()
.poll_delay(start, &socket_allocator.sockets().lock())
.unwrap_or(default_timeout)
};
for socket in &closing {
deadline = deadline.min(if socket.deadline <= start {
Duration::ZERO
} else {
socket.deadline - start
});
}
if deadline != Duration::ZERO {
timer
@@ -270,7 +243,6 @@ async fn run(
_ = &mut timer => {},
_ = receive(&mut async_iface,&mut recv_buf) => {}
_ = notify.notified() => {}
Some(socket) = closing_tcp.recv() => closing.push(socket),
_ = stopper.notified() => break,
};
}
@@ -318,21 +290,6 @@ async fn run(
iface.poll(now, &mut device, &mut sockets);
listener_wakers.extend(update_tcp_listeners(&mut listeners, &mut sockets));
if prepare_tcp_close(now, &closing, &mut sockets) {
iface.poll(now, &mut device, &mut sockets);
}
let mut index = 0;
while index < closing.len() {
let socket = sockets.get::<tcp::Socket>(*closing[index].handle);
// Closed can still have an RST to dispatch. TIME-WAIT must also
// remain available to acknowledge a retransmitted peer FIN.
if socket.state() == tcp::State::Closed && socket.remote_endpoint().is_none() {
closed.push(closing.swap_remove(index));
} else {
index += 1;
}
}
// wake up all closed sockets (smoltcp seems have a bug that it doesn't wake up closed sockets)
for (_, socket) in sockets.iter_mut() {
if let Socket::Tcp(tcp) = socket
@@ -352,30 +309,6 @@ async fn run(
Ok(())
}
fn prepare_tcp_close(
now: Instant,
closing: &[ClosingTcpSocket],
sockets: &mut SocketSet<'static>,
) -> bool {
let mut changed = false;
for entry in closing {
let socket = sockets.get_mut::<tcp::Socket>(*entry.handle);
// There is no application reader anymore. Keep the receive window
// open so queued peer data cannot prevent its FIN from reaching us.
if socket.can_recv() {
let _ = socket.recv(|data| (data.len(), ()));
changed = true;
}
// The normal TCP timeout measures inactivity, so ACKs can extend it.
// A released application resource has an absolute teardown deadline.
if now >= entry.deadline && socket.state() != tcp::State::Closed {
socket.abort();
changed = true;
}
}
changed
}
impl Reactor {
pub fn new(
async_device: impl super::device::AsyncDevice,
@@ -388,14 +321,12 @@ impl Reactor {
let notify = Arc::new(Notify::new());
let socket_allocator = SocketAlloctor::new(buffer_size);
let tcp_listeners = Arc::new(Mutex::new(TcpListenerRegistry::default()));
let (closing_tx, closing_rx) = mpsc::unbounded_channel();
let fut = run(
async_device,
iface.clone(),
device,
socket_allocator.clone(),
tcp_listeners.clone(),
closing_rx,
notify.clone(),
stopper,
);
@@ -406,7 +337,6 @@ impl Reactor {
iface,
socket_allocator,
tcp_listeners,
closing_tcp: closing_tx,
},
fut,
)
@@ -420,21 +350,6 @@ impl Reactor {
|sockets: &mut smoltcp::iface::SocketSet<'_>| sockets.get_mut::<T>(handle),
)
}
pub(super) fn close_tcp(
&self,
handle: OwnedSocketHandle,
keep_alive: Box<dyn Send>,
deadline: Instant,
) {
self.get_socket::<tcp::Socket>(*handle).close();
// The receiver and all closing sockets belong to run(), so stopping
// the reactor also releases them without a detached cleanup task.
let _ = self.closing_tcp.send(ClosingTcpSocket {
handle,
deadline,
_keep_alive: keep_alive,
});
}
pub fn context(&self) -> MappedMutexGuard<'_, Context> {
MutexGuard::map(self.iface.lock(), |iface| iface.context())
}
@@ -97,15 +97,6 @@ pub struct TcpStream {
}
impl TcpStream {
/// Release the application stream while the reactor finishes TCP teardown.
pub(crate) fn close(self, keep_alive: impl Send + 'static) {
self.reactor.close_tcp(
self.handle,
Box::new(keep_alive),
smoltcp::time::Instant::now() + super::socket_allocator::TCP_TIMEOUT,
);
}
pub(super) async fn connect(
reactor: Arc<Reactor>,
local_endpoint: IpEndpoint,
@@ -340,21 +331,21 @@ impl UdpSocket {
#[cfg(test)]
mod tests {
use std::{sync::Arc, time::Duration as StdDuration};
use std::time::Duration as StdDuration;
use smoltcp::{
iface::Config,
phy::{DeviceCapabilities, Medium},
socket::tcp,
time::Duration,
wire::{HardwareAddress, Ipv4Address, Ipv4Packet, TcpControl, TcpPacket, TcpSeqNumber},
wire::{HardwareAddress, Ipv4Address, TcpControl, TcpSeqNumber},
};
use super::super::{
Net, NetConfig, channel_device,
test_utils::{TcpPackets, recv_tcp, recv_tcp_for_port},
};
use super::{TCP_LISTENER_MAX_PENDING, TcpListener, TcpStream};
use super::{TCP_LISTENER_MAX_PENDING, TcpListener};
const LISTEN_ADDR: Ipv4Address = Ipv4Address::new(10, 126, 126, 1);
const LISTEN_PORT: u16 = 34569;
@@ -426,136 +417,6 @@ mod tests {
.collect()
}
async fn connected_stream(
net: &Net,
ingress: &tokio::sync::mpsc::Sender<std::io::Result<Vec<u8>>>,
egress: &mut tokio::sync::mpsc::Receiver<Vec<u8>>,
) -> TcpStream {
let mut listener = net
.tcp_bind("10.126.126.1:34569".parse().unwrap())
.await
.unwrap();
let client_addr = Ipv4Address::new(10, 126, 126, 2);
let sequence = begin_handshake(ingress, egress, client_addr, 40000, 1000).await;
finish_handshake(ingress, client_addr, 40000, 1000, sequence).await;
tokio::time::timeout(StdDuration::from_secs(1), listener.accept())
.await
.unwrap()
.unwrap()
.0
}
#[tokio::test]
async fn closing_stream_retransmits_fin_and_retains_time_wait() {
let (net, ingress, mut egress) = test_net();
let stream = connected_stream(&net, &ingress, &mut egress).await;
let reactor = stream.reactor.clone();
let keep_alive = Arc::new(());
let retained = Arc::downgrade(&keep_alive);
stream.close(keep_alive);
let fin = recv_tcp(&mut egress).await;
assert_eq!(fin.control, TcpControl::Fin);
// Lose the first FIN. The application handle is gone, but the reactor
// must still retransmit the same sequence number.
let retransmit = tokio::time::timeout(StdDuration::from_secs(3), egress.recv())
.await
.unwrap()
.unwrap();
let ipv4 = Ipv4Packet::new_checked(&retransmit).unwrap();
let tcp = TcpPacket::new_checked(ipv4.payload()).unwrap();
assert!(tcp.fin());
assert_eq!(tcp.seq_number(), fin.sequence);
let client_addr = Ipv4Address::new(10, 126, 126, 2);
let peer_fin = PACKETS.fin(client_addr, 40000, TcpSeqNumber(1001), fin.sequence + 1);
ingress.send(Ok(peer_fin.clone())).await.unwrap();
let ack = recv_tcp(&mut egress).await;
assert_eq!(ack.control, TcpControl::None);
assert_eq!(ack.ack_number, Some(TcpSeqNumber(1002)));
assert!(retained.upgrade().is_some());
// Lose our final ACK. TIME-WAIT must acknowledge the repeated FIN.
ingress.send(Ok(peer_fin)).await.unwrap();
let ack = recv_tcp(&mut egress).await;
assert_eq!(ack.control, TcpControl::None);
assert_eq!(ack.ack_number, Some(TcpSeqNumber(1002)));
tokio::time::timeout(StdDuration::from_secs(15), async {
while retained.upgrade().is_some() {
tokio::time::sleep(StdDuration::from_millis(10)).await;
}
})
.await
.expect("TIME-WAIT must eventually release the socket and its leases");
assert_eq!(
reactor.socket_allocator().sockets().lock().iter().count(),
0
);
}
#[tokio::test]
async fn closing_stream_deadline_sends_reset_and_releases_socket() {
let (net, ingress, mut egress) = test_net();
let stream = connected_stream(&net, &ingress, &mut egress).await;
let reactor = stream.reactor.clone();
let keep_alive = Arc::new(());
let retained = Arc::downgrade(&keep_alive);
// Exercise the same reactor deadline with a short test budget.
reactor.close_tcp(
stream.handle,
Box::new(keep_alive),
smoltcp::time::Instant::now() + Duration::from_millis(150),
);
let fin = recv_tcp(&mut egress).await;
assert_eq!(fin.control, TcpControl::Fin);
ingress
.send(Ok(PACKETS.ack(
Ipv4Address::new(10, 126, 126, 2),
40000,
TcpSeqNumber(1001),
fin.sequence + 1,
)))
.await
.unwrap();
// The peer ACKs our FIN but never closes its own half (FIN-WAIT-2).
let reset = recv_tcp(&mut egress).await;
assert_eq!(reset.control, TcpControl::Rst);
tokio::time::timeout(StdDuration::from_secs(1), async {
while retained.upgrade().is_some() {
tokio::task::yield_now().await;
}
})
.await
.unwrap();
assert_eq!(
reactor.socket_allocator().sockets().lock().iter().count(),
0
);
}
#[tokio::test]
async fn stopping_reactor_releases_closing_streams() {
let (net, ingress, mut egress) = test_net();
let stream = connected_stream(&net, &ingress, &mut egress).await;
let reactor = stream.reactor.clone();
let keep_alive = Arc::new(());
let retained = Arc::downgrade(&keep_alive);
stream.close(keep_alive);
assert_eq!(recv_tcp(&mut egress).await.control, TcpControl::Fin);
drop(net);
tokio::time::timeout(StdDuration::from_secs(1), async {
while retained.upgrade().is_some() {
tokio::task::yield_now().await;
}
})
.await
.expect("stopping the reactor must release unfinished TCP teardown");
assert_eq!(
reactor.socket_allocator().sockets().lock().iter().count(),
0
);
}
#[tokio::test]
async fn duplicate_syn_does_not_consume_another_backlog_slot() {
let (net, ingress, mut egress) = test_net();
@@ -114,7 +114,6 @@ pub(crate) struct TcpPacketSummary {
pub(crate) control: TcpControl,
pub(crate) sequence: TcpSeqNumber,
pub(crate) dst_port: u16,
pub(crate) ack_number: Option<TcpSeqNumber>,
}
pub(crate) async fn recv_tcp(receiver: &mut mpsc::Receiver<Vec<u8>>) -> TcpPacketSummary {
@@ -134,7 +133,6 @@ pub(crate) async fn recv_tcp(receiver: &mut mpsc::Receiver<Vec<u8>>) -> TcpPacke
control: repr.control,
sequence: repr.seq_number,
dst_port: repr.dst_port,
ack_number: repr.ack_number,
}
}
-6
View File
@@ -133,8 +133,6 @@ pub struct TestHost {
Option<tokio::sync::mpsc::UnboundedSender<(SocketAddr, tokio::io::DuplexStream)>>,
pub tcp_listeners: Option<Arc<Mutex<TestTcpListeners>>>,
pub reject_tcp_bind: Mutex<Option<SocketAddr>>,
pub tcp_bind_gate: Option<Arc<tokio::sync::Semaphore>>,
pub tcp_bind_runtime: Mutex<Option<tokio::runtime::Id>>,
pub reject_socks5_listener: bool,
}
@@ -181,10 +179,6 @@ impl VirtualTcpListenerFactory for TestHost {
async fn bind_tcp(&self, options: TcpListenOptions) -> anyhow::Result<Arc<Self::Listener>> {
self.tcp_binds.fetch_add(1, Ordering::Relaxed);
*self.tcp_bind_runtime.lock().unwrap() = Some(tokio::runtime::Handle::current().id());
if let Some(gate) = &self.tcp_bind_gate {
gate.acquire().await.unwrap().forget();
}
if self.reject_socks5_listener && options.purpose == TcpListenPurpose::Socks5 {
anyhow::bail!("test host rejected SOCKS5 listener");
}
-31
View File
@@ -92,16 +92,6 @@ impl Default for CoreInstanceHostConfig {
}
impl CoreInstanceHostConfig {
/// Record whether this host can serve the TCP subnet proxy from a user-space TCP stack.
///
/// Hosts without an OS TUN device (WASI guests, sandboxes) can only forward proxied TCP
/// through such a stack, so they must advertise it for the proxy runtime to pick that path.
/// `requires_smoltcp` is sticky: once the host needs the stack it keeps needing it.
pub fn set_smoltcp_stack(&mut self, available: bool) {
self.smoltcp_available = available;
self.requires_smoltcp |= available;
}
pub(crate) fn accepts_runtime_url(&self, url: &url::Url) -> bool {
!self.ignore_unsupported_config
|| self
@@ -775,24 +765,3 @@ data_compress_algo = "Zstd"
);
}
}
#[cfg(test)]
mod host_capability_tests {
use super::CoreInstanceHostConfig;
#[test]
fn set_smoltcp_stack_marks_the_host_as_capable_and_dependent() {
let mut config = CoreInstanceHostConfig::default();
config.set_smoltcp_stack(true);
assert!(config.smoltcp_available);
assert!(config.requires_smoltcp);
}
#[test]
fn set_smoltcp_stack_does_not_claim_a_missing_stack() {
let mut config = CoreInstanceHostConfig::default();
config.set_smoltcp_stack(false);
assert!(!config.smoltcp_available);
assert!(!config.requires_smoltcp);
}
}
+4 -13
View File
@@ -82,23 +82,14 @@ where
}
#[cfg(feature = "proxy-packet")]
pub(super) async fn start_packet_proxy(&self) -> anyhow::Result<()> {
async fn start_packet_proxy(&self) -> anyhow::Result<()> {
let config = self.runtime_config.snapshot();
let has_proxy_networks = !config.peer.runtime.core.routes.proxy_networks.is_empty();
if config.services.proxy.should_start(has_proxy_networks) {
if let Some(runtime) = self.runtime_handle() {
let proxy = self.packet_proxy.clone();
tokio_util::task::AbortOnDropHandle::new(
runtime.spawn(async move { proxy.start().await }),
)
self.packet_proxy
.start()
.await
.map_err(|error| anyhow::anyhow!("packet proxy start task failed: {error}"))??;
} else {
self.packet_proxy
.start()
.await
.map_err(anyhow::Error::new)?;
}
.map_err(anyhow::Error::new)?;
}
Ok(())
}
+2 -9
View File
@@ -10,7 +10,7 @@ mod management;
mod management_extension;
mod management_state;
pub mod manager;
pub(crate) mod packet_io;
mod packet_io;
mod packet_plane;
#[cfg(feature = "proxy-packet")]
mod packet_proxy_extension;
@@ -531,7 +531,6 @@ where
public_ipv6_host,
public_ipv6_events,
);
let running_listeners = Arc::new(RunningListenerRegistry::default());
let stun = (connectivity_mode == CoreConnectivityMode::Full)
.then(|| Self::prepare_stun(&adapters, &config.connectivity));
let (peer_stun, foreign_rpc_registrar): (
@@ -543,7 +542,6 @@ where
Arc::new(ForeignDirectConnectorRpcRegistrar::new(
adapters.host.clone(),
stun.clone(),
running_listeners.clone(),
)),
),
None => (Arc::new(()), Arc::new(())),
@@ -635,6 +633,7 @@ where
accepted_tunnel_handler.clone(),
)),
};
let running_listeners = Arc::new(RunningListenerRegistry::default());
let PreparedListenerPlan {
transports,
external,
@@ -988,12 +987,6 @@ where
if let Some(cleanup) = cleanup {
let _ = cleanup.await;
}
#[cfg(feature = "proxy-packet")]
if self.state() == CoreInstanceState::Running && self.startup_plan.packet_proxy {
// A running instance may gain its first proxy network here. Start
// from the published configuration, just as during initial startup.
self.start_packet_proxy().await?;
}
Ok(())
}
-27
View File
@@ -81,37 +81,10 @@ where
.evict_idle_sessions(idle);
}
#[doc(hidden)]
pub async fn foreign_network_has_gateway_for_test(
&self,
network_name: &str,
peer_id: crate::config::PeerId,
) -> Option<bool> {
self.peer_manager
.foreign_network_has_gateway(network_name, peer_id)
.await
}
#[doc(hidden)]
pub fn evict_unused_peer_sessions_for_test(&self, idle: Duration) {
self.peer_manager
.get_peer_session_store()
.evict_unused_sessions_idle(idle);
}
#[doc(hidden)]
pub fn foreign_forward_metered_bytes_for_test(&self, network_name: &str) -> Option<u64> {
self.peer_manager
.foreign_forward_metered_bytes(network_name)
}
#[doc(hidden)]
pub fn network_name_for_test(&self) -> String {
self.peer_manager.network_name().to_owned()
}
#[doc(hidden)]
pub fn instance_recv_metered_bytes_for_test(&self) -> Option<u64> {
self.peer_manager.instance_recv_metered_bytes()
}
}
-393
View File
@@ -3487,353 +3487,6 @@ virtual_ip = "10.82.0.2/24"
instance.stop().await;
}
#[cfg(feature = "proxy-packet")]
mod dynamic_packet_proxy {
use std::net::Ipv4Addr;
use smoltcp::wire::{Icmpv4Message, Icmpv4Packet, IpProtocol, Ipv4Packet};
use super::*;
use crate::{
gateway::proxy::icmp_host::{IcmpProxyHost, IcmpProxySocket, ProxyRuntimeError},
packet::{PacketType, ZCPacket},
};
struct RecordingIcmpHost {
opens: AtomicUsize,
requests: tokio::sync::mpsc::UnboundedSender<Ipv4Addr>,
}
#[async_trait]
impl IcmpProxyHost for RecordingIcmpHost {
async fn open_icmp_v4(
&self,
_context: SocketContext,
) -> Result<Arc<dyn IcmpProxySocket>, ProxyRuntimeError> {
self.opens.fetch_add(1, Ordering::Relaxed);
Ok(Arc::new(self.requests.clone()))
}
}
#[async_trait]
impl IcmpProxySocket for tokio::sync::mpsc::UnboundedSender<Ipv4Addr> {
async fn send(
&self,
destination: Ipv4Addr,
_packet: &[u8],
) -> Result<(), ProxyRuntimeError> {
self.send(destination).unwrap();
Ok(())
}
async fn recv(&self) -> Result<(IpAddr, Vec<u8>), ProxyRuntimeError> {
std::future::pending().await
}
}
fn echo_request(source_peer: u32, destination_peer: u32) -> ZCPacket {
let mut bytes = [0_u8; 28];
let mut icmp = Icmpv4Packet::new_unchecked(&mut bytes[20..]);
icmp.set_msg_type(Icmpv4Message::EchoRequest);
icmp.set_echo_ident(7);
icmp.set_echo_seq_no(1);
icmp.fill_checksum();
let mut ipv4 = Ipv4Packet::new_unchecked(&mut bytes[..]);
ipv4.set_version(4);
ipv4.set_header_len(20);
ipv4.set_total_len(28);
ipv4.set_hop_limit(64);
ipv4.set_next_header(IpProtocol::Icmp);
ipv4.set_src_addr("10.0.0.2".parse().unwrap());
ipv4.set_dst_addr("198.51.100.42".parse().unwrap());
ipv4.fill_checksum();
let mut packet = ZCPacket::new_with_payload(&bytes);
packet.fill_peer_manager_hdr(source_peer, destination_peer, PacketType::Data as u8);
packet
}
#[tokio::test]
async fn first_dynamic_proxy_network_starts_forwarding_and_reuses_resources() {
let mut config = test_config("dynamic-packet-proxy");
config.peer.snapshot.runtime.core.routes.ipv4 = Some(IpPrefix {
address: "10.0.0.1".parse().unwrap(),
prefix_len: 24,
});
let mut updated = runtime_snapshot(&config);
let host = Arc::new(TestHost::default());
let (packet_sink, mut packets) = tokio::sync::mpsc::channel(16);
let (requests, mut destinations) = tokio::sync::mpsc::unbounded_channel();
let icmp_host = Arc::new(RecordingIcmpHost {
opens: AtomicUsize::new(0),
requests,
});
let mut adapters = adapters_with_host(host.clone(), None, Arc::new(packet_sink));
adapters.icmp_proxy_host = Some(icmp_host.clone());
let instance = CoreInstance::new(config.clone(), adapters).unwrap();
config
.peer
.snapshot
.runtime
.core
.routes
.ipv4
.as_mut()
.unwrap()
.address = "10.0.0.2".parse().unwrap();
let source = build_instance(config).unwrap();
instance.start().await.unwrap();
source.start().await.unwrap();
assert_eq!(host.tcp_binds.load(Ordering::Relaxed), 0);
assert_eq!(icmp_host.opens.load(Ordering::Relaxed), 0);
let (client, server) = crate::tunnel::ring::create_ring_tunnel_pair();
let (client_result, server_result) = tokio::join!(
source.peer_manager.add_client_tunnel(client),
instance.peer_manager.add_tunnel_as_server(server),
);
client_result.unwrap();
server_result.unwrap();
let send = || async {
source
.peer_manager
.send_msg_for_proxy(
echo_request(source.peer_id(), instance.peer_id()),
instance.peer_id(),
)
.await
.unwrap();
};
send().await;
tokio::time::timeout(Duration::from_secs(5), packets.recv())
.await
.unwrap()
.unwrap();
assert_eq!(icmp_host.opens.load(Ordering::Relaxed), 0);
Arc::make_mut(&mut updated.peer)
.runtime
.core
.routes
.proxy_networks = vec![proxy_network("192.0.2.0/24", Some("198.51.100.0/24"))];
instance
.update_runtime_config(updated.clone())
.await
.unwrap();
assert_eq!(host.tcp_binds.load(Ordering::Relaxed), 1);
assert_eq!(icmp_host.opens.load(Ordering::Relaxed), 0);
send().await;
assert_eq!(
tokio::time::timeout(Duration::from_secs(5), destinations.recv())
.await
.unwrap(),
Some("192.0.2.42".parse().unwrap()),
);
let mut removed = updated.clone();
Arc::make_mut(&mut removed.peer)
.runtime
.core
.routes
.proxy_networks
.clear();
instance.update_runtime_config(removed).await.unwrap();
send().await;
tokio::time::timeout(Duration::from_secs(5), packets.recv())
.await
.unwrap()
.unwrap();
assert!(destinations.try_recv().is_err());
instance.update_runtime_config(updated).await.unwrap();
send().await;
assert_eq!(
tokio::time::timeout(Duration::from_secs(5), destinations.recv())
.await
.unwrap(),
Some("192.0.2.42".parse().unwrap()),
);
assert_eq!(host.tcp_binds.load(Ordering::Relaxed), 1);
assert_eq!(icmp_host.opens.load(Ordering::Relaxed), 1);
source.stop().await;
instance.stop().await;
assert_eq!(host.active_tcp_listeners.load(Ordering::Relaxed), 0);
}
#[tokio::test]
async fn dynamic_proxy_start_respects_instance_state_and_system_forwarding() {
for forward_by_system in [false, true] {
let mut config = test_config("dynamic-proxy-start-policy");
config.connectivity.runtime.proxy.forward_by_system = forward_by_system;
let mut updated = runtime_snapshot(&config);
let host = Arc::new(TestHost::default());
let (packet_sink, _packets) = tokio::sync::mpsc::channel(16);
let instance = CoreInstance::new(
config,
adapters_with_host(host.clone(), None, Arc::new(packet_sink)),
)
.unwrap();
Arc::make_mut(&mut updated.peer)
.runtime
.core
.routes
.proxy_networks = vec![proxy_network("192.0.2.0/24", None)];
instance
.update_runtime_config(updated.clone())
.await
.unwrap();
assert_eq!(host.tcp_binds.load(Ordering::Relaxed), 0);
instance.start().await.unwrap();
instance.update_runtime_config(updated).await.unwrap();
assert_eq!(
host.tcp_binds.load(Ordering::Relaxed),
usize::from(!forward_by_system),
);
instance.stop().await;
}
}
#[tokio::test]
async fn dynamic_proxy_start_reports_bind_failure_and_allows_retry() {
let config = test_config("dynamic-proxy-start-failure");
let mut updated = runtime_snapshot(&config);
let host = Arc::new(TestHost::default());
*host.reject_tcp_bind.lock().unwrap() = Some("0.0.0.0:20000".parse().unwrap());
let (packet_sink, _packets) = tokio::sync::mpsc::channel(16);
let instance = CoreInstance::new(
config,
adapters_with_host(host.clone(), None, Arc::new(packet_sink)),
)
.unwrap();
instance.start().await.unwrap();
Arc::make_mut(&mut updated.peer)
.runtime
.core
.routes
.proxy_networks = vec![proxy_network("192.0.2.0/24", None)];
let error = instance
.update_runtime_config(updated.clone())
.await
.unwrap_err();
assert!(error.to_string().contains("test host rejected TCP bind"));
assert_eq!(host.active_tcp_listeners.load(Ordering::Relaxed), 0);
// Configuration publication precedes resource startup. Retrying the
// same update must activate the already published proxy network.
assert_eq!(instance.node_snapshot().await.proxy_networks.len(), 1);
*host.reject_tcp_bind.lock().unwrap() = None;
instance.update_runtime_config(updated).await.unwrap();
assert_eq!(host.active_tcp_listeners.load(Ordering::Relaxed), 1);
instance.stop().await;
assert_eq!(host.active_tcp_listeners.load(Ordering::Relaxed), 0);
}
#[tokio::test]
async fn cancelled_dynamic_proxy_start_allows_retry() {
let config = test_config("dynamic-proxy-start-cancelled");
let mut updated = runtime_snapshot(&config);
let gate = Arc::new(tokio::sync::Semaphore::new(0));
let host = Arc::new(TestHost {
tcp_bind_gate: Some(gate.clone()),
..Default::default()
});
let (packet_sink, _packets) = tokio::sync::mpsc::channel(16);
let instance = CoreInstance::new(
config,
adapters_with_host(host.clone(), None, Arc::new(packet_sink)),
)
.unwrap();
instance.start().await.unwrap();
Arc::make_mut(&mut updated.peer)
.runtime
.core
.routes
.proxy_networks = vec![proxy_network("192.0.2.0/24", None)];
let update = tokio::spawn({
let instance = instance.clone();
let updated = updated.clone();
async move { instance.update_runtime_config(updated).await }
});
tokio::time::timeout(Duration::from_secs(5), async {
while host.tcp_binds.load(Ordering::Relaxed) == 0 {
tokio::task::yield_now().await;
}
})
.await
.unwrap();
update.abort();
assert!(update.await.unwrap_err().is_cancelled());
assert_eq!(host.active_tcp_listeners.load(Ordering::Relaxed), 0);
gate.add_permits(1);
instance.update_runtime_config(updated).await.unwrap();
assert_eq!(host.tcp_binds.load(Ordering::Relaxed), 2);
assert_eq!(host.active_tcp_listeners.load(Ordering::Relaxed), 1);
instance.stop().await;
assert_eq!(host.active_tcp_listeners.load(Ordering::Relaxed), 0);
}
#[test]
fn dynamic_proxy_start_uses_instance_executor_after_caller_runtime_exits() {
struct ExecutorRuntimeHost(tokio::runtime::Handle);
#[async_trait]
impl InstanceRuntimeHost for ExecutorRuntimeHost {
fn runtime_handle(&self) -> Option<tokio::runtime::Handle> {
Some(self.0.clone())
}
async fn prepare(
&self,
_packet_plane: Arc<CorePacketPlane>,
) -> anyhow::Result<Option<Arc<dyn DhcpHost>>> {
Ok(None)
}
async fn shutdown(&self) {}
}
let executor = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build()
.unwrap();
let caller = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let host = Arc::new(TestHost::default());
let config = test_config("dynamic-proxy-executor");
let mut updated = runtime_snapshot(&config);
let instance = {
let _runtime = executor.enter();
let (packet_sink, _packets) = tokio::sync::mpsc::channel(16);
let mut adapters = adapters_with_host(host.clone(), None, Arc::new(packet_sink));
adapters.instance_runtime =
Arc::new(ExecutorRuntimeHost(executor.handle().clone()));
CoreInstance::new(config, adapters).unwrap()
};
caller.block_on(async {
instance.start().await.unwrap();
assert_eq!(host.tcp_binds.load(Ordering::Relaxed), 0);
Arc::make_mut(&mut updated.peer)
.runtime
.core
.routes
.proxy_networks = vec![proxy_network("192.0.2.0/24", None)];
instance.update_runtime_config(updated).await.unwrap();
});
drop(caller);
assert_eq!(
*host.tcp_bind_runtime.lock().unwrap(),
Some(executor.handle().id()),
);
assert_eq!(host.active_tcp_listeners.load(Ordering::Relaxed), 1);
executor.block_on(instance.stop());
assert_eq!(host.active_tcp_listeners.load(Ordering::Relaxed), 0);
}
}
#[cfg(feature = "dhcp-ipv4")]
#[tokio::test]
async fn runtime_core_accepts_explicit_dhcp_runtime_snapshot() {
@@ -4094,52 +3747,6 @@ virtual_ip = "10.82.0.2/24"
assert_eq!(state.drop_calls.load(Ordering::Relaxed), 1);
}
#[tokio::test]
async fn foreign_direct_rpc_tracks_running_listeners() {
use crate::{
connectivity::direct::DirectConnectorRpcHandler,
proto::{
peer_rpc::{DirectConnectorRpc, GetIpListRequest},
rpc_types::controller::BaseController,
},
};
let mut config = test_config("foreign-listeners");
config.connectivity.listeners = Some(ListenerRuntimeConfig::new(
vec!["tcp://0.0.0.0:0".parse().unwrap()],
false,
SocketContext::default(),
));
let instance = build_instance(config).unwrap();
let handler = DirectConnectorRpcHandler::new_for_foreign_network_with_stun(
Arc::new(TestHost::default()),
instance.running_listeners.clone(),
SocketContext::default(),
None,
);
let advertised_listeners = || async {
handler
.get_ip_list(BaseController::default(), GetIpListRequest {})
.await
.unwrap()
.listeners
.into_iter()
.map(Url::from)
.collect::<Vec<_>>()
};
assert!(advertised_listeners().await.is_empty());
instance.start().await.unwrap();
assert!(
instance
.running_listeners()
.contains(&"tcp://0.0.0.0:20000".parse().unwrap())
);
assert_eq!(advertised_listeners().await, instance.running_listeners());
instance.stop().await;
assert!(advertised_listeners().await.is_empty());
}
#[tokio::test]
async fn external_listener_uses_core_running_listener_registry() {
let external_url: Url = "unix:///tmp/easytier-external-listener-test"
-3
View File
@@ -1,6 +1,3 @@
#[cfg(all(feature = "system-certificates", target_os = "android"))]
compile_error!("the system-certificates feature is not supported on Android");
pub mod config;
pub mod connectivity;
pub mod events;
+29 -28
View File
@@ -145,6 +145,7 @@ impl<Accepted> ListenerFactory<Accepted> {
#[derive(Debug, Clone)]
pub struct ListenerManagerOptions {
pub max_listen_retries: usize,
pub listen_retry_delay: Duration,
pub accept_retry_delay: Duration,
}
@@ -152,8 +153,9 @@ pub struct ListenerManagerOptions {
impl Default for ListenerManagerOptions {
fn default() -> Self {
Self {
listen_retry_delay: Duration::from_millis(100),
accept_retry_delay: Duration::from_millis(100),
max_listen_retries: 5,
listen_retry_delay: Duration::from_secs(1),
accept_retry_delay: Duration::from_secs(1),
}
}
}
@@ -359,7 +361,7 @@ async fn run_listener<Accepted, H>(
Accepted: Send + 'static,
H: AcceptedSocketHandler<Accepted> + ?Sized + 'static,
{
let mut listen_error_count: usize = 0;
let mut listen_error_count = 0;
loop {
let registered_listener = match initial_listener.take() {
Some(listener) => listener,
@@ -371,15 +373,18 @@ async fn run_listener<Accepted, H>(
RegisteredListener::new(listener, events.clone(), registry.clone())
}
Err(error) => {
listen_error_count = listen_error_count.saturating_add(1);
listen_error_count += 1;
let will_retry = listen_error_count <= options.max_listen_retries;
events.emit(CoreEvent::ListenerAddFailed {
url: listener.local_url(),
error: format!("{error:?}"),
retry_count: listen_error_count,
will_retry: true,
will_retry,
});
tracing::error!(?error, ?listener, "listener listen error");
drop(listener);
if !will_retry {
return;
}
crate::foundation::time::sleep(options.listen_retry_delay).await;
continue;
}
@@ -387,7 +392,7 @@ async fn run_listener<Accepted, H>(
}
};
let mut listener = registered_listener.listener;
let registration = registered_listener.registration;
let _registration = registered_listener.registration;
loop {
let listener_url = listener.local_url();
@@ -399,6 +404,7 @@ async fn run_listener<Accepted, H>(
error: format!("{error:?}"),
});
tracing::error!(?error, ?listener, "listener accept error");
crate::foundation::time::sleep(options.accept_retry_delay).await;
break;
}
};
@@ -417,9 +423,6 @@ async fn run_listener<Accepted, H>(
}
});
}
drop(listener);
drop(registration);
crate::foundation::time::sleep(options.accept_retry_delay).await;
}
}
@@ -648,6 +651,7 @@ mod tests {
ListenerManagerOptions {
accept_retry_delay: Duration::from_millis(1),
listen_retry_delay: Duration::from_millis(1),
max_listen_retries: 0,
},
);
@@ -700,6 +704,7 @@ mod tests {
ListenerManagerOptions {
accept_retry_delay: Duration::from_millis(1),
listen_retry_delay: Duration::from_millis(1),
max_listen_retries: 2,
},
);
@@ -746,6 +751,7 @@ mod tests {
ListenerManagerOptions {
accept_retry_delay: Duration::from_millis(1),
listen_retry_delay: Duration::from_millis(1),
max_listen_retries: 0,
},
);
@@ -809,6 +815,7 @@ mod tests {
ListenerManagerOptions {
accept_retry_delay: Duration::from_millis(1),
listen_retry_delay: Duration::from_millis(1),
max_listen_retries: 0,
},
);
@@ -929,6 +936,7 @@ mod tests {
ListenerManagerOptions {
accept_retry_delay: Duration::from_millis(1),
listen_retry_delay: Duration::from_millis(1),
max_listen_retries: 0,
},
);
let accepts = Arc::new(Mutex::new(VecDeque::from([Ok::<_, anyhow::Error>(
@@ -991,13 +999,12 @@ mod tests {
}
#[tokio::test]
async fn listen_recovery_keeps_in_flight_handler_tasks_until_manager_drop() {
async fn listen_retry_exhaustion_keeps_in_flight_handler_tasks_until_manager_drop() {
let (drop_tx, mut drop_rx) = tokio::sync::oneshot::channel();
let listen_results = Arc::new(Mutex::new(
std::iter::once(Ok(()))
.chain((0..8).map(|_| Err(anyhow::anyhow!("bind failed"))))
.collect::<VecDeque<_>>(),
));
let listen_results = Arc::new(Mutex::new(VecDeque::from([
Ok(()),
Err(anyhow::anyhow!("bind failed")),
])));
let accepts = Arc::new(Mutex::new(VecDeque::from([
Ok::<_, anyhow::Error>(DropSignal(Some(drop_tx))),
Err(anyhow::anyhow!("accept failed")),
@@ -1009,6 +1016,7 @@ mod tests {
ListenerManagerOptions {
accept_retry_delay: Duration::from_millis(1),
listen_retry_delay: Duration::from_millis(1),
max_listen_retries: 0,
},
);
let listener_results = listen_results.clone();
@@ -1016,7 +1024,7 @@ mod tests {
manager.add_listener(
move || {
Box::new(DropSignalListener {
url: "mock://retrying".parse().unwrap(),
url: "mock://retry-exhausted".parse().unwrap(),
listen_results: listener_results.clone(),
accepts: listener_accepts.clone(),
})
@@ -1027,22 +1035,15 @@ mod tests {
manager.run().await.unwrap();
crate::foundation::time::timeout(Duration::from_secs(1), async {
loop {
let recovered = {
let events = events.events.lock().unwrap();
assert!(events.iter().all(|event| !matches!(
if events.events.lock().unwrap().iter().any(|event| {
matches!(
event,
CoreEvent::ListenerAddFailed {
will_retry: false,
..
}
)));
events
.iter()
.filter(|event| matches!(event, CoreEvent::ListenerAdded { .. }))
.count()
== 2
};
if recovered {
)
}) {
break;
}
crate::foundation::time::sleep(Duration::from_millis(1)).await;
-204
View File
@@ -1,204 +0,0 @@
//! Local IP flow metadata for unfragmented TCP and UDP packets.
use std::net::SocketAddr;
use super::{
ZCPacket,
ip::{parse_ip_packet, parse_transport_ports},
};
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct FlowKey {
pub src: SocketAddr,
pub dst: SocketAddr,
pub protocol: u8,
}
pub(crate) fn parse_flow_key(packet: &ZCPacket) -> Option<FlowKey> {
let parsed = parse_ip_packet(packet.payload())?;
// Skip every actual fragment, including the first one with ports.
if parsed.is_fragment || !parsed.has_valid_length {
return None;
}
let (src_port, dst_port) = parse_transport_ports(parsed.protocol, parsed.transport_payload)?;
Some(FlowKey {
src: SocketAddr::new(parsed.src_ip, src_port?),
dst: SocketAddr::new(parsed.dst_ip, dst_port?),
protocol: parsed.protocol,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::packet::{PacketType, ZCPacketType};
fn ipv4(protocol: u8, flags: u16, id: u16) -> ZCPacket {
let len = if protocol == 6 { 40 } else { 28 };
let mut bytes = vec![0; len];
bytes[0] = 0x45;
bytes[2..4].copy_from_slice(&(len as u16).to_be_bytes());
bytes[4..6].copy_from_slice(&id.to_be_bytes());
bytes[6..8].copy_from_slice(&flags.to_be_bytes());
bytes[9] = protocol;
bytes[12..16].copy_from_slice(&[10, 0, 0, 1]);
bytes[16..20].copy_from_slice(&[10, 0, 0, 2]);
bytes[20..24].copy_from_slice(&[0x12, 0x34, 0x56, 0x78]);
packet(bytes)
}
fn packet(bytes: Vec<u8>) -> ZCPacket {
let mut packet = ZCPacket::new_with_payload(&bytes);
packet.fill_peer_manager_hdr(1, 2, PacketType::Data as u8);
packet
}
fn ipv6(fragment: Option<u16>) -> ZCPacket {
let mut bytes = vec![0; if fragment.is_some() { 64 } else { 56 }];
bytes[0] = 0x60;
let len = (bytes.len() - 40) as u16;
bytes[4..6].copy_from_slice(&len.to_be_bytes());
bytes[6] = 0; // Hop-by-hop options before the transport/fragment header.
bytes[23] = 1;
bytes[39] = 2;
bytes[40] = if fragment.is_some() { 44 } else { 17 };
let offset = if let Some(flags) = fragment {
bytes[48] = 17;
bytes[50..52].copy_from_slice(&flags.to_be_bytes());
bytes[52..56].copy_from_slice(&99u32.to_be_bytes());
56
} else {
48
};
bytes[offset..offset + 4].copy_from_slice(&[0x12, 0x34, 0x56, 0x78]);
packet(bytes)
}
#[test]
fn identifies_ipv4_tcp_udp_and_ipv6_extension_headers() {
let udp = parse_flow_key(&ipv4(17, 0, 0)).unwrap();
assert_eq!(udp.src, "10.0.0.1:4660".parse().unwrap());
assert_eq!(udp.dst, "10.0.0.2:22136".parse().unwrap());
let tcp = parse_flow_key(&ipv4(6, 0, 0)).unwrap();
assert_eq!(tcp.src, udp.src);
assert_eq!(tcp.dst, udp.dst);
assert_eq!(tcp.protocol, 6);
assert_ne!(udp, tcp);
assert_eq!(Some(udp), parse_flow_key(&ipv4(17, 0x4000, 0)));
let v6 = parse_flow_key(&ipv6(None)).unwrap();
assert_eq!(v6.src, "[::1]:4660".parse().unwrap());
assert_eq!(Some(v6), parse_flow_key(&ipv6(Some(0))));
}
#[test]
fn first_middle_and_last_fragments_do_not_supply_a_flow_key() {
for protocol in [6, 17, 1, 50] {
for flags in [0x2000, 0x2001, 1] {
assert!(parse_flow_key(&ipv4(protocol, flags, 99)).is_none());
}
}
for protocol in [6, 17, 58, 50] {
for flags in [1, 9, 8] {
let mut fragment = ipv6(Some(flags));
fragment.mut_payload()[48] = protocol;
assert!(parse_flow_key(&fragment).is_none());
}
}
}
#[test]
fn flow_key_survives_local_conversions_but_is_not_on_the_wire() {
let mut original = ipv4(17, 0, 0);
original.flow_key = parse_flow_key(&original);
let key = original.flow_key;
assert!(key.is_some());
let mut converted = original.clone();
for kind in [
ZCPacketType::TCP,
ZCPacketType::UDP,
ZCPacketType::DummyTunnel,
ZCPacketType::TCP,
] {
converted = converted.convert_type(kind);
assert_eq!(converted.flow_key, key);
assert_eq!(converted.payload(), original.payload());
}
let received = ZCPacket::new_from_buf(converted.inner(), ZCPacketType::TCP);
assert!(received.flow_key.is_none());
let wrapped = ZCPacket::new_for_foreign_network(&"network".to_owned(), 2, &original);
assert_eq!(wrapped.flow_key, key);
let unwrapped = wrapped.foreign_network_packet();
assert_eq!(unwrapped.flow_key, key);
assert_eq!(unwrapped.payload(), original.payload());
}
#[test]
fn truncated_headers_never_supply_a_flow_key() {
for original in [ipv4(17, 0, 0), ipv6(None), ipv6(Some(0))] {
for len in 0..original.payload().len() {
assert!(parse_flow_key(&packet(original.payload()[..len].to_vec())).is_none());
}
}
}
#[test]
fn invalid_ip_header_lengths_do_not_supply_a_flow_key() {
for ihl in [0, 4, 15] {
let mut packet = ipv4(17, 0, 0);
packet.mut_payload()[0] = 0x40 | ihl;
assert!(parse_flow_key(&packet).is_none());
}
}
#[test]
fn incomplete_transport_headers_do_not_supply_a_flow_key() {
for (protocol, header_len) in [(6, 20), (17, 8)] {
for len in 0..header_len {
let mut v4 = ipv4(protocol, 0, 0);
let offset = v4.payload_offset();
v4.mut_inner().truncate(offset + 20 + len);
v4.mut_payload()[2..4].copy_from_slice(&((20 + len) as u16).to_be_bytes());
assert!(parse_flow_key(&v4).is_none());
let mut v6 = ipv6(None);
let offset = v6.payload_offset();
v6.mut_inner().resize(offset + 48 + len, 0);
v6.mut_payload()[4..6].copy_from_slice(&((8 + len) as u16).to_be_bytes());
v6.mut_payload()[40] = protocol;
assert!(parse_flow_key(&v6).is_none());
}
}
}
#[cfg(all(feature = "zstd", feature = "aes-gcm"))]
#[tokio::test]
async fn compression_and_encryption_preserve_local_flow_metadata() {
use crate::packet::{
CompressorAlgo,
compressor::{Compressor, DefaultCompressor},
};
let mut original = ipv4(17, 0, 0);
let offset = original.payload_offset();
original.mut_inner().resize(offset + 1024, 0);
original.mut_payload()[2..4].copy_from_slice(&1024u16.to_be_bytes());
original.fill_peer_manager_hdr(1, 2, PacketType::Data as u8);
let mut packet = original.clone();
packet.flow_key = parse_flow_key(&packet);
let key = packet.flow_key;
assert!(key.is_some());
let compressor = DefaultCompressor::new();
compressor
.compress(&mut packet, CompressorAlgo::ZstdDefault)
.await
.unwrap();
assert!(packet.peer_manager_header().unwrap().is_compressed());
let encryptor = crate::tunnel::encrypt::create_encryptor("aes-gcm", [1; 16], [2; 32]);
encryptor.encrypt(&mut packet).unwrap();
assert_eq!(packet.flow_key, key);
let mut packet = packet.convert_type(ZCPacketType::UDP);
encryptor.decrypt(&mut packet).unwrap();
compressor.decompress(&mut packet).await.unwrap();
assert_eq!(packet.flow_key, key);
assert_eq!(packet.payload(), original.payload());
}
}
-194
View File
@@ -1,194 +0,0 @@
//! Shared IP and transport-header parsing for packet consumers.
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
pub(crate) const IP_PROTO_ICMP: u8 = 1;
pub(crate) const IP_PROTO_TCP: u8 = 6;
pub(crate) const IP_PROTO_UDP: u8 = 17;
pub(crate) const IP_PROTO_ICMPV6: u8 = 58;
pub(crate) const IP_PROTO_UNSPECIFIED: u8 = u8::MAX;
pub(crate) const IPV6_HOP_BY_HOP: u8 = 0;
pub(crate) const IPV6_ROUTING: u8 = 43;
pub(crate) const IPV6_FRAGMENT: u8 = 44;
pub(crate) const IPV6_AUTHENTICATION: u8 = 51;
pub(crate) const IPV6_DESTINATION_OPTIONS: u8 = 60;
#[derive(Clone, Copy)]
pub(crate) struct ParsedIpPacket<'a> {
pub src_ip: IpAddr,
pub dst_ip: IpAddr,
pub protocol: u8,
pub transport_payload: &'a [u8],
pub is_non_initial_fragment: bool,
pub is_fragment: bool,
/// Whether the IP header and declared packet length fit in the input.
pub has_valid_length: bool,
}
pub(crate) fn parse_ip_packet(payload: &[u8]) -> Option<ParsedIpPacket<'_>> {
let version = payload.first()? >> 4;
match version {
4 => parse_ipv4_packet(payload),
6 => parse_ipv6_packet(payload),
_ => None,
}
}
fn parse_ipv4_packet(payload: &[u8]) -> Option<ParsedIpPacket<'_>> {
if payload.len() < 20 {
return None;
}
let header_len = usize::from(payload[0] & 0x0f) * 4;
let options_len = header_len.saturating_sub(20);
let payload_offset = 20 + options_len;
let payload_start = payload_offset.min(payload.len());
let total_length = usize::from(u16::from_be_bytes([payload[2], payload[3]]));
let payload_len = total_length.saturating_sub(header_len);
let payload_end = payload_start.saturating_add(payload_len).min(payload.len());
Some(ParsedIpPacket {
src_ip: IpAddr::V4(Ipv4Addr::new(
payload[12],
payload[13],
payload[14],
payload[15],
)),
dst_ip: IpAddr::V4(Ipv4Addr::new(
payload[16],
payload[17],
payload[18],
payload[19],
)),
protocol: payload[9],
transport_payload: payload.get(payload_start..payload_end)?,
is_non_initial_fragment: u16::from_be_bytes([payload[6], payload[7]]) & 0x1fff != 0,
is_fragment: u16::from_be_bytes([payload[6], payload[7]]) & 0x3fff != 0,
has_valid_length: header_len >= 20
&& total_length >= header_len
&& total_length <= payload.len(),
})
}
fn parse_ipv6_packet(payload: &[u8]) -> Option<ParsedIpPacket<'_>> {
if payload.len() < 40 {
return None;
}
let payload_len = usize::from(u16::from_be_bytes([payload[4], payload[5]]));
// A valid base header still supplies the addresses needed for ACL rules,
// even when the upper-layer headers cannot be decoded.
let parsed = ParsedIpPacket {
src_ip: IpAddr::V6(Ipv6Addr::from(<[u8; 16]>::try_from(&payload[8..24]).ok()?)),
dst_ip: IpAddr::V6(Ipv6Addr::from(<[u8; 16]>::try_from(&payload[24..40]).ok()?)),
protocol: IP_PROTO_UNSPECIFIED,
transport_payload: &[],
is_non_initial_fragment: false,
is_fragment: false,
has_valid_length: 40 + payload_len <= payload.len(),
};
let payload_end = 40usize.saturating_add(payload_len).min(payload.len());
let mut protocol = payload[6];
let mut payload_offset = 40;
let mut is_non_initial_fragment = false;
let mut is_fragment = false;
let mut saw_fragment_header = false;
loop {
match protocol {
IPV6_HOP_BY_HOP | IPV6_ROUTING | IPV6_DESTINATION_OPTIONS => {
let header = &payload[payload_offset..payload_end];
let Some(&header_len) = header.get(1) else {
return Some(parsed);
};
let extension_len = (usize::from(header_len) + 1) * 8;
let next_protocol = header[0];
let extension_end = payload_offset + extension_len;
if extension_end > payload_end {
return Some(parsed);
}
protocol = next_protocol;
payload_offset = extension_end;
}
IPV6_AUTHENTICATION => {
let header = &payload[payload_offset..payload_end];
let Some(&header_len) = header.get(1) else {
return Some(parsed);
};
let extension_len = (usize::from(header_len) + 2) * 4;
let next_protocol = header[0];
let extension_end = payload_offset + extension_len;
if extension_end > payload_end {
return Some(parsed);
}
protocol = next_protocol;
payload_offset = extension_end;
}
IPV6_FRAGMENT => {
if saw_fragment_header {
return Some(parsed);
}
saw_fragment_header = true;
let fragment_end = payload_offset + 8;
if fragment_end > payload_end {
return Some(parsed);
}
let header = &payload[payload_offset..fragment_end];
let fragment_field = u16::from_be_bytes([header[2], header[3]]);
is_non_initial_fragment = fragment_field & 0xfff8 != 0;
// An atomic fragment has its complete transport header.
is_fragment = fragment_field & 0xfff9 != 0;
let next_protocol = header[0];
payload_offset = fragment_end;
protocol = next_protocol;
if is_non_initial_fragment {
// Extension headers after the Fragment Header are only in the first
// fragment. Do not interpret tail data as an extension header.
// Only ICMPv6 tails can use the response records without a
// transport header. Leave other tails to address/group ACLs.
if protocol != IP_PROTO_ICMPV6 {
protocol = IP_PROTO_UNSPECIFIED;
}
break;
}
}
_ => break,
}
}
let transport_payload = &payload[payload_offset..payload_end];
if payload_offset != 40 && parse_transport_ports(protocol, transport_payload).is_none() {
return Some(parsed);
}
Some(ParsedIpPacket {
protocol,
transport_payload,
is_non_initial_fragment,
is_fragment,
..parsed
})
}
pub(crate) fn parse_transport_ports(
protocol: u8,
payload: &[u8],
) -> Option<(Option<u16>, Option<u16>)> {
let min_len = match protocol {
IP_PROTO_TCP => 20,
IP_PROTO_UDP => 8,
_ => return Some((None, None)),
};
if payload.len() < min_len {
return None;
}
Some((
Some(u16::from_be_bytes([payload[0], payload[1]])),
Some(u16::from_be_bytes([payload[2], payload[3]])),
))
}
+4 -18
View File
@@ -1,11 +1,7 @@
pub(crate) mod compressor;
pub(crate) mod flow;
mod hole_punch;
pub(crate) mod ip;
pub mod stun;
pub use flow::FlowKey;
pub(crate) use hole_punch::{
HOLE_PUNCH_PACKET_BODY_LEN, hole_punch_packet_tid, new_hole_punch_packet,
};
@@ -543,8 +539,6 @@ impl ZCPacketType {
pub struct ZCPacket {
inner: BytesMut,
packet_type: ZCPacketType,
/// Local scheduling metadata; never serialized into a tunnel packet.
pub(crate) flow_key: Option<FlowKey>,
}
impl ZCPacket {
@@ -560,7 +554,6 @@ impl ZCPacket {
Self {
inner: BytesMut::new(),
packet_type: ZCPacketType::NIC,
flow_key: None,
}
}
@@ -568,7 +561,6 @@ impl ZCPacket {
Self {
inner: buf,
packet_type,
flow_key: None,
}
}
@@ -616,7 +608,6 @@ impl ZCPacket {
.copy_from_slice(foreign_zc_packet.tunnel_payload());
ret.fill_peer_manager_hdr(0, 0, PacketType::ForeignNetworkPacket as u8);
ret.flow_key = foreign_zc_packet.flow_key;
ret
}
@@ -792,14 +783,11 @@ impl ZCPacket {
let mut buf = BytesMut::with_capacity(new_pm_offset + tunnel_payload.len());
unsafe { buf.set_len(new_pm_offset) };
buf.extend_from_slice(tunnel_payload);
let mut packet = Self::new_from_buf(buf, target_packet_type);
packet.flow_key = self.flow_key;
return packet;
return Self::new_from_buf(buf, target_packet_type);
}
self.inner.advance(new_offset);
self.packet_type = target_packet_type;
self
Self::new_from_buf(self.inner, target_packet_type)
}
pub fn into_bytes(self) -> Bytes {
@@ -851,15 +839,13 @@ impl ZCPacket {
let hdr = self.foreign_network_hdr().unwrap();
let foreign_hdr_len = hdr.get_header_len();
let mut packet = Self::new_from_buf(
Self::new_from_buf(
{
self.inner.advance(foreign_hdr_len + self.payload_offset());
self.inner
},
ZCPacketType::DummyTunnel,
);
packet.flow_key = self.flow_key;
packet
)
}
pub fn get_src_peer_id(&self) -> Option<u32> {
+178 -15
View File
@@ -12,21 +12,36 @@ use quanta::Instant;
use tokio_util::task::AbortOnDropHandle;
use crate::{
packet::{
PacketType, ZCPacket,
ip::{
IP_PROTO_ICMP, IP_PROTO_ICMPV6, IP_PROTO_TCP, IP_PROTO_UDP, IP_PROTO_UNSPECIFIED,
ParsedIpPacket, parse_ip_packet, parse_transport_ports,
},
},
packet::{PacketType, ZCPacket},
peers::acl::processor::{AclProcessor, AclResult, AclStatKey, AclStatType, PacketInfo},
};
const IP_PROTO_ICMP: u8 = 1;
const IP_PROTO_TCP: u8 = 6;
const IP_PROTO_UDP: u8 = 17;
const IP_PROTO_ICMPV6: u8 = 58;
const IP_PROTO_UNSPECIFIED: u8 = u8::MAX;
const ICMP_ECHO_REPLY: u8 = 0;
const ICMP_ECHO_REQUEST: u8 = 8;
const ICMPV6_ECHO_REQUEST: u8 = 128;
const ICMPV6_ECHO_REPLY: u8 = 129;
const IPV6_HOP_BY_HOP: u8 = 0;
const IPV6_ROUTING: u8 = 43;
const IPV6_FRAGMENT: u8 = 44;
const IPV6_AUTHENTICATION: u8 = 51;
const IPV6_DESTINATION_OPTIONS: u8 = 60;
#[derive(Clone, Copy)]
struct ParsedIpPacket<'a> {
src_ip: IpAddr,
dst_ip: IpAddr,
protocol: u8,
transport_payload: &'a [u8],
is_non_initial_fragment: bool,
}
impl ParsedIpPacket<'_> {
fn can_use_allow_record(&self, is_in: bool) -> bool {
let expected_type = match (self.protocol, is_in) {
@@ -49,6 +64,159 @@ impl ParsedIpPacket<'_> {
}
}
fn parse_ip_packet(payload: &[u8]) -> Option<ParsedIpPacket<'_>> {
let version = payload.first()? >> 4;
match version {
4 => parse_ipv4_packet(payload),
6 => parse_ipv6_packet(payload),
_ => None,
}
}
fn parse_ipv4_packet(payload: &[u8]) -> Option<ParsedIpPacket<'_>> {
if payload.len() < 20 {
return None;
}
let header_len = usize::from(payload[0] & 0x0f) * 4;
let options_len = header_len.saturating_sub(20);
let payload_offset = 20 + options_len;
let payload_start = payload_offset.min(payload.len());
let total_length = usize::from(u16::from_be_bytes([payload[2], payload[3]]));
let payload_len = total_length.saturating_sub(header_len);
let payload_end = payload_start.saturating_add(payload_len).min(payload.len());
Some(ParsedIpPacket {
src_ip: IpAddr::V4(Ipv4Addr::new(
payload[12],
payload[13],
payload[14],
payload[15],
)),
dst_ip: IpAddr::V4(Ipv4Addr::new(
payload[16],
payload[17],
payload[18],
payload[19],
)),
protocol: payload[9],
transport_payload: payload.get(payload_start..payload_end)?,
is_non_initial_fragment: u16::from_be_bytes([payload[6], payload[7]]) & 0x1fff != 0,
})
}
fn parse_ipv6_packet(payload: &[u8]) -> Option<ParsedIpPacket<'_>> {
if payload.len() < 40 {
return None;
}
// A valid base header still supplies the addresses needed for ACL rules,
// even when the upper-layer headers cannot be decoded.
let parsed = ParsedIpPacket {
src_ip: IpAddr::V6(Ipv6Addr::from(<[u8; 16]>::try_from(&payload[8..24]).ok()?)),
dst_ip: IpAddr::V6(Ipv6Addr::from(<[u8; 16]>::try_from(&payload[24..40]).ok()?)),
protocol: IP_PROTO_UNSPECIFIED,
transport_payload: &[],
is_non_initial_fragment: false,
};
let payload_len = usize::from(u16::from_be_bytes([payload[4], payload[5]]));
let payload_end = 40usize.saturating_add(payload_len).min(payload.len());
let mut protocol = payload[6];
let mut payload_offset = 40;
let mut is_non_initial_fragment = false;
let mut saw_fragment_header = false;
loop {
match protocol {
IPV6_HOP_BY_HOP | IPV6_ROUTING | IPV6_DESTINATION_OPTIONS => {
let header = &payload[payload_offset..payload_end];
let Some(&header_len) = header.get(1) else {
return Some(parsed);
};
let extension_len = (usize::from(header_len) + 1) * 8;
let next_protocol = header[0];
let extension_end = payload_offset + extension_len;
if extension_end > payload_end {
return Some(parsed);
}
protocol = next_protocol;
payload_offset = extension_end;
}
IPV6_AUTHENTICATION => {
let header = &payload[payload_offset..payload_end];
let Some(&header_len) = header.get(1) else {
return Some(parsed);
};
let extension_len = (usize::from(header_len) + 2) * 4;
let next_protocol = header[0];
let extension_end = payload_offset + extension_len;
if extension_end > payload_end {
return Some(parsed);
}
protocol = next_protocol;
payload_offset = extension_end;
}
IPV6_FRAGMENT => {
if saw_fragment_header {
return Some(parsed);
}
saw_fragment_header = true;
let fragment_end = payload_offset + 8;
if fragment_end > payload_end {
return Some(parsed);
}
let header = &payload[payload_offset..fragment_end];
let fragment_field = u16::from_be_bytes([header[2], header[3]]);
is_non_initial_fragment = fragment_field & 0xfff8 != 0;
let next_protocol = header[0];
payload_offset = fragment_end;
protocol = next_protocol;
if is_non_initial_fragment {
// Extension headers after the Fragment Header are only in the first
// fragment. Do not interpret tail data as an extension header.
// Only ICMPv6 tails can use the response records without a
// transport header. Leave other tails to address/group ACLs.
if protocol != IP_PROTO_ICMPV6 {
protocol = IP_PROTO_UNSPECIFIED;
}
break;
}
}
_ => break,
}
}
let transport_payload = &payload[payload_offset..payload_end];
if payload_offset != 40 && parse_transport_ports(protocol, transport_payload).is_none() {
return Some(parsed);
}
Some(ParsedIpPacket {
protocol,
transport_payload,
is_non_initial_fragment,
..parsed
})
}
fn parse_transport_ports(protocol: u8, payload: &[u8]) -> Option<(Option<u16>, Option<u16>)> {
let min_len = match protocol {
IP_PROTO_TCP => 20,
IP_PROTO_UDP => 8,
_ => return Some((None, None)),
};
if payload.len() < min_len {
return None;
}
Some((
Some(u16::from_be_bytes([payload[0], payload[1]])),
Some(u16::from_be_bytes([payload[2], payload[3]])),
))
}
fn acl_protocol(protocol: u8) -> Protocol {
match protocol {
IP_PROTO_TCP => Protocol::Tcp,
@@ -400,18 +568,13 @@ mod tests {
use easytier_proto::acl::{Acl, AclV1, Action, Chain, ChainType, Protocol, Rule};
use crate::{
packet::ip::{
IPV6_AUTHENTICATION, IPV6_DESTINATION_OPTIONS, IPV6_FRAGMENT, IPV6_HOP_BY_HOP,
IPV6_ROUTING,
},
peers::acl::processor::PacketInfo,
};
use crate::peers::acl::processor::PacketInfo;
use super::{
AclFilter, ICMP_ECHO_REPLY, ICMP_ECHO_REQUEST, ICMPV6_ECHO_REPLY, ICMPV6_ECHO_REQUEST,
IP_PROTO_ICMP, IP_PROTO_ICMPV6, IP_PROTO_TCP, IP_PROTO_UDP, IP_PROTO_UNSPECIFIED,
OutboundAllowRecord, PacketType, ZCPacket, acl_protocol, parse_ip_packet,
IPV6_AUTHENTICATION, IPV6_DESTINATION_OPTIONS, IPV6_FRAGMENT, IPV6_HOP_BY_HOP,
IPV6_ROUTING, OutboundAllowRecord, PacketType, ZCPacket, acl_protocol, parse_ip_packet,
parse_transport_ports,
};
-1
View File
@@ -5,6 +5,5 @@ pub(crate) mod peer;
pub(crate) mod peer_conn;
pub(crate) mod peer_conn_liveness;
pub(crate) mod peer_conn_ping;
mod peer_flow;
pub(crate) mod peer_map;
pub(crate) mod peer_session;
+27 -338
View File
@@ -1,10 +1,4 @@
use std::{
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
time::{Duration, Instant},
};
use std::sync::Arc;
use arc_swap::ArcSwapOption;
use crossbeam_utils::atomic::AtomicCell;
@@ -15,10 +9,7 @@ use tokio::{select, sync::mpsc};
use tracing::Instrument;
use super::{
peer_conn::{PeerConn, PeerConnId},
peer_flow::FlowBindings,
};
use super::peer_conn::{PeerConn, PeerConnId};
use crate::peers::{
PacketRecvChan,
context::{ArcPeerContext, PeerEvent},
@@ -27,7 +18,7 @@ use crate::peers::{
use crate::{
config::PeerId,
foundation::small_map_shard_amount,
packet::{FlowKey, ZCPacket},
packet::ZCPacket,
peers::error::Error,
proto::{core_peer::peer::PeerConnInfo, peer_rpc::PeerIdentityType},
};
@@ -36,19 +27,8 @@ use tokio_util::task::AbortOnDropHandle;
type ArcPeerConn = Arc<PeerConn>;
type ConnMap = Arc<DashMap<PeerConnId, ArcPeerConn>>;
fn conn_selection_key(conn: &PeerConn) -> (u8, u64) {
let latency = conn.get_stats().latency_us;
(
if latency > 0 {
0
} else if conn.is_hole_punched() {
// Prefer regular connections until hole punching has a measured RTT.
2
} else {
1
},
latency,
)
fn conn_latency_sort_key(latency_us: u64, is_hole_punched: bool) -> (bool, u64) {
(is_hole_punched && latency_us == 0, latency_us)
}
pub struct Peer {
@@ -66,8 +46,6 @@ pub struct Peer {
default_conn: Arc<ArcSwapOption<PeerConn>>,
default_conn_update_lock: Arc<Mutex<()>>,
flow_bindings: Mutex<FlowBindings>,
data_active: Arc<AtomicBool>,
peer_identity_type: Arc<AtomicCell<Option<PeerIdentityType>>>,
peer_public_key: Arc<RwLock<Option<Vec<u8>>>>,
#[allow(dead_code)]
@@ -89,8 +67,6 @@ impl Peer {
let peer_public_key_copy = peer_public_key.clone();
let default_conn = Arc::new(ArcSwapOption::empty());
let default_conn_update_lock = Arc::new(Mutex::new(()));
let data_active = Arc::new(AtomicBool::new(false));
let flow_bindings = Mutex::new(FlowBindings::default());
let conns_copy = conns.clone();
let shutdown_notifier_copy = shutdown_notifier.clone();
@@ -155,18 +131,12 @@ impl Peer {
let conns_copy = conns.clone();
let default_conn_copy = default_conn.clone();
let data_active_copy = data_active.clone();
let default_conn_clear_task = AbortOnDropHandle::new(tokio::spawn(async move {
loop {
crate::foundation::time::sleep(Duration::from_secs(5)).await;
crate::foundation::time::sleep(std::time::Duration::from_secs(5)).await;
if conns_copy.len() > 1 {
default_conn_copy.store(None);
}
let active = data_active_copy.swap(false, Ordering::Relaxed);
let fast_ping = conns_copy.len() > 1 && active;
for conn in conns_copy.iter() {
conn.fast_ping.store(fast_ping, Ordering::Relaxed);
}
}
}));
@@ -182,8 +152,6 @@ impl Peer {
shutdown_notifier,
default_conn,
default_conn_update_lock,
flow_bindings,
data_active,
peer_identity_type,
peer_public_key,
default_conn_clear_task,
@@ -245,19 +213,22 @@ impl Peer {
fn select_conn(&self) -> Option<ArcPeerConn> {
let _update_guard = self.default_conn_update_lock.lock();
if let Some(conn) = self.default_conn.load_full()
&& !conn.is_closed()
{
if let Some(conn) = self.default_conn.load_full() {
return Some(conn);
}
// Unmeasured paths are usable for startup/recovery, but cannot outrank a
// measured path merely because their RTT is still zero.
// A zero latency on a hole-punched connection means the ping loop has not
// confirmed liveness yet. Prefer any other connection, so a freshly admitted
// hole-punched path cannot steal traffic before its first successful ping.
let selected = self
.conns
.iter()
.filter(|conn| !conn.is_closed())
.min_by_key(|conn| conn_selection_key(conn.value()))
.min_by_key(|conn| {
conn_latency_sort_key(
conn.value().get_stats().latency_us,
conn.value().is_hole_punched(),
)
})
.map(|conn| conn.value().clone());
if let Some(conn) = selected.as_ref() {
@@ -266,42 +237,9 @@ impl Peer {
selected
}
fn select_flow_conn(&self, key: FlowKey) -> Option<ArcPeerConn> {
let mut flows = self.flow_bindings.lock();
let bucket = flows.bucket(key);
if let Some(binding) = flows.buckets.get_mut(&bucket)
&& binding.last_active.elapsed() < FlowBindings::IDLE_TIMEOUT
{
let bound = self
.conns
.get(&binding.conn_id)
.map(|conn| conn.value().clone());
if let Some(conn) = bound.filter(|conn| !conn.is_closed()) {
binding.last_active = Instant::now();
return Some(conn);
}
}
let conn = self.select_conn()?;
// A collision joins the bucket's active binding rather than evicting an
// existing flow. This bounds memory without migrating flows under load.
flows.bind(bucket, conn.get_conn_id(), Instant::now());
Some(conn)
}
pub async fn send_msg(&self, msg: ZCPacket) -> Result<(), Error> {
if crate::peers::traffic_metrics::data_packet_payload_len(&msg).is_some() {
self.data_active.store(true, Ordering::Relaxed);
}
if let Some(key) = msg.flow_key {
let conn = self
.select_flow_conn(key)
.ok_or(Error::PeerNoConnectionError(self.peer_node_id))?;
return conn.send_msg(msg).await;
}
let default_conn = self.default_conn.load();
if let Some(conn) = default_conn.as_ref()
&& !conn.is_closed()
{
if let Some(conn) = default_conn.as_ref() {
conn.send_msg(msg).await?;
return Ok(());
}
@@ -401,269 +339,20 @@ impl Drop for Peer {
#[cfg(test)]
mod tests {
use super::*;
use crate::{
packet::PacketType,
peers::{
PeerConnectionOrigin, conn::peer_session::PeerSessionStore, create_packet_recv_chan,
recv_packet_envelope_from_chan, test_support::NoopPeerContext,
},
tunnel::ring::create_ring_tunnel_pair,
};
use super::conn_latency_sort_key;
fn flow(port: u16) -> FlowKey {
FlowKey {
src: ([10, 0, 0, 1], port).into(),
dst: ([10, 0, 0, 2], 5000).into(),
protocol: 17,
}
#[test]
fn measured_relay_precedes_unverified_hole_punch_path() {
assert!(conn_latency_sort_key(20_000, false) < conn_latency_sort_key(0, true));
}
async fn add_connection(
peer: &Peer,
recv: PacketRecvChan,
rtt: Option<u32>,
) -> (PeerConnId, PeerConn) {
let (left, right) = create_ring_tunnel_pair();
let sessions = Arc::new(PeerSessionStore::new());
let context = Arc::new(NoopPeerContext::default());
let mut client = PeerConn::new(1, context.clone(), left, sessions.clone());
let mut server = PeerConn::new(2, context, right, sessions);
let (left, right) = tokio::join!(
client.do_handshake_as_client(),
server.do_handshake_as_server_ext(|_, _| Ok(())),
);
left.unwrap();
right.unwrap();
server.start_recv_loop(recv).await;
if let Some(rtt) = rtt {
client.record_latency_for_test(rtt);
}
let id = client.get_conn_id();
peer.add_peer_conn(client).await.unwrap();
(id, server)
#[test]
fn unmeasured_regular_connection_keeps_existing_priority() {
assert!(conn_latency_sort_key(0, false) < conn_latency_sort_key(20_000, false));
}
fn new_peer() -> Peer {
let (sender, _receiver) = create_packet_recv_chan();
Peer::new(2, sender, Arc::new(NoopPeerContext::default()))
}
#[tokio::test]
async fn send_failure_marks_connection_closed_without_waiting_for_ping() {
let (left, right) = create_ring_tunnel_pair();
let (stream, _sink) = right.split();
drop(stream);
let conn = PeerConn::new(
1,
Arc::new(NoopPeerContext::default()),
left,
Arc::new(PeerSessionStore::new()),
);
crate::foundation::time::timeout(Duration::from_secs(1), async {
loop {
let mut packet = ZCPacket::new_with_payload(b"closed sink");
packet.fill_peer_manager_hdr(1, 2, PacketType::Data as u8);
if conn.send_msg(packet).await.is_err() {
break;
}
tokio::task::yield_now().await;
}
})
.await
.unwrap();
assert!(conn.is_closed());
}
async fn packet_skips_failed_connection_before_cache_cleanup(flow_key: Option<FlowKey>) {
let peer = new_peer();
let (sender, mut receiver) = create_packet_recv_chan();
let (old, old_server) = add_connection(&peer, sender.clone(), Some(20_000)).await;
let (_, new_server) = add_connection(&peer, sender, Some(1_000)).await;
let old_conn = peer.conns.get(&old).unwrap().value().clone();
peer.default_conn.store(Some(old_conn.clone()));
if let Some(key) = flow_key {
assert_eq!(peer.select_flow_conn(key).unwrap().get_conn_id(), old);
}
// Keep the stale cache entry so the result cannot depend on task ordering.
peer.close_event_listener.abort();
drop(old_server);
crate::foundation::time::timeout(Duration::from_secs(1), async {
loop {
let mut packet = ZCPacket::new_with_payload(b"closed sink");
packet.fill_peer_manager_hdr(1, 2, PacketType::Data as u8);
if old_conn.send_msg(packet).await.is_err() {
break;
}
tokio::task::yield_now().await;
}
})
.await
.unwrap();
assert!(old_conn.is_closed());
assert_eq!(peer.get_default_conn_id(), old);
let mut packet = ZCPacket::new_with_payload(b"packet after failure");
packet.fill_peer_manager_hdr(1, 2, PacketType::Data as u8);
packet.flow_key = flow_key;
peer.send_msg(packet).await.unwrap();
let envelope = crate::foundation::time::timeout(
Duration::from_secs(1),
recv_packet_envelope_from_chan(&mut receiver),
)
.await
.unwrap()
.unwrap();
let (packet, ingress) = envelope.into_parts();
assert_eq!(packet.payload(), b"packet after failure");
assert_eq!(
ingress.peer_connection().unwrap().1,
new_server.get_conn_id()
);
}
#[tokio::test]
async fn unkeyed_packet_skips_failed_connection_before_cache_cleanup() {
packet_skips_failed_connection_before_cache_cleanup(None).await;
}
#[tokio::test]
async fn active_flow_skips_failed_connection_before_cache_cleanup() {
packet_skips_failed_connection_before_cache_cleanup(Some(flow(1234))).await;
}
#[tokio::test]
async fn unmeasured_regular_connection_precedes_unverified_hole_punch() {
let (tunnel, _remote) = create_ring_tunnel_pair();
let regular = PeerConn::new(
1,
Arc::new(NoopPeerContext::default()),
tunnel,
Arc::new(PeerSessionStore::new()),
);
for origin in [
PeerConnectionOrigin::TcpHolePunch,
PeerConnectionOrigin::UdpHolePunch,
] {
let (tunnel, _remote) = create_ring_tunnel_pair();
let hole_punched = PeerConn::new_with_peer_id_hint_and_origin(
1,
Arc::new(NoopPeerContext::default()),
tunnel,
None,
Arc::new(PeerSessionStore::new()),
origin,
);
assert!(conn_selection_key(&regular) < conn_selection_key(&hole_punched));
hole_punched.record_latency_for_test(1_000);
assert!(conn_selection_key(&hole_punched) < conn_selection_key(&regular));
}
}
fn distinct_flow(peer: &Peer, existing: FlowKey) -> FlowKey {
let flows = peer.flow_bindings.lock();
(1..=u16::MAX)
.map(flow)
.find(|key| flows.bucket(*key) != flows.bucket(existing))
.unwrap()
}
#[tokio::test]
async fn active_flow_survives_new_connection_and_periodic_reselection() {
let peer = new_peer();
let (sender, mut receiver) = create_packet_recv_chan();
let (old, old_server) = add_connection(&peer, sender.clone(), Some(20_000)).await;
let key = flow(1234);
assert_eq!(peer.select_flow_conn(key).unwrap().get_conn_id(), old);
let (new, new_server) = add_connection(&peer, sender, Some(1_000)).await;
// A slow but open connection must keep its existing flow.
for _ in 0..15 {
peer.conns
.get(&old)
.unwrap()
.record_latency_for_test(500_000);
}
peer.default_conn.store(None);
assert_eq!(peer.select_flow_conn(key).unwrap().get_conn_id(), old);
let new_key = distinct_flow(&peer, key);
assert_eq!(peer.select_flow_conn(new_key).unwrap().get_conn_id(), new);
for (key, expected) in [
(key, old_server.get_conn_id()),
(new_key, new_server.get_conn_id()),
] {
let mut packet = ZCPacket::new_with_payload(b"business data");
packet.fill_peer_manager_hdr(1, 2, PacketType::Data as u8);
packet.flow_key = Some(key);
peer.send_msg(packet).await.unwrap();
let envelope = crate::foundation::time::timeout(
Duration::from_secs(1),
recv_packet_envelope_from_chan(&mut receiver),
)
.await
.unwrap()
.unwrap();
let (packet, ingress) = envelope.into_parts();
assert_eq!(packet.payload(), b"business data");
assert_eq!(ingress.peer_connection().unwrap().1, expected);
}
}
#[tokio::test]
async fn idle_flow_can_rebind_and_closed_connection_cannot_trap_active_flow() {
let peer = new_peer();
let (sender, _receiver) = create_packet_recv_chan();
let (old, _old_server) = add_connection(&peer, sender.clone(), Some(20_000)).await;
let key = flow(1234);
assert_eq!(peer.select_flow_conn(key).unwrap().get_conn_id(), old);
let (new, _new_server) = add_connection(&peer, sender, Some(1_000)).await;
peer.default_conn.store(None);
// Activity renews the idle timeout even across repeated intervals.
for _ in 0..2 {
{
let mut flows = peer.flow_bindings.lock();
let bucket = flows.bucket(key);
flows.buckets.get_mut(&bucket).unwrap().last_active -=
FlowBindings::IDLE_TIMEOUT - Duration::from_secs(1);
}
assert_eq!(peer.select_flow_conn(key).unwrap().get_conn_id(), old);
}
{
let mut flows = peer.flow_bindings.lock();
let bucket = flows.bucket(key);
flows.buckets.get_mut(&bucket).unwrap().last_active -= FlowBindings::IDLE_TIMEOUT;
}
assert_eq!(peer.select_flow_conn(key).unwrap().get_conn_id(), new);
peer.close_peer_conn(&new).await.unwrap();
crate::foundation::time::timeout(Duration::from_secs(1), async {
while peer.conns.contains_key(&new) {
tokio::task::yield_now().await;
}
})
.await
.unwrap();
assert_eq!(peer.select_flow_conn(key).unwrap().get_conn_id(), old);
}
#[tokio::test]
async fn bucket_collisions_and_capacity_never_evict_an_active_binding() {
let peer = new_peer();
let (sender, _receiver) = create_packet_recv_chan();
let (old, _old_server) = add_connection(&peer, sender.clone(), Some(20_000)).await;
for port in 1..=u16::MAX {
assert_eq!(
peer.select_flow_conn(flow(port)).unwrap().get_conn_id(),
old
);
}
assert!(peer.flow_bindings.lock().buckets.len() <= FlowBindings::BUCKETS);
let (_new, _new_server) = add_connection(&peer, sender, Some(1_000)).await;
peer.default_conn.store(None);
for port in 1..=u16::MAX {
assert_eq!(
peer.select_flow_conn(flow(port)).unwrap().get_conn_id(),
old
);
}
#[test]
fn verified_lower_latency_path_can_be_preferred() {
assert!(conn_latency_sort_key(5_000, true) < conn_latency_sort_key(20_000, false));
}
}
+8 -24
View File
@@ -7,7 +7,7 @@ use std::{
pin::Pin,
sync::{
Arc,
atomic::{AtomicBool, AtomicU32, Ordering},
atomic::{AtomicU32, Ordering},
},
};
@@ -298,7 +298,6 @@ pub struct PeerConn {
ctrl_resp_sender: broadcast::Sender<ZCPacket>,
latency_stats: Arc<WindowLatency>,
pub(super) fast_ping: Arc<AtomicBool>,
throughput: Arc<Throughput>,
loss_rate_stats: Arc<AtomicU32>,
liveness: PeerConnLiveness,
@@ -399,7 +398,6 @@ impl PeerConn {
ctrl_resp_sender: ctrl_sender,
latency_stats: Arc::new(WindowLatency::new(15)),
fast_ping: Arc::new(AtomicBool::new(false)),
throughput,
loss_rate_stats: Arc::new(AtomicU32::new(0)),
liveness,
@@ -1387,31 +1385,22 @@ impl PeerConn {
self.liveness.clone(),
self.max_ping_interval(),
);
pingpong.fast_ping = self.fast_ping.clone();
let close_event_notifier = self.close_event_notifier.clone();
let conn_id = self.conn_id;
self.tasks.spawn(
async move {
pingpong.pingpong().await;
self.tasks.spawn(async move {
pingpong.pingpong().await;
tracing::warn!(?pingpong, "pingpong task exit");
tracing::warn!(?pingpong, "pingpong task exit");
close_event_notifier.notify_close();
close_event_notifier.notify_close();
Ok(())
}
.instrument(tracing::info_span!("peer_conn_pingpong", ?conn_id)),
);
Ok(())
});
}
pub async fn send_msg(&self, msg: ZCPacket) -> Result<(), Error> {
if let Err(error) = self.sink.send(msg).await {
self.close_event_notifier.notify_close();
return Err(error.into());
}
Ok(())
Ok(self.sink.send(msg).await?)
}
pub fn get_peer_id(&self) -> PeerId {
@@ -1470,11 +1459,6 @@ impl PeerConn {
self.close_event_notifier.clone()
}
#[cfg(test)]
pub(super) fn record_latency_for_test(&self, latency_us: u32) {
self.latency_stats.record_latency(latency_us);
}
pub fn get_stats(&self) -> PeerConnStats {
PeerConnStats {
latency_us: self.latency_stats.get_latency_us(),
+2 -76
View File
@@ -1,7 +1,7 @@
use std::{
sync::{
Arc,
atomic::{AtomicBool, AtomicU32, Ordering},
atomic::{AtomicU32, Ordering},
},
time::{Duration, Instant},
};
@@ -124,7 +124,6 @@ impl PingIntervalController {
}
pub struct PeerConnPinger {
pub(super) fast_ping: Arc<AtomicBool>,
my_peer_id: PeerId,
peer_id: PeerId,
sink: MpscTunnelSender,
@@ -164,7 +163,6 @@ impl PeerConnPinger {
) -> Self {
Self {
my_peer_id,
fast_ping: Arc::new(AtomicBool::new(false)),
peer_id,
sink,
latency_stats,
@@ -190,13 +188,13 @@ impl PeerConnPinger {
seq: u32,
liveness_token: Option<u8>,
) -> Result<PingResponse, Error> {
let now = Instant::now();
// should add seq here. so latency can be calculated more accurately
let req = Self::new_ping_packet(self.my_peer_id, self.peer_id, seq);
let req_len = req.buf_len() as u64;
self.sink.send(req).await?;
self.context.record_control_tx(&self.network_name, req_len);
let now = Instant::now();
let wait_for_pong = async {
loop {
match receiver.recv().await {
@@ -259,17 +257,11 @@ impl PeerConnPinger {
let throughput = self.throughput_stats.clone();
let controller_loss_counter = loss_counter.clone();
let max_interval = self.max_interval;
let fast_ping = self.fast_ping.clone();
controller_tasks.spawn(async move {
let mut controller =
PingIntervalController::new(throughput, controller_loss_counter, max_interval);
loop {
controller.tick().await;
controller.max_interval = if fast_ping.load(Ordering::Relaxed) {
Duration::from_secs(1)
} else {
max_interval
};
if !controller.should_send_ping() {
continue;
}
@@ -298,8 +290,6 @@ impl PeerConnPinger {
req_seq = req_seq.wrapping_add(1);
if let Ok(response) = &ret {
// A liveness echo proves reachability, not prompt delivery of a
// queued packet. Only a matching Pong supplies an RTT sample.
if let PingResponse::Pong(lat) = response {
self.latency_stats.record_latency(*lat as u32);
}
@@ -360,70 +350,6 @@ mod tests {
},
};
#[tokio::test]
async fn pong_latency_includes_waiting_for_send_queue_capacity() {
use crate::tunnel::ring::{RingTunnel, create_ring_socket_pair};
let (left, right) = create_ring_socket_pair(1);
let tunnel = MpscTunnel::new(RingTunnel::new(left, None), None);
let remote = RingTunnel::new(right, None);
let (mut stream, _sink) = remote.split();
let sender = tunnel.get_sink();
// Fill both the underlying ring and the MPSC queue without draining.
assert!(
timeout(Duration::from_millis(10), async {
loop {
sender
.send(ZCPacket::new_with_payload(b"queued business"))
.await
.unwrap();
}
})
.await
.is_err()
);
let (ctrl_sender, mut receiver) = broadcast::channel(16);
let pinger = PeerConnPinger::new(
1,
2,
sender,
ctrl_sender.clone(),
Arc::new(WindowLatency::new(15)),
Arc::new(AtomicU32::new(0)),
Arc::new(Throughput::new()),
Arc::new(NoopPeerContext::default()),
"test".to_owned(),
PeerConnLiveness::new(),
Duration::from_secs(1),
);
let probe = pinger.do_pingpong_once(&mut receiver, 1, None);
tokio::pin!(probe);
assert!(
timeout(Duration::from_millis(50), &mut probe)
.await
.is_err()
);
let reply = tokio::spawn(async move {
while let Some(Ok(packet)) = stream.next().await {
if packet.peer_manager_header().unwrap().packet_type == PacketType::Ping as u8 {
ctrl_sender.send(packet).unwrap();
return;
}
}
panic!("queued Ping was not delivered");
});
let PingResponse::Pong(rtt) = timeout(Duration::from_secs(1), probe)
.await
.unwrap()
.unwrap()
else {
panic!("expected a measured Pong");
};
assert!(rtt >= 50_000, "send queue wait was excluded: {rtt} us");
reply.await.unwrap();
}
#[cfg(not(target_os = "wasi"))]
#[tokio::test(start_paused = true)]
async fn one_second_limit_disables_ping_backoff() {
-41
View File
@@ -1,41 +0,0 @@
//! Bounded flow bindings retained until idle or their connection closes.
use std::{
collections::{HashMap, hash_map::RandomState},
hash::BuildHasher,
time::{Duration, Instant},
};
use crate::packet::FlowKey;
use super::peer_conn::PeerConnId;
pub(super) struct FlowBinding {
pub conn_id: PeerConnId,
pub last_active: Instant,
}
#[derive(Default)]
pub(super) struct FlowBindings {
hash: RandomState,
pub buckets: HashMap<usize, FlowBinding>,
}
impl FlowBindings {
pub const BUCKETS: usize = 4096;
pub const IDLE_TIMEOUT: Duration = Duration::from_secs(120);
pub fn bucket(&self, key: FlowKey) -> usize {
self.hash.hash_one(key) as usize % Self::BUCKETS
}
pub fn bind(&mut self, bucket: usize, conn_id: PeerConnId, now: Instant) {
self.buckets.insert(
bucket,
FlowBinding {
conn_id,
last_active: now,
},
);
}
}
+6 -40
View File
@@ -453,12 +453,12 @@ pub(crate) async fn direct_peer_info(peer_maps: &[Arc<PeerMap>]) -> PeerInfoForG
break;
}
}
let Some(min_lat) = min_measured_latency_us(
conns
.into_iter()
.flatten()
.map(|conn| conn.stats.as_ref().unwrap().latency_us),
) else {
let Some(min_lat) = conns
.into_iter()
.flatten()
.map(|conn| conn.stats.as_ref().unwrap().latency_us)
.min()
else {
continue;
};
@@ -471,37 +471,3 @@ pub(crate) async fn direct_peer_info(peer_maps: &[Arc<PeerMap>]) -> PeerInfoForG
}
ret
}
/// Lowest latency among the connections that already have a latency sample.
///
/// A freshly established connection reports `latency_us == 0` until its first
/// ping completes. Treating that as a real value would advertise the peer as
/// 1 ms away to the peer center, so latency-first routing could detour through
/// a slow or lossy link until the next report. Unmeasured connections are
/// skipped instead; a peer with none measured is left out of the report and is
/// reported once its first sample arrives, because the peer list changes then.
fn min_measured_latency_us(latencies_us: impl IntoIterator<Item = u64>) -> Option<u64> {
latencies_us.into_iter().filter(|lat| *lat > 0).min()
}
#[cfg(test)]
mod tests {
use super::min_measured_latency_us;
#[test]
fn unmeasured_connections_are_not_reported_as_fastest() {
assert_eq!(min_measured_latency_us([0, 306_000]), Some(306_000));
assert_eq!(min_measured_latency_us([306_000, 0, 93_000]), Some(93_000));
}
#[test]
fn peer_without_measured_connection_is_skipped() {
assert_eq!(min_measured_latency_us([0, 0]), None);
assert_eq!(min_measured_latency_us([]), None);
}
#[test]
fn sub_millisecond_lan_latency_is_still_reported() {
assert_eq!(min_measured_latency_us([400]), Some(400));
}
}
@@ -1216,23 +1216,6 @@ impl ForeignNetworkManager {
});
}
/// Whether the peer map of `network_name` can route a packet back to
/// `peer_id`, using the same gateway lookup the packet router performs.
#[doc(hidden)]
pub async fn has_gateway_for_peer(&self, network_name: &str, peer_id: PeerId) -> Option<bool> {
let item = self
.data
.network_peer_maps
.get(network_name)
.map(|v| v.clone())?;
Some(
item.peer_map
.get_gateway_peer_id(peer_id, NextHopPolicy::LeastHop)
.await
.is_some(),
)
}
pub async fn list_foreign_network_infos(
&self,
include_trusted_keys: bool,
+1 -290
View File
@@ -1311,18 +1311,6 @@ impl PeerManagerCore {
self.get_route().list_foreign_network_info().await
}
/// Whether the foreign network peer map can route back to `peer_id`.
#[doc(hidden)]
pub async fn foreign_network_has_gateway(
&self,
network_name: &str,
peer_id: PeerId,
) -> Option<bool> {
self.foreign_network_manager
.has_gateway_for_peer(network_name, peer_id)
.await
}
pub async fn list_foreign_network_infos(
&self,
include_trusted_keys: bool,
@@ -1340,22 +1328,6 @@ impl PeerManagerCore {
self.acl_filter.get_stats()
}
/// Bytes charged by the foreign network forward limiter, if configured.
#[doc(hidden)]
pub fn foreign_forward_metered_bytes(&self, network_name: &str) -> Option<u64> {
self.context
.foreign_forward_limiter(network_name)
.map(|limiter| limiter.metered_bytes())
}
/// Bytes charged by the instance-wide receive limiter, if configured.
#[doc(hidden)]
pub fn instance_recv_metered_bytes(&self) -> Option<u64> {
self.context
.recv_limiter("", false)
.map(|limiter| limiter.metered_bytes())
}
pub fn acl_filter(&self) -> Arc<AclFilter> {
self.acl_filter.clone()
}
@@ -2659,10 +2631,6 @@ impl PeerOutboundPacketRouter {
self.mark_recent_traffic_with_policy(dst_peer_id, packet_policy);
self.check_p2p_only_before_send(dst_peer_id, packet_policy)?;
if msg.peer_manager_header().unwrap().packet_type == PacketType::Data as u8 {
msg.flow_key = crate::packet::flow::parse_flow_key(&msg);
}
self.counters
.compress_tx_bytes_before
.add(msg.buf_len() as u64);
@@ -2809,7 +2777,7 @@ impl PeerOutboundPacketRouter {
pub async fn send_msg_by_ip(
&self,
mut msg: ZCPacket,
mut ip_addr: IpAddr,
ip_addr: IpAddr,
not_send_to_self: bool,
) -> Result<(), Error> {
tracing::trace!(
@@ -2819,25 +2787,9 @@ impl PeerOutboundPacketRouter {
);
msg.fill_peer_manager_hdr(self.my_peer_id, 0, PacketType::Data as u8);
let filters_registered = !self.nic_packet_process_pipeline.load().is_empty();
if !self.run_nic_packet_process_pipeline(&mut msg).await {
return Ok(());
}
if filters_registered
&& let Ok(meta) = crate::instance::packet_io::parse_ip_packet(msg.payload())
&& meta.destination != ip_addr
{
tracing::trace!(
original_dst = %ip_addr,
new_dst = %meta.destination,
"destination rewritten by nic packet filter"
);
ip_addr = meta.destination;
}
msg.flow_key = crate::packet::flow::parse_flow_key(&msg);
let packet_policy = self.context.packet_policy();
let cur_to_peer_id = msg.peer_manager_header().unwrap().to_peer_id.into();
if cur_to_peer_id != 0 {
@@ -3932,247 +3884,6 @@ mod tests {
assert!(!is_self);
}
#[cfg(feature = "aes-gcm")]
#[tokio::test]
async fn proxy_ip_responses_get_flow_keys_without_classifying_fragments_or_wrapped_data() {
struct CaptureProxyPackets(mpsc::UnboundedSender<ZCPacket>);
#[async_trait::async_trait]
impl PeerPacketFilter for CaptureProxyPackets {
async fn try_process_packet_from_peer(
&self,
packet: ZCPacket,
) -> PeerPacketFilterResult {
let packet_type = packet.peer_manager_header().unwrap().packet_type;
if [
PacketType::Data,
PacketType::KcpSrc,
PacketType::KcpDst,
PacketType::QuicSrc,
PacketType::QuicDst,
]
.iter()
.any(|kind| *kind as u8 == packet_type)
{
self.0.send(packet).unwrap();
PeerPacketFilterResult::Consumed
} else {
PeerPacketFilterResult::Pass(packet)
}
}
}
let mut config = PortablePeerManagerConfig::new(portable_runtime_config("proxy-flow-key"));
config.snapshot.flags.enable_encryption = true;
config.snapshot.flags.encryption_algorithm = "aes-gcm".to_owned();
let core = build_portable_config_for_test(config).unwrap();
core.run().await.unwrap();
let (tx, mut rx) = mpsc::unbounded_channel();
core.add_packet_process_pipeline(Box::new(CaptureProxyPackets(tx)))
.await;
let expected = crate::packet::FlowKey {
src: ([10, 0, 0, 1], 1234).into(),
dst: ([10, 0, 0, 2], 5000).into(),
protocol: 17,
};
for (packet_type, fragment_flags, key) in [
(PacketType::Data, 0u16, Some(expected)),
(PacketType::Data, 0x2000, None),
(PacketType::Data, 1, None),
(PacketType::KcpSrc, 0, None),
(PacketType::KcpDst, 0, None),
(PacketType::QuicSrc, 0, None),
(PacketType::QuicDst, 0, None),
] {
// Wrapped payloads deliberately resemble IP to check the packet-type guard.
let mut bytes = vec![0u8; 28];
bytes[0] = 0x45;
bytes[2..4].copy_from_slice(&28u16.to_be_bytes());
bytes[6..8].copy_from_slice(&fragment_flags.to_be_bytes());
bytes[8] = 64;
bytes[9] = 17;
bytes[12..16].copy_from_slice(&[10, 0, 0, 1]);
bytes[20..22].copy_from_slice(&1234u16.to_be_bytes());
bytes[22..24].copy_from_slice(&5000u16.to_be_bytes());
bytes[24..26].copy_from_slice(&8u16.to_be_bytes());
set_packet_destination(&mut bytes, expected.dst.ip());
let mut packet = ZCPacket::new_with_payload(&bytes);
packet.fill_peer_manager_hdr(core.my_peer_id(), core.my_peer_id(), packet_type as u8);
packet.mut_peer_manager_header().unwrap().set_no_proxy(true);
assert!(packet.flow_key.is_none());
core.send_msg_for_proxy(packet, core.my_peer_id())
.await
.unwrap();
let received = tokio::time::timeout(Duration::from_secs(1), rx.recv())
.await
.unwrap()
.unwrap();
assert_eq!(received.flow_key, key, "{packet_type:?}/{fragment_flags}");
assert_eq!(received.payload(), bytes);
}
core.clear_resources().await;
}
fn set_packet_destination(packet: &mut [u8], destination: IpAddr) {
match destination {
IpAddr::V4(destination) => {
assert_eq!(packet[0] >> 4, 4);
packet[16..20].copy_from_slice(&destination.octets());
packet[10..12].fill(0);
let mut checksum: u32 = packet[..20]
.chunks_exact(2)
.map(|word| u32::from(u16::from_be_bytes([word[0], word[1]])))
.sum();
while checksum >> 16 != 0 {
checksum = (checksum & 0xffff) + (checksum >> 16);
}
packet[10..12].copy_from_slice(&(!(checksum as u16)).to_be_bytes());
}
IpAddr::V6(destination) => {
assert_eq!(packet[0] >> 4, 6);
packet[24..40].copy_from_slice(&destination.octets());
}
}
}
struct DestinationRewriteNicFilter(IpAddr);
#[async_trait::async_trait]
impl super::super::NicPacketFilter for DestinationRewriteNicFilter {
async fn try_process_packet_from_nic(&self, packet: &mut ZCPacket) -> bool {
// Leave to_peer_id unset: the router must use the rewritten IP header.
assert_eq!(packet.peer_manager_header().unwrap().to_peer_id.get(), 0);
set_packet_destination(packet.mut_payload(), self.0);
true
}
}
async fn check_nic_filter_destination_routing(addresses: [IpAddr; 4]) {
let [source, original, rewritten, unrouted] = addresses;
let mut peers = Vec::new();
let mut receivers = Vec::new();
for address in [source, original, rewritten] {
let mut runtime = portable_runtime_config("nic-filter-routing");
if address.is_ipv4() {
runtime.core.routes.ipv4 = Some(IpPrefix::new(address, 24).unwrap());
} else {
runtime.core.routes.ipv6 = Some(IpPrefix::new(address, 64).unwrap());
}
let (sender, receiver) = host_packet_channel();
let peer = PeerManagerCore::new_portable_for_test(
PortablePeerManagerConfig::new(runtime),
sender,
)
.unwrap();
peer.run().await.unwrap();
peers.push(peer);
receivers.push(receiver);
}
let sender = &peers[0];
for peer in &peers[1..] {
let (client, server) = crate::tunnel::ring::create_ring_tunnel_pair();
let (client_result, server_result) = tokio::join!(
sender.add_client_tunnel(client),
peer.add_tunnel_as_server(server),
);
client_result.unwrap();
server_result.unwrap();
}
tokio::time::timeout(Duration::from_secs(10), async {
loop {
if sender.get_msg_dst_peer(&original).await == (vec![peers[1].my_peer_id()], false)
&& sender.get_msg_dst_peer(&rewritten).await
== (vec![peers[2].my_peer_id()], false)
{
break;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await
.expect("both destination routes must be ready before sending");
assert_eq!(
sender.get_msg_dst_peer(&unrouted).await,
(Vec::new(), false)
);
sender
.add_nic_packet_process_pipeline(Box::new(DestinationRewriteNicFilter(rewritten)))
.await;
// Exercise both misdelivery to an existing peer and silent loss for an
// address with no route. Send once per case, without retrying data packets.
for destination in [original, unrouted] {
let mut payload = match source {
IpAddr::V4(source) => {
let mut payload = vec![0u8; 20];
payload[0] = 0x45;
payload[2..4].copy_from_slice(&20u16.to_be_bytes());
payload[8] = 64;
payload[9] = 253; // Experimental protocol; no transport header.
payload[12..16].copy_from_slice(&source.octets());
payload
}
IpAddr::V6(source) => {
let mut payload = vec![0u8; 40];
payload[0] = 0x60;
payload[6] = 59; // No next header.
payload[7] = 64;
payload[8..24].copy_from_slice(&source.octets());
payload
}
};
set_packet_destination(&mut payload, destination);
sender
.send_msg_by_ip(ZCPacket::new_with_payload(&payload), destination, false)
.await
.unwrap();
let packet = tokio::time::timeout(Duration::from_secs(5), receivers[2].recv())
.await
.expect("rewritten destination peer did not receive the packet")
.unwrap()
.into_core_packet();
set_packet_destination(&mut payload, rewritten);
assert_eq!(packet.payload(), payload);
let header = packet.peer_manager_header().unwrap();
assert_eq!(header.from_peer_id.get(), sender.my_peer_id());
assert_eq!(header.to_peer_id.get(), peers[2].my_peer_id());
assert!(
tokio::time::timeout(Duration::from_millis(100), receivers[1].recv())
.await
.is_err(),
"packet reached the old destination"
);
}
for peer in &peers {
peer.clear_resources().await;
}
}
#[tokio::test]
async fn nic_filter_rewritten_ipv4_destination_controls_routing() {
check_nic_filter_destination_routing([
"10.82.0.1".parse().unwrap(),
"10.82.0.2".parse().unwrap(),
"10.82.0.3".parse().unwrap(),
"10.82.0.99".parse().unwrap(),
])
.await;
}
#[tokio::test]
async fn nic_filter_rewritten_ipv6_destination_controls_routing() {
check_nic_filter_destination_routing([
"fd00::1".parse().unwrap(),
"fd00::2".parse().unwrap(),
"fd00::3".parse().unwrap(),
"fd00::99".parse().unwrap(),
])
.await;
}
#[tokio::test]
async fn foreign_network_stop_waits_for_inflight_admission() {
let core = build_portable_for_test(portable_runtime_config("portable-net")).unwrap();
-1
View File
@@ -6,7 +6,6 @@
//! egress, environment facts, and the WASI mechanism backend) live in
//! [`crate::host`].
pub mod retry;
pub mod ring;
pub mod tcp;
pub mod udp;
-85
View File
@@ -1,85 +0,0 @@
//! Retry runtime listener failures until the owning task is cancelled.
use super::SocketListener;
pub async fn wait() {
crate::foundation::time::sleep(std::time::Duration::from_millis(100)).await;
}
/// For owners whose listener can be restarted in place after an accept error.
/// The first listen remains fallible so startup errors reach the caller.
pub async fn accept_with_rebind<L: SocketListener + ?Sized>(listener: &mut L) -> L::Accepted {
loop {
match listener.accept().await {
Ok(accepted) => return accepted,
Err(error) => {
tracing::warn!(target: "CORE", url = %listener.local_url(), error = %format_args!("{error:#}"), "listener failed; rebinding");
}
}
loop {
wait().await;
match listener.listen().await {
Ok(()) => break,
Err(error) => {
tracing::warn!(target: "CORE", url = %listener.local_url(), error = %format_args!("{error:#}"), "listener bind failed; retrying");
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::{collections::VecDeque, io};
#[derive(Debug, Default)]
struct Listener {
accepts: VecDeque<io::Result<usize>>,
binds: VecDeque<io::Result<()>>,
}
#[async_trait::async_trait]
impl SocketListener for Listener {
type Accepted = usize;
async fn listen(&mut self) -> anyhow::Result<()> {
Ok(self.binds.pop_front().unwrap_or(Ok(()))?)
}
async fn accept(&mut self) -> anyhow::Result<usize> {
Ok(self.accepts.pop_front().expect("unexpected retry")?)
}
fn local_url(&self) -> url::Url {
"tcp://127.0.0.1:22020".parse().unwrap()
}
}
#[tokio::test(start_paused = true)]
async fn unknown_accept_and_repeated_bind_failures_do_not_stop_recovery() {
let mut listener = Listener {
accepts: VecDeque::from([Err(io::Error::other("unknown failure")), Ok(7)]),
binds: (0..10)
.map(|_| Err(io::ErrorKind::PermissionDenied.into()))
.collect(),
};
let start = tokio::time::Instant::now();
assert_eq!(accept_with_rebind(&mut listener).await, 7);
assert_eq!(start.elapsed(), std::time::Duration::from_millis(1100));
}
#[tokio::test(start_paused = true)]
async fn rebind_wait_is_cancelled_with_its_owner() {
let mut listener = Listener {
accepts: VecDeque::from([Err(io::ErrorKind::BrokenPipe.into()), Ok(7)]),
binds: VecDeque::from([Err(io::ErrorKind::AddrInUse.into()), Ok(())]),
};
assert!(
tokio::time::timeout(
std::time::Duration::from_millis(150),
accept_with_rebind(&mut listener)
)
.await
.is_err()
);
assert_eq!(listener.binds.len(), 1);
assert_eq!(listener.accepts.len(), 1);
}
}
+4 -24
View File
@@ -363,14 +363,15 @@ where
type Accepted = AcceptedTcpSocket<F>;
async fn listen(&mut self) -> anyhow::Result<()> {
// Calling listen again replaces the old binding, including after an error.
self.listener = None;
if self.listener.is_some() {
return Ok(());
}
let listener = self.factory.bind_tcp(self.options.clone()).await?;
let local_addr = listener.local_addr()?;
self.url
.set_port(Some(local_addr.port()))
.map_err(|_| anyhow::anyhow!("failed to update tcp listener port for {}", self.url))?;
self.options.bind.local_addr = Some(local_addr);
self.listener = Some(listener);
Ok(())
}
@@ -749,25 +750,4 @@ mod tests {
assert_eq!(accepted.peer_addr().unwrap(), peer_addr);
}
#[tokio::test]
async fn tcp_socket_listener_releases_failed_binding_and_keeps_assigned_port() {
let bound_addr: SocketAddr = "127.0.0.1:12010".parse().unwrap();
let listener = Arc::new(MockTcpListener::new(
bound_addr,
vec![Err(io::ErrorKind::BrokenPipe.into())],
));
let factory = Arc::new(MockTcpListenerFactory::new(listener));
let mut socket_listener = TcpSocketListener::new_with_options(
"tcp://127.0.0.1:0".parse().unwrap(),
TcpListenOptions::direct_connect("127.0.0.1:0".parse().unwrap()),
factory.clone(),
);
socket_listener.listen().await.unwrap();
assert!(socket_listener.accept().await.is_err());
assert!(socket_listener.listener.is_some());
socket_listener.listen().await.unwrap();
let binds = factory.binds.lock().unwrap();
assert_eq!(binds.len(), 2);
assert_eq!(binds[1].bind.local_addr, Some(bound_addr));
}
}
+2 -10
View File
@@ -295,15 +295,7 @@ where
let accept = self.classified_accept(protocol)?;
accept.accept_enabled.store(true, Ordering::Relaxed);
let mut accepted_rx = accept.accepted_rx.lock().await;
let mut shutdown = self.session_shutdown_tx.subscribe();
// The layer owns the classified senders, so recv() alone does not
// finish when the socket receive task fails.
let session = tokio::select! {
biased;
_ = shutdown.wait_for(|closed| *closed) => None,
session = accepted_rx.recv() => session,
};
session.ok_or_else(|| {
accepted_rx.recv().await.ok_or_else(|| {
io::Error::new(
io::ErrorKind::UnexpectedEof,
format!("{protocol:?} udp listener closed"),
@@ -1183,7 +1175,7 @@ where
self.factory.clone(),
));
let mut session = layer.open_classified_session(request.protocol, request.remote_addr)?;
session.keep_layer_alive(layer);
session._cleanup.layer_guard = Some(Box::new(layer));
Ok(session)
}
}
+3 -4
View File
@@ -116,16 +116,15 @@ where
type Accepted = UdpSession;
async fn listen(&mut self) -> anyhow::Result<()> {
self.layer = None;
self.socket = None;
*self.layer_ref.lock().unwrap() = None;
if self.layer.is_some() {
return Ok(());
}
let socket = self.factory.bind_udp(self.request.bind.clone()).await?;
let local_addr = socket.local_addr()?;
self.url
.set_port(Some(local_addr.port()))
.map_err(|_| anyhow::anyhow!("failed to update udp listener port for {}", self.url))?;
self.request.bind.local_addr = Some(local_addr);
let layer = Arc::new(UdpSessionLayer::new_with_stun_responder(
socket.clone(),
+7 -56
View File
@@ -541,7 +541,7 @@ pub(crate) struct UdpSessionCleanup {
session_close: Option<UdpSessionClose>,
shutdown: Option<watch::Sender<bool>>,
tasks: Vec<JoinHandle<()>>,
layer_guard: Arc<StdMutex<Option<Box<dyn Send + Sync>>>>,
pub(super) layer_guard: Option<Box<dyn Send + Sync>>,
}
impl std::fmt::Debug for UdpSessionCleanup {
@@ -550,10 +550,7 @@ impl std::fmt::Debug for UdpSessionCleanup {
.field("has_session_close", &self.session_close.is_some())
.field("has_shutdown", &self.shutdown.is_some())
.field("tasks", &self.tasks.len())
.field(
"has_layer_guard",
&self.layer_guard.lock().unwrap().is_some(),
)
.field("has_layer_guard", &self.layer_guard.is_some())
.finish()
}
}
@@ -597,12 +594,10 @@ impl UdpSession {
rings.session_send_rx,
close.clone(),
));
let layer_guard = Arc::new(StdMutex::new(None));
let shutdown_task = tokio::spawn(close_udp_session_on_shutdown(
shutdown,
close.clone(),
send_task.abort_handle(),
layer_guard.clone(),
));
Self {
@@ -617,7 +612,7 @@ impl UdpSession {
session_close: Some(close),
shutdown: None,
tasks: vec![send_task, shutdown_task],
layer_guard,
layer_guard: None,
},
}
}
@@ -626,12 +621,7 @@ impl UdpSession {
where
T: Send + Sync + 'static,
{
let mut guard = self._cleanup.layer_guard.lock().unwrap();
// Accept may finish after shutdown. Synchronize with cleanup so a
// closed session cannot retain a failed listener's socket.
if !*self.closed.borrow() {
*guard = Some(Box::new(layer_guard));
}
self._cleanup.layer_guard = Some(Box::new(layer_guard));
}
pub(crate) fn into_tunnel_parts(self) -> UdpSessionTunnelParts {
@@ -909,26 +899,12 @@ async fn close_udp_session_on_shutdown(
mut shutdown: watch::Receiver<bool>,
close: UdpSessionClose,
send_task: tokio::task::AbortHandle,
layer_guard: Arc<StdMutex<Option<Box<dyn Send + Sync>>>>,
) {
let mut closed = close.close.subscribe();
if !*closed.borrow() {
if *shutdown.borrow() {
close.close();
} else {
tokio::select! {
biased;
_ = closed.changed() => {},
_ = shutdown.changed() => {
if !*closed.borrow() {
close.close();
}
},
}
}
if !*shutdown.borrow() {
let _ = shutdown.changed().await;
}
close.close();
send_task.abort();
layer_guard.lock().unwrap().take();
}
pub(super) fn dispatch_payload_to_session(
@@ -958,31 +934,6 @@ pub(super) fn dispatch_payload_to_session(
mod tests {
use super::*;
#[tokio::test]
async fn closed_session_cleanup_preserves_replacement_with_same_key() {
let sessions = Arc::new(DashMap::new());
let key = UdpSessionKey::new("127.0.0.1:12001".parse().unwrap(), 1);
let old = create_udp_session_rings();
sessions.insert(key, udp_session_registry_entry(&old));
let close = UdpSessionClose::easy_tier(key, old.close_tx.clone(), sessions.clone());
let (shutdown_tx, shutdown_rx) = watch::channel(false);
let send_task = tokio::spawn(std::future::pending::<()>());
let mut cleanup = Box::pin(close_udp_session_on_shutdown(
shutdown_rx,
close.clone(),
send_task.abort_handle(),
Arc::new(StdMutex::new(None)),
));
assert!(futures::poll!(&mut cleanup).is_pending());
close.close();
let replacement = create_udp_session_rings();
sessions.insert(key, udp_session_registry_entry(&replacement));
shutdown_tx.send(true).unwrap();
cleanup.await;
assert!(sessions.contains_key(&key));
assert!(!*replacement.close_rx.borrow());
}
impl UdpSessionClose {
fn signal_only(close: watch::Sender<bool>) -> Self {
Self {
+7 -79
View File
@@ -1459,16 +1459,12 @@ async fn udp_session_recv_loop_error_closes_registered_sessions() {
let socket = Arc::new(MockVirtualUdpSocket::new(local_addr, Vec::new()));
let sessions = Arc::new(DashMap::new());
let (session_shutdown_tx, session_shutdown_rx) = watch::channel(false);
let mut session = create_test_easy_tier_mux_session_with_shutdown(
let session = create_test_easy_tier_mux_session_with_shutdown(
socket.clone(),
UdpSessionKey::new(peer_addr, conn_id),
sessions.clone(),
session_shutdown_rx,
);
let guard = Arc::new(());
let weak_guard = Arc::downgrade(&guard);
session.keep_layer_alive(guard);
assert!(weak_guard.upgrade().is_some());
let pending_connects = Arc::new(DashMap::new());
let classified_sessions = Arc::new(DashMap::new());
let classified_accepts = create_classified_udp_session_accepts();
@@ -1511,18 +1507,6 @@ async fn udp_session_recv_loop_error_closes_registered_sessions() {
.await
.unwrap();
assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
tokio::time::timeout(Duration::from_secs(1), async {
while weak_guard.upgrade().is_some() {
tokio::task::yield_now().await;
}
})
.await
.unwrap();
let guard = Arc::new(());
let weak_guard = Arc::downgrade(&guard);
session.keep_layer_alive(guard);
assert!(weak_guard.upgrade().is_none());
}
#[tokio::test]
@@ -2104,15 +2088,12 @@ impl VirtualUdpSocketFactory for MockVirtualUdpSocketFactory {
async fn bind_udp(&self, options: UdpBindOptions) -> anyhow::Result<Arc<Self::Socket>> {
self.bind_options.lock().unwrap().push(options.clone());
let local_addr = options
.local_addr
.filter(|addr| addr.port() != 0)
.unwrap_or_else(|| {
SocketAddr::from((
[127, 0, 0, 1],
self.next_port.fetch_add(1, Ordering::Relaxed),
))
});
let local_addr = options.local_addr.unwrap_or_else(|| {
SocketAddr::from((
[127, 0, 0, 1],
self.next_port.fetch_add(1, Ordering::Relaxed),
))
});
let incoming = self
.incoming
.lock()
@@ -2167,59 +2148,6 @@ async fn udp_session_socket_listener_builds_port_bound_bind_options() {
));
}
#[tokio::test]
async fn classified_accept_observes_receive_failure_before_and_during_wait() {
for protocol in [UdpSessionProtocol::WireGuard, UdpSessionProtocol::Quic] {
for failed_before_accept in [false, true] {
let socket = Arc::new(MockVirtualUdpSocket::new(
"127.0.0.1:12000".parse().unwrap(),
Vec::new(),
));
let layer = UdpSessionLayer::new(socket);
if failed_before_accept {
// The mux sender closes when the receive task has exited.
assert!(
tokio::time::timeout(Duration::from_secs(1), layer.accept())
.await
.unwrap()
.is_err()
);
}
let mut accept = Box::pin(layer.accept_classified_session(protocol));
if !failed_before_accept {
assert!(futures::poll!(&mut accept).is_pending());
}
let error = tokio::time::timeout(Duration::from_secs(1), accept)
.await
.unwrap()
.unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::UnexpectedEof);
}
}
}
#[tokio::test]
async fn udp_listener_releases_failed_binding_and_keeps_assigned_port() {
let factory = Arc::new(MockVirtualUdpSocketFactory::new(13000));
let mut listener = UdpSessionSocketListener::new(
"udp://127.0.0.1:0".parse().unwrap(),
"127.0.0.1:0".parse().unwrap(),
factory.clone(),
);
listener.listen().await.unwrap();
let bound_addr = listener.bound_socket().unwrap().local_addr().unwrap();
let old_socket = Arc::downgrade(&listener.bound_socket().unwrap());
factory.sockets.lock().unwrap().clear();
assert!(listener.accept().await.is_err());
listener.listen().await.unwrap();
assert!(old_socket.upgrade().is_none());
assert_eq!(
listener.bound_socket().unwrap().local_addr().unwrap(),
bound_addr
);
assert_eq!(factory.bind_options().len(), 2);
}
#[tokio::test]
async fn udp_session_socket_listener_accepts_easy_tier_mux_session() {
let local_addr = SocketAddr::from(([127, 0, 0, 1], 11010));
-6
View File
@@ -152,12 +152,6 @@ pub(super) fn new_wasi_core_runtime(
adapters.config.ignore_unsupported_config = true;
adapters.config.endpoint_protocols = host_tunnel_schemes.to_vec();
}
// A WASI guest has no OS TUN, so the TCP subnet proxy can only forward through the
// user-space stack. Advertise it whenever it is compiled in; never clear the flag, since
// the host-tunnel mode above may rely on it independently of this feature.
if cfg!(feature = "proxy-smoltcp-stack") {
adapters.config.set_smoltcp_stack(true);
}
#[cfg(feature = "wasm-host-tunnel")]
#[cfg(not(feature = "wasm-host-tunnel-outbound"))]
let tunnel_ingress = crate::wasi::adapter::tunnel::WasiHostTunnelIngress::new(
Binary file not shown.
+2 -2
View File
@@ -3,6 +3,6 @@
package artifact
const (
Commit = "5f1eaa7c9f9d3e89e9b75a6d87531a436fae40c4"
SHA256 = "de332a01eb4141e2e86ecee89500c40cc7d07a822a381868f95bb231bfc3739c"
Commit = "c51437c673a71f69b4b028c8d566a7b23b33e65c"
SHA256 = "17d6dd4561c1f1e818e7d70ed031c3cf8ea5dcfc1fd0461a6753f1f23f5fa6ab"
)
+5 -17
View File
@@ -93,11 +93,8 @@ type InstanceConfigPatch struct {
VpnPortalClients []*VpnPortalClientPatch `protobuf:"bytes,15,rep,name=vpn_portal_clients,json=vpnPortalClients,proto3" json:"vpn_portal_clients,omitempty"`
ManagedCredentials *manage.ManagedCredentialSet `protobuf:"bytes,16,opt,name=managed_credentials,json=managedCredentials,proto3" json:"managed_credentials,omitempty"`
PreferPeerRelay *bool `protobuf:"varint,17,opt,name=prefer_peer_relay,json=preferPeerRelay,proto3,oneof" json:"prefer_peer_relay,omitempty"`
// Applies with routes: true preserves an empty manual list; false restores
// automatic routes. Omission preserves legacy RoutePatch behavior.
EnableManualRoutes *bool `protobuf:"varint,18,opt,name=enable_manual_routes,json=enableManualRoutes,proto3,oneof" json:"enable_manual_routes,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *InstanceConfigPatch) Reset() {
@@ -249,13 +246,6 @@ func (x *InstanceConfigPatch) GetPreferPeerRelay() bool {
return false
}
func (x *InstanceConfigPatch) GetEnableManualRoutes() bool {
if x != nil && x.EnableManualRoutes != nil {
return *x.EnableManualRoutes
}
return false
}
type VpnPortalClientPatch struct {
state protoimpl.MessageState `protogen:"open.v1"`
Action ConfigPatchAction `protobuf:"varint,1,opt,name=action,proto3,enum=api.config.ConfigPatchAction" json:"action,omitempty"`
@@ -877,7 +867,7 @@ var File_api_config_proto protoreflect.FileDescriptor
const file_api_config_proto_rawDesc = "" +
"\n" +
"\x10api_config.proto\x12\n" +
"api.config\x1a\fcommon.proto\x1a\tacl.proto\x1a\x12api_instance.proto\x1a\x10api_manage.proto\"\xd6\t\n" +
"api.config\x1a\fcommon.proto\x1a\tacl.proto\x1a\x12api_instance.proto\x1a\x10api_manage.proto\"\x86\t\n" +
"\x13InstanceConfigPatch\x12\x1f\n" +
"\bhostname\x18\x01 \x01(\tH\x00R\bhostname\x88\x01\x01\x12)\n" +
"\x04ipv4\x18\x02 \x01(\v2\x10.common.Ipv4InetH\x01R\x04ipv4\x88\x01\x01\x12)\n" +
@@ -899,8 +889,7 @@ const file_api_config_proto_rawDesc = "" +
"\x12disable_relay_data\x18\x0e \x01(\bH\aR\x10disableRelayData\x88\x01\x01\x12N\n" +
"\x12vpn_portal_clients\x18\x0f \x03(\v2 .api.config.VpnPortalClientPatchR\x10vpnPortalClients\x12Q\n" +
"\x13managed_credentials\x18\x10 \x01(\v2 .api.manage.ManagedCredentialSetR\x12managedCredentials\x12/\n" +
"\x11prefer_peer_relay\x18\x11 \x01(\bH\bR\x0fpreferPeerRelay\x88\x01\x01\x125\n" +
"\x14enable_manual_routes\x18\x12 \x01(\bH\tR\x12enableManualRoutes\x88\x01\x01B\v\n" +
"\x11prefer_peer_relay\x18\x11 \x01(\bH\bR\x0fpreferPeerRelay\x88\x01\x01B\v\n" +
"\t_hostnameB\a\n" +
"\x05_ipv4B\a\n" +
"\x05_ipv6B\x06\n" +
@@ -909,8 +898,7 @@ const file_api_config_proto_rawDesc = "" +
"\x16_ipv6_public_addr_autoB\x1a\n" +
"\x18_ipv6_public_addr_prefixB\x15\n" +
"\x13_disable_relay_dataB\x14\n" +
"\x12_prefer_peer_relayB\x17\n" +
"\x15_enable_manual_routes\"\x88\x01\n" +
"\x12_prefer_peer_relay\"\x88\x01\n" +
"\x14VpnPortalClientPatch\x125\n" +
"\x06action\x18\x01 \x01(\x0e2\x1d.api.config.ConfigPatchActionR\x06action\x129\n" +
"\x06client\x18\x02 \x01(\v2!.api.manage.VpnPortalClientConfigR\x06client\"x\n" +
+2 -2
View File
@@ -3,6 +3,6 @@
package proto
const (
EasyTierCommit = "5f1eaa7c9f9d3e89e9b75a6d87531a436fae40c4"
SchemaSHA256 = "33516bc93a3fd43df341ce081a904c1f8c5fd00ea5c33d78925e2359312a926c"
EasyTierCommit = "c51437c673a71f69b4b028c8d566a7b23b33e65c"
SchemaSHA256 = "e49a4af09e5b66089eebb73fda78ffca7e8468a5a938cf7a593d689881686f0a"
)
+4 -15
View File
@@ -95,11 +95,8 @@ type HeartbeatRequest struct {
SupportConfigSource bool `protobuf:"varint,9,opt,name=support_config_source,json=supportConfigSource,proto3" json:"support_config_source,omitempty"`
FailedNetworkInstances []*common.UUID `protobuf:"bytes,10,rep,name=failed_network_instances,json=failedNetworkInstances,proto3" json:"failed_network_instances,omitempty"`
SupportHeartbeatPolicy bool `protobuf:"varint,11,opt,name=support_heartbeat_policy,json=supportHeartbeatPolicy,proto3" json:"support_heartbeat_policy,omitempty"`
// Supports explicit manual mode patches and typed empty-manual readback.
// Older peers silently ignore unknown patch fields; missing means false.
SupportManualRoutesPatch bool `protobuf:"varint,12,opt,name=support_manual_routes_patch,json=supportManualRoutesPatch,proto3" json:"support_manual_routes_patch,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *HeartbeatRequest) Reset() {
@@ -209,13 +206,6 @@ func (x *HeartbeatRequest) GetSupportHeartbeatPolicy() bool {
return false
}
func (x *HeartbeatRequest) GetSupportManualRoutesPatch() bool {
if x != nil {
return x.SupportManualRoutesPatch
}
return false
}
type HeartbeatResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
HeartbeatIntervalMs *uint32 `protobuf:"varint,1,opt,name=heartbeat_interval_ms,json=heartbeatIntervalMs,proto3,oneof" json:"heartbeat_interval_ms,omitempty"`
@@ -356,7 +346,7 @@ const file_web_proto_rawDesc = "" +
"\fDeviceOsInfo\x12\x17\n" +
"\aos_type\x18\x01 \x01(\tR\x06osType\x12\x18\n" +
"\aversion\x18\x02 \x01(\tR\aversion\x12\"\n" +
"\fdistribution\x18\x03 \x01(\tR\fdistribution\"\xdc\x04\n" +
"\fdistribution\x18\x03 \x01(\tR\fdistribution\"\x9d\x04\n" +
"\x10HeartbeatRequest\x12+\n" +
"\n" +
"machine_id\x18\x01 \x01(\v2\f.common.UUIDR\tmachineId\x12%\n" +
@@ -372,8 +362,7 @@ const file_web_proto_rawDesc = "" +
"\x15support_config_source\x18\t \x01(\bR\x13supportConfigSource\x12F\n" +
"\x18failed_network_instances\x18\n" +
" \x03(\v2\f.common.UUIDR\x16failedNetworkInstances\x128\n" +
"\x18support_heartbeat_policy\x18\v \x01(\bR\x16supportHeartbeatPolicy\x12=\n" +
"\x1bsupport_manual_routes_patch\x18\f \x01(\bR\x18supportManualRoutesPatch\"\xb6\x01\n" +
"\x18support_heartbeat_policy\x18\v \x01(\bR\x16supportHeartbeatPolicy\"\xb6\x01\n" +
"\x11HeartbeatResponse\x127\n" +
"\x15heartbeat_interval_ms\x18\x01 \x01(\rH\x00R\x13heartbeatIntervalMs\x88\x01\x01\x125\n" +
"\x14heartbeat_timeout_ms\x18\x02 \x01(\rH\x01R\x12heartbeatTimeoutMs\x88\x01\x01B\x18\n" +
-137
View File
@@ -186,143 +186,6 @@ func testTCPDataPlane(
case <-ctx.Done():
t.Fatalf("blocked TCP read did not wake: %v", ctx.Err())
}
// FIN gate: Close must half-close the stream so the peer's read ends with
// io.EOF. Releasing the resource without shutdown_write drops the socket
// instead, which leaves the peer ESTABLISHED forever (the exit-node leak).
if err := serverConnection.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil {
t.Fatalf("set server read deadline: %v", err)
}
if _, err := serverConnection.Read(make([]byte, 1)); !errors.Is(err, io.EOF) {
t.Fatalf("server read after client close = %v, want io.EOF (FIN not sent)", err)
}
if err := serverConnection.Close(); err != nil {
t.Fatalf("server close after EOF: %v", err)
}
}
// Application deadlines must not prevent the guest from closing TCP.
func TestTCPCloseAfterExpiredDeadlines(t *testing.T) {
past := time.Now().Add(-time.Second)
for name, expire := range map[string]func(net.Conn) error{
"write": func(c net.Conn) error { return c.SetWriteDeadline(past) },
"full": func(c net.Conn) error { return c.SetDeadline(past) },
} {
t.Run(name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
defer cancel()
host, server, client := startDataPlanePair(t, ctx)
defer host.Close(ctx)
defer server.Close(ctx)
defer client.Close(ctx)
listener := listenTCPEventually(t, ctx, server)
defer listener.Close()
accepted := make(chan connectionResult, 1)
go func() {
connection, err := listener.Accept()
accepted <- connectionResult{connection: connection, err: err}
}()
clientConnection := dialEventually(t, ctx, client,
fmt.Sprintf("10.144.0.101:%d", listener.Addr().(*net.TCPAddr).Port))
var serverConnection net.Conn
select {
case result := <-accepted:
if result.err != nil {
t.Fatalf("accept EasyTier TCP: %v", result.err)
}
serverConnection = result.connection
case <-ctx.Done():
t.Fatalf("accept EasyTier TCP: %v", ctx.Err())
}
if err := expire(clientConnection); err != nil {
t.Fatalf("set expired client deadline: %v", err)
}
if err := clientConnection.Close(); err != nil {
t.Fatalf("client close after expired deadline: %v", err)
}
if err := serverConnection.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil {
t.Fatalf("set server read deadline: %v", err)
}
if _, err := serverConnection.Read(make([]byte, 1)); !errors.Is(err, io.EOF) {
t.Fatalf("server read after expired-deadline close = %v, want io.EOF", err)
}
if err := expire(serverConnection); err != nil {
t.Fatalf("set expired server deadline: %v", err)
}
if err := serverConnection.Close(); err != nil {
t.Fatalf("server close after expired deadline: %v", err)
}
})
}
}
func TestTCPCloseWithBlockedWrite(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
defer cancel()
host, server, client := startDataPlanePair(t, ctx)
defer host.Close(ctx)
defer server.Close(ctx)
defer client.Close(ctx)
listener := listenTCPEventually(t, ctx, server)
defer listener.Close()
accepted := make(chan connectionResult, 1)
go func() {
connection, err := listener.Accept()
accepted <- connectionResult{connection: connection, err: err}
}()
clientConnection := dialEventually(t, ctx, client,
fmt.Sprintf("10.144.0.101:%d", listener.Addr().(*net.TCPAddr).Port))
defer clientConnection.Close()
var serverConnection net.Conn
select {
case result := <-accepted:
if result.err != nil {
t.Fatalf("accept EasyTier TCP: %v", result.err)
}
serverConnection = result.connection
case <-ctx.Done():
t.Fatalf("accept EasyTier TCP: %v", ctx.Err())
}
defer serverConnection.Close()
if err := serverConnection.SetReadDeadline(time.Now().Add(10 * time.Second)); err != nil {
t.Fatal(err)
}
written := make(chan error, 1)
go func() {
_, err := clientConnection.Write(make([]byte, 1024*1024))
written <- err
}()
// Observe real data before withholding reads to fill the TCP windows.
if _, err := io.ReadFull(serverConnection, make([]byte, 1)); err != nil {
t.Fatalf("read start of blocked write: %v", err)
}
select {
case err := <-written:
t.Fatalf("large write completed before Close: %v", err)
case <-time.After(100 * time.Millisecond):
}
if err := clientConnection.Close(); err != nil {
t.Fatalf("close with blocked write: %v", err)
}
select {
case err := <-written:
if !errors.Is(err, net.ErrClosed) {
t.Fatalf("blocked write after Close = %v, want net.ErrClosed", err)
}
case <-ctx.Done():
t.Fatalf("Close did not unblock Write: %v", ctx.Err())
}
// Bytes already accepted by TCP precede FIN; draining them must end in EOF.
if _, err := io.Copy(io.Discard, serverConnection); err != nil {
t.Fatalf("server read after Close = %v, want EOF", err)
}
if err := serverConnection.Close(); err != nil {
t.Fatalf("server close after EOF: %v", err)
}
}
func testUDPDataPlane(
+6 -43
View File
@@ -6,8 +6,6 @@ use std::{env, path::PathBuf};
#[cfg(target_os = "windows")]
use std::io::Cursor;
#[cfg(target_os = "windows")]
use std::path::Path;
#[cfg(target_os = "windows")]
fn check_protoc_exist() -> Option<PathBuf> {
@@ -70,52 +68,17 @@ fn download_protoc() -> PathBuf {
fname
}
/// Locate the well-known types include directory (containing
/// `google/protobuf/descriptor.proto`) that ships with a protoc release
/// next to the binary: `bin/protoc.exe` -> sibling `include/` directory.
#[cfg(target_os = "windows")]
fn bundled_protoc_include(protoc: &Path) -> Option<PathBuf> {
let include = protoc.parent()?.parent()?.join("include");
include
.join("google/protobuf/descriptor.proto")
.is_file()
.then_some(include)
}
#[cfg(target_os = "windows")]
fn ensure_protoc_for_windows() {
// Preserve an explicit include directory even when protoc is found on PATH
// or downloaded. Only infer the bundled directory when it is not configured.
let protoc_include = env::var_os("PROTOC_INCLUDE").map(PathBuf::from);
if env::var_os("PROTOC").is_some() && protoc_include.is_some() {
return;
}
let protoc_path = if let Some(path) = check_protoc_exist() {
println!("cargo:info=use os existing protoc: {:?}", path);
path
} else {
download_protoc()
};
if let Some(path) = check_protoc_exist() {
if let Some(include) = protoc_include
.clone()
.or_else(|| bundled_protoc_include(&path))
{
println!("cargo:info=use os existing protoc: {:?}", path);
unsafe {
env::set_var("PROTOC", path);
env::set_var("PROTOC_INCLUDE", include);
}
return;
}
println!(
"cargo:info=protoc {:?} has no bundled include dir, falling back to download",
path
);
}
let protoc_path = download_protoc();
let include = protoc_include
.or_else(|| bundled_protoc_include(&protoc_path))
.expect("downloaded protoc zip should contain include dir");
unsafe {
env::set_var("PROTOC", protoc_path);
env::set_var("PROTOC_INCLUDE", include);
}
}
-1
View File
@@ -50,7 +50,6 @@ message NetworkConfig {
optional bool use_smoltcp = 21;
optional bool disable_ipv6 = 47;
optional string tld_dns_zone = 73;
optional bool enable_kcp_proxy = 22;
optional bool disable_kcp_input = 23;
optional bool disable_p2p = 24;
+1 -1
View File
@@ -66,7 +66,7 @@ message Flags {
bool disable_sym_hole_punching = 30 [(easytier.flag) = { default: "false" }];
// tld dns zone for magic dns
string tld_dns_zone = 31 [(easytier.flag) = { default: "et.net.", form: true }];
string tld_dns_zone = 31 [(easytier.flag) = { default: "et.net." }];
bool p2p_only = 32 [(easytier.flag) = { default: "false" }];
@@ -168,7 +168,6 @@ function allFieldFixture() {
mtu: 1280,
mapped_listeners: ['tcp://127.0.0.1:13010'],
enable_magic_dns: true,
tld_dns_zone: 'nas.internal.',
enable_private_mode: true,
enable_quic_proxy: true,
disable_quic_input: true,
@@ -387,18 +387,6 @@ function removeVpnPortalClient(index: number) {
</div>
</div>
<div class="flex flex-row gap-x-9 flex-wrap">
<div class="flex flex-col gap-2 basis-5/12 grow">
<div class="flex">
<label for="tld_dns_zone">{{ t('tld_dns_zone') }}</label>
<span class="pi pi-question-circle ml-2 self-center" v-tooltip="t('tld_dns_zone_help')"></span>
</div>
<InputText id="tld_dns_zone" :model-value="curNetwork.tld_dns_zone"
@update:model-value="curNetwork.tld_dns_zone = $event?.trim() || undefined"
aria-describedby="tld_dns_zone-help" :placeholder="t('tld_dns_zone_placeholder')" />
</div>
</div>
<div class="flex flex-row gap-x-9 flex-wrap">
<div class="flex flex-col gap-2 basis-5/12 grow">
<label for="hostname">{{ t('hostname') }}</label>
@@ -255,11 +255,7 @@ disable_sym_hole_punching_help: 禁用对称NAT的打洞(生日攻击),将
enable_magic_dns: 启用魔法DNS
enable_magic_dns_help: |
启用魔法DNS,允许通过EasyTier的DNS服务器访问其他节点的虚拟IPv4地址, 如 node1.et.net。
tld_dns_zone: DNS域名后缀
tld_dns_zone_help: |
自定义魔法DNS的顶级域名(TLD)后缀。默认为 "et.net."。
如需使用其他域名后缀,可改为 "et.internal." 或 "easytier.lan" 等。
tld_dns_zone_placeholder: 例如 et.net.
enable_private_mode: 启用私有模式
enable_private_mode_help: |
启用私有模式,则不允许使用了与本网络不相同的网络名称和密码的节点通过本节点进行握手或中转。
@@ -254,11 +254,7 @@ disable_sym_hole_punching_help: Disable special hole punching handling for symme
enable_magic_dns: Enable Magic DNS
enable_magic_dns_help: |
Enable magic dns, all nodes in the network can access each other by domain name, e.g.: node1.et.net.
tld_dns_zone: DNS Zone TLD
tld_dns_zone_help: |
Custom top-level domain (TLD) zone for Magic DNS hostnames. Default is "et.net.".
Change this if you need a different domain suffix, e.g. "et.internal." or "easytier.lan".
tld_dns_zone_placeholder: e.g. et.net.
enable_private_mode: Enable Private Mode
enable_private_mode_help: |
Enable private mode, nodes with different network names or passwords from this network are not allowed to perform handshake or relay through this node.
@@ -207,7 +207,6 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
mtu: null,
instance_recv_bps_limit: null,
mapped_listeners: [],
tld_dns_zone: 'et.net.',
port_forwards: [],
acl: emptyAcl(),
}
@@ -416,7 +416,6 @@ describe('Config.vue network config projection', () => {
expect(input(wrapper, '#no_tun').checked).toBe(true)
expect(input(wrapper, '#hostname').value).toBe('host-a')
expect(input(wrapper, '#tld_dns_zone').value).toBe('et.net.')
expect(input(wrapper, '#subnet-proxy').value).toBe('10.10.0.0/16,172.16.1.0/24')
expect(input(wrapper, '#vpn_portal_wireguard_listen').value).toBe('0.0.0.0:22023')
expect(input(wrapper, '#dev_name').value).toBe('tun-test')
@@ -447,7 +446,6 @@ describe('Config.vue network config projection', () => {
await wrapper.find('#no_tun').setValue(false)
await wrapper.find('#disable_ipv6').setValue(false)
await setInput(wrapper, '#hostname', 'host-edited')
await setInput(wrapper, '#tld_dns_zone', 'nas.internal.')
await setInput(wrapper, '#subnet-proxy', '10.7.0.0/16,172.17.0.0/16')
await setInput(wrapper, '#vpn_portal_wireguard_listen', '[::]:23000')
await setInput(wrapper, 'input[data-add-label="add_listener_url"]', 'tcp://0.0.0.0:13010')
@@ -476,7 +474,6 @@ describe('Config.vue network config projection', () => {
no_tun: false,
disable_ipv6: false,
hostname: 'host-edited',
tld_dns_zone: 'nas.internal.',
proxy_cidrs: ['10.7.0.0/16', '172.17.0.0/16'],
vpn_portal_config: {
wireguard_listen: '[::]:23000',
@@ -512,7 +509,6 @@ describe('Config.vue network config projection', () => {
network_secret: 'secret-edited',
peer_urls: ['tcp://peer-x:11010', 'udp://peer-y:11010'],
listener_urls: ['tcp://0.0.0.0:13010'],
tld_dns_zone: 'nas.internal.',
mtu: 1260,
instance_recv_bps_limit: '9007199254740993',
vpn_portal_config: {
@@ -532,9 +528,6 @@ describe('Config.vue network config projection', () => {
dst_port: 9090,
}],
})
await setInput(wrapper, '#tld_dns_zone', '')
expect(toBackendNetworkConfig(curNetwork).tld_dns_zone).toBeUndefined()
})
it('shows a flag the configuration does not state as the value the core resolves', async () => {
@@ -7,39 +7,6 @@ use easytier_core::connectivity::protocol::raw::TunnelDialer;
use std::sync::atomic::AtomicUsize;
use tokio::{io::AsyncReadExt, net::TcpStream, sync::Notify, time::timeout};
#[derive(Debug)]
struct FailingListener {
inner: RuntimeRpcListener,
attempts: Arc<std::sync::Mutex<Vec<tokio::time::Instant>>>,
errors_remaining: usize,
error_kind: std::io::ErrorKind,
}
#[async_trait::async_trait]
impl SocketListener for FailingListener {
type Accepted = Box<dyn Tunnel>;
async fn listen(&mut self) -> anyhow::Result<()> {
self.inner.listen().await
}
async fn accept(&mut self) -> anyhow::Result<Self::Accepted> {
self.attempts
.lock()
.unwrap()
.push(tokio::time::Instant::now());
if self.errors_remaining > 0 {
self.errors_remaining -= 1;
return Err(std::io::Error::from(self.error_kind).into());
}
self.inner.accept().await
}
fn local_url(&self) -> url::Url {
self.inner.local_url()
}
}
#[derive(Debug)]
struct TestListener {
inner: RuntimeRpcListener,
@@ -94,66 +61,6 @@ async fn manager() -> ClientManager {
)
}
#[tokio::test]
async fn accept_errors_rebind_listener_with_fixed_delay() {
for error_kind in [std::io::ErrorKind::OutOfMemory, std::io::ErrorKind::Other] {
let mut manager = manager().await;
let attempts = Arc::new(std::sync::Mutex::new(Vec::new()));
let url = manager
.add_listener(FailingListener {
inner: runtime_rpc_listener("127.0.0.1:0".parse().unwrap()),
attempts: attempts.clone(),
errors_remaining: 2,
error_kind,
})
.await
.unwrap();
wait_until(|| attempts.lock().unwrap().len() >= 3).await;
assert!(manager.is_running());
let client = runtime_rpc_dialer(url).connect().await.unwrap();
let _client = timeout(
Duration::from_secs(1),
web_security::upgrade_client_tunnel(client),
)
.await
.expect("listener did not recover after accept errors")
.unwrap();
wait_until(|| manager.client_sessions.len() == 1).await;
{
let attempts = attempts.lock().unwrap();
assert!(attempts.len() >= 3);
for pair in attempts[..3].windows(2) {
assert!(pair[1] - pair[0] >= Duration::from_millis(100));
}
}
assert!(manager.is_running());
drop(manager);
wait_until(|| Arc::strong_count(&attempts) == 1).await;
}
}
#[tokio::test]
async fn manager_shutdown_cancels_accept_error_retries() {
let mut manager = manager().await;
let attempts = Arc::new(std::sync::Mutex::new(Vec::new()));
let url = manager
.add_listener(FailingListener {
inner: runtime_rpc_listener("127.0.0.1:0".parse().unwrap()),
attempts: attempts.clone(),
errors_remaining: usize::MAX,
error_kind: std::io::ErrorKind::Other,
})
.await
.unwrap();
wait_until(|| !attempts.lock().unwrap().is_empty()).await;
assert!(manager.is_running());
drop(manager);
wait_until(|| Arc::strong_count(&attempts) == 1).await;
let addr = url.socket_addrs(|| None).unwrap()[0];
assert!(TcpStream::connect(addr).await.is_err());
}
#[tokio::test]
async fn slow_handshakes_do_not_block_other_clients() {
let mut manager = manager().await;
+6 -10
View File
@@ -199,6 +199,7 @@ impl ClientManager {
self.listeners_cnt.fetch_add(1, Ordering::Relaxed);
let sessions = self.client_sessions.clone();
let storage = self.storage.weak_ref();
let listeners_cnt = self.listeners_cnt.clone();
let next_session_epoch = self.next_session_epoch.clone();
let geoip_db = self.geoip_db.clone();
let heartbeat_policy = self.heartbeat_policy;
@@ -206,15 +207,15 @@ impl ClientManager {
let webhook_config = self.webhook_config.clone();
self.tasks.spawn(async move {
let mut handshakes = JoinSet::new();
loop {
'accept: loop {
// Some listeners include a WebSocket upgrade in accept(). Keep
// that future alive while processing completed handshakes.
let accepting = easytier_core::socket::retry::accept_with_rebind(&mut listener);
let accepting = listener.accept();
tokio::pin!(accepting);
loop {
let result = tokio::select! {
accepted = &mut accepting, if handshakes.len() < MAX_PENDING_HANDSHAKES => {
let tunnel = accepted;
let Ok(tunnel) = accepted else { break 'accept };
handshakes.spawn(web_security::accept_or_upgrade_server_tunnel(tunnel));
break;
}
@@ -257,6 +258,7 @@ impl ClientManager {
session.mark_route_ready();
}
}
listeners_cnt.fetch_sub(1, Ordering::Relaxed);
});
Ok(local_url)
@@ -1086,7 +1088,6 @@ mod tests {
assert_eq!(updated.disable_upnp, Some(true));
assert_eq!(updated.disable_relay_data, Some(true));
assert_eq!(updated.enable_magic_dns, Some(true));
assert_eq!(updated.tld_dns_zone.as_deref(), Some("nas.internal."));
assert_eq!(updated.enable_private_mode, Some(true));
assert_eq!(updated.mtu, Some(1360));
assert_eq!(
@@ -1113,7 +1114,6 @@ mod tests {
"relay_all_peer_rpc": false,
"multi_thread": false,
"disable_relay_data": false,
"tld_dns_zone": "initial.internal.",
"mtu": 1380
})
}
@@ -1164,7 +1164,6 @@ mod tests {
disable_upnp: Some(true),
disable_relay_data: Some(true),
enable_magic_dns: Some(true),
tld_dns_zone: Some("nas.internal.".to_owned()),
enable_private_mode: Some(true),
mtu: Some(1360),
data_compress_algo: Some(CompressionAlgoPb::Zstd as i32),
@@ -1654,11 +1653,10 @@ mod tests {
.await
.unwrap();
let initial = wait_for_runtime_config(&core_manager, instance_id, |config| {
wait_for_runtime_config(&core_manager, instance_id, |config| {
config.network_name.as_deref() == Some("managed-initial")
})
.await;
assert_eq!(initial.tld_dns_zone.as_deref(), Some("initial.internal."));
wait_for_applied_revision(&mgr, user_id, machine_id, "rev-initial").await;
// Runtime-only mutations do not change SQLite. Invalidate the Session
@@ -1775,7 +1773,6 @@ mod tests {
Some("managed-redelivered")
);
assert_eq!(redelivered.enable_kcp_proxy, Some(true));
assert_eq!(redelivered.tld_dns_zone, None);
assert_eq!(redelivered.instance_recv_bps_limit, Some(654321));
assert_eq!(
core_manager.config_source(instance_id),
@@ -1811,7 +1808,6 @@ mod tests {
Some("managed-redelivered")
);
assert_eq!(replayed.enable_kcp_proxy, Some(true));
assert_eq!(replayed.tld_dns_zone, None);
assert_eq!(replayed.instance_recv_bps_limit, Some(654321));
webhook_server.abort();
@@ -730,26 +730,6 @@ mod tests {
.collect()
}
#[test]
fn magic_dns_zone_change_requires_recreate() {
let current = config_with_port_forwards(Vec::new());
let mut desired = current.clone();
desired.tld_dns_zone = Some("et.net.".to_owned());
let action =
prepare_web_source_runtime_reconcile_from_current(&current, desired.clone(), false)
.unwrap();
assert!(matches!(action, RuntimeReconcileAction::Unchanged(_)));
desired.tld_dns_zone = Some("nas.internal.".to_owned());
let action =
prepare_web_source_runtime_reconcile_from_current(&current, desired, false).unwrap();
let RuntimeReconcileAction::Run { config, overwrite } = action else {
panic!("changing the DNS zone must recreate the instance");
};
assert!(overwrite);
assert_eq!(config.tld_dns_zone.as_deref(), Some("nas.internal."));
}
#[test]
fn vpn_portal_client_changes_produce_hot_patches() {
let current = config_with_vpn_portal(
+1 -3
View File
@@ -113,7 +113,7 @@ rcgen = { version = "0.14.10", optional = true }
tokio-rustls = { workspace = true, optional = true }
# for tap device
tun = { package = "tun-easytier", git = "https://github.com/EasyTier/rust-tun", rev = "ac0c674c900dbdcf0b3fe9f5f6134c11579d1f7c", features = [
tun = { package = "tun-easytier", git = "https://github.com/EasyTier/rust-tun", rev = "5ba6be073e5b486b68c12f80fac5cfb50601f024", features = [
"async",
], optional = true }
# for net ns
@@ -426,8 +426,6 @@ endpoint-discovery = [
"dep:rustls",
"easytier-core/endpoint-discovery",
]
# Trust policy is opt-in, including when building with full features.
system-certificates = ["endpoint-discovery", "easytier-core/system-certificates"]
dhcp-ipv4 = ["easytier-core/dhcp-ipv4"]
dhcp-ipv6 = ["easytier-core/dhcp-ipv6"]
public-ipv6-provider = ["linux-netlink", "easytier-core/public-ipv6-provider"]
+4 -7
View File
@@ -127,14 +127,11 @@ core_clap:
en: "encryption algorithm to use, supported: '', 'xor', 'chacha20', 'aes-gcm', 'aes-gcm-256', 'openssl-aes128-gcm', 'openssl-aes256-gcm', 'openssl-chacha20'. Empty string means default (aes-gcm)"
zh-CN: "要使用的加密算法,支持:''(默认aes-gcm)、'xor'、'chacha20'、'aes-gcm'、'aes-gcm-256'、'openssl-aes128-gcm'、'openssl-aes256-gcm'、'openssl-chacha20'"
multi_thread:
en: "use a multi-thread runtime for each instance, default is true; ignored with --shared-runtime"
zh-CN: "为每个实例使用多线程运行时,默认开启;启用 --shared-runtime 时忽略此项"
en: "use multi-thread runtime, default is single-thread"
zh-CN: "使用多线程运行时,默认为单线程"
multi_thread_count:
en: "runtime worker count: default/minimum 2 per instance; with --shared-runtime, defaults to available CPUs with a minimum of 1"
zh-CN: "运行时工作线程数:独占多线程模式默认及最小值为 2;启用 --shared-runtime 时默认为可用 CPU 数,最小值为 1"
shared_runtime:
en: "share one multi-thread runtime across all instances; --multi-thread-count sets its worker count independently of instance configuration files"
zh-CN: "所有实例共用一个多线程运行时;通过 --multi-thread-count 设置工作线程数,不受实例配置文件影响"
en: "the number of threads to use, default is 2, only effective when multi-thread is enabled, must be greater than 2"
zh-CN: "使用的线程数,默认为2,仅在多线程模式下有效。取值必须大于2"
disable_ipv6:
en: "do not use ipv6"
zh-CN: "不使用IPv6"
+1 -16
View File
@@ -64,7 +64,6 @@ pub fn disable_connection_reset<S: AsRawSocket>(socket: &S) -> io::Result<()> {
Ok(())
}
// Configuration operations must see newly created or recreated adapters immediately.
pub fn find_interface_index(iface_name: &str) -> io::Result<u32> {
let ifaces = network_interface::NetworkInterface::show().map_err(|e| {
io::Error::new(
@@ -82,22 +81,8 @@ pub fn find_interface_index(iface_name: &str) -> io::Result<u32> {
))
}
fn find_interface_index_for_bind(iface_name: &str) -> io::Result<u32> {
let ifaces = crate::common::network::get_interfaces_cached().ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
format!("Failed to get interfaces for {}", iface_name),
)
})?;
ifaces
.iter()
.find(|iface| iface.name == iface_name)
.map(|iface| iface.index)
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, iface_name.to_string()))
}
pub fn set_ip_unicast_if(socket: SOCKET, addr: &SocketAddr, iface: &str) -> io::Result<()> {
let if_index = find_interface_index_for_bind(iface)?;
let if_index = find_interface_index(iface)?;
unsafe {
// https://docs.microsoft.com/en-us/windows/win32/winsock/ipproto-ip-socket-options
-103
View File
@@ -1,10 +1,6 @@
#[cfg(not(mobile))]
use std::collections::HashMap;
use std::net::IpAddr;
#[cfg(any(target_os = "windows", test))]
use std::sync::{Arc, RwLock};
#[cfg(any(target_os = "windows", test))]
use std::time::{Duration, Instant};
use network_interface::{NetworkInterface, NetworkInterfaceConfig};
#[cfg(all(target_os = "macos", not(feature = "macos-ne")))]
@@ -307,48 +303,6 @@ pub(crate) async fn collect_interfaces(net_ns: NetNS, filter: bool) -> Vec<Netwo
}
}
/// The Windows bind path resolves an interface name and then its index
/// through full `NetworkInterface::show()` enumerations for every bind
/// candidate. With 10+ adapters (VMware / Hyper-V / APIPA leftovers are
/// common), one connect round turns into O(candidates) synchronous
/// enumerations on the async worker. Interfaces rarely change within a
/// connect round, so snapshots are reused for a short TTL.
/// Only use this cache for socket binding; interface configuration needs
/// fresh enumeration to observe adapter creation and replacement immediately.
#[cfg(target_os = "windows")]
pub(crate) fn get_interfaces_cached() -> Option<Arc<Vec<NetworkInterface>>> {
static SNAPSHOT: InterfaceSnapshot = RwLock::new(None);
get_or_refresh_interfaces(&SNAPSHOT, || NetworkInterface::show().ok())
}
#[cfg(any(target_os = "windows", test))]
type InterfaceSnapshot = RwLock<Option<(Instant, Arc<Vec<NetworkInterface>>)>>;
#[cfg(any(target_os = "windows", test))]
fn get_or_refresh_interfaces(
snapshot: &InterfaceSnapshot,
enumerate: impl FnOnce() -> Option<Vec<NetworkInterface>>,
) -> Option<Arc<Vec<NetworkInterface>>> {
const SNAPSHOT_TTL: Duration = Duration::from_secs(1);
if let Some((at, ifaces)) = snapshot.read().ok().and_then(|guard| guard.clone())
&& at.elapsed() < SNAPSHOT_TTL
{
return Some(ifaces);
}
// Recheck after acquiring the write lock: another caller may have refreshed
// while we waited. Keep the lock during enumeration to coalesce refreshes.
let mut snapshot = snapshot.write().ok()?;
if let Some((at, ifaces)) = snapshot.as_ref()
&& at.elapsed() < SNAPSHOT_TTL
{
return Some(ifaces.clone());
}
let ifaces = Arc::new(enumerate()?);
*snapshot = Some((Instant::now(), ifaces.clone()));
Some(ifaces)
}
#[cfg(feature = "faketcp")]
fn convert_pnet_interface(iface: pnet_datalink::NetworkInterface) -> NetworkInterface {
let internal = iface.is_loopback();
@@ -544,60 +498,3 @@ mod tests {
assert_eq!(before, after);
}
}
#[cfg(test)]
mod interface_snapshot_tests {
use super::*;
use std::sync::{
Barrier,
atomic::{AtomicUsize, Ordering},
};
#[test]
fn concurrent_refreshes_share_cold_and_expired_snapshots() {
for initial in [
None,
Some((Instant::now() - Duration::from_secs(2), Arc::new(vec![]))),
] {
let snapshot = InterfaceSnapshot::new(initial);
let calls = AtomicUsize::new(0);
let barrier = Barrier::new(8);
let results = std::thread::scope(|scope| {
let handles: Vec<_> = (0..8)
.map(|_| {
scope.spawn(|| {
barrier.wait();
get_or_refresh_interfaces(&snapshot, || {
calls.fetch_add(1, Ordering::SeqCst);
std::thread::sleep(Duration::from_millis(20));
Some(vec![])
})
.unwrap()
})
})
.collect();
handles
.into_iter()
.map(|h| h.join().unwrap())
.collect::<Vec<_>>()
});
assert_eq!(calls.load(Ordering::SeqCst), 1);
for result in &results {
assert!(Arc::ptr_eq(&results[0], result));
}
}
}
#[test]
fn failed_refresh_is_retried_without_serving_expired_snapshot() {
let snapshot = InterfaceSnapshot::new(Some((
Instant::now() - Duration::from_secs(2),
Arc::new(vec![]),
)));
assert!(get_or_refresh_interfaces(&snapshot, || None).is_none());
let refreshed = get_or_refresh_interfaces(&snapshot, || Some(vec![])).unwrap();
let cached =
get_or_refresh_interfaces(&snapshot, || panic!("cache should be fresh")).unwrap();
assert!(Arc::ptr_eq(&refreshed, &cached));
}
}
+2 -90
View File
@@ -132,13 +132,6 @@ struct Cli {
)]
config_dir: Option<PathBuf>,
#[arg(
long,
env = "ET_SHARED_RUNTIME",
help = t!("core_clap.shared_runtime").to_string()
)]
shared_runtime: bool,
#[command(flatten)]
network_options: NetworkOptions,
@@ -513,26 +506,6 @@ struct RpcPortalOptions {
}
impl Cli {
fn take_runtime_thread_count(&mut self) -> u32 {
let count = self
.network_options
.flags
.multi_thread_count
.unwrap_or_else(|| {
if self.shared_runtime {
std::thread::available_parallelism().map_or(1, |count| count.get() as u32)
} else {
Flags::defaults().multi_thread_count
}
});
if self.shared_runtime {
// Process runtime options must not make instance configs read-only.
self.network_options.flags.multi_thread = None;
self.network_options.flags.multi_thread_count = None;
}
count
}
fn gen_listeners(addr: SocketAddr) -> impl Iterator<Item = String> {
let dynamic = addr.port() == 0;
IpScheme::VARIANTS.iter().map(move |proto| {
@@ -1237,7 +1210,7 @@ fn win_service_main(arg: Vec<std::ffi::OsString>) {
win_service_event_loop(stop_notify_recv, cli, status_handle);
}
async fn run_main(mut cli: Cli) -> anyhow::Result<()> {
async fn run_main(cli: Cli) -> anyhow::Result<()> {
defer!(dump_profile(0););
log::init_with_default_console_targets(
&cli.logging_options,
@@ -1245,10 +1218,7 @@ async fn run_main(mut cli: Cli) -> anyhow::Result<()> {
&["CORE", "easytier_core::instance::manager"],
)?;
let manager = Arc::new(
native_cli_instance_manager(cli.shared_runtime, cli.take_runtime_thread_count())?
.with_config_path(cli.config_dir.clone()),
);
let manager = Arc::new(native_cli_instance_manager().with_config_path(cli.config_dir.clone()));
let _rpc_server = ApiRpcServer::new(
cli.rpc_portal_options.rpc_portal,
@@ -1574,64 +1544,6 @@ mod tests {
flags_from(&cli_command().try_get_matches_from(argv)?)
}
#[test]
fn shared_runtime_is_process_only_and_reuses_thread_count() {
let cpus = std::thread::available_parallelism().map_or(1, |count| count.get() as u32);
for (args, shared, expected_count) in [
(vec![], false, Flags::defaults().multi_thread_count),
(vec!["--shared-runtime"], true, cpus),
(vec!["--multi-thread-count", "4"], false, 4),
(
vec![
"--shared-runtime",
"--multi-thread-count",
"1",
"--multi-thread",
"false",
],
true,
1,
),
(
vec![
"--shared-runtime",
"--multi-thread-count",
"4",
"-c",
"a.toml",
"b.toml",
],
true,
4,
),
] {
let matches = cli_command()
.try_get_matches_from(std::iter::once("easytier-core").chain(args))
.unwrap();
let mut cli = Cli::from_arg_matches(&matches).unwrap();
cli.network_options.flags = flags_from(&matches).unwrap();
assert_eq!(cli.shared_runtime, shared);
assert_eq!(cli.take_runtime_thread_count(), expected_count);
let config = TomlConfigLoader::default();
if shared {
assert_eq!(cli.network_options.flags.multi_thread, None);
assert_eq!(cli.network_options.flags.multi_thread_count, None);
assert!(!cli.network_options.can_merge(
&config,
ConfigFileSource::CliConfigFile,
1,
0,
));
}
cli.network_options.merge_into(&config).unwrap();
assert!(!config.dump().contains("shared_runtime"));
if cli.network_options.flags.multi_thread_count.is_none() {
assert_eq!(config.get_flags_patch().multi_thread_count, None);
}
}
}
#[test]
fn test_parse_listeners() {
type IpSchemeMap = fn(&IpScheme) -> String;
+27 -148
View File
@@ -1,4 +1,5 @@
use std::{
mem::MaybeUninit,
net::{IpAddr, Ipv4Addr, SocketAddrV4},
sync::Arc,
};
@@ -8,8 +9,6 @@ use easytier_core::{
socket::SocketContext,
};
use socket2::Socket;
use tokio::net::UdpSocket;
use tokio_util::sync::CancellationToken;
use crate::common::netns::NetNS;
@@ -28,43 +27,39 @@ impl RuntimeIcmpProxyHost {
Ipv4Addr::UNSPECIFIED,
0,
)))?;
socket.set_nonblocking(true)?;
Ok(socket)
}
}
#[derive(Debug)]
struct RuntimeIcmpSocket {
socket: UdpSocket,
cancel: CancellationToken,
socket: Arc<Socket>,
}
#[async_trait::async_trait]
impl IcmpProxySocket for RuntimeIcmpSocket {
async fn send(&self, destination: Ipv4Addr, packet: &[u8]) -> Result<(), ProxyRuntimeError> {
tokio::select! {
biased;
_ = self.cancel.cancelled() => return Err(std::io::Error::from(std::io::ErrorKind::NotConnected).into()),
result = self.socket.send_to(packet, SocketAddrV4::new(destination, 0)) => { result?; }
}
self.socket
.send_to(packet, &SocketAddrV4::new(destination, 0).into())?;
Ok(())
}
async fn recv(&self) -> Result<(IpAddr, Vec<u8>), ProxyRuntimeError> {
let mut buffer = vec![0_u8; 8192];
let (length, peer) = tokio::select! {
biased;
// The proxy receive loop terminates on an empty packet. A permanent
// error would be retried immediately and could prevent task abortion.
_ = self.cancel.cancelled() => return Ok((IpAddr::V4(Ipv4Addr::UNSPECIFIED), Vec::new())),
result = self.socket.recv_from(&mut buffer) => result?,
};
buffer.truncate(length);
Ok((peer.ip(), buffer))
let socket = self.socket.clone();
tokio::task::spawn_blocking(move || {
let mut buffer = vec![0_u8; 8192];
let uninitialized: &mut [MaybeUninit<u8>] =
unsafe { std::mem::transmute(&mut buffer[..]) };
let (length, peer_ip) = socket_recv(&socket, uninitialized)?;
buffer.truncate(length);
Ok((peer_ip, buffer))
})
.await
.map_err(|error| ProxyRuntimeError::Other(error.into()))?
}
fn close(&self) {
self.cancel.cancel();
let _ = self.socket.shutdown(std::net::Shutdown::Both);
}
}
@@ -77,136 +72,20 @@ impl IcmpProxyHost for RuntimeIcmpProxyHost {
let socket = Self::create_raw_socket(&context).inspect_err(|error| {
tracing::warn!(?error, "create ICMP socket failed");
})?;
// Tokio only supplies readiness and recvfrom/sendto here; the underlying
// socket remains raw ICMP, including the IPv4 header on received packets.
Ok(Arc::new(RuntimeIcmpSocket {
socket: UdpSocket::from_std(socket.into())?,
cancel: CancellationToken::new(),
socket: Arc::new(socket),
}))
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
#[tokio::test]
async fn close_ends_receivers_and_rejects_further_sends() {
let socket = RuntimeIcmpSocket {
socket: UdpSocket::bind((Ipv4Addr::LOCALHOST, 0)).await.unwrap(),
cancel: CancellationToken::new(),
};
let mut first = Box::pin(socket.recv());
let mut second = Box::pin(socket.recv());
assert!(futures::poll!(&mut first).is_pending());
assert!(futures::poll!(&mut second).is_pending());
socket.close();
socket.close();
tokio::time::timeout(Duration::from_secs(1), async {
assert!(first.await.unwrap().1.is_empty());
assert!(second.await.unwrap().1.is_empty());
assert!(socket.recv().await.unwrap().1.is_empty());
assert!(socket.send(Ipv4Addr::LOCALHOST, &[0; 8]).await.is_err());
})
.await
.unwrap();
}
#[tokio::test]
async fn cancelled_receive_does_not_consume_the_next_packet() {
let socket = RuntimeIcmpSocket {
socket: UdpSocket::bind((Ipv4Addr::LOCALHOST, 0)).await.unwrap(),
cancel: CancellationToken::new(),
};
let mut receive = Box::pin(socket.recv());
assert!(futures::poll!(&mut receive).is_pending());
drop(receive);
let sender = UdpSocket::bind((Ipv4Addr::LOCALHOST, 0)).await.unwrap();
sender
.send_to(b"after cancellation", socket.socket.local_addr().unwrap())
.await
.unwrap();
let (peer, packet) = tokio::time::timeout(Duration::from_secs(1), socket.recv())
.await
.unwrap()
.unwrap();
assert_eq!(peer, IpAddr::V4(Ipv4Addr::LOCALHOST));
assert_eq!(packet, b"after cancellation");
}
// Raw ICMP requires CAP_NET_RAW; run this test in the privileged test container.
#[test]
#[cfg(target_os = "linux")]
fn raw_echo_fanout_does_not_start_blocking_threads() {
use std::sync::atomic::{AtomicUsize, Ordering};
let threads = Arc::new(AtomicUsize::new(0));
let started = threads.clone();
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.on_thread_start(move || {
started.fetch_add(1, Ordering::Relaxed);
})
.build()
.unwrap();
let result = runtime.block_on(async {
tokio::time::timeout(Duration::from_secs(5), async {
let mut sockets = Vec::new();
for _ in 0..30 {
sockets.push(
RuntimeIcmpProxyHost
.open_icmp_v4(SocketContext::default())
.await
.unwrap(),
);
}
let identifier = rand::random::<u16>().to_be_bytes();
for sequence in 0..16 {
let mut request = vec![8, 0, 0, 0, identifier[0], identifier[1], 0, sequence];
request.extend_from_slice(b"async ICMP");
let mut checksum: u32 = request
.chunks_exact(2)
.map(|word| u32::from(u16::from_be_bytes([word[0], word[1]])))
.sum();
while checksum >> 16 != 0 {
checksum = (checksum & 0xffff) + (checksum >> 16);
}
request[2..4].copy_from_slice(&(!(checksum as u16)).to_be_bytes());
let receivers = futures::future::join_all(sockets.iter().map(|socket| async {
loop {
let (peer, packet) = socket.recv().await.unwrap();
let header_len = usize::from(packet[0] & 0x0f) * 4;
let Some(reply) = packet.get(header_len..) else {
continue;
};
if reply.len() == request.len()
&& reply[0] == 0
&& reply[4..] == request[4..]
{
assert_eq!(peer, IpAddr::V4(Ipv4Addr::LOCALHOST));
break;
}
}
}));
tokio::join!(receivers, async {
sockets[0]
.send(Ipv4Addr::LOCALHOST, &request)
.await
.unwrap();
});
}
for socket in sockets {
socket.close();
}
})
.await
});
runtime.shutdown_timeout(Duration::from_secs(1));
result.unwrap();
assert_eq!(threads.load(Ordering::Relaxed), 0);
}
fn socket_recv(
socket: &Socket,
buffer: &mut [MaybeUninit<u8>],
) -> Result<(usize, IpAddr), std::io::Error> {
let (size, address) = socket.recv_from(buffer)?;
let peer_ip = address
.as_socket()
.map(|address| address.ip())
.unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED));
Ok((size, peer_ip))
}
+1 -1
View File
@@ -52,7 +52,7 @@ pub(super) fn compose_native_core_instance(
process_runtime: Arc<CoreProcessRuntime>,
#[cfg(feature = "tun")] shared_virtual_nic_registry: ArcSharedVirtualNicRegistry,
compact_runtime: bool,
executor: Option<Arc<NativeInstanceExecutor>>,
executor: Option<NativeInstanceExecutor>,
) -> anyhow::Result<Arc<NativeCoreInstance>> {
let host_config = if compact_runtime {
compact_runtime_core_host_config()
+10 -103
View File
@@ -41,16 +41,12 @@ pub fn native_instance_manager() -> NativeInstanceManager {
}
#[cfg(feature = "management")]
pub fn native_cli_instance_manager(
shared_runtime: bool,
thread_count: u32,
) -> anyhow::Result<NativeInstanceManager> {
pub fn native_cli_instance_manager() -> NativeInstanceManager {
let process_runtime = CoreProcessRuntime::new();
let mut factory = NativeInstanceFactory::new(process_runtime).with_cli_event_logging();
if shared_runtime {
factory.shared_executor = Some(Arc::new(NativeInstanceExecutor::new(true, thread_count)?));
}
Ok(InstanceManager::new(factory, None))
InstanceManager::new(
NativeInstanceFactory::new(process_runtime).with_cli_event_logging(),
None,
)
}
pub fn create_native_instance(config: TomlConfig) -> anyhow::Result<Arc<NativeCoreInstance>> {
@@ -115,7 +111,6 @@ pub struct NativeInstanceFactory {
#[cfg(feature = "tun")]
shared_virtual_nic_registry: ArcSharedVirtualNicRegistry,
runtime_handle: Option<tokio::runtime::Handle>,
shared_executor: Option<Arc<NativeInstanceExecutor>>,
compact_runtime: bool,
#[cfg(feature = "logging")]
log_cli_events: bool,
@@ -128,7 +123,6 @@ impl NativeInstanceFactory {
#[cfg(feature = "tun")]
shared_virtual_nic_registry: Arc::new(Mutex::new(SharedVirtualNicRegistry::new())),
runtime_handle: None,
shared_executor: None,
compact_runtime: false,
#[cfg(feature = "logging")]
log_cli_events: false,
@@ -166,18 +160,16 @@ impl InstanceFactory for NativeInstanceFactory {
) -> Result<Arc<Self::Instance>, Self::Error> {
let executor = if self.compact_runtime {
None
} else if let Some(executor) = &self.shared_executor {
Some(executor.clone())
} else {
let flags = config.get_flags();
Some(Arc::new(NativeInstanceExecutor::new(
Some(NativeInstanceExecutor::new(
flags.multi_thread,
flags.multi_thread_count.max(2),
)?))
flags.multi_thread_count,
)?)
};
let runtime_handle = executor
.as_ref()
.map(|executor| executor.handle())
.map(NativeInstanceExecutor::handle)
.or_else(|| self.runtime_handle.clone());
let _runtime = runtime_handle.as_ref().map(tokio::runtime::Handle::enter);
let instance = compose_native_core_instance(
@@ -239,9 +231,7 @@ mod tests {
let manager = native_instance_manager_with_runtime(caller.clone());
let mut runtimes = Vec::new();
let mut ids = Vec::new();
for (multi_thread, count, workers) in
[(false, 8, 1), (true, 4, 4), (true, 0, 2), (true, 1, 2)]
{
for (multi_thread, count, workers) in [(false, 8, 1), (true, 4, 4)] {
let config = isolated_config(multi_thread, count);
let id = manager
.run_network_instance(config, ConfigFileControl::STATIC_CONFIG)
@@ -321,89 +311,6 @@ mod tests {
}
}
#[cfg(feature = "management")]
#[tokio::test]
async fn shared_runtime_survives_instance_deletion_and_stops_with_last_owner() {
for workers in [1, 4] {
let manager = native_cli_instance_manager(true, workers).unwrap();
let first = manager.create(isolated_config(false, 8), ()).unwrap();
let second = manager.create(isolated_config(true, 2), ()).unwrap();
let runtime = first.runtime_handle().unwrap();
assert_ne!(runtime.id(), tokio::runtime::Handle::current().id());
assert_eq!(runtime.id(), second.runtime_handle().unwrap().id());
assert_eq!(runtime.runtime_flavor(), RuntimeFlavor::MultiThread);
assert_eq!(runtime.metrics().num_workers(), workers as usize);
first.start().await.unwrap();
second.start().await.unwrap();
second
.add_connector(format!("ring://{}", first.instance_id()).parse().unwrap())
.unwrap();
tokio::time::timeout(Duration::from_secs(10), async {
while !first.connected_peers().await.contains(&second.peer_id())
|| !second.connected_peers().await.contains(&first.peer_id())
{
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await
.expect("instances must connect on the shared runtime");
manager
.delete_network_instances([first.instance_id()])
.await
.unwrap();
assert_eq!(first.state(), CoreInstanceState::Stopped);
drop(first);
assert_eq!(second.state(), CoreInstanceState::Running);
tokio::time::timeout(
Duration::from_secs(5),
runtime.spawn(async {
tokio::time::sleep(Duration::from_millis(10)).await;
}),
)
.await
.unwrap()
.unwrap();
let added = manager.create(isolated_config(false, 16), ()).unwrap();
assert_eq!(runtime.id(), added.runtime_handle().unwrap().id());
added.start().await.unwrap();
added
.add_connector(format!("ring://{}", second.instance_id()).parse().unwrap())
.unwrap();
tokio::time::timeout(Duration::from_secs(10), async {
while !second.connected_peers().await.contains(&added.peer_id())
|| !added.connected_peers().await.contains(&second.peer_id())
{
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await
.expect("deleting one instance must leave the others functional");
manager
.delete_network_instances([added.instance_id(), second.instance_id()])
.await
.unwrap();
drop(added);
assert_eq!(second.state(), CoreInstanceState::Stopped);
// A surviving instance owner keeps the executor alive after its manager.
let task = runtime.spawn(pending::<()>());
drop(manager);
runtime.spawn(async {}).await.unwrap();
assert!(!task.is_finished());
drop(second);
assert!(
tokio::time::timeout(Duration::from_secs(5), task)
.await
.expect("the last owner must shut down the shared executor")
.unwrap_err()
.is_cancelled()
);
}
}
#[test]
fn direct_instance_outlives_its_callers_runtime() {
for multi_thread in [false, true] {
+4 -4
View File
@@ -1,4 +1,4 @@
//! Native ownership and continuous driving of an instance or shared Tokio runtime.
//! Native ownership and continuous driving of one instance's Tokio runtime.
use std::sync::mpsc;
@@ -22,7 +22,7 @@ impl NativeInstanceExecutor {
.spawn(move || {
let mut builder = if multi_thread {
let mut builder = Builder::new_multi_thread();
builder.worker_threads(1.max(thread_count as usize));
builder.worker_threads(2.max(thread_count as usize));
builder
} else {
Builder::new_current_thread()
@@ -67,8 +67,8 @@ mod tests {
async fn honors_runtime_flavor_and_worker_count() {
for (multi_thread, count, expected_workers) in [
(false, 8, 1),
(true, 0, 1),
(true, 1, 1),
(true, 0, 2),
(true, 1, 2),
(true, 2, 2),
(true, 4, 4),
] {
+2 -2
View File
@@ -41,7 +41,7 @@ pub(crate) struct NativeInstanceRuntimeHost {
cancel: CancellationToken,
event_journal: EventJournal,
tun: NativeTunRuntime,
executor: Option<Arc<NativeInstanceExecutor>>,
executor: Option<NativeInstanceExecutor>,
}
impl NativeInstanceRuntimeHost {
@@ -61,7 +61,7 @@ impl NativeInstanceRuntimeHost {
pub(super) fn new_with_executor(
global_ctx: ArcGlobalCtx,
#[cfg(feature = "tun")] shared_virtual_nic_registry: ArcSharedVirtualNicRegistry,
executor: Option<Arc<NativeInstanceExecutor>>,
executor: Option<NativeInstanceExecutor>,
) -> Arc<Self> {
let cancel = CancellationToken::new();
let tun = NativeTunRuntime::new(
+24 -1
View File
@@ -303,7 +303,14 @@ pub(crate) async fn bind_tcp_listener(
let addr = bind_options.local_addr.ok_or_else(|| {
TunnelError::InvalidAddr("tcp listener requires a local bind address".to_owned())
})?;
if bind_options.bind_device.is_none() && purpose == TcpListenPurpose::PortLease {
if bind_options.bind_device.is_none()
&& matches!(
purpose,
TcpListenPurpose::PortLease | TcpListenPurpose::PortForward
)
{
// Port forwards follow the bind IP across interface recreation. An
// implicit device binding would retain the deleted interface's index.
bind_options.bind_device = Some(String::new());
}
let listener = create_tcp_socket(addr, &bind_options).await?.listen(1024)?;
@@ -363,6 +370,22 @@ mod tests {
use super::*;
#[cfg(target_os = "linux")]
#[rstest::rstest]
#[tokio::test]
async fn port_forward_listener_honors_device_binding(
#[values(None, Some("lo"), Some(""))] device: Option<&str>,
) {
let mut options = TcpListenOptions::port_forward("127.0.0.1:0".parse().unwrap());
options.bind.bind_device = device.map(str::to_owned);
let listener = bind_tcp_listener(options).await.unwrap();
let bound = SockRef::from(&listener.listener).device().unwrap();
assert_eq!(
bound.as_deref(),
device.filter(|name| !name.is_empty()).map(str::as_bytes)
);
}
#[tokio::test]
async fn runtime_tcp_owned_split_preserves_full_duplex_and_shutdown() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
+12 -8
View File
@@ -146,11 +146,11 @@ impl RuntimeUdpSocketFactory {
return BindDev::from(bind_device.as_str());
}
// Port forwards bind only their local address by default, so replacing
// its interface does not strand the socket on the old interface index.
if matches!(
options.purpose,
UdpSocketPurpose::DirectConnect
| UdpSocketPurpose::PortBoundListener
| UdpSocketPurpose::PortForward
UdpSocketPurpose::DirectConnect | UdpSocketPurpose::PortBoundListener
) {
return BindDev::Auto;
}
@@ -438,7 +438,7 @@ mod tests {
));
assert!(matches!(
factory.bind_device_for(&UdpBindOptions::port_forward(listener_addr)),
BindDev::Auto
BindDev::Disabled
));
assert!(matches!(
factory.bind_device_for(&UdpBindOptions::port_lease(listener_addr)),
@@ -450,12 +450,16 @@ mod tests {
));
}
#[test]
fn factory_applies_listener_bind_device_option() {
#[rstest::rstest]
fn factory_applies_listener_bind_device_option(#[values(false, true)] port_forward: bool) {
let listener_addr = SocketAddr::from(([0, 0, 0, 0], 11010));
let factory = RuntimeUdpSocketFactory::new();
let options = UdpBindOptions::port_bound_listener(listener_addr)
.with_bind_device(Some("eth0".to_owned()));
let options = if port_forward {
UdpBindOptions::port_forward(listener_addr)
} else {
UdpBindOptions::port_bound_listener(listener_addr)
}
.with_bind_device(Some("eth0".to_owned()));
match factory.bind_device_for(&options) {
BindDev::Custom(dev) => assert_eq!(dev, "eth0"),
-8
View File
@@ -46,14 +46,6 @@ pub(crate) async fn recv_from_with_dst_ip(
loop {
match recv_from_with_dst_ip_once(socket, buf) {
Err(err) if err.kind() == io::ErrorKind::Interrupted => continue,
// WSARecvMsg consumed this oversized datagram. Discard it
// just as the Unix adapter discards MSG_TRUNC packets.
Err(err)
if err.raw_os_error()
== Some(windows::Win32::Networking::WinSock::WSAEMSGSIZE.0) =>
{
continue;
}
ret => break ret,
}
}
+102 -247
View File
@@ -42,7 +42,6 @@ use crate::{
},
tunnel::common::tests::{
_tunnel_bench_netns, _tunnel_pingpong_netns_with_timeout, wait_for_condition,
wait_for_condition_named,
},
};
@@ -457,8 +456,6 @@ fn connect_ns_to_bridge(ns: &str, guest_if: &str, host_if: &str, bridge: &str) {
struct PublicIpv6Lab {
network: TestNetwork,
#[cfg(feature = "tun")]
shared_virtual_nic_registry: crate::instance::shared_virtual_nic::ArcSharedVirtualNicRegistry,
}
#[derive(Clone, Copy)]
@@ -645,30 +642,7 @@ impl PublicIpv6Lab {
"net.ipv6.conf.all.forwarding=1",
);
Self {
network,
#[cfg(feature = "tun")]
shared_virtual_nic_registry: Instance::new_shared_virtual_nic_registry(),
}
}
fn new_instance(
&self,
config: TomlConfigLoader,
process_runtime: Arc<CoreProcessRuntime>,
) -> Instance {
// Reconnect through the same registry, as NativeInstanceFactory does,
// so old member cleanup and new registration share the TUN owner.
#[cfg(feature = "tun")]
{
Instance::new_with_process_runtime_and_shared_virtual_nic_registry(
config,
process_runtime,
self.shared_virtual_nic_registry.clone(),
)
}
#[cfg(not(feature = "tun"))]
Instance::new_with_process_runtime(config, process_runtime)
Self { network }
}
}
@@ -727,8 +701,8 @@ async fn init_public_ipv6_two_node_with_topology(
);
client_cfg.set_ipv6_public_addr_auto(true);
let mut provider = lab.new_instance(provider_cfg, process_runtime.clone());
let mut client = lab.new_instance(client_cfg, process_runtime.clone());
let mut provider = Instance::new_with_process_runtime(provider_cfg, process_runtime.clone());
let mut client = Instance::new_with_process_runtime(client_cfg, process_runtime.clone());
provider.run().await.unwrap();
client.run().await.unwrap();
@@ -1010,7 +984,7 @@ pub async fn public_ipv6_auto_addr_reconnect_reuses_same_address() {
client_id,
);
client_cfg.set_ipv6_public_addr_auto(true);
let mut client = lab.new_instance(client_cfg, process_runtime.clone());
let mut client = Instance::new_with_process_runtime(client_cfg, process_runtime.clone());
client.run().await.unwrap();
provider.add_connector_url("tcp://10.1.1.2:11010".parse().unwrap());
@@ -2879,161 +2853,6 @@ pub async fn foreign_network_functional_cluster() {
drop_insts(vec![center_inst1, center_inst2, inst1, inst2]).await;
}
#[rstest::rstest]
#[tokio::test]
#[serial_test::serial]
async fn foreign_network_advertises_listeners_for_direct_connect(
#[values("tcp", "udp")] protocol: &str,
) {
use easytier_proto::{
peer_rpc::{DirectConnectorRpc, DirectConnectorRpcClientFactory, GetIpListRequest},
rpc_types::controller::BaseController,
};
let mut network = TestNetwork::new();
network.create_bridge("br");
let process_runtime = CoreProcessRuntime::new();
let mut instances = Vec::new();
for (index, name) in ["server_a", "server_b", "client_a", "client_b"]
.into_iter()
.enumerate()
{
network.create_namespace(
name,
&format!("10.81.0.{}/24", index + 1),
&format!("fd81::{}/64", index + 1),
);
network.add_to_bridge("br", name);
let config = TomlConfigLoader::default();
config.set_inst_name(name.to_owned());
config.set_netns(Some(network.namespace(name).to_owned()));
config.set_network_identity(NetworkIdentity::new(
if index < 2 {
"public-net"
} else {
"client-net"
}
.to_owned(),
String::new(),
));
config.set_listeners(if index < 2 {
// Keep listener discovery independent of interface collection in netns.
vec![
format!("{protocol}://10.81.0.{}:11010", index + 1)
.parse()
.unwrap(),
]
} else {
Vec::new()
});
let mut flags = config.get_flags();
flags.no_tun = true;
flags.enable_ipv6 = false;
flags.disable_udp_hole_punching = true;
flags.disable_tcp_hole_punching = true;
flags.relay_all_peer_rpc = true;
if index == 1 {
// The second server only relays RPC, as in the reported topology.
flags.relay_network_whitelist = String::new();
flags.disable_relay_data = true;
}
config.set_flags(flags);
assert!(config.get_mapped_listeners().is_empty());
let mut instance = Instance::new_with_process_runtime(config, process_runtime.clone());
instance.run().await.unwrap();
set_foreign_network_refresh_interval(&instance, 1).await;
instances.push(instance);
}
let [server_a, server_b, client_a, client_b] = instances.as_slice() else {
unreachable!()
};
// Bootstrap only the cluster link and each client's local server.
// Connections to the other server must use its advertised TCP/UDP listener.
server_a.add_connector_url(server_b.ring_listener_url());
client_a.add_connector_url(server_a.ring_listener_url());
client_b.add_connector_url(server_b.ring_listener_url());
for (client, remote_server) in [(client_a, server_b), (client_b, server_a)] {
let client = client.get_core_instance();
let remote_server = remote_server.get_core_instance();
wait_for_condition_named(
|| async {
remote_server
.foreign_network_snapshots(false)
.await
.contains_key("client-net")
},
Duration::from_secs(10),
"client-net foreign network snapshot on remote server",
)
.await;
let remote_peer_id = remote_server.foreign_network_snapshots(false).await["client-net"]
.my_peer_id_for_this_network;
wait_for_condition_named(
|| async {
client
.route_snapshots()
.await
.iter()
.any(|route| route.peer_id == remote_peer_id)
},
Duration::from_secs(10),
"route to remote peer on client",
)
.await;
// The reply is routed back by the remote server, so wait until it has
// learned the client peer in the client network as well.
let client_peer_id = client.peer_id();
wait_for_condition_named(
|| async {
remote_server
.foreign_network_has_gateway_for_test("client-net", client_peer_id)
.await
.unwrap_or(false)
},
Duration::from_secs(10),
"client peer learned by remote server in client-net",
)
.await;
let rpc = client
.peer_manager()
.get_peer_rpc_mgr()
.rpc_client()
.scoped_client::<DirectConnectorRpcClientFactory<BaseController>>(
client.peer_id(),
remote_peer_id,
"client-net".to_owned(),
);
let listeners = rpc
.get_ip_list(BaseController::default(), GetIpListRequest {})
.await
.unwrap()
.listeners
.into_iter()
.map(url::Url::from)
.collect::<Vec<_>>();
assert_eq!(listeners, remote_server.running_listeners());
wait_for_condition_named(
|| async {
client.peer_snapshots().await.iter().any(|peer| {
peer.peer_id == remote_peer_id
&& peer.conns.iter().any(|conn| {
conn.is_client
&& conn
.tunnel
.as_ref()
.is_some_and(|tunnel| tunnel.tunnel_type == protocol)
})
})
},
Duration::from_secs(10),
"direct connection over {protocol}",
)
.await;
}
drop_insts(instances).await;
}
#[rstest::rstest]
#[tokio::test]
#[serial_test::serial]
@@ -3118,6 +2937,84 @@ pub async fn manual_reconnector(#[values(true, false)] is_foreign: bool) {
drop_insts(vec![center_inst, inst1, inst2]).await;
}
#[rstest::rstest]
#[tokio::test]
#[serial_test::serial]
async fn port_forward_survives_interface_recreation(#[values("tcp", "udp")] proto: &str) {
let network = prepare_linux_namespaces();
let namespace = network.namespace("net_a");
let create_tun = || {
run_ip_in_ns(
namespace,
&["tuntap", "add", "dev", "pf_tun", "mode", "tun"],
);
run_ip_in_ns(namespace, &["addr", "add", "192.0.2.1/24", "dev", "pf_tun"]);
run_ip_in_ns(namespace, &["link", "set", "pf_tun", "up"]);
run_ip_in_ns_output(namespace, &["-o", "link", "show", "pf_tun"])
.split(':')
.next()
.unwrap()
.to_owned()
};
let original_index = create_tun();
let insts = init_three_node_ex(
&network,
"udp",
|cfg| {
if cfg.get_inst_name() == "inst1" {
cfg.set_port_forwards(vec![PortForwardConfig {
bind_addr: "192.0.2.1:23456".parse().unwrap(),
dst_addr: "10.144.144.3:23456".parse().unwrap(),
proto: proto.to_owned(),
}]);
}
cfg
},
false,
)
.await;
for recreated in [false, true] {
if recreated {
run_ip_in_ns(namespace, &["link", "del", "pf_tun"]);
assert_ne!(create_tun(), original_index);
}
// Keep the forwarding instance and its listener alive across deletion.
// Reusing the interface name and IP must not leave it bound to the old
// ifindex. Check an application round trip, including the return path.
let listener_url = format!("{proto}://0.0.0.0:23456").parse().unwrap();
let connector_url = format!("{proto}://192.0.2.1:23456").parse().unwrap();
let server_ns = NetNS::new(Some(network.namespace("net_c").into()));
let client_ns = NetNS::new(Some(namespace.into()));
let payload = b"port-forward interface recreation".to_vec();
let timeout = Duration::from_secs(5);
let result = if proto == "tcp" {
_tunnel_pingpong_netns_with_timeout(
core_tcp_listener(listener_url),
core_tcp_dialer(connector_url),
server_ns,
client_ns,
payload,
timeout,
)
.await
} else {
_tunnel_pingpong_netns_with_timeout(
core_udp_listener(listener_url),
core_udp_dialer(connector_url),
server_ns,
client_ns,
payload,
timeout,
)
.await
};
assert!(result.is_ok(), "{proto}, recreated={recreated}: {result:?}");
}
drop_insts(insts).await;
}
#[rstest::rstest]
#[tokio::test]
#[serial_test::serial]
@@ -3408,26 +3305,18 @@ pub async fn relay_bps_limit_test(#[values(100, 200, 400, 800)] bps_limit: u64)
let tcp_listener = core_tcp_listener("tcp://0.0.0.0:22223".parse().unwrap());
let tcp_connector = core_tcp_dialer("tcp://10.144.144.3:22223".parse().unwrap());
// The relay keys its forward limiter by the peer network name, which is
// the dialing instance's identity, not inst2's own "public" one.
let relay_network = insts[0].get_core_instance().network_name_for_test();
let meter = || relay_forward_metered_bytes(&insts[1], &relay_network);
let (bps, metered) = _tunnel_bench_netns(
let bps = _tunnel_bench_netns(
tcp_listener,
tcp_connector,
NetNS::new(Some(network.namespace("net_c").into())),
NetNS::new(Some(network.namespace("net_a").into())),
Some(&meter),
)
.await;
let metered = metered.expect("metered window should be sampled");
println!(
"payload bps: {}, metered: {} over {:?}",
bps, metered.0, metered.1
);
assert_limited_bps(metered, bps, bps_limit);
println!("bps: {}", bps);
let bps = bps as u64 / 1024;
assert_limited_payload_bps(bps, bps_limit);
drop_insts(insts).await;
}
@@ -3455,67 +3344,33 @@ pub async fn instance_recv_bps_limit_test(#[values(100, 800)] bps_limit: u64) {
let tcp_listener = core_tcp_listener("tcp://0.0.0.0:22223".parse().unwrap());
let tcp_connector = core_tcp_dialer("tcp://10.144.144.3:22223".parse().unwrap());
let meter = || instance_recv_metered_bytes(&insts[1]);
let (bps, metered) = _tunnel_bench_netns(
let bps = _tunnel_bench_netns(
tcp_listener,
tcp_connector,
NetNS::new(Some(network.namespace("net_c").into())),
NetNS::new(Some(network.namespace("net_a").into())),
Some(&meter),
)
.await;
let metered = metered.expect("metered window should be sampled");
println!(
"payload bps: {}, metered: {} over {:?}",
bps, metered.0, metered.1
);
assert_limited_bps(metered, bps, bps_limit);
println!("bps: {}", bps);
let bps = bps as u64 / 1024;
assert_limited_payload_bps(bps, bps_limit);
drop_insts(insts).await;
}
fn relay_forward_metered_bytes(inst: &Instance, network_name: &str) -> u64 {
inst.get_core_instance()
.foreign_forward_metered_bytes_for_test(network_name)
.expect("relay forward limiter should be configured")
}
fn instance_recv_metered_bytes(inst: &Instance) -> u64 {
inst.get_core_instance()
.instance_recv_metered_bytes_for_test()
.expect("instance receive limiter should be configured")
}
fn assert_limited_bps(metered: (u64, Duration), payload_bps: usize, bps_limit: u64) {
// The limiter charges each packet with its EasyTier payload length, which
// includes the inner IP and transport headers, so its counter is the rate
// it actually enforced. The benchmark only sees TCP application payload,
// so comparing the two directly made this assertion flaky. The window
// starts once the initial bucket burst is drained.
let (bytes, window) = metered;
let expected = bps_limit as f64 * 1024.0 * window.as_secs_f64();
let slack = expected * 0.05;
fn assert_limited_payload_bps(bps: u64, bps_limit: u64) {
// The benchmark measures TCP application payload while the limiter counts
// EasyTier data payload, including the inner IP and transport headers.
let min_bps = bps_limit.saturating_sub((bps_limit / 10).max(50));
let max_bps = bps_limit + 50;
assert!(
(bytes as f64) >= expected - slack && (bytes as f64) <= expected + slack,
"metered: {} over {:?}, expected: {:.0}..={:.0}",
bytes,
window,
expected - slack,
expected + slack
);
// Encapsulation only shrinks the payload the receiver sees, so a limiter
// whose rejection the forwarding path ignores still shows up here. The
// benchmark rounds its window down to whole seconds, which inflates the
// payload rate by a few percent, hence the same 50 KiB/s headroom.
let payload_bps = payload_bps as u64 / 1024;
assert!(
payload_bps <= bps_limit + 50,
"payload bps: {}, expected: <= {}",
payload_bps,
bps_limit + 50
bps >= min_bps && bps <= max_bps,
"bps: {}, expected: {}..={}",
bps,
min_bps,
max_bps
);
}
+9 -110
View File
@@ -1,4 +1,5 @@
use bon::builder;
use network_interface::NetworkInterfaceConfig as _;
use std::net::{IpAddr, SocketAddr};
use crate::common::netns::NetNS;
@@ -9,18 +10,11 @@ pub(crate) fn get_interface_name_by_ip(local_ip: &IpAddr) -> Option<String> {
if local_ip.is_unspecified() || local_ip.is_multicast() {
return None;
}
#[cfg(target_os = "windows")]
let ifaces = crate::common::network::get_interfaces_cached()?;
// Enumerate under the caller's namespace guard on non-Windows platforms.
#[cfg(not(target_os = "windows"))]
let ifaces = {
use network_interface::NetworkInterfaceConfig;
network_interface::NetworkInterface::show().ok()?
};
for iface in ifaces.iter() {
for addr in &iface.addr {
let ifaces = network_interface::NetworkInterface::show().ok()?;
for iface in ifaces {
for addr in iface.addr {
if addr.ip() == *local_ip {
return Some(iface.name.clone());
return Some(iface.name);
}
}
}
@@ -250,8 +244,6 @@ pub async fn bind<B: Bindable>(
#[cfg(test)]
pub(crate) mod tests {
use atomic_shim::AtomicU64;
#[cfg(target_os = "windows")]
use std::time::Duration;
use std::{sync::Arc, time::Instant};
use anyhow::Context as _;
@@ -269,74 +261,6 @@ pub(crate) mod tests {
use crate::common::netns::NetNS;
#[cfg(target_os = "windows")]
#[test]
fn interface_snapshot_lookup_smoke_test() {
// The process-global cache may expire between reads. Identity checks
// belong to the isolated helper tests in common::network instead.
for _ in 0..2 {
let ifaces = crate::common::network::get_interfaces_cached()
.expect("interface lookup should work");
assert!(!ifaces.is_empty());
}
}
// Diagnostic probe for the issue #2735 slow candidate loop: run with
// `cargo test -p easytier --lib probe_interface_lookup -- --ignored --nocapture`.
// Compare only IP-to-name and name-to-index lookups, not socket binding.
#[cfg(target_os = "windows")]
#[test]
#[ignore = "diagnostic probe; enumerates live Windows adapters"]
fn probe_interface_lookup_latency_with_and_without_cache() {
use network_interface::{NetworkInterface, NetworkInterfaceConfig};
use std::net::IpAddr;
type EnumerateInterfaces = fn() -> Option<Arc<Vec<NetworkInterface>>>;
fn lookup_index(ip: IpAddr, enumerate: EnumerateInterfaces) -> Option<u32> {
let ifaces = enumerate()?;
let name = &ifaces
.iter()
.find(|iface| iface.addr.iter().any(|addr| addr.ip() == ip))?
.name;
let ifaces = enumerate()?;
ifaces
.iter()
.find(|iface| iface.name == *name)
.map(|iface| iface.index)
}
let ifaces = NetworkInterface::show().unwrap();
let candidates: Vec<_> = ifaces
.iter()
.flat_map(|iface| {
iface.addr.iter().filter_map(move |addr| {
matches!(addr.ip(), IpAddr::V4(_)).then(|| (iface.name.clone(), addr.ip()))
})
})
.collect();
println!("IPv4 lookup candidates ({}):", candidates.len());
let providers: [(&str, EnumerateInterfaces); 2] = [
("uncached", || NetworkInterface::show().ok().map(Arc::new)),
("cached", crate::common::network::get_interfaces_cached),
];
for (label, enumerate) in providers {
let mut total = Duration::ZERO;
for (name, ip) in &candidates {
let start = Instant::now();
let index = lookup_index(*ip, enumerate);
let elapsed = start.elapsed();
total += elapsed;
println!(" [{label}] {ip:>16} ({name}): lookup {elapsed:?}, index={index:?}");
}
println!(
"{label} lookup total: {total:?} ({} candidates)",
candidates.len()
);
}
}
#[tokio::test]
async fn bind_rejects_occupied_ports() {
for ip in ["127.0.0.1", "::1"] {
@@ -647,8 +571,7 @@ pub(crate) mod tests {
connector: C,
netns_l: NetNS,
netns_c: NetNS,
meter: Option<&(dyn Fn() -> u64 + Sync)>,
) -> (usize, Option<(u64, std::time::Duration)>)
) -> usize
where
L: SocketListener<Accepted = Box<dyn Tunnel>> + Sync + 'static,
C: TunnelDialer,
@@ -695,23 +618,11 @@ pub(crate) mod tests {
}
let now = Instant::now();
let mut meter_base = None;
while now.elapsed().as_secs() < 10 {
// Skip the initial burst so the metered window reflects the
// sustained rate the limiter enforces.
if meter_base.is_none() && now.elapsed() >= std::time::Duration::from_secs(1) {
meter_base = meter.map(|meter| (meter(), now.elapsed()));
}
// send.feed(item)
let item = ZCPacket::new_with_payload(send_buf.as_ref());
send.feed(item).await.unwrap();
}
let metered = match (meter_base, meter) {
(Some((metered, started)), Some(meter)) => {
Some((meter().saturating_sub(metered), now.elapsed() - started))
}
_ => None,
};
send.close().await.unwrap();
drop(send);
@@ -723,25 +634,13 @@ pub(crate) mod tests {
println!("bps: {}", bps);
lis.abort();
(bps as usize, metered)
bps as usize
}
pub async fn wait_for_condition<F, FRet>(condition: F, timeout: std::time::Duration)
pub async fn wait_for_condition<F, FRet>(mut condition: F, timeout: std::time::Duration)
where
F: FnMut() -> FRet + Send,
FRet: Future<Output = bool>,
{
wait_for_condition_named(condition, timeout, "condition").await
}
/// Like [`wait_for_condition`], but the panic names the gate that timed out.
pub async fn wait_for_condition_named<F, FRet>(
mut condition: F,
timeout: std::time::Duration,
what: &str,
) where
F: FnMut() -> FRet + Send,
FRet: Future<Output = bool>,
{
let now = std::time::Instant::now();
while now.elapsed() < timeout {
@@ -750,6 +649,6 @@ pub(crate) mod tests {
}
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}
assert!(condition().await, "Timeout waiting for {what}")
assert!(condition().await, "Timeout")
}
}
+6 -2
View File
@@ -609,8 +609,12 @@ impl WgTunnelListener {
};
tracing::info!("New peer: {}", addr);
// Accepted sessions already keep their UDP layer alive until close.
let mut wg = WgPeer::new(Box::new(()), session, config.clone(), addr);
let mut wg = WgPeer::new(
Box::new(session_listener.clone()),
session,
config.clone(),
addr,
);
let (stream, sink) = wg.start_and_get_tunnel().split();
wg.spawn_session_recv_task(None);
let tunnel = Box::new(TunnelWrapper::new(
+24 -120
View File
@@ -9,7 +9,7 @@ mod engine;
use std::{
collections::{BTreeMap, BTreeSet},
fmt, io,
fmt,
net::{Ipv6Addr, SocketAddr, SocketAddrV6},
sync::{Arc, Mutex as StdMutex, RwLock, Weak},
};
@@ -32,51 +32,16 @@ use sha2::Sha256;
use tokio::{sync::mpsc, task::JoinSet};
use crate::{
common::global_ctx::ArcGlobalCtx, host_runtime::native_host_runtime,
common::global_ctx::ArcGlobalCtx,
host_runtime::{NativeHostRuntime, native_host_runtime},
socket::udp::RuntimeUdpSocket,
};
use self::engine::{DerivedClient, PortalEngine};
// Endpoints retain this family-specific slot, not a socket generation. Holding
// the read guard during send lets the reader release the old binding on failure.
struct PortalSocket {
socket: tokio::sync::RwLock<Option<Arc<RuntimeUdpSocket>>>,
options: UdpBindOptions,
}
impl PortalSocket {
async fn send_to(&self, data: &[u8], remote: SocketAddr) -> io::Result<usize> {
let socket = self.socket.read().await;
match socket.as_ref() {
Some(socket) => socket.send_to(data, remote).await,
None => Err(io::Error::new(
io::ErrorKind::WouldBlock,
"portal socket is rebinding",
)),
}
}
async fn rebind(&self) {
self.socket.write().await.take();
loop {
easytier_core::socket::retry::wait().await;
match native_host_runtime().bind_udp(self.options.clone()).await {
Ok(socket) => {
*self.socket.write().await = Some(socket);
return;
}
Err(error) => {
tracing::warn!(target: "CORE", %error, address = ?self.options.local_addr, "WireGuard portal UDP bind failed; retrying")
}
}
}
}
}
struct WireGuardPortalListener {
url: url::Url,
sockets: Vec<Arc<PortalSocket>>,
sockets: Vec<Arc<RuntimeUdpSocket>>,
receiver: mpsc::UnboundedReceiver<PortalSession>,
engine: Arc<PortalEngine>,
tasks: JoinSet<anyhow::Result<()>>,
@@ -101,24 +66,15 @@ impl SocketListener for WireGuardPortalListener {
return Ok(());
}
self.listened = true;
for socket in self.sockets.drain(..) {
for socket in &self.sockets {
let socket = socket.clone();
let engine = self.engine.clone();
self.tasks.spawn(async move {
loop {
let datagram = {
// Do not hold a read lock while waiting for traffic, or
// retain this generation while trying to rebind its port.
let current = socket.socket.read().await.as_ref().unwrap().clone();
current.recv_session_datagram().await
};
let datagram = match datagram {
Ok(datagram) => datagram,
Err(error) => {
tracing::warn!(target: "CORE", %error, address = ?socket.options.local_addr, "WireGuard portal UDP socket failed; rebinding");
socket.rebind().await;
continue;
}
};
let datagram = socket
.recv_session_datagram()
.await
.context("WireGuard portal UDP receive failed")?;
engine
.handle_datagram(socket.clone(), datagram.remote_addr, &datagram.payload)
.await;
@@ -235,21 +191,20 @@ impl WireGuardPortalHost {
async fn bind_socket(
&self,
runtime: &NativeHostRuntime,
address: SocketAddr,
only_v6: bool,
) -> anyhow::Result<Arc<PortalSocket>> {
) -> anyhow::Result<Arc<RuntimeUdpSocket>> {
let context = SocketContext::default()
.with_socket_mark(self.global_ctx.get_flags().socket_mark)
.with_netns(self.global_ctx.net_ns.name().map(NetNamespace::new));
let mut options = UdpBindOptions::port_bound_listener(address)
.with_context(context)
.with_only_v6(only_v6);
let socket = native_host_runtime().bind_udp(options.clone()).await?;
options.local_addr = Some(socket.local_addr()?);
Ok(Arc::new(PortalSocket {
socket: tokio::sync::RwLock::new(Some(socket)),
options,
}))
runtime
.bind_udp(
UdpBindOptions::port_bound_listener(address)
.with_context(context)
.with_only_v6(only_v6),
)
.await
}
async fn apply_client_updates(&self, clients: &[PortalClientConfig]) -> anyhow::Result<()> {
@@ -330,15 +285,16 @@ impl PortalHost for WireGuardPortalHost {
.setup
.as_ref()
.map_err(|error| anyhow::anyhow!(error.clone()))?;
let runtime = native_host_runtime();
let primary = self
.bind_socket(self.config.wireguard_listen, false)
.bind_socket(&runtime, self.config.wireguard_listen, false)
.await
.context("failed to bind WireGuard VPN portal")?;
let local = primary.options.local_addr.unwrap();
let mut sockets = vec![primary];
let local = primary.local_addr()?;
let mut sockets = vec![primary.clone()];
if let Some(v6_address) =
secondary_ipv6_bind_address(self.config.wireguard_listen, local.port())
&& let Ok(v6) = self.bind_socket(v6_address, true).await
&& let Ok(v6) = self.bind_socket(&runtime, v6_address, true).await
{
sockets.push(v6);
}
@@ -435,58 +391,6 @@ pub(crate) fn test_wireguard_keys(
mod tests {
use super::*;
#[tokio::test]
async fn portal_socket_rebinds_while_endpoints_keep_their_reference() {
let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
let mut options = UdpBindOptions::port_bound_listener("127.0.0.1:0".parse().unwrap())
.with_need_protect(false);
let initial = native_host_runtime()
.bind_udp(options.clone())
.await
.unwrap();
let local = initial.local_addr().unwrap();
options.local_addr = Some(local);
let old_socket = Arc::downgrade(&initial);
let socket = Arc::new(PortalSocket {
socket: tokio::sync::RwLock::new(Some(initial)),
options,
});
let endpoint = socket.clone();
let mut rebind = Box::pin(socket.rebind());
assert!(futures::poll!(&mut rebind).is_pending());
assert!(old_socket.upgrade().is_none());
assert_eq!(
endpoint
.send_to(b"during-rebind", client.local_addr().unwrap())
.await
.unwrap_err()
.kind(),
io::ErrorKind::WouldBlock
);
assert!(
tokio::time::timeout(std::time::Duration::from_millis(10), rebind)
.await
.is_err()
);
assert!(socket.socket.read().await.is_none());
socket.rebind().await;
endpoint
.send_to(b"after-rebind", client.local_addr().unwrap())
.await
.unwrap();
let mut buffer = [0; 32];
let (len, source) = tokio::time::timeout(
std::time::Duration::from_secs(1),
client.recv_from(&mut buffer),
)
.await
.unwrap()
.unwrap();
assert_eq!(&buffer[..len], b"after-rebind");
assert_eq!(source, local);
}
#[test]
fn named_wireguard_keys_are_stable_and_client_scoped() {
let master = [7; 32];
+10 -14
View File
@@ -18,7 +18,10 @@ use boringtun::{
},
x25519::{PublicKey, StaticSecret},
};
use easytier_core::gateway::vpn_portal::{PortalClientConfig, PortalSession};
use easytier_core::{
gateway::vpn_portal::{PortalClientConfig, PortalSession},
socket::udp::VirtualUdpSocket,
};
use rand::rngs::OsRng;
use tokio::{
sync::{Mutex, mpsc, watch},
@@ -26,7 +29,7 @@ use tokio::{
};
use tokio_util::sync::CancellationToken;
use super::PortalSocket;
use crate::socket::udp::RuntimeUdpSocket;
const MIN_WIREGUARD_PACKET_CAPACITY: usize = 148;
// We pre-verify through this shared limiter and BoringTun verifies again inside
// each Tunn. Doubling the threshold preserves the intended 100 datagrams/s
@@ -92,12 +95,12 @@ impl EngineSlots {
#[derive(Clone)]
struct Endpoint {
socket: Arc<PortalSocket>,
socket: Arc<RuntimeUdpSocket>,
remote: SocketAddr,
}
impl ClientSession {
fn update_endpoint(&mut self, socket: Arc<PortalSocket>, remote: SocketAddr) {
fn update_endpoint(&mut self, socket: Arc<RuntimeUdpSocket>, remote: SocketAddr) {
let changed = self
.endpoint
.as_ref()
@@ -220,7 +223,7 @@ impl PortalEngine {
pub(super) async fn handle_datagram(
self: &Arc<Self>,
socket: Arc<PortalSocket>,
socket: Arc<RuntimeUdpSocket>,
remote: SocketAddr,
datagram: &[u8],
) {
@@ -372,7 +375,7 @@ impl PortalEngine {
fn new_session(
self: &Arc<Self>,
slot: &Arc<ClientSlot>,
socket: Arc<PortalSocket>,
socket: Arc<RuntimeUdpSocket>,
remote: SocketAddr,
) -> ClientSession {
let generation = slot.next_generation.fetch_add(1, Ordering::Relaxed);
@@ -382,7 +385,7 @@ impl PortalEngine {
let engine = Arc::downgrade(self);
let slot_for_task = Arc::downgrade(slot);
let mut tasks = JoinSet::new();
let forward_packets = async move {
tasks.spawn(async move {
while let Some(payload) = to_client.recv().await {
let Some(engine) = engine.upgrade() else {
return;
@@ -397,13 +400,6 @@ impl PortalEngine {
if let (Some(engine), Some(slot)) = (engine.upgrade(), slot_for_task.upgrade()) {
engine.expire_if_current(slot, generation).await;
}
};
let cancel = self.cancel.clone();
tasks.spawn(async move {
tokio::select! {
_ = cancel.cancelled() => {},
_ = forward_packets => {},
}
});
ClientSession {
generation,
+1 -5
View File
@@ -10,10 +10,7 @@ cd "$repository_root"
readonly target_dir="${CARGO_TARGET_DIR:-target}"
readonly raw_artifact="${target_dir}/wasm32-wasip1/release/easytier_core.wasm"
readonly artifact="${target_dir}/wasm32-wasip1/release/easytier_core_go_host.wasm"
# The WASI host performs AEAD for the guest, and the guest has no TLS stack of its own, so the
# default feature set (endpoint-discovery -> rustls -> ring) must stay off: ring compiles C
# shims into wasm32-wasip1 that become env.ring_core_* imports, which the Go host cannot provide.
readonly core_features="management,proxy-smoltcp-stack,wasi-crypto-offload,aes-gcm,tcp-hole-punch"
readonly core_features="management-rpc,proxy-smoltcp-stack,ring-crypto,wasi-crypto-offload"
sha256_file() {
if command -v sha256sum >/dev/null; then
@@ -123,7 +120,6 @@ if [[ "$version" != *"version ${binaryen_version} "* ]]; then
fi
cargo build --release --target wasm32-wasip1 -p easytier-core \
--no-default-features \
--features "$core_features"
optimized="${artifact}.wasm-opt.$$"