From 2c533efe671252f7faf80e047f349acfe3777a2d Mon Sep 17 00:00:00 2001 From: lbl <49143209+lbl8603@users.noreply.github.com> Date: Wed, 15 Jul 2026 12:43:47 +0800 Subject: [PATCH] feat: add packetdrill TCP dual-stack validation and fix retransmission, zero-window, and connection-closing state issues --- .github/workflows/rust.yml | 52 +- Cargo.toml | 3 +- README.md | 25 + examples/packetdrill_harness.rs | 724 ++++++++++++++++++ scripts/packetdrill_case.sh | 98 +++ scripts/run_packetdrill.sh | 91 +++ scripts/run_packetdrill_wsl.ps1 | 25 + scripts/setup_packetdrill.sh | 40 + src/tcp/sys.rs | 7 +- src/tcp/tcb.rs | 44 +- tests/packetdrill/README.md | 33 + .../packetdrill/ipv4/active_close_fin_rto.pkt | 27 + tests/packetdrill/ipv4/active_open.pkt | 24 + .../packetdrill/ipv4/active_open_syn_rto.pkt | 23 + tests/packetdrill/ipv4/delayed_ack.pkt | 23 + .../packetdrill/ipv4/fast_retransmit_sack.pkt | 33 + tests/packetdrill/ipv4/out_of_order_sack.pkt | 35 + tests/packetdrill/ipv4/passive_data_close.pkt | 30 + tests/packetdrill/ipv4/passive_smoke.pkt | 22 + .../ipv4/rst_and_duplicate_syn.pkt | 31 + tests/packetdrill/ipv4/rto_retransmit.pkt | 23 + tests/packetdrill/ipv4/simultaneous_close.pkt | 26 + tests/packetdrill/ipv4/zero_window_probe.pkt | 24 + .../packetdrill/ipv6/active_close_fin_rto.pkt | 26 + tests/packetdrill/ipv6/active_open.pkt | 23 + .../packetdrill/ipv6/active_open_syn_rto.pkt | 22 + tests/packetdrill/ipv6/delayed_ack.pkt | 22 + .../packetdrill/ipv6/fast_retransmit_sack.pkt | 32 + tests/packetdrill/ipv6/out_of_order_sack.pkt | 34 + tests/packetdrill/ipv6/passive_data_close.pkt | 29 + tests/packetdrill/ipv6/passive_smoke.pkt | 21 + .../ipv6/rst_and_duplicate_syn.pkt | 30 + tests/packetdrill/ipv6/rto_retransmit.pkt | 22 + tests/packetdrill/ipv6/simultaneous_close.pkt | 25 + tests/packetdrill/ipv6/zero_window_probe.pkt | 23 + 35 files changed, 1741 insertions(+), 31 deletions(-) create mode 100644 examples/packetdrill_harness.rs create mode 100644 scripts/packetdrill_case.sh create mode 100644 scripts/run_packetdrill.sh create mode 100644 scripts/run_packetdrill_wsl.ps1 create mode 100644 scripts/setup_packetdrill.sh create mode 100644 tests/packetdrill/README.md create mode 100644 tests/packetdrill/ipv4/active_close_fin_rto.pkt create mode 100644 tests/packetdrill/ipv4/active_open.pkt create mode 100644 tests/packetdrill/ipv4/active_open_syn_rto.pkt create mode 100644 tests/packetdrill/ipv4/delayed_ack.pkt create mode 100644 tests/packetdrill/ipv4/fast_retransmit_sack.pkt create mode 100644 tests/packetdrill/ipv4/out_of_order_sack.pkt create mode 100644 tests/packetdrill/ipv4/passive_data_close.pkt create mode 100644 tests/packetdrill/ipv4/passive_smoke.pkt create mode 100644 tests/packetdrill/ipv4/rst_and_duplicate_syn.pkt create mode 100644 tests/packetdrill/ipv4/rto_retransmit.pkt create mode 100644 tests/packetdrill/ipv4/simultaneous_close.pkt create mode 100644 tests/packetdrill/ipv4/zero_window_probe.pkt create mode 100644 tests/packetdrill/ipv6/active_close_fin_rto.pkt create mode 100644 tests/packetdrill/ipv6/active_open.pkt create mode 100644 tests/packetdrill/ipv6/active_open_syn_rto.pkt create mode 100644 tests/packetdrill/ipv6/delayed_ack.pkt create mode 100644 tests/packetdrill/ipv6/fast_retransmit_sack.pkt create mode 100644 tests/packetdrill/ipv6/out_of_order_sack.pkt create mode 100644 tests/packetdrill/ipv6/passive_data_close.pkt create mode 100644 tests/packetdrill/ipv6/passive_smoke.pkt create mode 100644 tests/packetdrill/ipv6/rst_and_duplicate_syn.pkt create mode 100644 tests/packetdrill/ipv6/rto_retransmit.pkt create mode 100644 tests/packetdrill/ipv6/simultaneous_close.pkt create mode 100644 tests/packetdrill/ipv6/zero_window_probe.pkt diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index f714202..9122922 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -8,22 +8,48 @@ env: jobs: build_n_test: - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest] - - runs-on: ${{ matrix.os }} + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy - name: rustfmt run: cargo fmt --all -- --check - - name: check - run: cargo check --verbose - name: clippy run: cargo clippy --all-targets --all-features -- -D warnings - - name: build - run: cargo build --verbose --examples --tests --all-features - - name: test - run: cargo test --all-features --examples \ No newline at end of file + - name: Default-feature tests + run: cargo test + - name: All-feature build + run: cargo build --all-targets --all-features + - name: All-feature library tests + run: cargo test --all-features --lib + - name: All-feature documentation tests + run: cargo test --all-features --doc + + packetdrill: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - name: Install packetdrill dependencies + run: | + sudo apt-get update + sudo apt-get install -y git gcc make bison flex python3 net-tools iptables + - name: Build pinned packetdrill + run: | + packetdrill_bin="$(bash scripts/setup_packetdrill.sh)" + echo "PACKETDRILL_BIN=${packetdrill_bin}" >> "$GITHUB_ENV" + - name: Build tcp_ip packetdrill harness + run: cargo build --example packetdrill_harness + - name: Run IPv4 and IPv6 packetdrill suites + run: sudo --preserve-env=PACKETDRILL_BIN env HOME="$HOME" TCP_IP_PD_SKIP_BUILD=1 bash scripts/run_packetdrill.sh + - name: Upload packetdrill failure logs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: packetdrill-logs + path: target/packetdrill/logs + if-no-files-found: ignore diff --git a/Cargo.toml b/Cargo.toml index cf9ba05..00e2fe8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ anyhow = "1" env_logger = "0.11" tun-rs = { version = "2.8.6", features = ["async"] } clap = { version = "4", features = ["derive"] } +libc = "0.2" [[example]] name = "tcp" @@ -49,4 +50,4 @@ name = "tcp_proxy" required-features = ["global-ip-stack"] [[example]] name = "tcp_connect" -required-features = ["global-ip-stack"] \ No newline at end of file +required-features = ["global-ip-stack"] diff --git a/README.md b/README.md index 072cf98..cfe6be0 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,31 @@ Use TcpListener and TcpStream. Supported over IPv4 and IPv6. - The timeout waiting time is fixed and can be configured - Selective acknowledgements permitted. (Proactively ACK the need for improvement) +### packetdrill TCP conformance tests + +The Linux-only suite under `tests/packetdrill` drives the userspace stack through +packetdrill's local TUN mode. It covers IPv4 and IPv6 handshake negotiation, data +and ACK behavior, reassembly/SACK, retransmission and fast recovery, zero-window +probing, close/TIME_WAIT paths, and RST/duplicate-SYN handling. + +On Ubuntu, run as root because the harness needs TUN, raw-socket, and firewall +access: + +```bash +sudo -E bash scripts/setup_packetdrill.sh --install-deps +sudo -E bash scripts/run_packetdrill.sh +``` + +On Windows with WSL2 Ubuntu 24.04: + +```powershell +pwsh scripts/run_packetdrill_wsl.ps1 +``` + +The setup script checks out the pinned upstream packetdrill revision into the +user cache. It does not vendor packetdrill into this repository. Per-case verbose +logs are written to `target/packetdrill/logs`. + #### Other Using IpSocket to send and receive packets of other protocols.(Handles all IP upper-layer protocols without requiring diff --git a/examples/packetdrill_harness.rs b/examples/packetdrill_harness.rs new file mode 100644 index 0000000..a075831 --- /dev/null +++ b/examples/packetdrill_harness.rs @@ -0,0 +1,724 @@ +#[cfg(not(target_os = "linux"))] +fn main() { + eprintln!("packetdrill_harness is only supported on Linux"); + std::process::exit(2); +} + +#[cfg(target_os = "linux")] +mod linux { + use anyhow::{anyhow, bail, Context, Result}; + use clap::{Parser, Subcommand}; + use std::ffi::CString; + use std::io; + use std::mem::{size_of, zeroed}; + use std::net::SocketAddr; + use std::os::fd::{AsRawFd, FromRawFd, OwnedFd}; + use std::path::{Path, PathBuf}; + use std::sync::Arc; + use std::time::Duration; + use tcp_ip::tcp::{TcpConfig, TcpListener, TcpStream}; + use tcp_ip::{ip_stack, IpStackConfig, IpStackRecv, IpStackSend}; + use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader}; + use tokio::net::{UnixListener, UnixStream}; + use tokio::sync::{mpsc, Mutex, Notify, OwnedMutexGuard}; + + const ETH_P_ALL: u16 = 0x0003; + const PACKET_OUTGOING: u8 = 4; + + #[derive(Parser)] + #[command(name = "packetdrill_harness")] + #[command(about = "packetdrill adapter for the tcp_ip userspace stack")] + struct Args { + #[command(subcommand)] + command: Command, + } + + #[derive(Subcommand)] + enum Command { + /// Run the packet adapter and control server. + Daemon { + #[arg(long, default_value = "tun0")] + interface: String, + #[arg(long)] + socket: PathBuf, + #[arg(long)] + ack_delay_ms: Option, + }, + /// Start a passive listener. The accepted stream becomes the current stream. + Listen { + #[arg(long)] + socket: PathBuf, + address: SocketAddr, + }, + /// Start an active connection without blocking the packetdrill timeline. + Connect { + #[arg(long)] + socket: PathBuf, + local: SocketAddr, + peer: SocketAddr, + }, + /// Write bytes encoded as hexadecimal to the current stream. + WriteHex { + #[arg(long)] + socket: PathBuf, + hex: String, + }, + /// Write zero bytes, matching packetdrill's generated payload. + WriteZero { + #[arg(long)] + socket: PathBuf, + length: usize, + }, + /// Send FIN on the current stream's write half. + ShutdownWrite { + #[arg(long)] + socket: PathBuf, + }, + /// Read and compare exactly the supplied hexadecimal bytes. + ExpectReadHex { + #[arg(long)] + socket: PathBuf, + hex: String, + #[arg(long, default_value_t = 1000)] + timeout_ms: u64, + }, + /// Read the requested number of bytes and require zero payload. + ExpectReadZero { + #[arg(long)] + socket: PathBuf, + length: usize, + #[arg(long, default_value_t = 1000)] + timeout_ms: u64, + }, + /// Require EOF from the current stream. + ExpectEof { + #[arg(long)] + socket: PathBuf, + #[arg(long, default_value_t = 1000)] + timeout_ms: u64, + }, + /// Print whether a stream is pending, connected, or failed. + Status { + #[arg(long)] + socket: PathBuf, + }, + /// Shut down the daemon. + Stop { + #[arg(long)] + socket: PathBuf, + }, + } + + #[derive(Default)] + struct AppState { + stream: Option, + pending: bool, + last_error: Option, + } + + #[cfg(not(feature = "global-ip-stack"))] + type StackHandle = tcp_ip::IpStack; + #[cfg(feature = "global-ip-stack")] + #[derive(Clone, Copy)] + struct StackHandle; + + #[cfg(not(feature = "global-ip-stack"))] + fn clone_stack(stack: &StackHandle) -> StackHandle { + stack.clone() + } + + #[cfg(feature = "global-ip-stack")] + fn clone_stack(_stack: &StackHandle) -> StackHandle { + StackHandle + } + + struct PacketDevice { + recv_fd: OwnedFd, + sender: PacketSender, + } + + impl PacketDevice { + fn open(interface: &str) -> Result { + let name = CString::new(interface).context("interface contains a NUL byte")?; + let ifindex = unsafe { libc::if_nametoindex(name.as_ptr()) } as i32; + if ifindex == 0 { + return Err(io::Error::last_os_error()).context("if_nametoindex"); + } + + let protocol = ETH_P_ALL.to_be() as i32; + // Match packetdrill/tcpdump and use SOCK_RAW. TUN devices have no + // link-layer header, so the bytes still begin with the IP header. + let fd = unsafe { libc::socket(libc::AF_PACKET, libc::SOCK_RAW | libc::SOCK_CLOEXEC, protocol) }; + if fd < 0 { + return Err(io::Error::last_os_error()).context("socket(AF_PACKET)"); + } + let recv_fd = unsafe { OwnedFd::from_raw_fd(fd) }; + + let mut address: libc::sockaddr_ll = unsafe { zeroed() }; + address.sll_family = libc::AF_PACKET as u16; + address.sll_protocol = ETH_P_ALL.to_be(); + address.sll_ifindex = ifindex; + let status = unsafe { + libc::bind( + recv_fd.as_raw_fd(), + (&address as *const libc::sockaddr_ll).cast(), + size_of::() as libc::socklen_t, + ) + }; + if status < 0 { + return Err(io::Error::last_os_error()).context("bind(AF_PACKET)"); + } + + let sender = PacketSender::open(interface)?; + Ok(Self { recv_fd, sender }) + } + + fn into_parts(self) -> (OwnedFd, PacketSender) { + (self.recv_fd, self.sender) + } + } + + struct PacketSender { + ipv4_fd: OwnedFd, + ipv6_fd: OwnedFd, + } + + impl PacketSender { + fn open(interface: &str) -> Result { + let ipv4_fd = open_raw_socket(libc::AF_INET, interface)?; + let ipv6_fd = open_raw_socket(libc::AF_INET6, interface)?; + let enabled: libc::c_int = 1; + let status = unsafe { + libc::setsockopt( + ipv4_fd.as_raw_fd(), + libc::IPPROTO_IP, + libc::IP_HDRINCL, + (&enabled as *const libc::c_int).cast(), + size_of::() as libc::socklen_t, + ) + }; + if status < 0 { + return Err(io::Error::last_os_error()).context("setsockopt(IP_HDRINCL)"); + } + let status = unsafe { + libc::setsockopt( + ipv6_fd.as_raw_fd(), + libc::IPPROTO_IPV6, + libc::IPV6_HDRINCL, + (&enabled as *const libc::c_int).cast(), + size_of::() as libc::socklen_t, + ) + }; + if status < 0 { + return Err(io::Error::last_os_error()).context("setsockopt(IPV6_HDRINCL)"); + } + Ok(Self { ipv4_fd, ipv6_fd }) + } + + fn send(&self, packet: &[u8]) -> io::Result<()> { + let sent = match packet.first().map(|v| v >> 4) { + Some(4) if packet.len() >= 20 => { + let mut address: libc::sockaddr_in = unsafe { zeroed() }; + address.sin_family = libc::AF_INET as u16; + address.sin_addr.s_addr = u32::from_ne_bytes(packet[16..20].try_into().expect("four-byte IPv4 address")); + unsafe { + libc::sendto( + self.ipv4_fd.as_raw_fd(), + packet.as_ptr().cast(), + packet.len(), + 0, + (&address as *const libc::sockaddr_in).cast(), + size_of::() as libc::socklen_t, + ) + } + } + Some(6) if packet.len() >= 40 => { + let mut address: libc::sockaddr_in6 = unsafe { zeroed() }; + address.sin6_family = libc::AF_INET6 as u16; + address.sin6_addr.s6_addr.copy_from_slice(&packet[24..40]); + unsafe { + libc::sendto( + self.ipv6_fd.as_raw_fd(), + packet.as_ptr().cast(), + packet.len(), + 0, + (&address as *const libc::sockaddr_in6).cast(), + size_of::() as libc::socklen_t, + ) + } + } + _ => return Err(io::Error::new(io::ErrorKind::InvalidInput, "not an IPv4/IPv6 packet")), + }; + if sent < 0 { + return Err(io::Error::last_os_error()); + } + if sent as usize != packet.len() { + return Err(io::Error::new(io::ErrorKind::WriteZero, "short AF_PACKET send")); + } + Ok(()) + } + } + + fn open_raw_socket(family: libc::c_int, interface: &str) -> Result { + let fd = unsafe { libc::socket(family, libc::SOCK_RAW | libc::SOCK_CLOEXEC, libc::IPPROTO_RAW) }; + if fd < 0 { + return Err(io::Error::last_os_error()).context("socket(IPPROTO_RAW)"); + } + let fd = unsafe { OwnedFd::from_raw_fd(fd) }; + let interface = CString::new(interface).context("interface contains a NUL byte")?; + let status = unsafe { + libc::setsockopt( + fd.as_raw_fd(), + libc::SOL_SOCKET, + libc::SO_BINDTODEVICE, + interface.as_ptr().cast(), + interface.as_bytes_with_nul().len() as libc::socklen_t, + ) + }; + if status < 0 { + return Err(io::Error::last_os_error()).context("setsockopt(SO_BINDTODEVICE)"); + } + Ok(fd) + } + + pub async fn main() -> Result<()> { + let _ = env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("tcp_ip=debug")).try_init(); + match Args::parse().command { + Command::Daemon { + interface, + socket, + ack_delay_ms, + } => run_daemon(&interface, &socket, ack_delay_ms).await, + Command::Listen { socket, address } => client(&socket, &format!("LISTEN {address}")).await, + Command::Connect { socket, local, peer } => client(&socket, &format!("CONNECT {local} {peer}")).await, + Command::WriteHex { socket, hex } => { + decode_hex(&hex)?; + client(&socket, &format!("WRITE_HEX {hex}")).await + } + Command::WriteZero { socket, length } => client(&socket, &format!("WRITE_ZERO {length}")).await, + Command::ShutdownWrite { socket } => client(&socket, "SHUTDOWN_WRITE").await, + Command::ExpectReadHex { socket, hex, timeout_ms } => { + decode_hex(&hex)?; + client(&socket, &format!("EXPECT_READ_HEX {timeout_ms} {hex}")).await + } + Command::ExpectReadZero { + socket, + length, + timeout_ms, + } => client(&socket, &format!("EXPECT_READ_ZERO {timeout_ms} {length}")).await, + Command::ExpectEof { socket, timeout_ms } => client(&socket, &format!("EXPECT_EOF {timeout_ms}")).await, + Command::Status { socket } => client(&socket, "STATUS").await, + Command::Stop { socket } => client(&socket, "STOP").await, + } + } + + async fn client(socket: &Path, command: &str) -> Result<()> { + let mut stream = UnixStream::connect(socket) + .await + .with_context(|| format!("connect control socket {}", socket.display()))?; + stream.write_all(command.as_bytes()).await?; + stream.write_all(b"\n").await?; + stream.shutdown().await?; + + let mut response = String::new(); + BufReader::new(stream).read_line(&mut response).await?; + let response = response.trim_end(); + println!("{response}"); + if let Some(message) = response.strip_prefix("ERR ") { + bail!("{message}"); + } + if !response.starts_with("OK") { + bail!("invalid daemon response: {response}"); + } + Ok(()) + } + + async fn run_daemon(interface: &str, socket: &Path, ack_delay_ms: Option) -> Result<()> { + if socket.exists() { + std::fs::remove_file(socket).with_context(|| format!("remove stale socket {}", socket.display()))?; + } + + let device = PacketDevice::open(interface)?; + eprintln!("packetdrill_harness: attached to {interface}"); + let (recv_fd, sender) = device.into_parts(); + let tcp_config = TcpConfig { + retransmission_timeout: Duration::from_millis(200), + time_wait_timeout: Duration::from_millis(500), + ack_delay: ack_delay_ms.map(Duration::from_millis), + ..TcpConfig::default() + }; + let config = IpStackConfig::builder().tcp_config(tcp_config).build(); + let (stack, ip_send, ip_recv) = create_stack(config)?; + let state = Arc::new(Mutex::new(AppState::default())); + let stop = Arc::new(Notify::new()); + + let (packet_tx, mut packet_rx) = mpsc::channel::>(256); + std::thread::Builder::new() + .name("packetdrill-ingress".into()) + .spawn(move || receive_packets(recv_fd, packet_tx)) + .context("spawn AF_PACKET receive thread")?; + tokio::spawn(async move { + while let Some(packet) = packet_rx.recv().await { + if let Err(error) = ip_send.send_ip_packet(&packet).await { + eprintln!("packetdrill_harness: ingress error: {error}"); + } + } + }); + tokio::spawn(send_packets(ip_recv, sender)); + + let listener = UnixListener::bind(socket).with_context(|| format!("bind control socket {}", socket.display()))?; + let result = control_loop(listener, stack, state, stop.clone()).await; + stop.notify_waiters(); + let _ = std::fs::remove_file(socket); + result + } + + fn receive_packets(fd: OwnedFd, sender: mpsc::Sender>) { + let mut buffer = vec![0u8; 65_535]; + loop { + let mut address: libc::sockaddr_ll = unsafe { zeroed() }; + let mut address_len = size_of::() as libc::socklen_t; + let length = unsafe { + libc::recvfrom( + fd.as_raw_fd(), + buffer.as_mut_ptr().cast(), + buffer.len(), + 0, + (&mut address as *mut libc::sockaddr_ll).cast(), + &mut address_len, + ) + }; + if length < 0 { + let error = io::Error::last_os_error(); + if error.kind() == io::ErrorKind::Interrupted { + continue; + } + eprintln!("packetdrill_harness: AF_PACKET receive failed: {error}"); + return; + } + if address.sll_pkttype == PACKET_OUTGOING { + continue; + } + let packet = &buffer[..length as usize]; + if !is_tcp_ip_packet(packet) { + continue; + } + if sender.blocking_send(packet.to_vec()).is_err() { + return; + } + } + } + + fn is_tcp_ip_packet(packet: &[u8]) -> bool { + match packet.first().map(|v| v >> 4) { + Some(4) => packet.get(9).copied() == Some(6), + Some(6) => packet.get(6).copied() == Some(6), + _ => false, + } + } + + async fn send_packets(mut receiver: IpStackRecv, sender: PacketSender) { + let mut buffer = vec![0u8; 65_535]; + loop { + match receiver.recv(&mut buffer).await { + Ok(length) => { + if let Err(error) = sender.send(&buffer[..length]) { + eprintln!("packetdrill_harness: raw socket send failed: {error}"); + return; + } + } + Err(error) => { + eprintln!("packetdrill_harness: stack egress failed: {error}"); + return; + } + } + } + } + + async fn control_loop(listener: UnixListener, stack: StackHandle, state: Arc>, stop: Arc) -> Result<()> { + loop { + tokio::select! { + _ = stop.notified() => return Ok(()), + accepted = listener.accept() => { + let (stream, _) = accepted?; + let should_stop = handle_control(stream, clone_stack(&stack), state.clone()).await?; + if should_stop { + return Ok(()); + } + } + } + } + } + + async fn handle_control(stream: UnixStream, stack: StackHandle, state: Arc>) -> Result { + let (read_half, mut write_half) = stream.into_split(); + let mut line = String::new(); + BufReader::new(read_half).read_line(&mut line).await?; + let result = execute_command(line.trim(), stack, state).await; + let should_stop = matches!(result, Ok(CommandResult::Stop)); + let response = match result { + Ok(CommandResult::Message(message)) => format!("OK {message}\n"), + Ok(CommandResult::Stop) => "OK stopping\n".to_string(), + Err(error) => format!("ERR {error:#}\n").replace('\n', " "), + }; + write_half.write_all(response.as_bytes()).await?; + write_half.shutdown().await?; + Ok(should_stop) + } + + enum CommandResult { + Message(String), + Stop, + } + + async fn execute_command(command: &str, stack: StackHandle, state: Arc>) -> Result { + let mut parts = command.split_whitespace(); + match parts.next() { + Some("LISTEN") => { + let address = parse_addr(parts.next(), "listen address")?; + ensure_no_extra(parts)?; + mark_pending(&state).await?; + let mut listener = match bind_listener(stack, address).await { + Ok(listener) => listener, + Err(error) => { + finish_pending(state, Err(error)).await; + bail!("failed to bind listener"); + } + }; + tokio::spawn(async move { + let result = listener.accept().await.map(|(stream, _)| stream); + finish_pending(state, result).await; + }); + Ok(CommandResult::Message("listening".into())) + } + Some("CONNECT") => { + let local = parse_addr(parts.next(), "local address")?; + let peer = parse_addr(parts.next(), "peer address")?; + ensure_no_extra(parts)?; + mark_pending(&state).await?; + tokio::spawn(async move { + let result = connect_stream(stack, local, peer).await; + finish_pending(state, result).await; + }); + Ok(CommandResult::Message("connecting".into())) + } + Some("WRITE_HEX") => { + let bytes = decode_hex(parts.next().ok_or_else(|| anyhow!("missing hex payload"))?)?; + ensure_no_extra(parts)?; + let mut guard = wait_for_stream(&state).await?; + let stream = current_stream(&mut guard)?; + stream.write_all(&bytes).await?; + Ok(CommandResult::Message(format!("wrote {} bytes", bytes.len()))) + } + Some("WRITE_ZERO") => { + let length: usize = parts + .next() + .ok_or_else(|| anyhow!("missing length"))? + .parse() + .context("invalid length")?; + ensure_no_extra(parts)?; + let mut guard = wait_for_stream(&state).await?; + current_stream(&mut guard)?.write_all(&vec![0; length]).await?; + Ok(CommandResult::Message(format!("wrote {length} zero bytes"))) + } + Some("SHUTDOWN_WRITE") => { + ensure_no_extra(parts)?; + let mut guard = wait_for_stream(&state).await?; + current_stream(&mut guard)?.shutdown().await?; + Ok(CommandResult::Message("write side shut down".into())) + } + Some("EXPECT_READ_HEX") => { + let timeout = parse_timeout(parts.next())?; + let expected = decode_hex(parts.next().ok_or_else(|| anyhow!("missing expected hex payload"))?)?; + ensure_no_extra(parts)?; + let mut guard = wait_for_stream(&state).await?; + let stream = current_stream(&mut guard)?; + let mut actual = vec![0; expected.len()]; + tokio::time::timeout(timeout, stream.read_exact(&mut actual)) + .await + .context("timed out waiting for stream data")??; + if actual != expected { + bail!("read mismatch: expected {}, got {}", encode_hex(&expected), encode_hex(&actual)); + } + Ok(CommandResult::Message(format!("read {} bytes", actual.len()))) + } + Some("EXPECT_READ_ZERO") => { + let timeout = parse_timeout(parts.next())?; + let length: usize = parts + .next() + .ok_or_else(|| anyhow!("missing length"))? + .parse() + .context("invalid length")?; + ensure_no_extra(parts)?; + let mut guard = wait_for_stream(&state).await?; + let stream = current_stream(&mut guard)?; + let mut actual = vec![0; length]; + tokio::time::timeout(timeout, stream.read_exact(&mut actual)) + .await + .context("timed out waiting for stream data")??; + if let Some((index, byte)) = actual.iter().enumerate().find(|(_, byte)| **byte != 0) { + bail!("expected zero payload, byte {index} was {byte:#04x}"); + } + Ok(CommandResult::Message(format!("read {length} zero bytes"))) + } + Some("EXPECT_EOF") => { + let timeout = parse_timeout(parts.next())?; + ensure_no_extra(parts)?; + let mut guard = wait_for_stream(&state).await?; + let stream = current_stream(&mut guard)?; + let mut byte = [0u8; 1]; + let length = tokio::time::timeout(timeout, stream.read(&mut byte)) + .await + .context("timed out waiting for EOF")??; + if length != 0 { + bail!("expected EOF, read byte {:02x}", byte[0]); + } + Ok(CommandResult::Message("EOF observed".into())) + } + Some("STATUS") => { + ensure_no_extra(parts)?; + let guard = state.lock().await; + let status = if let Some(error) = &guard.last_error { + format!("failed {error}") + } else if guard.stream.is_some() { + "connected".into() + } else if guard.pending { + "pending".into() + } else { + "idle".into() + }; + Ok(CommandResult::Message(status)) + } + Some("STOP") => { + ensure_no_extra(parts)?; + Ok(CommandResult::Stop) + } + Some(other) => bail!("unknown command {other}"), + None => bail!("empty command"), + } + } + + async fn mark_pending(state: &Arc>) -> Result<()> { + let mut guard = state.lock().await; + if guard.pending || guard.stream.is_some() { + bail!("a stream is already pending or connected"); + } + guard.pending = true; + guard.last_error = None; + Ok(()) + } + + async fn finish_pending(state: Arc>, result: io::Result) { + let mut guard = state.lock().await; + guard.pending = false; + match result { + Ok(stream) => guard.stream = Some(stream), + Err(error) => guard.last_error = Some(error.to_string()), + } + } + + async fn wait_for_stream(state: &Arc>) -> Result> { + let deadline = tokio::time::Instant::now() + Duration::from_secs(1); + loop { + let guard = state.clone().lock_owned().await; + if guard.stream.is_some() || guard.last_error.is_some() || !guard.pending { + return Ok(guard); + } + drop(guard); + if tokio::time::Instant::now() >= deadline { + bail!("timed out waiting for pending stream"); + } + tokio::time::sleep(Duration::from_millis(2)).await; + } + } + + fn current_stream(state: &mut AppState) -> Result<&mut TcpStream> { + if let Some(error) = &state.last_error { + bail!("stream setup failed: {error}"); + } + state.stream.as_mut().ok_or_else(|| { + if state.pending { + anyhow!("stream is still pending") + } else { + anyhow!("no current stream") + } + }) + } + + fn parse_addr(value: Option<&str>, name: &str) -> Result { + value + .ok_or_else(|| anyhow!("missing {name}"))? + .parse() + .with_context(|| format!("invalid {name}")) + } + + fn parse_timeout(value: Option<&str>) -> Result { + let millis: u64 = value + .ok_or_else(|| anyhow!("missing timeout"))? + .parse() + .context("invalid timeout")?; + Ok(Duration::from_millis(millis)) + } + + fn ensure_no_extra<'a>(mut parts: impl Iterator) -> Result<()> { + if let Some(extra) = parts.next() { + bail!("unexpected argument {extra}"); + } + Ok(()) + } + + fn decode_hex(value: &str) -> Result> { + if !value.len().is_multiple_of(2) { + bail!("hex payload must have an even number of digits"); + } + value + .as_bytes() + .chunks_exact(2) + .map(|pair| { + let pair = std::str::from_utf8(pair).expect("hex input is valid UTF-8"); + u8::from_str_radix(pair, 16).with_context(|| format!("invalid hex byte {pair}")) + }) + .collect() + } + + fn encode_hex(value: &[u8]) -> String { + value.iter().map(|byte| format!("{byte:02x}")).collect() + } + + #[cfg(not(feature = "global-ip-stack"))] + fn create_stack(config: IpStackConfig) -> io::Result<(StackHandle, IpStackSend, IpStackRecv)> { + ip_stack(config) + } + + #[cfg(feature = "global-ip-stack")] + fn create_stack(config: IpStackConfig) -> io::Result<(StackHandle, IpStackSend, IpStackRecv)> { + let (send, recv) = ip_stack(config)?; + Ok((StackHandle, send, recv)) + } + + #[cfg(not(feature = "global-ip-stack"))] + async fn bind_listener(stack: StackHandle, address: SocketAddr) -> io::Result { + TcpListener::bind(stack, address).await + } + + #[cfg(feature = "global-ip-stack")] + async fn bind_listener(_stack: StackHandle, address: SocketAddr) -> io::Result { + TcpListener::bind(address).await + } + + #[cfg(not(feature = "global-ip-stack"))] + async fn connect_stream(stack: StackHandle, local: SocketAddr, peer: SocketAddr) -> io::Result { + TcpStream::bind(stack, local)?.connect_to(peer).await + } + + #[cfg(feature = "global-ip-stack")] + async fn connect_stream(_stack: StackHandle, local: SocketAddr, peer: SocketAddr) -> io::Result { + TcpStream::bind(local)?.connect_to(peer).await + } +} + +#[cfg(target_os = "linux")] +#[tokio::main] +async fn main() -> anyhow::Result<()> { + linux::main().await +} diff --git a/scripts/packetdrill_case.sh b/scripts/packetdrill_case.sh new file mode 100644 index 0000000..fcd3d5e --- /dev/null +++ b/scripts/packetdrill_case.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${TCP_IP_PD_HARNESS:?TCP_IP_PD_HARNESS is required}" +: "${TCP_IP_PD_SOCKET:?TCP_IP_PD_SOCKET is required}" +: "${TCP_IP_PD_PID_FILE:?TCP_IP_PD_PID_FILE is required}" +: "${TCP_IP_PD_HARNESS_LOG:?TCP_IP_PD_HARNESS_LOG is required}" + +delete_rule() { + local family="$1" + local tool="iptables" + [[ "${family}" == "ipv6" ]] && tool="ip6tables" + while "${tool}" -w -t raw -C PREROUTING -i tun0 -p tcp -j DROP 2>/dev/null; do + "${tool}" -w -t raw -D PREROUTING -i tun0 -p tcp -j DROP + done + while "${tool}" -w -C OUTPUT -m mark --mark 20548 -j DROP 2>/dev/null; do + "${tool}" -w -D OUTPUT -m mark --mark 20548 -j DROP + done +} + +cleanup() { + if [[ -S "${TCP_IP_PD_SOCKET}" ]]; then + "${TCP_IP_PD_HARNESS}" stop --socket "${TCP_IP_PD_SOCKET}" >/dev/null 2>&1 || true + fi + if [[ -f "${TCP_IP_PD_PID_FILE}" ]]; then + pid="$(<"${TCP_IP_PD_PID_FILE}")" + if [[ -n "${pid}" ]] && kill -0 "${pid}" 2>/dev/null; then + kill "${pid}" 2>/dev/null || true + for _ in {1..20}; do + kill -0 "${pid}" 2>/dev/null || break + sleep 0.05 + done + kill -KILL "${pid}" 2>/dev/null || true + fi + fi + delete_rule ipv4 + delete_rule ipv6 + rm -f -- "${TCP_IP_PD_SOCKET}" "${TCP_IP_PD_PID_FILE}" +} + +setup_case() { + local family="$1" + local ack_delay_ms="${2:-}" + cleanup + + local tool="iptables" + [[ "${family}" == "ipv6" ]] && tool="ip6tables" + "${tool}" -w -t raw -I PREROUTING 1 -i tun0 -p tcp -j DROP + # Active-open scripts use a marked kernel socket only to give packetdrill + # syscall state. Suppress that socket's packets; the userspace stack is the + # sole system under test and sends unmarked traffic. + "${tool}" -w -I OUTPUT 1 -m mark --mark 20548 -j DROP + + args=(daemon --interface tun0 --socket "${TCP_IP_PD_SOCKET}") + if [[ -n "${ack_delay_ms}" ]]; then + args+=(--ack-delay-ms "${ack_delay_ms}") + fi + nohup "${TCP_IP_PD_HARNESS}" "${args[@]}" >"${TCP_IP_PD_HARNESS_LOG}" 2>&1 & + echo "$!" >"${TCP_IP_PD_PID_FILE}" + + for _ in {1..100}; do + if [[ -S "${TCP_IP_PD_SOCKET}" ]] && "${TCP_IP_PD_HARNESS}" status --socket "${TCP_IP_PD_SOCKET}" >/dev/null 2>&1; then + return 0 + fi + if ! kill -0 "$(<"${TCP_IP_PD_PID_FILE}")" 2>/dev/null; then + cat "${TCP_IP_PD_HARNESS_LOG}" >&2 || true + echo "packetdrill harness exited during startup" >&2 + return 1 + fi + sleep 0.02 + done + echo "timed out waiting for packetdrill harness" >&2 + return 1 +} + +control() { + local command="${1:?control command is required}" + shift + "${TCP_IP_PD_HARNESS}" "${command}" --socket "${TCP_IP_PD_SOCKET}" "$@" +} + +case "${1:-}" in + setup) + shift + setup_case "$@" + ;; + cleanup) + cleanup + ;; + ctl) + shift + control "$@" + ;; + *) + echo "usage: $0 {setup FAMILY [ACK_DELAY_MS]|cleanup|ctl COMMAND ...}" >&2 + exit 2 + ;; +esac diff --git a/scripts/run_packetdrill.sh b/scripts/run_packetdrill.sh new file mode 100644 index 0000000..5d716e4 --- /dev/null +++ b/scripts/run_packetdrill.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +set -uo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +packetdrill_bin="${PACKETDRILL_BIN:-}" +if [[ -z "${packetdrill_bin}" ]]; then + packetdrill_bin="$(bash "${repo_root}/scripts/setup_packetdrill.sh")" || exit $? +fi +if [[ ! -x "${packetdrill_bin}" ]]; then + echo "packetdrill binary is not executable: ${packetdrill_bin}" >&2 + exit 2 +fi +if [[ "$(id -u)" -ne 0 ]]; then + echo "packetdrill tests require root (CAP_NET_ADMIN and CAP_NET_RAW)" >&2 + exit 2 +fi + +export PATH="${HOME}/.cargo/bin:${PATH}" +cd "${repo_root}" || exit 2 +if [[ "${TCP_IP_PD_SKIP_BUILD:-0}" != "1" ]]; then + cargo build --example packetdrill_harness || exit $? +fi + +export TCP_IP_PD_HARNESS="${repo_root}/target/debug/examples/packetdrill_harness" +if [[ ! -x "${TCP_IP_PD_HARNESS}" ]]; then + echo "packetdrill harness is missing: ${TCP_IP_PD_HARNESS}" >&2 + exit 2 +fi +export TCP_IP_PD_CASE="bash ${repo_root}/scripts/packetdrill_case.sh" +run_root="${repo_root}/target/packetdrill" +log_root="${run_root}/logs" +mkdir -p "${log_root}" + +cleanup_current_case() { + if [[ -n "${TCP_IP_PD_SOCKET:-}" && -n "${TCP_IP_PD_PID_FILE:-}" && -n "${TCP_IP_PD_HARNESS_LOG:-}" ]]; then + bash "${repo_root}/scripts/packetdrill_case.sh" cleanup >/dev/null 2>&1 || true + fi +} +trap cleanup_current_case EXIT +trap 'exit 130' INT TERM + +mapfile -t tests < <( + if [[ "$#" -gt 0 ]]; then + printf '%s\n' "$@" + else + find "${repo_root}/tests/packetdrill" -type f -name '*.pkt' -print | sort + fi +) + +if [[ "${#tests[@]}" -eq 0 ]]; then + echo "no packetdrill tests found" >&2 + exit 2 +fi + +failures=0 +extra_args=() +if [[ -n "${PACKETDRILL_EXTRA_ARGS:-}" ]]; then + read -r -a extra_args <<<"${PACKETDRILL_EXTRA_ARGS}" +fi +for test_path in "${tests[@]}"; do + if [[ "${test_path}" != /* ]]; then + test_path="${repo_root}/${test_path}" + fi + name="${test_path#"${repo_root}/tests/packetdrill/"}" + safe_name="${name//\//_}" + safe_name="${safe_name%.pkt}" + export TCP_IP_PD_SOCKET="/tmp/tcp_ip-packetdrill-$$-${safe_name}.sock" + export TCP_IP_PD_PID_FILE="/tmp/tcp_ip-packetdrill-$$-${safe_name}.pid" + export TCP_IP_PD_HARNESS_LOG="${log_root}/${safe_name}.harness.log" + packetdrill_log="${log_root}/${safe_name}.packetdrill.log" + + echo "[packetdrill] ${name}" + "${packetdrill_bin}" --tolerance_usecs="${PACKETDRILL_TOLERANCE_USECS:-150000}" --verbose "${extra_args[@]}" "${test_path}" >"${packetdrill_log}" 2>&1 + status=$? + cleanup_current_case + if [[ "${status}" -ne 0 ]]; then + failures=$((failures + 1)) + echo "FAILED: ${name}" >&2 + cat "${packetdrill_log}" >&2 + if [[ -s "${TCP_IP_PD_HARNESS_LOG}" ]]; then + echo "--- harness ---" >&2 + cat "${TCP_IP_PD_HARNESS_LOG}" >&2 + fi + fi +done + +if [[ "${failures}" -ne 0 ]]; then + echo "${failures} packetdrill test(s) failed; logs: ${log_root}" >&2 + exit 1 +fi +echo "all ${#tests[@]} packetdrill tests passed" diff --git a/scripts/run_packetdrill_wsl.ps1 b/scripts/run_packetdrill_wsl.ps1 new file mode 100644 index 0000000..bf069f0 --- /dev/null +++ b/scripts/run_packetdrill_wsl.ps1 @@ -0,0 +1,25 @@ +param( + [string]$Distribution = "Ubuntu-24.04", + [string[]]$Test = @() +) + +$ErrorActionPreference = "Stop" +function Convert-ToWslPath([string]$Path) { + $resolved = (Resolve-Path $Path).Path + if ($resolved -notmatch '^([A-Za-z]):\\(.*)$') { + throw "Only local Windows drive paths are supported: $resolved" + } + $drive = $Matches[1].ToLowerInvariant() + $tail = $Matches[2].Replace('\', '/') + return "/mnt/$drive/$tail" +} + +$repo = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path +$wslRepo = Convert-ToWslPath $repo +$arguments = @("-d", $Distribution, "-u", "root", "--cd", $wslRepo, "--", "bash", "scripts/run_packetdrill.sh") +foreach ($path in $Test) { + $arguments += Convert-ToWslPath $path +} + +& wsl @arguments +exit $LASTEXITCODE diff --git a/scripts/setup_packetdrill.sh b/scripts/setup_packetdrill.sh new file mode 100644 index 0000000..a18878a --- /dev/null +++ b/scripts/setup_packetdrill.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +set -euo pipefail + +readonly PACKETDRILL_COMMIT="2c4001c4d6fc04a3bbd01d4b92be62717a37648a" +cache_root="${PACKETDRILL_CACHE_DIR:-${HOME}/.cache/tcp_ip/packetdrill}" + +install_deps=false +[[ "${1:-}" == "--install-deps" ]] && install_deps=true +for command in git gcc make bison flex python3 ifconfig iptables; do + if ! command -v "${command}" >/dev/null 2>&1; then + install_deps=true + break + fi +done + +if [[ "${install_deps}" == "true" ]]; then + if [[ "$(id -u)" -ne 0 ]]; then + echo "packetdrill build dependencies are missing; rerun setup as root" >&2 + exit 2 + fi + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get install -y git gcc make bison flex python3 net-tools iptables +fi + +mkdir -p "$(dirname "${cache_root}")" +if [[ ! -d "${cache_root}/.git" ]]; then + git clone https://github.com/google/packetdrill.git "${cache_root}" >&2 +fi + +git -C "${cache_root}" fetch origin "${PACKETDRILL_COMMIT}" >&2 +git -C "${cache_root}" checkout --detach "${PACKETDRILL_COMMIT}" >&2 + +source_dir="${cache_root}/gtests/net/packetdrill" +( + cd "${source_dir}" + ./configure >&2 + make -j"$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 2)" >&2 +) + +printf '%s\n' "${source_dir}/packetdrill" diff --git a/src/tcp/sys.rs b/src/tcp/sys.rs index cfd30c2..4979c92 100644 --- a/src/tcp/sys.rs +++ b/src/tcp/sys.rs @@ -231,7 +231,10 @@ impl TcpStreamTask { self.tcb.mss() } fn only_recv_in(&self) -> bool { - self.retransmission || self.last_buffer.is_some() || self.write_half_closed || self.tcb.limit() + // A zero peer window alone must not stop us from receiving the first + // application write. Once that write becomes `last_buffer`, this task + // switches to network-only polling and arms the persist/probe timer. + self.retransmission || self.last_buffer.is_some() || self.write_half_closed } fn push_application_layer(&mut self) { if let Some(sender) = self.application_layer_sender.as_ref() { @@ -424,7 +427,7 @@ impl TcpStreamTask { impl TcpStreamTask { pub async fn connect(&mut self) -> io::Result<()> { let mut attempts = 0; - let mut time = Duration::from_millis(100); + let mut time = self.tcb.rto(); while attempts < 50 { let Some(packet) = self.tcb.try_syn_sent() else { return if self.tcb.is_close() { diff --git a/src/tcp/tcb.rs b/src/tcp/tcb.rs index 3d755af..dbd9ed2 100644 --- a/src/tcp/tcb.rs +++ b/src/tcp/tcb.rs @@ -174,6 +174,9 @@ impl SeqNum { fn add_num(self, n: u32) -> Self { SeqNum(self.0.wrapping_add(n)) } + fn sub_num(self, n: u32) -> Self { + SeqNum(self.0.wrapping_sub(n)) + } fn add_update(&mut self, n: u32) { self.0 = self.0.wrapping_add(n) } @@ -424,6 +427,11 @@ impl Tcb { self.rcv_ack = SeqNum(packet.get_acknowledgement()); self.recv_syn_ack(); self.init_congestion_window(); + // The SYN-ACK already advertised the current receive window. + // Starting the stream task with the constructor's zero snapshot + // would make the next unrelated inbound packet trigger a spurious + // window-update ACK. + self.last_snd_wnd = self.recv_window(); if !packet.payload().is_empty() { let seq = SeqNum(packet.get_sequence()); buf.advance(header_len); @@ -640,7 +648,7 @@ impl Tcb { // Only a pure ACK carrying no data counts as a duplicate ACK if self.rcv_ack != self.snd_seq && packet.payload().is_empty() && flags & FIN != FIN && wnd_unchanged { self.duplicate_ack_count += 1; - if self.duplicate_ack_count > 3 { + if self.duplicate_ack_count >= 3 { self.duplicate_ack_count = 0; if self.back_n() { self.rto_recovery = false; @@ -689,7 +697,7 @@ impl Tcb { if flags & ACK == ACK { let acknowledgement = AckNum::from(packet.get_acknowledgement()); self.update_last_ack(&packet); - if acknowledgement == self.snd_seq.add_num(1) { + if acknowledgement == self.snd_seq { self.recv_fin_ack() } } @@ -851,14 +859,7 @@ impl Tcb { } fn update_last_ack(&mut self, tcp_packet: &TcpPacket<'_>) { let ack = AckNum::from(tcp_packet.get_acknowledgement()); - let max_ack = if matches!( - self.state, - TcpState::FinWait1 | TcpState::FinWait2 | TcpState::Closing | TcpState::LastAck - ) { - self.snd_seq.add_num(1) - } else { - self.snd_seq - }; + let max_ack = self.snd_seq; if ack > max_ack { self.requires_ack_repeat = true; return; @@ -898,7 +899,7 @@ impl Tcb { } else { self.reset_write_timeout(); } - if !self.writeable_state() && self.rcv_ack > self.snd_seq { + if !self.writeable_state() && self.rcv_ack >= self.snd_seq { self.recv_fin_ack() } self.reset_write_timeout(); @@ -1101,8 +1102,14 @@ impl Tcb { pub fn sent_fin(&mut self) { match self.state { - TcpState::Established => self.state = TcpState::FinWait1, - TcpState::CloseWait => self.state = TcpState::LastAck, + TcpState::Established => { + self.snd_seq.add_update(1); + self.state = TcpState::FinWait1; + } + TcpState::CloseWait => { + self.snd_seq.add_update(1); + self.state = TcpState::LastAck; + } _ => {} } } @@ -1127,7 +1134,10 @@ impl Tcb { fn recv_fin_ack(&mut self) { match self.state { TcpState::FinWait1 => self.state = TcpState::FinWait2, - TcpState::Closing => self.state = TcpState::TimeWait, + TcpState::Closing => { + self.time_wait = Some(Instant::now() + self.time_wait_timeout); + self.state = TcpState::TimeWait; + } TcpState::LastAck => self.state = TcpState::Closed, _ => {} } @@ -1143,7 +1153,11 @@ impl Tcb { self.state = TcpState::Closed } pub fn fin_packet(&self) -> TransportPacket { - let seq = self.snd_seq.0; + let seq = if matches!(self.state, TcpState::FinWait1 | TcpState::Closing | TcpState::LastAck) { + self.snd_seq.sub_num(1).0 + } else { + self.snd_seq.0 + }; self.create_transport_packet_seq(FIN | ACK, seq, &[]) } pub fn ack_packet(&self) -> TransportPacket { diff --git a/tests/packetdrill/README.md b/tests/packetdrill/README.md new file mode 100644 index 0000000..88436a2 --- /dev/null +++ b/tests/packetdrill/README.md @@ -0,0 +1,33 @@ +# packetdrill TCP tests + +These scripts test `tcp_ip`, not the host kernel TCP implementation. The harness +captures packetdrill input from `tun0` with `AF_PACKET`, injects complete IP packets +into `IpStackSend`, and sends `IpStackRecv` output back through raw IPv4/IPv6 +sockets for packetdrill to verify. + +During each case, exact TCP drop rules on `tun0` prevent the host TCP stack from +generating RST packets. Cleanup removes both IPv4/IPv6 rules, the per-case Unix +socket, and the harness process even after a failed script. + +The test configuration uses a 200 ms initial retransmission timeout and 500 ms +TIME_WAIT. The delayed-ACK cases explicitly configure 50 ms; other cases use +immediate ACKs. Runner timing tolerance defaults to 150 ms and can be overridden +with `PACKETDRILL_TOLERANCE_USECS`. + +Run all cases: + +```bash +sudo -E bash scripts/run_packetdrill.sh +``` + +Run selected cases: + +```bash +sudo -E bash scripts/run_packetdrill.sh \ + tests/packetdrill/ipv4/passive_smoke.pkt \ + tests/packetdrill/ipv6/passive_smoke.pkt +``` + +Set `PACKETDRILL_BIN` to use an existing binary. Otherwise the runner invokes +`scripts/setup_packetdrill.sh`, which builds the pinned upstream revision in +`${PACKETDRILL_CACHE_DIR:-$HOME/.cache/tcp_ip/packetdrill}`. diff --git a/tests/packetdrill/ipv4/active_close_fin_rto.pkt b/tests/packetdrill/ipv4/active_close_fin_rto.pkt new file mode 100644 index 0000000..b0e0b1f --- /dev/null +++ b/tests/packetdrill/ipv4/active_close_fin_rto.pkt @@ -0,0 +1,27 @@ +--ip_version=ipv4 +--local_ip=192.168.42.2 +--gateway_ip=192.168.42.1 +--netmask_ip=255.255.255.0 +--remote_ip=192.0.2.1 + +`$TCP_IP_PD_CASE setup ipv4` + +0.000 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3 ++0.000 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 ++0.000 bind(3, ..., ...) = 0 ++0.000 listen(3, 1) = 0 ++0.000 `$TCP_IP_PD_CASE ctl listen 192.168.42.2:8080` ++0.010 < S 0:0(0) win 65535 ++0.000 > S. 0:0(0) ack 1 win 65535 <...> ++0.000 < . 1:1(0) ack 1 win 65535 + ++0.010 `$TCP_IP_PD_CASE ctl shutdown-write` ++0.000 > F. 1:1(0) ack 1 +// Drop the first FIN and require it to be retransmitted at the configured RTO. ++0.150~+0.450 > F. 1:1(0) ack 1 ++0.000 < . 1:1(0) ack 2 win 65535 ++0.010 < F. 1:1(0) ack 2 win 65535 ++0.000 > . 2:2(0) ack 2 ++0.010 `$TCP_IP_PD_CASE ctl expect-eof` + +`$TCP_IP_PD_CASE cleanup` diff --git a/tests/packetdrill/ipv4/active_open.pkt b/tests/packetdrill/ipv4/active_open.pkt new file mode 100644 index 0000000..d683411 --- /dev/null +++ b/tests/packetdrill/ipv4/active_open.pkt @@ -0,0 +1,24 @@ +--ip_version=ipv4 +--local_ip=192.168.42.2 +--gateway_ip=192.168.42.1 +--netmask_ip=255.255.255.0 +--remote_ip=192.0.2.1 +--bind_port=8080 +--connect_port=8080 + +`$TCP_IP_PD_CASE setup ipv4` + +0.000 socket(..., SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP) = 3 ++0.000 setsockopt(3, SOL_SOCKET, SO_MARK, [20548], 4) = 0 ++0.000 bind(3, ..., ...) = 0 ++0.000 connect(3, ..., ...) = -1 EINPROGRESS (Operation now in progress) ++0.000 `$TCP_IP_PD_CASE ctl connect 192.168.42.2:8080 192.0.2.1:8080` ++0.000 > S 0:0(0) win 65535 <...> ++0.000 < S. 0:0(0) ack 1 win 65535 ++0.000 > . 1:1(0) ack 1 win 65535 + ++0.010 `$TCP_IP_PD_CASE ctl write-zero 2` ++0.000 > P. 1:3(2) ack 1 win 65535 ++0.000 < . 1:1(0) ack 3 win 65535 + +`$TCP_IP_PD_CASE cleanup` diff --git a/tests/packetdrill/ipv4/active_open_syn_rto.pkt b/tests/packetdrill/ipv4/active_open_syn_rto.pkt new file mode 100644 index 0000000..6b9973c --- /dev/null +++ b/tests/packetdrill/ipv4/active_open_syn_rto.pkt @@ -0,0 +1,23 @@ +--ip_version=ipv4 +--local_ip=192.168.42.2 +--gateway_ip=192.168.42.1 +--netmask_ip=255.255.255.0 +--remote_ip=192.0.2.1 +--bind_port=8080 +--connect_port=8080 + +`$TCP_IP_PD_CASE setup ipv4` + +0.000 socket(..., SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP) = 3 ++0.000 setsockopt(3, SOL_SOCKET, SO_MARK, [20548], 4) = 0 ++0.000 bind(3, ..., ...) = 0 ++0.000 connect(3, ..., ...) = -1 EINPROGRESS (Operation now in progress) ++0.000 `$TCP_IP_PD_CASE ctl connect 192.168.42.2:8080 192.0.2.1:8080` ++0.000 > S 0:0(0) win 65535 <...> +// Drop the first SYN and require the configured 200 ms initial RTO. ++0.150~+0.450 > S 0:0(0) win 65535 <...> ++0.000 < S. 0:0(0) ack 1 win 65535 ++0.000 > . 1:1(0) ack 1 win 65535 ++0.010 `$TCP_IP_PD_CASE ctl status` + +`$TCP_IP_PD_CASE cleanup` diff --git a/tests/packetdrill/ipv4/delayed_ack.pkt b/tests/packetdrill/ipv4/delayed_ack.pkt new file mode 100644 index 0000000..d1a8583 --- /dev/null +++ b/tests/packetdrill/ipv4/delayed_ack.pkt @@ -0,0 +1,23 @@ +--ip_version=ipv4 +--local_ip=192.168.42.2 +--gateway_ip=192.168.42.1 +--netmask_ip=255.255.255.0 +--remote_ip=192.0.2.1 + +`$TCP_IP_PD_CASE setup ipv4 50` + +0.000 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3 ++0.000 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 ++0.000 bind(3, ..., ...) = 0 ++0.000 listen(3, 1) = 0 ++0.000 `$TCP_IP_PD_CASE ctl listen 192.168.42.2:8080` ++0.010 < S 0:0(0) win 65535 ++0.000 > S. 0:0(0) ack 1 win 65535 <...> ++0.000 < . 1:1(0) ack 1 win 65535 ++0.010 `$TCP_IP_PD_CASE ctl write-zero 0` + ++0.010 < . 1:2(1) ack 1 win 65535 ++0.030~+0.200 > . 1:1(0) ack 2 ++0.010 `$TCP_IP_PD_CASE ctl expect-read-zero 1` + +`$TCP_IP_PD_CASE cleanup` diff --git a/tests/packetdrill/ipv4/fast_retransmit_sack.pkt b/tests/packetdrill/ipv4/fast_retransmit_sack.pkt new file mode 100644 index 0000000..ea91145 --- /dev/null +++ b/tests/packetdrill/ipv4/fast_retransmit_sack.pkt @@ -0,0 +1,33 @@ +--ip_version=ipv4 +--local_ip=192.168.42.2 +--gateway_ip=192.168.42.1 +--netmask_ip=255.255.255.0 +--remote_ip=192.0.2.1 + +`$TCP_IP_PD_CASE setup ipv4` + +0.000 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3 ++0.000 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 ++0.000 bind(3, ..., ...) = 0 ++0.000 listen(3, 1) = 0 ++0.000 `$TCP_IP_PD_CASE ctl listen 192.168.42.2:8080` ++0.010 < S 0:0(0) win 65535 ++0.000 > S. 0:0(0) ack 1 win 65535 <...> ++0.000 < . 1:1(0) ack 1 win 65535 + ++0.010 `$TCP_IP_PD_CASE ctl write-zero 1000` ++0.000 > P. 1:1001(1000) ack 1 ++0.010 `$TCP_IP_PD_CASE ctl write-zero 1000` ++0.000 > P. 1001:2001(1000) ack 1 ++0.010 `$TCP_IP_PD_CASE ctl write-zero 1000` ++0.000 > P. 2001:3001(1000) ack 1 ++0.010 `$TCP_IP_PD_CASE ctl write-zero 1000` ++0.000 > P. 3001:4001(1000) ack 1 + ++0.010 < . 1:1(0) ack 1 win 65535 ++0.010 < . 1:1(0) ack 1 win 65535 ++0.010 < . 1:1(0) ack 1 win 65535 ++0.000 > . 1:1001(1000) ack 1 ++0.000 < . 1:1(0) ack 4001 win 65535 + +`$TCP_IP_PD_CASE cleanup` diff --git a/tests/packetdrill/ipv4/out_of_order_sack.pkt b/tests/packetdrill/ipv4/out_of_order_sack.pkt new file mode 100644 index 0000000..c9bb5cd --- /dev/null +++ b/tests/packetdrill/ipv4/out_of_order_sack.pkt @@ -0,0 +1,35 @@ +--ip_version=ipv4 +--local_ip=192.168.42.2 +--gateway_ip=192.168.42.1 +--netmask_ip=255.255.255.0 +--remote_ip=192.0.2.1 + +`$TCP_IP_PD_CASE setup ipv4` + +0.000 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3 ++0.000 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 ++0.000 bind(3, ..., ...) = 0 ++0.000 listen(3, 1) = 0 ++0.000 `$TCP_IP_PD_CASE ctl listen 192.168.42.2:8080` ++0.010 < S 0:0(0) win 65535 ++0.000 > S. 0:0(0) ack 1 win 65535 <...> ++0.000 < . 1:1(0) ack 1 win 65535 ++0.010 `$TCP_IP_PD_CASE ctl write-zero 0` + ++0.010 < . 1001:2001(1000) ack 1 win 65535 ++0.000 > . 1:1(0) ack 1 ++0.010 < . 1:1001(1000) ack 1 win 65535 ++0.000 > . 1:1(0) ack 2001 ++0.010 `$TCP_IP_PD_CASE ctl expect-read-zero 2000` + ++0.010 < . 1:1001(1000) ack 1 win 65535 ++0.000 > . 1:1(0) ack 2001 + +// An out-of-order segment overlapping the next segment is delivered once. ++0.010 < . 2501:3501(1000) ack 1 win 65535 ++0.000 > . 1:1(0) ack 2001 ++0.010 < . 2001:3001(1000) ack 1 win 65535 ++0.000 > . 1:1(0) ack 3501 ++0.010 `$TCP_IP_PD_CASE ctl expect-read-zero 1500` + +`$TCP_IP_PD_CASE cleanup` diff --git a/tests/packetdrill/ipv4/passive_data_close.pkt b/tests/packetdrill/ipv4/passive_data_close.pkt new file mode 100644 index 0000000..464b6f5 --- /dev/null +++ b/tests/packetdrill/ipv4/passive_data_close.pkt @@ -0,0 +1,30 @@ +--ip_version=ipv4 +--local_ip=192.168.42.2 +--gateway_ip=192.168.42.1 +--netmask_ip=255.255.255.0 +--remote_ip=192.0.2.1 + +`$TCP_IP_PD_CASE setup ipv4` + +0.000 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3 ++0.000 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 ++0.000 bind(3, ..., ...) = 0 ++0.000 listen(3, 1) = 0 ++0.000 `$TCP_IP_PD_CASE ctl listen 192.168.42.2:8080` ++0.010 < S 0:0(0) win 65535 ++0.000 > S. 0:0(0) ack 1 win 65535 ++0.000 < . 1:1(0) ack 1 win 65535 ++0.010 `$TCP_IP_PD_CASE ctl write-zero 0` + ++0.010 < P. 1:5(4) ack 1 win 65535 ++0.000 > . 1:1(0) ack 5 ++0.010 `$TCP_IP_PD_CASE ctl expect-read-zero 4` + ++0.010 < F. 5:5(0) ack 1 win 65535 ++0.000 > . 1:1(0) ack 6 ++0.010 `$TCP_IP_PD_CASE ctl expect-eof` ++0.010 `$TCP_IP_PD_CASE ctl shutdown-write` ++0.000 > F. 1:1(0) ack 6 ++0.000 < . 6:6(0) ack 2 win 65535 + +`$TCP_IP_PD_CASE cleanup` diff --git a/tests/packetdrill/ipv4/passive_smoke.pkt b/tests/packetdrill/ipv4/passive_smoke.pkt new file mode 100644 index 0000000..b87e0be --- /dev/null +++ b/tests/packetdrill/ipv4/passive_smoke.pkt @@ -0,0 +1,22 @@ +--ip_version=ipv4 +--local_ip=192.168.42.2 +--gateway_ip=192.168.42.1 +--netmask_ip=255.255.255.0 +--remote_ip=192.0.2.1 + +`$TCP_IP_PD_CASE setup ipv4` + +0.000 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3 ++0.000 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 ++0.000 bind(3, ..., ...) = 0 ++0.000 listen(3, 1) = 0 ++0.000 `$TCP_IP_PD_CASE ctl listen 192.168.42.2:8080` ++0.010 < S 0:0(0) win 65535 ++0.000 > S. 0:0(0) ack 1 win 65535 <...> ++0.000 < . 1:1(0) ack 1 win 65535 + ++0.010 `$TCP_IP_PD_CASE ctl write-zero 2` ++0.000 > P. 1:3(2) ack 1 win 65535 ++0.000 < . 1:1(0) ack 3 win 65535 + +`$TCP_IP_PD_CASE cleanup` diff --git a/tests/packetdrill/ipv4/rst_and_duplicate_syn.pkt b/tests/packetdrill/ipv4/rst_and_duplicate_syn.pkt new file mode 100644 index 0000000..ce98db3 --- /dev/null +++ b/tests/packetdrill/ipv4/rst_and_duplicate_syn.pkt @@ -0,0 +1,31 @@ +--ip_version=ipv4 +--local_ip=192.168.42.2 +--gateway_ip=192.168.42.1 +--netmask_ip=255.255.255.0 +--remote_ip=192.0.2.1 + +`$TCP_IP_PD_CASE setup ipv4` + +0.000 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3 ++0.000 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 ++0.000 bind(3, ..., ...) = 0 ++0.000 listen(3, 1) = 0 ++0.000 `$TCP_IP_PD_CASE ctl listen 192.168.42.2:8080` ++0.010 < S 0:0(0) win 65535 ++0.000 > S. 0:0(0) ack 1 win 65535 <...> ++0.000 < . 1:1(0) ack 1 win 65535 ++0.010 `$TCP_IP_PD_CASE ctl write-zero 0` + +// An out-of-window RST must not tear down the synchronized connection. ++0.010 < R 10000000:10000000(0) win 0 ++0.010 < S 0:0(0) win 65535 ++0.000 > . 1:1(0) ack 1 ++0.050 < . 1:2(1) ack 1 win 65535 ++0.000 > . 1:1(0) ack 2 ++0.010 `$TCP_IP_PD_CASE ctl expect-read-zero 1` + +// An in-window RST closes the stream. ++0.010 < R 2:2(0) win 0 ++0.010 `$TCP_IP_PD_CASE ctl expect-eof` + +`$TCP_IP_PD_CASE cleanup` diff --git a/tests/packetdrill/ipv4/rto_retransmit.pkt b/tests/packetdrill/ipv4/rto_retransmit.pkt new file mode 100644 index 0000000..e68c329 --- /dev/null +++ b/tests/packetdrill/ipv4/rto_retransmit.pkt @@ -0,0 +1,23 @@ +--ip_version=ipv4 +--local_ip=192.168.42.2 +--gateway_ip=192.168.42.1 +--netmask_ip=255.255.255.0 +--remote_ip=192.0.2.1 + +`$TCP_IP_PD_CASE setup ipv4` + +0.000 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3 ++0.000 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 ++0.000 bind(3, ..., ...) = 0 ++0.000 listen(3, 1) = 0 ++0.000 `$TCP_IP_PD_CASE ctl listen 192.168.42.2:8080` ++0.010 < S 0:0(0) win 65535 ++0.000 > S. 0:0(0) ack 1 win 65535 <...> ++0.000 < . 1:1(0) ack 1 win 65535 + ++0.010 `$TCP_IP_PD_CASE ctl write-zero 1000` ++0.000 > P. 1:1001(1000) ack 1 ++0.150~+0.450 > . 1:1001(1000) ack 1 ++0.000 < . 1:1(0) ack 1001 win 65535 + +`$TCP_IP_PD_CASE cleanup` diff --git a/tests/packetdrill/ipv4/simultaneous_close.pkt b/tests/packetdrill/ipv4/simultaneous_close.pkt new file mode 100644 index 0000000..9415641 --- /dev/null +++ b/tests/packetdrill/ipv4/simultaneous_close.pkt @@ -0,0 +1,26 @@ +--ip_version=ipv4 +--local_ip=192.168.42.2 +--gateway_ip=192.168.42.1 +--netmask_ip=255.255.255.0 +--remote_ip=192.0.2.1 + +`$TCP_IP_PD_CASE setup ipv4` + +0.000 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3 ++0.000 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 ++0.000 bind(3, ..., ...) = 0 ++0.000 listen(3, 1) = 0 ++0.000 `$TCP_IP_PD_CASE ctl listen 192.168.42.2:8080` ++0.010 < S 0:0(0) win 65535 ++0.000 > S. 0:0(0) ack 1 win 65535 <...> ++0.000 < . 1:1(0) ack 1 win 65535 + ++0.010 `$TCP_IP_PD_CASE ctl shutdown-write` ++0.000 > F. 1:1(0) ack 1 +// The peer closes before acknowledging our FIN: FIN_WAIT_1 -> CLOSING. ++0.010 < F. 1:1(0) ack 1 win 65535 ++0.000 > . 2:2(0) ack 2 ++0.010 `$TCP_IP_PD_CASE ctl expect-eof` ++0.010 < . 2:2(0) ack 2 win 65535 + +`$TCP_IP_PD_CASE cleanup` diff --git a/tests/packetdrill/ipv4/zero_window_probe.pkt b/tests/packetdrill/ipv4/zero_window_probe.pkt new file mode 100644 index 0000000..e3ba8b0 --- /dev/null +++ b/tests/packetdrill/ipv4/zero_window_probe.pkt @@ -0,0 +1,24 @@ +--ip_version=ipv4 +--local_ip=192.168.42.2 +--gateway_ip=192.168.42.1 +--netmask_ip=255.255.255.0 +--remote_ip=192.0.2.1 + +`$TCP_IP_PD_CASE setup ipv4` + +0.000 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3 ++0.000 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 ++0.000 bind(3, ..., ...) = 0 ++0.000 listen(3, 1) = 0 ++0.000 `$TCP_IP_PD_CASE ctl listen 192.168.42.2:8080` ++0.010 < S 0:0(0) win 0 ++0.000 > S. 0:0(0) ack 1 win 65535 <...> ++0.000 < . 1:1(0) ack 1 win 0 + ++0.010 `$TCP_IP_PD_CASE ctl write-zero 2` ++0.150~+0.450 > . 1:2(1) ack 1 ++0.000 < . 1:1(0) ack 2 win 1000 ++0.000 > P. 2:3(1) ack 1 ++0.000 < . 1:1(0) ack 3 win 1000 + +`$TCP_IP_PD_CASE cleanup` diff --git a/tests/packetdrill/ipv6/active_close_fin_rto.pkt b/tests/packetdrill/ipv6/active_close_fin_rto.pkt new file mode 100644 index 0000000..e3a5f03 --- /dev/null +++ b/tests/packetdrill/ipv6/active_close_fin_rto.pkt @@ -0,0 +1,26 @@ +--ip_version=ipv6 +--local_ip=fd3d:fa7b:d17d::2 +--gateway_ip=fd3d:fa7b:d17d::1 +--remote_ip=2001:db8::1 + +`$TCP_IP_PD_CASE setup ipv6` + +0.000 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3 ++0.000 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 ++0.000 bind(3, ..., ...) = 0 ++0.000 listen(3, 1) = 0 ++0.000 `$TCP_IP_PD_CASE ctl listen [fd3d:fa7b:d17d::2]:8080` ++0.010 < S 0:0(0) win 65535 ++0.000 > S. 0:0(0) ack 1 win 65535 <...> ++0.000 < . 1:1(0) ack 1 win 65535 + ++0.010 `$TCP_IP_PD_CASE ctl shutdown-write` ++0.000 > F. 1:1(0) ack 1 +// Drop the first FIN and require it to be retransmitted at the configured RTO. ++0.150~+0.450 > F. 1:1(0) ack 1 ++0.000 < . 1:1(0) ack 2 win 65535 ++0.010 < F. 1:1(0) ack 2 win 65535 ++0.000 > . 2:2(0) ack 2 ++0.010 `$TCP_IP_PD_CASE ctl expect-eof` + +`$TCP_IP_PD_CASE cleanup` diff --git a/tests/packetdrill/ipv6/active_open.pkt b/tests/packetdrill/ipv6/active_open.pkt new file mode 100644 index 0000000..af72ffa --- /dev/null +++ b/tests/packetdrill/ipv6/active_open.pkt @@ -0,0 +1,23 @@ +--ip_version=ipv6 +--local_ip=fd3d:fa7b:d17d::2 +--gateway_ip=fd3d:fa7b:d17d::1 +--remote_ip=2001:db8::1 +--bind_port=8080 +--connect_port=8080 + +`$TCP_IP_PD_CASE setup ipv6` + +0.000 socket(..., SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP) = 3 ++0.000 setsockopt(3, SOL_SOCKET, SO_MARK, [20548], 4) = 0 ++0.000 bind(3, ..., ...) = 0 ++0.000 connect(3, ..., ...) = -1 EINPROGRESS (Operation now in progress) ++0.000 `$TCP_IP_PD_CASE ctl connect [fd3d:fa7b:d17d::2]:8080 [2001:db8::1]:8080` ++0.000 > S 0:0(0) win 65535 <...> ++0.000 < S. 0:0(0) ack 1 win 65535 ++0.000 > . 1:1(0) ack 1 win 65535 + ++0.010 `$TCP_IP_PD_CASE ctl write-zero 2` ++0.000 > P. 1:3(2) ack 1 win 65535 ++0.000 < . 1:1(0) ack 3 win 65535 + +`$TCP_IP_PD_CASE cleanup` diff --git a/tests/packetdrill/ipv6/active_open_syn_rto.pkt b/tests/packetdrill/ipv6/active_open_syn_rto.pkt new file mode 100644 index 0000000..17eb5c5 --- /dev/null +++ b/tests/packetdrill/ipv6/active_open_syn_rto.pkt @@ -0,0 +1,22 @@ +--ip_version=ipv6 +--local_ip=fd3d:fa7b:d17d::2 +--gateway_ip=fd3d:fa7b:d17d::1 +--remote_ip=2001:db8::1 +--bind_port=8080 +--connect_port=8080 + +`$TCP_IP_PD_CASE setup ipv6` + +0.000 socket(..., SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP) = 3 ++0.000 setsockopt(3, SOL_SOCKET, SO_MARK, [20548], 4) = 0 ++0.000 bind(3, ..., ...) = 0 ++0.000 connect(3, ..., ...) = -1 EINPROGRESS (Operation now in progress) ++0.000 `$TCP_IP_PD_CASE ctl connect [fd3d:fa7b:d17d::2]:8080 [2001:db8::1]:8080` ++0.000 > S 0:0(0) win 65535 <...> +// Drop the first SYN and require the configured 200 ms initial RTO. ++0.150~+0.450 > S 0:0(0) win 65535 <...> ++0.000 < S. 0:0(0) ack 1 win 65535 ++0.000 > . 1:1(0) ack 1 win 65535 ++0.010 `$TCP_IP_PD_CASE ctl status` + +`$TCP_IP_PD_CASE cleanup` diff --git a/tests/packetdrill/ipv6/delayed_ack.pkt b/tests/packetdrill/ipv6/delayed_ack.pkt new file mode 100644 index 0000000..0a7a2fc --- /dev/null +++ b/tests/packetdrill/ipv6/delayed_ack.pkt @@ -0,0 +1,22 @@ +--ip_version=ipv6 +--local_ip=fd3d:fa7b:d17d::2 +--gateway_ip=fd3d:fa7b:d17d::1 +--remote_ip=2001:db8::1 + +`$TCP_IP_PD_CASE setup ipv6 50` + +0.000 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3 ++0.000 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 ++0.000 bind(3, ..., ...) = 0 ++0.000 listen(3, 1) = 0 ++0.000 `$TCP_IP_PD_CASE ctl listen [fd3d:fa7b:d17d::2]:8080` ++0.010 < S 0:0(0) win 65535 ++0.000 > S. 0:0(0) ack 1 win 65535 <...> ++0.000 < . 1:1(0) ack 1 win 65535 ++0.010 `$TCP_IP_PD_CASE ctl write-zero 0` + ++0.010 < . 1:2(1) ack 1 win 65535 ++0.030~+0.200 > . 1:1(0) ack 2 ++0.010 `$TCP_IP_PD_CASE ctl expect-read-zero 1` + +`$TCP_IP_PD_CASE cleanup` diff --git a/tests/packetdrill/ipv6/fast_retransmit_sack.pkt b/tests/packetdrill/ipv6/fast_retransmit_sack.pkt new file mode 100644 index 0000000..8aa38d5 --- /dev/null +++ b/tests/packetdrill/ipv6/fast_retransmit_sack.pkt @@ -0,0 +1,32 @@ +--ip_version=ipv6 +--local_ip=fd3d:fa7b:d17d::2 +--gateway_ip=fd3d:fa7b:d17d::1 +--remote_ip=2001:db8::1 + +`$TCP_IP_PD_CASE setup ipv6` + +0.000 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3 ++0.000 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 ++0.000 bind(3, ..., ...) = 0 ++0.000 listen(3, 1) = 0 ++0.000 `$TCP_IP_PD_CASE ctl listen [fd3d:fa7b:d17d::2]:8080` ++0.010 < S 0:0(0) win 65535 ++0.000 > S. 0:0(0) ack 1 win 65535 <...> ++0.000 < . 1:1(0) ack 1 win 65535 + ++0.010 `$TCP_IP_PD_CASE ctl write-zero 1000` ++0.000 > P. 1:1001(1000) ack 1 ++0.010 `$TCP_IP_PD_CASE ctl write-zero 1000` ++0.000 > P. 1001:2001(1000) ack 1 ++0.010 `$TCP_IP_PD_CASE ctl write-zero 1000` ++0.000 > P. 2001:3001(1000) ack 1 ++0.010 `$TCP_IP_PD_CASE ctl write-zero 1000` ++0.000 > P. 3001:4001(1000) ack 1 + ++0.010 < . 1:1(0) ack 1 win 65535 ++0.010 < . 1:1(0) ack 1 win 65535 ++0.010 < . 1:1(0) ack 1 win 65535 ++0.000 > . 1:1001(1000) ack 1 ++0.000 < . 1:1(0) ack 4001 win 65535 + +`$TCP_IP_PD_CASE cleanup` diff --git a/tests/packetdrill/ipv6/out_of_order_sack.pkt b/tests/packetdrill/ipv6/out_of_order_sack.pkt new file mode 100644 index 0000000..650269e --- /dev/null +++ b/tests/packetdrill/ipv6/out_of_order_sack.pkt @@ -0,0 +1,34 @@ +--ip_version=ipv6 +--local_ip=fd3d:fa7b:d17d::2 +--gateway_ip=fd3d:fa7b:d17d::1 +--remote_ip=2001:db8::1 + +`$TCP_IP_PD_CASE setup ipv6` + +0.000 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3 ++0.000 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 ++0.000 bind(3, ..., ...) = 0 ++0.000 listen(3, 1) = 0 ++0.000 `$TCP_IP_PD_CASE ctl listen [fd3d:fa7b:d17d::2]:8080` ++0.010 < S 0:0(0) win 65535 ++0.000 > S. 0:0(0) ack 1 win 65535 <...> ++0.000 < . 1:1(0) ack 1 win 65535 ++0.010 `$TCP_IP_PD_CASE ctl write-zero 0` + ++0.010 < . 1001:2001(1000) ack 1 win 65535 ++0.000 > . 1:1(0) ack 1 ++0.010 < . 1:1001(1000) ack 1 win 65535 ++0.000 > . 1:1(0) ack 2001 ++0.010 `$TCP_IP_PD_CASE ctl expect-read-zero 2000` + ++0.010 < . 1:1001(1000) ack 1 win 65535 ++0.000 > . 1:1(0) ack 2001 + +// An out-of-order segment overlapping the next segment is delivered once. ++0.010 < . 2501:3501(1000) ack 1 win 65535 ++0.000 > . 1:1(0) ack 2001 ++0.010 < . 2001:3001(1000) ack 1 win 65535 ++0.000 > . 1:1(0) ack 3501 ++0.010 `$TCP_IP_PD_CASE ctl expect-read-zero 1500` + +`$TCP_IP_PD_CASE cleanup` diff --git a/tests/packetdrill/ipv6/passive_data_close.pkt b/tests/packetdrill/ipv6/passive_data_close.pkt new file mode 100644 index 0000000..04b169b --- /dev/null +++ b/tests/packetdrill/ipv6/passive_data_close.pkt @@ -0,0 +1,29 @@ +--ip_version=ipv6 +--local_ip=fd3d:fa7b:d17d::2 +--gateway_ip=fd3d:fa7b:d17d::1 +--remote_ip=2001:db8::1 + +`$TCP_IP_PD_CASE setup ipv6` + +0.000 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3 ++0.000 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 ++0.000 bind(3, ..., ...) = 0 ++0.000 listen(3, 1) = 0 ++0.000 `$TCP_IP_PD_CASE ctl listen [fd3d:fa7b:d17d::2]:8080` ++0.010 < S 0:0(0) win 65535 ++0.000 > S. 0:0(0) ack 1 win 65535 ++0.000 < . 1:1(0) ack 1 win 65535 ++0.010 `$TCP_IP_PD_CASE ctl write-zero 0` + ++0.010 < P. 1:5(4) ack 1 win 65535 ++0.000 > . 1:1(0) ack 5 ++0.010 `$TCP_IP_PD_CASE ctl expect-read-zero 4` + ++0.010 < F. 5:5(0) ack 1 win 65535 ++0.000 > . 1:1(0) ack 6 ++0.010 `$TCP_IP_PD_CASE ctl expect-eof` ++0.010 `$TCP_IP_PD_CASE ctl shutdown-write` ++0.000 > F. 1:1(0) ack 6 ++0.000 < . 6:6(0) ack 2 win 65535 + +`$TCP_IP_PD_CASE cleanup` diff --git a/tests/packetdrill/ipv6/passive_smoke.pkt b/tests/packetdrill/ipv6/passive_smoke.pkt new file mode 100644 index 0000000..f7af97b --- /dev/null +++ b/tests/packetdrill/ipv6/passive_smoke.pkt @@ -0,0 +1,21 @@ +--ip_version=ipv6 +--local_ip=fd3d:fa7b:d17d::2 +--gateway_ip=fd3d:fa7b:d17d::1 +--remote_ip=2001:db8::1 + +`$TCP_IP_PD_CASE setup ipv6` + +0.000 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3 ++0.000 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 ++0.000 bind(3, ..., ...) = 0 ++0.000 listen(3, 1) = 0 ++0.000 `$TCP_IP_PD_CASE ctl listen [fd3d:fa7b:d17d::2]:8080` ++0.010 < S 0:0(0) win 65535 ++0.000 > S. 0:0(0) ack 1 win 65535 <...> ++0.000 < . 1:1(0) ack 1 win 65535 + ++0.010 `$TCP_IP_PD_CASE ctl write-zero 2` ++0.000 > P. 1:3(2) ack 1 win 65535 ++0.000 < . 1:1(0) ack 3 win 65535 + +`$TCP_IP_PD_CASE cleanup` diff --git a/tests/packetdrill/ipv6/rst_and_duplicate_syn.pkt b/tests/packetdrill/ipv6/rst_and_duplicate_syn.pkt new file mode 100644 index 0000000..645c422 --- /dev/null +++ b/tests/packetdrill/ipv6/rst_and_duplicate_syn.pkt @@ -0,0 +1,30 @@ +--ip_version=ipv6 +--local_ip=fd3d:fa7b:d17d::2 +--gateway_ip=fd3d:fa7b:d17d::1 +--remote_ip=2001:db8::1 + +`$TCP_IP_PD_CASE setup ipv6` + +0.000 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3 ++0.000 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 ++0.000 bind(3, ..., ...) = 0 ++0.000 listen(3, 1) = 0 ++0.000 `$TCP_IP_PD_CASE ctl listen [fd3d:fa7b:d17d::2]:8080` ++0.010 < S 0:0(0) win 65535 ++0.000 > S. 0:0(0) ack 1 win 65535 <...> ++0.000 < . 1:1(0) ack 1 win 65535 ++0.010 `$TCP_IP_PD_CASE ctl write-zero 0` + +// An out-of-window RST must not tear down the synchronized connection. ++0.010 < R 10000000:10000000(0) win 0 ++0.010 < S 0:0(0) win 65535 ++0.000 > . 1:1(0) ack 1 ++0.050 < . 1:2(1) ack 1 win 65535 ++0.000 > . 1:1(0) ack 2 ++0.010 `$TCP_IP_PD_CASE ctl expect-read-zero 1` + +// An in-window RST closes the stream. ++0.010 < R 2:2(0) win 0 ++0.010 `$TCP_IP_PD_CASE ctl expect-eof` + +`$TCP_IP_PD_CASE cleanup` diff --git a/tests/packetdrill/ipv6/rto_retransmit.pkt b/tests/packetdrill/ipv6/rto_retransmit.pkt new file mode 100644 index 0000000..42e8a0a --- /dev/null +++ b/tests/packetdrill/ipv6/rto_retransmit.pkt @@ -0,0 +1,22 @@ +--ip_version=ipv6 +--local_ip=fd3d:fa7b:d17d::2 +--gateway_ip=fd3d:fa7b:d17d::1 +--remote_ip=2001:db8::1 + +`$TCP_IP_PD_CASE setup ipv6` + +0.000 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3 ++0.000 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 ++0.000 bind(3, ..., ...) = 0 ++0.000 listen(3, 1) = 0 ++0.000 `$TCP_IP_PD_CASE ctl listen [fd3d:fa7b:d17d::2]:8080` ++0.010 < S 0:0(0) win 65535 ++0.000 > S. 0:0(0) ack 1 win 65535 <...> ++0.000 < . 1:1(0) ack 1 win 65535 + ++0.010 `$TCP_IP_PD_CASE ctl write-zero 1000` ++0.000 > P. 1:1001(1000) ack 1 ++0.150~+0.450 > . 1:1001(1000) ack 1 ++0.000 < . 1:1(0) ack 1001 win 65535 + +`$TCP_IP_PD_CASE cleanup` diff --git a/tests/packetdrill/ipv6/simultaneous_close.pkt b/tests/packetdrill/ipv6/simultaneous_close.pkt new file mode 100644 index 0000000..99c1a6e --- /dev/null +++ b/tests/packetdrill/ipv6/simultaneous_close.pkt @@ -0,0 +1,25 @@ +--ip_version=ipv6 +--local_ip=fd3d:fa7b:d17d::2 +--gateway_ip=fd3d:fa7b:d17d::1 +--remote_ip=2001:db8::1 + +`$TCP_IP_PD_CASE setup ipv6` + +0.000 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3 ++0.000 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 ++0.000 bind(3, ..., ...) = 0 ++0.000 listen(3, 1) = 0 ++0.000 `$TCP_IP_PD_CASE ctl listen [fd3d:fa7b:d17d::2]:8080` ++0.010 < S 0:0(0) win 65535 ++0.000 > S. 0:0(0) ack 1 win 65535 <...> ++0.000 < . 1:1(0) ack 1 win 65535 + ++0.010 `$TCP_IP_PD_CASE ctl shutdown-write` ++0.000 > F. 1:1(0) ack 1 +// The peer closes before acknowledging our FIN: FIN_WAIT_1 -> CLOSING. ++0.010 < F. 1:1(0) ack 1 win 65535 ++0.000 > . 2:2(0) ack 2 ++0.010 `$TCP_IP_PD_CASE ctl expect-eof` ++0.010 < . 2:2(0) ack 2 win 65535 + +`$TCP_IP_PD_CASE cleanup` diff --git a/tests/packetdrill/ipv6/zero_window_probe.pkt b/tests/packetdrill/ipv6/zero_window_probe.pkt new file mode 100644 index 0000000..3d42661 --- /dev/null +++ b/tests/packetdrill/ipv6/zero_window_probe.pkt @@ -0,0 +1,23 @@ +--ip_version=ipv6 +--local_ip=fd3d:fa7b:d17d::2 +--gateway_ip=fd3d:fa7b:d17d::1 +--remote_ip=2001:db8::1 + +`$TCP_IP_PD_CASE setup ipv6` + +0.000 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3 ++0.000 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 ++0.000 bind(3, ..., ...) = 0 ++0.000 listen(3, 1) = 0 ++0.000 `$TCP_IP_PD_CASE ctl listen [fd3d:fa7b:d17d::2]:8080` ++0.010 < S 0:0(0) win 0 ++0.000 > S. 0:0(0) ack 1 win 65535 <...> ++0.000 < . 1:1(0) ack 1 win 0 + ++0.010 `$TCP_IP_PD_CASE ctl write-zero 2` ++0.150~+0.450 > . 1:2(1) ack 1 ++0.000 < . 1:1(0) ack 2 win 1000 ++0.000 > P. 2:3(1) ack 1 ++0.000 < . 1:1(0) ack 3 win 1000 + +`$TCP_IP_PD_CASE cleanup`