Added DHT peer resolver, fixed MTU

This commit is contained in:
ab
2026-09-22 15:44:33 +03:00
parent 72eaf9a228
commit 9698f21d55
27 changed files with 2041 additions and 161 deletions
+9 -9
View File
@@ -58,23 +58,22 @@ pub const WIREGUARD_PROTOCOL: &str = "wg-quic";
/// Smallest interface MTU the overlay accepts.
///
/// 576 bytes is what IPv4 guarantees every host can reassemble (RFC 1122),
/// so nothing below it is worth offering. The floor used to be 1280 because
/// Linux tears IPv6 down on an interface below that; the overlay is IPv4
/// now, so that constraint is gone and a path with small datagrams — a
/// relay, typically — can be matched instead of warned about.
/// so nothing below it is worth offering. The current overlay is IPv4.
/// Lowering this is not needed to accommodate a narrow QUIC path: the default
/// transport fragments opaque payloads below the plugin.
pub const MIN_MTU: u32 = 576;
/// Default interface MTU.
///
/// Comfortably under what a direct path carries, and the same number the
/// overlay used before, so an existing network does not have to change.
/// Kept stable across path changes. The default transport splits encrypted
/// packets when the current QUIC path cannot carry them in one datagram.
pub const DEFAULT_MTU: u32 = 1280;
/// Bytes WireGuard adds to a packet: type and reserved, receiver index,
/// counter and the Poly1305 tag.
///
/// A link therefore has to carry `mtu + WIREGUARD_OVERHEAD` bytes in one
/// datagram for a full-size packet to get through.
/// A link must carry `mtu + WIREGUARD_OVERHEAD` logical payload bytes.
/// Transport fragmentation is independent of WireGuard and the inner IP flags.
pub const WIREGUARD_OVERHEAD: u32 = 32;
/// Configuration of the WireGuard plugin.
@@ -87,7 +86,8 @@ pub struct WireguardConfig {
/// The largest packet a tunnel will carry.
///
/// Not the interface MTU, which belongs to the agent: this is what this
/// protocol refuses to encrypt because it would not fit one datagram.
/// protocol is configured to carry. The transport may split ciphertext
/// into smaller datagrams without changing the original IP packet.
pub mtu: u32,
/// How long to coalesce changes before reconciling.
pub reconcile_debounce: Duration,
+95 -3
View File
@@ -23,12 +23,26 @@ use tsunagi::overlay::{
MemoryTun, MemoryTunFactory, OverlayError, TunDevice, TunFactory, TunRequest,
};
use tsunagi::state::Ipv4Range;
use tsunagi::testing::{config_with, network, settle, wait_event, wait_for_peers, wait_until};
use tsunagi::testing::{network, settle, wait_event, wait_for_peers, wait_until};
use tsunagi::{Agent, NetworkStatus};
use tsunagi_wg_quic::{
WIREGUARD_PROTOCOL, WgAnnouncement, WgSecretKey, WireguardConfig, WireguardPlugin,
};
/// Real QUIC at its minimum path MTU: loopback PMTU discovery must not hide
/// failures that occur on ordinary Internet paths. The TUN MTU stays at 1280.
fn config_with(root: &std::path::Path, discovery: &SharedMemoryDiscovery) -> tsunagi::AgentConfig {
let mut config = tsunagi::testing::config_with(root, discovery);
config.test_quic_transport = Some(
iroh::endpoint::QuicTransportConfig::builder()
.initial_mtu(1200)
.min_mtu(1200)
.mtu_discovery_config(None)
.build(),
);
config
}
/// A factory that claims the host and puts nothing on it.
///
/// This is the case the missing-address report exists for: a provisioner
@@ -291,6 +305,84 @@ fn ipv4_packet(source: Ipv4Addr, destination: Ipv4Addr, payload: &[u8]) -> Bytes
Bytes::from(packet)
}
#[tokio::test]
async fn full_size_tcp_packets_cross_the_default_overlay() {
let discovery = SharedMemoryDiscovery::new();
let (name, secret) = network("wg-default-mtu");
let a = WgAgent::spawn(&discovery, "tmtua").await;
let b = WgAgent::spawn(&discovery, "tmtub").await;
let id = a.agent.join_network(&name, &secret).await.unwrap();
b.agent.join_network(&name, &secret).await.unwrap();
a.wait_for_tunnels(id, 1).await;
b.wait_for_tunnels(id, 1).await;
let addr_a = a.overlay(id).await;
let addr_b = b.overlay(id).await;
let tun_a = a.tun(id).await;
let tun_b = b.tun(id).await;
assert_eq!(tun_a.mtu(), tsunagi_wg_quic::DEFAULT_MTU);
for size in [1280, 1279, 1098, 1097, 64] {
for (source, dest, from, to) in [
(addr_a, addr_b, &tun_a, &tun_b),
(addr_b, addr_a, &tun_b, &tun_a),
] {
let packet = tcp_packet(source, dest, size);
from.push_from_os(packet.clone());
let received = tokio::time::timeout(Duration::from_secs(5), to.pop_to_os())
.await
.expect("a full-size TCP packet must arrive at the default MTU")
.unwrap();
assert_eq!(received, packet, "including DF, TCP flags and checksums");
}
}
for peer in [&a, &b] {
let view = peer.plugin.overview(id).unwrap();
assert_eq!(
view.peers[0]
.tunnel
.as_ref()
.unwrap()
.stats
.dropped_oversize,
0
);
}
a.shutdown().await;
b.shutdown().await;
}
/// A checksummed IPv4/TCP segment with DF set, as a host's TCP stack sends it.
fn tcp_packet(source: Ipv4Addr, destination: Ipv4Addr, size: usize) -> Bytes {
fn checksum(bytes: &[u8]) -> u16 {
let mut sum: u32 = bytes
.chunks(2)
.map(|c| u16::from_be_bytes([c[0], *c.get(1).unwrap_or(&0)]) as u32)
.sum();
while sum > 0xffff {
sum = (sum & 0xffff) + (sum >> 16);
}
!(sum as u16)
}
assert!(size >= 40);
let mut packet = ipv4_packet(source, destination, &vec![0x5a; size - 20]).to_vec();
packet[6..8].copy_from_slice(&0x4000u16.to_be_bytes()); // don't fragment
packet[9] = 6; // TCP
packet[20..40].fill(0);
packet[20..22].copy_from_slice(&40000u16.to_be_bytes());
packet[22..24].copy_from_slice(&22u16.to_be_bytes());
packet[24..28].copy_from_slice(&1u32.to_be_bytes());
packet[32] = 5 << 4;
packet[33] = 0x18; // PSH, ACK
packet[34..36].copy_from_slice(&65535u16.to_be_bytes());
let mut pseudo = Vec::from(&packet[12..20]);
pseudo.extend_from_slice(&[0, 6]);
pseudo.extend_from_slice(&((size - 20) as u16).to_be_bytes());
pseudo.extend_from_slice(&packet[20..]);
packet[36..38].copy_from_slice(&checksum(&pseudo).to_be_bytes());
let ip_checksum = checksum(&packet[..20]);
packet[10..12].copy_from_slice(&ip_checksum.to_be_bytes());
Bytes::from(packet)
}
#[tokio::test]
async fn two_agents_carry_real_ip_packets_through_a_wireguard_tunnel() {
let discovery = SharedMemoryDiscovery::new();
@@ -1404,7 +1496,7 @@ async fn a_peer_with_no_direct_path_is_reached_through_one_that_has_both() {
// And a real packet crosses: A's interface to B's interface, through C.
a.tun(network_id)
.await
.push_from_os(ipv4_packet(a_addr, b_addr, b"through the middle"));
.push_from_os(tcp_packet(a_addr, b_addr, 1280));
let seen = tokio::time::timeout(
tsunagi::testing::DEADLINE,
b.tun(network_id).await.pop_to_os(),
@@ -1412,7 +1504,7 @@ async fn a_peer_with_no_direct_path_is_reached_through_one_that_has_both() {
.await
.expect("the packet should arrive")
.unwrap();
assert_eq!(&seen[20..], b"through the middle");
assert_eq!(seen, tcp_packet(a_addr, b_addr, 1280));
a.shutdown().await;
b.shutdown().await;