diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..0f243d7 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,5 @@ +{ + "enabledPlugins": { + "superpowers@claude-plugins-official": true + } +} diff --git a/.gitignore b/.gitignore index 551c56c..1dcbf39 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ .terraform/ .terraform.lock.hcl *.tfstate* +*.data +*.data.old +*.perf diff --git a/.vscode/settings.json b/.vscode/settings.json index fc6712e..e7f9fae 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,5 @@ { "rust-analyzer.cargo.features": "all", "rust-analyzer.check.features": "all", + "remote.autoForwardPortsFallback": 0, } diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..a152fdf --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,94 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +**VoidNet** is a high-performance, zero-copy AF_XDP networking library for Rust. It provides safe bindings to Linux's AF_XDP interface, enabling ultra-low latency packet processing by bypassing the kernel network stack. The library crate is named `libvoid` (crate name `voidnet`). + +**Requirements:** Linux kernel >= 5.4, Rust >= 1.85 (2024 edition), root or CAP_NET_RAW + CAP_BPF, XDP-compatible NIC. Supported targets: x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu. + +**System dependencies:** `libbpf-dev libelf-dev libpcap-dev libmnl-dev linux-headers-$(uname -r)` and `clang` (for BPF compilation). + +## Build Commands + +```bash +cargo build # Build (default, no async features) +cargo build --all-features # Build with all async runtimes +cargo build --features tokio # Build with tokio support +cargo build --features smol # Build with smol support +cargo build --features local # Build with custom local executor + +cargo test # Run sync integration tests +cargo test --features local # Run local executor tests +cargo test --features tokio # Run tokio tests +cargo test --features smol # Run smol tests +cargo test --all-features # Run all tests + +cargo test --test sync # Run a specific test suite +cargo test --test sync test_name # Run a single test + +cargo llvm-cov --all-features --workspace --codecov # Coverage report +``` + +## Feature Flags + +- `async` — Enables futures-core/futures-util (base for all async features) +- `local` — Custom high-performance local executor (implies `async`) +- `smol` — Smol runtime integration (implies `async`) +- `tokio` — Tokio runtime integration (implies `async`) + +## Architecture + +### Module Structure + +**`xdp/`** — Core AF_XDP abstraction (the heart of the library): +- `context/` — XDP program loading and interface attachment (`XdpContext`, `XdpContextBuilder`) +- `socket/` — AF_XDP socket I/O (`Socket`, `SocketRx`, `SocketTx`, `SocketBuilder`) +- `umem/` — Shared memory management (`Umem`, `UmemBuilder`, `FillQueue`, `CompletionQueue`) +- `frame/` — Packet frame buffers (`Frame`, `FrameBuffer` trait, `BasicFrameBuffer`) +- `program/` — BPF program and map management (`XdpProgram`, `AttachMode`) +- `ring/` — Ring buffer primitives (`Producer`, `Consumer`) +- `futures/` — Async runtime integrations, feature-gated (`tokio/`, `smol/`, `local/`) + +**`net/`** — Protocol stack (currently being refactored on `runtime` branch): +- `wire/` — Wire format parsing (ethernet, ARP, IPv4/IPv6, ICMPv4/v6, UDP) +- `packet/` — Packet composition (`PacketReader`, `PacketWriter`) +- `handler/` — Protocol handlers (IPv4, IPv6, ICMPv4, ICMPv6, UDP) +- `socket/` — High-level socket abstractions (`UdpSocket`, `SharedQueue`) +- `neighbor.rs` — ARP neighbor resolution cache +- `pmtu.rs` — Path MTU discovery cache + +**`rt/`** — High-level runtime utilities: +- `local.rs` — `LocalRuntimeBuilder` pre-configured packet processing pipeline with integrated protocol handlers +- `affinity.rs` — CPU affinity management +- `thread.rs` — Thread utilities +- `waker.rs` — No-op waker for the local executor + +### Key Data Flow + +Packets flow: NIC → XDP BPF program → XSKMAP → AF_XDP socket → shared UMEM frames → userspace processing. All via mmap'd shared memory with zero copies. + +### Design Patterns + +- **Builder pattern** on all major types (`XdpContextBuilder`, `SocketBuilder`, `UmemBuilder`) +- **Ownership tokens** (`UmemOwner`, `SocketOwner`) for resource lifetime safety +- **Split I/O** — `Socket` splits into independent `SocketRx` + `SocketTx` +- **Feature-gated compilation** — async code behind `cfg(feature = "...")` with zero cost for unused runtimes +- **Batch processing** — Frames processed in `FrameBuffer` collections, not individually + +### BPF Kernel Program + +`bpf/xdp_kern.c` is compiled at build time by `build.rs` using clang. It implements round-robin packet distribution across AF_XDP sockets via XSKMAP. The compiled object (`bpf/xdp_kern.o`) is gitignored. + +### Testing Infrastructure + +Integration tests use `xdp::test_utils::TestVethPair` to create virtual ethernet pairs. Tests spawn cross-thread packet exchange targeting 1M packets for throughput validation. Tests require root privileges — `.cargo/config.toml` configures `sudo -E` as the test runner for both x86_64 and aarch64 targets. + +### Running Examples + +```bash +cargo run --example sync-echo -- --if-name eth0 --queue 0 +cargo run --example sync-rx -- --if-name eth0 --queue 0 +cargo run --example tokio-rx --features tokio -- --if-name eth0 --queue 0 +``` diff --git a/Cargo.lock b/Cargo.lock index 1015847..51d0c30 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,6 +76,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "anyhow" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" + [[package]] name = "async-channel" version = "2.5.0" @@ -310,6 +316,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core", +] + [[package]] name = "ciborium" version = "0.2.2" @@ -388,6 +405,17 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +[[package]] +name = "coarsetime" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e58eb270476aa4fc7843849f8a35063e8743b4dbcdf6dd0f8ea0886980c204c2" +dependencies = [ + "libc", + "wasix", + "wasm-bindgen", +] + [[package]] name = "colorchoice" version = "1.0.4" @@ -403,6 +431,26 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "core_affinity" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a034b3a7b624016c6e13f5df875747cc25f884156aad2abd12b6c46797971342" +dependencies = [ + "libc", + "num_cpus", + "winapi", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "criterion" version = "0.8.1" @@ -457,6 +505,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -523,7 +580,7 @@ checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" dependencies = [ "cfg-if", "crossbeam-utils", - "hashbrown", + "hashbrown 0.14.5", "lock_api", "once_cell", "parking_lot_core", @@ -578,6 +635,12 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "errno" version = "0.3.14" @@ -627,6 +690,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "futures" version = "0.3.31" @@ -729,16 +798,29 @@ dependencies = [ "slab", ] +[[package]] +name = "getifaddrs" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06f770241660efeeedb30b48ccbe0a8e6f649322148237539e71f3ccd14a4461" +dependencies = [ + "bitflags", + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "getrandom" -version = "0.3.4" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" dependencies = [ "cfg-if", "libc", "r-efi", + "rand_core", "wasip2", + "wasip3", ] [[package]] @@ -776,6 +858,21 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "heck" version = "0.5.0" @@ -788,12 +885,30 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + [[package]] name = "ipnetwork" version = "0.20.0" @@ -834,6 +949,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.178" @@ -994,6 +1115,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "objc2" version = "0.6.3" @@ -1228,15 +1359,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - [[package]] name = "prettyplease" version = "0.2.37" @@ -1295,32 +1417,20 @@ checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" dependencies = [ - "ppv-lite86", + "chacha20", + "getrandom", "rand_core", ] [[package]] name = "rand_core" -version = "0.9.3" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" -dependencies = [ - "getrandom", -] +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" [[package]] name = "rayon" @@ -1420,6 +1530,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" version = "1.0.228" @@ -1612,6 +1728,12 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "utf8parse" version = "0.2.2" @@ -1624,13 +1746,18 @@ version = "0.1.0" dependencies = [ "async-io", "clap", + "coarsetime", + "core_affinity", "criterion", + "crossbeam-queue", + "crossbeam-utils", "ctrlc", "dashmap", "errno", "futures", "futures-core", "futures-util", + "getifaddrs", "libc", "libxdp-sys", "macro_rules_attribute", @@ -1638,6 +1765,8 @@ dependencies = [ "neli", "pnet", "rand", + "rustc-hash", + "smallvec", "smol", "smol-macros", "thiserror", @@ -1666,7 +1795,25 @@ version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.46.0", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasix" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1757e0d1f8456693c7e5c6c629bdb54884e032aa0bb53c155f6a39f94440d332" +dependencies = [ + "wasi", ] [[package]] @@ -1714,6 +1861,40 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "web-sys" version = "0.3.83" @@ -1761,13 +1942,22 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets", + "windows-targets 0.53.5", ] [[package]] @@ -1779,6 +1969,22 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + [[package]] name = "windows-targets" version = "0.53.5" @@ -1786,58 +1992,106 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + [[package]] name = "windows_aarch64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + [[package]] name = "windows_aarch64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + [[package]] name = "windows_i686_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + [[package]] name = "windows_i686_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + [[package]] name = "windows_i686_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + [[package]] name = "windows_x86_64_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + [[package]] name = "windows_x86_64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "windows_x86_64_msvc" version = "0.53.1" @@ -1850,6 +2104,94 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + [[package]] name = "zerocopy" version = "0.8.31" diff --git a/Cargo.toml b/Cargo.toml index d48b0bb..35aa79c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,17 +4,22 @@ version = "0.1.0" edition = "2024" [dependencies] +async-io = { version = "2", default-features = false, optional = true } +coarsetime = { version = "0.1", default-features = false } +core_affinity = { version = "0.8", default-features = false } +crossbeam-queue = { version = "0.3", default-features = true } +crossbeam-utils = { version = "0.8", default-features = true } dashmap = { version = "6.1", default-features = false } errno = { version = "0.3", default-features = false } -futures-core = { version = "0.3", default-features = false, optional = true } -futures-util = { version = "0.3", default-features = false, features = [ - "std", -], optional = true } +futures-core = { version = "0.3", default-features = false } +futures-util = { version = "0.3", default-features = false, features = ["std"] } +getifaddrs = { version = "0.6", default-features = false } libc = { version = "0.2", default-features = false } libxdp-sys = { version = "=0.2.2", default-features = false } # Latest version of this library has a broken build.... memmap2 = { version = "0.9", default-features = false } +rustc-hash = { version = "2" } neli = { version = "0.7", default-features = false, features = ["sync"] } -async-io = { version = "2", default-features = false, optional = true } +smallvec = { version = "1.15", default-features = false } thiserror = { version = "2", default-features = false } tokio = { version = "1", default-features = false, features = [ "net", @@ -24,17 +29,17 @@ tokio = { version = "1", default-features = false, features = [ clap = { version = "4.5", features = ["derive", "cargo"] } criterion = { version = "0.8" } ctrlc = { version = "3.2", features = ["termination"] } -pnet = { version = "0.35" } -rand = { version = "0.9" } -tokio = { version = "1.40", features = ["full"] } futures = { version = "0.3" } +macro_rules_attribute = { version = "0.2" } +pnet = { version = "0.35" } +rand = { version = "0.10" } smol = { version = "2" } smol-macros = { version = "0.1" } -macro_rules_attribute = { version = "0.2" } +tokio = { version = "1.40", features = ["full"] } [features] default = [] -async = ["dep:futures-core", "dep:futures-util"] +async = [] local = ["async"] smol = ["dep:async-io", "async"] tokio = ["dep:tokio", "async"] @@ -113,3 +118,11 @@ required-features = ["tokio"] name = "tokio-tx" path = "examples/tokio-tx.rs" required-features = ["tokio"] + +[[example]] +name = "std-tcp-echo-server" +path = "examples/std-tcp-echo-server.rs" + +[[example]] +name = "std-tcp-echo-client" +path = "examples/std-tcp-echo-client.rs" diff --git a/dist/docs/rfc-9293-tcp-condensed.md b/dist/docs/rfc-9293-tcp-condensed.md new file mode 100644 index 0000000..54583fa --- /dev/null +++ b/dist/docs/rfc-9293-tcp-condensed.md @@ -0,0 +1,1177 @@ +# RFC 9293 - TCP Implementation Reference + +Condensed from RFC 9293 (August 2022, obsoletes RFC 793). +Organized as an implementation guide for a fully compliant TCP stack. + +--- + +## 1. Header Format + +``` + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Source Port | Destination Port | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Sequence Number | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Acknowledgment Number | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Data | |C|E|U|A|P|R|S|F| | +| Offset| Rsrvd |W|C|R|C|S|S|Y|I| Window | +| | |R|E|G|K|H|T|N|N| | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Checksum | Urgent Pointer | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| [Options + Padding] | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +### Field Details + +| Field | Bits | Description | +|-------|------|-------------| +| Source Port | 16 | Sender's port | +| Destination Port | 16 | Receiver's port | +| Sequence Number | 32 | First data octet seq (or ISN if SYN set; first data octet is ISN+1) | +| Acknowledgment Number | 32 | Next seq expected (valid when ACK set; ACK is always set once connection established) | +| Data Offset | 4 | Header length in 32-bit words (min 5, max 15) | +| Reserved | 4 | Must be zero on send, ignored on receive | +| CWR | 1 | Congestion Window Reduced (RFC 3168) | +| ECE | 1 | ECN-Echo (RFC 3168) | +| URG | 1 | Urgent pointer is significant | +| ACK | 1 | Acknowledgment field is significant | +| PSH | 1 | Push data to receiver application | +| RST | 1 | Reset the connection | +| SYN | 1 | Synchronize sequence numbers | +| FIN | 1 | No more data from sender | +| Window | 16 | Receive window size in octets (MUST treat as unsigned, MUST-1). RECOMMENDED to use 32-bit fields internally (REC-1). | +| Checksum | 16 | Ones' complement checksum over pseudo-header + header + data | +| Urgent Pointer | 16 | Offset from SEQ to octet AFTER urgent data (when URG set) | + +### Checksum Pseudo-Headers + +**IPv4 pseudo-header (96 bits):** +- Source Address (32), Destination Address (32), zero (8), Protocol=6 (8), TCP Length (16) + +**IPv6 pseudo-header (320 bits):** +- Source Address (128), Destination Address (128), Upper-Layer Packet Length (32), zero (24), Next Header=6 (8) + +**Computation details:** +- Sum all 16-bit words of pseudo-header + TCP header + data +- Checksum field itself is replaced with zeros during computation +- If segment has odd number of octets, pad with a zero octet for checksum purposes (not transmitted) +- Store the ones' complement of the sum in the checksum field + +**Requirements:** +- Sender MUST compute checksum (MUST-2) +- Receiver MUST verify checksum (MUST-3) +- Checksum is NEVER optional + +--- + +## 2. Options + +### Mandatory Options (MUST-4) + +| Kind | Length | Total Bytes | Name | +|------|--------|-------------|------| +| 0 | - | 1 | End of Option List (EOL) — no length field (single-octet option) | +| 1 | - | 1 | No-Operation (NOP) — no length field (single-octet option, used for alignment padding) | +| 2 | 4 | 4 | Maximum Segment Size (MSS) — SYN segments only (MUST NOT send on non-SYN, MUST-65) | + +### Recommended Options (not required for compliance) + +| Kind | Length | Name | RFC | +|------|--------|------|-----| +| 3 | 3 | Window Scale (WS) | 7323 | +| 4 | 2 | SACK Permitted | 2018 | +| 5 | var | SACK | 2018 | +| 8 | 10 | Timestamps (TS) | 7323 | + +### Option Processing Rules + +- MUST receive options in any segment (MUST-5) +- MUST ignore unknown options with length fields (MUST-6) +- All options except EOL and NOP MUST have length fields (MUST-68) +- MUST handle illegal option lengths (e.g., zero) - suggested: reset + log (MUST-7) +- MUST process options regardless of word alignment (MUST-64) +- Padding after options MUST be zeros (MUST-69) + +--- + +## 3. Transmission Control Block (TCB) + +The TCB holds all per-connection state. + +### Send Sequence Variables + +| Variable | Description | +|----------|-------------| +| SND.UNA | Oldest unacknowledged sequence number | +| SND.NXT | Next sequence number to send | +| SND.WND | Send window (octets peer will accept) | +| SND.UP | Send urgent pointer | +| SND.WL1 | Segment seq used for last window update | +| SND.WL2 | Segment ack used for last window update | +| ISS | Initial send sequence number | + +### Receive Sequence Variables + +| Variable | Description | +|----------|-------------| +| RCV.NXT | Next expected receive sequence number (left edge of receive window) | +| RCV.WND | Receive window size | +| RCV.UP | Receive urgent pointer | +| IRS | Initial receive sequence number | + +### Current Segment Variables (per-packet, not stored) + +| Variable | Description | +|----------|-------------| +| SEG.SEQ | Segment sequence number | +| SEG.ACK | Segment acknowledgment number | +| SEG.LEN | Segment length (data + SYN/FIN occupy sequence space) | +| SEG.WND | Segment window | +| SEG.UP | Segment urgent pointer | + +### Additional TCB Fields + +- Local/remote IP + port (connection identity / 4-tuple) +- Connection state (see state machine) +- Send/receive buffers and retransmission queue +- RTO timer state (SRTT, RTTVAR, RTO per RFC 6298) +- Whether connection originated from passive or active OPEN (MUST-11) +- User timeout value +- Congestion control state (cwnd, ssthresh) +- MAX.SND.WND - largest window ever received (for RFC 5961) + +--- + +## 4. Sequence Number Arithmetic + +All sequence number math is **modulo 2^32**. + +### Comparisons + +Use wrapping comparison (treating as signed difference): + +``` +a < b iff (a - b) as i32 < 0 (wrapping subtraction) +a <= b iff (a - b) as i32 <= 0 +``` + +### Key Tests + +**Acceptable ACK:** +``` +SND.UNA < SEG.ACK <= SND.NXT +``` + +**Segment fully acknowledged** (for retransmit queue removal): +``` +SEG.SEQ + SEG.LEN <= SEG.ACK (from the ACK) +``` + +**Segment acceptability** (4 cases): + +| Seg Len | Recv Win | Test | +|---------|----------|------| +| 0 | 0 | SEG.SEQ == RCV.NXT | +| 0 | >0 | RCV.NXT <= SEG.SEQ < RCV.NXT+RCV.WND | +| >0 | 0 | NOT acceptable | +| >0 | >0 | RCV.NXT <= SEG.SEQ < RCV.NXT+RCV.WND **OR** RCV.NXT <= SEG.SEQ+SEG.LEN-1 < RCV.NXT+RCV.WND | + +When RCV.WND is zero: still MUST process RST and URG fields (MUST-66). + +### SYN and FIN in Sequence Space + +- SYN occupies 1 sequence number, positioned BEFORE any data in the segment +- FIN occupies 1 sequence number, positioned AFTER the last data octet +- SEG.LEN includes data + SYN (if present) + FIN (if present) +- When SYN is present, SEG.SEQ is the sequence number of the SYN + +--- + +## 5. Initial Sequence Number (ISN) Selection + +MUST use a clock-driven scheme (MUST-8). + +SHOULD use the following formula (SHLD-1): + +``` +ISN = M + F(local_ip, local_port, remote_ip, remote_port, secret_key) +``` + +- **M**: 32-bit counter incrementing ~every 4 microseconds (cycles every ~4.55 hours) +- **F()**: PRF (e.g., cryptographic hash) — MUST NOT be computable from outside (MUST-9) +- **secret_key**: random, unknown to attackers + +The PRF prevents off-path attackers from predicting ISNs. + +Note: The specific formula is SHLD-1 (a SHOULD recommendation). Any clock-driven ISN selection that satisfies MUST-8 and MUST-9 is compliant. + +### Quiet Time Concept + +After a reboot or crash that loses memory of sequence numbers in use, a host should delay at least MSL (2 minutes) before emitting any TCP segments, to avoid overlap with potentially still in-flight segments from prior connection incarnations. This is the "quiet time" specification. + +In practice, this is considered safe to ignore on modern networks because: (a) ISS and ephemeral port randomization reduce reuse likelihood, (b) effective MSL of the Internet has declined, and (c) reboots typically take longer than MSL. Implementations MAY violate quiet time, at the risk of old data being accepted as new. + +### High-Speed Sequence Space Concerns (PAWS) + +At high data rates the 32-bit sequence space cycles quickly: +- 1 Gbps: 34 seconds +- 10 Gbps: 3 seconds +- 100 Gbps: ~0.3 seconds + +These cycle times can be shorter than MSL, so TCP Timestamp Options and Protection Against Wrapped Sequences (PAWS, RFC 7323) are needed to detect and discard old duplicates at high speeds. + +--- + +## 6. Connection State Machine + +### States + +| State | Description | +|-------|-------------| +| CLOSED | No connection (fictional - no TCB exists) | +| LISTEN | Waiting for incoming SYN (passive open) | +| SYN-SENT | SYN sent, waiting for SYN-ACK (active open) | +| SYN-RECEIVED | SYN received and SYN-ACK sent, waiting for ACK | +| ESTABLISHED | Connection open, data transfer | +| FIN-WAIT-1 | FIN sent, waiting for ACK of FIN or remote FIN | +| FIN-WAIT-2 | Our FIN ACKed, waiting for remote FIN | +| CLOSE-WAIT | Remote FIN received, waiting for local CLOSE | +| CLOSING | Both sides sent FIN, waiting for ACK of our FIN | +| LAST-ACK | Remote FIN received + our FIN sent, waiting for ACK | +| TIME-WAIT | Waiting 2*MSL before deleting TCB | + +### Key Transitions + +``` +CLOSED --[passive OPEN]--> LISTEN +CLOSED --[active OPEN, send SYN]--> SYN-SENT + +LISTEN --[rcv SYN, send SYN,ACK]--> SYN-RECEIVED +LISTEN --[active OPEN, send SYN]--> SYN-SENT + +SYN-SENT --[rcv SYN,ACK, send ACK]--> ESTABLISHED +SYN-SENT --[rcv SYN (no ACK), send SYN,ACK]--> SYN-RECEIVED (simultaneous open) + +SYN-RECEIVED --[rcv ACK of SYN]--> ESTABLISHED +SYN-RECEIVED --[rcv RST, was passive]--> LISTEN +SYN-RECEIVED --[rcv RST, was active]--> CLOSED +SYN-RECEIVED --[CLOSE, send FIN]--> FIN-WAIT-1 + +ESTABLISHED --[CLOSE, send FIN]--> FIN-WAIT-1 +ESTABLISHED --[rcv FIN, send ACK]--> CLOSE-WAIT + +FIN-WAIT-1 --[rcv ACK of FIN]--> FIN-WAIT-2 +FIN-WAIT-1 --[rcv FIN, send ACK]--> CLOSING +FIN-WAIT-1 --[rcv FIN+ACK of FIN, send ACK]--> TIME-WAIT + +FIN-WAIT-2 --[rcv FIN, send ACK]--> TIME-WAIT + +CLOSE-WAIT --[CLOSE, send FIN]--> LAST-ACK + +CLOSING --[rcv ACK of FIN]--> TIME-WAIT + +LAST-ACK --[rcv ACK of FIN]--> CLOSED (delete TCB) + +TIME-WAIT --[2*MSL timeout]--> CLOSED (delete TCB) +``` + +**Requirements:** +- MUST support simultaneous open (MUST-10) +- MUST track whether SYN-RECEIVED came from passive or active OPEN (MUST-11) +- TIME-WAIT MUST last 2*MSL (MUST-13). MSL = 2 minutes. +- MAY accept new SYN from TIME-WAIT if new ISN > largest sequence number used on previous incarnation; returns to TIME-WAIT if old dup (MAY-2) +- SHOULD use Timestamps to reduce TIME-WAIT duration when Timestamp Options are in use (SHLD-4) + +--- + +## 7. Connection Establishment (Three-Way Handshake) + +### Normal 3WHS + +``` +A (CLOSED) B (LISTEN) + SYN-SENT --> --> SYN-RECEIVED + ESTABLISHED <-- <-- SYN-RECEIVED + ESTABLISHED --> --> ESTABLISHED +``` + +### Simultaneous Open + +Both peers send SYN before receiving peer's SYN. +Each transitions: CLOSED -> SYN-SENT -> SYN-RECEIVED -> ESTABLISHED. + +### Old Duplicate SYN Recovery + +If a stale SYN arrives at a listener, the listener sends SYN-ACK. +The original sender detects incorrect ACK field, sends RST. +Listener returns to LISTEN. + +### Half-Open Connections + +An established connection is "half-open" if one side has closed/crashed without the other knowing, or if the two ends become desynchronized. Half-open connections are automatically reset when data transfer is attempted: + +**Case 1 — Crashed side reopens:** A reboots, sends new SYN. B (still ESTABLISHED) replies with ACK for old sequence. A detects unacceptable ACK, sends RST. B aborts. Then normal 3WHS proceeds. + +**Case 2 — Live side sends data:** B sends data to rebooted A. A has no TCB for this connection, sends RST. B aborts. + +**Case 3 — Old duplicate SYN to two passive sockets:** A stale SYN triggers SYN-ACK from a listener. The peer (also in LISTEN) generates RST because the ACK is unacceptable. Original listener returns to LISTEN. + +These cases are all handled by the RST generation and processing rules in Section 8. + +--- + +## 8. Reset (RST) Generation and Processing + +### When to Generate RST + +The side of a connection issuing a reset SHOULD enter the TIME-WAIT state, as this helps reduce load on busy servers (see RFC reference on TIME-WAIT effects). + +**CLOSED state:** Any non-RST segment -> send RST. +- ACK off: `` +- ACK on: `` + +**Non-synchronized states (LISTEN, SYN-SENT, SYN-RECEIVED):** +Unacceptable ACK -> send RST (same format rules as CLOSED). + +**Synchronized states (ESTABLISHED through TIME-WAIT):** +- Out-of-window segment: send ACK (not RST), stay in same state +- Security mismatch: send RST, close connection + +### RST Processing + +**Validation:** +- SYN-SENT: RST valid only if ACK field acknowledges our SYN; if no ACK present, drop RST +- All other states: RST valid if SEQ is in the receive window + +**RFC 5961 mitigation (SHOULD implement for non-SYN-SENT states):** +1. RST with SEQ outside window: silently drop +2. RST with SEQ == RCV.NXT: reset connection per state rules +3. RST with SEQ in window but != RCV.NXT: send challenge ACK, drop segment + +**State-specific RST actions:** +- LISTEN: ignore RST +- SYN-RECEIVED from passive OPEN: return to LISTEN +- SYN-RECEIVED from active OPEN: signal "connection refused", delete TCB -> CLOSED +- ESTABLISHED, FIN-WAIT-1/2, CLOSE-WAIT: signal "connection reset", delete TCB -> CLOSED +- CLOSING, LAST-ACK, TIME-WAIT: delete TCB -> CLOSED + +RST SHOULD be allowed to contain data (SHLD-2). + +--- + +## 9. Connection Closing + +### Normal Close (FIN Handshake) + +``` +A (ESTABLISHED) B (ESTABLISHED) + FIN-WAIT-1 --> --> CLOSE-WAIT + FIN-WAIT-2 <-- <-- CLOSE-WAIT + TIME-WAIT <-- <-- LAST-ACK + TIME-WAIT --> --> CLOSED + (2*MSL) -> CLOSED +``` + +### Simultaneous Close + +Both send FIN before receiving peer's FIN. +Both transition: ESTABLISHED -> FIN-WAIT-1 -> CLOSING -> TIME-WAIT -> CLOSED + +### Half-Closed Connections + +After sending FIN, a side can still receive data. After receiving FIN, a side can still send data (CLOSE-WAIT state). Connection is fully closed only when both sides have sent and acknowledged FINs. + +**Half-duplex close (MAY-1):** Implementation MAY prevent reading after CLOSE. If so, and data is pending or arrives, SHOULD send RST (SHLD-3). + +### Requirements + +- Application MUST be informed whether close was normal (FIN) or abort (RST) (MUST-12) +- TIME-WAIT MUST linger for 2*MSL (MUST-13) +- FIN implies PUSH for any buffered data + +--- + +## 10. Segmentation + +### Maximum Segment Size (MSS) + +MUST implement send and receive of MSS option (MUST-14). + +**MSS Option:** Only in SYN segments (MUST-65). 4 bytes: Kind=2, Length=4, MSS value (16-bit). + +**Defaults when no MSS option received:** +- IPv4: 536 (576 - 40) (MUST-15) +- IPv6: 1220 (1280 - 60) (MUST-15) + +**Effective send MSS calculation (MUST-16):** +``` +Eff.snd.MSS = min(SendMSS + 20, MMS_S) - TCPhdrsize - IPoptionsize +``` +- SendMSS = received MSS value (or default) +- MMS_S = max transport-layer message size from IP layer +- TCPhdrsize = TCP header including options (>= 20) +- IPoptionsize = IP options/extension headers size + +**MSS value to send (MUST-67):** +``` +MSS_to_send <= MMS_R - 20 +``` +Where MMS_R is max receivable transport message (from IP layer). MAY always send MSS option (MAY-3). + +SHOULD send MSS option in SYN when receive MSS differs from default (SHLD-5). +When interface has variable MTU, SHOULD use the smallest effective MTU for MSS calculation (SHLD-6). + +### Path MTU Discovery + +Strongly recommended (PMTUD per RFC 1191/8201, PLPMTUD per RFC 4821). Not required for basic compliance but essential for performance. + +### IPv6 Jumbograms + +To support TCP over IPv6 Jumbograms (RFC 2675), implementations need to send TCP segments larger than the 64 KB limit the MSS Option can convey. An MSS value of 65,535 bytes is treated as infinity, and Path MTU Discovery is used to determine the actual MSS. Support for Jumbograms is not required by IPv6 Node Requirements (RFC 8504) unless attached to links with MTU > 65,575. + +### Nagle Algorithm + +SHOULD implement (SHLD-7). Application MUST be able to disable it (MUST-17, i.e., TCP_NODELAY). + +**Rule:** If unacknowledged data exists (SND.NXT > SND.UNA), buffer all new data until: +- Outstanding data is acknowledged, OR +- A full-sized segment (Eff.snd.MSS) can be sent + +**Nagle Modification (Minshall):** A common modification improves performance for request-response protocols where the combination of Nagle + delayed ACKs causes poor latency. Instead of checking `SND.NXT > SND.UNA` (is there unacknowledged data?), check whether the last transmission was less than a full segment. This is implemented in some OSes and does not impact interoperability. Not yet part of the standard, but implementers may find it beneficial. + +--- + +## 11. Data Communication + +### Retransmission Timeout (RTO) + +MUST compute RTO per RFC 6298 (MUST-18), including Karn's algorithm: +- Don't use RTT samples from retransmitted segments +- Use exponential backoff on retransmission + +**RFC 6298 algorithm summary:** +``` +On first RTT measurement R: + SRTT = R + RTTVAR = R/2 + RTO = SRTT + max(G, 4 * RTTVAR) where G = clock granularity + +On subsequent measurements R': + RTTVAR = (1 - beta) * RTTVAR + beta * |SRTT - R'| (beta = 1/4) + SRTT = (1 - alpha) * SRTT + alpha * R' (alpha = 1/8) + RTO = SRTT + max(G, 4 * RTTVAR) + +Constraints: + RTO >= 1 second (initial RTO = 1 second) + On retransmit: RTO = RTO * 2 (exponential backoff) + After successful ACK of new data: restore RTO from SRTT/RTTVAR +``` + +If a retransmitted packet is identical to the original, the same IPv4 Identification field MAY be reused (MAY-4), though this field is only meaningful for fragmented datagrams and TCP should not rely on it. + +### Congestion Control + +MUST implement (MUST-19): +- **Slow start**: cwnd starts at IW (initial window), increases by 1 MSS per ACK during slow start +- **Congestion avoidance**: linear increase after cwnd >= ssthresh +- **Exponential backoff**: RTO doubles on each retransmit timeout + +An endpoint MAY implement alternative conformant congestion control algorithms (MAY-18), provided they comply with RFC 2914, RFC 5033, and RFC 8961. + +SHOULD implement ECN (SHLD-8, RFC 3168). + +Reference: RFC 5681 (slow start, congestion avoidance, fast retransmit, fast recovery). + +### Connection Failures + +Two thresholds R1 and R2 (MUST-20): +- **R1** (SHOULD >= 3 retransmissions, SHLD-10): trigger negative advice to IP layer +- **R2** (SHOULD >= 100 seconds, SHLD-11): close the connection +- Application MUST be able to set R2 (MUST-21) +- SHOULD inform application between R1 and R2 (SHLD-9) +- SYN retransmissions MUST use the same R1/R2 mechanism as data retransmissions (MUST-22) +- SYN retransmissions: R2 MUST be >= 3 minutes (MUST-23) + +### Keep-Alives + +MAY implement (MAY-5). If implemented: +- Application MUST be able to enable/disable per connection (MUST-24) +- MUST default to off (MUST-25) +- MUST only send when no sent data is outstanding AND no data/ACK received for configured interval (MUST-26) +- Interval MUST be configurable (MUST-27), default >= 2 hours (MUST-28) +- MUST NOT treat single probe failure as dead connection (MUST-29) +- Keep-alive probe: `SEG.SEQ = SND.NXT - 1`, no data (SHLD-12) or 1 garbage octet (MAY-6) + +--- + +## 12. Urgent Mechanism + +New applications SHOULD NOT use it (SHLD-13), but MUST still support it (MUST-30). + +- Urgent pointer = offset from SEQ to octet AFTER urgent data (MUST-62) +- MUST support arbitrary length urgent data (MUST-31) +- MUST inform application asynchronously when urgent pointer arrives/advances (MUST-32) +- MUST let application query how much urgent data remains (MUST-33) +- When RCV.UP > data consumed: user in "urgent mode" +- When data consumed catches up to RCV.UP: user leaves "urgent mode" + +--- + +## 13. Window Management + +### Window Update Rule + +Applies when `SND.UNA <= SEG.ACK <= SND.NXT` (note: inclusive of `==` on left, so duplicate ACKs can carry window updates). + +Update send window when (prevents stale window info): +``` +SND.WL1 < SEG.SEQ OR (SND.WL1 == SEG.SEQ AND SND.WL2 <= SEG.ACK) +``` +Then set: +``` +SND.WND = SEG.WND +SND.WL1 = SEG.SEQ +SND.WL2 = SEG.ACK +``` + +### Receiver SHOULD NOT shrink window (SHLD-14) + +Sender MUST be robust against window shrinking (MUST-34). If usable window goes negative: +- SHOULD NOT send new data (SHLD-15) +- SHOULD retransmit old unacked data in [SND.UNA, SND.UNA+SND.WND] (SHLD-16) +- MAY retransmit old data beyond SND.UNA+SND.WND (MAY-7) +- SHOULD NOT time out the connection if data beyond the right window edge is not acknowledged (SHLD-17) +- If window shrinks to zero: MUST probe (MUST-35) + +### Zero-Window Probing + +MUST support (MUST-36). Sender periodically sends 1 octet to probe a zero window. +- First probe SHOULD occur after RTO (SHLD-29) +- SHOULD exponentially backoff probe interval (SHLD-30) +- MUST keep connection open as long as receiver ACKs probes (MUST-37) +- A receiver MAY keep its offered receive window closed indefinitely (MAY-8) + +### Silly Window Syndrome (SWS) Avoidance + +MUST include SWS avoidance in sender (MUST-38) and receiver (MUST-39). + +**Sender's algorithm - when to send:** + +Usable window: `U = SND.UNA + SND.WND - SND.NXT` + +Send if any of: +1. Can send full-sized segment: `min(D, U) >= Eff.snd.MSS` +2. Data is pushed, all queued data fits, and (Nagle allows): `PUSHed AND D <= U [AND SND.NXT == SND.UNA]` +3. At least half max window: `min(D, U) >= 0.5 * Max(SND.WND) [AND SND.NXT == SND.UNA]` +4. Override timeout fires (0.1 - 1.0 seconds) + +**Receiver's algorithm - when to update window:** + +Keep `RCV.NXT + RCV.WND` fixed until: +``` +RCV.BUFF - RCV.USER - RCV.WND >= min(0.5 * RCV.BUFF, Eff.snd.MSS) +``` +Then set `RCV.WND = RCV.BUFF - RCV.USER`. + +### Delayed ACKs + +SHOULD implement delayed ACKs (SHLD-18): +- Delay MUST be < 500ms (MUST-40) +- SHOULD ACK at least every 2nd full-sized segment or 2*RMSS bytes (SHLD-19) +- Immediately ACK out-of-order segments and segments filling gaps + +--- + +## 14. User/TCP Interface + +These are the logical operations the TCP stack must provide. + +### OPEN + +`OPEN(local_port, remote_socket, active/passive, [timeout], [dscp], [local_ip]) -> connection_name` + +- **Passive:** Enter LISTEN state. Each passive OPEN creates new connection record (MUST NOT affect existing, MUST-41). +- **Active:** Send SYN, enter SYN-SENT. Remote socket must be specified. +- MUST support concurrent LISTEN while SYN-SENT/SYN-RECEIVED exists on same port (MUST-42) +- MUST support local IP address parameter (MUST-43) +- On multihomed active open without specified local IP: MUST ask IP layer for source address before sending SYN (MUST-44) +- MUST use same local address for all segments on a connection (MUST-45) +- MUST reject OPEN to broadcast/multicast address (MUST-46) + +**PUSH flags:** A TCP endpoint MAY implement PUSH flags on SEND calls (MAY-15). If PUSH flags are not implemented, then: (1) the sender MUST NOT buffer data indefinitely (MUST-60), and (2) MUST set PSH on the last buffered segment (MUST-61). When an application issues a series of SENDs without setting the PUSH flag, TCP MAY aggregate the data internally without sending it (MAY-16). A TCP receiver MAY pass a received PSH bit to the application layer (MAY-17). + +### SEND + +`SEND(connection, buffer, byte_count, urgent_flag, [push_flag], [timeout])` + +- If PUSH supported: PSH causes prompt transmission +- SHOULD collapse successive PSH bits to send the largest possible segment (SHLD-27) +- SHOULD send maximum-sized segments (SHLD-28) + +### RECEIVE + +`RECEIVE(connection, buffer, byte_count) -> byte_count, urgent_flag, [push_flag]` + +- Deliver data from receive buffer. Returns PUSH/URGENT status. + +### CLOSE + +`CLOSE(connection)` + +- Graceful close. Queue remaining data, send FIN. +- CLOSE implies PUSH for buffered data. +- User should continue to RECEIVE after CLOSE (remote may still be sending). + +### ABORT + +`ABORT(connection)` + +- Immediate teardown. Send RST, flush queues, delete TCB. + +### STATUS + +`STATUS(connection) -> state_info` + +- Returns connection state, window sizes, addresses, etc. + +### FLUSH + +`FLUSH(connection)` + +- Empty the TCP send queue of data that is still to the right of the current send window (MAY-14). Flushes as much queued send data as possible without losing sequence number synchronization. + +### Asynchronous Reports + +MUST report soft errors to application (MUST-47): +- ICMP errors, excessive retransmissions, urgent pointer advance +- Application SHOULD be able to disable reports (SHLD-20) + +### Differentiated Services + +- Application MUST be able to specify DSCP for outgoing segments (MUST-48) +- SHOULD be changeable during connection (SHLD-21) +- SHOULD pass to IP unchanged (SHLD-22) +- Application generally SHOULD NOT change DSCP during a connection (SHLD-23) +- MAY pass most recently received Diffserv field up to the application (MAY-9) + +### 14.1 Per-State Processing of User Calls + +The following details how each user call is processed depending on the current connection state. + +#### OPEN Call + +| State | Action | +|-------|--------| +| CLOSED | Create TCB. If passive: enter LISTEN, return. If active: select ISS, send ``, set SND.UNA=ISS, SND.NXT=ISS+1, enter SYN-SENT. Error if active and remote socket unspecified. | +| LISTEN | If active OPEN with remote socket specified: select ISS, send ``, set SND.UNA=ISS, SND.NXT=ISS+1, enter SYN-SENT. Data from SEND may be sent with SYN or queued. Error if remote unspecified. | +| All others | Return "error: connection already exists". | + +#### SEND Call + +| State | Action | +|-------|--------| +| CLOSED | Return "error: connection does not exist". | +| LISTEN | If remote socket specified: transition to active, select ISS, send SYN, enter SYN-SENT. Data may be sent with SYN or queued. Error if remote unspecified. | +| SYN-SENT, SYN-RECEIVED | Queue data for transmission after entering ESTABLISHED. | +| ESTABLISHED, CLOSE-WAIT | Segmentize buffer and send with piggybacked ACK (ACK=RCV.NXT). If URGENT flag set: SND.UP <- SND.NXT, set urgent pointer in outgoing segments. | +| FIN-WAIT-1/2, CLOSING, LAST-ACK, TIME-WAIT | Return "error: connection closing". | + +#### RECEIVE Call + +| State | Action | +|-------|--------| +| CLOSED | Return "error: connection does not exist". | +| LISTEN, SYN-SENT, SYN-RECEIVED | Queue for processing after entering ESTABLISHED. | +| ESTABLISHED, FIN-WAIT-1, FIN-WAIT-2 | If insufficient data queued: queue the request. Otherwise: reassemble into receive buffer, return to user. Mark PUSH if seen. Notify user of urgent data if RCV.UP is ahead of consumed data. | +| CLOSE-WAIT | Serve from already-received data only. If no data awaiting delivery: return "error: connection closing". | +| CLOSING, LAST-ACK, TIME-WAIT | Return "error: connection closing". | + +#### CLOSE Call + +| State | Action | +|-------|--------| +| CLOSED | Return "error: connection does not exist". | +| LISTEN | Return outstanding RECEIVEs with "error: closing". Delete TCB, enter CLOSED. | +| SYN-SENT | Delete TCB. Return "error: closing" to queued SENDs/RECEIVEs. | +| SYN-RECEIVED | If no pending data: send FIN, enter FIN-WAIT-1. Otherwise: queue for processing after ESTABLISHED. | +| ESTABLISHED | Queue until all preceding SENDs are segmentized, then send FIN. Enter FIN-WAIT-1. | +| FIN-WAIT-1, FIN-WAIT-2 | Return "error: connection closing" (strictly an error; a second FIN MUST NOT be sent). | +| CLOSE-WAIT | Queue until all preceding SENDs are segmentized, then send FIN. Enter LAST-ACK. | +| CLOSING, LAST-ACK, TIME-WAIT | Return "error: connection closing". | + +#### ABORT Call + +| State | Action | +|-------|--------| +| CLOSED | Return "error: connection does not exist". | +| LISTEN | Return outstanding RECEIVEs with "connection reset". Delete TCB, enter CLOSED. | +| SYN-SENT | Notify queued SENDs/RECEIVEs "connection reset". Delete TCB, enter CLOSED. | +| SYN-RECEIVED, ESTABLISHED, FIN-WAIT-1, FIN-WAIT-2, CLOSE-WAIT | Send ``. Notify all queued SENDs/RECEIVEs "connection reset". Flush all segment queues. Delete TCB, enter CLOSED. | +| CLOSING, LAST-ACK, TIME-WAIT | Delete TCB, enter CLOSED. | + +--- + +## 15. TCP/Lower-Level Interface + +### IP Interface + +- TTL MUST be configurable (MUST-49) +- MUST ignore unknown IP options (MUST-50) +- MAY support IP Timestamp option (MAY-10) +- MAY support IP Record Route option (MAY-11) + +### Source Routing + +- Application MUST be able to specify source route on active open (MUST-51, takes precedence MUST-52) +- On passive open with source route: MUST save and use return route (MUST-53) +- Later source route SHOULD override (SHLD-24) + +### ICMP Processing + +MUST act on ICMP errors directed to connection (MUST-54). + +| ICMP Type | Action | +|-----------|--------| +| Source Quench | MUST silently discard (MUST-55) | +| Dest Unreachable codes 0,1,5 (IPv4) / 0,3 (IPv6) | Soft error. MUST NOT abort (MUST-56). SHOULD inform app (SHLD-25). | +| Time Exceeded, Parameter Problem | Soft error. Same as above. | +| Dest Unreachable codes 2-4 (IPv4) | Hard error. SHOULD abort connection (SHLD-26). | + +### Address Validation + +- MUST ignore/reject segments with invalid source address (MUST-63) — applies to all incoming segments, not just SYNs +- MUST silently discard SYN to broadcast/multicast address (MUST-57) + +--- + +## 16. Segment Processing (Event Machine) + +This is the core state machine. Processing order matters. + +### 16.1 CLOSED State (no TCB) + +Any non-RST segment: send RST. +- ACK off: `` +- ACK on: `` +- RST segment: discard. + +### 16.2 LISTEN State + +Process in order: + +1. **RST:** Ignore. Return. +2. **ACK:** Bad. Send ``. Return. +3. **SYN:** Check security. If listen was not fully specified (wildcard remote), fill in unspecified fields now. Set RCV.NXT = SEG.SEQ+1, IRS = SEG.SEQ. Select ISS. Send ``. Set SND.NXT = ISS+1, SND.UNA = ISS. Enter SYN-RECEIVED. Queue any data/controls for later processing. +4. **Other:** Drop segment. + +### 16.3 SYN-SENT State + +Process in order: + +1. **Check ACK:** If ACK set: + - If `SEG.ACK <= ISS` or `SEG.ACK > SND.NXT`: send RST `` (unless RST set), discard, return. + - Acceptable if `SND.UNA < SEG.ACK <= SND.NXT`. + +2. **Check RST:** If RST set: + - RFC 5961: SHOULD check SEG.SEQ == RCV.NXT first + - If ACK was acceptable: signal "connection reset", delete TCB -> CLOSED + - If no ACK: drop segment, return + +3. **Check security:** If security/compartment mismatch: + - ACK present: send ``, discard, return + - No ACK: send ``, discard, return + +4. **Check SYN:** If SYN set: + - Set RCV.NXT = SEG.SEQ+1, IRS = SEG.SEQ + - Advance SND.UNA to SEG.ACK (if ACK present), remove acknowledged segments from retransmit queue + - If `SND.UNA > ISS` (our SYN has been ACKed): + - Enter ESTABLISHED + - Send `` + - Set SND.WND = SEG.WND, SND.WL1 = SEG.SEQ, SND.WL2 = SEG.ACK + - Note: The RFC says to continue at step 6 (URG check), skipping step 5 (ACK check). Implementations MUST explicitly set SND.WND/WL1/WL2 here (as the simultaneous open case does) since step 5's window update logic will not run for this path. + - Continue to step 6 (URG check) of "Other States" if segment has data/controls; otherwise return + - Else (simultaneous open): + - Enter SYN-RECEIVED + - Send `` + - Set SND.WND = SEG.WND, SND.WL1 = SEG.SEQ, SND.WL2 = SEG.ACK + - Queue any data for processing after ESTABLISHED + - Note: Data on SYN segments is legal (see TCP Fast Open, RFC 7413) + +5. **Neither SYN nor RST:** Drop segment. + +### 16.4 All Other States (SYN-RECEIVED through TIME-WAIT) + +Process in order: + +#### Step 1: Check Sequence Number + +Apply segment acceptability test (see Section 4 table above). + +- Unacceptable and RST set: drop segment, return +- Unacceptable and RST not set: send ``, drop, return +- If RCV.WND is zero: still MUST accept valid ACKs, URGs, and RSTs +- If acceptable, trim segment to fit window (including trimming SYN and FIN if they fall outside the window). Hold out-of-order segments (SHLD-31). +- In TIME-WAIT: an improved algorithm using Timestamps may override normal sequence checking for incoming SYNs (see RFC 6191) +- MUST aggregate ACKs when processing queued segments (MUST-58, MUST-59) + +#### Step 2: Check RST + +**With RFC 5961 mitigation:** +1. SEQ outside window: silently drop +2. SEQ == RCV.NXT: reset per state rules below +3. SEQ in window but != RCV.NXT: MUST send challenge ACK ``, MUST drop segment + +Note: RFC 5961 and Errata ID 4772 contain additional considerations for ACK throttling to prevent amplification. + +**State-specific reset actions:** +- **SYN-RECEIVED:** If from passive OPEN -> flush retransmission queue, return to LISTEN. If from active OPEN -> signal "refused", flush retransmission queue, delete TCB -> CLOSED. +- **ESTABLISHED, FIN-WAIT-1/2, CLOSE-WAIT:** Return "reset" responses to outstanding RECEIVEs/SENDs, flush all segment queues, signal "connection reset" to user, delete TCB -> CLOSED. +- **CLOSING, LAST-ACK, TIME-WAIT:** Delete TCB -> CLOSED. + +#### Step 3: Check Security + +If security/compartment mismatch: send RST, CLOSED. (MLS systems only; ignore for non-MLS.) + +#### Step 4: Check SYN + +- **SYN-RECEIVED (passive):** Return to LISTEN. +- **SYN-RECEIVED (active, i.e., simultaneous open):** Handle per synchronized states below. +- **All synchronized states (ESTABLISHED through TIME-WAIT):** RFC 5961: MUST send challenge ACK ``, MUST drop segment. (Without RFC 5961: if SYN in window, send RST, close connection.) +- Note: ACK throttling considerations from RFC 5961 and Errata ID 4772 apply here as well. + +#### Step 5: Check ACK + +If ACK bit off: drop segment, return. + +If ACK bit on: + +**RFC 5961 ACK validation (MAY-12):** Accept only if `(SND.UNA - MAX.SND.WND) <= SEG.ACK <= SND.NXT`. Otherwise discard + send ACK. + +**SYN-RECEIVED:** +- `SND.UNA < SEG.ACK <= SND.NXT`: enter ESTABLISHED. Set SND.WND, SND.WL1, SND.WL2. **Continue processing** through subsequent steps (URG, data, FIN) in ESTABLISHED state. +- Otherwise: send RST ``. + +**ESTABLISHED:** +- `SND.UNA < SEG.ACK <= SND.NXT` (new ACK): advance SND.UNA, remove acked segments from retransmit queue, return positive acknowledgments for completed SEND buffers. +- `SEG.ACK <= SND.UNA` (duplicate ACK): can be ignored for advancement purposes, but still check window update below. +- `SEG.ACK > SND.NXT` (future ACK): send ACK, drop, return. +- **Window update** (applies when `SND.UNA <= SEG.ACK <= SND.NXT`, note `<=` on left — includes duplicate ACKs): If `SND.WL1 < SEG.SEQ` or (`SND.WL1 == SEG.SEQ` and `SND.WL2 <= SEG.ACK`): update SND.WND, SND.WL1, SND.WL2. + +**FIN-WAIT-1:** Same as ESTABLISHED, plus: if our FIN is now ACKed -> FIN-WAIT-2, continue processing in that state. + +**FIN-WAIT-2:** Same as ESTABLISHED, plus: if retransmit queue empty, signal app "close ok". + +**CLOSE-WAIT:** Same as ESTABLISHED. + +**CLOSING:** Same as ESTABLISHED, plus: if our FIN is ACKed -> TIME-WAIT; otherwise, ignore the segment. + +**LAST-ACK:** If our FIN is ACKed: delete TCB -> CLOSED. Return. + +**TIME-WAIT:** Only retransmitted FIN can arrive. ACK it, restart 2*MSL timer. + +#### Step 6: Check URG + +**ESTABLISHED, FIN-WAIT-1, FIN-WAIT-2:** +- If URG set: `RCV.UP = max(RCV.UP, SEG.UP)`. Signal user if RCV.UP is ahead of consumed data (don't re-signal if already in urgent mode for same sequence). + +**CLOSE-WAIT, CLOSING, LAST-ACK, TIME-WAIT:** Ignore URG (FIN already received). + +#### Step 7: Process Segment Data + +**ESTABLISHED, FIN-WAIT-1, FIN-WAIT-2:** +- Deliver data to receive buffer +- If segment carries PSH flag and empties, inform user that PUSH has been received when buffer is returned +- Advance RCV.NXT over accepted data +- Adjust RCV.WND (total of RCV.NXT+RCV.WND should not decrease) +- Send ACK: `` (piggyback if possible) +- MAY send ACK for valid out-of-order segments (MAY-13) + +**CLOSE-WAIT, CLOSING, LAST-ACK, TIME-WAIT:** Ignore data (FIN already received). + +#### Step 8: Check FIN + +Do NOT process FIN in CLOSED, LISTEN, or SYN-SENT (SEQ cannot be validated). + +If FIN bit set: +- Signal user "connection closing" +- Return pending RECEIVEs with "closing" message +- Advance RCV.NXT over the FIN +- Send ACK for the FIN +- FIN implies PUSH for any undelivered data + +**State transitions on FIN:** +- **SYN-RECEIVED, ESTABLISHED:** -> CLOSE-WAIT +- **FIN-WAIT-1:** If our FIN also ACKed in this segment -> TIME-WAIT (start 2*MSL timer, turn off other timers). Else -> CLOSING. +- **FIN-WAIT-2:** -> TIME-WAIT (start 2*MSL timer, turn off other timers) +- **CLOSE-WAIT, CLOSING, LAST-ACK:** Remain in same state. +- **TIME-WAIT:** Remain. Restart 2*MSL timer. + +--- + +## 17. Timeouts + +### User Timeout + +Any state: flush all queues, signal "connection aborted due to user timeout" (both general and for any outstanding SENDs/RECEIVEs), delete TCB -> CLOSED. + +### Retransmission Timeout + +Any state: retransmit segment at front of retransmission queue, reinitialize RTO (with backoff). + +### TIME-WAIT Timeout + +2*MSL expires: delete TCB -> CLOSED. + +--- + +## 18. Implementation Modules Breakdown + +For a clean implementation, the following modules/components are suggested: + +### Module 1: Wire Format (Parsing & Serialization) +- TCP header read/write (20-60 bytes) +- Option parsing (EOL, NOP, MSS, + extensible for WS, SACK, Timestamps) +- Checksum computation (with IPv4/IPv6 pseudo-header) +- Segment length calculation (data + SYN + FIN) + +### Module 2: Sequence Number Arithmetic +- Wrapping u32 comparison operators (`<`, `<=`, `==`, `>=`, `>`) +- Wrapping addition/subtraction +- Window containment tests + +### Module 3: Transmission Control Block (TCB) +- All per-connection state variables +- Connection identification (4-tuple: local_ip, local_port, remote_ip, remote_port) +- State enum +- Send/receive buffers +- Retransmission queue + +### Module 4: ISN Generator +- Clock-driven 4us counter +- PRF: HMAC or SipHash over (local_ip, local_port, remote_ip, remote_port, secret) +- ISN = clock + PRF output + +### Module 5: Connection State Machine +- State transition logic (Section 16 above) +- OPEN/SEND/RECEIVE/CLOSE/ABORT/STATUS call handling +- Segment arrival processing (the 8-step processing pipeline) + +### Module 6: Timer Management +- Retransmission timer (per RFC 6298) +- TIME-WAIT timer (2*MSL = 4 minutes) +- User timeout +- Zero-window probe timer +- Keep-alive timer +- Delayed ACK timer (< 500ms) + +### Module 7: Send Path +- Segmentation (split app data into MSS-sized segments) +- Nagle algorithm (coalesce small writes) +- SWS avoidance (sender side) +- Send window management +- PSH flag logic +- Urgent pointer management +- Retransmission queue management + +### Module 8: Receive Path +- Segment acceptability testing +- In-order delivery to application +- Out-of-order segment queuing (SHLD-31) +- Reassembly +- Window advertisement (SWS avoidance, receiver side) +- Delayed ACK logic +- Urgent data notification + +### Module 9: Congestion Control +- Slow start / congestion avoidance (RFC 5681) +- Fast retransmit / fast recovery +- RTO exponential backoff (RFC 6298) +- ECN support (RFC 3168, SHLD-8) +- cwnd, ssthresh management + +### Module 10: Connection Table +- TCB lookup by 4-tuple +- LISTEN socket management (wildcard matching) +- TIME-WAIT bucket management +- Port allocation + +### Module 11: ICMP Integration +- Map ICMP errors to connections +- Soft vs hard error classification +- PMTUD integration + +### Module 12: Application Interface (Socket API) +- OPEN (listen/connect) +- SEND (write) +- RECEIVE (read) +- CLOSE (shutdown/close) +- ABORT (RST) +- STATUS +- Socket options: TCP_NODELAY, SO_KEEPALIVE, timeouts, MSS, DSCP + +--- + +## 19. Compliance Requirement Summary (Key MUST Items) + +| ID | Requirement | +|----|-------------| +| MUST-1 | Window treated as unsigned | +| MUST-2 | Sender computes checksum | +| MUST-3 | Receiver verifies checksum | +| MUST-4 | Support mandatory options (EOL, NOP, MSS) | +| MUST-5 | Accept options in any segment | +| MUST-6 | Ignore unknown options | +| MUST-7 | Handle illegal option lengths | +| MUST-8 | Clock-driven ISN selection | +| MUST-9 | ISN PRF not externally computable | +| MUST-10 | Support simultaneous open | +| MUST-11 | Track passive vs active origin of SYN-RECEIVED | +| MUST-12 | Inform app of normal close vs abort | +| MUST-13 | TIME-WAIT = 2*MSL | +| MUST-14 | Implement MSS option send + receive | +| MUST-15 | Default send MSS: 536 (IPv4), 1220 (IPv6) | +| MUST-16 | Calculate effective send MSS | +| MUST-17 | Allow disabling Nagle per connection | +| MUST-18 | RFC 6298 RTO computation with Karn's algorithm | +| MUST-19 | Implement slow start, congestion avoidance, exponential backoff | +| MUST-20 | R1/R2 retransmission failure thresholds | +| MUST-21 | Application must be able to set R2 | +| MUST-22 | Same R1/R2 mechanism for SYN retransmissions | +| MUST-23 | SYN R2 >= 3 minutes | +| MUST-24..29 | Keep-alive rules (if implemented) | +| MUST-30 | Support urgent mechanism | +| MUST-34 | Robust against window shrinking | +| MUST-35..37 | Zero-window probing | +| MUST-38..39 | SWS avoidance (sender + receiver) | +| MUST-40 | Delayed ACK < 500ms | +| MUST-41 | Passive OPEN doesn't affect existing connections | +| MUST-42 | Concurrent LISTEN on same port | +| MUST-43 | Support local IP address parameter in OPEN | +| MUST-44 | Ask IP for source address for SYN if not specified | +| MUST-45 | Use same local address for all segments on a connection | +| MUST-46 | Reject OPEN to broadcast/multicast | +| MUST-47 | Error reporting to application | +| MUST-48 | Application can specify DSCP for outgoing segments | +| MUST-49 | Configurable TTL | +| MUST-50 | Ignore unknown IP options | +| MUST-51 | Application can specify source route on active open | +| MUST-52 | Source route takes precedence | +| MUST-53 | Save and use return route on passive open | +| MUST-54..57 | ICMP handling | +| MUST-58..59 | ACK aggregation | +| MUST-60..62 | PUSH and urgent pointer semantics | +| MUST-63 | Reject segments with invalid source address | +| MUST-64 | Process options regardless of word alignment | +| MUST-65 | MSS option only in SYN segments | +| MUST-66 | Process RST and URG even when RCV.WND is zero | +| MUST-67 | MSS value to send based on MMS_R | +| MUST-68 | All options except EOL/NOP must have length fields | +| MUST-69 | Padding after options must be zeros | + +### Key SHOULD Items + +| ID | Requirement | +|----|-------------| +| SHLD-1 | Use PRF-based ISN generation formula | +| SHLD-2 | RST may contain data | +| SHLD-3 | Send RST if data lost on half-duplex close | +| SHLD-4 | Use Timestamps to reduce TIME-WAIT | +| SHLD-5 | Send MSS option when receive MSS differs from default | +| SHLD-6 | Use smallest effective MTU for variable-MTU interfaces | +| SHLD-7 | Implement Nagle algorithm | +| SHLD-8 | Implement ECN (RFC 3168) | +| SHLD-9 | Inform application between R1 and R2 | +| SHLD-10 | R1 >= 3 retransmissions | +| SHLD-11 | R2 >= 100 seconds | +| SHLD-12 | Keep-alive probe with no data | +| SHLD-13 | New applications SHOULD NOT use urgent mechanism | +| SHLD-14 | Receiver SHOULD NOT shrink window | +| SHLD-15..17 | Window shrinking sender behavior | +| SHLD-18 | Implement delayed ACKs | +| SHLD-19 | ACK at least every 2nd full-sized segment | +| SHLD-20 | Application can disable error reports | +| SHLD-21..23 | Diffserv field behavior | +| SHLD-24 | Later source route overrides earlier | +| SHLD-25..26 | ICMP soft/hard error handling | +| SHLD-27 | Collapse successive PSH bits | +| SHLD-28 | Send maximum-sized segments | +| SHLD-29..30 | Zero-window probe timing | +| SHLD-31 | Hold out-of-order segments for later processing | + +### Key MAY Items + +| ID | Requirement | +|----|-------------| +| MAY-1 | Half-duplex close | +| MAY-2 | Accept SYN from TIME-WAIT | +| MAY-3 | Always send MSS option | +| MAY-4 | Retransmit with same IPv4 Identification | +| MAY-5 | Implement keep-alives | +| MAY-6 | Keep-alive with garbage octet | +| MAY-7 | Retransmit old data beyond SND.UNA+SND.WND | +| MAY-8 | Receiver keeps window closed indefinitely | +| MAY-9 | Pass received Diffserv field to application | +| MAY-10 | Support IP Timestamp option | +| MAY-11 | Support IP Record Route option | +| MAY-12 | RFC 5961 ACK validation (blind data injection protection) | +| MAY-13 | Send ACK for valid out-of-order segments | +| MAY-14 | FLUSH call implementation | +| MAY-15 | Implement PUSH flags on SEND calls | +| MAY-16 | Aggregate un-pushed data internally | +| MAY-17 | Pass received PSH to application | +| MAY-18 | Implement alternative conformant congestion control | + +### Recommendation + +| ID | Requirement | +|----|-------------| +| REC-1 | Use 32-bit fields for send/receive window sizes internally | + +--- + +## 20. Constants and Defaults + +| Constant | Value | Notes | +|----------|-------|-------| +| MSL | 2 minutes | Maximum Segment Lifetime | +| TIME-WAIT duration | 4 minutes | 2 * MSL | +| Default IPv4 MSS | 536 | 576 - 40 | +| Default IPv6 MSS | 1220 | 1280 - 60 | +| Min TCP header | 20 bytes | 5 * 32-bit words | +| Max TCP header | 60 bytes | 15 * 32-bit words (40 bytes of options) | +| Initial RTO | 1 second | Per RFC 6298 | +| Min RTO | 1 second | Per RFC 6298 | +| Delayed ACK max | < 500ms | MUST-40 (500ms itself is forbidden) | +| Keep-alive default interval | >= 2 hours | MUST-28 | +| SYN retry timeout (R2) | >= 3 minutes | MUST-23 | +| R1 threshold | >= 3 retransmissions | SHLD-10 | +| R2 threshold | >= 100 seconds | SHLD-11 | +| SWS fraction (Fs) | 1/2 | Recommended | +| SWS receiver fraction (Fr) | 1/2 | Recommended | +| SWS override timeout | 0.1 - 1.0 seconds | Recommended | +| Protocol number | 6 | For pseudo-header | + +--- + +## 21. Implementation Notes (from RFC Appendices) + +### IP Security Compartment and Precedence + +References to IP "security/compartment" in segment processing (e.g., Section 16 Steps 3-4) are relevant for Multi-Level Secure (MLS) systems but can be ignored for non-MLS implementations. The old IPv4 TOS precedence processing from RFC 793 is obsolete — replaced by Differentiated Services. TCP implementations SHOULD NOT include the old TOS precedence logic; Diffserv is asymmetric per-direction, and the old symmetric precedence matching was deprecated by RFC 2873. + +### Sequence Number Validation Edge Cases + +There are edge cases where TCP sequence number validation rules can prevent ACK fields from being processed, causing connection issues in scenarios including: simultaneous open, self-connects, simultaneous close, and simultaneous window probes. In Internet usage these rarely occur, and common OSes include varying mitigations. Implementers should be aware of these edge cases (described in draft-gont-tcpm-tcp-seq-validation). + +### Low Watermark Settings + +Some OS kernel TCP implementations include socket options for controlling buffer thresholds: +- **SO_SNDLOWAT**: Bytes in buffer before the socket layer passes data to TCP +- **SO_RCVLOWAT**: Bytes in buffer before data is passed to the application +- **TCP_NOTSENT_LOWAT**: Controls amount of unsent bytes in the write queue, useful for applications multiplexing multiple streams (e.g., mix of interactive and bulk data) to limit buffered latency diff --git a/dist/docs/rfc768-udp.txt b/dist/docs/rfc768-udp.txt new file mode 100644 index 0000000..bd73b9d --- /dev/null +++ b/dist/docs/rfc768-udp.txt @@ -0,0 +1,173 @@ + + +RFC 768 J. Postel + ISI + 28 August 1980 + + + + User Datagram Protocol + ---------------------- + +Introduction +------------ + +This User Datagram Protocol (UDP) is defined to make available a +datagram mode of packet-switched computer communication in the +environment of an interconnected set of computer networks. This +protocol assumes that the Internet Protocol (IP) [1] is used as the +underlying protocol. + +This protocol provides a procedure for application programs to send +messages to other programs with a minimum of protocol mechanism. The +protocol is transaction oriented, and delivery and duplicate protection +are not guaranteed. Applications requiring ordered reliable delivery of +streams of data should use the Transmission Control Protocol (TCP) [2]. + +Format +------ + + + 0 7 8 15 16 23 24 31 + +--------+--------+--------+--------+ + | Source | Destination | + | Port | Port | + +--------+--------+--------+--------+ + | | | + | Length | Checksum | + +--------+--------+--------+--------+ + | + | data octets ... + +---------------- ... + + User Datagram Header Format + +Fields +------ + +Source Port is an optional field, when meaningful, it indicates the port +of the sending process, and may be assumed to be the port to which a +reply should be addressed in the absence of any other information. If +not used, a value of zero is inserted. + + + + + +Postel [page 1] + + + 28 Aug 1980 +User Datagram Protocol RFC 768 +Fields + + + +Destination Port has a meaning within the context of a particular +internet destination address. + +Length is the length in octets of this user datagram including this +header and the data. (This means the minimum value of the length is +eight.) + +Checksum is the 16-bit one's complement of the one's complement sum of a +pseudo header of information from the IP header, the UDP header, and the +data, padded with zero octets at the end (if necessary) to make a +multiple of two octets. + +The pseudo header conceptually prefixed to the UDP header contains the +source address, the destination address, the protocol, and the UDP +length. This information gives protection against misrouted datagrams. +This checksum procedure is the same as is used in TCP. + + 0 7 8 15 16 23 24 31 + +--------+--------+--------+--------+ + | source address | + +--------+--------+--------+--------+ + | destination address | + +--------+--------+--------+--------+ + | zero |protocol| UDP length | + +--------+--------+--------+--------+ + +If the computed checksum is zero, it is transmitted as all ones (the +equivalent in one's complement arithmetic). An all zero transmitted +checksum value means that the transmitter generated no checksum (for +debugging or for higher level protocols that don't care). + +User Interface +-------------- + +A user interface should allow + + the creation of new receive ports, + + receive operations on the receive ports that return the data octets + and an indication of source port and source address, + + and an operation that allows a datagram to be sent, specifying the + data, source and destination ports and addresses to be sent. + + + + + + +[page 2] Postel + + +28 Aug 1980 +RFC 768 User Datagram Protocol + IP Interface + + + +IP Interface +------------- + +The UDP module must be able to determine the source and destination +internet addresses and the protocol field from the internet header. One +possible UDP/IP interface would return the whole internet datagram +including all of the internet header in response to a receive operation. +Such an interface would also allow the UDP to pass a full internet +datagram complete with header to the IP to send. The IP would verify +certain fields for consistency and compute the internet header checksum. + +Protocol Application +-------------------- + +The major uses of this protocol is the Internet Name Server [3], and the +Trivial File Transfer [4]. + +Protocol Number +--------------- + +This is protocol 17 (21 octal) when used in the Internet Protocol. +Other protocol numbers are listed in [5]. + +References +---------- + +[1] Postel, J., "Internet Protocol," RFC 760, USC/Information + Sciences Institute, January 1980. + +[2] Postel, J., "Transmission Control Protocol," RFC 761, + USC/Information Sciences Institute, January 1980. + +[3] Postel, J., "Internet Name Server," USC/Information Sciences + Institute, IEN 116, August 1979. + +[4] Sollins, K., "The TFTP Protocol," Massachusetts Institute of + Technology, IEN 133, January 1980. + +[5] Postel, J., "Assigned Numbers," USC/Information Sciences + Institute, RFC 762, January 1980. + + + + + + + + + +Postel [page 3] diff --git a/dist/docs/rfc791-ipv4.txt b/dist/docs/rfc791-ipv4.txt new file mode 100644 index 0000000..7bd4fb4 --- /dev/null +++ b/dist/docs/rfc791-ipv4.txt @@ -0,0 +1,2886 @@ + + +RFC: 791 + + + + + + + + INTERNET PROTOCOL + + + DARPA INTERNET PROGRAM + + PROTOCOL SPECIFICATION + + + + September 1981 + + + + + + + + + + + + + + prepared for + + Defense Advanced Research Projects Agency + Information Processing Techniques Office + 1400 Wilson Boulevard + Arlington, Virginia 22209 + + + + + + + + by + + Information Sciences Institute + University of Southern California + 4676 Admiralty Way + Marina del Rey, California 90291 + + + +September 1981 + Internet Protocol + + + + TABLE OF CONTENTS + + PREFACE ........................................................ iii + +1. INTRODUCTION ..................................................... 1 + + 1.1 Motivation .................................................... 1 + 1.2 Scope ......................................................... 1 + 1.3 Interfaces .................................................... 1 + 1.4 Operation ..................................................... 2 + +2. OVERVIEW ......................................................... 5 + + 2.1 Relation to Other Protocols ................................... 9 + 2.2 Model of Operation ............................................ 5 + 2.3 Function Description .......................................... 7 + 2.4 Gateways ...................................................... 9 + +3. SPECIFICATION ................................................... 11 + + 3.1 Internet Header Format ....................................... 11 + 3.2 Discussion ................................................... 23 + 3.3 Interfaces ................................................... 31 + +APPENDIX A: Examples & Scenarios ................................... 34 +APPENDIX B: Data Transmission Order ................................ 39 + +GLOSSARY ............................................................ 41 + +REFERENCES .......................................................... 45 + + + + + + + + + + + + + + + + + + + + + + [Page i] + + + September 1981 +Internet Protocol + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[Page ii] + + +September 1981 + Internet Protocol + + + + PREFACE + + + +This document specifies the DoD Standard Internet Protocol. This +document is based on six earlier editions of the ARPA Internet Protocol +Specification, and the present text draws heavily from them. There have +been many contributors to this work both in terms of concepts and in +terms of text. This edition revises aspects of addressing, error +handling, option codes, and the security, precedence, compartments, and +handling restriction features of the internet protocol. + + Jon Postel + + Editor + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + [Page iii] + + + + September 1981 + + +RFC: 791 +Replaces: RFC 760 +IENs 128, 123, 111, +80, 54, 44, 41, 28, 26 + + INTERNET PROTOCOL + + DARPA INTERNET PROGRAM + PROTOCOL SPECIFICATION + + + + 1. INTRODUCTION + +1.1. Motivation + + The Internet Protocol is designed for use in interconnected systems of + packet-switched computer communication networks. Such a system has + been called a "catenet" [1]. The internet protocol provides for + transmitting blocks of data called datagrams from sources to + destinations, where sources and destinations are hosts identified by + fixed length addresses. The internet protocol also provides for + fragmentation and reassembly of long datagrams, if necessary, for + transmission through "small packet" networks. + +1.2. Scope + + The internet protocol is specifically limited in scope to provide the + functions necessary to deliver a package of bits (an internet + datagram) from a source to a destination over an interconnected system + of networks. There are no mechanisms to augment end-to-end data + reliability, flow control, sequencing, or other services commonly + found in host-to-host protocols. The internet protocol can capitalize + on the services of its supporting networks to provide various types + and qualities of service. + +1.3. Interfaces + + This protocol is called on by host-to-host protocols in an internet + environment. This protocol calls on local network protocols to carry + the internet datagram to the next gateway or destination host. + + For example, a TCP module would call on the internet module to take a + TCP segment (including the TCP header and user data) as the data + portion of an internet datagram. The TCP module would provide the + addresses and other parameters in the internet header to the internet + module as arguments of the call. The internet module would then + create an internet datagram and call on the local network interface to + transmit the internet datagram. + + In the ARPANET case, for example, the internet module would call on a + + + [Page 1] + + + September 1981 +Internet Protocol +Introduction + + + + local net module which would add the 1822 leader [2] to the internet + datagram creating an ARPANET message to transmit to the IMP. The + ARPANET address would be derived from the internet address by the + local network interface and would be the address of some host in the + ARPANET, that host might be a gateway to other networks. + +1.4. Operation + + The internet protocol implements two basic functions: addressing and + fragmentation. + + The internet modules use the addresses carried in the internet header + to transmit internet datagrams toward their destinations. The + selection of a path for transmission is called routing. + + The internet modules use fields in the internet header to fragment and + reassemble internet datagrams when necessary for transmission through + "small packet" networks. + + The model of operation is that an internet module resides in each host + engaged in internet communication and in each gateway that + interconnects networks. These modules share common rules for + interpreting address fields and for fragmenting and assembling + internet datagrams. In addition, these modules (especially in + gateways) have procedures for making routing decisions and other + functions. + + The internet protocol treats each internet datagram as an independent + entity unrelated to any other internet datagram. There are no + connections or logical circuits (virtual or otherwise). + + The internet protocol uses four key mechanisms in providing its + service: Type of Service, Time to Live, Options, and Header Checksum. + + The Type of Service is used to indicate the quality of the service + desired. The type of service is an abstract or generalized set of + parameters which characterize the service choices provided in the + networks that make up the internet. This type of service indication + is to be used by gateways to select the actual transmission parameters + for a particular network, the network to be used for the next hop, or + the next gateway when routing an internet datagram. + + The Time to Live is an indication of an upper bound on the lifetime of + an internet datagram. It is set by the sender of the datagram and + reduced at the points along the route where it is processed. If the + time to live reaches zero before the internet datagram reaches its + destination, the internet datagram is destroyed. The time to live can + be thought of as a self destruct time limit. + + +[Page 2] + + +September 1981 + Internet Protocol + Introduction + + + + The Options provide for control functions needed or useful in some + situations but unnecessary for the most common communications. The + options include provisions for timestamps, security, and special + routing. + + The Header Checksum provides a verification that the information used + in processing internet datagram has been transmitted correctly. The + data may contain errors. If the header checksum fails, the internet + datagram is discarded at once by the entity which detects the error. + + The internet protocol does not provide a reliable communication + facility. There are no acknowledgments either end-to-end or + hop-by-hop. There is no error control for data, only a header + checksum. There are no retransmissions. There is no flow control. + + Errors detected may be reported via the Internet Control Message + Protocol (ICMP) [3] which is implemented in the internet protocol + module. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + [Page 3] + + + September 1981 +Internet Protocol + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[Page 4] + + +September 1981 + Internet Protocol + + + + 2. OVERVIEW + +2.1. Relation to Other Protocols + + The following diagram illustrates the place of the internet protocol + in the protocol hierarchy: + + + +------+ +-----+ +-----+ +-----+ + |Telnet| | FTP | | TFTP| ... | ... | + +------+ +-----+ +-----+ +-----+ + | | | | + +-----+ +-----+ +-----+ + | TCP | | UDP | ... | ... | + +-----+ +-----+ +-----+ + | | | + +--------------------------+----+ + | Internet Protocol & ICMP | + +--------------------------+----+ + | + +---------------------------+ + | Local Network Protocol | + +---------------------------+ + + Protocol Relationships + + Figure 1. + + Internet protocol interfaces on one side to the higher level + host-to-host protocols and on the other side to the local network + protocol. In this context a "local network" may be a small network in + a building or a large network such as the ARPANET. + +2.2. Model of Operation + + The model of operation for transmitting a datagram from one + application program to another is illustrated by the following + scenario: + + We suppose that this transmission will involve one intermediate + gateway. + + The sending application program prepares its data and calls on its + local internet module to send that data as a datagram and passes the + destination address and other parameters as arguments of the call. + + The internet module prepares a datagram header and attaches the data + to it. The internet module determines a local network address for + this internet address, in this case it is the address of a gateway. + + + [Page 5] + + + September 1981 +Internet Protocol +Overview + + + + It sends this datagram and the local network address to the local + network interface. + + The local network interface creates a local network header, and + attaches the datagram to it, then sends the result via the local + network. + + The datagram arrives at a gateway host wrapped in the local network + header, the local network interface strips off this header, and + turns the datagram over to the internet module. The internet module + determines from the internet address that the datagram is to be + forwarded to another host in a second network. The internet module + determines a local net address for the destination host. It calls + on the local network interface for that network to send the + datagram. + + This local network interface creates a local network header and + attaches the datagram sending the result to the destination host. + + At this destination host the datagram is stripped of the local net + header by the local network interface and handed to the internet + module. + + The internet module determines that the datagram is for an + application program in this host. It passes the data to the + application program in response to a system call, passing the source + address and other parameters as results of the call. + + + Application Application + Program Program + \ / + Internet Module Internet Module Internet Module + \ / \ / + LNI-1 LNI-1 LNI-2 LNI-2 + \ / \ / + Local Network 1 Local Network 2 + + + + Transmission Path + + Figure 2 + + + + + + + +[Page 6] + + +September 1981 + Internet Protocol + Overview + + + +2.3. Function Description + + The function or purpose of Internet Protocol is to move datagrams + through an interconnected set of networks. This is done by passing + the datagrams from one internet module to another until the + destination is reached. The internet modules reside in hosts and + gateways in the internet system. The datagrams are routed from one + internet module to another through individual networks based on the + interpretation of an internet address. Thus, one important mechanism + of the internet protocol is the internet address. + + In the routing of messages from one internet module to another, + datagrams may need to traverse a network whose maximum packet size is + smaller than the size of the datagram. To overcome this difficulty, a + fragmentation mechanism is provided in the internet protocol. + + Addressing + + A distinction is made between names, addresses, and routes [4]. A + name indicates what we seek. An address indicates where it is. A + route indicates how to get there. The internet protocol deals + primarily with addresses. It is the task of higher level (i.e., + host-to-host or application) protocols to make the mapping from + names to addresses. The internet module maps internet addresses to + local net addresses. It is the task of lower level (i.e., local net + or gateways) procedures to make the mapping from local net addresses + to routes. + + Addresses are fixed length of four octets (32 bits). An address + begins with a network number, followed by local address (called the + "rest" field). There are three formats or classes of internet + addresses: in class a, the high order bit is zero, the next 7 bits + are the network, and the last 24 bits are the local address; in + class b, the high order two bits are one-zero, the next 14 bits are + the network and the last 16 bits are the local address; in class c, + the high order three bits are one-one-zero, the next 21 bits are the + network and the last 8 bits are the local address. + + Care must be taken in mapping internet addresses to local net + addresses; a single physical host must be able to act as if it were + several distinct hosts to the extent of using several distinct + internet addresses. Some hosts will also have several physical + interfaces (multi-homing). + + That is, provision must be made for a host to have several physical + interfaces to the network with each having several logical internet + addresses. + + + + [Page 7] + + + September 1981 +Internet Protocol +Overview + + + + Examples of address mappings may be found in "Address Mappings" [5]. + + Fragmentation + + Fragmentation of an internet datagram is necessary when it + originates in a local net that allows a large packet size and must + traverse a local net that limits packets to a smaller size to reach + its destination. + + An internet datagram can be marked "don't fragment." Any internet + datagram so marked is not to be internet fragmented under any + circumstances. If internet datagram marked don't fragment cannot be + delivered to its destination without fragmenting it, it is to be + discarded instead. + + Fragmentation, transmission and reassembly across a local network + which is invisible to the internet protocol module is called + intranet fragmentation and may be used [6]. + + The internet fragmentation and reassembly procedure needs to be able + to break a datagram into an almost arbitrary number of pieces that + can be later reassembled. The receiver of the fragments uses the + identification field to ensure that fragments of different datagrams + are not mixed. The fragment offset field tells the receiver the + position of a fragment in the original datagram. The fragment + offset and length determine the portion of the original datagram + covered by this fragment. The more-fragments flag indicates (by + being reset) the last fragment. These fields provide sufficient + information to reassemble datagrams. + + The identification field is used to distinguish the fragments of one + datagram from those of another. The originating protocol module of + an internet datagram sets the identification field to a value that + must be unique for that source-destination pair and protocol for the + time the datagram will be active in the internet system. The + originating protocol module of a complete datagram sets the + more-fragments flag to zero and the fragment offset to zero. + + To fragment a long internet datagram, an internet protocol module + (for example, in a gateway), creates two new internet datagrams and + copies the contents of the internet header fields from the long + datagram into both new internet headers. The data of the long + datagram is divided into two portions on a 8 octet (64 bit) boundary + (the second portion might not be an integral multiple of 8 octets, + but the first must be). Call the number of 8 octet blocks in the + first portion NFB (for Number of Fragment Blocks). The first + portion of the data is placed in the first new internet datagram, + and the total length field is set to the length of the first + + +[Page 8] + + +September 1981 + Internet Protocol + Overview + + + + datagram. The more-fragments flag is set to one. The second + portion of the data is placed in the second new internet datagram, + and the total length field is set to the length of the second + datagram. The more-fragments flag carries the same value as the + long datagram. The fragment offset field of the second new internet + datagram is set to the value of that field in the long datagram plus + NFB. + + This procedure can be generalized for an n-way split, rather than + the two-way split described. + + To assemble the fragments of an internet datagram, an internet + protocol module (for example at a destination host) combines + internet datagrams that all have the same value for the four fields: + identification, source, destination, and protocol. The combination + is done by placing the data portion of each fragment in the relative + position indicated by the fragment offset in that fragment's + internet header. The first fragment will have the fragment offset + zero, and the last fragment will have the more-fragments flag reset + to zero. + +2.4. Gateways + + Gateways implement internet protocol to forward datagrams between + networks. Gateways also implement the Gateway to Gateway Protocol + (GGP) [7] to coordinate routing and other internet control + information. + + In a gateway the higher level protocols need not be implemented and + the GGP functions are added to the IP module. + + + +-------------------------------+ + | Internet Protocol & ICMP & GGP| + +-------------------------------+ + | | + +---------------+ +---------------+ + | Local Net | | Local Net | + +---------------+ +---------------+ + + Gateway Protocols + + Figure 3. + + + + + + + + [Page 9] + + + September 1981 +Internet Protocol + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[Page 10] + + +September 1981 + Internet Protocol + + + + 3. SPECIFICATION + +3.1. Internet Header Format + + A summary of the contents of the internet header follows: + + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |Version| IHL |Type of Service| Total Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Identification |Flags| Fragment Offset | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Time to Live | Protocol | Header Checksum | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Source Address | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Destination Address | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Options | Padding | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Example Internet Datagram Header + + Figure 4. + + Note that each tick mark represents one bit position. + + Version: 4 bits + + The Version field indicates the format of the internet header. This + document describes version 4. + + IHL: 4 bits + + Internet Header Length is the length of the internet header in 32 + bit words, and thus points to the beginning of the data. Note that + the minimum value for a correct header is 5. + + + + + + + + + + + + + [Page 11] + + + September 1981 +Internet Protocol +Specification + + + + Type of Service: 8 bits + + The Type of Service provides an indication of the abstract + parameters of the quality of service desired. These parameters are + to be used to guide the selection of the actual service parameters + when transmitting a datagram through a particular network. Several + networks offer service precedence, which somehow treats high + precedence traffic as more important than other traffic (generally + by accepting only traffic above a certain precedence at time of high + load). The major choice is a three way tradeoff between low-delay, + high-reliability, and high-throughput. + + Bits 0-2: Precedence. + Bit 3: 0 = Normal Delay, 1 = Low Delay. + Bits 4: 0 = Normal Throughput, 1 = High Throughput. + Bits 5: 0 = Normal Relibility, 1 = High Relibility. + Bit 6-7: Reserved for Future Use. + + 0 1 2 3 4 5 6 7 + +-----+-----+-----+-----+-----+-----+-----+-----+ + | | | | | | | + | PRECEDENCE | D | T | R | 0 | 0 | + | | | | | | | + +-----+-----+-----+-----+-----+-----+-----+-----+ + + Precedence + + 111 - Network Control + 110 - Internetwork Control + 101 - CRITIC/ECP + 100 - Flash Override + 011 - Flash + 010 - Immediate + 001 - Priority + 000 - Routine + + The use of the Delay, Throughput, and Reliability indications may + increase the cost (in some sense) of the service. In many networks + better performance for one of these parameters is coupled with worse + performance on another. Except for very unusual cases at most two + of these three indications should be set. + + The type of service is used to specify the treatment of the datagram + during its transmission through the internet system. Example + mappings of the internet type of service to the actual service + provided on networks such as AUTODIN II, ARPANET, SATNET, and PRNET + is given in "Service Mappings" [8]. + + + +[Page 12] + + +September 1981 + Internet Protocol + Specification + + + + The Network Control precedence designation is intended to be used + within a network only. The actual use and control of that + designation is up to each network. The Internetwork Control + designation is intended for use by gateway control originators only. + If the actual use of these precedence designations is of concern to + a particular network, it is the responsibility of that network to + control the access to, and use of, those precedence designations. + + Total Length: 16 bits + + Total Length is the length of the datagram, measured in octets, + including internet header and data. This field allows the length of + a datagram to be up to 65,535 octets. Such long datagrams are + impractical for most hosts and networks. All hosts must be prepared + to accept datagrams of up to 576 octets (whether they arrive whole + or in fragments). It is recommended that hosts only send datagrams + larger than 576 octets if they have assurance that the destination + is prepared to accept the larger datagrams. + + The number 576 is selected to allow a reasonable sized data block to + be transmitted in addition to the required header information. For + example, this size allows a data block of 512 octets plus 64 header + octets to fit in a datagram. The maximal internet header is 60 + octets, and a typical internet header is 20 octets, allowing a + margin for headers of higher level protocols. + + Identification: 16 bits + + An identifying value assigned by the sender to aid in assembling the + fragments of a datagram. + + Flags: 3 bits + + Various Control Flags. + + Bit 0: reserved, must be zero + Bit 1: (DF) 0 = May Fragment, 1 = Don't Fragment. + Bit 2: (MF) 0 = Last Fragment, 1 = More Fragments. + + 0 1 2 + +---+---+---+ + | | D | M | + | 0 | F | F | + +---+---+---+ + + Fragment Offset: 13 bits + + This field indicates where in the datagram this fragment belongs. + + + [Page 13] + + + September 1981 +Internet Protocol +Specification + + + + The fragment offset is measured in units of 8 octets (64 bits). The + first fragment has offset zero. + + Time to Live: 8 bits + + This field indicates the maximum time the datagram is allowed to + remain in the internet system. If this field contains the value + zero, then the datagram must be destroyed. This field is modified + in internet header processing. The time is measured in units of + seconds, but since every module that processes a datagram must + decrease the TTL by at least one even if it process the datagram in + less than a second, the TTL must be thought of only as an upper + bound on the time a datagram may exist. The intention is to cause + undeliverable datagrams to be discarded, and to bound the maximum + datagram lifetime. + + Protocol: 8 bits + + This field indicates the next level protocol used in the data + portion of the internet datagram. The values for various protocols + are specified in "Assigned Numbers" [9]. + + Header Checksum: 16 bits + + A checksum on the header only. Since some header fields change + (e.g., time to live), this is recomputed and verified at each point + that the internet header is processed. + + The checksum algorithm is: + + The checksum field is the 16 bit one's complement of the one's + complement sum of all 16 bit words in the header. For purposes of + computing the checksum, the value of the checksum field is zero. + + This is a simple to compute checksum and experimental evidence + indicates it is adequate, but it is provisional and may be replaced + by a CRC procedure, depending on further experience. + + Source Address: 32 bits + + The source address. See section 3.2. + + Destination Address: 32 bits + + The destination address. See section 3.2. + + + + + +[Page 14] + + +September 1981 + Internet Protocol + Specification + + + + Options: variable + + The options may appear or not in datagrams. They must be + implemented by all IP modules (host and gateways). What is optional + is their transmission in any particular datagram, not their + implementation. + + In some environments the security option may be required in all + datagrams. + + The option field is variable in length. There may be zero or more + options. There are two cases for the format of an option: + + Case 1: A single octet of option-type. + + Case 2: An option-type octet, an option-length octet, and the + actual option-data octets. + + The option-length octet counts the option-type octet and the + option-length octet as well as the option-data octets. + + The option-type octet is viewed as having 3 fields: + + 1 bit copied flag, + 2 bits option class, + 5 bits option number. + + The copied flag indicates that this option is copied into all + fragments on fragmentation. + + 0 = not copied + 1 = copied + + The option classes are: + + 0 = control + 1 = reserved for future use + 2 = debugging and measurement + 3 = reserved for future use + + + + + + + + + + + + [Page 15] + + + September 1981 +Internet Protocol +Specification + + + + The following internet options are defined: + + CLASS NUMBER LENGTH DESCRIPTION + ----- ------ ------ ----------- + 0 0 - End of Option list. This option occupies only + 1 octet; it has no length octet. + 0 1 - No Operation. This option occupies only 1 + octet; it has no length octet. + 0 2 11 Security. Used to carry Security, + Compartmentation, User Group (TCC), and + Handling Restriction Codes compatible with DOD + requirements. + 0 3 var. Loose Source Routing. Used to route the + internet datagram based on information + supplied by the source. + 0 9 var. Strict Source Routing. Used to route the + internet datagram based on information + supplied by the source. + 0 7 var. Record Route. Used to trace the route an + internet datagram takes. + 0 8 4 Stream ID. Used to carry the stream + identifier. + 2 4 var. Internet Timestamp. + + + + Specific Option Definitions + + End of Option List + + +--------+ + |00000000| + +--------+ + Type=0 + + This option indicates the end of the option list. This might + not coincide with the end of the internet header according to + the internet header length. This is used at the end of all + options, not the end of each option, and need only be used if + the end of the options would not otherwise coincide with the end + of the internet header. + + May be copied, introduced, or deleted on fragmentation, or for + any other reason. + + + + + + +[Page 16] + + +September 1981 + Internet Protocol + Specification + + + + No Operation + + +--------+ + |00000001| + +--------+ + Type=1 + + This option may be used between options, for example, to align + the beginning of a subsequent option on a 32 bit boundary. + + May be copied, introduced, or deleted on fragmentation, or for + any other reason. + + Security + + This option provides a way for hosts to send security, + compartmentation, handling restrictions, and TCC (closed user + group) parameters. The format for this option is as follows: + + +--------+--------+---//---+---//---+---//---+---//---+ + |10000010|00001011|SSS SSS|CCC CCC|HHH HHH| TCC | + +--------+--------+---//---+---//---+---//---+---//---+ + Type=130 Length=11 + + Security (S field): 16 bits + + Specifies one of 16 levels of security (eight of which are + reserved for future use). + + 00000000 00000000 - Unclassified + 11110001 00110101 - Confidential + 01111000 10011010 - EFTO + 10111100 01001101 - MMMM + 01011110 00100110 - PROG + 10101111 00010011 - Restricted + 11010111 10001000 - Secret + 01101011 11000101 - Top Secret + 00110101 11100010 - (Reserved for future use) + 10011010 11110001 - (Reserved for future use) + 01001101 01111000 - (Reserved for future use) + 00100100 10111101 - (Reserved for future use) + 00010011 01011110 - (Reserved for future use) + 10001001 10101111 - (Reserved for future use) + 11000100 11010110 - (Reserved for future use) + 11100010 01101011 - (Reserved for future use) + + + + + + [Page 17] + + + September 1981 +Internet Protocol +Specification + + + + Compartments (C field): 16 bits + + An all zero value is used when the information transmitted is + not compartmented. Other values for the compartments field + may be obtained from the Defense Intelligence Agency. + + Handling Restrictions (H field): 16 bits + + The values for the control and release markings are + alphanumeric digraphs and are defined in the Defense + Intelligence Agency Manual DIAM 65-19, "Standard Security + Markings". + + Transmission Control Code (TCC field): 24 bits + + Provides a means to segregate traffic and define controlled + communities of interest among subscribers. The TCC values are + trigraphs, and are available from HQ DCA Code 530. + + Must be copied on fragmentation. This option appears at most + once in a datagram. + + Loose Source and Record Route + + +--------+--------+--------+---------//--------+ + |10000011| length | pointer| route data | + +--------+--------+--------+---------//--------+ + Type=131 + + The loose source and record route (LSRR) option provides a means + for the source of an internet datagram to supply routing + information to be used by the gateways in forwarding the + datagram to the destination, and to record the route + information. + + The option begins with the option type code. The second octet + is the option length which includes the option type code and the + length octet, the pointer octet, and length-3 octets of route + data. The third octet is the pointer into the route data + indicating the octet which begins the next source address to be + processed. The pointer is relative to this option, and the + smallest legal value for the pointer is 4. + + A route data is composed of a series of internet addresses. + Each internet address is 32 bits or 4 octets. If the pointer is + greater than the length, the source route is empty (and the + recorded route full) and the routing is to be based on the + destination address field. + + +[Page 18] + + +September 1981 + Internet Protocol + Specification + + + + If the address in destination address field has been reached and + the pointer is not greater than the length, the next address in + the source route replaces the address in the destination address + field, and the recorded route address replaces the source + address just used, and pointer is increased by four. + + The recorded route address is the internet module's own internet + address as known in the environment into which this datagram is + being forwarded. + + This procedure of replacing the source route with the recorded + route (though it is in the reverse of the order it must be in to + be used as a source route) means the option (and the IP header + as a whole) remains a constant length as the datagram progresses + through the internet. + + This option is a loose source route because the gateway or host + IP is allowed to use any route of any number of other + intermediate gateways to reach the next address in the route. + + Must be copied on fragmentation. Appears at most once in a + datagram. + + Strict Source and Record Route + + +--------+--------+--------+---------//--------+ + |10001001| length | pointer| route data | + +--------+--------+--------+---------//--------+ + Type=137 + + The strict source and record route (SSRR) option provides a + means for the source of an internet datagram to supply routing + information to be used by the gateways in forwarding the + datagram to the destination, and to record the route + information. + + The option begins with the option type code. The second octet + is the option length which includes the option type code and the + length octet, the pointer octet, and length-3 octets of route + data. The third octet is the pointer into the route data + indicating the octet which begins the next source address to be + processed. The pointer is relative to this option, and the + smallest legal value for the pointer is 4. + + A route data is composed of a series of internet addresses. + Each internet address is 32 bits or 4 octets. If the pointer is + greater than the length, the source route is empty (and the + + + + [Page 19] + + + September 1981 +Internet Protocol +Specification + + + + recorded route full) and the routing is to be based on the + destination address field. + + If the address in destination address field has been reached and + the pointer is not greater than the length, the next address in + the source route replaces the address in the destination address + field, and the recorded route address replaces the source + address just used, and pointer is increased by four. + + The recorded route address is the internet module's own internet + address as known in the environment into which this datagram is + being forwarded. + + This procedure of replacing the source route with the recorded + route (though it is in the reverse of the order it must be in to + be used as a source route) means the option (and the IP header + as a whole) remains a constant length as the datagram progresses + through the internet. + + This option is a strict source route because the gateway or host + IP must send the datagram directly to the next address in the + source route through only the directly connected network + indicated in the next address to reach the next gateway or host + specified in the route. + + Must be copied on fragmentation. Appears at most once in a + datagram. + + Record Route + + +--------+--------+--------+---------//--------+ + |00000111| length | pointer| route data | + +--------+--------+--------+---------//--------+ + Type=7 + + The record route option provides a means to record the route of + an internet datagram. + + The option begins with the option type code. The second octet + is the option length which includes the option type code and the + length octet, the pointer octet, and length-3 octets of route + data. The third octet is the pointer into the route data + indicating the octet which begins the next area to store a route + address. The pointer is relative to this option, and the + smallest legal value for the pointer is 4. + + A recorded route is composed of a series of internet addresses. + Each internet address is 32 bits or 4 octets. If the pointer is + + +[Page 20] + + +September 1981 + Internet Protocol + Specification + + + + greater than the length, the recorded route data area is full. + The originating host must compose this option with a large + enough route data area to hold all the address expected. The + size of the option does not change due to adding addresses. The + intitial contents of the route data area must be zero. + + When an internet module routes a datagram it checks to see if + the record route option is present. If it is, it inserts its + own internet address as known in the environment into which this + datagram is being forwarded into the recorded route begining at + the octet indicated by the pointer, and increments the pointer + by four. + + If the route data area is already full (the pointer exceeds the + length) the datagram is forwarded without inserting the address + into the recorded route. If there is some room but not enough + room for a full address to be inserted, the original datagram is + considered to be in error and is discarded. In either case an + ICMP parameter problem message may be sent to the source + host [3]. + + Not copied on fragmentation, goes in first fragment only. + Appears at most once in a datagram. + + Stream Identifier + + +--------+--------+--------+--------+ + |10001000|00000010| Stream ID | + +--------+--------+--------+--------+ + Type=136 Length=4 + + This option provides a way for the 16-bit SATNET stream + identifier to be carried through networks that do not support + the stream concept. + + Must be copied on fragmentation. Appears at most once in a + datagram. + + + + + + + + + + + + + + [Page 21] + + + September 1981 +Internet Protocol +Specification + + + + Internet Timestamp + + +--------+--------+--------+--------+ + |01000100| length | pointer|oflw|flg| + +--------+--------+--------+--------+ + | internet address | + +--------+--------+--------+--------+ + | timestamp | + +--------+--------+--------+--------+ + | . | + . + . + Type = 68 + + The Option Length is the number of octets in the option counting + the type, length, pointer, and overflow/flag octets (maximum + length 40). + + The Pointer is the number of octets from the beginning of this + option to the end of timestamps plus one (i.e., it points to the + octet beginning the space for next timestamp). The smallest + legal value is 5. The timestamp area is full when the pointer + is greater than the length. + + The Overflow (oflw) [4 bits] is the number of IP modules that + cannot register timestamps due to lack of space. + + The Flag (flg) [4 bits] values are + + 0 -- time stamps only, stored in consecutive 32-bit words, + + 1 -- each timestamp is preceded with internet address of the + registering entity, + + 3 -- the internet address fields are prespecified. An IP + module only registers its timestamp if it matches its own + address with the next specified internet address. + + The Timestamp is a right-justified, 32-bit timestamp in + milliseconds since midnight UT. If the time is not available in + milliseconds or cannot be provided with respect to midnight UT + then any time may be inserted as a timestamp provided the high + order bit of the timestamp field is set to one to indicate the + use of a non-standard value. + + The originating host must compose this option with a large + enough timestamp data area to hold all the timestamp information + expected. The size of the option does not change due to adding + + +[Page 22] + + +September 1981 + Internet Protocol + Specification + + + + timestamps. The intitial contents of the timestamp data area + must be zero or internet address/zero pairs. + + If the timestamp data area is already full (the pointer exceeds + the length) the datagram is forwarded without inserting the + timestamp, but the overflow count is incremented by one. + + If there is some room but not enough room for a full timestamp + to be inserted, or the overflow count itself overflows, the + original datagram is considered to be in error and is discarded. + In either case an ICMP parameter problem message may be sent to + the source host [3]. + + The timestamp option is not copied upon fragmentation. It is + carried in the first fragment. Appears at most once in a + datagram. + + Padding: variable + + The internet header padding is used to ensure that the internet + header ends on a 32 bit boundary. The padding is zero. + +3.2. Discussion + + The implementation of a protocol must be robust. Each implementation + must expect to interoperate with others created by different + individuals. While the goal of this specification is to be explicit + about the protocol there is the possibility of differing + interpretations. In general, an implementation must be conservative + in its sending behavior, and liberal in its receiving behavior. That + is, it must be careful to send well-formed datagrams, but must accept + any datagram that it can interpret (e.g., not object to technical + errors where the meaning is still clear). + + The basic internet service is datagram oriented and provides for the + fragmentation of datagrams at gateways, with reassembly taking place + at the destination internet protocol module in the destination host. + Of course, fragmentation and reassembly of datagrams within a network + or by private agreement between the gateways of a network is also + allowed since this is transparent to the internet protocols and the + higher-level protocols. This transparent type of fragmentation and + reassembly is termed "network-dependent" (or intranet) fragmentation + and is not discussed further here. + + Internet addresses distinguish sources and destinations to the host + level and provide a protocol field as well. It is assumed that each + protocol will provide for whatever multiplexing is necessary within a + host. + + + [Page 23] + + + September 1981 +Internet Protocol +Specification + + + + Addressing + + To provide for flexibility in assigning address to networks and + allow for the large number of small to intermediate sized networks + the interpretation of the address field is coded to specify a small + number of networks with a large number of host, a moderate number of + networks with a moderate number of hosts, and a large number of + networks with a small number of hosts. In addition there is an + escape code for extended addressing mode. + + Address Formats: + + High Order Bits Format Class + --------------- ------------------------------- ----- + 0 7 bits of net, 24 bits of host a + 10 14 bits of net, 16 bits of host b + 110 21 bits of net, 8 bits of host c + 111 escape to extended addressing mode + + A value of zero in the network field means this network. This is + only used in certain ICMP messages. The extended addressing mode + is undefined. Both of these features are reserved for future use. + + The actual values assigned for network addresses is given in + "Assigned Numbers" [9]. + + The local address, assigned by the local network, must allow for a + single physical host to act as several distinct internet hosts. + That is, there must be a mapping between internet host addresses and + network/host interfaces that allows several internet addresses to + correspond to one interface. It must also be allowed for a host to + have several physical interfaces and to treat the datagrams from + several of them as if they were all addressed to a single host. + + Address mappings between internet addresses and addresses for + ARPANET, SATNET, PRNET, and other networks are described in "Address + Mappings" [5]. + + Fragmentation and Reassembly. + + The internet identification field (ID) is used together with the + source and destination address, and the protocol fields, to identify + datagram fragments for reassembly. + + The More Fragments flag bit (MF) is set if the datagram is not the + last fragment. The Fragment Offset field identifies the fragment + location, relative to the beginning of the original unfragmented + datagram. Fragments are counted in units of 8 octets. The + + +[Page 24] + + +September 1981 + Internet Protocol + Specification + + + + fragmentation strategy is designed so than an unfragmented datagram + has all zero fragmentation information (MF = 0, fragment offset = + 0). If an internet datagram is fragmented, its data portion must be + broken on 8 octet boundaries. + + This format allows 2**13 = 8192 fragments of 8 octets each for a + total of 65,536 octets. Note that this is consistent with the the + datagram total length field (of course, the header is counted in the + total length and not in the fragments). + + When fragmentation occurs, some options are copied, but others + remain with the first fragment only. + + Every internet module must be able to forward a datagram of 68 + octets without further fragmentation. This is because an internet + header may be up to 60 octets, and the minimum fragment is 8 octets. + + Every internet destination must be able to receive a datagram of 576 + octets either in one piece or in fragments to be reassembled. + + The fields which may be affected by fragmentation include: + + (1) options field + (2) more fragments flag + (3) fragment offset + (4) internet header length field + (5) total length field + (6) header checksum + + If the Don't Fragment flag (DF) bit is set, then internet + fragmentation of this datagram is NOT permitted, although it may be + discarded. This can be used to prohibit fragmentation in cases + where the receiving host does not have sufficient resources to + reassemble internet fragments. + + One example of use of the Don't Fragment feature is to down line + load a small host. A small host could have a boot strap program + that accepts a datagram stores it in memory and then executes it. + + The fragmentation and reassembly procedures are most easily + described by examples. The following procedures are example + implementations. + + General notation in the following pseudo programs: "=<" means "less + than or equal", "#" means "not equal", "=" means "equal", "<-" means + "is set to". Also, "x to y" includes x and excludes y; for example, + "4 to 7" would include 4, 5, and 6 (but not 7). + + + + [Page 25] + + + September 1981 +Internet Protocol +Specification + + + + An Example Fragmentation Procedure + + The maximum sized datagram that can be transmitted through the + next network is called the maximum transmission unit (MTU). + + If the total length is less than or equal the maximum transmission + unit then submit this datagram to the next step in datagram + processing; otherwise cut the datagram into two fragments, the + first fragment being the maximum size, and the second fragment + being the rest of the datagram. The first fragment is submitted + to the next step in datagram processing, while the second fragment + is submitted to this procedure in case it is still too large. + + Notation: + + FO - Fragment Offset + IHL - Internet Header Length + DF - Don't Fragment flag + MF - More Fragments flag + TL - Total Length + OFO - Old Fragment Offset + OIHL - Old Internet Header Length + OMF - Old More Fragments flag + OTL - Old Total Length + NFB - Number of Fragment Blocks + MTU - Maximum Transmission Unit + + Procedure: + + IF TL =< MTU THEN Submit this datagram to the next step + in datagram processing ELSE IF DF = 1 THEN discard the + datagram ELSE + To produce the first fragment: + (1) Copy the original internet header; + (2) OIHL <- IHL; OTL <- TL; OFO <- FO; OMF <- MF; + (3) NFB <- (MTU-IHL*4)/8; + (4) Attach the first NFB*8 data octets; + (5) Correct the header: + MF <- 1; TL <- (IHL*4)+(NFB*8); + Recompute Checksum; + (6) Submit this fragment to the next step in + datagram processing; + To produce the second fragment: + (7) Selectively copy the internet header (some options + are not copied, see option definitions); + (8) Append the remaining data; + (9) Correct the header: + IHL <- (((OIHL*4)-(length of options not copied))+3)/4; + + +[Page 26] + + +September 1981 + Internet Protocol + Specification + + + + TL <- OTL - NFB*8 - (OIHL-IHL)*4); + FO <- OFO + NFB; MF <- OMF; Recompute Checksum; + (10) Submit this fragment to the fragmentation test; DONE. + + In the above procedure each fragment (except the last) was made + the maximum allowable size. An alternative might produce less + than the maximum size datagrams. For example, one could implement + a fragmentation procedure that repeatly divided large datagrams in + half until the resulting fragments were less than the maximum + transmission unit size. + + An Example Reassembly Procedure + + For each datagram the buffer identifier is computed as the + concatenation of the source, destination, protocol, and + identification fields. If this is a whole datagram (that is both + the fragment offset and the more fragments fields are zero), then + any reassembly resources associated with this buffer identifier + are released and the datagram is forwarded to the next step in + datagram processing. + + If no other fragment with this buffer identifier is on hand then + reassembly resources are allocated. The reassembly resources + consist of a data buffer, a header buffer, a fragment block bit + table, a total data length field, and a timer. The data from the + fragment is placed in the data buffer according to its fragment + offset and length, and bits are set in the fragment block bit + table corresponding to the fragment blocks received. + + If this is the first fragment (that is the fragment offset is + zero) this header is placed in the header buffer. If this is the + last fragment ( that is the more fragments field is zero) the + total data length is computed. If this fragment completes the + datagram (tested by checking the bits set in the fragment block + table), then the datagram is sent to the next step in datagram + processing; otherwise the timer is set to the maximum of the + current timer value and the value of the time to live field from + this fragment; and the reassembly routine gives up control. + + If the timer runs out, the all reassembly resources for this + buffer identifier are released. The initial setting of the timer + is a lower bound on the reassembly waiting time. This is because + the waiting time will be increased if the Time to Live in the + arriving fragment is greater than the current timer value but will + not be decreased if it is less. The maximum this timer value + could reach is the maximum time to live (approximately 4.25 + minutes). The current recommendation for the initial timer + setting is 15 seconds. This may be changed as experience with + + + [Page 27] + + + September 1981 +Internet Protocol +Specification + + + + this protocol accumulates. Note that the choice of this parameter + value is related to the buffer capacity available and the data + rate of the transmission medium; that is, data rate times timer + value equals buffer size (e.g., 10Kb/s X 15s = 150Kb). + + Notation: + + FO - Fragment Offset + IHL - Internet Header Length + MF - More Fragments flag + TTL - Time To Live + NFB - Number of Fragment Blocks + TL - Total Length + TDL - Total Data Length + BUFID - Buffer Identifier + RCVBT - Fragment Received Bit Table + TLB - Timer Lower Bound + + Procedure: + + (1) BUFID <- source|destination|protocol|identification; + (2) IF FO = 0 AND MF = 0 + (3) THEN IF buffer with BUFID is allocated + (4) THEN flush all reassembly for this BUFID; + (5) Submit datagram to next step; DONE. + (6) ELSE IF no buffer with BUFID is allocated + (7) THEN allocate reassembly resources + with BUFID; + TIMER <- TLB; TDL <- 0; + (8) put data from fragment into data buffer with + BUFID from octet FO*8 to + octet (TL-(IHL*4))+FO*8; + (9) set RCVBT bits from FO + to FO+((TL-(IHL*4)+7)/8); + (10) IF MF = 0 THEN TDL <- TL-(IHL*4)+(FO*8) + (11) IF FO = 0 THEN put header in header buffer + (12) IF TDL # 0 + (13) AND all RCVBT bits from 0 + to (TDL+7)/8 are set + (14) THEN TL <- TDL+(IHL*4) + (15) Submit datagram to next step; + (16) free all reassembly resources + for this BUFID; DONE. + (17) TIMER <- MAX(TIMER,TTL); + (18) give up until next fragment or timer expires; + (19) timer expires: flush all reassembly with this BUFID; DONE. + + In the case that two or more fragments contain the same data + + +[Page 28] + + +September 1981 + Internet Protocol + Specification + + + + either identically or through a partial overlap, this procedure + will use the more recently arrived copy in the data buffer and + datagram delivered. + + Identification + + The choice of the Identifier for a datagram is based on the need to + provide a way to uniquely identify the fragments of a particular + datagram. The protocol module assembling fragments judges fragments + to belong to the same datagram if they have the same source, + destination, protocol, and Identifier. Thus, the sender must choose + the Identifier to be unique for this source, destination pair and + protocol for the time the datagram (or any fragment of it) could be + alive in the internet. + + It seems then that a sending protocol module needs to keep a table + of Identifiers, one entry for each destination it has communicated + with in the last maximum packet lifetime for the internet. + + However, since the Identifier field allows 65,536 different values, + some host may be able to simply use unique identifiers independent + of destination. + + It is appropriate for some higher level protocols to choose the + identifier. For example, TCP protocol modules may retransmit an + identical TCP segment, and the probability for correct reception + would be enhanced if the retransmission carried the same identifier + as the original transmission since fragments of either datagram + could be used to construct a correct TCP segment. + + Type of Service + + The type of service (TOS) is for internet service quality selection. + The type of service is specified along the abstract parameters + precedence, delay, throughput, and reliability. These abstract + parameters are to be mapped into the actual service parameters of + the particular networks the datagram traverses. + + Precedence. An independent measure of the importance of this + datagram. + + Delay. Prompt delivery is important for datagrams with this + indication. + + Throughput. High data rate is important for datagrams with this + indication. + + + + + [Page 29] + + + September 1981 +Internet Protocol +Specification + + + + Reliability. A higher level of effort to ensure delivery is + important for datagrams with this indication. + + For example, the ARPANET has a priority bit, and a choice between + "standard" messages (type 0) and "uncontrolled" messages (type 3), + (the choice between single packet and multipacket messages can also + be considered a service parameter). The uncontrolled messages tend + to be less reliably delivered and suffer less delay. Suppose an + internet datagram is to be sent through the ARPANET. Let the + internet type of service be given as: + + Precedence: 5 + Delay: 0 + Throughput: 1 + Reliability: 1 + + In this example, the mapping of these parameters to those available + for the ARPANET would be to set the ARPANET priority bit on since + the Internet precedence is in the upper half of its range, to select + standard messages since the throughput and reliability requirements + are indicated and delay is not. More details are given on service + mappings in "Service Mappings" [8]. + + Time to Live + + The time to live is set by the sender to the maximum time the + datagram is allowed to be in the internet system. If the datagram + is in the internet system longer than the time to live, then the + datagram must be destroyed. + + This field must be decreased at each point that the internet header + is processed to reflect the time spent processing the datagram. + Even if no local information is available on the time actually + spent, the field must be decremented by 1. The time is measured in + units of seconds (i.e. the value 1 means one second). Thus, the + maximum time to live is 255 seconds or 4.25 minutes. Since every + module that processes a datagram must decrease the TTL by at least + one even if it process the datagram in less than a second, the TTL + must be thought of only as an upper bound on the time a datagram may + exist. The intention is to cause undeliverable datagrams to be + discarded, and to bound the maximum datagram lifetime. + + Some higher level reliable connection protocols are based on + assumptions that old duplicate datagrams will not arrive after a + certain time elapses. The TTL is a way for such protocols to have + an assurance that their assumption is met. + + + + +[Page 30] + + +September 1981 + Internet Protocol + Specification + + + + Options + + The options are optional in each datagram, but required in + implementations. That is, the presence or absence of an option is + the choice of the sender, but each internet module must be able to + parse every option. There can be several options present in the + option field. + + The options might not end on a 32-bit boundary. The internet header + must be filled out with octets of zeros. The first of these would + be interpreted as the end-of-options option, and the remainder as + internet header padding. + + Every internet module must be able to act on every option. The + Security Option is required if classified, restricted, or + compartmented traffic is to be passed. + + Checksum + + The internet header checksum is recomputed if the internet header is + changed. For example, a reduction of the time to live, additions or + changes to internet options, or due to fragmentation. This checksum + at the internet level is intended to protect the internet header + fields from transmission errors. + + There are some applications where a few data bit errors are + acceptable while retransmission delays are not. If the internet + protocol enforced data correctness such applications could not be + supported. + + Errors + + Internet protocol errors may be reported via the ICMP messages [3]. + +3.3. Interfaces + + The functional description of user interfaces to the IP is, at best, + fictional, since every operating system will have different + facilities. Consequently, we must warn readers that different IP + implementations may have different user interfaces. However, all IPs + must provide a certain minimum set of services to guarantee that all + IP implementations can support the same protocol hierarchy. This + section specifies the functional interfaces required of all IP + implementations. + + Internet protocol interfaces on one side to the local network and on + the other side to either a higher level protocol or an application + program. In the following, the higher level protocol or application + + + [Page 31] + + + September 1981 +Internet Protocol +Specification + + + + program (or even a gateway program) will be called the "user" since it + is using the internet module. Since internet protocol is a datagram + protocol, there is minimal memory or state maintained between datagram + transmissions, and each call on the internet protocol module by the + user supplies all information necessary for the IP to perform the + service requested. + + An Example Upper Level Interface + + The following two example calls satisfy the requirements for the user + to internet protocol module communication ("=>" means returns): + + SEND (src, dst, prot, TOS, TTL, BufPTR, len, Id, DF, opt => result) + + where: + + src = source address + dst = destination address + prot = protocol + TOS = type of service + TTL = time to live + BufPTR = buffer pointer + len = length of buffer + Id = Identifier + DF = Don't Fragment + opt = option data + result = response + OK = datagram sent ok + Error = error in arguments or local network error + + Note that the precedence is included in the TOS and the + security/compartment is passed as an option. + + RECV (BufPTR, prot, => result, src, dst, TOS, len, opt) + + where: + + BufPTR = buffer pointer + prot = protocol + result = response + OK = datagram received ok + Error = error in arguments + len = length of buffer + src = source address + dst = destination address + TOS = type of service + opt = option data + + + +[Page 32] + + +September 1981 + Internet Protocol + Specification + + + + When the user sends a datagram, it executes the SEND call supplying + all the arguments. The internet protocol module, on receiving this + call, checks the arguments and prepares and sends the message. If the + arguments are good and the datagram is accepted by the local network, + the call returns successfully. If either the arguments are bad, or + the datagram is not accepted by the local network, the call returns + unsuccessfully. On unsuccessful returns, a reasonable report must be + made as to the cause of the problem, but the details of such reports + are up to individual implementations. + + When a datagram arrives at the internet protocol module from the local + network, either there is a pending RECV call from the user addressed + or there is not. In the first case, the pending call is satisfied by + passing the information from the datagram to the user. In the second + case, the user addressed is notified of a pending datagram. If the + user addressed does not exist, an ICMP error message is returned to + the sender, and the data is discarded. + + The notification of a user may be via a pseudo interrupt or similar + mechanism, as appropriate in the particular operating system + environment of the implementation. + + A user's RECV call may then either be immediately satisfied by a + pending datagram, or the call may be pending until a datagram arrives. + + The source address is included in the send call in case the sending + host has several addresses (multiple physical connections or logical + addresses). The internet module must check to see that the source + address is one of the legal address for this host. + + An implementation may also allow or require a call to the internet + module to indicate interest in or reserve exclusive use of a class of + datagrams (e.g., all those with a certain value in the protocol + field). + + This section functionally characterizes a USER/IP interface. The + notation used is similar to most procedure of function calls in high + level languages, but this usage is not meant to rule out trap type + service calls (e.g., SVCs, UUOs, EMTs), or any other form of + interprocess communication. + + + + + + + + + + + [Page 33] + + + September 1981 +Internet Protocol + + + +APPENDIX A: Examples & Scenarios + +Example 1: + + This is an example of the minimal data carrying internet datagram: + + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |Ver= 4 |IHL= 5 |Type of Service| Total Length = 21 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Identification = 111 |Flg=0| Fragment Offset = 0 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Time = 123 | Protocol = 1 | header checksum | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | source address | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | destination address | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | data | + +-+-+-+-+-+-+-+-+ + + Example Internet Datagram + + Figure 5. + + Note that each tick mark represents one bit position. + + This is a internet datagram in version 4 of internet protocol; the + internet header consists of five 32 bit words, and the total length of + the datagram is 21 octets. This datagram is a complete datagram (not + a fragment). + + + + + + + + + + + + + + + + + + +[Page 34] + + +September 1981 + Internet Protocol + + + +Example 2: + + In this example, we show first a moderate size internet datagram (452 + data octets), then two internet fragments that might result from the + fragmentation of this datagram if the maximum sized transmission + allowed were 280 octets. + + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |Ver= 4 |IHL= 5 |Type of Service| Total Length = 472 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Identification = 111 |Flg=0| Fragment Offset = 0 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Time = 123 | Protocol = 6 | header checksum | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | source address | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | destination address | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | data | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | data | + \ \ + \ \ + | data | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | data | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Example Internet Datagram + + Figure 6. + + + + + + + + + + + + + + + + + + [Page 35] + + + September 1981 +Internet Protocol + + + + Now the first fragment that results from splitting the datagram after + 256 data octets. + + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |Ver= 4 |IHL= 5 |Type of Service| Total Length = 276 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Identification = 111 |Flg=1| Fragment Offset = 0 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Time = 119 | Protocol = 6 | Header Checksum | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | source address | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | destination address | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | data | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | data | + \ \ + \ \ + | data | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | data | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Example Internet Fragment + + Figure 7. + + + + + + + + + + + + + + + + + + + + + +[Page 36] + + +September 1981 + Internet Protocol + + + + And the second fragment. + + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |Ver= 4 |IHL= 5 |Type of Service| Total Length = 216 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Identification = 111 |Flg=0| Fragment Offset = 32 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Time = 119 | Protocol = 6 | Header Checksum | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | source address | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | destination address | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | data | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | data | + \ \ + \ \ + | data | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | data | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Example Internet Fragment + + Figure 8. + + + + + + + + + + + + + + + + + + + + + + + [Page 37] + + + September 1981 +Internet Protocol + + + +Example 3: + + Here, we show an example of a datagram containing options: + + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |Ver= 4 |IHL= 8 |Type of Service| Total Length = 576 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Identification = 111 |Flg=0| Fragment Offset = 0 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Time = 123 | Protocol = 6 | Header Checksum | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | source address | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | destination address | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Opt. Code = x | Opt. Len.= 3 | option value | Opt. Code = x | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Opt. Len. = 4 | option value | Opt. Code = 1 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Opt. Code = y | Opt. Len. = 3 | option value | Opt. Code = 0 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | data | + \ \ + \ \ + | data | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | data | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Example Internet Datagram + + Figure 9. + + + + + + + + + + + + + + + + +[Page 38] + + +September 1981 + Internet Protocol + + + +APPENDIX B: Data Transmission Order + +The order of transmission of the header and data described in this +document is resolved to the octet level. Whenever a diagram shows a +group of octets, the order of transmission of those octets is the normal +order in which they are read in English. For example, in the following +diagram the octets are transmitted in the order they are numbered. + + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 1 | 2 | 3 | 4 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 5 | 6 | 7 | 8 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 9 | 10 | 11 | 12 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Transmission Order of Bytes + + Figure 10. + +Whenever an octet represents a numeric quantity the left most bit in the +diagram is the high order or most significant bit. That is, the bit +labeled 0 is the most significant bit. For example, the following +diagram represents the value 170 (decimal). + + + 0 1 2 3 4 5 6 7 + +-+-+-+-+-+-+-+-+ + |1 0 1 0 1 0 1 0| + +-+-+-+-+-+-+-+-+ + + Significance of Bits + + Figure 11. + +Similarly, whenever a multi-octet field represents a numeric quantity +the left most bit of the whole field is the most significant bit. When +a multi-octet quantity is transmitted the most significant octet is +transmitted first. + + + + + + + + + + [Page 39] + + + September 1981 +Internet Protocol + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[Page 40] + + +September 1981 + Internet Protocol + + + + GLOSSARY + + + +1822 + BBN Report 1822, "The Specification of the Interconnection of + a Host and an IMP". The specification of interface between a + host and the ARPANET. + +ARPANET leader + The control information on an ARPANET message at the host-IMP + interface. + +ARPANET message + The unit of transmission between a host and an IMP in the + ARPANET. The maximum size is about 1012 octets (8096 bits). + +ARPANET packet + A unit of transmission used internally in the ARPANET between + IMPs. The maximum size is about 126 octets (1008 bits). + +Destination + The destination address, an internet header field. + +DF + The Don't Fragment bit carried in the flags field. + +Flags + An internet header field carrying various control flags. + +Fragment Offset + This internet header field indicates where in the internet + datagram a fragment belongs. + +GGP + Gateway to Gateway Protocol, the protocol used primarily + between gateways to control routing and other gateway + functions. + +header + Control information at the beginning of a message, segment, + datagram, packet or block of data. + +ICMP + Internet Control Message Protocol, implemented in the internet + module, the ICMP is used from gateways to hosts and between + hosts to report errors and make routing suggestions. + + + + + [Page 41] + + + September 1981 +Internet Protocol +Glossary + + + +Identification + An internet header field carrying the identifying value + assigned by the sender to aid in assembling the fragments of a + datagram. + +IHL + The internet header field Internet Header Length is the length + of the internet header measured in 32 bit words. + +IMP + The Interface Message Processor, the packet switch of the + ARPANET. + +Internet Address + A four octet (32 bit) source or destination address consisting + of a Network field and a Local Address field. + +internet datagram + The unit of data exchanged between a pair of internet modules + (includes the internet header). + +internet fragment + A portion of the data of an internet datagram with an internet + header. + +Local Address + The address of a host within a network. The actual mapping of + an internet local address on to the host addresses in a + network is quite general, allowing for many to one mappings. + +MF + The More-Fragments Flag carried in the internet header flags + field. + +module + An implementation, usually in software, of a protocol or other + procedure. + +more-fragments flag + A flag indicating whether or not this internet datagram + contains the end of an internet datagram, carried in the + internet header Flags field. + +NFB + The Number of Fragment Blocks in a the data portion of an + internet fragment. That is, the length of a portion of data + measured in 8 octet units. + + + +[Page 42] + + +September 1981 + Internet Protocol + Glossary + + + +octet + An eight bit byte. + +Options + The internet header Options field may contain several options, + and each option may be several octets in length. + +Padding + The internet header Padding field is used to ensure that the + data begins on 32 bit word boundary. The padding is zero. + +Protocol + In this document, the next higher level protocol identifier, + an internet header field. + +Rest + The local address portion of an Internet Address. + +Source + The source address, an internet header field. + +TCP + Transmission Control Protocol: A host-to-host protocol for + reliable communication in internet environments. + +TCP Segment + The unit of data exchanged between TCP modules (including the + TCP header). + +TFTP + Trivial File Transfer Protocol: A simple file transfer + protocol built on UDP. + +Time to Live + An internet header field which indicates the upper bound on + how long this internet datagram may exist. + +TOS + Type of Service + +Total Length + The internet header field Total Length is the length of the + datagram in octets including internet header and data. + +TTL + Time to Live + + + + + [Page 43] + + + September 1981 +Internet Protocol +Glossary + + + +Type of Service + An internet header field which indicates the type (or quality) + of service for this internet datagram. + +UDP + User Datagram Protocol: A user level protocol for transaction + oriented applications. + +User + The user of the internet protocol. This may be a higher level + protocol module, an application program, or a gateway program. + +Version + The Version field indicates the format of the internet header. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[Page 44] + + +September 1981 + Internet Protocol + + + + REFERENCES + + + +[1] Cerf, V., "The Catenet Model for Internetworking," Information + Processing Techniques Office, Defense Advanced Research Projects + Agency, IEN 48, July 1978. + +[2] Bolt Beranek and Newman, "Specification for the Interconnection of + a Host and an IMP," BBN Technical Report 1822, Revised May 1978. + +[3] Postel, J., "Internet Control Message Protocol - DARPA Internet + Program Protocol Specification," RFC 792, USC/Information Sciences + Institute, September 1981. + +[4] Shoch, J., "Inter-Network Naming, Addressing, and Routing," + COMPCON, IEEE Computer Society, Fall 1978. + +[5] Postel, J., "Address Mappings," RFC 796, USC/Information Sciences + Institute, September 1981. + +[6] Shoch, J., "Packet Fragmentation in Inter-Network Protocols," + Computer Networks, v. 3, n. 1, February 1979. + +[7] Strazisar, V., "How to Build a Gateway", IEN 109, Bolt Beranek and + Newman, August 1979. + +[8] Postel, J., "Service Mappings," RFC 795, USC/Information Sciences + Institute, September 1981. + +[9] Postel, J., "Assigned Numbers," RFC 790, USC/Information Sciences + Institute, September 1981. + + + + + + + + + + + + + + + + + + + + [Page 45] diff --git a/dist/docs/rfc8200-ipv6.txt b/dist/docs/rfc8200-ipv6.txt new file mode 100644 index 0000000..58e85e0 --- /dev/null +++ b/dist/docs/rfc8200-ipv6.txt @@ -0,0 +1,2354 @@ + + + + + + +Internet Engineering Task Force (IETF) S. Deering +Request for Comments: 8200 Retired +STD: 86 R. Hinden +Obsoletes: 2460 Check Point Software +Category: Standards Track July 2017 +ISSN: 2070-1721 + + + Internet Protocol, Version 6 (IPv6) Specification + +Abstract + + This document specifies version 6 of the Internet Protocol (IPv6). + It obsoletes RFC 2460. + +Status of This Memo + + This is an Internet Standards Track document. + + This document is a product of the Internet Engineering Task Force + (IETF). It represents the consensus of the IETF community. It has + received public review and has been approved for publication by the + Internet Engineering Steering Group (IESG). Further information on + Internet Standards is available in Section 2 of RFC 7841. + + Information about the current status of this document, any errata, + and how to provide feedback on it may be obtained at + http://www.rfc-editor.org/info/rfc8200. + + + + + + + + + + + + + + + + + + + + + + + +Deering & Hinden Standards Track [Page 1] + +RFC 8200 IPv6 Specification July 2017 + + +Copyright Notice + + Copyright (c) 2017 IETF Trust and the persons identified as the + document authors. All rights reserved. + + This document is subject to BCP 78 and the IETF Trust's Legal + Provisions Relating to IETF Documents + (http://trustee.ietf.org/license-info) in effect on the date of + publication of this document. Please review these documents + carefully, as they describe your rights and restrictions with respect + to this document. Code Components extracted from this document must + include Simplified BSD License text as described in Section 4.e of + the Trust Legal Provisions and are provided without warranty as + described in the Simplified BSD License. + + This document may contain material from IETF Documents or IETF + Contributions published or made publicly available before November + 10, 2008. The person(s) controlling the copyright in some of this + material may not have granted the IETF Trust the right to allow + modifications of such material outside the IETF Standards Process. + Without obtaining an adequate license from the person(s) controlling + the copyright in such materials, this document may not be modified + outside the IETF Standards Process, and derivative works of it may + not be created outside the IETF Standards Process, except to format + it for publication as an RFC or to translate it into languages other + than English. + + + + + + + + + + + + + + + + + + + + + + + + + +Deering & Hinden Standards Track [Page 2] + +RFC 8200 IPv6 Specification July 2017 + + +Table of Contents + + 1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . 4 + 2. Terminology . . . . . . . . . . . . . . . . . . . . . . . . . 5 + 3. IPv6 Header Format . . . . . . . . . . . . . . . . . . . . . 6 + 4. IPv6 Extension Headers . . . . . . . . . . . . . . . . . . . 7 + 4.1. Extension Header Order . . . . . . . . . . . . . . . . . 10 + 4.2. Options . . . . . . . . . . . . . . . . . . . . . . . . . 11 + 4.3. Hop-by-Hop Options Header . . . . . . . . . . . . . . . . 13 + 4.4. Routing Header . . . . . . . . . . . . . . . . . . . . . 14 + 4.5. Fragment Header . . . . . . . . . . . . . . . . . . . . . 15 + 4.6. Destination Options Header . . . . . . . . . . . . . . . 23 + 4.7. No Next Header . . . . . . . . . . . . . . . . . . . . . 24 + 4.8. Defining New Extension Headers and Options . . . . . . . 24 + 5. Packet Size Issues . . . . . . . . . . . . . . . . . . . . . 25 + 6. Flow Labels . . . . . . . . . . . . . . . . . . . . . . . . . 26 + 7. Traffic Classes . . . . . . . . . . . . . . . . . . . . . . . 26 + 8. Upper-Layer Protocol Issues . . . . . . . . . . . . . . . . . 27 + 8.1. Upper-Layer Checksums . . . . . . . . . . . . . . . . . . 27 + 8.2. Maximum Packet Lifetime . . . . . . . . . . . . . . . . . 28 + 8.3. Maximum Upper-Layer Payload Size . . . . . . . . . . . . 29 + 8.4. Responding to Packets Carrying Routing Headers . . . . . 29 + 9. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 29 + 10. Security Considerations . . . . . . . . . . . . . . . . . . . 30 + 11. References . . . . . . . . . . . . . . . . . . . . . . . . . 32 + 11.1. Normative References . . . . . . . . . . . . . . . . . . 32 + 11.2. Informative References . . . . . . . . . . . . . . . . . 33 + Appendix A. Formatting Guidelines for Options . . . . . . . . . 36 + Appendix B. Changes Since RFC 2460 . . . . . . . . . . . . . . . 39 + Acknowledgments . . . . . . . . . . . . . . . . . . . . . . . . . 42 + Authors' Addresses . . . . . . . . . . . . . . . . . . . . . . . 42 + + + + + + + + + + + + + + + + + + + + +Deering & Hinden Standards Track [Page 3] + +RFC 8200 IPv6 Specification July 2017 + + +1. Introduction + + IP version 6 (IPv6) is a new version of the Internet Protocol (IP), + designed as the successor to IP version 4 (IPv4) [RFC791]. The + changes from IPv4 to IPv6 fall primarily into the following + categories: + + o Expanded Addressing Capabilities + + IPv6 increases the IP address size from 32 bits to 128 bits, to + support more levels of addressing hierarchy, a much greater + number of addressable nodes, and simpler autoconfiguration of + addresses. The scalability of multicast routing is improved by + adding a "scope" field to multicast addresses. And a new type + of address called an "anycast address" is defined; it is used + to send a packet to any one of a group of nodes. + + o Header Format Simplification + + Some IPv4 header fields have been dropped or made optional, to + reduce the common-case processing cost of packet handling and + to limit the bandwidth cost of the IPv6 header. + + o Improved Support for Extensions and Options + + Changes in the way IP header options are encoded allows for + more efficient forwarding, less stringent limits on the length + of options, and greater flexibility for introducing new options + in the future. + + o Flow Labeling Capability + + A new capability is added to enable the labeling of sequences + of packets that the sender requests to be treated in the + network as a single flow. + + o Authentication and Privacy Capabilities + + Extensions to support authentication, data integrity, and + (optional) data confidentiality are specified for IPv6. + + This document specifies the basic IPv6 header and the initially + defined IPv6 extension headers and options. It also discusses packet + size issues, the semantics of flow labels and traffic classes, and + the effects of IPv6 on upper-layer protocols. The format and + semantics of IPv6 addresses are specified separately in [RFC4291]. + The IPv6 version of ICMP, which all IPv6 implementations are required + to include, is specified in [RFC4443]. + + + +Deering & Hinden Standards Track [Page 4] + +RFC 8200 IPv6 Specification July 2017 + + + The data transmission order for IPv6 is the same as for IPv4 as + defined in Appendix B of [RFC791]. + + Note: As this document obsoletes [RFC2460], any document referenced + in this document that includes pointers to RFC 2460 should be + interpreted as referencing this document. + +2. Terminology + + node a device that implements IPv6. + + router a node that forwards IPv6 packets not explicitly + addressed to itself. (See Note below.) + + host any node that is not a router. (See Note below.) + + upper layer a protocol layer immediately above IPv6. Examples are + transport protocols such as TCP and UDP, control + protocols such as ICMP, routing protocols such as OSPF, + and internet-layer or lower-layer protocols being + "tunneled" over (i.e., encapsulated in) IPv6 such as + Internetwork Packet Exchange (IPX), AppleTalk, or IPv6 + itself. + + link a communication facility or medium over which nodes can + communicate at the link layer, i.e., the layer + immediately below IPv6. Examples are Ethernets (simple + or bridged); PPP links; X.25, Frame Relay, or ATM + networks; and internet-layer or higher-layer "tunnels", + such as tunnels over IPv4 or IPv6 itself. + + neighbors nodes attached to the same link. + + interface a node's attachment to a link. + + address an IPv6-layer identifier for an interface or a set of + interfaces. + + packet an IPv6 header plus payload. + + link MTU the maximum transmission unit, i.e., maximum packet size + in octets, that can be conveyed over a link. + + path MTU the minimum link MTU of all the links in a path between + a source node and a destination node. + + + + + + +Deering & Hinden Standards Track [Page 5] + +RFC 8200 IPv6 Specification July 2017 + + + Note: it is possible for a device with multiple interfaces to be + configured to forward non-self-destined packets arriving from some + set (fewer than all) of its interfaces and to discard non-self- + destined packets arriving from its other interfaces. Such a device + must obey the protocol requirements for routers when receiving + packets from, and interacting with neighbors over, the former + (forwarding) interfaces. It must obey the protocol requirements for + hosts when receiving packets from, and interacting with neighbors + over, the latter (non-forwarding) interfaces. + +3. IPv6 Header Format + + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |Version| Traffic Class | Flow Label | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Payload Length | Next Header | Hop Limit | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + + + | | + + Source Address + + | | + + + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + + + | | + + Destination Address + + | | + + + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Version 4-bit Internet Protocol version number = 6. + + Traffic Class 8-bit Traffic Class field. See Section 7. + + Flow Label 20-bit flow label. See Section 6. + + Payload Length 16-bit unsigned integer. Length of the IPv6 + payload, i.e., the rest of the packet + following this IPv6 header, in octets. (Note + that any extension headers (see Section 4) + present are considered part of the payload, + i.e., included in the length count.) + + + + + +Deering & Hinden Standards Track [Page 6] + +RFC 8200 IPv6 Specification July 2017 + + + Next Header 8-bit selector. Identifies the type of header + immediately following the IPv6 header. Uses + the same values as the IPv4 Protocol field + [IANA-PN]. + + Hop Limit 8-bit unsigned integer. Decremented by 1 by + each node that forwards the packet. When + forwarding, the packet is discarded if Hop + Limit was zero when received or is decremented + to zero. A node that is the destination of a + packet should not discard a packet with Hop + Limit equal to zero; it should process the + packet normally. + + Source Address 128-bit address of the originator of the + packet. See [RFC4291]. + + Destination Address 128-bit address of the intended recipient of + the packet (possibly not the ultimate + recipient, if a Routing header is present). + See [RFC4291] and Section 4.4. + +4. IPv6 Extension Headers + + In IPv6, optional internet-layer information is encoded in separate + headers that may be placed between the IPv6 header and the upper- + layer header in a packet. There is a small number of such extension + headers, each one identified by a distinct Next Header value. + + Extension headers are numbered from IANA IP Protocol Numbers + [IANA-PN], the same values used for IPv4 and IPv6. When processing a + sequence of Next Header values in a packet, the first one that is not + an extension header [IANA-EH] indicates that the next item in the + packet is the corresponding upper-layer header. A special "No Next + Header" value is used if there is no upper-layer header. + + + + + + + + + + + + + + + + +Deering & Hinden Standards Track [Page 7] + +RFC 8200 IPv6 Specification July 2017 + + + As illustrated in these examples, an IPv6 packet may carry zero, one, + or more extension headers, each identified by the Next Header field + of the preceding header: + + +---------------+------------------------ + | IPv6 header | TCP header + data + | | + | Next Header = | + | TCP | + +---------------+------------------------ + + +---------------+----------------+------------------------ + | IPv6 header | Routing header | TCP header + data + | | | + | Next Header = | Next Header = | + | Routing | TCP | + +---------------+----------------+------------------------ + + +---------------+----------------+-----------------+----------------- + | IPv6 header | Routing header | Fragment header | fragment of TCP + | | | | header + data + | Next Header = | Next Header = | Next Header = | + | Routing | Fragment | TCP | + +---------------+----------------+-----------------+----------------- + + Extension headers (except for the Hop-by-Hop Options header) are not + processed, inserted, or deleted by any node along a packet's delivery + path, until the packet reaches the node (or each of the set of nodes, + in the case of multicast) identified in the Destination Address field + of the IPv6 header. + + The Hop-by-Hop Options header is not inserted or deleted, but may be + examined or processed by any node along a packet's delivery path, + until the packet reaches the node (or each of the set of nodes, in + the case of multicast) identified in the Destination Address field of + the IPv6 header. The Hop-by-Hop Options header, when present, must + immediately follow the IPv6 header. Its presence is indicated by the + value zero in the Next Header field of the IPv6 header. + + NOTE: While [RFC2460] required that all nodes must examine and + process the Hop-by-Hop Options header, it is now expected that nodes + along a packet's delivery path only examine and process the + Hop-by-Hop Options header if explicitly configured to do so. + + + + + + + + +Deering & Hinden Standards Track [Page 8] + +RFC 8200 IPv6 Specification July 2017 + + + At the destination node, normal demultiplexing on the Next Header + field of the IPv6 header invokes the module to process the first + extension header, or the upper-layer header if no extension header is + present. The contents and semantics of each extension header + determine whether or not to proceed to the next header. Therefore, + extension headers must be processed strictly in the order they appear + in the packet; a receiver must not, for example, scan through a + packet looking for a particular kind of extension header and process + that header prior to processing all preceding ones. + + If, as a result of processing a header, the destination node is + required to proceed to the next header but the Next Header value in + the current header is unrecognized by the node, it should discard the + packet and send an ICMP Parameter Problem message to the source of + the packet, with an ICMP Code value of 1 ("unrecognized Next Header + type encountered") and the ICMP Pointer field containing the offset + of the unrecognized value within the original packet. The same + action should be taken if a node encounters a Next Header value of + zero in any header other than an IPv6 header. + + Each extension header is an integer multiple of 8 octets long, in + order to retain 8-octet alignment for subsequent headers. Multi- + octet fields within each extension header are aligned on their + natural boundaries, i.e., fields of width n octets are placed at an + integer multiple of n octets from the start of the header, for n = 1, + 2, 4, or 8. + + A full implementation of IPv6 includes implementation of the + following extension headers: + + Hop-by-Hop Options + Fragment + Destination Options + Routing + Authentication + Encapsulating Security Payload + + The first four are specified in this document; the last two are + specified in [RFC4302] and [RFC4303], respectively. The current list + of IPv6 extension headers can be found at [IANA-EH]. + + + + + + + + + + + +Deering & Hinden Standards Track [Page 9] + +RFC 8200 IPv6 Specification July 2017 + + +4.1. Extension Header Order + + When more than one extension header is used in the same packet, it is + recommended that those headers appear in the following order: + + IPv6 header + Hop-by-Hop Options header + Destination Options header (note 1) + Routing header + Fragment header + Authentication header (note 2) + Encapsulating Security Payload header (note 2) + Destination Options header (note 3) + Upper-Layer header + + note 1: for options to be processed by the first destination that + appears in the IPv6 Destination Address field plus + subsequent destinations listed in the Routing header. + + note 2: additional recommendations regarding the relative order of + the Authentication and Encapsulating Security Payload + headers are given in [RFC4303]. + + note 3: for options to be processed only by the final destination + of the packet. + + Each extension header should occur at most once, except for the + Destination Options header, which should occur at most twice (once + before a Routing header and once before the upper-layer header). + + If the upper-layer header is another IPv6 header (in the case of IPv6 + being tunneled over or encapsulated in IPv6), it may be followed by + its own extension headers, which are separately subject to the same + ordering recommendations. + + If and when other extension headers are defined, their ordering + constraints relative to the above listed headers must be specified. + + IPv6 nodes must accept and attempt to process extension headers in + any order and occurring any number of times in the same packet, + except for the Hop-by-Hop Options header, which is restricted to + appear immediately after an IPv6 header only. Nonetheless, it is + strongly advised that sources of IPv6 packets adhere to the above + recommended order until and unless subsequent specifications revise + that recommendation. + + + + + + +Deering & Hinden Standards Track [Page 10] + +RFC 8200 IPv6 Specification July 2017 + + +4.2. Options + + Two of the currently defined extension headers specified in this + document -- the Hop-by-Hop Options header and the Destination Options + header -- carry a variable number of "options" that are type-length- + value (TLV) encoded in the following format: + + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- - - - - - - - - + | Option Type | Opt Data Len | Option Data + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- - - - - - - - - + + Option Type 8-bit identifier of the type of option. + + Opt Data Len 8-bit unsigned integer. Length of the Option + Data field of this option, in octets. + + Option Data Variable-length field. Option-Type-specific + data. + + The sequence of options within a header must be processed strictly in + the order they appear in the header; a receiver must not, for + example, scan through the header looking for a particular kind of + option and process that option prior to processing all preceding + ones. + + The Option Type identifiers are internally encoded such that their + highest-order 2 bits specify the action that must be taken if the + processing IPv6 node does not recognize the Option Type: + + 00 - skip over this option and continue processing the header. + + 01 - discard the packet. + + 10 - discard the packet and, regardless of whether or not the + packet's Destination Address was a multicast address, send an + ICMP Parameter Problem, Code 2, message to the packet's + Source Address, pointing to the unrecognized Option Type. + + 11 - discard the packet and, only if the packet's Destination + Address was not a multicast address, send an ICMP Parameter + Problem, Code 2, message to the packet's Source Address, + pointing to the unrecognized Option Type. + + The third-highest-order bit of the Option Type specifies whether or + not the Option Data of that option can change en route to the + packet's final destination. When an Authentication header is present + + + + + +Deering & Hinden Standards Track [Page 11] + +RFC 8200 IPv6 Specification July 2017 + + + in the packet, for any option whose data may change en route, its + entire Option Data field must be treated as zero-valued octets when + computing or verifying the packet's authenticating value. + + 0 - Option Data does not change en route + + 1 - Option Data may change en route + + The three high-order bits described above are to be treated as part + of the Option Type, not independent of the Option Type. That is, a + particular option is identified by a full 8-bit Option Type, not just + the low-order 5 bits of an Option Type. + + The same Option Type numbering space is used for both the Hop-by-Hop + Options header and the Destination Options header. However, the + specification of a particular option may restrict its use to only one + of those two headers. + + Individual options may have specific alignment requirements, to + ensure that multi-octet values within Option Data fields fall on + natural boundaries. The alignment requirement of an option is + specified using the notation xn+y, meaning the Option Type must + appear at an integer multiple of x octets from the start of the + header, plus y octets. For example: + + 2n means any 2-octet offset from the start of the header. + 8n+2 means any 8-octet offset from the start of the header, plus + 2 octets. + + There are two padding options that are used when necessary to align + subsequent options and to pad out the containing header to a multiple + of 8 octets in length. These padding options must be recognized by + all IPv6 implementations: + + Pad1 option (alignment requirement: none) + + +-+-+-+-+-+-+-+-+ + | 0 | + +-+-+-+-+-+-+-+-+ + + NOTE! the format of the Pad1 option is a special case -- it does + not have length and value fields. + + The Pad1 option is used to insert 1 octet of padding into the + Options area of a header. If more than one octet of padding is + required, the PadN option, described next, should be used, rather + than multiple Pad1 options. + + + + +Deering & Hinden Standards Track [Page 12] + +RFC 8200 IPv6 Specification July 2017 + + + PadN option (alignment requirement: none) + + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- - - - - - - - - + | 1 | Opt Data Len | Option Data + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- - - - - - - - - + + The PadN option is used to insert two or more octets of padding + into the Options area of a header. For N octets of padding, the + Opt Data Len field contains the value N-2, and the Option Data + consists of N-2 zero-valued octets. + + Appendix A contains formatting guidelines for designing new options. + +4.3. Hop-by-Hop Options Header + + The Hop-by-Hop Options header is used to carry optional information + that may be examined and processed by every node along a packet's + delivery path. The Hop-by-Hop Options header is identified by a Next + Header value of 0 in the IPv6 header and has the following format: + + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Next Header | Hdr Ext Len | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + | | + . . + . Options . + . . + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + + Next Header 8-bit selector. Identifies the type of header + immediately following the Hop-by-Hop Options + header. Uses the same values as the IPv4 + Protocol field [IANA-PN]. + + Hdr Ext Len 8-bit unsigned integer. Length of the + Hop-by-Hop Options header in 8-octet units, + not including the first 8 octets. + + Options Variable-length field, of length such that the + complete Hop-by-Hop Options header is an + integer multiple of 8 octets long. Contains + one or more TLV-encoded options, as described + in Section 4.2. + + The only hop-by-hop options defined in this document are the Pad1 and + PadN options specified in Section 4.2. + + + +Deering & Hinden Standards Track [Page 13] + +RFC 8200 IPv6 Specification July 2017 + + +4.4. Routing Header + + The Routing header is used by an IPv6 source to list one or more + intermediate nodes to be "visited" on the way to a packet's + destination. This function is very similar to IPv4's Loose Source + and Record Route option. The Routing header is identified by a Next + Header value of 43 in the immediately preceding header and has the + following format: + + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Next Header | Hdr Ext Len | Routing Type | Segments Left | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + . . + . type-specific data . + . . + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Next Header 8-bit selector. Identifies the type of header + immediately following the Routing header. + Uses the same values as the IPv4 Protocol + field [IANA-PN]. + + Hdr Ext Len 8-bit unsigned integer. Length of the Routing + header in 8-octet units, not including the + first 8 octets. + + Routing Type 8-bit identifier of a particular Routing + header variant. + + Segments Left 8-bit unsigned integer. Number of route + segments remaining, i.e., number of explicitly + listed intermediate nodes still to be visited + before reaching the final destination. + + type-specific data Variable-length field, of format determined by + the Routing Type, and of length such that the + complete Routing header is an integer multiple + of 8 octets long. + + + + + + + + + + + +Deering & Hinden Standards Track [Page 14] + +RFC 8200 IPv6 Specification July 2017 + + + If, while processing a received packet, a node encounters a Routing + header with an unrecognized Routing Type value, the required behavior + of the node depends on the value of the Segments Left field, as + follows: + + If Segments Left is zero, the node must ignore the Routing header + and proceed to process the next header in the packet, whose type + is identified by the Next Header field in the Routing header. + + If Segments Left is non-zero, the node must discard the packet and + send an ICMP Parameter Problem, Code 0, message to the packet's + Source Address, pointing to the unrecognized Routing Type. + + If, after processing a Routing header of a received packet, an + intermediate node determines that the packet is to be forwarded onto + a link whose link MTU is less than the size of the packet, the node + must discard the packet and send an ICMP Packet Too Big message to + the packet's Source Address. + + The currently defined IPv6 Routing Headers and their status can be + found at [IANA-RH]. Allocation guidelines for IPv6 Routing Headers + can be found in [RFC5871]. + +4.5. Fragment Header + + The Fragment header is used by an IPv6 source to send a packet larger + than would fit in the path MTU to its destination. (Note: unlike + IPv4, fragmentation in IPv6 is performed only by source nodes, not by + routers along a packet's delivery path -- see Section 5.) The + Fragment header is identified by a Next Header value of 44 in the + immediately preceding header and has the following format: + + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Next Header | Reserved | Fragment Offset |Res|M| + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Identification | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Next Header 8-bit selector. Identifies the initial header + type of the Fragmentable Part of the original + packet (defined below). Uses the same values + as the IPv4 Protocol field [IANA-PN]. + + Reserved 8-bit reserved field. Initialized to zero for + transmission; ignored on reception. + + + + + + +Deering & Hinden Standards Track [Page 15] + +RFC 8200 IPv6 Specification July 2017 + + + Fragment Offset 13-bit unsigned integer. The offset, in + 8-octet units, of the data following this + header, relative to the start of the + Fragmentable Part of the original packet. + + Res 2-bit reserved field. Initialized to zero for + transmission; ignored on reception. + + M flag 1 = more fragments; 0 = last fragment. + + Identification 32 bits. See description below. + + In order to send a packet that is too large to fit in the MTU of the + path to its destination, a source node may divide the packet into + fragments and send each fragment as a separate packet, to be + reassembled at the receiver. + + For every packet that is to be fragmented, the source node generates + an Identification value. The Identification must be different than + that of any other fragmented packet sent recently* with the same + Source Address and Destination Address. If a Routing header is + present, the Destination Address of concern is that of the final + destination. + + * "recently" means within the maximum likely lifetime of a + packet, including transit time from source to destination and + time spent awaiting reassembly with other fragments of the same + packet. However, it is not required that a source node knows + the maximum packet lifetime. Rather, it is assumed that the + requirement can be met by implementing an algorithm that + results in a low identification reuse frequency. Examples of + algorithms that can meet this requirement are described in + [RFC7739]. + + + + + + + + + + + + + + + + + + +Deering & Hinden Standards Track [Page 16] + +RFC 8200 IPv6 Specification July 2017 + + + The initial, large, unfragmented packet is referred to as the + "original packet", and it is considered to consist of three parts, as + illustrated: + + original packet: + + +------------------+-------------------------+---//----------------+ + | Per-Fragment | Extension & Upper-Layer | Fragmentable | + | Headers | Headers | Part | + +------------------+-------------------------+---//----------------+ + + The Per-Fragment headers must consist of the IPv6 header plus any + extension headers that must be processed by nodes en route to the + destination, that is, all headers up to and including the Routing + header if present, else the Hop-by-Hop Options header if present, + else no extension headers. + + The Extension headers are all other extension headers that are not + included in the Per-Fragment headers part of the packet. For this + purpose, the Encapsulating Security Payload (ESP) is not + considered an extension header. The Upper-Layer header is the + first upper-layer header that is not an IPv6 extension header. + Examples of upper-layer headers include TCP, UDP, IPv4, IPv6, + ICMPv6, and as noted ESP. + + The Fragmentable Part consists of the rest of the packet after the + upper-layer header or after any header (i.e., initial IPv6 header + or extension header) that contains a Next Header value of No Next + Header. + + The Fragmentable Part of the original packet is divided into + fragments. The lengths of the fragments must be chosen such that the + resulting fragment packets fit within the MTU of the path to the + packet's destination(s). Each complete fragment, except possibly the + last ("rightmost") one, is an integer multiple of 8 octets long. + + + + + + + + + + + + + + + + +Deering & Hinden Standards Track [Page 17] + +RFC 8200 IPv6 Specification July 2017 + + + The fragments are transmitted in separate "fragment packets" as + illustrated: + + original packet: + + +-----------------+-----------------+--------+--------+-//-+--------+ + | Per-Fragment |Ext & Upper-Layer| first | second | | last | + | Headers | Headers |fragment|fragment|....|fragment| + +-----------------+-----------------+--------+--------+-//-+--------+ + + fragment packets: + + +------------------+---------+-------------------+----------+ + | Per-Fragment |Fragment | Ext & Upper-Layer | first | + | Headers | Header | Headers | fragment | + +------------------+---------+-------------------+----------+ + + +------------------+--------+-------------------------------+ + | Per-Fragment |Fragment| second | + | Headers | Header | fragment | + +------------------+--------+-------------------------------+ + o + o + o + +------------------+--------+----------+ + | Per-Fragment |Fragment| last | + | Headers | Header | fragment | + +------------------+--------+----------+ + + The first fragment packet is composed of: + + (1) The Per-Fragment headers of the original packet, with the + Payload Length of the original IPv6 header changed to contain + the length of this fragment packet only (excluding the length + of the IPv6 header itself), and the Next Header field of the + last header of the Per-Fragment headers changed to 44. + + (2) A Fragment header containing: + + The Next Header value that identifies the first header + after the Per-Fragment headers of the original packet. + + A Fragment Offset containing the offset of the fragment, + in 8-octet units, relative to the start of the + Fragmentable Part of the original packet. The Fragment + Offset of the first ("leftmost") fragment is 0. + + An M flag value of 1 as this is the first fragment. + + + +Deering & Hinden Standards Track [Page 18] + +RFC 8200 IPv6 Specification July 2017 + + + The Identification value generated for the original + packet. + + (3) Extension headers, if any, and the Upper-Layer header. These + headers must be in the first fragment. Note: This restricts + the size of the headers through the Upper-Layer header to the + MTU of the path to the packet's destinations(s). + + (4) The first fragment. + + The subsequent fragment packets are composed of: + + (1) The Per-Fragment headers of the original packet, with the + Payload Length of the original IPv6 header changed to contain + the length of this fragment packet only (excluding the length + of the IPv6 header itself), and the Next Header field of the + last header of the Per-Fragment headers changed to 44. + + (2) A Fragment header containing: + + The Next Header value that identifies the first header + after the Per-Fragment headers of the original packet. + + A Fragment Offset containing the offset of the fragment, + in 8-octet units, relative to the start of the + Fragmentable Part of the original packet. + + An M flag value of 0 if the fragment is the last + ("rightmost") one, else an M flag value of 1. + + The Identification value generated for the original + packet. + + (3) The fragment itself. + + Fragments must not be created that overlap with any other fragments + created from the original packet. + + + + + + + + + + + + + + +Deering & Hinden Standards Track [Page 19] + +RFC 8200 IPv6 Specification July 2017 + + + At the destination, fragment packets are reassembled into their + original, unfragmented form, as illustrated: + + reassembled original packet: + + +---------------+-----------------+---------+--------+-//--+--------+ + | Per-Fragment |Ext & Upper-Layer| first | second | | last | + | Headers | Headers |frag data|fragment|.....|fragment| + +---------------+-----------------+---------+--------+-//--+--------+ + + The following rules govern reassembly: + + An original packet is reassembled only from fragment packets that + have the same Source Address, Destination Address, and Fragment + Identification. + + The Per-Fragment headers of the reassembled packet consists of all + headers up to, but not including, the Fragment header of the first + fragment packet (that is, the packet whose Fragment Offset is + zero), with the following two changes: + + The Next Header field of the last header of the Per-Fragment + headers is obtained from the Next Header field of the first + fragment's Fragment header. + + The Payload Length of the reassembled packet is computed from + the length of the Per-Fragment headers and the length and + offset of the last fragment. For example, a formula for + computing the Payload Length of the reassembled original packet + is: + + PL.orig = PL.first - FL.first - 8 + (8 * FO.last) + FL.last + + where + PL.orig = Payload Length field of reassembled packet. + PL.first = Payload Length field of first fragment packet. + FL.first = length of fragment following Fragment header of + first fragment packet. + FO.last = Fragment Offset field of Fragment header of last + fragment packet. + FL.last = length of fragment following Fragment header of + last fragment packet. + + The Fragmentable Part of the reassembled packet is constructed + from the fragments following the Fragment headers in each of + the fragment packets. The length of each fragment is computed + by subtracting from the packet's Payload Length the length of + the headers between the IPv6 header and fragment itself; its + + + +Deering & Hinden Standards Track [Page 20] + +RFC 8200 IPv6 Specification July 2017 + + + relative position in Fragmentable Part is computed from its + Fragment Offset value. + + The Fragment header is not present in the final, reassembled + packet. + + If the fragment is a whole datagram (that is, both the Fragment + Offset field and the M flag are zero), then it does not need + any further reassembly and should be processed as a fully + reassembled packet (i.e., updating Next Header, adjust Payload + Length, removing the Fragment header, etc.). Any other + fragments that match this packet (i.e., the same IPv6 Source + Address, IPv6 Destination Address, and Fragment Identification) + should be processed independently. + + The following error conditions may arise when reassembling fragmented + packets: + + o If insufficient fragments are received to complete reassembly + of a packet within 60 seconds of the reception of the first- + arriving fragment of that packet, reassembly of that packet + must be abandoned and all the fragments that have been received + for that packet must be discarded. If the first fragment + (i.e., the one with a Fragment Offset of zero) has been + received, an ICMP Time Exceeded -- Fragment Reassembly Time + Exceeded message should be sent to the source of that fragment. + + o If the length of a fragment, as derived from the fragment + packet's Payload Length field, is not a multiple of 8 octets + and the M flag of that fragment is 1, then that fragment must + be discarded and an ICMP Parameter Problem, Code 0, message + should be sent to the source of the fragment, pointing to the + Payload Length field of the fragment packet. + + o If the length and offset of a fragment are such that the + Payload Length of the packet reassembled from that fragment + would exceed 65,535 octets, then that fragment must be + discarded and an ICMP Parameter Problem, Code 0, message should + be sent to the source of the fragment, pointing to the Fragment + Offset field of the fragment packet. + + o If the first fragment does not include all headers through an + Upper-Layer header, then that fragment should be discarded and + an ICMP Parameter Problem, Code 3, message should be sent to + the source of the fragment, with the Pointer field set to zero. + + + + + + +Deering & Hinden Standards Track [Page 21] + +RFC 8200 IPv6 Specification July 2017 + + + o If any of the fragments being reassembled overlap with any + other fragments being reassembled for the same packet, + reassembly of that packet must be abandoned and all the + fragments that have been received for that packet must be + discarded, and no ICMP error messages should be sent. + + It should be noted that fragments may be duplicated in the + network. Instead of treating these exact duplicate fragments + as overlapping fragments, an implementation may choose to + detect this case and drop exact duplicate fragments while + keeping the other fragments belonging to the same packet. + + The following conditions are not expected to occur frequently but are + not considered errors if they do: + + The number and content of the headers preceding the Fragment + header of different fragments of the same original packet may + differ. Whatever headers are present, preceding the Fragment + header in each fragment packet, are processed when the packets + arrive, prior to queueing the fragments for reassembly. Only + those headers in the Offset zero fragment packet are retained in + the reassembled packet. + + The Next Header values in the Fragment headers of different + fragments of the same original packet may differ. Only the value + from the Offset zero fragment packet is used for reassembly. + + Other fields in the IPv6 header may also vary across the fragments + being reassembled. Specifications that use these fields may + provide additional instructions if the basic mechanism of using + the values from the Offset zero fragment is not sufficient. For + example, Section 5.3 of [RFC3168] describes how to combine the + Explicit Congestion Notification (ECN) bits from different + fragments to derive the ECN bits of the reassembled packet. + + + + + + + + + + + + + + + + + +Deering & Hinden Standards Track [Page 22] + +RFC 8200 IPv6 Specification July 2017 + + +4.6. Destination Options Header + + The Destination Options header is used to carry optional information + that need be examined only by a packet's destination node(s). The + Destination Options header is identified by a Next Header value of 60 + in the immediately preceding header and has the following format: + + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Next Header | Hdr Ext Len | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + | | + . . + . Options . + . . + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Next Header 8-bit selector. Identifies the type of header + immediately following the Destination Options + header. Uses the same values as the IPv4 + Protocol field [IANA-PN]. + + Hdr Ext Len 8-bit unsigned integer. Length of the + Destination Options header in 8-octet units, + not including the first 8 octets. + + Options Variable-length field, of length such that the + complete Destination Options header is an + integer multiple of 8 octets long. Contains + one or more TLV-encoded options, as described + in Section 4.2. + + The only destination options defined in this document are the Pad1 + and PadN options specified in Section 4.2. + + Note that there are two possible ways to encode optional destination + information in an IPv6 packet: either as an option in the Destination + Options header or as a separate extension header. The Fragment + header and the Authentication header are examples of the latter + approach. Which approach can be used depends on what action is + desired of a destination node that does not understand the optional + information: + + o If the desired action is for the destination node to discard + the packet and, only if the packet's Destination Address is not + a multicast address, send an ICMP Unrecognized Type message to + the packet's Source Address, then the information may be + encoded either as a separate header or as an option in the + + + +Deering & Hinden Standards Track [Page 23] + +RFC 8200 IPv6 Specification July 2017 + + + Destination Options header whose Option Type has the value 11 + in its highest-order 2 bits. The choice may depend on such + factors as which takes fewer octets, or which yields better + alignment or more efficient parsing. + + o If any other action is desired, the information must be encoded + as an option in the Destination Options header whose Option + Type has the value 00, 01, or 10 in its highest-order 2 bits, + specifying the desired action (see Section 4.2). + +4.7. No Next Header + + The value 59 in the Next Header field of an IPv6 header or any + extension header indicates that there is nothing following that + header. If the Payload Length field of the IPv6 header indicates the + presence of octets past the end of a header whose Next Header field + contains 59, those octets must be ignored and passed on unchanged if + the packet is forwarded. + +4.8. Defining New Extension Headers and Options + + Defining new IPv6 extension headers is not recommended, unless there + are no existing IPv6 extension headers that can be used by specifying + a new option for that IPv6 extension header. A proposal to specify a + new IPv6 extension header must include a detailed technical + explanation of why an existing IPv6 extension header can not be used + for the desired new function. See [RFC6564] for additional + background information. + + Note: New extension headers that require hop-by-hop behavior must not + be defined because, as specified in Section 4 of this document, the + only extension header that has hop-by-hop behavior is the Hop-by-Hop + Options header. + + New hop-by-hop options are not recommended because nodes may be + configured to ignore the Hop-by-Hop Options header, drop packets + containing a Hop-by-Hop Options header, or assign packets containing + a Hop-by-Hop Options header to a slow processing path. Designers + considering defining new hop-by-hop options need to be aware of this + likely behavior. There has to be a very clear justification why any + new hop-by-hop option is needed before it is standardized. + + Instead of defining new extension headers, it is recommended that the + Destination Options header is used to carry optional information that + must be examined only by a packet's destination node(s), because they + provide better handling and backward compatibility. + + + + + +Deering & Hinden Standards Track [Page 24] + +RFC 8200 IPv6 Specification July 2017 + + + If new extension headers are defined, they need to use the following + format: + + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Next Header | Hdr Ext Len | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + | | + . . + . Header-Specific Data . + . . + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Next Header 8-bit selector. Identifies the type of + header immediately following the extension + header. Uses the same values as the IPv4 + Protocol field [IANA-PN]. + + Hdr Ext Len 8-bit unsigned integer. Length of the + Destination Options header in 8-octet units, + not including the first 8 octets. + + Header Specific Data Variable-length field. Fields specific to + the extension header. + +5. Packet Size Issues + + IPv6 requires that every link in the Internet have an MTU of 1280 + octets or greater. This is known as the IPv6 minimum link MTU. On + any link that cannot convey a 1280-octet packet in one piece, link- + specific fragmentation and reassembly must be provided at a layer + below IPv6. + + Links that have a configurable MTU (for example, PPP links [RFC1661]) + must be configured to have an MTU of at least 1280 octets; it is + recommended that they be configured with an MTU of 1500 octets or + greater, to accommodate possible encapsulations (i.e., tunneling) + without incurring IPv6-layer fragmentation. + + From each link to which a node is directly attached, the node must be + able to accept packets as large as that link's MTU. + + It is strongly recommended that IPv6 nodes implement Path MTU + Discovery [RFC8201], in order to discover and take advantage of path + MTUs greater than 1280 octets. However, a minimal IPv6 + implementation (e.g., in a boot ROM) may simply restrict itself to + sending packets no larger than 1280 octets, and omit implementation + of Path MTU Discovery. + + + +Deering & Hinden Standards Track [Page 25] + +RFC 8200 IPv6 Specification July 2017 + + + In order to send a packet larger than a path's MTU, a node may use + the IPv6 Fragment header to fragment the packet at the source and + have it reassembled at the destination(s). However, the use of such + fragmentation is discouraged in any application that is able to + adjust its packets to fit the measured path MTU (i.e., down to 1280 + octets). + + A node must be able to accept a fragmented packet that, after + reassembly, is as large as 1500 octets. A node is permitted to + accept fragmented packets that reassemble to more than 1500 octets. + An upper-layer protocol or application that depends on IPv6 + fragmentation to send packets larger than the MTU of a path should + not send packets larger than 1500 octets unless it has assurance that + the destination is capable of reassembling packets of that larger + size. + +6. Flow Labels + + The 20-bit Flow Label field in the IPv6 header is used by a source to + label sequences of packets to be treated in the network as a single + flow. + + The current definition of the IPv6 Flow Label can be found in + [RFC6437]. + +7. Traffic Classes + + The 8-bit Traffic Class field in the IPv6 header is used by the + network for traffic management. The value of the Traffic Class bits + in a received packet or fragment might be different from the value + sent by the packet's source. + + The current use of the Traffic Class field for Differentiated + Services and Explicit Congestion Notification is specified in + [RFC2474] and [RFC3168]. + + + + + + + + + + + + + + + + +Deering & Hinden Standards Track [Page 26] + +RFC 8200 IPv6 Specification July 2017 + + +8. Upper-Layer Protocol Issues + +8.1. Upper-Layer Checksums + + Any transport or other upper-layer protocol that includes the + addresses from the IP header in its checksum computation must be + modified for use over IPv6, to include the 128-bit IPv6 addresses + instead of 32-bit IPv4 addresses. In particular, the following + illustration shows the TCP and UDP "pseudo-header" for IPv6: + + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + + + | | + + Source Address + + | | + + + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + + + | | + + Destination Address + + | | + + + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Upper-Layer Packet Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | zero | Next Header | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + o If the IPv6 packet contains a Routing header, the Destination + Address used in the pseudo-header is that of the final + destination. At the originating node, that address will be in + the last element of the Routing header; at the recipient(s), + that address will be in the Destination Address field of the + IPv6 header. + + o The Next Header value in the pseudo-header identifies the + upper-layer protocol (e.g., 6 for TCP or 17 for UDP). It will + differ from the Next Header value in the IPv6 header if there + are extension headers between the IPv6 header and the upper- + layer header. + + + + + + + +Deering & Hinden Standards Track [Page 27] + +RFC 8200 IPv6 Specification July 2017 + + + o The Upper-Layer Packet Length in the pseudo-header is the + length of the upper-layer header and data (e.g., TCP header + plus TCP data). Some upper-layer protocols carry their own + length information (e.g., the Length field in the UDP header); + for such protocols, that is the length used in the pseudo- + header. Other protocols (such as TCP) do not carry their own + length information, in which case the length used in the + pseudo-header is the Payload Length from the IPv6 header, minus + the length of any extension headers present between the IPv6 + header and the upper-layer header. + + o Unlike IPv4, the default behavior when UDP packets are + originated by an IPv6 node is that the UDP checksum is not + optional. That is, whenever originating a UDP packet, an IPv6 + node must compute a UDP checksum over the packet and the + pseudo-header, and, if that computation yields a result of + zero, it must be changed to hex FFFF for placement in the UDP + header. IPv6 receivers must discard UDP packets containing a + zero checksum and should log the error. + + o As an exception to the default behavior, protocols that use UDP + as a tunnel encapsulation may enable zero-checksum mode for a + specific port (or set of ports) for sending and/or receiving. + Any node implementing zero-checksum mode must follow the + requirements specified in "Applicability Statement for the Use + of IPv6 UDP Datagrams with Zero Checksums" [RFC6936]. + + The IPv6 version of ICMP [RFC4443] includes the above pseudo-header + in its checksum computation; this is a change from the IPv4 version + of ICMP, which does not include a pseudo-header in its checksum. The + reason for the change is to protect ICMP from misdelivery or + corruption of those fields of the IPv6 header on which it depends, + which, unlike IPv4, are not covered by an internet-layer checksum. + The Next Header field in the pseudo-header for ICMP contains the + value 58, which identifies the IPv6 version of ICMP. + +8.2. Maximum Packet Lifetime + + Unlike IPv4, IPv6 nodes are not required to enforce maximum packet + lifetime. That is the reason the IPv4 "Time-to-Live" field was + renamed "Hop Limit" in IPv6. In practice, very few, if any, IPv4 + implementations conform to the requirement that they limit packet + lifetime, so this is not a change in practice. Any upper-layer + protocol that relies on the internet layer (whether IPv4 or IPv6) to + limit packet lifetime ought to be upgraded to provide its own + mechanisms for detecting and discarding obsolete packets. + + + + + +Deering & Hinden Standards Track [Page 28] + +RFC 8200 IPv6 Specification July 2017 + + +8.3. Maximum Upper-Layer Payload Size + + When computing the maximum payload size available for upper-layer + data, an upper-layer protocol must take into account the larger size + of the IPv6 header relative to the IPv4 header. For example, in + IPv4, TCP's Maximum Segment Size (MSS) option is computed as the + maximum packet size (a default value or a value learned through Path + MTU Discovery) minus 40 octets (20 octets for the minimum-length IPv4 + header and 20 octets for the minimum-length TCP header). When using + TCP over IPv6, the MSS must be computed as the maximum packet size + minus 60 octets, because the minimum-length IPv6 header (i.e., an + IPv6 header with no extension headers) is 20 octets longer than a + minimum-length IPv4 header. + +8.4. Responding to Packets Carrying Routing Headers + + When an upper-layer protocol sends one or more packets in response to + a received packet that included a Routing header, the response + packet(s) must not include a Routing header that was automatically + derived by "reversing" the received Routing header UNLESS the + integrity and authenticity of the received Source Address and Routing + header have been verified (e.g., via the use of an Authentication + header in the received packet). In other words, only the following + kinds of packets are permitted in response to a received packet + bearing a Routing header: + + o Response packets that do not carry Routing headers. + + o Response packets that carry Routing headers that were NOT + derived by reversing the Routing header of the received packet + (for example, a Routing header supplied by local + configuration). + + o Response packets that carry Routing headers that were derived + by reversing the Routing header of the received packet IF AND + ONLY IF the integrity and authenticity of the Source Address + and Routing header from the received packet have been verified + by the responder. + +9. IANA Considerations + + RFC 2460 is referenced in a number of IANA registries. These + include: + + o Internet Protocol Version 6 (IPv6) Parameters [IANA-6P] + + o Assigned Internet Protocol Numbers [IANA-PN] + + + + +Deering & Hinden Standards Track [Page 29] + +RFC 8200 IPv6 Specification July 2017 + + + o ONC RPC Network Identifiers (netids) [IANA-NI] + + o Network Layer Protocol Identifiers (NLPIDs) of Interest + [IANA-NL] + + o Protocol Registries [IANA-PR] + + The IANA has updated these references to point to this document. + +10. Security Considerations + + IPv6, from the viewpoint of the basic format and transmission of + packets, has security properties that are similar to IPv4. These + security issues include: + + o Eavesdropping, where on-path elements can observe the whole + packet (including both contents and metadata) of each IPv6 + datagram. + o Replay, where the attacker records a sequence of packets off of + the wire and plays them back to the party that originally + received them. + o Packet insertion, where the attacker forges a packet with some + chosen set of properties and injects it into the network. + o Packet deletion, where the attacker removes a packet from the + wire. + o Packet modification, where the attacker removes a packet from + the wire, modifies it, and reinjects it into the network. + o Man-in-the-middle (MITM) attacks, where the attacker subverts + the communication stream in order to pose as the sender to + receiver and the receiver to the sender. + o Denial-of-service (DoS) attacks, where the attacker sends large + amounts of legitimate traffic to a destination to overwhelm it. + + IPv6 packets can be protected from eavesdropping, replay, packet + insertion, packet modification, and MITM attacks by use of the + "Security Architecture for the Internet Protocol" [RFC4301]. In + addition, upper-layer protocols such as Transport Layer Security + (TLS) or Secure Shell (SSH) can be used to protect the application- + layer traffic running on top of IPv6. + + There is not any mechanism to protect against DoS attacks. Defending + against these type of attacks is outside the scope of this + specification. + + IPv6 addresses are significantly larger than IPv4 addresses making it + much harder to scan the address space across the Internet and even on + a single network link (e.g., Local Area Network). See [RFC7707] for + more information. + + + +Deering & Hinden Standards Track [Page 30] + +RFC 8200 IPv6 Specification July 2017 + + + IPv6 addresses of nodes are expected to be more visible on the + Internet as compared with IPv4 since the use of address translation + technology is reduced. This creates some additional privacy issues + such as making it easier to distinguish endpoints. See [RFC7721] for + more information. + + The design of IPv6 extension header architecture, while adding a lot + of flexibility, also creates new security challenges. As noted + below, issues relating to the Fragment extension header have been + resolved, but it's clear that for any new extension header designed + in the future, the security implications need to be examined + thoroughly, and this needs to include how the new extension header + works with existing extension headers. See [RFC7045] for more + information. + + This version of the IPv6 specification resolves a number of security + issues that were found with the previous version [RFC2460] of the + IPv6 specification. These include: + + o Revised the text to handle the case of fragments that are whole + datagrams (i.e., both the Fragment Offset field and the M flag + are zero). If received, they should be processed as a + reassembled packet. Any other fragments that match should be + processed independently. The Fragment creation process was + modified to not create whole datagram fragments (Fragment + Offset field and the M flag are zero). See [RFC6946] and + [RFC8021] for more information. + + o Removed the paragraph in Section 5 that required including a + Fragment header to outgoing packets if an ICMP Packet Too Big + message reporting a Next-Hop MTU is less than 1280. See + [RFC6946] for more information. + + o Changed the text to require that IPv6 nodes must not create + overlapping fragments. Also, when reassembling an IPv6 + datagram, if one or more of its constituent fragments is + determined to be an overlapping fragment, the entire datagram + (and any constituent fragments) must be silently discarded. + Includes clarification that no ICMP error message should be + sent if overlapping fragments are received. See [RFC5722] for + more information. + + o Revised the text to require that all headers through the first + upper-layer header are in the first fragment. See [RFC7112] + for more information. + + + + + + +Deering & Hinden Standards Track [Page 31] + +RFC 8200 IPv6 Specification July 2017 + + + o Incorporated the updates from [RFC5095] and [RFC5871] to remove + the description of the Routing Header type 0 (RH0), that the + allocations guidelines for Routing headers are specified in RFC + 5871, and removed RH0 from the list of required extension + headers. + + Security issues relating to other parts of IPv6 including addressing, + ICMPv6, Path MTU Discovery, etc., are discussed in the appropriate + specifications. + +11. References + +11.1. Normative References + + [RFC791] Postel, J., "Internet Protocol", STD 5, RFC 791, + DOI 10.17487/RFC0791, September 1981, + . + + [RFC2474] Nichols, K., Blake, S., Baker, F., and D. Black, + "Definition of the Differentiated Services Field (DS + Field) in the IPv4 and IPv6 Headers", RFC 2474, + DOI 10.17487/RFC2474, December 1998, + . + + [RFC3168] Ramakrishnan, K., Floyd, S., and D. Black, "The Addition + of Explicit Congestion Notification (ECN) to IP", + RFC 3168, DOI 10.17487/RFC3168, September 2001, + . + + [RFC4291] Hinden, R. and S. Deering, "IP Version 6 Addressing + Architecture", RFC 4291, DOI 10.17487/RFC4291, February + 2006, . + + [RFC4443] Conta, A., Deering, S., and M. Gupta, Ed., "Internet + Control Message Protocol (ICMPv6) for the Internet + Protocol Version 6 (IPv6) Specification", STD 89, + RFC 4443, DOI 10.17487/RFC4443, March 2006, + . + + [RFC6437] Amante, S., Carpenter, B., Jiang, S., and J. Rajahalme, + "IPv6 Flow Label Specification", RFC 6437, + DOI 10.17487/RFC6437, November 2011, + . + + + + + + + + +Deering & Hinden Standards Track [Page 32] + +RFC 8200 IPv6 Specification July 2017 + + +11.2. Informative References + + [Err2541] RFC Errata, Erratum ID 2541, RFC 2460. + + [Err4279] RFC Errata, Erratum ID 4279, RFC 2460. + + [Err4657] RFC Errata, Erratum ID 4657, RFC 2460. + + [Err4662] RFC Errata, Erratum ID 4662, RFC 2460. + + [IANA-6P] IANA, "Internet Protocol Version 6 (IPv6) Parameters", + . + + [IANA-EH] IANA, "IPv6 Extension Header Types", + . + + [IANA-NI] IANA, "ONC RPC Network Identifiers (netids)", + . + + [IANA-NL] IANA, "Network Layer Protocol Identifiers (NLPIDs) of + Interest", . + + [IANA-PN] IANA, "Protocol Numbers", + . + + [IANA-PR] IANA, "Protocol Registries", . + + [IANA-RH] IANA, "Routing Types", . + + [RFC1661] Simpson, W., Ed., "The Point-to-Point Protocol (PPP)", + STD 51, RFC 1661, DOI 10.17487/RFC1661, July 1994, + . + + [RFC2460] Deering, S. and R. Hinden, "Internet Protocol, Version 6 + (IPv6) Specification", RFC 2460, DOI 10.17487/RFC2460, + December 1998, . + + [RFC4301] Kent, S. and K. Seo, "Security Architecture for the + Internet Protocol", RFC 4301, DOI 10.17487/RFC4301, + December 2005, . + + [RFC4302] Kent, S., "IP Authentication Header", RFC 4302, + DOI 10.17487/RFC4302, December 2005, + . + + + + + +Deering & Hinden Standards Track [Page 33] + +RFC 8200 IPv6 Specification July 2017 + + + [RFC4303] Kent, S., "IP Encapsulating Security Payload (ESP)", + RFC 4303, DOI 10.17487/RFC4303, December 2005, + . + + [RFC5095] Abley, J., Savola, P., and G. Neville-Neil, "Deprecation + of Type 0 Routing Headers in IPv6", RFC 5095, + DOI 10.17487/RFC5095, December 2007, + . + + [RFC5722] Krishnan, S., "Handling of Overlapping IPv6 Fragments", + RFC 5722, DOI 10.17487/RFC5722, December 2009, + . + + [RFC5871] Arkko, J. and S. Bradner, "IANA Allocation Guidelines for + the IPv6 Routing Header", RFC 5871, DOI 10.17487/RFC5871, + May 2010, . + + [RFC6564] Krishnan, S., Woodyatt, J., Kline, E., Hoagland, J., and + M. Bhatia, "A Uniform Format for IPv6 Extension Headers", + RFC 6564, DOI 10.17487/RFC6564, April 2012, + . + + [RFC6936] Fairhurst, G. and M. Westerlund, "Applicability Statement + for the Use of IPv6 UDP Datagrams with Zero Checksums", + RFC 6936, DOI 10.17487/RFC6936, April 2013, + . + + [RFC6946] Gont, F., "Processing of IPv6 "Atomic" Fragments", + RFC 6946, DOI 10.17487/RFC6946, May 2013, + . + + [RFC7045] Carpenter, B. and S. Jiang, "Transmission and Processing + of IPv6 Extension Headers", RFC 7045, + DOI 10.17487/RFC7045, December 2013, + . + + [RFC7112] Gont, F., Manral, V., and R. Bonica, "Implications of + Oversized IPv6 Header Chains", RFC 7112, + DOI 10.17487/RFC7112, January 2014, + . + + [RFC7707] Gont, F. and T. Chown, "Network Reconnaissance in IPv6 + Networks", RFC 7707, DOI 10.17487/RFC7707, March 2016, + . + + + + + + + +Deering & Hinden Standards Track [Page 34] + +RFC 8200 IPv6 Specification July 2017 + + + [RFC7721] Cooper, A., Gont, F., and D. Thaler, "Security and Privacy + Considerations for IPv6 Address Generation Mechanisms", + RFC 7721, DOI 10.17487/RFC7721, March 2016, + . + + [RFC7739] Gont, F., "Security Implications of Predictable Fragment + Identification Values", RFC 7739, DOI 10.17487/RFC7739, + February 2016, . + + [RFC8021] Gont, F., Liu, W., and T. Anderson, "Generation of IPv6 + Atomic Fragments Considered Harmful", RFC 8021, + DOI 10.17487/RFC8021, January 2017, + . + + [RFC8201] McCann, J., Deering, S., Mogul, J., and R. Hinden, "Path + MTU Discovery for IP version 6", STD 87, RFC 8201, + DOI 10.17487/RFC8201, July 2017, + . + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Deering & Hinden Standards Track [Page 35] + +RFC 8200 IPv6 Specification July 2017 + + +Appendix A. Formatting Guidelines for Options + + This appendix gives some advice on how to lay out the fields when + designing new options to be used in the Hop-by-Hop Options header or + the Destination Options header, as described in Section 4.2. These + guidelines are based on the following assumptions: + + o One desirable feature is that any multi-octet fields within the + Option Data area of an option be aligned on their natural + boundaries, i.e., fields of width n octets should be placed at + an integer multiple of n octets from the start of the + Hop-by-Hop or Destination Options header, for n = 1, 2, 4, or + 8. + + o Another desirable feature is that the Hop-by-Hop or Destination + Options header take up as little space as possible, subject to + the requirement that the header be an integer multiple of 8 + octets long. + + o It may be assumed that, when either of the option-bearing + headers are present, they carry a very small number of options, + usually only one. + + These assumptions suggest the following approach to laying out the + fields of an option: order the fields from smallest to largest, with + no interior padding, then derive the alignment requirement for the + entire option based on the alignment requirement of the largest field + (up to a maximum alignment of 8 octets). This approach is + illustrated in the following examples: + + Example 1 + + If an option X required two data fields, one of length 8 octets and + one of length 4 octets, it would be laid out as follows: + + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Option Type=X |Opt Data Len=12| + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 4-octet field | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + 8-octet field + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + + + + + + +Deering & Hinden Standards Track [Page 36] + +RFC 8200 IPv6 Specification July 2017 + + + Its alignment requirement is 8n+2, to ensure that the 8-octet field + starts at a multiple-of-8 offset from the start of the enclosing + header. A complete Hop-by-Hop or Destination Options header + containing this one option would look as follows: + + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Next Header | Hdr Ext Len=1 | Option Type=X |Opt Data Len=12| + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 4-octet field | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + 8-octet field + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Example 2 + + If an option Y required three data fields, one of length 4 octets, + one of length 2 octets, and one of length 1 octet, it would be laid + out as follows: + + +-+-+-+-+-+-+-+-+ + | Option Type=Y | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |Opt Data Len=7 | 1-octet field | 2-octet field | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 4-octet field | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Its alignment requirement is 4n+3, to ensure that the 4-octet field + starts at a multiple-of-4 offset from the start of the enclosing + header. A complete Hop-by-Hop or Destination Options header + containing this one option would look as follows: + + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Next Header | Hdr Ext Len=1 | Pad1 Option=0 | Option Type=Y | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |Opt Data Len=7 | 1-octet field | 2-octet field | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 4-octet field | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | PadN Option=1 |Opt Data Len=2 | 0 | 0 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + + + + + + + +Deering & Hinden Standards Track [Page 37] + +RFC 8200 IPv6 Specification July 2017 + + + Example 3 + + A Hop-by-Hop or Destination Options header containing both options X + and Y from Examples 1 and 2 would have one of the two following + formats, depending on which option appeared first: + + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Next Header | Hdr Ext Len=3 | Option Type=X |Opt Data Len=12| + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 4-octet field | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + 8-octet field + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | PadN Option=1 |Opt Data Len=1 | 0 | Option Type=Y | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |Opt Data Len=7 | 1-octet field | 2-octet field | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 4-octet field | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | PadN Option=1 |Opt Data Len=2 | 0 | 0 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Next Header | Hdr Ext Len=3 | Pad1 Option=0 | Option Type=Y | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |Opt Data Len=7 | 1-octet field | 2-octet field | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 4-octet field | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | PadN Option=1 |Opt Data Len=4 | 0 | 0 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 0 | 0 | Option Type=X |Opt Data Len=12| + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 4-octet field | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + 8-octet field + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + + + + + + + + + +Deering & Hinden Standards Track [Page 38] + +RFC 8200 IPv6 Specification July 2017 + + +Appendix B. Changes Since RFC 2460 + + This memo has the following changes from RFC 2460. + + o Removed IP Next Generation from the Abstract. + + o Added text in Section 1 that the data transmission order is the + same as IPv4 as defined in RFC 791. + + o Clarified the text in Section 3 about decrementing the Hop Limit. + + o Clarified that extension headers (except for the Hop-by-Hop + Options header) are not processed, inserted, or deleted by any + node along a packet's delivery path. + + o Changed requirement for the Hop-by-Hop Options header to a "may", + and added a note to indicate what is expected regarding the + Hop-by-Hop Options header. + + o Added a paragraph to Section 4 to clarify how extension headers + are numbered and which are upper-layer headers. + + o Added a reference to the end of Section 4 to the "IPv6 Extension + Header Types" IANA registry. + + o Incorporated the updates from RFCs 5095 and 5871 to remove the + description of RH0, that the allocations guidelines for routing + headers are specified in RFC 5871, and removed RH0 from the list + of required extension headers. + + o Revised Section 4.5 on IPv6 fragmentation based on updates from + RFCs 5722, 6946, 7112, and 8021. This includes: + + - Revised the text to handle the case of fragments that are whole + datagrams (i.e., both the Fragment Offset field and the M flag + are zero). If received, they should be processed as a + reassembled packet. Any other fragments that match should be + processed independently. The revised Fragment creation process + was modified to not create whole datagram fragments (Fragment + Offset field and the M flag are zero). + + - Changed the text to require that IPv6 nodes must not create + overlapping fragments. Also, when reassembling an IPv6 + datagram, if one or more its constituent fragments is + determined to be an overlapping fragment, the entire datagram + (and any constituent fragments) must be silently discarded. + Includes a clarification that no ICMP error message should be + sent if overlapping fragments are received. + + + +Deering & Hinden Standards Track [Page 39] + +RFC 8200 IPv6 Specification July 2017 + + + - Revised the text to require that all headers through the first + Upper-Layer header are in the first fragment. This changed the + text describing how packets are fragmented and reassembled and + added a new error case. + + - Added text to the Fragment header process on handling exact + duplicate fragments. + + - Updated the Fragmentation header text to correct the inclusion + of an Authentication Header (AH) and noted No Next Header case. + + - Changed terminology in the Fragment header section from + "Unfragmentable Headers" to "Per-Fragment headers". + + - Removed the paragraph in Section 5 that required including a + Fragment header to outgoing packets if an ICMP Packet Too Big + message reports a Next-Hop MTU less than 1280. + + - Changed the text to clarify MTU restriction and 8-byte + restrictions, and noted the restriction on headers in the first + fragment. + + o In Section 4.5, added clarification noting that some fields in the + IPv6 header may also vary across the fragments being reassembled, + and that other specifications may provide additional instructions + for how they should be reassembled. See, for example, Section 5.3 + of [RFC3168]. + + o Incorporated the update from RFC 6564 to add a new Section 4.8 + that describes recommendations for defining new extension headers + and options. + + o Added text to Section 5 to define "IPv6 minimum link MTU". + + o Simplified the text in Section 6 about Flow Labels and removed + what was Appendix A ("Semantics and Usage of the Flow Label + Field"); instead, pointed to the current specifications of the + IPv6 Flow Label field in [RFC6437] and the Traffic Class field in + [RFC2474] and [RFC3168]. + + o Incorporated the update made by RFC 6935 ("IPv6 and UDP Checksums + for Tunneled Packets") in Section 8. Added an exception to the + default behavior for the handling of UDP packets with zero + checksums for tunnels. + + o Added instruction to Section 9, "IANA Considerations", to change + references to RFC 2460 to this document. + + + + +Deering & Hinden Standards Track [Page 40] + +RFC 8200 IPv6 Specification July 2017 + + + o Revised and expanded Section 10, "Security Considerations". + + o Added a paragraph to the Acknowledgments section acknowledging the + authors of the updating documents. + + o Updated references to current versions and assigned references to + normative and informative. + + o Made changes to resolve the errata on RFC 2460. These are: + + Erratum ID 2541 [Err2541]: This erratum notes that RFC 2460 + didn't update RFC 2205 when the length of the flow label was + changed from 24 to 20 bits from RFC 1883. This issue was + resolved in RFC 6437 where the flow label is defined. This + specification now references RFC 6437. No change is required. + + Erratum ID 4279 [Err4279]: This erratum noted that the + specification doesn't handle the case of a forwarding node + receiving a packet with a zero Hop Limit. This is fixed in + Section 3 of this specification. + + Erratum ID 4657 [Err4657]: This erratum proposed text that + extension headers must never be inserted by any node other than + the source of the packet. This was resolved in Section 4, + "IPv6 Extension Headers". + + Erratum ID 4662 [Err4662]: This erratum proposed text that + extension headers, with one exception, are not examined, + processed, modified, inserted, or deleted by any node along a + packet's delivery path. This was resolved in Section 4, "IPv6 + Extension Headers". + + Erratum ID 2843: This erratum is marked "Rejected". No change + was made. + + + + + + + + + + + + + + + + + +Deering & Hinden Standards Track [Page 41] + +RFC 8200 IPv6 Specification July 2017 + + +Acknowledgments + + The authors gratefully acknowledge the many helpful suggestions of + the members of the IPng Working Group, the End-to-End Protocols + research group, and the Internet community at large. + + The authors would also like to acknowledge the authors of the + updating RFCs that were incorporated in this document to move the + IPv6 specification to Internet Standard. They are Joe Abley, Shane + Amante, Jari Arkko, Manav Bhatia, Ronald P. Bonica, Scott Bradner, + Brian Carpenter, P.F. Chimento, Marshall Eubanks, Fernando Gont, + James Hoagland, Sheng Jiang, Erik Kline, Suresh Krishnan, Vishwas + Manral, George Neville-Neil, Jarno Rajahalme, Pekka Savola, Magnus + Westerlund, and James Woodyatt. + +Authors' Addresses + + Stephen E. Deering + Retired + Vancouver, British Columbia + Canada + + + Robert M. Hinden + Check Point Software + 959 Skyway Road + San Carlos, CA 94070 + United States of America + + Email: bob.hinden@gmail.com + + + + + + + + + + + + + + + + + + + + + +Deering & Hinden Standards Track [Page 42] diff --git a/dist/docs/rfc9293-tcp.txt b/dist/docs/rfc9293-tcp.txt new file mode 100644 index 0000000..c31ffa5 --- /dev/null +++ b/dist/docs/rfc9293-tcp.txt @@ -0,0 +1,5576 @@ + + + + +Internet Engineering Task Force (IETF) W. Eddy, Ed. +STD: 7 MTI Systems +Request for Comments: 9293 August 2022 +Obsoletes: 793, 879, 2873, 6093, 6429, 6528, + 6691 +Updates: 1011, 1122, 5961 +Category: Standards Track +ISSN: 2070-1721 + + + Transmission Control Protocol (TCP) + +Abstract + + This document specifies the Transmission Control Protocol (TCP). TCP + is an important transport-layer protocol in the Internet protocol + stack, and it has continuously evolved over decades of use and growth + of the Internet. Over this time, a number of changes have been made + to TCP as it was specified in RFC 793, though these have only been + documented in a piecemeal fashion. This document collects and brings + those changes together with the protocol specification from RFC 793. + This document obsoletes RFC 793, as well as RFCs 879, 2873, 6093, + 6429, 6528, and 6691 that updated parts of RFC 793. It updates RFCs + 1011 and 1122, and it should be considered as a replacement for the + portions of those documents dealing with TCP requirements. It also + updates RFC 5961 by adding a small clarification in reset handling + while in the SYN-RECEIVED state. The TCP header control bits from + RFC 793 have also been updated based on RFC 3168. + +Status of This Memo + + This is an Internet Standards Track document. + + This document is a product of the Internet Engineering Task Force + (IETF). It represents the consensus of the IETF community. It has + received public review and has been approved for publication by the + Internet Engineering Steering Group (IESG). Further information on + Internet Standards is available in Section 2 of RFC 7841. + + Information about the current status of this document, any errata, + and how to provide feedback on it may be obtained at + https://www.rfc-editor.org/info/rfc9293. + +Copyright Notice + + Copyright (c) 2022 IETF Trust and the persons identified as the + document authors. All rights reserved. + + This document is subject to BCP 78 and the IETF Trust's Legal + Provisions Relating to IETF Documents + (https://trustee.ietf.org/license-info) in effect on the date of + publication of this document. Please review these documents + carefully, as they describe your rights and restrictions with respect + to this document. Code Components extracted from this document must + include Revised BSD License text as described in Section 4.e of the + Trust Legal Provisions and are provided without warranty as described + in the Revised BSD License. + + This document may contain material from IETF Documents or IETF + Contributions published or made publicly available before November + 10, 2008. The person(s) controlling the copyright in some of this + material may not have granted the IETF Trust the right to allow + modifications of such material outside the IETF Standards Process. + Without obtaining an adequate license from the person(s) controlling + the copyright in such materials, this document may not be modified + outside the IETF Standards Process, and derivative works of it may + not be created outside the IETF Standards Process, except to format + it for publication as an RFC or to translate it into languages other + than English. + +Table of Contents + + 1. Purpose and Scope + 2. Introduction + 2.1. Requirements Language + 2.2. Key TCP Concepts + 3. Functional Specification + 3.1. Header Format + 3.2. Specific Option Definitions + 3.2.1. Other Common Options + 3.2.2. Experimental TCP Options + 3.3. TCP Terminology Overview + 3.3.1. Key Connection State Variables + 3.3.2. State Machine Overview + 3.4. Sequence Numbers + 3.4.1. Initial Sequence Number Selection + 3.4.2. Knowing When to Keep Quiet + 3.4.3. The TCP Quiet Time Concept + 3.5. Establishing a Connection + 3.5.1. Half-Open Connections and Other Anomalies + 3.5.2. Reset Generation + 3.5.3. Reset Processing + 3.6. Closing a Connection + 3.6.1. Half-Closed Connections + 3.7. Segmentation + 3.7.1. Maximum Segment Size Option + 3.7.2. Path MTU Discovery + 3.7.3. Interfaces with Variable MTU Values + 3.7.4. Nagle Algorithm + 3.7.5. IPv6 Jumbograms + 3.8. Data Communication + 3.8.1. Retransmission Timeout + 3.8.2. TCP Congestion Control + 3.8.3. TCP Connection Failures + 3.8.4. TCP Keep-Alives + 3.8.5. The Communication of Urgent Information + 3.8.6. Managing the Window + 3.9. Interfaces + 3.9.1. User/TCP Interface + 3.9.2. TCP/Lower-Level Interface + 3.10. Event Processing + 3.10.1. OPEN Call + 3.10.2. SEND Call + 3.10.3. RECEIVE Call + 3.10.4. CLOSE Call + 3.10.5. ABORT Call + 3.10.6. STATUS Call + 3.10.7. SEGMENT ARRIVES + 3.10.8. Timeouts + 4. Glossary + 5. Changes from RFC 793 + 6. IANA Considerations + 7. Security and Privacy Considerations + 8. References + 8.1. Normative References + 8.2. Informative References + Appendix A. Other Implementation Notes + A.1. IP Security Compartment and Precedence + A.1.1. Precedence + A.1.2. MLS Systems + A.2. Sequence Number Validation + A.3. Nagle Modification + A.4. Low Watermark Settings + Appendix B. TCP Requirement Summary + Acknowledgments + Author's Address + +1. Purpose and Scope + + In 1981, RFC 793 [16] was released, documenting the Transmission + Control Protocol (TCP) and replacing earlier published specifications + for TCP. + + Since then, TCP has been widely implemented, and it has been used as + a transport protocol for numerous applications on the Internet. + + For several decades, RFC 793 plus a number of other documents have + combined to serve as the core specification for TCP [49]. Over time, + a number of errata have been filed against RFC 793. There have also + been deficiencies found and resolved in security, performance, and + many other aspects. The number of enhancements has grown over time + across many separate documents. These were never accumulated + together into a comprehensive update to the base specification. + + The purpose of this document is to bring together all of the IETF + Standards Track changes and other clarifications that have been made + to the base TCP functional specification (RFC 793) and to unify them + into an updated version of the specification. + + Some companion documents are referenced for important algorithms that + are used by TCP (e.g., for congestion control) but have not been + completely included in this document. This is a conscious choice, as + this base specification can be used with multiple additional + algorithms that are developed and incorporated separately. This + document focuses on the common basis that all TCP implementations + must support in order to interoperate. Since some additional TCP + features have become quite complicated themselves (e.g., advanced + loss recovery and congestion control), future companion documents may + attempt to similarly bring these together. + + In addition to the protocol specification that describes the TCP + segment format, generation, and processing rules that are to be + implemented in code, RFC 793 and other updates also contain + informative and descriptive text for readers to understand aspects of + the protocol design and operation. This document does not attempt to + alter or update this informative text and is focused only on updating + the normative protocol specification. This document preserves + references to the documentation containing the important explanations + and rationale, where appropriate. + + This document is intended to be useful both in checking existing TCP + implementations for conformance purposes, as well as in writing new + implementations. + +2. Introduction + + RFC 793 contains a discussion of the TCP design goals and provides + examples of its operation, including examples of connection + establishment, connection termination, and packet retransmission to + repair losses. + + This document describes the basic functionality expected in modern + TCP implementations and replaces the protocol specification in RFC + 793. It does not replicate or attempt to update the introduction and + philosophy content in Sections 1 and 2 of RFC 793. Other documents + are referenced to provide explanations of the theory of operation, + rationale, and detailed discussion of design decisions. This + document only focuses on the normative behavior of the protocol. + + The "TCP Roadmap" [49] provides a more extensive guide to the RFCs + that define TCP and describe various important algorithms. The TCP + Roadmap contains sections on strongly encouraged enhancements that + improve performance and other aspects of TCP beyond the basic + operation specified in this document. As one example, implementing + congestion control (e.g., [8]) is a TCP requirement, but it is a + complex topic on its own and not described in detail in this + document, as there are many options and possibilities that do not + impact basic interoperability. Similarly, most TCP implementations + today include the high-performance extensions in [47], but these are + not strictly required or discussed in this document. Multipath + considerations for TCP are also specified separately in [59]. + + A list of changes from RFC 793 is contained in Section 5. + +2.1. Requirements Language + + The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and + "OPTIONAL" in this document are to be interpreted as described in + BCP 14 [3] [12] when, and only when, they appear in all capitals, as + shown here. + + Each use of RFC 2119 keywords in the document is individually labeled + and referenced in Appendix B, which summarizes implementation + requirements. + + Sentences using "MUST" are labeled as "MUST-X" with X being a numeric + identifier enabling the requirement to be located easily when + referenced from Appendix B. + + Similarly, sentences using "SHOULD" are labeled with "SHLD-X", "MAY" + with "MAY-X", and "RECOMMENDED" with "REC-X". + + For the purposes of this labeling, "SHOULD NOT" and "MUST NOT" are + labeled the same as "SHOULD" and "MUST" instances. + +2.2. Key TCP Concepts + + TCP provides a reliable, in-order, byte-stream service to + applications. + + The application byte-stream is conveyed over the network via TCP + segments, with each TCP segment sent as an Internet Protocol (IP) + datagram. + + TCP reliability consists of detecting packet losses (via sequence + numbers) and errors (via per-segment checksums), as well as + correction via retransmission. + + TCP supports unicast delivery of data. There are anycast + applications that can successfully use TCP without modifications, + though there is some risk of instability due to changes of lower- + layer forwarding behavior [46]. + + TCP is connection oriented, though it does not inherently include a + liveness detection capability. + + Data flow is supported bidirectionally over TCP connections, though + applications are free to send data only unidirectionally, if they so + choose. + + TCP uses port numbers to identify application services and to + multiplex distinct flows between hosts. + + A more detailed description of TCP features compared to other + transport protocols can be found in Section 3.1 of [52]. Further + description of the motivations for developing TCP and its role in the + Internet protocol stack can be found in Section 2 of [16] and earlier + versions of the TCP specification. + +3. Functional Specification + +3.1. Header Format + + TCP segments are sent as internet datagrams. The Internet Protocol + (IP) header carries several information fields, including the source + and destination host addresses [1] [13]. A TCP header follows the IP + headers, supplying information specific to TCP. This division allows + for the existence of host-level protocols other than TCP. In the + early development of the Internet suite of protocols, the IP header + fields had been a part of TCP. + + This document describes TCP, which uses TCP headers. + + A TCP header, followed by any user data in the segment, is formatted + as follows, using the style from [66]: + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Source Port | Destination Port | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Sequence Number | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Acknowledgment Number | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Data | |C|E|U|A|P|R|S|F| | + | Offset| Rsrvd |W|C|R|C|S|S|Y|I| Window | + | | |R|E|G|K|H|T|N|N| | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Checksum | Urgent Pointer | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | [Options] | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | : + : Data : + : | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note that one tick mark represents one bit position. + + Figure 1: TCP Header Format + + where: + + Source Port: 16 bits + + The source port number. + + Destination Port: 16 bits + + The destination port number. + + Sequence Number: 32 bits + + The sequence number of the first data octet in this segment (except + when the SYN flag is set). If SYN is set, the sequence number is + the initial sequence number (ISN) and the first data octet is + ISN+1. + + Acknowledgment Number: 32 bits + + If the ACK control bit is set, this field contains the value of the + next sequence number the sender of the segment is expecting to + receive. Once a connection is established, this is always sent. + + Data Offset (DOffset): 4 bits + + The number of 32-bit words in the TCP header. This indicates where + the data begins. The TCP header (even one including options) is an + integer multiple of 32 bits long. + + Reserved (Rsrvd): 4 bits + + A set of control bits reserved for future use. Must be zero in + generated segments and must be ignored in received segments if the + corresponding future features are not implemented by the sending or + receiving host. + + Control bits: The control bits are also known as "flags". + Assignment is managed by IANA from the "TCP Header Flags" registry + [62]. The currently assigned control bits are CWR, ECE, URG, ACK, + PSH, RST, SYN, and FIN. + + CWR: 1 bit + + Congestion Window Reduced (see [6]). + + ECE: 1 bit + + ECN-Echo (see [6]). + + URG: 1 bit + + Urgent pointer field is significant. + + ACK: 1 bit + + Acknowledgment field is significant. + + PSH: 1 bit + + Push function (see the Send Call description in Section 3.9.1). + + RST: 1 bit + + Reset the connection. + + SYN: 1 bit + + Synchronize sequence numbers. + + FIN: 1 bit + + No more data from sender. + + Window: 16 bits + + The number of data octets beginning with the one indicated in the + acknowledgment field that the sender of this segment is willing to + accept. The value is shifted when the window scaling extension is + used [47]. + + The window size MUST be treated as an unsigned number, or else + large window sizes will appear like negative windows and TCP will + not work (MUST-1). It is RECOMMENDED that implementations will + reserve 32-bit fields for the send and receive window sizes in the + connection record and do all window computations with 32 bits (REC- + 1). + + Checksum: 16 bits + + The checksum field is the 16-bit ones' complement of the ones' + complement sum of all 16-bit words in the header and text. The + checksum computation needs to ensure the 16-bit alignment of the + data being summed. If a segment contains an odd number of header + and text octets, alignment can be achieved by padding the last + octet with zeros on its right to form a 16-bit word for checksum + purposes. The pad is not transmitted as part of the segment. + While computing the checksum, the checksum field itself is replaced + with zeros. + + The checksum also covers a pseudo-header (Figure 2) conceptually + prefixed to the TCP header. The pseudo-header is 96 bits for IPv4 + and 320 bits for IPv6. Including the pseudo-header in the checksum + gives the TCP connection protection against misrouted segments. + This information is carried in IP headers and is transferred across + the TCP/network interface in the arguments or results of calls by + the TCP implementation on the IP layer. + + +--------+--------+--------+--------+ + | Source Address | + +--------+--------+--------+--------+ + | Destination Address | + +--------+--------+--------+--------+ + | zero | PTCL | TCP Length | + +--------+--------+--------+--------+ + + Figure 2: IPv4 Pseudo-header + + Pseudo-header components for IPv4: + Source Address: the IPv4 source address in network byte order + + Destination Address: the IPv4 destination address in network + byte order + + zero: bits set to zero + + PTCL: the protocol number from the IP header + + TCP Length: the TCP header length plus the data length in octets + (this is not an explicitly transmitted quantity but is + computed), and it does not count the 12 octets of the pseudo- + header. + + For IPv6, the pseudo-header is defined in Section 8.1 of RFC 8200 + [13] and contains the IPv6 Source Address and Destination Address, + an Upper-Layer Packet Length (a 32-bit value otherwise equivalent + to TCP Length in the IPv4 pseudo-header), three bytes of zero + padding, and a Next Header value, which differs from the IPv6 + header value if there are extension headers present between IPv6 + and TCP. + + The TCP checksum is never optional. The sender MUST generate it + (MUST-2) and the receiver MUST check it (MUST-3). + + Urgent Pointer: 16 bits + + This field communicates the current value of the urgent pointer as + a positive offset from the sequence number in this segment. The + urgent pointer points to the sequence number of the octet following + the urgent data. This field is only to be interpreted in segments + with the URG control bit set. + + Options: [TCP Option]; size(Options) == (DOffset-5)*32; present only + when DOffset > 5. Note that this size expression also includes any + padding trailing the actual options present. + + Options may occupy space at the end of the TCP header and are a + multiple of 8 bits in length. All options are included in the + checksum. An option may begin on any octet boundary. There are + two cases for the format of an option: + + Case 1: A single octet of option-kind. + + Case 2: An octet of option-kind (Kind), an octet of option-length, + and the actual option-data octets. + + The option-length counts the two octets of option-kind and option- + length as well as the option-data octets. + + Note that the list of options may be shorter than the Data Offset + field might imply. The content of the header beyond the End of + Option List Option MUST be header padding of zeros (MUST-69). + + The list of all currently defined options is managed by IANA [62], + and each option is defined in other RFCs, as indicated there. That + set includes experimental options that can be extended to support + multiple concurrent usages [45]. + + A given TCP implementation can support any currently defined + options, but the following options MUST be supported (MUST-4 -- + note Maximum Segment Size Option support is also part of MUST-14 in + Section 3.7.1): + + +======+========+============================+ + | Kind | Length | Meaning | + +======+========+============================+ + | 0 | - | End of Option List Option. | + +------+--------+----------------------------+ + | 1 | - | No-Operation. | + +------+--------+----------------------------+ + | 2 | 4 | Maximum Segment Size. | + +------+--------+----------------------------+ + + Table 1: Mandatory Option Set + + These options are specified in detail in Section 3.2. + + A TCP implementation MUST be able to receive a TCP Option in any + segment (MUST-5). + + A TCP implementation MUST (MUST-6) ignore without error any TCP + Option it does not implement, assuming that the option has a length + field. All TCP Options except End of Option List Option (EOL) and + No-Operation (NOP) MUST have length fields, including all future + options (MUST-68). TCP implementations MUST be prepared to handle + an illegal option length (e.g., zero); a suggested procedure is to + reset the connection and log the error cause (MUST-7). + + Note: There is ongoing work to extend the space available for TCP + Options, such as [65]. + + Data: variable length + + User data carried by the TCP segment. + +3.2. Specific Option Definitions + + A TCP Option, in the mandatory option set, is one of an End of Option + List Option, a No-Operation Option, or a Maximum Segment Size Option. + + An End of Option List Option is formatted as follows: + + 0 + 0 1 2 3 4 5 6 7 + +-+-+-+-+-+-+-+-+ + | 0 | + +-+-+-+-+-+-+-+-+ + + where: + + Kind: 1 byte; Kind == 0. + + This option code indicates the end of the option list. This might + not coincide with the end of the TCP header according to the Data + Offset field. This is used at the end of all options, not the end + of each option, and need only be used if the end of the options + would not otherwise coincide with the end of the TCP header. + + A No-Operation Option is formatted as follows: + + 0 + 0 1 2 3 4 5 6 7 + +-+-+-+-+-+-+-+-+ + | 1 | + +-+-+-+-+-+-+-+-+ + + where: + + Kind: 1 byte; Kind == 1. + + This option code can be used between options, for example, to align + the beginning of a subsequent option on a word boundary. There is + no guarantee that senders will use this option, so receivers MUST + be prepared to process options even if they do not begin on a word + boundary (MUST-64). + + A Maximum Segment Size Option is formatted as follows: + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 2 | Length | Maximum Segment Size (MSS) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + where: + + Kind: 1 byte; Kind == 2. + + If this option is present, then it communicates the maximum receive + segment size at the TCP endpoint that sends this segment. This + value is limited by the IP reassembly limit. This field may be + sent in the initial connection request (i.e., in segments with the + SYN control bit set) and MUST NOT be sent in other segments (MUST- + 65). If this option is not used, any segment size is allowed. A + more complete description of this option is provided in + Section 3.7.1. + + Length: 1 byte; Length == 4. + + Length of the option in bytes. + + Maximum Segment Size (MSS): 2 bytes. + + The maximum receive segment size at the TCP endpoint that sends + this segment. + +3.2.1. Other Common Options + + Additional RFCs define some other commonly used options that are + recommended to implement for high performance but are not necessary + for basic TCP interoperability. These are the TCP Selective + Acknowledgment (SACK) Option [22] [26], TCP Timestamp (TS) Option + [47], and TCP Window Scale (WS) Option [47]. + +3.2.2. Experimental TCP Options + + Experimental TCP Option values are defined in [30], and [45] + describes the current recommended usage for these experimental + values. + +3.3. TCP Terminology Overview + + This section includes an overview of key terms needed to understand + the detailed protocol operation in the rest of the document. There + is a glossary of terms in Section 4. + +3.3.1. Key Connection State Variables + + Before we can discuss the operation of the TCP implementation in + detail, we need to introduce some detailed terminology. The + maintenance of a TCP connection requires maintaining state for + several variables. We conceive of these variables being stored in a + connection record called a Transmission Control Block or TCB. Among + the variables stored in the TCB are the local and remote IP addresses + and port numbers, the IP security level, and compartment of the + connection (see Appendix A.1), pointers to the user's send and + receive buffers, pointers to the retransmit queue and to the current + segment. In addition, several variables relating to the send and + receive sequence numbers are stored in the TCB. + + +==========+=====================================================+ + | Variable | Description | + +==========+=====================================================+ + | SND.UNA | send unacknowledged | + +----------+-----------------------------------------------------+ + | SND.NXT | send next | + +----------+-----------------------------------------------------+ + | SND.WND | send window | + +----------+-----------------------------------------------------+ + | SND.UP | send urgent pointer | + +----------+-----------------------------------------------------+ + | SND.WL1 | segment sequence number used for last window update | + +----------+-----------------------------------------------------+ + | SND.WL2 | segment acknowledgment number used for last window | + | | update | + +----------+-----------------------------------------------------+ + | ISS | initial send sequence number | + +----------+-----------------------------------------------------+ + + Table 2: Send Sequence Variables + + +==========+=================================+ + | Variable | Description | + +==========+=================================+ + | RCV.NXT | receive next | + +----------+---------------------------------+ + | RCV.WND | receive window | + +----------+---------------------------------+ + | RCV.UP | receive urgent pointer | + +----------+---------------------------------+ + | IRS | initial receive sequence number | + +----------+---------------------------------+ + + Table 3: Receive Sequence Variables + + The following diagrams may help to relate some of these variables to + the sequence space. + + 1 2 3 4 + ----------|----------|----------|---------- + SND.UNA SND.NXT SND.UNA + +SND.WND + + 1 - old sequence numbers that have been acknowledged + 2 - sequence numbers of unacknowledged data + 3 - sequence numbers allowed for new data transmission + 4 - future sequence numbers that are not yet allowed + + Figure 3: Send Sequence Space + + The send window is the portion of the sequence space labeled 3 in + Figure 3. + + 1 2 3 + ----------|----------|---------- + RCV.NXT RCV.NXT + +RCV.WND + + 1 - old sequence numbers that have been acknowledged + 2 - sequence numbers allowed for new reception + 3 - future sequence numbers that are not yet allowed + + Figure 4: Receive Sequence Space + + The receive window is the portion of the sequence space labeled 2 in + Figure 4. + + There are also some variables used frequently in the discussion that + take their values from the fields of the current segment. + + +==========+===============================+ + | Variable | Description | + +==========+===============================+ + | SEG.SEQ | segment sequence number | + +----------+-------------------------------+ + | SEG.ACK | segment acknowledgment number | + +----------+-------------------------------+ + | SEG.LEN | segment length | + +----------+-------------------------------+ + | SEG.WND | segment window | + +----------+-------------------------------+ + | SEG.UP | segment urgent pointer | + +----------+-------------------------------+ + + Table 4: Current Segment Variables + +3.3.2. State Machine Overview + + A connection progresses through a series of states during its + lifetime. The states are: LISTEN, SYN-SENT, SYN-RECEIVED, + ESTABLISHED, FIN-WAIT-1, FIN-WAIT-2, CLOSE-WAIT, CLOSING, LAST-ACK, + TIME-WAIT, and the fictional state CLOSED. CLOSED is fictional + because it represents the state when there is no TCB, and therefore, + no connection. Briefly the meanings of the states are: + + LISTEN - represents waiting for a connection request from any remote + TCP peer and port. + + SYN-SENT - represents waiting for a matching connection request + after having sent a connection request. + + SYN-RECEIVED - represents waiting for a confirming connection + request acknowledgment after having both received and sent a + connection request. + + ESTABLISHED - represents an open connection, data received can be + delivered to the user. The normal state for the data transfer + phase of the connection. + + FIN-WAIT-1 - represents waiting for a connection termination request + from the remote TCP peer, or an acknowledgment of the connection + termination request previously sent. + + FIN-WAIT-2 - represents waiting for a connection termination request + from the remote TCP peer. + + CLOSE-WAIT - represents waiting for a connection termination request + from the local user. + + CLOSING - represents waiting for a connection termination request + acknowledgment from the remote TCP peer. + + LAST-ACK - represents waiting for an acknowledgment of the + connection termination request previously sent to the remote TCP + peer (this termination request sent to the remote TCP peer already + included an acknowledgment of the termination request sent from + the remote TCP peer). + + TIME-WAIT - represents waiting for enough time to pass to be sure + the remote TCP peer received the acknowledgment of its connection + termination request and to avoid new connections being impacted by + delayed segments from previous connections. + + CLOSED - represents no connection state at all. + + A TCP connection progresses from one state to another in response to + events. The events are the user calls, OPEN, SEND, RECEIVE, CLOSE, + ABORT, and STATUS; the incoming segments, particularly those + containing the SYN, ACK, RST, and FIN flags; and timeouts. + + The OPEN call specifies whether connection establishment is to be + actively pursued, or to be passively waited for. + + A passive OPEN request means that the process wants to accept + incoming connection requests, in contrast to an active OPEN + attempting to initiate a connection. + + The state diagram in Figure 5 illustrates only state changes, + together with the causing events and resulting actions, but addresses + neither error conditions nor actions that are not connected with + state changes. In a later section, more detail is offered with + respect to the reaction of the TCP implementation to events. Some + state names are abbreviated or hyphenated differently in the diagram + from how they appear elsewhere in the document. + + NOTA BENE: This diagram is only a summary and must not be taken as + the total specification. Many details are not included. + + +---------+ ---------\ active OPEN + | CLOSED | \ ----------- + +---------+<---------\ \ create TCB + | ^ \ \ snd SYN + passive OPEN | | CLOSE \ \ + ------------ | | ---------- \ \ + create TCB | | delete TCB \ \ + V | \ \ + rcv RST (note 1) +---------+ CLOSE | \ + -------------------->| LISTEN | ---------- | | + / +---------+ delete TCB | | + / rcv SYN | | SEND | | + / ----------- | | ------- | V + +--------+ snd SYN,ACK / \ snd SYN +--------+ + | |<----------------- ------------------>| | + | SYN | rcv SYN | SYN | + | RCVD |<-----------------------------------------------| SENT | + | | snd SYN,ACK | | + | |------------------ -------------------| | + +--------+ rcv ACK of SYN \ / rcv SYN,ACK +--------+ + | -------------- | | ----------- + | x | | snd ACK + | V V + | CLOSE +---------+ + | ------- | ESTAB | + | snd FIN +---------+ + | CLOSE | | rcv FIN + V ------- | | ------- + +---------+ snd FIN / \ snd ACK +---------+ + | FIN |<---------------- ------------------>| CLOSE | + | WAIT-1 |------------------ | WAIT | + +---------+ rcv FIN \ +---------+ + | rcv ACK of FIN ------- | CLOSE | + | -------------- snd ACK | ------- | + V x V snd FIN V + +---------+ +---------+ +---------+ + |FINWAIT-2| | CLOSING | | LAST-ACK| + +---------+ +---------+ +---------+ + | rcv ACK of FIN | rcv ACK of FIN | + | rcv FIN -------------- | Timeout=2MSL -------------- | + | ------- x V ------------ x V + \ snd ACK +---------+delete TCB +---------+ + -------------------->|TIME-WAIT|------------------->| CLOSED | + +---------+ +---------+ + + Figure 5: TCP Connection State Diagram + + The following notes apply to Figure 5: + + Note 1: The transition from SYN-RECEIVED to LISTEN on receiving a + RST is conditional on having reached SYN-RECEIVED after a passive + OPEN. + + Note 2: The figure omits a transition from FIN-WAIT-1 to TIME-WAIT + if a FIN is received and the local FIN is also acknowledged. + + Note 3: A RST can be sent from any state with a corresponding + transition to TIME-WAIT (see [70] for rationale). These + transitions are not explicitly shown; otherwise, the diagram would + become very difficult to read. Similarly, receipt of a RST from + any state results in a transition to LISTEN or CLOSED, though this + is also omitted from the diagram for legibility. + +3.4. Sequence Numbers + + A fundamental notion in the design is that every octet of data sent + over a TCP connection has a sequence number. Since every octet is + sequenced, each of them can be acknowledged. The acknowledgment + mechanism employed is cumulative so that an acknowledgment of + sequence number X indicates that all octets up to but not including X + have been received. This mechanism allows for straightforward + duplicate detection in the presence of retransmission. The numbering + scheme of octets within a segment is as follows: the first data octet + immediately following the header is the lowest numbered, and the + following octets are numbered consecutively. + + It is essential to remember that the actual sequence number space is + finite, though large. This space ranges from 0 to 2^32 - 1. Since + the space is finite, all arithmetic dealing with sequence numbers + must be performed modulo 2^32. This unsigned arithmetic preserves + the relationship of sequence numbers as they cycle from 2^32 - 1 to 0 + again. There are some subtleties to computer modulo arithmetic, so + great care should be taken in programming the comparison of such + values. The symbol "=<" means "less than or equal" (modulo 2^32). + + The typical kinds of sequence number comparisons that the TCP + implementation must perform include: + + (a) Determining that an acknowledgment refers to some sequence + number sent but not yet acknowledged. + + (b) Determining that all sequence numbers occupied by a segment have + been acknowledged (e.g., to remove the segment from a + retransmission queue). + + (c) Determining that an incoming segment contains sequence numbers + that are expected (i.e., that the segment "overlaps" the receive + window). + + In response to sending data, the TCP endpoint will receive + acknowledgments. The following comparisons are needed to process the + acknowledgments: + + SND.UNA = oldest unacknowledged sequence number + + SND.NXT = next sequence number to be sent + + SEG.ACK = acknowledgment from the receiving TCP peer (next + sequence number expected by the receiving TCP peer) + + SEG.SEQ = first sequence number of a segment + + SEG.LEN = the number of octets occupied by the data in the segment + (counting SYN and FIN) + + SEG.SEQ+SEG.LEN-1 = last sequence number of a segment + + A new acknowledgment (called an "acceptable ack") is one for which + the inequality below holds: + + SND.UNA < SEG.ACK =< SND.NXT + + A segment on the retransmission queue is fully acknowledged if the + sum of its sequence number and length is less than or equal to the + acknowledgment value in the incoming segment. + + When data is received, the following comparisons are needed: + + RCV.NXT = next sequence number expected on an incoming segment, + and is the left or lower edge of the receive window + + RCV.NXT+RCV.WND-1 = last sequence number expected on an incoming + segment, and is the right or upper edge of the receive window + + SEG.SEQ = first sequence number occupied by the incoming segment + + SEG.SEQ+SEG.LEN-1 = last sequence number occupied by the incoming + segment + + A segment is judged to occupy a portion of valid receive sequence + space if + + RCV.NXT =< SEG.SEQ < RCV.NXT+RCV.WND + + or + + RCV.NXT =< SEG.SEQ+SEG.LEN-1 < RCV.NXT+RCV.WND + + The first part of this test checks to see if the beginning of the + segment falls in the window, the second part of the test checks to + see if the end of the segment falls in the window; if the segment + passes either part of the test, it contains data in the window. + + Actually, it is a little more complicated than this. Due to zero + windows and zero-length segments, we have four cases for the + acceptability of an incoming segment: + + +=========+=========+======================================+ + | Segment | Receive | Test | + | Length | Window | | + +=========+=========+======================================+ + | 0 | 0 | SEG.SEQ = RCV.NXT | + +---------+---------+--------------------------------------+ + | 0 | >0 | RCV.NXT =< SEG.SEQ < RCV.NXT+RCV.WND | + +---------+---------+--------------------------------------+ + | >0 | 0 | not acceptable | + +---------+---------+--------------------------------------+ + | >0 | >0 | RCV.NXT =< SEG.SEQ < RCV.NXT+RCV.WND | + | | | | + | | | or | + | | | | + | | | RCV.NXT =< SEG.SEQ+SEG.LEN-1 < | + | | | RCV.NXT+RCV.WND | + +---------+---------+--------------------------------------+ + + Table 5: Segment Acceptability Tests + + Note that when the receive window is zero no segments should be + acceptable except ACK segments. Thus, it is possible for a TCP + implementation to maintain a zero receive window while transmitting + data and receiving ACKs. A TCP receiver MUST process the RST and URG + fields of all incoming segments, even when the receive window is zero + (MUST-66). + + We have taken advantage of the numbering scheme to protect certain + control information as well. This is achieved by implicitly + including some control flags in the sequence space so they can be + retransmitted and acknowledged without confusion (i.e., one and only + one copy of the control will be acted upon). Control information is + not physically carried in the segment data space. Consequently, we + must adopt rules for implicitly assigning sequence numbers to + control. The SYN and FIN are the only controls requiring this + protection, and these controls are used only at connection opening + and closing. For sequence number purposes, the SYN is considered to + occur before the first actual data octet of the segment in which it + occurs, while the FIN is considered to occur after the last actual + data octet in a segment in which it occurs. The segment length + (SEG.LEN) includes both data and sequence space-occupying controls. + When a SYN is present, then SEG.SEQ is the sequence number of the + SYN. + +3.4.1. Initial Sequence Number Selection + + A connection is defined by a pair of sockets. Connections can be + reused. New instances of a connection will be referred to as + incarnations of the connection. The problem that arises from this is + -- "how does the TCP implementation identify duplicate segments from + previous incarnations of the connection?" This problem becomes + apparent if the connection is being opened and closed in quick + succession, or if the connection breaks with loss of memory and is + then reestablished. To support this, the TIME-WAIT state limits the + rate of connection reuse, while the initial sequence number selection + described below further protects against ambiguity about which + incarnation of a connection an incoming packet corresponds to. + + To avoid confusion, we must prevent segments from one incarnation of + a connection from being used while the same sequence numbers may + still be present in the network from an earlier incarnation. We want + to assure this even if a TCP endpoint loses all knowledge of the + sequence numbers it has been using. When new connections are + created, an initial sequence number (ISN) generator is employed that + selects a new 32-bit ISN. There are security issues that result if + an off-path attacker is able to predict or guess ISN values [42]. + + TCP initial sequence numbers are generated from a number sequence + that monotonically increases until it wraps, known loosely as a + "clock". This clock is a 32-bit counter that typically increments at + least once every roughly 4 microseconds, although it is neither + assumed to be realtime nor precise, and need not persist across + reboots. The clock component is intended to ensure that with a + Maximum Segment Lifetime (MSL), generated ISNs will be unique since + it cycles approximately every 4.55 hours, which is much longer than + the MSL. Please note that for modern networks that support high data + rates where the connection might start and quickly advance sequence + numbers to overlap within the MSL, it is recommended to implement the + Timestamp Option as mentioned later in Section 3.4.3. + + A TCP implementation MUST use the above type of "clock" for clock- + driven selection of initial sequence numbers (MUST-8), and SHOULD + generate its initial sequence numbers with the expression: + + ISN = M + F(localip, localport, remoteip, remoteport, secretkey) + + where M is the 4 microsecond timer, and F() is a pseudorandom + function (PRF) of the connection's identifying parameters ("localip, + localport, remoteip, remoteport") and a secret key ("secretkey") + (SHLD-1). F() MUST NOT be computable from the outside (MUST-9), or + an attacker could still guess at sequence numbers from the ISN used + for some other connection. The PRF could be implemented as a + cryptographic hash of the concatenation of the TCP connection + parameters and some secret data. For discussion of the selection of + a specific hash algorithm and management of the secret key data, + please see Section 3 of [42]. + + For each connection there is a send sequence number and a receive + sequence number. The initial send sequence number (ISS) is chosen by + the data sending TCP peer, and the initial receive sequence number + (IRS) is learned during the connection-establishing procedure. + + For a connection to be established or initialized, the two TCP peers + must synchronize on each other's initial sequence numbers. This is + done in an exchange of connection-establishing segments carrying a + control bit called "SYN" (for synchronize) and the initial sequence + numbers. As a shorthand, segments carrying the SYN bit are also + called "SYNs". Hence, the solution requires a suitable mechanism for + picking an initial sequence number and a slightly involved handshake + to exchange the ISNs. + + The synchronization requires each side to send its own initial + sequence number and to receive a confirmation of it in acknowledgment + from the remote TCP peer. Each side must also receive the remote + peer's initial sequence number and send a confirming acknowledgment. + + 1) A --> B SYN my sequence number is X + 2) A <-- B ACK your sequence number is X + 3) A <-- B SYN my sequence number is Y + 4) A --> B ACK your sequence number is Y + + Because steps 2 and 3 can be combined in a single message this is + called the three-way (or three message) handshake (3WHS). + + A 3WHS is necessary because sequence numbers are not tied to a global + clock in the network, and TCP implementations may have different + mechanisms for picking the ISNs. The receiver of the first SYN has + no way of knowing whether the segment was an old one or not, unless + it remembers the last sequence number used on the connection (which + is not always possible), and so it must ask the sender to verify this + SYN. The three-way handshake and the advantages of a clock-driven + scheme for ISN selection are discussed in [69]. + +3.4.2. Knowing When to Keep Quiet + + A theoretical problem exists where data could be corrupted due to + confusion between old segments in the network and new ones after a + host reboots if the same port numbers and sequence space are reused. + The "quiet time" concept discussed below addresses this, and the + discussion of it is included for situations where it might be + relevant, although it is not felt to be necessary in most current + implementations. The problem was more relevant earlier in the + history of TCP. In practical use on the Internet today, the error- + prone conditions are sufficiently unlikely that it is safe to ignore. + Reasons why it is now negligible include: (a) ISS and ephemeral port + randomization have reduced likelihood of reuse of port numbers and + sequence numbers after reboots, (b) the effective MSL of the Internet + has declined as links have become faster, and (c) reboots often + taking longer than an MSL anyways. + + To be sure that a TCP implementation does not create a segment + carrying a sequence number that may be duplicated by an old segment + remaining in the network, the TCP endpoint must keep quiet for an MSL + before assigning any sequence numbers upon starting up or recovering + from a situation where memory of sequence numbers in use was lost. + For this specification the MSL is taken to be 2 minutes. This is an + engineering choice, and may be changed if experience indicates it is + desirable to do so. Note that if a TCP endpoint is reinitialized in + some sense, yet retains its memory of sequence numbers in use, then + it need not wait at all; it must only be sure to use sequence numbers + larger than those recently used. + +3.4.3. The TCP Quiet Time Concept + + Hosts that for any reason lose knowledge of the last sequence numbers + transmitted on each active (i.e., not closed) connection shall delay + emitting any TCP segments for at least the agreed MSL in the internet + system that the host is a part of. In the paragraphs below, an + explanation for this specification is given. TCP implementers may + violate the "quiet time" restriction, but only at the risk of causing + some old data to be accepted as new or new data rejected as old + duplicated data by some receivers in the internet system. + + TCP endpoints consume sequence number space each time a segment is + formed and entered into the network output queue at a source host. + The duplicate detection and sequencing algorithm in TCP relies on the + unique binding of segment data to sequence space to the extent that + sequence numbers will not cycle through all 2^32 values before the + segment data bound to those sequence numbers has been delivered and + acknowledged by the receiver and all duplicate copies of the segments + have "drained" from the internet. Without such an assumption, two + distinct TCP segments could conceivably be assigned the same or + overlapping sequence numbers, causing confusion at the receiver as to + which data is new and which is old. Remember that each segment is + bound to as many consecutive sequence numbers as there are octets of + data and SYN or FIN flags in the segment. + + Under normal conditions, TCP implementations keep track of the next + sequence number to emit and the oldest awaiting acknowledgment so as + to avoid mistakenly reusing a sequence number before its first use + has been acknowledged. This alone does not guarantee that old + duplicate data is drained from the net, so the sequence space has + been made large to reduce the probability that a wandering duplicate + will cause trouble upon arrival. At 2 megabits/sec., it takes 4.5 + hours to use up 2^32 octets of sequence space. Since the maximum + segment lifetime in the net is not likely to exceed a few tens of + seconds, this is deemed ample protection for foreseeable nets, even + if data rates escalate to 10s of megabits/sec. At 100 megabits/sec., + the cycle time is 5.4 minutes, which may be a little short but still + within reason. Much higher data rates are possible today, with + implications described in the final paragraph of this subsection. + + The basic duplicate detection and sequencing algorithm in TCP can be + defeated, however, if a source TCP endpoint does not have any memory + of the sequence numbers it last used on a given connection. For + example, if the TCP implementation were to start all connections with + sequence number 0, then upon the host rebooting, a TCP peer might re- + form an earlier connection (possibly after half-open connection + resolution) and emit packets with sequence numbers identical to or + overlapping with packets still in the network, which were emitted on + an earlier incarnation of the same connection. In the absence of + knowledge about the sequence numbers used on a particular connection, + the TCP specification recommends that the source delay for MSL + seconds before emitting segments on the connection, to allow time for + segments from the earlier connection incarnation to drain from the + system. + + Even hosts that can remember the time of day and use it to select + initial sequence number values are not immune from this problem + (i.e., even if time of day is used to select an initial sequence + number for each new connection incarnation). + + Suppose, for example, that a connection is opened starting with + sequence number S. Suppose that this connection is not used much and + that eventually the initial sequence number function (ISN(t)) takes + on a value equal to the sequence number, say S1, of the last segment + sent by this TCP endpoint on a particular connection. Now suppose, + at this instant, the host reboots and establishes a new incarnation + of the connection. The initial sequence number chosen is S1 = ISN(t) + -- last used sequence number on old incarnation of connection! If + the recovery occurs quickly enough, any old duplicates in the net + bearing sequence numbers in the neighborhood of S1 may arrive and be + treated as new packets by the receiver of the new incarnation of the + connection. + + The problem is that the recovering host may not know for how long it + was down between rebooting nor does it know whether there are still + old duplicates in the system from earlier connection incarnations. + + One way to deal with this problem is to deliberately delay emitting + segments for one MSL after recovery from a reboot -- this is the + "quiet time" specification. Hosts that prefer to avoid waiting and + are willing to risk possible confusion of old and new packets at a + given destination may choose not to wait for the "quiet time". + Implementers may provide TCP users with the ability to select on a + connection-by-connection basis whether to wait after a reboot, or may + informally implement the "quiet time" for all connections. + Obviously, even where a user selects to "wait", this is not necessary + after the host has been "up" for at least MSL seconds. + + To summarize: every segment emitted occupies one or more sequence + numbers in the sequence space, and the numbers occupied by a segment + are "busy" or "in use" until MSL seconds have passed. Upon + rebooting, a block of space-time is occupied by the octets and SYN or + FIN flags of any potentially still in-flight segments. If a new + connection is started too soon and uses any of the sequence numbers + in the space-time footprint of those potentially still in-flight + segments of the previous connection incarnation, there is a potential + sequence number overlap area that could cause confusion at the + receiver. + + High-performance cases will have shorter cycle times than those in + the megabits per second that the base TCP design described above + considers. At 1 Gbps, the cycle time is 34 seconds, only 3 seconds + at 10 Gbps, and around a third of a second at 100 Gbps. In these + higher-performance cases, TCP Timestamp Options and Protection + Against Wrapped Sequences (PAWS) [47] provide the needed capability + to detect and discard old duplicates. + +3.5. Establishing a Connection + + The "three-way handshake" is the procedure used to establish a + connection. This procedure normally is initiated by one TCP peer and + responded to by another TCP peer. The procedure also works if two + TCP peers simultaneously initiate the procedure. When simultaneous + open occurs, each TCP peer receives a SYN segment that carries no + acknowledgment after it has sent a SYN. Of course, the arrival of an + old duplicate SYN segment can potentially make it appear, to the + recipient, that a simultaneous connection initiation is in progress. + Proper use of "reset" segments can disambiguate these cases. + + Several examples of connection initiation follow. Although these + examples do not show connection synchronization using data-carrying + segments, this is perfectly legitimate, so long as the receiving TCP + endpoint doesn't deliver the data to the user until it is clear the + data is valid (e.g., the data is buffered at the receiver until the + connection reaches the ESTABLISHED state, given that the three-way + handshake reduces the possibility of false connections). It is a + trade-off between memory and messages to provide information for this + checking. + + The simplest 3WHS is shown in Figure 6. The figures should be + interpreted in the following way. Each line is numbered for + reference purposes. Right arrows (-->) indicate departure of a TCP + segment from TCP Peer A to TCP Peer B or arrival of a segment at B + from A. Left arrows (<--) indicate the reverse. Ellipses (...) + indicate a segment that is still in the network (delayed). Comments + appear in parentheses. TCP connection states represent the state + AFTER the departure or arrival of the segment (whose contents are + shown in the center of each line). Segment contents are shown in + abbreviated form, with sequence number, control flags, and ACK field. + Other fields such as window, addresses, lengths, and text have been + left out in the interest of clarity. + + TCP Peer A TCP Peer B + + 1. CLOSED LISTEN + + 2. SYN-SENT --> --> SYN-RECEIVED + + 3. ESTABLISHED <-- <-- SYN-RECEIVED + + 4. ESTABLISHED --> --> ESTABLISHED + + 5. ESTABLISHED --> --> ESTABLISHED + + Figure 6: Basic Three-Way Handshake for Connection Synchronization + + In line 2 of Figure 6, TCP Peer A begins by sending a SYN segment + indicating that it will use sequence numbers starting with sequence + number 100. In line 3, TCP Peer B sends a SYN and acknowledges the + SYN it received from TCP Peer A. Note that the acknowledgment field + indicates TCP Peer B is now expecting to hear sequence 101, + acknowledging the SYN that occupied sequence 100. + + At line 4, TCP Peer A responds with an empty segment containing an + ACK for TCP Peer B's SYN; and in line 5, TCP Peer A sends some data. + Note that the sequence number of the segment in line 5 is the same as + in line 4 because the ACK does not occupy sequence number space (if + it did, we would wind up ACKing ACKs!). + + Simultaneous initiation is only slightly more complex, as is shown in + Figure 7. Each TCP peer's connection state cycles from CLOSED to + SYN-SENT to SYN-RECEIVED to ESTABLISHED. + + TCP Peer A TCP Peer B + + 1. CLOSED CLOSED + + 2. SYN-SENT --> ... + + 3. SYN-RECEIVED <-- <-- SYN-SENT + + 4. ... --> SYN-RECEIVED + + 5. SYN-RECEIVED --> ... + + 6. ESTABLISHED <-- <-- SYN-RECEIVED + + 7. ... --> ESTABLISHED + + Figure 7: Simultaneous Connection Synchronization + + A TCP implementation MUST support simultaneous open attempts (MUST- + 10). + + Note that a TCP implementation MUST keep track of whether a + connection has reached SYN-RECEIVED state as the result of a passive + OPEN or an active OPEN (MUST-11). + + The principal reason for the three-way handshake is to prevent old + duplicate connection initiations from causing confusion. To deal + with this, a special control message, reset, is specified. If the + receiving TCP peer is in a non-synchronized state (i.e., SYN-SENT, + SYN-RECEIVED), it returns to LISTEN on receiving an acceptable reset. + If the TCP peer is in one of the synchronized states (ESTABLISHED, + FIN-WAIT-1, FIN-WAIT-2, CLOSE-WAIT, CLOSING, LAST-ACK, TIME-WAIT), it + aborts the connection and informs its user. We discuss this latter + case under "half-open" connections below. + + TCP Peer A TCP Peer B + + 1. CLOSED LISTEN + + 2. SYN-SENT --> ... + + 3. (duplicate) ... --> SYN-RECEIVED + + 4. SYN-SENT <-- <-- SYN-RECEIVED + + 5. SYN-SENT --> --> LISTEN + + 6. ... --> SYN-RECEIVED + + 7. ESTABLISHED <-- <-- SYN-RECEIVED + + 8. ESTABLISHED --> --> ESTABLISHED + + Figure 8: Recovery from Old Duplicate SYN + + As a simple example of recovery from old duplicates, consider + Figure 8. At line 3, an old duplicate SYN arrives at TCP Peer B. + TCP Peer B cannot tell that this is an old duplicate, so it responds + normally (line 4). TCP Peer A detects that the ACK field is + incorrect and returns a RST (reset) with its SEQ field selected to + make the segment believable. TCP Peer B, on receiving the RST, + returns to the LISTEN state. When the original SYN finally arrives + at line 6, the synchronization proceeds normally. If the SYN at line + 6 had arrived before the RST, a more complex exchange might have + occurred with RSTs sent in both directions. + +3.5.1. Half-Open Connections and Other Anomalies + + An established connection is said to be "half-open" if one of the TCP + peers has closed or aborted the connection at its end without the + knowledge of the other, or if the two ends of the connection have + become desynchronized owing to a failure or reboot that resulted in + loss of memory. Such connections will automatically become reset if + an attempt is made to send data in either direction. However, half- + open connections are expected to be unusual. + + If at site A the connection no longer exists, then an attempt by the + user at site B to send any data on it will result in the site B TCP + endpoint receiving a reset control message. Such a message indicates + to the site B TCP endpoint that something is wrong, and it is + expected to abort the connection. + + Assume that two user processes A and B are communicating with one + another when a failure or reboot occurs causing loss of memory to A's + TCP implementation. Depending on the operating system supporting A's + TCP implementation, it is likely that some error recovery mechanism + exists. When the TCP endpoint is up again, A is likely to start + again from the beginning or from a recovery point. As a result, A + will probably try to OPEN the connection again or try to SEND on the + connection it believes open. In the latter case, it receives the + error message "connection not open" from the local (A's) TCP + implementation. In an attempt to establish the connection, A's TCP + implementation will send a segment containing SYN. This scenario + leads to the example shown in Figure 9. After TCP Peer A reboots, + the user attempts to reopen the connection. TCP Peer B, in the + meantime, thinks the connection is open. + + TCP Peer A TCP Peer B + + 1. (REBOOT) (send 300,receive 100) + + 2. CLOSED ESTABLISHED + + 3. SYN-SENT --> --> (??) + + 4. (!!) <-- <-- ESTABLISHED + + 5. SYN-SENT --> --> (Abort!!) + + 6. SYN-SENT CLOSED + + 7. SYN-SENT --> --> + + Figure 9: Half-Open Connection Discovery + + When the SYN arrives at line 3, TCP Peer B, being in a synchronized + state, and the incoming segment outside the window, responds with an + acknowledgment indicating what sequence it next expects to hear (ACK + 100). TCP Peer A sees that this segment does not acknowledge + anything it sent and, being unsynchronized, sends a reset (RST) + because it has detected a half-open connection. TCP Peer B aborts at + line 5. TCP Peer A will continue to try to establish the connection; + the problem is now reduced to the basic three-way handshake of + Figure 6. + + An interesting alternative case occurs when TCP Peer A reboots and + TCP Peer B tries to send data on what it thinks is a synchronized + connection. This is illustrated in Figure 10. In this case, the + data arriving at TCP Peer A from TCP Peer B (line 2) is unacceptable + because no such connection exists, so TCP Peer A sends a RST. The + RST is acceptable so TCP Peer B processes it and aborts the + connection. + + TCP Peer A TCP Peer B + + 1. (REBOOT) (send 300,receive 100) + + 2. (??) <-- <-- ESTABLISHED + + 3. --> --> (ABORT!!) + + Figure 10: Active Side Causes Half-Open Connection Discovery + + In Figure 11, two TCP Peers A and B with passive connections waiting + for SYN are depicted. An old duplicate arriving at TCP Peer B (line + 2) stirs B into action. A SYN-ACK is returned (line 3) and causes + TCP A to generate a RST (the ACK in line 3 is not acceptable). TCP + Peer B accepts the reset and returns to its passive LISTEN state. + + TCP Peer A TCP Peer B + + 1. LISTEN LISTEN + + 2. ... --> SYN-RECEIVED + + 3. (??) <-- <-- SYN-RECEIVED + + 4. --> --> (return to LISTEN!) + + 5. LISTEN LISTEN + + Figure 11: Old Duplicate SYN Initiates a Reset on Two Passive Sockets + + A variety of other cases are possible, all of which are accounted for + by the following rules for RST generation and processing. + +3.5.2. Reset Generation + + A TCP user or application can issue a reset on a connection at any + time, though reset events are also generated by the protocol itself + when various error conditions occur, as described below. The side of + a connection issuing a reset should enter the TIME-WAIT state, as + this generally helps to reduce the load on busy servers for reasons + described in [70]. + + As a general rule, reset (RST) is sent whenever a segment arrives + that apparently is not intended for the current connection. A reset + must not be sent if it is not clear that this is the case. + + There are three groups of states: + + 1. If the connection does not exist (CLOSED), then a reset is sent + in response to any incoming segment except another reset. A SYN + segment that does not match an existing connection is rejected by + this means. + + If the incoming segment has the ACK bit set, the reset takes its + sequence number from the ACK field of the segment; otherwise, the + reset has sequence number zero and the ACK field is set to the + sum of the sequence number and segment length of the incoming + segment. The connection remains in the CLOSED state. + + 2. If the connection is in any non-synchronized state (LISTEN, SYN- + SENT, SYN-RECEIVED), and the incoming segment acknowledges + something not yet sent (the segment carries an unacceptable ACK), + or if an incoming segment has a security level or compartment + (Appendix A.1) that does not exactly match the level and + compartment requested for the connection, a reset is sent. + + If the incoming segment has an ACK field, the reset takes its + sequence number from the ACK field of the segment; otherwise, the + reset has sequence number zero and the ACK field is set to the + sum of the sequence number and segment length of the incoming + segment. The connection remains in the same state. + + 3. If the connection is in a synchronized state (ESTABLISHED, FIN- + WAIT-1, FIN-WAIT-2, CLOSE-WAIT, CLOSING, LAST-ACK, TIME-WAIT), + any unacceptable segment (out-of-window sequence number or + unacceptable acknowledgment number) must be responded to with an + empty acknowledgment segment (without any user data) containing + the current send sequence number and an acknowledgment indicating + the next sequence number expected to be received, and the + connection remains in the same state. + + If an incoming segment has a security level or compartment that + does not exactly match the level and compartment requested for + the connection, a reset is sent and the connection goes to the + CLOSED state. The reset takes its sequence number from the ACK + field of the incoming segment. + +3.5.3. Reset Processing + + In all states except SYN-SENT, all reset (RST) segments are validated + by checking their SEQ fields. A reset is valid if its sequence + number is in the window. In the SYN-SENT state (a RST received in + response to an initial SYN), the RST is acceptable if the ACK field + acknowledges the SYN. + + The receiver of a RST first validates it, then changes state. If the + receiver was in the LISTEN state, it ignores it. If the receiver was + in SYN-RECEIVED state and had previously been in the LISTEN state, + then the receiver returns to the LISTEN state; otherwise, the + receiver aborts the connection and goes to the CLOSED state. If the + receiver was in any other state, it aborts the connection and advises + the user and goes to the CLOSED state. + + TCP implementations SHOULD allow a received RST segment to include + data (SHLD-2). It has been suggested that a RST segment could + contain diagnostic data that explains the cause of the RST. No + standard has yet been established for such data. + +3.6. Closing a Connection + + CLOSE is an operation meaning "I have no more data to send." The + notion of closing a full-duplex connection is subject to ambiguous + interpretation, of course, since it may not be obvious how to treat + the receiving side of the connection. We have chosen to treat CLOSE + in a simplex fashion. The user who CLOSEs may continue to RECEIVE + until the TCP receiver is told that the remote peer has CLOSED also. + Thus, a program could initiate several SENDs followed by a CLOSE, and + then continue to RECEIVE until signaled that a RECEIVE failed because + the remote peer has CLOSED. The TCP implementation will signal a + user, even if no RECEIVEs are outstanding, that the remote peer has + closed, so the user can terminate their side gracefully. A TCP + implementation will reliably deliver all buffers SENT before the + connection was CLOSED so a user who expects no data in return need + only wait to hear the connection was CLOSED successfully to know that + all their data was received at the destination TCP endpoint. Users + must keep reading connections they close for sending until the TCP + implementation indicates there is no more data. + + There are essentially three cases: + + 1) The user initiates by telling the TCP implementation to CLOSE the + connection (TCP Peer A in Figure 12). + + 2) The remote TCP endpoint initiates by sending a FIN control signal + (TCP Peer B in Figure 12). + + 3) Both users CLOSE simultaneously (Figure 13). + + Case 1: Local user initiates the close + + In this case, a FIN segment can be constructed and placed on the + outgoing segment queue. No further SENDs from the user will be + accepted by the TCP implementation, and it enters the FIN-WAIT-1 + state. RECEIVEs are allowed in this state. All segments + preceding and including FIN will be retransmitted until + acknowledged. When the other TCP peer has both acknowledged the + FIN and sent a FIN of its own, the first TCP peer can ACK this + FIN. Note that a TCP endpoint receiving a FIN will ACK but not + send its own FIN until its user has CLOSED the connection also. + + Case 2: TCP endpoint receives a FIN from the network + + If an unsolicited FIN arrives from the network, the receiving TCP + endpoint can ACK it and tell the user that the connection is + closing. The user will respond with a CLOSE, upon which the TCP + endpoint can send a FIN to the other TCP peer after sending any + remaining data. The TCP endpoint then waits until its own FIN is + acknowledged whereupon it deletes the connection. If an ACK is + not forthcoming, after the user timeout the connection is aborted + and the user is told. + + Case 3: Both users close simultaneously + + A simultaneous CLOSE by users at both ends of a connection causes + FIN segments to be exchanged (Figure 13). When all segments + preceding the FINs have been processed and acknowledged, each TCP + peer can ACK the FIN it has received. Both will, upon receiving + these ACKs, delete the connection. + + TCP Peer A TCP Peer B + + 1. ESTABLISHED ESTABLISHED + + 2. (Close) + FIN-WAIT-1 --> --> CLOSE-WAIT + + 3. FIN-WAIT-2 <-- <-- CLOSE-WAIT + + 4. (Close) + TIME-WAIT <-- <-- LAST-ACK + + 5. TIME-WAIT --> --> CLOSED + + 6. (2 MSL) + CLOSED + + Figure 12: Normal Close Sequence + + TCP Peer A TCP Peer B + + 1. ESTABLISHED ESTABLISHED + + 2. (Close) (Close) + FIN-WAIT-1 --> ... FIN-WAIT-1 + <-- <-- + ... --> + + 3. CLOSING --> ... CLOSING + <-- <-- + ... --> + + 4. TIME-WAIT TIME-WAIT + (2 MSL) (2 MSL) + CLOSED CLOSED + + Figure 13: Simultaneous Close Sequence + + A TCP connection may terminate in two ways: (1) the normal TCP close + sequence using a FIN handshake (Figure 12), and (2) an "abort" in + which one or more RST segments are sent and the connection state is + immediately discarded. If the local TCP connection is closed by the + remote side due to a FIN or RST received from the remote side, then + the local application MUST be informed whether it closed normally or + was aborted (MUST-12). + + +3.6.1. Half-Closed Connections + + The normal TCP close sequence delivers buffered data reliably in both + directions. Since the two directions of a TCP connection are closed + independently, it is possible for a connection to be "half closed", + i.e., closed in only one direction, and a host is permitted to + continue sending data in the open direction on a half-closed + connection. + + A host MAY implement a "half-duplex" TCP close sequence, so that an + application that has called CLOSE cannot continue to read data from + the connection (MAY-1). If such a host issues a CLOSE call while + received data is still pending in the TCP connection, or if new data + is received after CLOSE is called, its TCP implementation SHOULD send + a RST to show that data was lost (SHLD-3). See [23], Section 2.17 + for discussion. + + When a connection is closed actively, it MUST linger in the TIME-WAIT + state for a time 2xMSL (Maximum Segment Lifetime) (MUST-13). + However, it MAY accept a new SYN from the remote TCP endpoint to + reopen the connection directly from TIME-WAIT state (MAY-2), if it: + + (1) assigns its initial sequence number for the new connection to be + larger than the largest sequence number it used on the previous + connection incarnation, and + + (2) returns to TIME-WAIT state if the SYN turns out to be an old + duplicate. + + When the TCP Timestamp Options are available, an improved algorithm + is described in [40] in order to support higher connection + establishment rates. This algorithm for reducing TIME-WAIT is a Best + Current Practice that SHOULD be implemented since Timestamp Options + are commonly used, and using them to reduce TIME-WAIT provides + benefits for busy Internet servers (SHLD-4). + +3.7. Segmentation + + The term "segmentation" refers to the activity TCP performs when + ingesting a stream of bytes from a sending application and + packetizing that stream of bytes into TCP segments. Individual TCP + segments often do not correspond one-for-one to individual send (or + socket write) calls from the application. Applications may perform + writes at the granularity of messages in the upper-layer protocol, + but TCP guarantees no correlation between the boundaries of TCP + segments sent and received and the boundaries of the read or write + buffers of user application data. In some specific protocols, such + as Remote Direct Memory Access (RDMA) using Direct Data Placement + (DDP) and Marker PDU Aligned Framing (MPA) [34], there are + performance optimizations possible when the relation between TCP + segments and application data units can be controlled, and MPA + includes a specific mechanism for detecting and verifying this + relationship between TCP segments and application message data + structures, but this is specific to applications like RDMA. In + general, multiple goals influence the sizing of TCP segments created + by a TCP implementation. + + Goals driving the sending of larger segments include: + + * Reducing the number of packets in flight within the network. + + * Increasing processing efficiency and potential performance by + enabling a smaller number of interrupts and inter-layer + interactions. + + * Limiting the overhead of TCP headers. + + Note that the performance benefits of sending larger segments may + decrease as the size increases, and there may be boundaries where + advantages are reversed. For instance, on some implementation + architectures, 1025 bytes within a segment could lead to worse + performance than 1024 bytes, due purely to data alignment on copy + operations. + + Goals driving the sending of smaller segments include: + + * Avoiding sending a TCP segment that would result in an IP datagram + larger than the smallest MTU along an IP network path because this + results in either packet loss or packet fragmentation. Making + matters worse, some firewalls or middleboxes may drop fragmented + packets or ICMP messages related to fragmentation. + + * Preventing delays to the application data stream, especially when + TCP is waiting on the application to generate more data, or when + the application is waiting on an event or input from its peer in + order to generate more data. + + * Enabling "fate sharing" between TCP segments and lower-layer data + units (e.g., below IP, for links with cell or frame sizes smaller + than the IP MTU). + + Towards meeting these competing sets of goals, TCP includes several + mechanisms, including the Maximum Segment Size Option, Path MTU + Discovery, the Nagle algorithm, and support for IPv6 Jumbograms, as + discussed in the following subsections. + +3.7.1. Maximum Segment Size Option + + TCP endpoints MUST implement both sending and receiving the MSS + Option (MUST-14). + + TCP implementations SHOULD send an MSS Option in every SYN segment + when its receive MSS differs from the default 536 for IPv4 or 1220 + for IPv6 (SHLD-5), and MAY send it always (MAY-3). + + If an MSS Option is not received at connection setup, TCP + implementations MUST assume a default send MSS of 536 (576 - 40) for + IPv4 or 1220 (1280 - 60) for IPv6 (MUST-15). + + The maximum size of a segment that a TCP endpoint really sends, the + "effective send MSS", MUST be the smaller (MUST-16) of the send MSS + (that reflects the available reassembly buffer size at the remote + host, the EMTU_R [19]) and the largest transmission size permitted by + the IP layer (EMTU_S [19]): + + Eff.snd.MSS = min(SendMSS+20, MMS_S) - TCPhdrsize - IPoptionsize + + where: + + * SendMSS is the MSS value received from the remote host, or the + default 536 for IPv4 or 1220 for IPv6, if no MSS Option is + received. + + * MMS_S is the maximum size for a transport-layer message that TCP + may send. + + * TCPhdrsize is the size of the fixed TCP header and any options. + This is 20 in the (rare) case that no options are present but may + be larger if TCP Options are to be sent. Note that some options + might not be included on all segments, but that for each segment + sent, the sender should adjust the data length accordingly, within + the Eff.snd.MSS. + + * IPoptionsize is the size of any IPv4 options or IPv6 extension + headers associated with a TCP connection. Note that some options + or extension headers might not be included on all packets, but + that for each segment sent, the sender should adjust the data + length accordingly, within the Eff.snd.MSS. + + The MSS value to be sent in an MSS Option should be equal to the + effective MTU minus the fixed IP and TCP headers. By ignoring both + IP and TCP Options when calculating the value for the MSS Option, if + there are any IP or TCP Options to be sent in a packet, then the + sender must decrease the size of the TCP data accordingly. RFC 6691 + [43] discusses this in greater detail. + + The MSS value to be sent in an MSS Option must be less than or equal + to: + + MMS_R - 20 + + where MMS_R is the maximum size for a transport-layer message that + can be received (and reassembled at the IP layer) (MUST-67). TCP + obtains MMS_R and MMS_S from the IP layer; see the generic call + GET_MAXSIZES in Section 3.4 of RFC 1122. These are defined in terms + of their IP MTU equivalents, EMTU_R and EMTU_S [19]. + + When TCP is used in a situation where either the IP or TCP headers + are not fixed, the sender must reduce the amount of TCP data in any + given packet by the number of octets used by the IP and TCP options. + This has been a point of confusion historically, as explained in RFC + 6691, Section 3.1. + +3.7.2. Path MTU Discovery + + A TCP implementation may be aware of the MTU on directly connected + links, but will rarely have insight about MTUs across an entire + network path. For IPv4, RFC 1122 recommends an IP-layer default + effective MTU of less than or equal to 576 for destinations not + directly connected, and for IPv6 this would be 1280. Using these + fixed values limits TCP connection performance and efficiency. + Instead, implementation of Path MTU Discovery (PMTUD) and + Packetization Layer Path MTU Discovery (PLPMTUD) is strongly + recommended in order for TCP to improve segmentation decisions. Both + PMTUD and PLPMTUD help TCP choose segment sizes that avoid both on- + path (for IPv4) and source fragmentation (IPv4 and IPv6). + + PMTUD for IPv4 [2] or IPv6 [14] is implemented in conjunction between + TCP, IP, and ICMP. It relies both on avoiding source fragmentation + and setting the IPv4 DF (don't fragment) flag, the latter to inhibit + on-path fragmentation. It relies on ICMP errors from routers along + the path whenever a segment is too large to traverse a link. Several + adjustments to a TCP implementation with PMTUD are described in RFC + 2923 in order to deal with problems experienced in practice [27]. + PLPMTUD [31] is a Standards Track improvement to PMTUD that relaxes + the requirement for ICMP support across a path, and improves + performance in cases where ICMP is not consistently conveyed, but + still tries to avoid source fragmentation. The mechanisms in all + four of these RFCs are recommended to be included in TCP + implementations. + + The TCP MSS Option specifies an upper bound for the size of packets + that can be received (see [43]). Hence, setting the value in the MSS + Option too small can impact the ability for PMTUD or PLPMTUD to find + a larger path MTU. RFC 1191 discusses this implication of many older + TCP implementations setting the TCP MSS to 536 (corresponding to the + IPv4 576 byte default MTU) for non-local destinations, rather than + deriving it from the MTUs of connected interfaces as recommended. + +3.7.3. Interfaces with Variable MTU Values + + The effective MTU can sometimes vary, as when used with variable + compression, e.g., RObust Header Compression (ROHC) [37]. It is + tempting for a TCP implementation to advertise the largest possible + MSS, to support the most efficient use of compressed payloads. + Unfortunately, some compression schemes occasionally need to transmit + full headers (and thus smaller payloads) to resynchronize state at + their endpoint compressors/decompressors. If the largest MTU is used + to calculate the value to advertise in the MSS Option, TCP + retransmission may interfere with compressor resynchronization. + + As a result, when the effective MTU of an interface varies packet-to- + packet, TCP implementations SHOULD use the smallest effective MTU of + the interface to calculate the value to advertise in the MSS Option + (SHLD-6). + +3.7.4. Nagle Algorithm + + The "Nagle algorithm" was described in RFC 896 [17] and was + recommended in RFC 1122 [19] for mitigation of an early problem of + too many small packets being generated. It has been implemented in + most current TCP code bases, sometimes with minor variations (see + Appendix A.3). + + If there is unacknowledged data (i.e., SND.NXT > SND.UNA), then the + sending TCP endpoint buffers all user data (regardless of the PSH + bit) until the outstanding data has been acknowledged or until the + TCP endpoint can send a full-sized segment (Eff.snd.MSS bytes). + + A TCP implementation SHOULD implement the Nagle algorithm to coalesce + short segments (SHLD-7). However, there MUST be a way for an + application to disable the Nagle algorithm on an individual + connection (MUST-17). In all cases, sending data is also subject to + the limitation imposed by the slow start algorithm [8]. + + Since there can be problematic interactions between the Nagle + algorithm and delayed acknowledgments, some implementations use minor + variations of the Nagle algorithm, such as the one described in + Appendix A.3. + +3.7.5. IPv6 Jumbograms + + In order to support TCP over IPv6 Jumbograms, implementations need to + be able to send TCP segments larger than the 64-KB limit that the MSS + Option can convey. RFC 2675 [24] defines that an MSS value of 65,535 + bytes is to be treated as infinity, and Path MTU Discovery [14] is + used to determine the actual MSS. + + The Jumbo Payload Option need not be implemented or understood by + IPv6 nodes that do not support attachment to links with an MTU + greater than 65,575 [24], and the present IPv6 Node Requirements does + not include support for Jumbograms [55]. + +3.8. Data Communication + + Once the connection is established, data is communicated by the + exchange of segments. Because segments may be lost due to errors + (checksum test failure) or network congestion, TCP uses + retransmission to ensure delivery of every segment. Duplicate + segments may arrive due to network or TCP retransmission. As + discussed in the section on sequence numbers (Section 3.4), the TCP + implementation performs certain tests on the sequence and + acknowledgment numbers in the segments to verify their acceptability. + + The sender of data keeps track of the next sequence number to use in + the variable SND.NXT. The receiver of data keeps track of the next + sequence number to expect in the variable RCV.NXT. The sender of + data keeps track of the oldest unacknowledged sequence number in the + variable SND.UNA. If the data flow is momentarily idle and all data + sent has been acknowledged, then the three variables will be equal. + + When the sender creates a segment and transmits it, the sender + advances SND.NXT. When the receiver accepts a segment, it advances + RCV.NXT and sends an acknowledgment. When the data sender receives + an acknowledgment, it advances SND.UNA. The extent to which the + values of these variables differ is a measure of the delay in the + communication. The amount by which the variables are advanced is the + length of the data and SYN or FIN flags in the segment. Note that, + once in the ESTABLISHED state, all segments must carry current + acknowledgment information. + + The CLOSE user call implies a push function (see Section 3.9.1), as + does the FIN control flag in an incoming segment. + +3.8.1. Retransmission Timeout + + Because of the variability of the networks that compose an + internetwork system and the wide range of uses of TCP connections, + the retransmission timeout (RTO) must be dynamically determined. + + The RTO MUST be computed according to the algorithm in [10], + including Karn's algorithm for taking RTT samples (MUST-18). + + RFC 793 contains an early example procedure for computing the RTO, + based on work mentioned in IEN 177 [71]. This was then replaced by + the algorithm described in RFC 1122, which was subsequently updated + in RFC 2988 and then again in RFC 6298. + + RFC 1122 allows that if a retransmitted packet is identical to the + original packet (which implies not only that the data boundaries have + not changed, but also that none of the headers have changed), then + the same IPv4 Identification field MAY be used (see Section 3.2.1.5 + of RFC 1122) (MAY-4). The same IP Identification field may be reused + anyways since it is only meaningful when a datagram is fragmented + [44]. TCP implementations should not rely on or typically interact + with this IPv4 header field in any way. It is not a reasonable way + to indicate duplicate sent segments nor to identify duplicate + received segments. + +3.8.2. TCP Congestion Control + + RFC 2914 [5] explains the importance of congestion control for the + Internet. + + RFC 1122 required implementation of Van Jacobson's congestion control + algorithms slow start and congestion avoidance together with + exponential backoff for successive RTO values for the same segment. + RFC 2581 provided IETF Standards Track description of slow start and + congestion avoidance, along with fast retransmit and fast recovery. + RFC 5681 is the current description of these algorithms and is the + current Standards Track specification providing guidelines for TCP + congestion control. RFC 6298 describes exponential backoff of RTO + values, including keeping the backed-off value until a subsequent + segment with new data has been sent and acknowledged without + retransmission. + + A TCP endpoint MUST implement the basic congestion control algorithms + slow start, congestion avoidance, and exponential backoff of RTO to + avoid creating congestion collapse conditions (MUST-19). RFC 5681 + and RFC 6298 describe the basic algorithms on the IETF Standards + Track that are broadly applicable. Multiple other suitable + algorithms exist and have been widely used. Many TCP implementations + support a set of alternative algorithms that can be configured for + use on the endpoint. An endpoint MAY implement such alternative + algorithms provided that the algorithms are conformant with the TCP + specifications from the IETF Standards Track as described in RFC + 2914, RFC 5033 [7], and RFC 8961 [15] (MAY-18). + + Explicit Congestion Notification (ECN) was defined in RFC 3168 and is + an IETF Standards Track enhancement that has many benefits [51]. + + A TCP endpoint SHOULD implement ECN as described in RFC 3168 (SHLD- + 8). + +3.8.3. TCP Connection Failures + + Excessive retransmission of the same segment by a TCP endpoint + indicates some failure of the remote host or the internetwork path. + This failure may be of short or long duration. The following + procedure MUST be used to handle excessive retransmissions of data + segments (MUST-20): + + (a) There are two thresholds R1 and R2 measuring the amount of + retransmission that has occurred for the same segment. R1 and + R2 might be measured in time units or as a count of + retransmissions (with the current RTO and corresponding backoffs + as a conversion factor, if needed). + + (b) When the number of transmissions of the same segment reaches or + exceeds threshold R1, pass negative advice (see Section 3.3.1.4 + of [19]) to the IP layer, to trigger dead-gateway diagnosis. + + (c) When the number of transmissions of the same segment reaches a + threshold R2 greater than R1, close the connection. + + (d) An application MUST (MUST-21) be able to set the value for R2 + for a particular connection. For example, an interactive + application might set R2 to "infinity", giving the user control + over when to disconnect. + + (e) TCP implementations SHOULD inform the application of the + delivery problem (unless such information has been disabled by + the application; see the "Asynchronous Reports" section + (Section 3.9.1.8)), when R1 is reached and before R2 (SHLD-9). + This will allow a remote login application program to inform the + user, for example. + + The value of R1 SHOULD correspond to at least 3 retransmissions, at + the current RTO (SHLD-10). The value of R2 SHOULD correspond to at + least 100 seconds (SHLD-11). + + An attempt to open a TCP connection could fail with excessive + retransmissions of the SYN segment or by receipt of a RST segment or + an ICMP Port Unreachable. SYN retransmissions MUST be handled in the + general way just described for data retransmissions, including + notification of the application layer. + + However, the values of R1 and R2 may be different for SYN and data + segments. In particular, R2 for a SYN segment MUST be set large + enough to provide retransmission of the segment for at least 3 + minutes (MUST-23). The application can close the connection (i.e., + give up on the open attempt) sooner, of course. + +3.8.4. TCP Keep-Alives + + A TCP connection is said to be "idle" if for some long amount of time + there have been no incoming segments received and there is no new or + unacknowledged data to be sent. + + Implementers MAY include "keep-alives" in their TCP implementations + (MAY-5), although this practice is not universally accepted. Some + TCP implementations, however, have included a keep-alive mechanism. + To confirm that an idle connection is still active, these + implementations send a probe segment designed to elicit a response + from the TCP peer. Such a segment generally contains SEG.SEQ = + SND.NXT-1 and may or may not contain one garbage octet of data. If + keep-alives are included, the application MUST be able to turn them + on or off for each TCP connection (MUST-24), and they MUST default to + off (MUST-25). + + Keep-alive packets MUST only be sent when no sent data is + outstanding, and no data or acknowledgment packets have been received + for the connection within an interval (MUST-26). This interval MUST + be configurable (MUST-27) and MUST default to no less than two hours + (MUST-28). + + It is extremely important to remember that ACK segments that contain + no data are not reliably transmitted by TCP. Consequently, if a + keep-alive mechanism is implemented it MUST NOT interpret failure to + respond to any specific probe as a dead connection (MUST-29). + + An implementation SHOULD send a keep-alive segment with no data + (SHLD-12); however, it MAY be configurable to send a keep-alive + segment containing one garbage octet (MAY-6), for compatibility with + erroneous TCP implementations. + +3.8.5. The Communication of Urgent Information + + As a result of implementation differences and middlebox interactions, + new applications SHOULD NOT employ the TCP urgent mechanism (SHLD- + 13). However, TCP implementations MUST still include support for the + urgent mechanism (MUST-30). Information on how some TCP + implementations interpret the urgent pointer can be found in RFC 6093 + [39]. + + The objective of the TCP urgent mechanism is to allow the sending + user to stimulate the receiving user to accept some urgent data and + to permit the receiving TCP endpoint to indicate to the receiving + user when all the currently known urgent data has been received by + the user. + + This mechanism permits a point in the data stream to be designated as + the end of urgent information. Whenever this point is in advance of + the receive sequence number (RCV.NXT) at the receiving TCP endpoint, + then the TCP implementation must tell the user to go into "urgent + mode"; when the receive sequence number catches up to the urgent + pointer, the TCP implementation must tell user to go into "normal + mode". If the urgent pointer is updated while the user is in "urgent + mode", the update will be invisible to the user. + + The method employs an urgent field that is carried in all segments + transmitted. The URG control flag indicates that the urgent field is + meaningful and must be added to the segment sequence number to yield + the urgent pointer. The absence of this flag indicates that there is + no urgent data outstanding. + + To send an urgent indication, the user must also send at least one + data octet. If the sending user also indicates a push, timely + delivery of the urgent information to the destination process is + enhanced. Note that because changes in the urgent pointer correspond + to data being written by a sending application, the urgent pointer + cannot "recede" in the sequence space, but a TCP receiver should be + robust to invalid urgent pointer values. + + A TCP implementation MUST support a sequence of urgent data of any + length (MUST-31) [19]. + + The urgent pointer MUST point to the sequence number of the octet + following the urgent data (MUST-62). + + A TCP implementation MUST (MUST-32) inform the application layer + asynchronously whenever it receives an urgent pointer and there was + previously no pending urgent data, or whenever the urgent pointer + advances in the data stream. The TCP implementation MUST (MUST-33) + provide a way for the application to learn how much urgent data + remains to be read from the connection, or at least to determine + whether more urgent data remains to be read [19]. + +3.8.6. Managing the Window + + The window sent in each segment indicates the range of sequence + numbers the sender of the window (the data receiver) is currently + prepared to accept. There is an assumption that this is related to + the data buffer space currently available for this connection. + + The sending TCP endpoint packages the data to be transmitted into + segments that fit the current window, and may repackage segments on + the retransmission queue. Such repackaging is not required but may + be helpful. + + In a connection with a one-way data flow, the window information will + be carried in acknowledgment segments that all have the same sequence + number, so there will be no way to reorder them if they arrive out of + order. This is not a serious problem, but it will allow the window + information to be on occasion temporarily based on old reports from + the data receiver. A refinement to avoid this problem is to act on + the window information from segments that carry the highest + acknowledgment number (that is, segments with an acknowledgment + number equal to or greater than the highest previously received). + + Indicating a large window encourages transmissions. If more data + arrives than can be accepted, it will be discarded. This will result + in excessive retransmissions, adding unnecessarily to the load on the + network and the TCP endpoints. Indicating a small window may + restrict the transmission of data to the point of introducing a + round-trip delay between each new segment transmitted. + + The mechanisms provided allow a TCP endpoint to advertise a large + window and to subsequently advertise a much smaller window without + having accepted that much data. This so-called "shrinking the + window" is strongly discouraged. The robustness principle [19] + dictates that TCP peers will not shrink the window themselves, but + will be prepared for such behavior on the part of other TCP peers. + + A TCP receiver SHOULD NOT shrink the window, i.e., move the right + window edge to the left (SHLD-14). However, a sending TCP peer MUST + be robust against window shrinking, which may cause the "usable + window" (see Section 3.8.6.2.1) to become negative (MUST-34). + + If this happens, the sender SHOULD NOT send new data (SHLD-15), but + SHOULD retransmit normally the old unacknowledged data between + SND.UNA and SND.UNA+SND.WND (SHLD-16). The sender MAY also + retransmit old data beyond SND.UNA+SND.WND (MAY-7), but SHOULD NOT + time out the connection if data beyond the right window edge is not + acknowledged (SHLD-17). If the window shrinks to zero, the TCP + implementation MUST probe it in the standard way (described below) + (MUST-35). + +3.8.6.1. Zero-Window Probing + + The sending TCP peer must regularly transmit at least one octet of + new data (if available), or retransmit to the receiving TCP peer even + if the send window is zero, in order to "probe" the window. This + retransmission is essential to guarantee that when either TCP peer + has a zero window the reopening of the window will be reliably + reported to the other. This is referred to as Zero-Window Probing + (ZWP) in other documents. + + Probing of zero (offered) windows MUST be supported (MUST-36). + + A TCP implementation MAY keep its offered receive window closed + indefinitely (MAY-8). As long as the receiving TCP peer continues to + send acknowledgments in response to the probe segments, the sending + TCP peer MUST allow the connection to stay open (MUST-37). This + enables TCP to function in scenarios such as the "printer ran out of + paper" situation described in Section 4.2.2.17 of [19]. The behavior + is subject to the implementation's resource management concerns, as + noted in [41]. + + When the receiving TCP peer has a zero window and a segment arrives, + it must still send an acknowledgment showing its next expected + sequence number and current window (zero). + + The transmitting host SHOULD send the first zero-window probe when a + zero window has existed for the retransmission timeout period (SHLD- + 29) (Section 3.8.1), and SHOULD increase exponentially the interval + between successive probes (SHLD-30). + +3.8.6.2. Silly Window Syndrome Avoidance + + The "Silly Window Syndrome" (SWS) is a stable pattern of small + incremental window movements resulting in extremely poor TCP + performance. Algorithms to avoid SWS are described below for both + the sending side and the receiving side. RFC 1122 contains more + detailed discussion of the SWS problem. Note that the Nagle + algorithm and the sender SWS avoidance algorithm play complementary + roles in improving performance. The Nagle algorithm discourages + sending tiny segments when the data to be sent increases in small + increments, while the SWS avoidance algorithm discourages small + segments resulting from the right window edge advancing in small + increments. + +3.8.6.2.1. Sender's Algorithm -- When to Send Data + + A TCP implementation MUST include a SWS avoidance algorithm in the + sender (MUST-38). + + The Nagle algorithm from Section 3.7.4 additionally describes how to + coalesce short segments. + + The sender's SWS avoidance algorithm is more difficult than the + receiver's because the sender does not know (directly) the receiver's + total buffer space (RCV.BUFF). An approach that has been found to + work well is for the sender to calculate Max(SND.WND), which is the + maximum send window it has seen so far on the connection, and to use + this value as an estimate of RCV.BUFF. Unfortunately, this can only + be an estimate; the receiver may at any time reduce the size of + RCV.BUFF. To avoid a resulting deadlock, it is necessary to have a + timeout to force transmission of data, overriding the SWS avoidance + algorithm. In practice, this timeout should seldom occur. + + The "usable window" is: + + U = SND.UNA + SND.WND - SND.NXT + + i.e., the offered window less the amount of data sent but not + acknowledged. If D is the amount of data queued in the sending TCP + endpoint but not yet sent, then the following set of rules is + recommended. + + Send data: + + (1) if a maximum-sized segment can be sent, i.e., if: + + min(D,U) >= Eff.snd.MSS; + + (2) or if the data is pushed and all queued data can be sent now, + i.e., if: + + [SND.NXT = SND.UNA and] PUSHed and D <= U + + (the bracketed condition is imposed by the Nagle algorithm); + + (3) or if at least a fraction Fs of the maximum window can be sent, + i.e., if: + + [SND.NXT = SND.UNA and] + + min(D,U) >= Fs * Max(SND.WND); + + (4) or if the override timeout occurs. + + Here Fs is a fraction whose recommended value is 1/2. The override + timeout should be in the range 0.1 - 1.0 seconds. It may be + convenient to combine this timer with the timer used to probe zero + windows (Section 3.8.6.1). + +3.8.6.2.2. Receiver's Algorithm -- When to Send a Window Update + + A TCP implementation MUST include a SWS avoidance algorithm in the + receiver (MUST-39). + + The receiver's SWS avoidance algorithm determines when the right + window edge may be advanced; this is customarily known as "updating + the window". This algorithm combines with the delayed ACK algorithm + (Section 3.8.6.3) to determine when an ACK segment containing the + current window will really be sent to the receiver. + + The solution to receiver SWS is to avoid advancing the right window + edge RCV.NXT+RCV.WND in small increments, even if data is received + from the network in small segments. + + Suppose the total receive buffer space is RCV.BUFF. At any given + moment, RCV.USER octets of this total may be tied up with data that + has been received and acknowledged but that the user process has not + yet consumed. When the connection is quiescent, RCV.WND = RCV.BUFF + and RCV.USER = 0. + + Keeping the right window edge fixed as data arrives and is + acknowledged requires that the receiver offer less than its full + buffer space, i.e., the receiver must specify a RCV.WND that keeps + RCV.NXT+RCV.WND constant as RCV.NXT increases. Thus, the total + buffer space RCV.BUFF is generally divided into three parts: + + |<------- RCV.BUFF ---------------->| + 1 2 3 + ----|---------|------------------|------|---- + RCV.NXT ^ + (Fixed) + + 1 - RCV.USER = data received but not yet consumed; + 2 - RCV.WND = space advertised to sender; + 3 - Reduction = space available but not yet + advertised. + + The suggested SWS avoidance algorithm for the receiver is to keep + RCV.NXT+RCV.WND fixed until the reduction satisfies: + + RCV.BUFF - RCV.USER - RCV.WND >= + + min( Fr * RCV.BUFF, Eff.snd.MSS ) + + where Fr is a fraction whose recommended value is 1/2, and + Eff.snd.MSS is the effective send MSS for the connection (see + Section 3.7.1). When the inequality is satisfied, RCV.WND is set to + RCV.BUFF-RCV.USER. + + Note that the general effect of this algorithm is to advance RCV.WND + in increments of Eff.snd.MSS (for realistic receive buffers: + Eff.snd.MSS < RCV.BUFF/2). Note also that the receiver must use its + own Eff.snd.MSS, making the assumption that it is the same as the + sender's. + +3.8.6.3. Delayed Acknowledgments -- When to Send an ACK Segment + + A host that is receiving a stream of TCP data segments can increase + efficiency in both the network and the hosts by sending fewer than + one ACK (acknowledgment) segment per data segment received; this is + known as a "delayed ACK". + + A TCP endpoint SHOULD implement a delayed ACK (SHLD-18), but an ACK + should not be excessively delayed; in particular, the delay MUST be + less than 0.5 seconds (MUST-40). An ACK SHOULD be generated for at + least every second full-sized segment or 2*RMSS bytes of new data + (where RMSS is the MSS specified by the TCP endpoint receiving the + segments to be acknowledged, or the default value if not specified) + (SHLD-19). Excessive delays on ACKs can disturb the round-trip + timing and packet "clocking" algorithms. More complete discussion of + delayed ACK behavior is in Section 4.2 of RFC 5681 [8], including + recommendations to immediately acknowledge out-of-order segments, + segments above a gap in sequence space, or segments that fill all or + part of a gap, in order to accelerate loss recovery. + + Note that there are several current practices that further lead to a + reduced number of ACKs, including generic receive offload (GRO) [72], + ACK compression, and ACK decimation [28]. + +3.9. Interfaces + + There are of course two interfaces of concern: the user/TCP interface + and the TCP/lower-level interface. We have a fairly elaborate model + of the user/TCP interface, but the interface to the lower-level + protocol module is left unspecified here since it will be specified + in detail by the specification of the lower-level protocol. For the + case that the lower level is IP, we note some of the parameter values + that TCP implementations might use. + +3.9.1. User/TCP Interface + + The following functional description of user commands to the TCP + implementation is, at best, fictional, since every operating system + will have different facilities. Consequently, we must warn readers + that different TCP implementations may have different user + interfaces. However, all TCP implementations must provide a certain + minimum set of services to guarantee that all TCP implementations can + support the same protocol hierarchy. This section specifies the + functional interfaces required of all TCP implementations. + + Section 3.1 of [53] also identifies primitives provided by TCP and + could be used as an additional reference for implementers. + + The following sections functionally characterize a user/TCP + interface. The notation used is similar to most procedure or + function calls in high-level languages, but this usage is not meant + to rule out trap-type service calls. + + The user commands described below specify the basic functions the TCP + implementation must perform to support interprocess communication. + Individual implementations must define their own exact format and may + provide combinations or subsets of the basic functions in single + calls. In particular, some implementations may wish to automatically + OPEN a connection on the first SEND or RECEIVE issued by the user for + a given connection. + + In providing interprocess communication facilities, the TCP + implementation must not only accept commands, but must also return + information to the processes it serves. The latter consists of: + + (a) general information about a connection (e.g., interrupts, remote + close, binding of unspecified remote socket). + + (b) replies to specific user commands indicating success or various + types of failure. + +3.9.1.1. Open + + Format: OPEN (local port, remote socket, active/passive [, timeout] + [, Diffserv field] [, security/compartment] [, local IP address] [, + options]) -> local connection name + + If the active/passive flag is set to passive, then this is a call to + LISTEN for an incoming connection. A passive OPEN may have either a + fully specified remote socket to wait for a particular connection or + an unspecified remote socket to wait for any call. A fully specified + passive call can be made active by the subsequent execution of a + SEND. + + A transmission control block (TCB) is created and partially filled in + with data from the OPEN command parameters. + + Every passive OPEN call either creates a new connection record in + LISTEN state, or it returns an error; it MUST NOT affect any + previously created connection record (MUST-41). + + A TCP implementation that supports multiple concurrent connections + MUST provide an OPEN call that will functionally allow an application + to LISTEN on a port while a connection block with the same local port + is in SYN-SENT or SYN-RECEIVED state (MUST-42). + + On an active OPEN command, the TCP endpoint will begin the procedure + to synchronize (i.e., establish) the connection at once. + + The timeout, if present, permits the caller to set up a timeout for + all data submitted to TCP. If data is not successfully delivered to + the destination within the timeout period, the TCP endpoint will + abort the connection. The present global default is five minutes. + + The TCP implementation or some component of the operating system will + verify the user's authority to open a connection with the specified + Diffserv field value or security/compartment. The absence of a + Diffserv field value or security/compartment specification in the + OPEN call indicates the default values must be used. + + TCP will accept incoming requests as matching only if the security/ + compartment information is exactly the same as that requested in the + OPEN call. + + The Diffserv field value indicated by the user only impacts outgoing + packets, may be altered en route through the network, and has no + direct bearing or relation to received packets. + + A local connection name will be returned to the user by the TCP + implementation. The local connection name can then be used as a + shorthand term for the connection defined by the pair. + + The optional "local IP address" parameter MUST be supported to allow + the specification of the local IP address (MUST-43). This enables + applications that need to select the local IP address used when + multihoming is present. + + A passive OPEN call with a specified "local IP address" parameter + will await an incoming connection request to that address. If the + parameter is unspecified, a passive OPEN will await an incoming + connection request to any local IP address and then bind the local IP + address of the connection to the particular address that is used. + + For an active OPEN call, a specified "local IP address" parameter + will be used for opening the connection. If the parameter is + unspecified, the host will choose an appropriate local IP address + (see RFC 1122, Section 3.3.4.2). + + If an application on a multihomed host does not specify the local IP + address when actively opening a TCP connection, then the TCP + implementation MUST ask the IP layer to select a local IP address + before sending the (first) SYN (MUST-44). See the function + GET_SRCADDR() in Section 3.4 of RFC 1122. + + At all other times, a previous segment has either been sent or + received on this connection, and TCP implementations MUST use the + same local address that was used in those previous segments (MUST- + 45). + + A TCP implementation MUST reject as an error a local OPEN call for an + invalid remote IP address (e.g., a broadcast or multicast address) + (MUST-46). + +3.9.1.2. Send + + Format: SEND (local connection name, buffer address, byte count, + URGENT flag [, PUSH flag] [, timeout]) + + This call causes the data contained in the indicated user buffer to + be sent on the indicated connection. If the connection has not been + opened, the SEND is considered an error. Some implementations may + allow users to SEND first; in which case, an automatic OPEN would be + done. For example, this might be one way for application data to be + included in SYN segments. If the calling process is not authorized + to use this connection, an error is returned. + + A TCP endpoint MAY implement PUSH flags on SEND calls (MAY-15). If + PUSH flags are not implemented, then the sending TCP peer: (1) MUST + NOT buffer data indefinitely (MUST-60), and (2) MUST set the PSH bit + in the last buffered segment (i.e., when there is no more queued data + to be sent) (MUST-61). The remaining description below assumes the + PUSH flag is supported on SEND calls. + + If the PUSH flag is set, the application intends the data to be + transmitted promptly to the receiver, and the PSH bit will be set in + the last TCP segment created from the buffer. + + The PSH bit is not a record marker and is independent of segment + boundaries. The transmitter SHOULD collapse successive bits when it + packetizes data, to send the largest possible segment (SHLD-27). + + If the PUSH flag is not set, the data may be combined with data from + subsequent SENDs for transmission efficiency. When an application + issues a series of SEND calls without setting the PUSH flag, the TCP + implementation MAY aggregate the data internally without sending it + (MAY-16). Note that when the Nagle algorithm is in use, TCP + implementations may buffer the data before sending, without regard to + the PUSH flag (see Section 3.7.4). + + An application program is logically required to set the PUSH flag in + a SEND call whenever it needs to force delivery of the data to avoid + a communication deadlock. However, a TCP implementation SHOULD send + a maximum-sized segment whenever possible (SHLD-28) to improve + performance (see Section 3.8.6.2.1). + + New applications SHOULD NOT set the URGENT flag [39] due to + implementation differences and middlebox issues (SHLD-13). + + If the URGENT flag is set, segments sent to the destination TCP peer + will have the urgent pointer set. The receiving TCP peer will signal + the urgent condition to the receiving process if the urgent pointer + indicates that data preceding the urgent pointer has not been + consumed by the receiving process. The purpose of the URGENT flag is + to stimulate the receiver to process the urgent data and to indicate + to the receiver when all the currently known urgent data has been + received. The number of times the sending user's TCP implementation + signals urgent will not necessarily be equal to the number of times + the receiving user will be notified of the presence of urgent data. + + If no remote socket was specified in the OPEN, but the connection is + established (e.g., because a LISTENing connection has become specific + due to a remote segment arriving for the local socket), then the + designated buffer is sent to the implied remote socket. Users who + make use of OPEN with an unspecified remote socket can make use of + SEND without ever explicitly knowing the remote socket address. + + However, if a SEND is attempted before the remote socket becomes + specified, an error will be returned. Users can use the STATUS call + to determine the status of the connection. Some TCP implementations + may notify the user when an unspecified socket is bound. + + If a timeout is specified, the current user timeout for this + connection is changed to the new one. + + In the simplest implementation, SEND would not return control to the + sending process until either the transmission was complete or the + timeout had been exceeded. However, this simple method is both + subject to deadlocks (for example, both sides of the connection might + try to do SENDs before doing any RECEIVEs) and offers poor + performance, so it is not recommended. A more sophisticated + implementation would return immediately to allow the process to run + concurrently with network I/O, and, furthermore, to allow multiple + SENDs to be in progress. Multiple SENDs are served in first come, + first served order, so the TCP endpoint will queue those it cannot + service immediately. + + We have implicitly assumed an asynchronous user interface in which a + SEND later elicits some kind of SIGNAL or pseudo-interrupt from the + serving TCP endpoint. An alternative is to return a response + immediately. For instance, SENDs might return immediate local + acknowledgment, even if the segment sent had not been acknowledged by + the distant TCP endpoint. We could optimistically assume eventual + success. If we are wrong, the connection will close anyway due to + the timeout. In implementations of this kind (synchronous), there + will still be some asynchronous signals, but these will deal with the + connection itself, and not with specific segments or buffers. + + In order for the process to distinguish among error or success + indications for different SENDs, it might be appropriate for the + buffer address to be returned along with the coded response to the + SEND request. TCP-to-user signals are discussed below, indicating + the information that should be returned to the calling process. + +3.9.1.3. Receive + + Format: RECEIVE (local connection name, buffer address, byte count) + -> byte count, URGENT flag [, PUSH flag] + + This command allocates a receiving buffer associated with the + specified connection. If no OPEN precedes this command or the + calling process is not authorized to use this connection, an error is + returned. + + In the simplest implementation, control would not return to the + calling program until either the buffer was filled or some error + occurred, but this scheme is highly subject to deadlocks. A more + sophisticated implementation would permit several RECEIVEs to be + outstanding at once. These would be filled as segments arrive. This + strategy permits increased throughput at the cost of a more elaborate + scheme (possibly asynchronous) to notify the calling program that a + PUSH has been seen or a buffer filled. + + A TCP receiver MAY pass a received PSH bit to the application layer + via the PUSH flag in the interface (MAY-17), but it is not required + (this was clarified in RFC 1122, Section 4.2.2.2). The remainder of + text describing the RECEIVE call below assumes that passing the PUSH + indication is supported. + + If enough data arrive to fill the buffer before a PUSH is seen, the + PUSH flag will not be set in the response to the RECEIVE. The buffer + will be filled with as much data as it can hold. If a PUSH is seen + before the buffer is filled, the buffer will be returned partially + filled and PUSH indicated. + + If there is urgent data, the user will have been informed as soon as + it arrived via a TCP-to-user signal. The receiving user should thus + be in "urgent mode". If the URGENT flag is on, additional urgent + data remains. If the URGENT flag is off, this call to RECEIVE has + returned all the urgent data, and the user may now leave "urgent + mode". Note that data following the urgent pointer (non-urgent data) + cannot be delivered to the user in the same buffer with preceding + urgent data unless the boundary is clearly marked for the user. + + To distinguish among several outstanding RECEIVEs and to take care of + the case that a buffer is not completely filled, the return code is + accompanied by both a buffer pointer and a byte count indicating the + actual length of the data received. + + Alternative implementations of RECEIVE might have the TCP endpoint + allocate buffer storage, or the TCP endpoint might share a ring + buffer with the user. + +3.9.1.4. Close + + Format: CLOSE (local connection name) + + This command causes the connection specified to be closed. If the + connection is not open or the calling process is not authorized to + use this connection, an error is returned. Closing connections is + intended to be a graceful operation in the sense that outstanding + SENDs will be transmitted (and retransmitted), as flow control + permits, until all have been serviced. Thus, it should be acceptable + to make several SEND calls, followed by a CLOSE, and expect all the + data to be sent to the destination. It should also be clear that + users should continue to RECEIVE on CLOSING connections since the + remote peer may be trying to transmit the last of its data. Thus, + CLOSE means "I have no more to send" but does not mean "I will not + receive any more." It may happen (if the user-level protocol is not + well thought out) that the closing side is unable to get rid of all + its data before timing out. In this event, CLOSE turns into ABORT, + and the closing TCP peer gives up. + + The user may CLOSE the connection at any time on their own + initiative, or in response to various prompts from the TCP + implementation (e.g., remote close executed, transmission timeout + exceeded, destination inaccessible). + + Because closing a connection requires communication with the remote + TCP peer, connections may remain in the closing state for a short + time. Attempts to reopen the connection before the TCP peer replies + to the CLOSE command will result in error responses. + + Close also implies push function. + +3.9.1.5. Status + + Format: STATUS (local connection name) -> status data + + This is an implementation-dependent user command and could be + excluded without adverse effect. Information returned would + typically come from the TCB associated with the connection. + + This command returns a data block containing the following + information: + + local socket, + + remote socket, + + local connection name, + + receive window, + + send window, + + connection state, + + number of buffers awaiting acknowledgment, + + number of buffers pending receipt, + + urgent state, + + Diffserv field value, + + security/compartment, and + + transmission timeout. + + Depending on the state of the connection, or on the implementation + itself, some of this information may not be available or meaningful. + If the calling process is not authorized to use this connection, an + error is returned. This prevents unauthorized processes from gaining + information about a connection. + +3.9.1.6. Abort + + Format: ABORT (local connection name) + + This command causes all pending SENDs and RECEIVES to be aborted, the + TCB to be removed, and a special RST message to be sent to the remote + TCP peer of the connection. Depending on the implementation, users + may receive abort indications for each outstanding SEND or RECEIVE, + or may simply receive an ABORT-acknowledgment. + +3.9.1.7. Flush + + Some TCP implementations have included a FLUSH call, which will empty + the TCP send queue of any data that the user has issued SEND calls + for but is still to the right of the current send window. That is, + it flushes as much queued send data as possible without losing + sequence number synchronization. The FLUSH call MAY be implemented + (MAY-14). + +3.9.1.8. Asynchronous Reports + + There MUST be a mechanism for reporting soft TCP error conditions to + the application (MUST-47). Generically, we assume this takes the + form of an application-supplied ERROR_REPORT routine that may be + upcalled asynchronously from the transport layer: + + ERROR_REPORT(local connection name, reason, subreason) + + The precise encoding of the reason and subreason parameters is not + specified here. However, the conditions that are reported + asynchronously to the application MUST include: + + * ICMP error message arrived (see Section 3.9.2.2 for description of + handling each ICMP message type since some message types need to + be suppressed from generating reports to the application) + + * Excessive retransmissions (see Section 3.8.3) + + * Urgent pointer advance (see Section 3.8.5) + + However, an application program that does not want to receive such + ERROR_REPORT calls SHOULD be able to effectively disable these calls + (SHLD-20). + +3.9.1.9. Set Differentiated Services Field (IPv4 TOS or IPv6 Traffic + Class) + + The application layer MUST be able to specify the Differentiated + Services field for segments that are sent on a connection (MUST-48). + The Differentiated Services field includes the 6-bit Differentiated + Services Codepoint (DSCP) value. It is not required, but the + application SHOULD be able to change the Differentiated Services + field during the connection lifetime (SHLD-21). TCP implementations + SHOULD pass the current Differentiated Services field value without + change to the IP layer, when it sends segments on the connection + (SHLD-22). + + The Differentiated Services field will be specified independently in + each direction on the connection, so that the receiver application + will specify the Differentiated Services field used for ACK segments. + + TCP implementations MAY pass the most recently received + Differentiated Services field up to the application (MAY-9). + +3.9.2. TCP/Lower-Level Interface + + The TCP endpoint calls on a lower-level protocol module to actually + send and receive information over a network. The two current + standard Internet Protocol (IP) versions layered below TCP are IPv4 + [1] and IPv6 [13]. + + If the lower-level protocol is IPv4, it provides arguments for a type + of service (used within the Differentiated Services field) and for a + time to live. TCP uses the following settings for these parameters: + + Diffserv field: The IP header value for the Diffserv field is given + by the user. This includes the bits of the Diffserv Codepoint + (DSCP). + + Time to Live (TTL): The TTL value used to send TCP segments MUST be + configurable (MUST-49). + + * Note that RFC 793 specified one minute (60 seconds) as a + constant for the TTL because the assumed maximum segment + lifetime was two minutes. This was intended to explicitly ask + that a segment be destroyed if it could not be delivered by the + internet system within one minute. RFC 1122 updated RFC 793 to + require that the TTL be configurable. + + * Note that the Diffserv field is permitted to change during a + connection (Section 4.2.4.2 of RFC 1122). However, the + application interface might not support this ability, and the + application does not have knowledge about individual TCP + segments, so this can only be done on a coarse granularity, at + best. This limitation is further discussed in RFC 7657 + (Sections 5.1, 5.3, and 6) [50]. Generally, an application + SHOULD NOT change the Diffserv field value during the course of + a connection (SHLD-23). + + Any lower-level protocol will have to provide the source address, + destination address, and protocol fields, and some way to determine + the "TCP length", both to provide the functional equivalent service + of IP and to be used in the TCP checksum. + + When received options are passed up to TCP from the IP layer, a TCP + implementation MUST ignore options that it does not understand (MUST- + 50). + + A TCP implementation MAY support the Timestamp (MAY-10) and Record + Route (MAY-11) Options. + +3.9.2.1. Source Routing + + If the lower level is IP (or other protocol that provides this + feature) and source routing is used, the interface must allow the + route information to be communicated. This is especially important + so that the source and destination addresses used in the TCP checksum + be the originating source and ultimate destination. It is also + important to preserve the return route to answer connection requests. + + An application MUST be able to specify a source route when it + actively opens a TCP connection (MUST-51), and this MUST take + precedence over a source route received in a datagram (MUST-52). + + When a TCP connection is OPENed passively and a packet arrives with a + completed IP Source Route Option (containing a return route), TCP + implementations MUST save the return route and use it for all + segments sent on this connection (MUST-53). If a different source + route arrives in a later segment, the later definition SHOULD + override the earlier one (SHLD-24). + +3.9.2.2. ICMP Messages + + TCP implementations MUST act on an ICMP error message passed up from + the IP layer, directing it to the connection that created the error + (MUST-54). The necessary demultiplexing information can be found in + the IP header contained within the ICMP message. + + This applies to ICMPv6 in addition to IPv4 ICMP. + + [35] contains discussion of specific ICMP and ICMPv6 messages + classified as either "soft" or "hard" errors that may bear different + responses. Treatment for classes of ICMP messages is described + below: + + Source Quench + TCP implementations MUST silently discard any received ICMP Source + Quench messages (MUST-55). See [11] for discussion. + + Soft Errors + For IPv4 ICMP, these include: Destination Unreachable -- codes 0, + 1, 5; Time Exceeded -- codes 0, 1; and Parameter Problem. + + For ICMPv6, these include: Destination Unreachable -- codes 0, 3; + Time Exceeded -- codes 0, 1; and Parameter Problem -- codes 0, 1, + 2. + + Since these Unreachable messages indicate soft error conditions, a + TCP implementation MUST NOT abort the connection (MUST-56), and it + SHOULD make the information available to the application (SHLD-25). + + Hard Errors + For ICMP these include Destination Unreachable -- codes 2-4. + + These are hard error conditions, so TCP implementations SHOULD + abort the connection (SHLD-26). [35] notes that some + implementations do not abort connections when an ICMP hard error is + received for a connection that is in any of the synchronized + states. + + Note that [35], Section 4 describes widespread implementation + behavior that treats soft errors as hard errors during connection + establishment. + +3.9.2.3. Source Address Validation + + RFC 1122 requires addresses to be validated in incoming SYN packets: + + | An incoming SYN with an invalid source address MUST be ignored + | either by TCP or by the IP layer [(MUST-63)] (see + | Section 3.2.1.3). + | + | A TCP implementation MUST silently discard an incoming SYN segment + | that is addressed to a broadcast or multicast address [(MUST-57)]. + + This prevents connection state and replies from being erroneously + generated, and implementers should note that this guidance is + applicable to all incoming segments, not just SYNs, as specifically + indicated in RFC 1122. + +3.10. Event Processing + + The processing depicted in this section is an example of one possible + implementation. Other implementations may have slightly different + processing sequences, but they should differ from those in this + section only in detail, not in substance. + + The activity of the TCP endpoint can be characterized as responding + to events. The events that occur can be cast into three categories: + user calls, arriving segments, and timeouts. This section describes + the processing the TCP endpoint does in response to each of the + events. In many cases, the processing required depends on the state + of the connection. + + Events that occur: + + User Calls + + OPEN + + SEND + + RECEIVE + + CLOSE + + ABORT + + STATUS + + Arriving Segments + + SEGMENT ARRIVES + + Timeouts + + USER TIMEOUT + + RETRANSMISSION TIMEOUT + + TIME-WAIT TIMEOUT + + The model of the TCP/user interface is that user commands receive an + immediate return and possibly a delayed response via an event or + pseudo-interrupt. In the following descriptions, the term "signal" + means cause a delayed response. + + Error responses in this document are identified by character strings. + For example, user commands referencing connections that do not exist + receive "error: connection not open". + + Please note in the following that all arithmetic on sequence numbers, + acknowledgment numbers, windows, et cetera, is modulo 2^32 (the size + of the sequence number space). Also note that "=<" means less than + or equal to (modulo 2^32). + + A natural way to think about processing incoming segments is to + imagine that they are first tested for proper sequence number (i.e., + that their contents lie in the range of the expected "receive window" + in the sequence number space) and then that they are generally queued + and processed in sequence number order. + + When a segment overlaps other already received segments, we + reconstruct the segment to contain just the new data and adjust the + header fields to be consistent. + + Note that if no state change is mentioned, the TCP connection stays + in the same state. + +3.10.1. OPEN Call + + CLOSED STATE (i.e., TCB does not exist) + + * Create a new transmission control block (TCB) to hold connection + state information. Fill in local socket identifier, remote + socket, Diffserv field, security/compartment, and user timeout + information. Note that some parts of the remote socket may be + unspecified in a passive OPEN and are to be filled in by the + parameters of the incoming SYN segment. Verify the security and + Diffserv value requested are allowed for this user, if not, return + "error: Diffserv value not allowed" or "error: security/ + compartment not allowed". If passive, enter the LISTEN state and + return. If active and the remote socket is unspecified, return + "error: remote socket unspecified"; if active and the remote + socket is specified, issue a SYN segment. An initial send + sequence number (ISS) is selected. A SYN segment of the form + is sent. Set SND.UNA to ISS, SND.NXT to ISS+1, + enter SYN-SENT state, and return. + + * If the caller does not have access to the local socket specified, + return "error: connection illegal for this process". If there is + no room to create a new connection, return "error: insufficient + resources". + + LISTEN STATE + + * If the OPEN call is active and the remote socket is specified, + then change the connection from passive to active, select an ISS. + Send a SYN segment, set SND.UNA to ISS, SND.NXT to ISS+1. Enter + SYN-SENT state. Data associated with SEND may be sent with SYN + segment or queued for transmission after entering ESTABLISHED + state. The urgent bit if requested in the command must be sent + with the data segments sent as a result of this command. If there + is no room to queue the request, respond with "error: insufficient + resources". If the remote socket was not specified, then return + "error: remote socket unspecified". + + SYN-SENT STATE + + SYN-RECEIVED STATE + + ESTABLISHED STATE + + FIN-WAIT-1 STATE + + FIN-WAIT-2 STATE + + CLOSE-WAIT STATE + + CLOSING STATE + + LAST-ACK STATE + + TIME-WAIT STATE + + * Return "error: connection already exists". + +3.10.2. SEND Call + + CLOSED STATE (i.e., TCB does not exist) + + * If the user does not have access to such a connection, then return + "error: connection illegal for this process". + + * Otherwise, return "error: connection does not exist". + + LISTEN STATE + + * If the remote socket is specified, then change the connection from + passive to active, select an ISS. Send a SYN segment, set SND.UNA + to ISS, SND.NXT to ISS+1. Enter SYN-SENT state. Data associated + with SEND may be sent with SYN segment or queued for transmission + after entering ESTABLISHED state. The urgent bit if requested in + the command must be sent with the data segments sent as a result + of this command. If there is no room to queue the request, + respond with "error: insufficient resources". If the remote + socket was not specified, then return "error: remote socket + unspecified". + + SYN-SENT STATE + + SYN-RECEIVED STATE + + * Queue the data for transmission after entering ESTABLISHED state. + If no space to queue, respond with "error: insufficient + resources". + + ESTABLISHED STATE + + CLOSE-WAIT STATE + + * Segmentize the buffer and send it with a piggybacked + acknowledgment (acknowledgment value = RCV.NXT). If there is + insufficient space to remember this buffer, simply return "error: + insufficient resources". + + * If the URGENT flag is set, then SND.UP <- SND.NXT and set the + urgent pointer in the outgoing segments. + + FIN-WAIT-1 STATE + + FIN-WAIT-2 STATE + + CLOSING STATE + + LAST-ACK STATE + + TIME-WAIT STATE + + * Return "error: connection closing" and do not service request. + +3.10.3. RECEIVE Call + + CLOSED STATE (i.e., TCB does not exist) + + * If the user does not have access to such a connection, return + "error: connection illegal for this process". + + * Otherwise, return "error: connection does not exist". + + LISTEN STATE + + SYN-SENT STATE + + SYN-RECEIVED STATE + + * Queue for processing after entering ESTABLISHED state. If there + is no room to queue this request, respond with "error: + insufficient resources". + + ESTABLISHED STATE + + FIN-WAIT-1 STATE + + FIN-WAIT-2 STATE + + * If insufficient incoming segments are queued to satisfy the + request, queue the request. If there is no queue space to + remember the RECEIVE, respond with "error: insufficient + resources". + + * Reassemble queued incoming segments into receive buffer and return + to user. Mark "push seen" (PUSH) if this is the case. + + * If RCV.UP is in advance of the data currently being passed to the + user, notify the user of the presence of urgent data. + + * When the TCP endpoint takes responsibility for delivering data to + the user, that fact must be communicated to the sender via an + acknowledgment. The formation of such an acknowledgment is + described below in the discussion of processing an incoming + segment. + + CLOSE-WAIT STATE + + * Since the remote side has already sent FIN, RECEIVEs must be + satisfied by data already on hand, but not yet delivered to the + user. If no text is awaiting delivery, the RECEIVE will get an + "error: connection closing" response. Otherwise, any remaining + data can be used to satisfy the RECEIVE. + + CLOSING STATE + + LAST-ACK STATE + + TIME-WAIT STATE + + * Return "error: connection closing". + +3.10.4. CLOSE Call + + CLOSED STATE (i.e., TCB does not exist) + + * If the user does not have access to such a connection, return + "error: connection illegal for this process". + + * Otherwise, return "error: connection does not exist". + + LISTEN STATE + + * Any outstanding RECEIVEs are returned with "error: closing" + responses. Delete TCB, enter CLOSED state, and return. + + SYN-SENT STATE + + * Delete the TCB and return "error: closing" responses to any queued + SENDs, or RECEIVEs. + + SYN-RECEIVED STATE + + * If no SENDs have been issued and there is no pending data to send, + then form a FIN segment and send it, and enter FIN-WAIT-1 state; + otherwise, queue for processing after entering ESTABLISHED state. + + ESTABLISHED STATE + + * Queue this until all preceding SENDs have been segmentized, then + form a FIN segment and send it. In any case, enter FIN-WAIT-1 + state. + + FIN-WAIT-1 STATE + + FIN-WAIT-2 STATE + + * Strictly speaking, this is an error and should receive an "error: + connection closing" response. An "ok" response would be + acceptable, too, as long as a second FIN is not emitted (the first + FIN may be retransmitted, though). + + CLOSE-WAIT STATE + + * Queue this request until all preceding SENDs have been + segmentized; then send a FIN segment, enter LAST-ACK state. + + CLOSING STATE + + LAST-ACK STATE + + TIME-WAIT STATE + + * Respond with "error: connection closing". + +3.10.5. ABORT Call + + CLOSED STATE (i.e., TCB does not exist) + + * If the user should not have access to such a connection, return + "error: connection illegal for this process". + + * Otherwise, return "error: connection does not exist". + + LISTEN STATE + + * Any outstanding RECEIVEs should be returned with "error: + connection reset" responses. Delete TCB, enter CLOSED state, and + return. + + SYN-SENT STATE + + * All queued SENDs and RECEIVEs should be given "connection reset" + notification. Delete the TCB, enter CLOSED state, and return. + + SYN-RECEIVED STATE + + ESTABLISHED STATE + + FIN-WAIT-1 STATE + + FIN-WAIT-2 STATE + + CLOSE-WAIT STATE + + * Send a reset segment: + + + + * All queued SENDs and RECEIVEs should be given "connection reset" + notification; all segments queued for transmission (except for the + RST formed above) or retransmission should be flushed. Delete the + TCB, enter CLOSED state, and return. + + CLOSING STATE + + LAST-ACK STATE + + TIME-WAIT STATE + + * Respond with "ok" and delete the TCB, enter CLOSED state, and + return. + +3.10.6. STATUS Call + + CLOSED STATE (i.e., TCB does not exist) + + * If the user should not have access to such a connection, return + "error: connection illegal for this process". + + * Otherwise, return "error: connection does not exist". + + LISTEN STATE + + * Return "state = LISTEN" and the TCB pointer. + + SYN-SENT STATE + + * Return "state = SYN-SENT" and the TCB pointer. + + SYN-RECEIVED STATE + + * Return "state = SYN-RECEIVED" and the TCB pointer. + + ESTABLISHED STATE + + * Return "state = ESTABLISHED" and the TCB pointer. + + FIN-WAIT-1 STATE + + * Return "state = FIN-WAIT-1" and the TCB pointer. + + FIN-WAIT-2 STATE + + * Return "state = FIN-WAIT-2" and the TCB pointer. + + CLOSE-WAIT STATE + + * Return "state = CLOSE-WAIT" and the TCB pointer. + + CLOSING STATE + + * Return "state = CLOSING" and the TCB pointer. + + LAST-ACK STATE + + * Return "state = LAST-ACK" and the TCB pointer. + + TIME-WAIT STATE + + * Return "state = TIME-WAIT" and the TCB pointer. + +3.10.7. SEGMENT ARRIVES + +3.10.7.1. CLOSED STATE + + If the state is CLOSED (i.e., TCB does not exist), then + + all data in the incoming segment is discarded. An incoming + segment containing a RST is discarded. An incoming segment not + containing a RST causes a RST to be sent in response. The + acknowledgment and sequence field values are selected to make the + reset sequence acceptable to the TCP endpoint that sent the + offending segment. + + If the ACK bit is off, sequence number zero is used, + + + + If the ACK bit is on, + + + + Return. + +3.10.7.2. LISTEN STATE + + If the state is LISTEN, then + + First, check for a RST: + + - An incoming RST segment could not be valid since it could not + have been sent in response to anything sent by this incarnation + of the connection. An incoming RST should be ignored. Return. + + Second, check for an ACK: + + - Any acknowledgment is bad if it arrives on a connection still + in the LISTEN state. An acceptable reset segment should be + formed for any arriving ACK-bearing segment. The RST should be + formatted as follows: + + + + - Return. + + Third, check for a SYN: + + - If the SYN bit is set, check the security. If the security/ + compartment on the incoming segment does not exactly match the + security/compartment in the TCB, then send a reset and return. + + + + - Set RCV.NXT to SEG.SEQ+1, IRS is set to SEG.SEQ, and any other + control or text should be queued for processing later. ISS + should be selected and a SYN segment sent of the form: + + + + - SND.NXT is set to ISS+1 and SND.UNA to ISS. The connection + state should be changed to SYN-RECEIVED. Note that any other + incoming control or data (combined with SYN) will be processed + in the SYN-RECEIVED state, but processing of SYN and ACK should + not be repeated. If the listen was not fully specified (i.e., + the remote socket was not fully specified), then the + unspecified fields should be filled in now. + + Fourth, other data or control: + + - This should not be reached. Drop the segment and return. Any + other control or data-bearing segment (not containing SYN) must + have an ACK and thus would have been discarded by the ACK + processing in the second step, unless it was first discarded by + RST checking in the first step. + +3.10.7.3. SYN-SENT STATE + + If the state is SYN-SENT, then + + First, check the ACK bit: + + - If the ACK bit is set, + + o If SEG.ACK =< ISS or SEG.ACK > SND.NXT, send a reset (unless + the RST bit is set, if so drop the segment and return) + + + + o and discard the segment. Return. + + o If SND.UNA < SEG.ACK =< SND.NXT, then the ACK is acceptable. + Some deployed TCP code has used the check SEG.ACK == SND.NXT + (using "==" rather than "=<"), but this is not appropriate + when the stack is capable of sending data on the SYN because + the TCP peer may not accept and acknowledge all of the data + on the SYN. + + Second, check the RST bit: + + - If the RST bit is set, + + o A potential blind reset attack is described in RFC 5961 [9]. + The mitigation described in that document has specific + applicability explained therein, and is not a substitute for + cryptographic protection (e.g., IPsec or TCP-AO). A TCP + implementation that supports the mitigation described in RFC + 5961 SHOULD first check that the sequence number exactly + matches RCV.NXT prior to executing the action in the next + paragraph. + + o If the ACK was acceptable, then signal to the user "error: + connection reset", drop the segment, enter CLOSED state, + delete TCB, and return. Otherwise (no ACK), drop the + segment and return. + + Third, check the security: + + - If the security/compartment in the segment does not exactly + match the security/compartment in the TCB, send a reset: + + o If there is an ACK, + + + + o Otherwise, + + + + - If a reset was sent, discard the segment and return. + + Fourth, check the SYN bit: + + - This step should be reached only if the ACK is ok, or there is + no ACK, and the segment did not contain a RST. + + - If the SYN bit is on and the security/compartment is + acceptable, then RCV.NXT is set to SEG.SEQ+1, IRS is set to + SEG.SEQ. SND.UNA should be advanced to equal SEG.ACK (if there + is an ACK), and any segments on the retransmission queue that + are thereby acknowledged should be removed. + + - If SND.UNA > ISS (our SYN has been ACKed), change the + connection state to ESTABLISHED, form an ACK segment + + + + - and send it. Data or controls that were queued for + transmission MAY be included. Some TCP implementations + suppress sending this segment when the received segment + contains data that will anyways generate an acknowledgment in + the later processing steps, saving this extra acknowledgment of + the SYN from being sent. If there are other controls or text + in the segment, then continue processing at the sixth step + under Section 3.10.7.4 where the URG bit is checked; otherwise, + return. + + - Otherwise, enter SYN-RECEIVED, form a SYN,ACK segment + + + + - and send it. Set the variables: + + SND.WND <- SEG.WND + + SND.WL1 <- SEG.SEQ + + SND.WL2 <- SEG.ACK + + If there are other controls or text in the segment, queue them + for processing after the ESTABLISHED state has been reached, + return. + + - Note that it is legal to send and receive application data on + SYN segments (this is the "text in the segment" mentioned + above). There has been significant misinformation and + misunderstanding of this topic historically. Some firewalls + and security devices consider this suspicious. However, the + capability was used in T/TCP [21] and is used in TCP Fast Open + (TFO) [48], so is important for implementations and network + devices to permit. + + Fifth, if neither of the SYN or RST bits is set, then drop the + segment and return. + +3.10.7.4. Other States + + Otherwise, + + First, check sequence number: + + - SYN-RECEIVED STATE + + - ESTABLISHED STATE + + - FIN-WAIT-1 STATE + + - FIN-WAIT-2 STATE + + - CLOSE-WAIT STATE + + - CLOSING STATE + + - LAST-ACK STATE + + - TIME-WAIT STATE + + o Segments are processed in sequence. Initial tests on + arrival are used to discard old duplicates, but further + processing is done in SEG.SEQ order. If a segment's + contents straddle the boundary between old and new, only the + new parts are processed. + + o In general, the processing of received segments MUST be + implemented to aggregate ACK segments whenever possible + (MUST-58). For example, if the TCP endpoint is processing a + series of queued segments, it MUST process them all before + sending any ACK segments (MUST-59). + + o There are four cases for the acceptability test for an + incoming segment: + + +=========+=========+======================================+ + | Segment | Receive | Test | + | Length | Window | | + +=========+=========+======================================+ + | 0 | 0 | SEG.SEQ = RCV.NXT | + +---------+---------+--------------------------------------+ + | 0 | >0 | RCV.NXT =< SEG.SEQ < | + | | | RCV.NXT+RCV.WND | + +---------+---------+--------------------------------------+ + | >0 | 0 | not acceptable | + +---------+---------+--------------------------------------+ + | >0 | >0 | RCV.NXT =< SEG.SEQ < | + | | | RCV.NXT+RCV.WND | + | | | | + | | | or | + | | | | + | | | RCV.NXT =< SEG.SEQ+SEG.LEN-1 | + | | | < RCV.NXT+RCV.WND | + +---------+---------+--------------------------------------+ + + Table 6: Segment Acceptability Tests + + o In implementing sequence number validation as described + here, please note Appendix A.2. + + o If the RCV.WND is zero, no segments will be acceptable, but + special allowance should be made to accept valid ACKs, URGs, + and RSTs. + + o If an incoming segment is not acceptable, an acknowledgment + should be sent in reply (unless the RST bit is set, if so + drop the segment and return): + + + + o After sending the acknowledgment, drop the unacceptable + segment and return. + + o Note that for the TIME-WAIT state, there is an improved + algorithm described in [40] for handling incoming SYN + segments that utilizes timestamps rather than relying on the + sequence number check described here. When the improved + algorithm is implemented, the logic above is not applicable + for incoming SYN segments with Timestamp Options, received + on a connection in the TIME-WAIT state. + + o In the following it is assumed that the segment is the + idealized segment that begins at RCV.NXT and does not exceed + the window. One could tailor actual segments to fit this + assumption by trimming off any portions that lie outside the + window (including SYN and FIN) and only processing further + if the segment then begins at RCV.NXT. Segments with higher + beginning sequence numbers SHOULD be held for later + processing (SHLD-31). + + Second, check the RST bit: + + - RFC 5961 [9], Section 3 describes a potential blind reset + attack and optional mitigation approach. This does not provide + a cryptographic protection (e.g., as in IPsec or TCP-AO) but + can be applicable in situations described in RFC 5961. For + stacks implementing the protection described in RFC 5961, the + three checks below apply; otherwise, processing for these + states is indicated further below. + + 1) If the RST bit is set and the sequence number is outside + the current receive window, silently drop the segment. + + 2) If the RST bit is set and the sequence number exactly + matches the next expected sequence number (RCV.NXT), then + TCP endpoints MUST reset the connection in the manner + prescribed below according to the connection state. + + 3) If the RST bit is set and the sequence number does not + exactly match the next expected sequence value, yet is + within the current receive window, TCP endpoints MUST send + an acknowledgment (challenge ACK): + + + + After sending the challenge ACK, TCP endpoints MUST drop + the unacceptable segment and stop processing the incoming + packet further. Note that RFC 5961 and Errata ID 4772 [99] + contain additional considerations for ACK throttling in an + implementation. + + - SYN-RECEIVED STATE + + o If the RST bit is set, + + + If this connection was initiated with a passive OPEN + (i.e., came from the LISTEN state), then return this + connection to LISTEN state and return. The user need not + be informed. If this connection was initiated with an + active OPEN (i.e., came from SYN-SENT state), then the + connection was refused; signal the user "connection + refused". In either case, the retransmission queue + should be flushed. And in the active OPEN case, enter + the CLOSED state and delete the TCB, and return. + + - ESTABLISHED STATE + + - FIN-WAIT-1 STATE + + - FIN-WAIT-2 STATE + + - CLOSE-WAIT STATE + + o If the RST bit is set, then any outstanding RECEIVEs and + SEND should receive "reset" responses. All segment queues + should be flushed. Users should also receive an unsolicited + general "connection reset" signal. Enter the CLOSED state, + delete the TCB, and return. + + - CLOSING STATE + + - LAST-ACK STATE + + - TIME-WAIT STATE + + o If the RST bit is set, then enter the CLOSED state, delete + the TCB, and return. + + Third, check security: + + - SYN-RECEIVED STATE + + o If the security/compartment in the segment does not exactly + match the security/compartment in the TCB, then send a reset + and return. + + - ESTABLISHED STATE + + - FIN-WAIT-1 STATE + + - FIN-WAIT-2 STATE + + - CLOSE-WAIT STATE + + - CLOSING STATE + + - LAST-ACK STATE + + - TIME-WAIT STATE + + o If the security/compartment in the segment does not exactly + match the security/compartment in the TCB, then send a + reset; any outstanding RECEIVEs and SEND should receive + "reset" responses. All segment queues should be flushed. + Users should also receive an unsolicited general "connection + reset" signal. Enter the CLOSED state, delete the TCB, and + return. + + - Note this check is placed following the sequence check to + prevent a segment from an old connection between these port + numbers with a different security from causing an abort of the + current connection. + + Fourth, check the SYN bit: + + - SYN-RECEIVED STATE + + o If the connection was initiated with a passive OPEN, then + return this connection to the LISTEN state and return. + Otherwise, handle per the directions for synchronized states + below. + + - ESTABLISHED STATE + + - FIN-WAIT-1 STATE + + - FIN-WAIT-2 STATE + + - CLOSE-WAIT STATE + + - CLOSING STATE + + - LAST-ACK STATE + + - TIME-WAIT STATE + + o If the SYN bit is set in these synchronized states, it may + be either a legitimate new connection attempt (e.g., in the + case of TIME-WAIT), an error where the connection should be + reset, or the result of an attack attempt, as described in + RFC 5961 [9]. For the TIME-WAIT state, new connections can + be accepted if the Timestamp Option is used and meets + expectations (per [40]). For all other cases, RFC 5961 + provides a mitigation with applicability to some situations, + though there are also alternatives that offer cryptographic + protection (see Section 7). RFC 5961 recommends that in + these synchronized states, if the SYN bit is set, + irrespective of the sequence number, TCP endpoints MUST send + a "challenge ACK" to the remote peer: + + + + o After sending the acknowledgment, TCP implementations MUST + drop the unacceptable segment and stop processing further. + Note that RFC 5961 and Errata ID 4772 [99] contain + additional ACK throttling notes for an implementation. + + o For implementations that do not follow RFC 5961, the + original behavior described in RFC 793 follows in this + paragraph. If the SYN is in the window it is an error: send + a reset, any outstanding RECEIVEs and SEND should receive + "reset" responses, all segment queues should be flushed, the + user should also receive an unsolicited general "connection + reset" signal, enter the CLOSED state, delete the TCB, and + return. + + o If the SYN is not in the window, this step would not be + reached and an ACK would have been sent in the first step + (sequence number check). + + Fifth, check the ACK field: + + - if the ACK bit is off, drop the segment and return + + - if the ACK bit is on, + + o RFC 5961 [9], Section 5 describes a potential blind data + injection attack, and mitigation that implementations MAY + choose to include (MAY-12). TCP stacks that implement RFC + 5961 MUST add an input check that the ACK value is + acceptable only if it is in the range of ((SND.UNA - + MAX.SND.WND) =< SEG.ACK =< SND.NXT). All incoming segments + whose ACK value doesn't satisfy the above condition MUST be + discarded and an ACK sent back. The new state variable + MAX.SND.WND is defined as the largest window that the local + sender has ever received from its peer (subject to window + scaling) or may be hard-coded to a maximum permissible + window value. When the ACK value is acceptable, the per- + state processing below applies: + + o SYN-RECEIVED STATE + + + If SND.UNA < SEG.ACK =< SND.NXT, then enter ESTABLISHED + state and continue processing with the variables below + set to: + + SND.WND <- SEG.WND + + SND.WL1 <- SEG.SEQ + + SND.WL2 <- SEG.ACK + + + If the segment acknowledgment is not acceptable, form a + reset segment + + + + + and send it. + + o ESTABLISHED STATE + + + If SND.UNA < SEG.ACK =< SND.NXT, then set SND.UNA <- + SEG.ACK. Any segments on the retransmission queue that + are thereby entirely acknowledged are removed. Users + should receive positive acknowledgments for buffers that + have been SENT and fully acknowledged (i.e., SEND buffer + should be returned with "ok" response). If the ACK is a + duplicate (SEG.ACK =< SND.UNA), it can be ignored. If + the ACK acks something not yet sent (SEG.ACK > SND.NXT), + then send an ACK, drop the segment, and return. + + + If SND.UNA =< SEG.ACK =< SND.NXT, the send window should + be updated. If (SND.WL1 < SEG.SEQ or (SND.WL1 = SEG.SEQ + and SND.WL2 =< SEG.ACK)), set SND.WND <- SEG.WND, set + SND.WL1 <- SEG.SEQ, and set SND.WL2 <- SEG.ACK. + + + Note that SND.WND is an offset from SND.UNA, that SND.WL1 + records the sequence number of the last segment used to + update SND.WND, and that SND.WL2 records the + acknowledgment number of the last segment used to update + SND.WND. The check here prevents using old segments to + update the window. + + o FIN-WAIT-1 STATE + + + In addition to the processing for the ESTABLISHED state, + if the FIN segment is now acknowledged, then enter FIN- + WAIT-2 and continue processing in that state. + + o FIN-WAIT-2 STATE + + + In addition to the processing for the ESTABLISHED state, + if the retransmission queue is empty, the user's CLOSE + can be acknowledged ("ok") but do not delete the TCB. + + o CLOSE-WAIT STATE + + + Do the same processing as for the ESTABLISHED state. + + o CLOSING STATE + + + In addition to the processing for the ESTABLISHED state, + if the ACK acknowledges our FIN, then enter the TIME-WAIT + state; otherwise, ignore the segment. + + o LAST-ACK STATE + + + The only thing that can arrive in this state is an + acknowledgment of our FIN. If our FIN is now + acknowledged, delete the TCB, enter the CLOSED state, and + return. + + o TIME-WAIT STATE + + + The only thing that can arrive in this state is a + retransmission of the remote FIN. Acknowledge it, and + restart the 2 MSL timeout. + + Sixth, check the URG bit: + + - ESTABLISHED STATE + + - FIN-WAIT-1 STATE + + - FIN-WAIT-2 STATE + + o If the URG bit is set, RCV.UP <- max(RCV.UP,SEG.UP), and + signal the user that the remote side has urgent data if the + urgent pointer (RCV.UP) is in advance of the data consumed. + If the user has already been signaled (or is still in the + "urgent mode") for this continuous sequence of urgent data, + do not signal the user again. + + - CLOSE-WAIT STATE + + - CLOSING STATE + + - LAST-ACK STATE + + - TIME-WAIT STATE + + o This should not occur since a FIN has been received from the + remote side. Ignore the URG. + + Seventh, process the segment text: + + - ESTABLISHED STATE + + - FIN-WAIT-1 STATE + + - FIN-WAIT-2 STATE + + o Once in the ESTABLISHED state, it is possible to deliver + segment data to user RECEIVE buffers. Data from segments + can be moved into buffers until either the buffer is full or + the segment is empty. If the segment empties and carries a + PUSH flag, then the user is informed, when the buffer is + returned, that a PUSH has been received. + + o When the TCP endpoint takes responsibility for delivering + the data to the user, it must also acknowledge the receipt + of the data. + + o Once the TCP endpoint takes responsibility for the data, it + advances RCV.NXT over the data accepted, and adjusts RCV.WND + as appropriate to the current buffer availability. The + total of RCV.NXT and RCV.WND should not be reduced. + + o A TCP implementation MAY send an ACK segment acknowledging + RCV.NXT when a valid segment arrives that is in the window + but not at the left window edge (MAY-13). + + o Please note the window management suggestions in + Section 3.8. + + o Send an acknowledgment of the form: + + + + o This acknowledgment should be piggybacked on a segment being + transmitted if possible without incurring undue delay. + + - CLOSE-WAIT STATE + + - CLOSING STATE + + - LAST-ACK STATE + + - TIME-WAIT STATE + + o This should not occur since a FIN has been received from the + remote side. Ignore the segment text. + + Eighth, check the FIN bit: + + - Do not process the FIN if the state is CLOSED, LISTEN, or SYN- + SENT since the SEG.SEQ cannot be validated; drop the segment + and return. + + - If the FIN bit is set, signal the user "connection closing" and + return any pending RECEIVEs with same message, advance RCV.NXT + over the FIN, and send an acknowledgment for the FIN. Note + that FIN implies PUSH for any segment text not yet delivered to + the user. + + o SYN-RECEIVED STATE + + o ESTABLISHED STATE + + + Enter the CLOSE-WAIT state. + + o FIN-WAIT-1 STATE + + + If our FIN has been ACKed (perhaps in this segment), then + enter TIME-WAIT, start the time-wait timer, turn off the + other timers; otherwise, enter the CLOSING state. + + o FIN-WAIT-2 STATE + + + Enter the TIME-WAIT state. Start the time-wait timer, + turn off the other timers. + + o CLOSE-WAIT STATE + + + Remain in the CLOSE-WAIT state. + + o CLOSING STATE + + + Remain in the CLOSING state. + + o LAST-ACK STATE + + + Remain in the LAST-ACK state. + + o TIME-WAIT STATE + + + Remain in the TIME-WAIT state. Restart the 2 MSL time- + wait timeout. + + and return. + +3.10.8. Timeouts + + USER TIMEOUT + + * For any state if the user timeout expires, flush all queues, + signal the user "error: connection aborted due to user timeout" in + general and for any outstanding calls, delete the TCB, enter the + CLOSED state, and return. + + RETRANSMISSION TIMEOUT + + * For any state if the retransmission timeout expires on a segment + in the retransmission queue, send the segment at the front of the + retransmission queue again, reinitialize the retransmission timer, + and return. + + TIME-WAIT TIMEOUT + + * If the time-wait timeout expires on a connection, delete the TCB, + enter the CLOSED state, and return. + +4. Glossary + + ACK + A control bit (acknowledge) occupying no sequence space, + which indicates that the acknowledgment field of this segment + specifies the next sequence number the sender of this segment + is expecting to receive, hence acknowledging receipt of all + previous sequence numbers. + + connection + A logical communication path identified by a pair of sockets. + + datagram + A message sent in a packet-switched computer communications + network. + + Destination Address + The network-layer address of the endpoint intended to receive + a segment. + + FIN + A control bit (finis) occupying one sequence number, which + indicates that the sender will send no more data or control + occupying sequence space. + + flush + To remove all of the contents (data or segments) from a store + (buffer or queue). + + fragment + A portion of a logical unit of data. In particular, an + internet fragment is a portion of an internet datagram. + + header + Control information at the beginning of a message, segment, + fragment, packet, or block of data. + + host + A computer. In particular, a source or destination of + messages from the point of view of the communication network. + + Identification + An Internet Protocol field. This identifying value assigned + by the sender aids in assembling the fragments of a datagram. + + internet address + A network-layer address. + + internet datagram + A unit of data exchanged between internet hosts, together + with the internet header that allows the datagram to be + routed from source to destination. + + internet fragment + A portion of the data of an internet datagram with an + internet header. + + IP + Internet Protocol. See [1] and [13]. + + IRS + The Initial Receive Sequence number. The first sequence + number used by the sender on a connection. + + ISN + The Initial Sequence Number. The first sequence number used + on a connection (either ISS or IRS). Selected in a way that + is unique within a given period of time and is unpredictable + to attackers. + + ISS + The Initial Send Sequence number. The first sequence number + used by the sender on a connection. + + left sequence + This is the next sequence number to be acknowledged by the + data-receiving TCP endpoint (or the lowest currently + unacknowledged sequence number) and is sometimes referred to + as the left edge of the send window. + + module + An implementation, usually in software, of a protocol or + other procedure. + + MSL + Maximum Segment Lifetime, the time a TCP segment can exist in + the internetwork system. Arbitrarily defined to be 2 + minutes. + + octet + An eight-bit byte. + + Options + An Option field may contain several options, and each option + may be several octets in length. + + packet + A package of data with a header that may or may not be + logically complete. More often a physical packaging than a + logical packaging of data. + + port + The portion of a connection identifier used for + demultiplexing connections at an endpoint. + + process + A program in execution. A source or destination of data from + the point of view of the TCP endpoint or other host-to-host + protocol. + + PUSH + A control bit occupying no sequence space, indicating that + this segment contains data that must be pushed through to the + receiving user. + + RCV.NXT + receive next sequence number + + RCV.UP + receive urgent pointer + + RCV.WND + receive window + + receive next sequence number + This is the next sequence number the local TCP endpoint is + expecting to receive. + + receive window + This represents the sequence numbers the local (receiving) + TCP endpoint is willing to receive. Thus, the local TCP + endpoint considers that segments overlapping the range + RCV.NXT to RCV.NXT + RCV.WND - 1 carry acceptable data or + control. Segments containing sequence numbers entirely + outside this range are considered duplicates or injection + attacks and discarded. + + RST + A control bit (reset), occupying no sequence space, + indicating that the receiver should delete the connection + without further interaction. The receiver can determine, + based on the sequence number and acknowledgment fields of the + incoming segment, whether it should honor the reset command + or ignore it. In no case does receipt of a segment + containing RST give rise to a RST in response. + + SEG.ACK + segment acknowledgment + + SEG.LEN + segment length + + SEG.SEQ + segment sequence + + SEG.UP + segment urgent pointer field + + SEG.WND + segment window field + + segment + A logical unit of data. In particular, a TCP segment is the + unit of data transferred between a pair of TCP modules. + + segment acknowledgment + The sequence number in the acknowledgment field of the + arriving segment. + + segment length + The amount of sequence number space occupied by a segment, + including any controls that occupy sequence space. + + segment sequence + The number in the sequence field of the arriving segment. + + send sequence + This is the next sequence number the local (sending) TCP + endpoint will use on the connection. It is initially + selected from an initial sequence number curve (ISN) and is + incremented for each octet of data or sequenced control + transmitted. + + send window + This represents the sequence numbers that the remote + (receiving) TCP endpoint is willing to receive. It is the + value of the window field specified in segments from the + remote (data-receiving) TCP endpoint. The range of new + sequence numbers that may be emitted by a TCP implementation + lies between SND.NXT and SND.UNA + SND.WND - 1. + (Retransmissions of sequence numbers between SND.UNA and + SND.NXT are expected, of course.) + + SND.NXT + send sequence + + SND.UNA + left sequence + + SND.UP + send urgent pointer + + SND.WL1 + segment sequence number at last window update + + SND.WL2 + segment acknowledgment number at last window update + + SND.WND + send window + + socket (or socket number, or socket address, or socket identifier) + An address that specifically includes a port identifier, that + is, the concatenation of an Internet Address with a TCP port. + + Source Address + The network-layer address of the sending endpoint. + + SYN + A control bit in the incoming segment, occupying one sequence + number, used at the initiation of a connection to indicate + where the sequence numbering will start. + + TCB + Transmission control block, the data structure that records + the state of a connection. + + TCP + Transmission Control Protocol: a host-to-host protocol for + reliable communication in internetwork environments. + + TOS + Type of Service, an obsoleted IPv4 field. The same header + bits currently are used for the Differentiated Services field + [4] containing the Differentiated Services Codepoint (DSCP) + value and the 2-bit ECN codepoint [6]. + + Type of Service + See "TOS". + + URG + A control bit (urgent), occupying no sequence space, used to + indicate that the receiving user should be notified to do + urgent processing as long as there is data to be consumed + with sequence numbers less than the value indicated by the + urgent pointer. + + urgent pointer + A control field meaningful only when the URG bit is on. This + field communicates the value of the urgent pointer that + indicates the data octet associated with the sending user's + urgent call. + +5. Changes from RFC 793 + + This document obsoletes RFC 793 as well as RFCs 6093 and 6528, which + updated 793. In all cases, only the normative protocol specification + and requirements have been incorporated into this document, and some + informational text with background and rationale may not have been + carried in. The informational content of those documents is still + valuable in learning about and understanding TCP, and they are valid + Informational references, even though their normative content has + been incorporated into this document. + + The main body of this document was adapted from RFC 793's Section 3, + titled "FUNCTIONAL SPECIFICATION", with an attempt to keep formatting + and layout as close as possible. + + The collection of applicable RFC errata that have been reported and + either accepted or held for an update to RFC 793 were incorporated + (Errata IDs: 573 [73], 574 [74], 700 [75], 701 [76], 1283 [77], 1561 + [78], 1562 [79], 1564 [80], 1571 [81], 1572 [82], 2297 [83], 2298 + [84], 2748 [85], 2749 [86], 2934 [87], 3213 [88], 3300 [89], 3301 + [90], 6222 [91]). Some errata were not applicable due to other + changes (Errata IDs: 572 [92], 575 [93], 1565 [94], 1569 [95], 2296 + [96], 3305 [97], 3602 [98]). + + Changes to the specification of the urgent pointer described in RFCs + 1011, 1122, and 6093 were incorporated. See RFC 6093 for detailed + discussion of why these changes were necessary. + + The discussion of the RTO from RFC 793 was updated to refer to RFC + 6298. The text on the RTO in RFC 1122 originally replaced the text + in RFC 793; however, RFC 2988 should have updated RFC 1122 and has + subsequently been obsoleted by RFC 6298. + + RFC 1011 [18] contains a number of comments about RFC 793, including + some needed changes to the TCP specification. These are expanded in + RFC 1122, which contains a collection of other changes and + clarifications to RFC 793. The normative items impacting the + protocol have been incorporated here, though some historically useful + implementation advice and informative discussion from RFC 1122 is not + included here. The present document, which is now the TCP + specification rather than RFC 793, updates RFC 1011, and the comments + noted in RFC 1011 have been incorporated. + + RFC 1122 contains more than just TCP requirements, so this document + can't obsolete RFC 1122 entirely. It is only marked as "updating" + RFC 1122; however, it should be understood to effectively obsolete + all of the material on TCP found in RFC 1122. + + The more secure initial sequence number generation algorithm from RFC + 6528 was incorporated. See RFC 6528 for discussion of the attacks + that this mitigates, as well as advice on selecting PRF algorithms + and managing secret key data. + + A note based on RFC 6429 was added to explicitly clarify that system + resource management concerns allow connection resources to be + reclaimed. RFC 6429 is obsoleted in the sense that the clarification + it describes has been reflected within this base TCP specification. + + The description of congestion control implementation was added based + on the set of documents that are IETF BCP or Standards Track on the + topic and the current state of common implementations. + +6. IANA Considerations + + In the "Transmission Control Protocol (TCP) Header Flags" registry, + IANA has made several changes as described in this section. + + RFC 3168 originally created this registry but only populated it with + the new bits defined in RFC 3168, neglecting the other bits that had + previously been described in RFC 793 and other documents. Bit 7 has + since also been updated by RFC 8311 [54]. + + The "Bit" column has been renamed below as the "Bit Offset" column + because it references each header flag's offset within the 16-bit + aligned view of the TCP header in Figure 1. The bits in offsets 0 + through 3 are the TCP segment Data Offset field, and not header + flags. + + IANA has added a column for "Assignment Notes". + + IANA has assigned values as indicated below. + + +========+===================+===========+====================+ + | Bit | Name | Reference | Assignment Notes | + | Offset | | | | + +========+===================+===========+====================+ + | 4 | Reserved for | RFC 9293 | | + | | future use | | | + +--------+-------------------+-----------+--------------------+ + | 5 | Reserved for | RFC 9293 | | + | | future use | | | + +--------+-------------------+-----------+--------------------+ + | 6 | Reserved for | RFC 9293 | | + | | future use | | | + +--------+-------------------+-----------+--------------------+ + | 7 | Reserved for | RFC 8311 | Previously used by | + | | future use | | Historic RFC 3540 | + | | | | as NS (Nonce Sum). | + +--------+-------------------+-----------+--------------------+ + | 8 | CWR (Congestion | RFC 3168 | | + | | Window Reduced) | | | + +--------+-------------------+-----------+--------------------+ + | 9 | ECE (ECN-Echo) | RFC 3168 | | + +--------+-------------------+-----------+--------------------+ + | 10 | Urgent pointer | RFC 9293 | | + | | field is | | | + | | significant (URG) | | | + +--------+-------------------+-----------+--------------------+ + | 11 | Acknowledgment | RFC 9293 | | + | | field is | | | + | | significant (ACK) | | | + +--------+-------------------+-----------+--------------------+ + | 12 | Push function | RFC 9293 | | + | | (PSH) | | | + +--------+-------------------+-----------+--------------------+ + | 13 | Reset the | RFC 9293 | | + | | connection (RST) | | | + +--------+-------------------+-----------+--------------------+ + | 14 | Synchronize | RFC 9293 | | + | | sequence numbers | | | + | | (SYN) | | | + +--------+-------------------+-----------+--------------------+ + | 15 | No more data from | RFC 9293 | | + | | sender (FIN) | | | + +--------+-------------------+-----------+--------------------+ + + Table 7: TCP Header Flags + + The "TCP Header Flags" registry has also been moved to a subregistry + under the global "Transmission Control Protocol (TCP) Parameters" + registry . + + The registry's Registration Procedure remains Standards Action, but + the Reference has been updated to this document, and the Note has + been removed. + +7. Security and Privacy Considerations + + The TCP design includes only rudimentary security features that + improve the robustness and reliability of connections and application + data transfer, but there are no built-in cryptographic capabilities + to support any form of confidentiality, authentication, or other + typical security functions. Non-cryptographic enhancements (e.g., + [9]) have been developed to improve robustness of TCP connections to + particular types of attacks, but the applicability and protections of + non-cryptographic enhancements are limited (e.g., see Section 1.1 of + [9]). Applications typically utilize lower-layer (e.g., IPsec) and + upper-layer (e.g., TLS) protocols to provide security and privacy for + TCP connections and application data carried in TCP. Methods based + on TCP Options have been developed as well, to support some security + capabilities. + + In order to fully provide confidentiality, integrity protection, and + authentication for TCP connections (including their control flags), + IPsec is the only current effective method. For integrity protection + and authentication, the TCP Authentication Option (TCP-AO) [38] is + available, with a proposed extension to also provide confidentiality + for the segment payload. Other methods discussed in this section may + provide confidentiality or integrity protection for the payload, but + for the TCP header only cover either a subset of the fields (e.g., + tcpcrypt [57]) or none at all (e.g., TLS). Other security features + that have been added to TCP (e.g., ISN generation, sequence number + checks, and others) are only capable of partially hindering attacks. + + Applications using long-lived TCP flows have been vulnerable to + attacks that exploit the processing of control flags described in + earlier TCP specifications [33]. TCP-MD5 was a commonly implemented + TCP Option to support authentication for some of these connections, + but had flaws and is now deprecated. TCP-AO provides a capability to + protect long-lived TCP connections from attacks and has superior + properties to TCP-MD5. It does not provide any privacy for + application data or for the TCP headers. + + The "tcpcrypt" [57] experimental extension to TCP provides the + ability to cryptographically protect connection data. Metadata + aspects of the TCP flow are still visible, but the application stream + is well protected. Within the TCP header, only the urgent pointer + and FIN flag are protected through tcpcrypt. + + The TCP Roadmap [49] includes notes about several RFCs related to TCP + security. Many of the enhancements provided by these RFCs have been + integrated into the present document, including ISN generation, + mitigating blind in-window attacks, and improving handling of soft + errors and ICMP packets. These are all discussed in greater detail + in the referenced RFCs that originally described the changes needed + to earlier TCP specifications. Additionally, see RFC 6093 [39] for + discussion of security considerations related to the urgent pointer + field, which also discourages new applications from using the urgent + pointer. + + Since TCP is often used for bulk transfer flows, some attacks are + possible that abuse the TCP congestion control logic. An example is + "ACK-division" attacks. Updates that have been made to the TCP + congestion control specifications include mechanisms like Appropriate + Byte Counting (ABC) [29] that act as mitigations to these attacks. + + Other attacks are focused on exhausting the resources of a TCP + server. Examples include SYN flooding [32] or wasting resources on + non-progressing connections [41]. Operating systems commonly + implement mitigations for these attacks. Some common defenses also + utilize proxies, stateful firewalls, and other technologies outside + the end-host TCP implementation. + + The concept of a protocol's "wire image" is described in RFC 8546 + [56], which describes how TCP's cleartext headers expose more + metadata to nodes on the path than is strictly required to route the + packets to their destination. On-path adversaries may be able to + leverage this metadata. Lessons learned in this respect from TCP + have been applied in the design of newer transports like QUIC [60]. + Additionally, based partly on experiences with TCP and its + extensions, there are considerations that might be applicable for + future TCP extensions and other transports that the IETF has + documented in RFC 9065 [61], along with IAB recommendations in RFC + 8558 [58] and [67]. + + There are also methods of "fingerprinting" that can be used to infer + the host TCP implementation (operating system) version or platform + information. These collect observations of several aspects, such as + the options present in segments, the ordering of options, the + specific behaviors in the case of various conditions, packet timing, + packet sizing, and other aspects of the protocol that are left to be + determined by an implementer, and can use those observations to + identify information about the host and implementation. + + Since ICMP message processing also can interact with TCP connections, + there is potential for ICMP-based attacks against TCP connections. + These are discussed in RFC 5927 [100], along with mitigations that + have been implemented. + +8. References + +8.1. Normative References + + [1] Postel, J., "Internet Protocol", STD 5, RFC 791, + DOI 10.17487/RFC0791, September 1981, + . + + [2] Mogul, J. and S. Deering, "Path MTU discovery", RFC 1191, + DOI 10.17487/RFC1191, November 1990, + . + + [3] Bradner, S., "Key words for use in RFCs to Indicate + Requirement Levels", BCP 14, RFC 2119, + DOI 10.17487/RFC2119, March 1997, + . + + [4] Nichols, K., Blake, S., Baker, F., and D. Black, + "Definition of the Differentiated Services Field (DS + Field) in the IPv4 and IPv6 Headers", RFC 2474, + DOI 10.17487/RFC2474, December 1998, + . + + [5] Floyd, S., "Congestion Control Principles", BCP 41, + RFC 2914, DOI 10.17487/RFC2914, September 2000, + . + + [6] Ramakrishnan, K., Floyd, S., and D. Black, "The Addition + of Explicit Congestion Notification (ECN) to IP", + RFC 3168, DOI 10.17487/RFC3168, September 2001, + . + + [7] Floyd, S. and M. Allman, "Specifying New Congestion + Control Algorithms", BCP 133, RFC 5033, + DOI 10.17487/RFC5033, August 2007, + . + + [8] Allman, M., Paxson, V., and E. Blanton, "TCP Congestion + Control", RFC 5681, DOI 10.17487/RFC5681, September 2009, + . + + [9] Ramaiah, A., Stewart, R., and M. Dalal, "Improving TCP's + Robustness to Blind In-Window Attacks", RFC 5961, + DOI 10.17487/RFC5961, August 2010, + . + + [10] Paxson, V., Allman, M., Chu, J., and M. Sargent, + "Computing TCP's Retransmission Timer", RFC 6298, + DOI 10.17487/RFC6298, June 2011, + . + + [11] Gont, F., "Deprecation of ICMP Source Quench Messages", + RFC 6633, DOI 10.17487/RFC6633, May 2012, + . + + [12] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC + 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, + May 2017, . + + [13] Deering, S. and R. Hinden, "Internet Protocol, Version 6 + (IPv6) Specification", STD 86, RFC 8200, + DOI 10.17487/RFC8200, July 2017, + . + + [14] McCann, J., Deering, S., Mogul, J., and R. Hinden, Ed., + "Path MTU Discovery for IP version 6", STD 87, RFC 8201, + DOI 10.17487/RFC8201, July 2017, + . + + [15] Allman, M., "Requirements for Time-Based Loss Detection", + BCP 233, RFC 8961, DOI 10.17487/RFC8961, November 2020, + . + +8.2. Informative References + + [16] Postel, J., "Transmission Control Protocol", STD 7, + RFC 793, DOI 10.17487/RFC0793, September 1981, + . + + [17] Nagle, J., "Congestion Control in IP/TCP Internetworks", + RFC 896, DOI 10.17487/RFC0896, January 1984, + . + + [18] Reynolds, J. and J. Postel, "Official Internet protocols", + RFC 1011, DOI 10.17487/RFC1011, May 1987, + . + + [19] Braden, R., Ed., "Requirements for Internet Hosts - + Communication Layers", STD 3, RFC 1122, + DOI 10.17487/RFC1122, October 1989, + . + + [20] Almquist, P., "Type of Service in the Internet Protocol + Suite", RFC 1349, DOI 10.17487/RFC1349, July 1992, + . + + [21] Braden, R., "T/TCP -- TCP Extensions for Transactions + Functional Specification", RFC 1644, DOI 10.17487/RFC1644, + July 1994, . + + [22] Mathis, M., Mahdavi, J., Floyd, S., and A. Romanow, "TCP + Selective Acknowledgment Options", RFC 2018, + DOI 10.17487/RFC2018, October 1996, + . + + [23] Paxson, V., Allman, M., Dawson, S., Fenner, W., Griner, + J., Heavens, I., Lahey, K., Semke, J., and B. Volz, "Known + TCP Implementation Problems", RFC 2525, + DOI 10.17487/RFC2525, March 1999, + . + + [24] Borman, D., Deering, S., and R. Hinden, "IPv6 Jumbograms", + RFC 2675, DOI 10.17487/RFC2675, August 1999, + . + + [25] Xiao, X., Hannan, A., Paxson, V., and E. Crabbe, "TCP + Processing of the IPv4 Precedence Field", RFC 2873, + DOI 10.17487/RFC2873, June 2000, + . + + [26] Floyd, S., Mahdavi, J., Mathis, M., and M. Podolsky, "An + Extension to the Selective Acknowledgement (SACK) Option + for TCP", RFC 2883, DOI 10.17487/RFC2883, July 2000, + . + + [27] Lahey, K., "TCP Problems with Path MTU Discovery", + RFC 2923, DOI 10.17487/RFC2923, September 2000, + . + + [28] Balakrishnan, H., Padmanabhan, V., Fairhurst, G., and M. + Sooriyabandara, "TCP Performance Implications of Network + Path Asymmetry", BCP 69, RFC 3449, DOI 10.17487/RFC3449, + December 2002, . + + [29] Allman, M., "TCP Congestion Control with Appropriate Byte + Counting (ABC)", RFC 3465, DOI 10.17487/RFC3465, February + 2003, . + + [30] Fenner, B., "Experimental Values In IPv4, IPv6, ICMPv4, + ICMPv6, UDP, and TCP Headers", RFC 4727, + DOI 10.17487/RFC4727, November 2006, + . + + [31] Mathis, M. and J. Heffner, "Packetization Layer Path MTU + Discovery", RFC 4821, DOI 10.17487/RFC4821, March 2007, + . + + [32] Eddy, W., "TCP SYN Flooding Attacks and Common + Mitigations", RFC 4987, DOI 10.17487/RFC4987, August 2007, + . + + [33] Touch, J., "Defending TCP Against Spoofing Attacks", + RFC 4953, DOI 10.17487/RFC4953, July 2007, + . + + [34] Culley, P., Elzur, U., Recio, R., Bailey, S., and J. + Carrier, "Marker PDU Aligned Framing for TCP + Specification", RFC 5044, DOI 10.17487/RFC5044, October + 2007, . + + [35] Gont, F., "TCP's Reaction to Soft Errors", RFC 5461, + DOI 10.17487/RFC5461, February 2009, + . + + [36] StJohns, M., Atkinson, R., and G. Thomas, "Common + Architecture Label IPv6 Security Option (CALIPSO)", + RFC 5570, DOI 10.17487/RFC5570, July 2009, + . + + [37] Sandlund, K., Pelletier, G., and L-E. Jonsson, "The RObust + Header Compression (ROHC) Framework", RFC 5795, + DOI 10.17487/RFC5795, March 2010, + . + + [38] Touch, J., Mankin, A., and R. Bonica, "The TCP + Authentication Option", RFC 5925, DOI 10.17487/RFC5925, + June 2010, . + + [39] Gont, F. and A. Yourtchenko, "On the Implementation of the + TCP Urgent Mechanism", RFC 6093, DOI 10.17487/RFC6093, + January 2011, . + + [40] Gont, F., "Reducing the TIME-WAIT State Using TCP + Timestamps", BCP 159, RFC 6191, DOI 10.17487/RFC6191, + April 2011, . + + [41] Bashyam, M., Jethanandani, M., and A. Ramaiah, "TCP Sender + Clarification for Persist Condition", RFC 6429, + DOI 10.17487/RFC6429, December 2011, + . + + [42] Gont, F. and S. Bellovin, "Defending against Sequence + Number Attacks", RFC 6528, DOI 10.17487/RFC6528, February + 2012, . + + [43] Borman, D., "TCP Options and Maximum Segment Size (MSS)", + RFC 6691, DOI 10.17487/RFC6691, July 2012, + . + + [44] Touch, J., "Updated Specification of the IPv4 ID Field", + RFC 6864, DOI 10.17487/RFC6864, February 2013, + . + + [45] Touch, J., "Shared Use of Experimental TCP Options", + RFC 6994, DOI 10.17487/RFC6994, August 2013, + . + + [46] McPherson, D., Oran, D., Thaler, D., and E. Osterweil, + "Architectural Considerations of IP Anycast", RFC 7094, + DOI 10.17487/RFC7094, January 2014, + . + + [47] Borman, D., Braden, B., Jacobson, V., and R. + Scheffenegger, Ed., "TCP Extensions for High Performance", + RFC 7323, DOI 10.17487/RFC7323, September 2014, + . + + [48] Cheng, Y., Chu, J., Radhakrishnan, S., and A. Jain, "TCP + Fast Open", RFC 7413, DOI 10.17487/RFC7413, December 2014, + . + + [49] Duke, M., Braden, R., Eddy, W., Blanton, E., and A. + Zimmermann, "A Roadmap for Transmission Control Protocol + (TCP) Specification Documents", RFC 7414, + DOI 10.17487/RFC7414, February 2015, + . + + [50] Black, D., Ed. and P. Jones, "Differentiated Services + (Diffserv) and Real-Time Communication", RFC 7657, + DOI 10.17487/RFC7657, November 2015, + . + + [51] Fairhurst, G. and M. Welzl, "The Benefits of Using + Explicit Congestion Notification (ECN)", RFC 8087, + DOI 10.17487/RFC8087, March 2017, + . + + [52] Fairhurst, G., Ed., Trammell, B., Ed., and M. Kuehlewind, + Ed., "Services Provided by IETF Transport Protocols and + Congestion Control Mechanisms", RFC 8095, + DOI 10.17487/RFC8095, March 2017, + . + + [53] Welzl, M., Tuexen, M., and N. Khademi, "On the Usage of + Transport Features Provided by IETF Transport Protocols", + RFC 8303, DOI 10.17487/RFC8303, February 2018, + . + + [54] Black, D., "Relaxing Restrictions on Explicit Congestion + Notification (ECN) Experimentation", RFC 8311, + DOI 10.17487/RFC8311, January 2018, + . + + [55] Chown, T., Loughney, J., and T. Winters, "IPv6 Node + Requirements", BCP 220, RFC 8504, DOI 10.17487/RFC8504, + January 2019, . + + [56] Trammell, B. and M. Kuehlewind, "The Wire Image of a + Network Protocol", RFC 8546, DOI 10.17487/RFC8546, April + 2019, . + + [57] Bittau, A., Giffin, D., Handley, M., Mazieres, D., Slack, + Q., and E. Smith, "Cryptographic Protection of TCP Streams + (tcpcrypt)", RFC 8548, DOI 10.17487/RFC8548, May 2019, + . + + [58] Hardie, T., Ed., "Transport Protocol Path Signals", + RFC 8558, DOI 10.17487/RFC8558, April 2019, + . + + [59] Ford, A., Raiciu, C., Handley, M., Bonaventure, O., and C. + Paasch, "TCP Extensions for Multipath Operation with + Multiple Addresses", RFC 8684, DOI 10.17487/RFC8684, March + 2020, . + + [60] Iyengar, J., Ed. and M. Thomson, Ed., "QUIC: A UDP-Based + Multiplexed and Secure Transport", RFC 9000, + DOI 10.17487/RFC9000, May 2021, + . + + [61] Fairhurst, G. and C. Perkins, "Considerations around + Transport Header Confidentiality, Network Operations, and + the Evolution of Internet Transport Protocols", RFC 9065, + DOI 10.17487/RFC9065, July 2021, + . + + [62] IANA, "Transmission Control Protocol (TCP) Parameters", + . + + [63] Gont, F., "Processing of IP Security/Compartment and + Precedence Information by TCP", Work in Progress, + Internet-Draft, draft-gont-tcpm-tcp-seccomp-prec-00, 29 + March 2012, . + + [64] Gont, F. and D. Borman, "On the Validation of TCP Sequence + Numbers", Work in Progress, Internet-Draft, draft-gont- + tcpm-tcp-seq-validation-04, 11 March 2019, + . + + [65] Touch, J. and W. M. Eddy, "TCP Extended Data Offset + Option", Work in Progress, Internet-Draft, draft-ietf- + tcpm-tcp-edo-12, 15 April 2022, + . + + [66] McQuistin, S., Band, V., Jacob, D., and C. Perkins, + "Describing Protocol Data Units with Augmented Packet + Header Diagrams", Work in Progress, Internet-Draft, draft- + mcquistin-augmented-ascii-diagrams-10, 7 March 2022, + . + + [67] Thomson, M. and T. Pauly, "Long-Term Viability of Protocol + Extension Mechanisms", RFC 9170, DOI 10.17487/RFC9170, + December 2021, . + + [68] Minshall, G., "A Suggested Modification to Nagle's + Algorithm", Work in Progress, Internet-Draft, draft- + minshall-nagle-01, 18 June 1999, + . + + [69] Dalal, Y. and C. Sunshine, "Connection Management in + Transport Protocols", Computer Networks, Vol. 2, No. 6, + pp. 454-473, DOI 10.1016/0376-5075(78)90053-3, December + 1978, . + + [70] Faber, T., Touch, J., and W. Yui, "The TIME-WAIT state in + TCP and Its Effect on Busy Servers", Proceedings of IEEE + INFOCOM, pp. 1573-1583, DOI 10.1109/INFCOM.1999.752180, + March 1999, . + + [71] Postel, J., "Comments on Action Items from the January + Meeting", IEN 177, March 1981, + . + + [72] "Segmentation Offloads", The Linux Kernel Documentation, + . + + [73] RFC Errata, Erratum ID 573, RFC 793, + . + + [74] RFC Errata, Erratum ID 574, RFC 793, + . + + [75] RFC Errata, Erratum ID 700, RFC 793, + . + + [76] RFC Errata, Erratum ID 701, RFC 793, + . + + [77] RFC Errata, Erratum ID 1283, RFC 793, + . + + [78] RFC Errata, Erratum ID 1561, RFC 793, + . + + [79] RFC Errata, Erratum ID 1562, RFC 793, + . + + [80] RFC Errata, Erratum ID 1564, RFC 793, + . + + [81] RFC Errata, Erratum ID 1571, RFC 793, + . + + [82] RFC Errata, Erratum ID 1572, RFC 793, + . + + [83] RFC Errata, Erratum ID 2297, RFC 793, + . + + [84] RFC Errata, Erratum ID 2298, RFC 793, + . + + [85] RFC Errata, Erratum ID 2748, RFC 793, + . + + [86] RFC Errata, Erratum ID 2749, RFC 793, + . + + [87] RFC Errata, Erratum ID 2934, RFC 793, + . + + [88] RFC Errata, Erratum ID 3213, RFC 793, + . + + [89] RFC Errata, Erratum ID 3300, RFC 793, + . + + [90] RFC Errata, Erratum ID 3301, RFC 793, + . + + [91] RFC Errata, Erratum ID 6222, RFC 793, + . + + [92] RFC Errata, Erratum ID 572, RFC 793, + . + + [93] RFC Errata, Erratum ID 575, RFC 793, + . + + [94] RFC Errata, Erratum ID 1565, RFC 793, + . + + [95] RFC Errata, Erratum ID 1569, RFC 793, + . + + [96] RFC Errata, Erratum ID 2296, RFC 793, + . + + [97] RFC Errata, Erratum ID 3305, RFC 793, + . + + [98] RFC Errata, Erratum ID 3602, RFC 793, + . + + [99] RFC Errata, Erratum ID 4772, RFC 5961, + . + + [100] Gont, F., "ICMP Attacks against TCP", RFC 5927, + DOI 10.17487/RFC5927, July 2010, + . + +Appendix A. Other Implementation Notes + + This section includes additional notes and references on TCP + implementation decisions that are currently not a part of the RFC + series or included within the TCP standard. These items can be + considered by implementers, but there was not yet a consensus to + include them in the standard. + +A.1. IP Security Compartment and Precedence + + The IPv4 specification [1] includes a precedence value in the (now + obsoleted) Type of Service (TOS) field. It was modified in [20] and + then obsoleted by the definition of Differentiated Services + (Diffserv) [4]. Setting and conveying TOS between the network layer, + TCP implementation, and applications is obsolete and is replaced by + Diffserv in the current TCP specification. + + RFC 793 required checking the IP security compartment and precedence + on incoming TCP segments for consistency within a connection and with + application requests. Each of these aspects of IP have become + outdated, without specific updates to RFC 793. The issues with + precedence were fixed by [25], which is Standards Track, and so this + present TCP specification includes those changes. However, the state + of IP security options that may be used by Multi-Level Secure (MLS) + systems is not as apparent in the IETF currently. + + Resetting connections when incoming packets do not meet expected + security compartment or precedence expectations has been recognized + as a possible attack vector [63], and there has been discussion about + amending the TCP specification to prevent connections from being + aborted due to nonmatching IP security compartment and Diffserv + codepoint values. + +A.1.1. Precedence + + In Diffserv, the former precedence values are treated as Class + Selector codepoints, and methods for compatible treatment are + described in the Diffserv architecture. The RFC TCP specification + defined by RFCs 793 and 1122 included logic intending to have + connections use the highest precedence requested by either endpoint + application, and to keep the precedence consistent throughout a + connection. This logic from the obsolete TOS is not applicable to + Diffserv and should not be included in TCP implementations, though + changes to Diffserv values within a connection are discouraged. For + discussion of this, see RFC 7657 (Sections 5.1, 5.3, and 6) [50]. + + The obsoleted TOS processing rules in TCP assumed bidirectional (or + symmetric) precedence values used on a connection, but the Diffserv + architecture is asymmetric. Problems with the old TCP logic in this + regard were described in [25], and the solution described is to + ignore IP precedence in TCP. Since RFC 2873 is a Standards Track + document (although not marked as updating RFC 793), current + implementations are expected to be robust in these conditions. Note + that the Diffserv field value used in each direction is a part of the + interface between TCP and the network layer, and values in use can be + indicated both ways between TCP and the application. + +A.1.2. MLS Systems + + The IP Security Option (IPSO) and compartment defined in [1] was + refined in RFC 1038, which was later obsoleted by RFC 1108. The + Commercial IP Security Option (CIPSO) is defined in FIPS-188 + (withdrawn by NIST in 2015) and is supported by some vendors and + operating systems. RFC 1108 is now Historic, though RFC 791 itself + has not been updated to remove the IP Security Option. For IPv6, a + similar option (Common Architecture Label IPv6 Security Option + (CALIPSO)) has been defined [36]. RFC 793 includes logic that + includes the IP security/compartment information in treatment of TCP + segments. References to the IP "security/compartment" in this + document may be relevant for Multi-Level Secure (MLS) system + implementers but can be ignored for non-MLS implementations, + consistent with running code on the Internet. See Appendix A.1 for + further discussion. Note that RFC 5570 describes some MLS networking + scenarios where IPSO, CIPSO, or CALIPSO may be used. In these + special cases, TCP implementers should see Section 7.3.1 of RFC 5570 + and follow the guidance in that document. + +A.2. Sequence Number Validation + + There are cases where the TCP sequence number validation rules can + prevent ACK fields from being processed. This can result in + connection issues, as described in [64], which includes descriptions + of potential problems in conditions of simultaneous open, self- + connects, simultaneous close, and simultaneous window probes. The + document also describes potential changes to the TCP specification to + mitigate the issue by expanding the acceptable sequence numbers. + + In Internet usage of TCP, these conditions rarely occur. Common + operating systems include different alternative mitigations, and the + standard has not been updated yet to codify one of them, but + implementers should consider the problems described in [64]. + +A.3. Nagle Modification + + In common operating systems, both the Nagle algorithm and delayed + acknowledgments are implemented and enabled by default. TCP is used + by many applications that have a request-response style of + communication, where the combination of the Nagle algorithm and + delayed acknowledgments can result in poor application performance. + A modification to the Nagle algorithm is described in [68] that + improves the situation for these applications. + + This modification is implemented in some common operating systems and + does not impact TCP interoperability. Additionally, many + applications simply disable Nagle since this is generally supported + by a socket option. The TCP standard has not been updated to include + this Nagle modification, but implementers may find it beneficial to + consider. + +A.4. Low Watermark Settings + + Some operating system kernel TCP implementations include socket + options that allow specifying the number of bytes in the buffer until + the socket layer will pass sent data to TCP (SO_SNDLOWAT) or to the + application on receiving (SO_RCVLOWAT). + + In addition, another socket option (TCP_NOTSENT_LOWAT) can be used to + control the amount of unsent bytes in the write queue. This can help + a sending TCP application to avoid creating large amounts of buffered + data (and corresponding latency). As an example, this may be useful + for applications that are multiplexing data from multiple upper-level + streams onto a connection, especially when streams may be a mix of + interactive/real-time and bulk data transfer. + +Appendix B. TCP Requirement Summary + + This section is adapted from RFC 1122. + + Note that there is no requirement related to PLPMTUD in this list, + but that PLPMTUD is recommended. + + +=================+=========+======+========+=====+========+======+ + | Feature | ReqID | MUST | SHOULD | MAY | SHOULD | MUST | + | | | | | | NOT | NOT | + +=================+=========+======+========+=====+========+======+ + | PUSH flag | + +=================+=========+======+========+=====+========+======+ + | Aggregate or | MAY-16 | | | X | | | + | queue un-pushed | | | | | | | + | data | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Sender collapse | SHLD-27 | | X | | | | + | successive PSH | | | | | | | + | bits | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | SEND call can | MAY-15 | | | X | | | + | specify PUSH | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | * If cannot: | MUST-60 | | | | | X | + | sender | | | | | | | + | buffer | | | | | | | + | indefinitely | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | * If cannot: | MUST-61 | X | | | | | + | PSH last | | | | | | | + | segment | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Notify | MAY-17 | | | X | | | + | receiving ALP^1 | | | | | | | + | of PSH | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Send max size | SHLD-28 | | X | | | | + | segment when | | | | | | | + | possible | | | | | | | + +=================+=========+======+========+=====+========+======+ + | Window | + +=================+=========+======+========+=====+========+======+ + | Treat as | MUST-1 | X | | | | | + | unsigned number | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Handle as | REC-1 | | X | | | | + | 32-bit number | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Shrink window | SHLD-14 | | | | X | | + | from right | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | * Send new | SHLD-15 | | | | X | | + | data when | | | | | | | + | window | | | | | | | + | shrinks | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | * Retransmit | SHLD-16 | | X | | | | + | old unacked | | | | | | | + | data within | | | | | | | + | window | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | * Time out | SHLD-17 | | | | X | | + | conn for | | | | | | | + | data past | | | | | | | + | right edge | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Robust against | MUST-34 | X | | | | | + | shrinking | | | | | | | + | window | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Receiver's | MAY-8 | | | X | | | + | window closed | | | | | | | + | indefinitely | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Use standard | MUST-35 | X | | | | | + | probing logic | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Sender probe | MUST-36 | X | | | | | + | zero window | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | * First probe | SHLD-29 | | X | | | | + | after RTO | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | * Exponential | SHLD-30 | | X | | | | + | backoff | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Allow window | MUST-37 | X | | | | | + | stay zero | | | | | | | + | indefinitely | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Retransmit old | MAY-7 | | | X | | | + | data beyond | | | | | | | + | SND.UNA+SND.WND | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Process RST and | MUST-66 | X | | | | | + | URG even with | | | | | | | + | zero window | | | | | | | + +=================+=========+======+========+=====+========+======+ + | Urgent Data | + +=================+=========+======+========+=====+========+======+ + | Include support | MUST-30 | X | | | | | + | for urgent | | | | | | | + | pointer | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Pointer | MUST-62 | X | | | | | + | indicates first | | | | | | | + | non-urgent | | | | | | | + | octet | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Arbitrary | MUST-31 | X | | | | | + | length urgent | | | | | | | + | data sequence | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Inform ALP^1 | MUST-32 | X | | | | | + | asynchronously | | | | | | | + | of urgent data | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | ALP^1 can learn | MUST-33 | X | | | | | + | if/how much | | | | | | | + | urgent data Q'd | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | ALP employ the | SHLD-13 | | | | X | | + | urgent | | | | | | | + | mechanism | | | | | | | + +=================+=========+======+========+=====+========+======+ + | TCP Options | + +=================+=========+======+========+=====+========+======+ + | Support the | MUST-4 | X | | | | | + | mandatory | | | | | | | + | option set | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Receive TCP | MUST-5 | X | | | | | + | Option in any | | | | | | | + | segment | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Ignore | MUST-6 | X | | | | | + | unsupported | | | | | | | + | options | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Include length | MUST-68 | X | | | | | + | for all options | | | | | | | + | except EOL+NOP | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Cope with | MUST-7 | X | | | | | + | illegal option | | | | | | | + | length | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Process options | MUST-64 | X | | | | | + | regardless of | | | | | | | + | word alignment | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Implement | MUST-14 | X | | | | | + | sending & | | | | | | | + | receiving MSS | | | | | | | + | Option | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | IPv4 Send MSS | SHLD-5 | | X | | | | + | Option unless | | | | | | | + | 536 | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | IPv6 Send MSS | SHLD-5 | | X | | | | + | Option unless | | | | | | | + | 1220 | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Send MSS Option | MAY-3 | | | X | | | + | always | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | IPv4 Send-MSS | MUST-15 | X | | | | | + | default is 536 | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | IPv6 Send-MSS | MUST-15 | X | | | | | + | default is 1220 | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Calculate | MUST-16 | X | | | | | + | effective send | | | | | | | + | seg size | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | MSS accounts | SHLD-6 | | X | | | | + | for varying MTU | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | MSS not sent on | MUST-65 | | | | | X | + | non-SYN | | | | | | | + | segments | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | MSS value based | MUST-67 | X | | | | | + | on MMS_R | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Pad with zero | MUST-69 | X | | | | | + +=================+=========+======+========+=====+========+======+ + | TCP Checksums | + +=================+=========+======+========+=====+========+======+ + | Sender compute | MUST-2 | X | | | | | + | checksum | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Receiver check | MUST-3 | X | | | | | + | checksum | | | | | | | + +=================+=========+======+========+=====+========+======+ + | ISN Selection | + +=================+=========+======+========+=====+========+======+ + | Include a | MUST-8 | X | | | | | + | clock-driven | | | | | | | + | ISN generator | | | | | | | + | component | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Secure ISN | SHLD-1 | | X | | | | + | generator with | | | | | | | + | a PRF component | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | PRF computable | MUST-9 | | | | | X | + | from outside | | | | | | | + | the host | | | | | | | + +=================+=========+======+========+=====+========+======+ + | Opening Connections | + +=================+=========+======+========+=====+========+======+ + | Support | MUST-10 | X | | | | | + | simultaneous | | | | | | | + | open attempts | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | SYN-RECEIVED | MUST-11 | X | | | | | + | remembers last | | | | | | | + | state | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Passive OPEN | MUST-41 | | | | | X | + | call interfere | | | | | | | + | with others | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Function: | MUST-42 | X | | | | | + | simultaneously | | | | | | | + | LISTENs for | | | | | | | + | same port | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Ask IP for src | MUST-44 | X | | | | | + | address for SYN | | | | | | | + | if necessary | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | * Otherwise, | MUST-45 | X | | | | | + | use local | | | | | | | + | addr of | | | | | | | + | connection | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | OPEN to | MUST-46 | | | | | X | + | broadcast/ | | | | | | | + | multicast IP | | | | | | | + | address | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Silently | MUST-57 | X | | | | | + | discard seg to | | | | | | | + | bcast/mcast | | | | | | | + | addr | | | | | | | + +=================+=========+======+========+=====+========+======+ + | Closing Connections | + +=================+=========+======+========+=====+========+======+ + | RST can contain | SHLD-2 | | X | | | | + | data | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Inform | MUST-12 | X | | | | | + | application of | | | | | | | + | aborted conn | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Half-duplex | MAY-1 | | | X | | | + | close | | | | | | | + | connections | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | * Send RST to | SHLD-3 | | X | | | | + | indicate | | | | | | | + | data lost | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | In TIME-WAIT | MUST-13 | X | | | | | + | state for 2MSL | | | | | | | + | seconds | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | * Accept SYN | MAY-2 | | | X | | | + | from TIME- | | | | | | | + | WAIT state | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | * Use | SHLD-4 | | X | | | | + | Timestamps | | | | | | | + | to reduce | | | | | | | + | TIME-WAIT | | | | | | | + +=================+=========+======+========+=====+========+======+ + | Retransmissions | + +=================+=========+======+========+=====+========+======+ + | Implement | MUST-19 | X | | | | | + | exponential | | | | | | | + | backoff, slow | | | | | | | + | start, and | | | | | | | + | congestion | | | | | | | + | avoidance | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Retransmit with | MAY-4 | | | X | | | + | same IP | | | | | | | + | identity | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Karn's | MUST-18 | X | | | | | + | algorithm | | | | | | | + +=================+=========+======+========+=====+========+======+ + | Generating ACKs | + +=================+=========+======+========+=====+========+======+ + | Aggregate | MUST-58 | X | | | | | + | whenever | | | | | | | + | possible | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Queue out-of- | SHLD-31 | | X | | | | + | order segments | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Process all Q'd | MUST-59 | X | | | | | + | before send ACK | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Send ACK for | MAY-13 | | | X | | | + | out-of-order | | | | | | | + | segment | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Delayed ACKs | SHLD-18 | | X | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | * Delay < 0.5 | MUST-40 | X | | | | | + | seconds | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | * Every 2nd | SHLD-19 | | X | | | | + | full-sized | | | | | | | + | segment or | | | | | | | + | 2*RMSS ACK'd | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Receiver SWS- | MUST-39 | X | | | | | + | Avoidance | | | | | | | + | Algorithm | | | | | | | + +=================+=========+======+========+=====+========+======+ + | Sending Data | + +=================+=========+======+========+=====+========+======+ + | Configurable | MUST-49 | X | | | | | + | TTL | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Sender SWS- | MUST-38 | X | | | | | + | Avoidance | | | | | | | + | Algorithm | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Nagle algorithm | SHLD-7 | | X | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | * Application | MUST-17 | X | | | | | + | can disable | | | | | | | + | Nagle | | | | | | | + | algorithm | | | | | | | + +=================+=========+======+========+=====+========+======+ + | Connection Failures | + +=================+=========+======+========+=====+========+======+ + | Negative advice | MUST-20 | X | | | | | + | to IP on R1 | | | | | | | + | retransmissions | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Close | MUST-20 | X | | | | | + | connection on | | | | | | | + | R2 | | | | | | | + | retransmissions | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | ALP^1 can set | MUST-21 | X | | | | | + | R2 | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Inform ALP of | SHLD-9 | | X | | | | + | R1<=retxs | | | | | | | + | inform ALP | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | * Abort on | MUST-56 | | | | | X | + | Dest Unreach | | | | | | | + | (0,1,5) | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | * Dest Unreach | SHLD-26 | | X | | | | + | (2-4) => | | | | | | | + | abort conn | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | * Source | MUST-55 | X | | | | | + | Quench => | | | | | | | + | silent | | | | | | | + | discard | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | * Abort on | MUST-56 | | | | | X | + | Time | | | | | | | + | Exceeded | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | * Abort on | MUST-56 | | | | | X | + | Param | | | | | | | + | Problem | | | | | | | + +=================+=========+======+========+=====+========+======+ + | Address Validation | + +=================+=========+======+========+=====+========+======+ + | Reject OPEN | MUST-46 | X | | | | | + | call to invalid | | | | | | | + | IP address | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Reject SYN from | MUST-63 | X | | | | | + | invalid IP | | | | | | | + | address | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Silently | MUST-57 | X | | | | | + | discard SYN to | | | | | | | + | bcast/mcast | | | | | | | + | addr | | | | | | | + +=================+=========+======+========+=====+========+======+ + | TCP/ALP Interface Services | + +=================+=========+======+========+=====+========+======+ + | Error Report | MUST-47 | X | | | | | + | mechanism | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | ALP can disable | SHLD-20 | | X | | | | + | Error Report | | | | | | | + | Routine | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | ALP can specify | MUST-48 | X | | | | | + | Diffserv field | | | | | | | + | for sending | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | * Passed | SHLD-22 | | X | | | | + | unchanged to | | | | | | | + | IP | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | ALP can change | SHLD-21 | | X | | | | + | Diffserv field | | | | | | | + | during | | | | | | | + | connection | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | ALP generally | SHLD-23 | | | | X | | + | changing | | | | | | | + | Diffserv during | | | | | | | + | conn. | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Pass received | MAY-9 | | | X | | | + | Diffserv field | | | | | | | + | up to ALP | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + | FLUSH call | MAY-14 | | | X | | | + +-----------------+---------+------+--------+-----+--------+------+ + | Optional local | MUST-43 | X | | | | | + | IP addr param | | | | | | | + | in OPEN | | | | | | | + +=================+=========+======+========+=====+========+======+ + | RFC 5961 Support | + +=================+=========+======+========+=====+========+======+ + | Implement data | MAY-12 | | | X | | | + | injection | | | | | | | + | protection | | | | | | | + +=================+=========+======+========+=====+========+======+ + | Explicit Congestion Notification | + +=================+=========+======+========+=====+========+======+ + | Support ECN | SHLD-8 | | X | | | | + +=================+=========+======+========+=====+========+======+ + | Alternative Congestion Control | + +=================+=========+======+========+=====+========+======+ + | Implement | MAY-18 | | | X | | | + | alternative | | | | | | | + | conformant | | | | | | | + | algorithm(s) | | | | | | | + +-----------------+---------+------+--------+-----+--------+------+ + + Table 8: TCP Requirements Summary + + FOOTNOTES: (1) "ALP" means Application-Layer Program. + +Acknowledgments + + This document is largely a revision of RFC 793, of which Jon Postel + was the editor. Due to his excellent work, it was able to last for + three decades before we felt the need to revise it. + + Andre Oppermann was a contributor and helped to edit the first + revision of this document. + + We are thankful for the assistance of the IETF TCPM working group + chairs over the course of work on this document: + + Michael Scharf + + + Yoshifumi Nishida + + + Pasi Sarolahti + + + Michael Tüxen + + + During the discussions of this work on the TCPM mailing list, in + working group meetings, and via area reviews, helpful comments, + critiques, and reviews were received from (listed alphabetically by + last name): Praveen Balasubramanian, David Borman, Mohamed Boucadair, + Bob Briscoe, Neal Cardwell, Yuchung Cheng, Martin Duke, Francis + Dupont, Ted Faber, Gorry Fairhurst, Fernando Gont, Rodney Grimes, Yi + Huang, Rahul Jadhav, Markku Kojo, Mike Kosek, Juhamatti Kuusisaari, + Kevin Lahey, Kevin Mason, Matt Mathis, Stephen McQuistin, Jonathan + Morton, Matt Olson, Tommy Pauly, Tom Petch, Hagen Paul Pfeifer, Kyle + Rose, Anthony Sabatini, Michael Scharf, Greg Skinner, Joe Touch, + Michael Tüxen, Reji Varghese, Bernie Volz, Tim Wicinski, Lloyd Wood, + and Alex Zimmermann. + + Joe Touch provided additional help in clarifying the description of + segment size parameters and PMTUD/PLPMTUD recommendations. Markku + Kojo helped put together the text in the section on TCP Congestion + Control. + + This document includes content from errata that were reported by + (listed chronologically): Yin Shuming, Bob Braden, Morris M. Keesan, + Pei-chun Cheng, Constantin Hagemeier, Vishwas Manral, Mykyta + Yevstifeyev, EungJun Yi, Botong Huang, Charles Deng, Merlin Buge. + +Author's Address + + Wesley M. Eddy (editor) + MTI Systems + United States of America + Email: wes@mti-systems.com diff --git a/dist/setup.sh b/dist/setup.sh index c020182..6e73638 100644 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -8,6 +8,7 @@ sudo apt install -y \ m4 \ pkg-config \ git \ + gnupg \ libbpf-dev \ libxdp-dev \ libpcap-dev \ @@ -30,3 +31,34 @@ sudo /tmp/aws-cli/aws/install curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source $HOME/.cargo/env rustup component add rust-analyzer +cargo +stable install cargo-llvm-cov --locked + +curl -fsSL https://claude.ai/install.sh | bash + +cat >> ~/.zshrc << EOF +export PATH="$HOME/.local/bin:$PATH" + +SSH_ENV="$HOME/.ssh/agent-environment" + +function start_agent { + echo "Initialising new SSH agent..." + /usr/bin/ssh-agent | sed 's/^echo/#echo/' >"$SSH_ENV" + echo succeeded + chmod 600 "$SSH_ENV" + . "$SSH_ENV" >/dev/null + /usr/bin/ssh-add; +} + +# Source SSH settings, if applicable + +if [ -f "$SSH_ENV" ]; then + . "$SSH_ENV" >/dev/null + #ps $SSH_AGENT_PID doesn't work under Cygwin + ps -ef | grep $SSH_AGENT_PID | grep ssh-agent$ >/dev/null || { + start_agent + } +else + start_agent +fi + +EOF diff --git a/docs/plans/2026-03-06-tcp-connection-teardown-design.md b/docs/plans/2026-03-06-tcp-connection-teardown-design.md new file mode 100644 index 0000000..ae7598d --- /dev/null +++ b/docs/plans/2026-03-06-tcp-connection-teardown-design.md @@ -0,0 +1,199 @@ +# TCP Connection Teardown Design + +## Context + +VoidNet's TCP implementation has full handshake, data transfer, retransmission, and congestion control. Connection close currently sends RST immediately — no graceful FIN sequence, no TIME-WAIT. This design covers RFC 9293-compliant connection teardown. + +Target use case: high-performance HTTP/2 servers on both datacenter and internet traffic. The implementation must handle all teardown paths correctly while maintaining the frame safety invariant and zero hot-path allocations. + +## Frame Safety Invariant + +Same as data transfer: every `Frame<'umem>` popped from `free_frames` MUST be pushed to `tx_return` or `rx_return`. FIN segments are built the same way as data segments via `poll_send` — no new frame concerns. Incoming frames in teardown states are returned to `rx_return` immediately. + +## State Machine Transitions + +### Active Close (we initiate) + +``` +Established → FinWait1 (send FIN via poll_send) +FinWait1 + recv ACK of FIN → FinWait2 +FinWait2 + recv FIN → TimeWait (send ACK, start timer) +TimeWait + timer expires → remove connection +``` + +### Passive Close (remote initiates) + +``` +Established + recv FIN → CloseWait (send ACK, signal EOF) +CloseWait + send buffer drained → LastAck (send FIN via poll_send) +LastAck + recv ACK of FIN → remove connection +``` + +### Simultaneous Close (both FIN at same time) + +``` +FinWait1 + recv FIN (without ACK of our FIN) → Closing (send ACK) +Closing + recv ACK of our FIN → TimeWait (start timer) +``` + +## FIN Sending Mechanism + +Lazy FIN via `pending_fin` flag on the TCB. `TcpStream::close()` sets the flag. `poll_send` checks the flag each tick: + +- If send buffer has unsent data: send data as normal, FIN waits. +- If send buffer fully sent (`snd_nxt` caught up to all buffered data): + - Build segment with FIN+ACK flags, no payload. + - `fin_seq = snd_nxt`, `snd_nxt += 1` (FIN consumes one sequence number). + - `pending_fin = false`. + - Transition: Established → FinWait1, CloseWait → LastAck. + +FIN segments reuse `SegmentBuilder` — just ACK+FIN flags with empty payload. Same frame allocation path as data segments. + +### FIN ACK Tracking + +`fin_seq: Option` stores the sequence number of our FIN. Our FIN is ACKed when `snd_una > fin_seq`. This triggers FinWait1 → FinWait2 and LastAck → Closed transitions. + +## Receiving FIN in Established State + +In `process_established`, after processing any data payload (FIN can piggyback on data): + +1. `rcv_nxt += 1` (FIN consumes one sequence number). +2. Send ACK. +3. Transition to CloseWait. +4. Push `TcpEvent::RemoteClose` to event queue. + +## Teardown State Processing + +Each teardown state handles incoming segments. Common pattern: RST check first, then ACK/FIN processing. + +### FinWait1 + +- RST → abort (remove connection). +- ACK covers `fin_seq + 1` → transition to FinWait2. +- FIN received → send ACK, `rcv_nxt += 1`. + - If ACK also covers our FIN → TimeWait (start timer). + - Else → Closing. +- Data payload → process same as Established (remote hasn't closed yet). + +### FinWait2 + +- RST → abort. +- FIN received → send ACK, `rcv_nxt += 1`, transition to TimeWait (start timer). +- Data payload → process same as Established (remote still sending). + +### CloseWait + +- RST → abort. +- No new data expected from remote. +- `poll_send` handles draining send buffer and sending FIN → LastAck. + +### Closing + +- RST → abort. +- ACK covers `fin_seq + 1` → transition to TimeWait (start timer). + +### LastAck + +- RST → abort. +- ACK covers `fin_seq + 1` → remove connection. + +### TimeWait + +- RST → **ignore** (not abort — prevents RST attacks from killing TIME-WAIT). +- FIN retransmit → re-send ACK, restart timer. +- Everything else → ignore. +- Timer expires → remove connection. + +## EOF Signaling + +`TcpEvent::RemoteClose` is a new variant on the `TcpEvent` enum. + +`TcpStream::read()` behavior: +- If `recv_buffer` has data → `Ready(n)` as today. +- If `recv_buffer` empty AND connection state is CloseWait, LastAck, TimeWait, or gone → `Ready(0)` (EOF). +- Else → `Pending`. + +This lets the user drain remaining buffered data after remote FINs, then see EOF. Standard socket semantics. + +## TIME-WAIT Cleanup + +`evict_stale` is called every ~65536 iterations in the runtime loop. Currently empty. Fill it in: + +``` +For each connection in TimeWait state: + If now >= time_wait_deadline → remove connection +``` + +Cheap iteration — no frame allocation. TIME-WAIT connections only send ACKs in response to FIN retransmits (handled in `process_teardown`). + +## TcpStream::close() Rewrite + +Replace the current RST hack (dummy frame buffers, `MacAddress::zero()`): + +``` +close(): + If already closed → return + Set closed = true + Get handler, find connection by conn_id + If connection exists and state is Established or CloseWait: + Set pending_fin = true + Else: + Leave it (already tearing down, or already gone) +``` + +No frame buffers needed. No MAC addresses needed. `poll_send` handles everything on the next tick. + +## TCB Additions + +``` +pending_fin: bool, // Set by close(), consumed by poll_send +fin_seq: Option, // Sequence number of our FIN +time_wait_deadline: Option, // When to remove TIME-WAIT connection +time_wait_duration: u64, // Configurable, from TcpConfig, in ms +``` + +## TcpConfig Addition + +```rust +pub struct TcpConfig { + pub send_buffer_size: usize, // existing, default 256KB + pub recv_buffer_size: usize, // existing, default 256KB + pub backlog: usize, // existing, default 128 + pub time_wait_duration_ms: u64, // new, default 60_000 +} +``` + +Flows through `listen_with_config` / `connect_with_config` into the TCB at connection creation, same pattern as buffer sizes. + +## New/Modified Methods + +| Method | Change | +|---|---| +| `poll_send` | Extended: FIN sending for Established (pending_fin) and CloseWait | +| `process_established` | Extended: FIN flag → CloseWait transition | +| `process_teardown` | New: dispatches FinWait1/FinWait2/CloseWait/Closing/LastAck/TimeWait | +| `evict_stale` | Filled in: TIME-WAIT deadline checks | +| `TcpStream::close()` | Rewritten: sets pending_fin flag only | +| `TcpStream::read()` | Extended: returns Ready(0) on EOF | +| `TcpEvent` | New variant: RemoteClose | + +## Decisions Summary + +| Decision | Choice | +|---|---| +| Scope | Full RFC 9293 — all 6 teardown states + simultaneous close | +| TIME-WAIT duration | Configurable via TcpConfig, default 60s | +| Half-open (CloseWait) | Drain send buffer, auto-FIN, read() returns 0 on EOF | +| FIN mechanism | Lazy — pending_fin flag, poll_send sends next tick | +| RST in teardown | Aborts all states except TIME-WAIT | +| FIN tracking | fin_seq on TCB, ACKed when snd_una > fin_seq | +| EOF signaling | TcpEvent::RemoteClose + read() checks connection state | +| TIME-WAIT cleanup | evict_stale checks time_wait_deadline | +| close() rewrite | Sets flag only, no frame buffers or MACs needed | +| Frame safety | No new concerns — FIN uses same path as data segments | + +## Deferred + +- Linger option (SO_LINGER — force RST on close with timeout) +- TCP keep-alive probes +- Half-close API (`shutdown(SHUT_WR)` vs full `close()`) diff --git a/docs/plans/2026-03-06-tcp-connection-teardown-impl.md b/docs/plans/2026-03-06-tcp-connection-teardown-impl.md new file mode 100644 index 0000000..91ba1a3 --- /dev/null +++ b/docs/plans/2026-03-06-tcp-connection-teardown-impl.md @@ -0,0 +1,1362 @@ +# TCP Connection Teardown Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Implement RFC 9293-compliant TCP connection teardown with FIN/FIN-ACK, all 6 teardown states (FinWait1, FinWait2, CloseWait, Closing, LastAck, TimeWait), configurable TIME-WAIT duration, and graceful half-open draining. + +**Architecture:** Lazy FIN sending via `pending_fin` flag on the TCB — `poll_send` sends FIN on next tick after send buffer drains. Teardown state processing in a new `process_teardown` method dispatching by state. TIME-WAIT cleaned up by `evict_stale`. `TcpStream::close()` rewritten to set flag only. + +**Tech Stack:** Rust (2024 edition), no new dependencies. Uses existing `coarsetime`, `SegmentBuilder`, `FrameBuffer`, `LocalQueue` infrastructure. + +**Design doc:** `docs/plans/2026-03-06-tcp-connection-teardown-design.md` + +--- + +### Task 1: TCB Teardown Fields + TcpEvent::RemoteClose + +**Files:** +- Modify: `src/net/handler/tcp/tcb.rs` + +**Step 1: Add `RemoteClose` to `TcpEvent`** + +In `tcb.rs`, add to the `TcpEvent` enum (~line 35): + +```rust +pub enum TcpEvent { + Connected, + ConnectionRefused, + Reset, + Timeout, + RemoteClose, +} +``` + +**Step 2: Add teardown fields to `Tcb`** + +After the `last_send_time` field (~line 171), add: + +```rust + // --- Connection teardown --- + /// Set by close(), consumed by poll_send to send FIN. + pub pending_fin: bool, + /// Sequence number of our FIN (needed to detect when FIN is ACKed). + pub fin_seq: Option, + /// Deadline for removing TIME-WAIT connections. + pub time_wait_deadline: Option, + /// TIME-WAIT duration in milliseconds (from TcpConfig). + pub time_wait_duration: u64, +``` + +**Step 3: Add `time_wait_duration_ms` to `TcpConfig`** + +In `TcpConfig` (~line 73), add: + +```rust +pub struct TcpConfig { + pub send_buffer_size: usize, + pub recv_buffer_size: usize, + pub backlog: usize, + /// TIME-WAIT duration in milliseconds. Default: 60000 (60 seconds). + pub time_wait_duration_ms: u64, +} +``` + +Update `Default` impl: + +```rust +impl Default for TcpConfig { + fn default() -> Self { + Self { + send_buffer_size: 256 * 1024, + recv_buffer_size: 256 * 1024, + backlog: 128, + time_wait_duration_ms: 60_000, + } + } +} +``` + +**Step 4: Add `is_closing` helper to `TcpState`** + +In `src/net/handler/tcp/state.rs`, add a helper method: + +```rust +/// Returns `true` for states where the remote has sent FIN +/// (receive side is closed — read should return EOF when buffer empty). +#[inline] +pub fn is_remote_closed(self) -> bool { + matches!( + self, + TcpState::CloseWait + | TcpState::LastAck + | TcpState::TimeWait + | TcpState::Closing + | TcpState::Closed + ) +} +``` + +**Step 5: Update all TCB construction sites in `mod.rs`** + +There are two construction sites in `src/net/handler/tcp/mod.rs`: +1. `connect_with_config()` (~line 135-167) +2. `process_listen()` (~line 538-568) + +Add to both: +```rust +pending_fin: false, +fin_seq: None, +time_wait_deadline: None, +time_wait_duration: config.time_wait_duration_ms, // or 60_000 for default path +``` + +For `process_listen`, the listener's `ListenEntry` needs a `time_wait_duration` field. Add it to `ListenEntry` and populate from config in `listen_with_config`. + +**Step 6: Run tests to verify compilation** + +Run: `cargo test` +Expected: PASS (all existing tests still work, new fields have defaults) + +**Step 7: Commit** + +```bash +git add src/net/handler/tcp/tcb.rs src/net/handler/tcp/state.rs src/net/handler/tcp/mod.rs +git commit -m "feat(tcp): TCB teardown fields, TcpEvent::RemoteClose, TcpConfig time_wait" +``` + +--- + +### Task 2: SegmentBuilder::build_fin_ack + +**Files:** +- Modify: `src/net/handler/tcp/segment.rs` + +**Step 1: Write the failing test** + +Add to the `tests` module in `segment.rs`: + +```rust +#[test] +fn build_fin_ack_ipv4() { + let mut free = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + free.push(alloc_frame(100)); + + SegmentBuilder::build_fin_ack( + IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])), + IpAddress::V4(Ipv4Address::new([10, 0, 0, 2])), + 80, 12345, + 5000, 3000, 65535, + [0xAA; 6].into(), [0xBB; 6].into(), + false, &mut free, &mut tx, + ); + + assert_eq!(tx.num_frames(), 1, "FIN-ACK segment built"); + assert_eq!(free.num_frames(), 0, "free frame consumed"); + + let frame = tx.pop().unwrap(); + let tcp_offset = ETH_LEN + IPV4_MIN_HEADER_LEN; + let tcp = unsafe { TcpHeader::from_bytes_at(&frame, tcp_offset) }; + assert_eq!(tcp.flags(), flags::ACK | flags::FIN); + assert_eq!(tcp.seq_num(), 5000); + assert_eq!(tcp.ack_num(), 3000); +} +``` + +**Step 2: Run test to verify it fails** + +Run: `cargo test build_fin_ack` +Expected: FAIL — `build_fin_ack` not defined. + +**Step 3: Implement build_fin_ack** + +Add to `impl SegmentBuilder` (after `build_ack`): + +```rust +/// Build a FIN-ACK segment (no data). Used for graceful connection close. +pub fn build_fin_ack<'umem>( + local_addr: IpAddress, + remote_addr: IpAddress, + local_port: u16, + remote_port: u16, + seq: u32, + ack: u32, + window: u16, + src_mac: MacAddress, + dst_mac: MacAddress, + tx_offload: bool, + free_frames: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, +) { + match (local_addr, remote_addr) { + (IpAddress::V4(local_ip), IpAddress::V4(remote_ip)) => { + Self::build_ipv4_segment( + local_ip, remote_ip, + local_port, remote_port, + seq, ack, flags::ACK | flags::FIN, window, + &[], + src_mac, dst_mac, + tx_offload, free_frames, tx_return, + ); + } + (IpAddress::V6(local_ip), IpAddress::V6(remote_ip)) => { + Self::build_ipv6_segment( + local_ip, remote_ip, + local_port, remote_port, + seq, ack, flags::ACK | flags::FIN, window, + &[], + src_mac, dst_mac, + tx_offload, free_frames, tx_return, + ); + } + _ => {} + } +} +``` + +**Step 4: Run tests to verify they pass** + +Run: `cargo test build_fin_ack` +Expected: PASS + +**Step 5: Commit** + +```bash +git add src/net/handler/tcp/segment.rs +git commit -m "feat(tcp): SegmentBuilder::build_fin_ack for graceful close" +``` + +--- + +### Task 3: poll_send — FIN Sending for Established and CloseWait + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs` + +**Step 1: Write the failing test** + +Add to the test module in `mod.rs`: + +```rust +#[test] +fn poll_send_sends_fin_when_pending() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1000, 0, flags::SYN, 65535, &[]); + let syn_len = syn_data.len(); + handler.process_ipv4(Frame::new(0, leak(syn_data), syn_len, false), &nh, &mut free, &mut rx, &mut tx); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1001, server_iss.wrapping_add(1), flags::ACK, 65535, &[]); + let ack_len = ack_data.len(); + handler.process_ipv4(Frame::new(1, leak(ack_data), ack_len, false), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + + // Set pending_fin. + handler.connections[0].pending_fin = true; + + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + + // FIN should have been sent. + assert_eq!(tx.num_frames(), 1, "FIN segment sent"); + let tcb = &handler.connections[0]; + assert_eq!(tcb.state, TcpState::FinWait1); + assert!(!tcb.pending_fin, "pending_fin consumed"); + assert!(tcb.fin_seq.is_some(), "fin_seq recorded"); +} + +#[test] +fn poll_send_drains_data_before_fin() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1000, 0, flags::SYN, 65535, &[]); + let syn_len = syn_data.len(); + handler.process_ipv4(Frame::new(0, leak(syn_data), syn_len, false), &nh, &mut free, &mut rx, &mut tx); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1001, server_iss.wrapping_add(1), flags::ACK, 65535, &[]); + let ack_len = ack_data.len(); + handler.process_ipv4(Frame::new(1, leak(ack_data), ack_len, false), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + + // Write data AND set pending_fin. + handler.connections[0].send_buffer.write(b"final data"); + handler.connections[0].snd_wnd = 65535; + handler.connections[0].pending_fin = true; + + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + + // Should send data first, NOT FIN yet (data still in flight). + assert_eq!(tx.num_frames(), 1, "data segment sent"); + assert_eq!(handler.connections[0].state, TcpState::Established, "still Established until data ACKed"); + assert!(handler.connections[0].pending_fin, "pending_fin still set"); +} +``` + +**Step 2: Run test to verify it fails** + +Run: `cargo test poll_send_sends_fin` +Expected: FAIL — current `poll_send` skips non-Established connections and doesn't check `pending_fin`. + +**Step 3: Modify poll_send** + +Rewrite `poll_send` (~line 1145) to handle FIN: + +1. Change the state check from `if tcb.state != TcpState::Established { continue; }` to also allow `TcpState::CloseWait`: +```rust +if tcb.state != TcpState::Established && tcb.state != TcpState::CloseWait { + continue; +} +``` + +2. After the existing data sending logic (after `tcb.snd_nxt = ...`), add FIN logic. The full revised structure: + +```rust +// (existing data sending code stays the same) + +// After data sending: check if we should send FIN. +if tcb.pending_fin { + let bytes_in_flight = tcb.snd_nxt.wrapping_sub(tcb.snd_una) as usize; + let data_available = tcb.send_buffer.available().saturating_sub(bytes_in_flight); + + // Only send FIN when all data has been sent (may still be in flight awaiting ACK). + if data_available == 0 { + let id = tcb.id; + let dst_mac = neighbor_handler + .lookup(now, &id.remote_addr) + .unwrap_or(crate::net::wire::ethernet::MacAddress::broadcast()); + let window = tcb.recv_buffer.free_space().min(u16::MAX as usize) as u16; + + SegmentBuilder::build_fin_ack( + id.local_addr, id.remote_addr, + id.local_port, id.remote_port, + tcb.snd_nxt, tcb.rcv_nxt, window, + src_mac, dst_mac, + self.tx_offload, free_frames, tx_return, + ); + + tcb.fin_seq = Some(tcb.snd_nxt); + tcb.snd_nxt = tcb.snd_nxt.wrapping_add(1); // FIN consumes one sequence number + + tcb.pending_fin = false; + + match tcb.state { + TcpState::Established => tcb.state = TcpState::FinWait1, + TcpState::CloseWait => tcb.state = TcpState::LastAck, + _ => {} + } + + // Set retransmit timer for FIN. + if tcb.retransmit_deadline.is_none() { + tcb.retransmit_deadline = Some(now + coarsetime::Duration::from_millis(tcb.rto)); + } + } +} +``` + +**Step 4: Run tests to verify they pass** + +Run: `cargo test poll_send` +Expected: PASS (all poll_send tests including new ones) + +**Step 5: Commit** + +```bash +git add src/net/handler/tcp/mod.rs +git commit -m "feat(tcp): poll_send sends FIN after draining send buffer" +``` + +--- + +### Task 4: Receive FIN in Established State + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs` + +**Step 1: Write the failing test** + +```rust +#[test] +fn established_receives_fin_transitions_to_close_wait() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1000, 0, flags::SYN, 65535, &[]); + let syn_len = syn_data.len(); + handler.process_ipv4(Frame::new(0, leak(syn_data), syn_len, false), &nh, &mut free, &mut rx, &mut tx); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1001, server_iss.wrapping_add(1), flags::ACK, 65535, &[]); + let ack_len = ack_data.len(); + handler.process_ipv4(Frame::new(1, leak(ack_data), ack_len, false), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + + // Remote sends FIN. + let fin_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1001, server_iss.wrapping_add(1), flags::ACK | flags::FIN, 65535, &[]); + let fin_len = fin_data.len(); + handler.process_ipv4(Frame::new(2, leak(fin_data), fin_len, false), &nh, &mut free, &mut rx, &mut tx); + + // Should transition to CloseWait, send ACK, advance rcv_nxt. + assert_eq!(handler.connections[0].state, TcpState::CloseWait); + assert_eq!(handler.connections[0].rcv_nxt, 1002); // 1001 + FIN=1 + assert_eq!(tx.num_frames(), 1, "ACK for FIN sent"); +} + +#[test] +fn established_receives_fin_with_data() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1000, 0, flags::SYN, 65535, &[]); + let syn_len = syn_data.len(); + handler.process_ipv4(Frame::new(0, leak(syn_data), syn_len, false), &nh, &mut free, &mut rx, &mut tx); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1001, server_iss.wrapping_add(1), flags::ACK, 65535, &[]); + let ack_len = ack_data.len(); + handler.process_ipv4(Frame::new(1, leak(ack_data), ack_len, false), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + + // Remote sends data + FIN piggybacked. + let payload = b"goodbye"; + let fin_data = build_tcp_frame_with_payload( + REMOTE_IP, LOCAL_IP, 12345, 80, + 1001, server_iss.wrapping_add(1), + flags::ACK | flags::FIN, 65535, &[], payload, + ); + let fin_len = fin_data.len(); + handler.process_ipv4(Frame::new(2, leak(fin_data), fin_len, false), &nh, &mut free, &mut rx, &mut tx); + + // Data should be in recv buffer, state should be CloseWait. + assert_eq!(handler.connections[0].state, TcpState::CloseWait); + assert_eq!(handler.connections[0].recv_buffer.available(), payload.len()); + // rcv_nxt = 1001 + 7 bytes data + 1 FIN = 1009 + assert_eq!(handler.connections[0].rcv_nxt, 1001 + payload.len() as u32 + 1); +} +``` + +**Step 2: Run test to verify it fails** + +Run: `cargo test established_receives_fin` +Expected: FAIL — current `process_established` doesn't handle FIN flag. + +**Step 3: Extend process_established for FIN** + +In `process_established` (~line 830), after the data processing section (the `if payload_len > 0` block), add FIN handling. The FIN must be processed after data because FIN can piggyback on data: + +```rust +// Step 4: Process FIN flag. +if seg_flags & flags::FIN != 0 { + let tcb = &mut self.connections[idx]; + // Only process FIN if it's at the expected sequence number. + // After data processing, rcv_nxt should be at seg_seq + payload_len. + // FIN occupies the next sequence number after the data. + tcb.rcv_nxt = tcb.rcv_nxt.wrapping_add(1); // FIN consumes one sequence number + tcb.state = TcpState::CloseWait; + tcb.event_queue.push(TcpEvent::RemoteClose); + + // Send ACK for FIN. + let id = tcb.id; + let snd_nxt = tcb.snd_nxt; + let new_rcv_nxt = tcb.rcv_nxt; + let window = tcb.recv_buffer.free_space().min(u16::MAX as usize) as u16; + SegmentBuilder::build_ack( + id.local_addr, id.remote_addr, + id.local_port, id.remote_port, + snd_nxt, new_rcv_nxt, window, + src_mac, dst_mac, + self.tx_offload, free_frames, tx_return, + ); +} +``` + +**Step 4: Run tests to verify they pass** + +Run: `cargo test tcp` +Expected: PASS + +**Step 5: Commit** + +```bash +git add src/net/handler/tcp/mod.rs +git commit -m "feat(tcp): handle FIN in Established state → CloseWait transition" +``` + +--- + +### Task 5: process_teardown — FinWait1 and FinWait2 + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs` + +**Step 1: Write the failing tests** + +```rust +#[test] +fn active_close_fin_wait1_to_fin_wait2() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1000, 0, flags::SYN, 65535, &[]); + let syn_len = syn_data.len(); + handler.process_ipv4(Frame::new(0, leak(syn_data), syn_len, false), &nh, &mut free, &mut rx, &mut tx); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1001, server_iss.wrapping_add(1), flags::ACK, 65535, &[]); + let ack_len = ack_data.len(); + handler.process_ipv4(Frame::new(1, leak(ack_data), ack_len, false), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + + // Active close: set pending_fin, poll_send sends FIN → FinWait1. + handler.connections[0].pending_fin = true; + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + while tx.pop().is_some() {} + assert_eq!(handler.connections[0].state, TcpState::FinWait1); + let fin_seq = handler.connections[0].fin_seq.unwrap(); + + // Remote ACKs our FIN → FinWait2. + let ack = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1001, fin_seq.wrapping_add(1), flags::ACK, 65535, &[]); + let ack_len = ack.len(); + handler.process_ipv4(Frame::new(3, leak(ack), ack_len, false), &nh, &mut free, &mut rx, &mut tx); + + assert_eq!(handler.connections[0].state, TcpState::FinWait2); +} + +#[test] +fn fin_wait2_receives_fin_to_time_wait() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1000, 0, flags::SYN, 65535, &[]); + let syn_len = syn_data.len(); + handler.process_ipv4(Frame::new(0, leak(syn_data), syn_len, false), &nh, &mut free, &mut rx, &mut tx); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1001, server_iss.wrapping_add(1), flags::ACK, 65535, &[]); + let ack_len = ack_data.len(); + handler.process_ipv4(Frame::new(1, leak(ack_data), ack_len, false), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + + // Active close → FinWait1 → FinWait2. + handler.connections[0].pending_fin = true; + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + while tx.pop().is_some() {} + let fin_seq = handler.connections[0].fin_seq.unwrap(); + let ack = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1001, fin_seq.wrapping_add(1), flags::ACK, 65535, &[]); + let ack_len = ack.len(); + handler.process_ipv4(Frame::new(3, leak(ack), ack_len, false), &nh, &mut free, &mut rx, &mut tx); + assert_eq!(handler.connections[0].state, TcpState::FinWait2); + while tx.pop().is_some() {} + + // Remote sends FIN → TimeWait. + let fin = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1001, fin_seq.wrapping_add(1), flags::ACK | flags::FIN, 65535, &[]); + let fin_len = fin.len(); + handler.process_ipv4(Frame::new(4, leak(fin), fin_len, false), &nh, &mut free, &mut rx, &mut tx); + + assert_eq!(handler.connections[0].state, TcpState::TimeWait); + assert!(handler.connections[0].time_wait_deadline.is_some()); + assert_eq!(tx.num_frames(), 1, "ACK for remote FIN"); +} +``` + +**Step 2: Run tests to verify they fail** + +Run: `cargo test active_close_fin_wait1` +Expected: FAIL — teardown states currently hit `_ => { rx_return.push(frame); }` in `process_segment`. + +**Step 3: Implement process_teardown and wire it into process_segment** + +In `process_segment` (~line 451), replace `_ => { rx_return.push(frame); }` with: + +```rust +TcpState::FinWait1 | TcpState::FinWait2 | TcpState::CloseWait +| TcpState::Closing | TcpState::LastAck | TcpState::TimeWait => { + let payload_offset = tcp_offset + tcp_header_len; + let payload_len = frame.len().saturating_sub(payload_offset); + self.process_teardown( + idx, frame, now, seg_seq, seg_ack, seg_flags, seg_wnd, + payload_offset, payload_len, + src_mac, dst_mac, + free_frames, rx_return, tx_return, + ); +} +_ => { + rx_return.push(frame); +} +``` + +Then add the `process_teardown` method. For this task, implement FinWait1 and FinWait2: + +```rust +fn process_teardown<'umem>( + &mut self, + idx: usize, + frame: Frame<'umem>, + now: Instant, + seg_seq: u32, + seg_ack: u32, + seg_flags: u8, + seg_wnd: u32, + payload_offset: usize, + payload_len: usize, + src_mac: crate::net::wire::ethernet::MacAddress, + dst_mac: crate::net::wire::ethernet::MacAddress, + free_frames: &mut impl FrameBuffer<'umem>, + rx_return: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, +) { + let state = self.connections[idx].state; + + // RST check — abort all states except TimeWait. + if seg_flags & flags::RST != 0 { + if state == TcpState::TimeWait { + // Ignore RST in TIME-WAIT (prevents RST attacks). + rx_return.push(frame); + return; + } + self.connections[idx].event_queue.push(TcpEvent::Reset); + self.connections.remove(idx); + rx_return.push(frame); + return; + } + + match state { + TcpState::FinWait1 => { + let tcb = &mut self.connections[idx]; + let fin_acked = if seg_flags & flags::ACK != 0 { + if let Some(fin_seq) = tcb.fin_seq { + crate::net::wire::tcp::seq_lt(fin_seq, seg_ack) + } else { + false + } + } else { + false + }; + + // Process ACK (advance snd_una if valid). + if seg_flags & flags::ACK != 0 { + let snd_una = tcb.snd_una; + let snd_nxt = tcb.snd_nxt; + if crate::net::wire::tcp::seq_lt(snd_una, seg_ack) + && crate::net::wire::tcp::seq_le(seg_ack, snd_nxt) + { + let bytes_acked = seg_ack.wrapping_sub(snd_una) as usize; + tcb.snd_una = seg_ack; + tcb.send_buffer.advance(bytes_acked); + tcb.snd_wnd = seg_wnd; + } + } + + // Process data if present (remote may still be sending). + if payload_len > 0 && seg_seq == tcb.rcv_nxt { + let payload = &frame[payload_offset..payload_offset + payload_len]; + tcb.recv_buffer.write(payload); + tcb.rcv_nxt = tcb.rcv_nxt.wrapping_add(payload_len as u32); + } + + // Check for FIN from remote. + let remote_fin = seg_flags & flags::FIN != 0; + if remote_fin { + tcb.rcv_nxt = tcb.rcv_nxt.wrapping_add(1); + } + + // Determine new state. + let tcb = &mut self.connections[idx]; + if fin_acked && remote_fin { + // Both sides FINed and our FIN is ACKed → TimeWait. + tcb.state = TcpState::TimeWait; + tcb.time_wait_deadline = Some(now + coarsetime::Duration::from_millis(tcb.time_wait_duration)); + tcb.retransmit_deadline = None; + } else if fin_acked { + // Our FIN ACKed but no remote FIN yet → FinWait2. + tcb.state = TcpState::FinWait2; + tcb.retransmit_deadline = None; + } else if remote_fin { + // Remote FINed but our FIN not yet ACKed → Closing. + tcb.state = TcpState::Closing; + } + + // Send ACK if FIN or data received. + if remote_fin || payload_len > 0 { + let id = self.connections[idx].id; + let snd_nxt = self.connections[idx].snd_nxt; + let rcv_nxt = self.connections[idx].rcv_nxt; + let window = self.connections[idx].recv_buffer.free_space().min(u16::MAX as usize) as u16; + SegmentBuilder::build_ack( + id.local_addr, id.remote_addr, + id.local_port, id.remote_port, + snd_nxt, rcv_nxt, window, + src_mac, dst_mac, + self.tx_offload, free_frames, tx_return, + ); + } + + rx_return.push(frame); + } + + TcpState::FinWait2 => { + let tcb = &mut self.connections[idx]; + + // Process data if present (remote still sending). + if payload_len > 0 && seg_seq == tcb.rcv_nxt { + let payload = &frame[payload_offset..payload_offset + payload_len]; + tcb.recv_buffer.write(payload); + tcb.rcv_nxt = tcb.rcv_nxt.wrapping_add(payload_len as u32); + } + + // Check for FIN. + if seg_flags & flags::FIN != 0 { + tcb.rcv_nxt = tcb.rcv_nxt.wrapping_add(1); + tcb.state = TcpState::TimeWait; + tcb.time_wait_deadline = Some(now + coarsetime::Duration::from_millis(tcb.time_wait_duration)); + } + + // Send ACK if FIN or data. + if seg_flags & flags::FIN != 0 || payload_len > 0 { + let id = tcb.id; + let snd_nxt = tcb.snd_nxt; + let rcv_nxt = tcb.rcv_nxt; + let window = tcb.recv_buffer.free_space().min(u16::MAX as usize) as u16; + SegmentBuilder::build_ack( + id.local_addr, id.remote_addr, + id.local_port, id.remote_port, + snd_nxt, rcv_nxt, window, + src_mac, dst_mac, + self.tx_offload, free_frames, tx_return, + ); + } + + rx_return.push(frame); + } + + // Remaining states added in next tasks. + _ => { + rx_return.push(frame); + } + } +} +``` + +**Step 4: Run tests to verify they pass** + +Run: `cargo test tcp` +Expected: PASS + +**Step 5: Commit** + +```bash +git add src/net/handler/tcp/mod.rs +git commit -m "feat(tcp): process_teardown for FinWait1 and FinWait2 states" +``` + +--- + +### Task 6: process_teardown — Closing, LastAck, TimeWait + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs` + +**Step 1: Write the failing tests** + +```rust +#[test] +fn simultaneous_close_closing_to_time_wait() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1000, 0, flags::SYN, 65535, &[]); + let syn_len = syn_data.len(); + handler.process_ipv4(Frame::new(0, leak(syn_data), syn_len, false), &nh, &mut free, &mut rx, &mut tx); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1001, server_iss.wrapping_add(1), flags::ACK, 65535, &[]); + let ack_len = ack_data.len(); + handler.process_ipv4(Frame::new(1, leak(ack_data), ack_len, false), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + + // Active close → FinWait1. + handler.connections[0].pending_fin = true; + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + while tx.pop().is_some() {} + assert_eq!(handler.connections[0].state, TcpState::FinWait1); + + // Simultaneous close: remote sends FIN without ACKing ours → Closing. + let fin = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1001, server_iss.wrapping_add(1), flags::ACK | flags::FIN, 65535, &[]); + let fin_len = fin.len(); + handler.process_ipv4(Frame::new(3, leak(fin), fin_len, false), &nh, &mut free, &mut rx, &mut tx); + assert_eq!(handler.connections[0].state, TcpState::Closing); + while tx.pop().is_some() {} + + // Remote ACKs our FIN → TimeWait. + let fin_seq = handler.connections[0].fin_seq.unwrap(); + let ack = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1002, fin_seq.wrapping_add(1), flags::ACK, 65535, &[]); + let ack_len = ack.len(); + handler.process_ipv4(Frame::new(4, leak(ack), ack_len, false), &nh, &mut free, &mut rx, &mut tx); + assert_eq!(handler.connections[0].state, TcpState::TimeWait); +} + +#[test] +fn passive_close_last_ack_removes_connection() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1000, 0, flags::SYN, 65535, &[]); + let syn_len = syn_data.len(); + handler.process_ipv4(Frame::new(0, leak(syn_data), syn_len, false), &nh, &mut free, &mut rx, &mut tx); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1001, server_iss.wrapping_add(1), flags::ACK, 65535, &[]); + let ack_len = ack_data.len(); + handler.process_ipv4(Frame::new(1, leak(ack_data), ack_len, false), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + + // Remote sends FIN → CloseWait. + let fin = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1001, server_iss.wrapping_add(1), flags::ACK | flags::FIN, 65535, &[]); + let fin_len = fin.len(); + handler.process_ipv4(Frame::new(2, leak(fin), fin_len, false), &nh, &mut free, &mut rx, &mut tx); + assert_eq!(handler.connections[0].state, TcpState::CloseWait); + while tx.pop().is_some() {} + + // We close → pending_fin, poll_send sends FIN → LastAck. + handler.connections[0].pending_fin = true; + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + while tx.pop().is_some() {} + assert_eq!(handler.connections[0].state, TcpState::LastAck); + let fin_seq = handler.connections[0].fin_seq.unwrap(); + + // Remote ACKs our FIN → connection removed. + let ack = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1002, fin_seq.wrapping_add(1), flags::ACK, 65535, &[]); + let ack_len = ack.len(); + handler.process_ipv4(Frame::new(4, leak(ack), ack_len, false), &nh, &mut free, &mut rx, &mut tx); + assert_eq!(handler.connections.len(), 0, "connection removed after LastAck"); +} + +#[test] +fn time_wait_ignores_rst() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1000, 0, flags::SYN, 65535, &[]); + let syn_len = syn_data.len(); + handler.process_ipv4(Frame::new(0, leak(syn_data), syn_len, false), &nh, &mut free, &mut rx, &mut tx); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1001, server_iss.wrapping_add(1), flags::ACK, 65535, &[]); + let ack_len = ack_data.len(); + handler.process_ipv4(Frame::new(1, leak(ack_data), ack_len, false), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + + // Full active close → TimeWait. + handler.connections[0].pending_fin = true; + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + while tx.pop().is_some() {} + let fin_seq = handler.connections[0].fin_seq.unwrap(); + let ack = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1001, fin_seq.wrapping_add(1), flags::ACK, 65535, &[]); + let ack_len = ack.len(); + handler.process_ipv4(Frame::new(3, leak(ack), ack_len, false), &nh, &mut free, &mut rx, &mut tx); + let fin = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1001, fin_seq.wrapping_add(1), flags::ACK | flags::FIN, 65535, &[]); + let fin_len = fin.len(); + handler.process_ipv4(Frame::new(4, leak(fin), fin_len, false), &nh, &mut free, &mut rx, &mut tx); + assert_eq!(handler.connections[0].state, TcpState::TimeWait); + while tx.pop().is_some() {} + + // RST in TIME-WAIT should be ignored. + let rst = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1002, 0, flags::RST, 0, &[]); + let rst_len = rst.len(); + handler.process_ipv4(Frame::new(5, leak(rst), rst_len, false), &nh, &mut free, &mut rx, &mut tx); + assert_eq!(handler.connections.len(), 1, "connection NOT removed by RST in TIME-WAIT"); + assert_eq!(handler.connections[0].state, TcpState::TimeWait); +} +``` + +**Step 2: Run tests to verify they fail** + +Run: `cargo test simultaneous_close` +Expected: FAIL — Closing, LastAck, TimeWait not yet handled in `process_teardown`. + +**Step 3: Add remaining states to process_teardown** + +Add these match arms to the existing `process_teardown` method: + +```rust +TcpState::Closing => { + let tcb = &mut self.connections[idx]; + // Waiting for ACK of our FIN. + if seg_flags & flags::ACK != 0 { + if let Some(fin_seq) = tcb.fin_seq { + if crate::net::wire::tcp::seq_lt(fin_seq, seg_ack) { + tcb.snd_una = seg_ack; + tcb.state = TcpState::TimeWait; + tcb.time_wait_deadline = Some(now + coarsetime::Duration::from_millis(tcb.time_wait_duration)); + tcb.retransmit_deadline = None; + } + } + } + rx_return.push(frame); +} + +TcpState::LastAck => { + // Waiting for ACK of our FIN. + if seg_flags & flags::ACK != 0 { + let tcb = &self.connections[idx]; + if let Some(fin_seq) = tcb.fin_seq { + if crate::net::wire::tcp::seq_lt(fin_seq, seg_ack) { + self.connections.remove(idx); + rx_return.push(frame); + return; + } + } + } + rx_return.push(frame); +} + +TcpState::TimeWait => { + let tcb = &mut self.connections[idx]; + // FIN retransmit → re-ACK and restart timer. + if seg_flags & flags::FIN != 0 { + let id = tcb.id; + let snd_nxt = tcb.snd_nxt; + let rcv_nxt = tcb.rcv_nxt; + let window = tcb.recv_buffer.free_space().min(u16::MAX as usize) as u16; + SegmentBuilder::build_ack( + id.local_addr, id.remote_addr, + id.local_port, id.remote_port, + snd_nxt, rcv_nxt, window, + src_mac, dst_mac, + self.tx_offload, free_frames, tx_return, + ); + tcb.time_wait_deadline = Some(now + coarsetime::Duration::from_millis(tcb.time_wait_duration)); + } + // Everything else (including RST) is ignored — RST handled above. + rx_return.push(frame); +} + +TcpState::CloseWait => { + // Remote already FINed. No new data expected. + // Just handle RST (already handled above) and ignore everything else. + rx_return.push(frame); +} +``` + +**Step 4: Run tests to verify they pass** + +Run: `cargo test tcp` +Expected: PASS + +**Step 5: Commit** + +```bash +git add src/net/handler/tcp/mod.rs +git commit -m "feat(tcp): process_teardown for Closing, LastAck, TimeWait, CloseWait" +``` + +--- + +### Task 7: evict_stale — TIME-WAIT Cleanup + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs` + +**Step 1: Write the failing test** + +```rust +#[test] +fn time_wait_evicted_after_deadline() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1000, 0, flags::SYN, 65535, &[]); + let syn_len = syn_data.len(); + handler.process_ipv4(Frame::new(0, leak(syn_data), syn_len, false), &nh, &mut free, &mut rx, &mut tx); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1001, server_iss.wrapping_add(1), flags::ACK, 65535, &[]); + let ack_len = ack_data.len(); + handler.process_ipv4(Frame::new(1, leak(ack_data), ack_len, false), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + + // Force into TimeWait state with expired deadline. + handler.connections[0].state = TcpState::TimeWait; + handler.connections[0].time_wait_deadline = Some(coarsetime::Instant::now()); + + // Evict with a time in the future. + let future = coarsetime::Instant::now() + coarsetime::Duration::from_secs(120); + handler.evict_stale(future, &mut rx); + assert_eq!(handler.connections.len(), 0, "TIME-WAIT connection evicted"); +} +``` + +**Step 2: Run test to verify it fails** + +Run: `cargo test time_wait_evicted` +Expected: FAIL — `evict_stale` is currently a no-op. + +**Step 3: Implement evict_stale** + +Replace the placeholder `evict_stale` (~line 1133): + +```rust +pub fn evict_stale<'umem>( + &mut self, + now: Instant, + _rx_return: &mut impl FrameBuffer<'umem>, +) { + self.connections.retain(|tcb| { + if tcb.state == TcpState::TimeWait { + if let Some(deadline) = tcb.time_wait_deadline { + if now >= deadline { + return false; // remove + } + } + } + true // keep + }); +} +``` + +**Step 4: Run tests to verify they pass** + +Run: `cargo test tcp` +Expected: PASS + +**Step 5: Commit** + +```bash +git add src/net/handler/tcp/mod.rs +git commit -m "feat(tcp): evict_stale removes expired TIME-WAIT connections" +``` + +--- + +### Task 8: TcpStream::close() Rewrite + read() EOF + +**Files:** +- Modify: `src/net/socket/tcp.rs` +- Modify: `src/net/handler/tcp/mod.rs` (add `initiate_close` method) + +**Step 1: Add `initiate_close` to TcpHandler** + +In `src/net/handler/tcp/mod.rs`, add a method to `impl TcpHandler`: + +```rust +/// Initiate graceful close by setting pending_fin on the connection. +/// Called by TcpStream::close(). The actual FIN is sent by poll_send. +pub fn initiate_close(&mut self, id: &ConnectionId) { + if let Some(tcb) = self.connections.iter_mut().find(|c| c.id == *id) { + match tcb.state { + TcpState::Established | TcpState::CloseWait => { + tcb.pending_fin = true; + } + _ => {} // Already closing or closed. + } + } +} +``` + +**Step 2: Rewrite TcpStream::close()** + +In `src/net/socket/tcp.rs`, replace the current `close()` method (~line 339): + +```rust +/// Initiate graceful close of this connection. +/// +/// Sets a flag on the TCB; the runtime's `poll_send` will drain +/// any remaining send buffer data and then send FIN on the next tick. +pub fn close(&mut self) { + if self.closed { + return; + } + self.closed = true; + let handler = unsafe { &mut *self.handler.get() }; + handler.initiate_close(&self.conn_id); +} +``` + +**Step 3: Update TcpRead to return EOF** + +Modify the `TcpRead` future's `poll` method (~line 431): + +```rust +impl<'stream> Future for TcpRead<'stream> { + type Output = usize; + + fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll { + let this = self.get_mut(); + let handler = unsafe { &mut *this.handler.get() }; + if let Some(tcb) = handler.get_connection_mut(&this.conn_id) { + let n = tcb.recv_buffer.read(this.buf); + if n > 0 { + Poll::Ready(n) + } else if tcb.state.is_remote_closed() { + Poll::Ready(0) // EOF — remote has sent FIN and buffer is drained + } else { + Poll::Pending + } + } else { + Poll::Ready(0) // connection gone + } + } +} +``` + +This requires importing `TcpState` or using the `is_remote_closed()` method through the `state` field. The `is_remote_closed()` method was added to `TcpState` in Task 1. + +**Step 4: Remove old `remove_connection` usage** + +Remove the unused imports in `tcp.rs` that were only needed for the old RST hack: `SharedFrameBuffer`, `BasicFrameBuffer`. Keep the `remove_connection` method on TcpHandler for now (it may be useful for abort scenarios), but `TcpStream::close()` no longer calls it. + +Clean up any unused imports. + +**Step 5: Run tests to verify nothing broke** + +Run: `cargo test` +Expected: PASS + +**Step 6: Commit** + +```bash +git add src/net/socket/tcp.rs src/net/handler/tcp/mod.rs +git commit -m "feat(tcp): TcpStream::close() sets pending_fin, read() returns EOF" +``` + +--- + +### Task 9: Full Teardown Integration Test + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs` (test only) + +**Step 1: Write comprehensive teardown test** + +```rust +#[test] +fn full_active_close_lifecycle() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + let initial_free = free.num_frames(); + + // 1. Handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1000, 0, flags::SYN, 65535, &[]); + let syn_len = syn_data.len(); + handler.process_ipv4(Frame::new(0, leak(syn_data), syn_len, false), &nh, &mut free, &mut rx, &mut tx); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1001, server_iss.wrapping_add(1), flags::ACK, 65535, &[]); + let ack_len = ack_data.len(); + handler.process_ipv4(Frame::new(1, leak(ack_data), ack_len, false), &nh, &mut free, &mut rx, &mut tx); + assert_eq!(handler.connections[0].state, TcpState::Established); + + // 2. Data exchange. + let payload = b"hello"; + let data = build_tcp_frame_with_payload( + REMOTE_IP, LOCAL_IP, 12345, 80, + 1001, server_iss.wrapping_add(1), + flags::ACK, 65535, &[], payload, + ); + let data_len = data.len(); + handler.process_ipv4(Frame::new(2, leak(data), data_len, false), &nh, &mut free, &mut rx, &mut tx); + + // 3. Active close. + handler.connections[0].pending_fin = true; + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + assert_eq!(handler.connections[0].state, TcpState::FinWait1); + while tx.pop().is_some() {} + + // 4. Remote ACKs our FIN → FinWait2. + let fin_seq = handler.connections[0].fin_seq.unwrap(); + let ack = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1006, fin_seq.wrapping_add(1), flags::ACK, 65535, &[]); + let ack_len = ack.len(); + handler.process_ipv4(Frame::new(3, leak(ack), ack_len, false), &nh, &mut free, &mut rx, &mut tx); + assert_eq!(handler.connections[0].state, TcpState::FinWait2); + + // 5. Remote sends FIN → TimeWait. + let fin = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1006, fin_seq.wrapping_add(1), flags::ACK | flags::FIN, 65535, &[]); + let fin_len = fin.len(); + handler.process_ipv4(Frame::new(4, leak(fin), fin_len, false), &nh, &mut free, &mut rx, &mut tx); + assert_eq!(handler.connections[0].state, TcpState::TimeWait); + while tx.pop().is_some() {} + + // 6. TIME-WAIT expires → connection removed. + let future = coarsetime::Instant::now() + coarsetime::Duration::from_secs(120); + handler.evict_stale(future, &mut rx); + assert_eq!(handler.connections.len(), 0, "connection removed after TIME-WAIT"); + + // 7. Frame accounting: all frames accounted for. + let total = free.num_frames() + rx.num_frames() + tx.num_frames(); + assert_eq!(total, initial_free + 5, "all frames accounted for (initial + 5 incoming)"); +} + +#[test] +fn full_passive_close_lifecycle() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // 1. Handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1000, 0, flags::SYN, 65535, &[]); + let syn_len = syn_data.len(); + handler.process_ipv4(Frame::new(0, leak(syn_data), syn_len, false), &nh, &mut free, &mut rx, &mut tx); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1001, server_iss.wrapping_add(1), flags::ACK, 65535, &[]); + let ack_len = ack_data.len(); + handler.process_ipv4(Frame::new(1, leak(ack_data), ack_len, false), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + + // 2. Remote sends FIN → CloseWait. + let fin = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1001, server_iss.wrapping_add(1), flags::ACK | flags::FIN, 65535, &[]); + let fin_len = fin.len(); + handler.process_ipv4(Frame::new(2, leak(fin), fin_len, false), &nh, &mut free, &mut rx, &mut tx); + assert_eq!(handler.connections[0].state, TcpState::CloseWait); + while tx.pop().is_some() {} + + // 3. We close → LastAck. + handler.connections[0].pending_fin = true; + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + assert_eq!(handler.connections[0].state, TcpState::LastAck); + while tx.pop().is_some() {} + let fin_seq = handler.connections[0].fin_seq.unwrap(); + + // 4. Remote ACKs our FIN → connection removed. + let ack = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1002, fin_seq.wrapping_add(1), flags::ACK, 65535, &[]); + let ack_len = ack.len(); + handler.process_ipv4(Frame::new(3, leak(ack), ack_len, false), &nh, &mut free, &mut rx, &mut tx); + assert_eq!(handler.connections.len(), 0, "connection removed after LastAck"); +} +``` + +**Step 2: Run tests** + +Run: `cargo test full_active_close_lifecycle full_passive_close_lifecycle` +Expected: PASS + +**Step 3: Commit** + +```bash +git add src/net/handler/tcp/mod.rs +git commit -m "test(tcp): full active and passive close lifecycle integration tests" +``` + +--- + +## Task Dependency Graph + +``` +Task 1 (TCB fields + TcpEvent::RemoteClose) + ├─> Task 2 (SegmentBuilder::build_fin_ack) + │ └─> Task 3 (poll_send FIN sending) + │ └─> Task 5 (process_teardown FinWait1/FinWait2) + │ └─> Task 6 (process_teardown Closing/LastAck/TimeWait) + │ └─> Task 7 (evict_stale TIME-WAIT) + │ └─> Task 9 (integration tests) + └─> Task 4 (FIN in Established → CloseWait) + └─> Task 8 (TcpStream::close() + read() EOF) + └─> Task 9 (integration tests) +``` + +Tasks 2 and 4 can be parallelized after Task 1. Tasks 5 and 8 can be parallelized after their respective dependencies. Task 9 depends on everything. diff --git a/docs/plans/2026-03-06-tcp-data-transfer-design.md b/docs/plans/2026-03-06-tcp-data-transfer-design.md new file mode 100644 index 0000000..0cfa4a5 --- /dev/null +++ b/docs/plans/2026-03-06-tcp-data-transfer-design.md @@ -0,0 +1,208 @@ +# TCP Data Transfer Design + +## Context + +VoidNet is a zero-copy AF_XDP networking library. TCP handshake (3-way SYN/SYN-ACK/ACK), RST handling, ISN generation, and socket abstractions (TcpListener, TcpStream) are already implemented. This design covers the data transfer phase for established TCP connections. + +Target use case: high-performance HTTP/2 servers. The implementation must minimize copies, avoid hot-path allocations, and never hold `Frame<'umem>` references across ticks. + +## Frame Safety Invariant + +Every `Frame<'umem>` popped from `free_frames` MUST be pushed to either `tx_return` or `rx_return` — never dropped. Frames from incoming packets go to `rx_return`. Frames written for transmission go to `tx_return`. If any operation fails partway, the frame must be returned to its source. No `Frame<'umem>` is ever stored in TCP data structures across ticks — ring buffers hold copied bytes, not frames. + +## Ring Buffer + +A single ring buffer structure used for both send and receive sides. + +### Structure + +- Pre-allocated `Vec` sized at connection creation, never resized. +- `head: usize` — read position (oldest unread/unACKed byte). +- `tail: usize` — write position (next byte to write). +- `len: usize` — bytes currently in the buffer. +- Capacity is always a power of two. Index wrapping uses bitwise AND (`pos & (capacity - 1)`) instead of modulo. + +### Operations (all O(1), no allocation) + +- `write_at(offset, &[u8])` — write at an arbitrary offset from `head`. Used by receive side for out-of-order segments. Wraps around the ring with at most two memcpy calls. +- `write(&[u8])` — append bytes at `tail`. Used by send side when user writes data. +- `read(&mut [u8]) -> usize` — read contiguous bytes from `head`. Used by `TcpStream::read()` and send side when building segments. +- `peek_at(offset, &mut [u8])` — read without advancing `head`. Used by send side for retransmission. +- `advance(n)` — move `head` forward. Used when bytes are ACKed (send) or consumed (receive). +- `available() -> usize` — bytes available to read (`len`). +- `free_space() -> usize` — bytes available to write (`capacity - len`). + +### Default Sizes + +- Send buffer: 256KB +- Receive buffer: 256KB +- Configurable per-connection via `TcpConfig` at listen/connect time. + +## Send Path + +### Copy Budget + +Two copies per segment, both `memcpy` of at most MSS bytes into pre-allocated memory: +1. User data into send ring buffer. +2. Ring buffer into `Frame<'umem>` for kernel handoff. + +### Flow + +1. User calls `TcpStream::write(data)` — returns a future. Copies data into the send ring buffer. Returns `Poll::Pending` if the buffer is full (backpressure). + +2. `TcpHandler::poll_send(now)` — called each runtime tick. For each established connection with data in the send ring buffer: + - Compute effective send window: `min(snd_wnd, cwnd) - bytes_in_flight`. + - If window allows, read up to `eff_snd_mss` bytes from the ring buffer. + - Build segment via `SegmentBuilder::build_data()`. + - Set sequence number to `snd_nxt`, advance `snd_nxt` by payload length. + - Push frame to `tx_return`. + - Record segment sequence range and send timestamp (using the `now` parameter) for RTO tracking. + +3. Retransmission (RTO fires or 3 duplicate ACKs): + - Re-read from oldest unACKed position in the ring buffer via `peek_at`. + - Build segment into a fresh `Frame<'umem>` from `free_frames`. + - On RTO: `ssthresh = cwnd / 2`, `cwnd = eff_snd_mss`. + - On fast retransmit: `ssthresh = cwnd / 2`, `cwnd = ssthresh`. + +4. ACK processing (valid ACK advances `snd_una`): + - Advance `head` in the send ring buffer (frees space for new writes). + - Update RTT estimate (RFC 6298) using the `now` parameter if this ACK covers a timed segment. + - Slow start: if `cwnd < ssthresh`, `cwnd += eff_snd_mss`. + - Congestion avoidance: if `cwnd >= ssthresh`, `cwnd += eff_snd_mss * eff_snd_mss / cwnd`. + - Reset `dup_ack_count`. + +## Receive Path + +### Copy Budget + +One copy per segment: frame payload into receive ring buffer. Frame returned to `rx_return` immediately. + +### Flow + +1. `TcpHandler::process_established()` — on receiving a data segment: + - Sequence check: is `seg_seq` within the receive window (`rcv_nxt` to `rcv_nxt + rcv_wnd`)? + - Outside window: send duplicate ACK, return frame to `rx_return`. + - In-order (`seg_seq == rcv_nxt`): + - Copy payload from frame into receive ring buffer. + - Return frame to `rx_return` immediately. + - Advance `rcv_nxt` by payload length. + - Check out-of-order metadata for now-contiguous ranges, advance `rcv_nxt` for each. + - Send ACK. + - Out-of-order (`seg_seq > rcv_nxt`): + - Copy payload into receive ring buffer at correct offset (`seg_seq - rcv_nxt` ahead of write cursor) via `write_at`. + - Return frame to `rx_return` immediately. + - Record range in `BTreeMap` (seq -> length) — metadata only, no frames held. + - Send duplicate ACK. + - Duplicate (`seg_seq < rcv_nxt`): + - Return frame to `rx_return`, send ACK. + +2. `TcpStream::read(&mut [u8])` — read contiguous bytes from the receive ring buffer. Returns a future, `Poll::Pending` when no data available. + +### Out-of-Order Tracking + +`BTreeMap` maps sequence number to byte length. Stores metadata only — the actual bytes are already in the ring buffer via `write_at`. When an in-order segment arrives and fills a gap, contiguous ranges are merged and `rcv_nxt` is advanced. This structure is bounded by the receive window size. + +## TCB Additions + +Per-connection state added to `Tcb`: + +``` +// Send ring buffer +send_buffer: RingBuffer, + +// Receive ring buffer +recv_buffer: RingBuffer, + +// Out-of-order receive tracking (metadata only) +ooo_ranges: BTreeMap, + +// Congestion control +cwnd: u32, // Initialized to 10 * eff_snd_mss +ssthresh: u32, // Initialized to u32::MAX +dup_ack_count: u8, // Fast retransmit at 3 + +// RTT estimation (RFC 6298) +srtt: Option, // Smoothed RTT in microseconds +rttvar: u64, // RTT variance +rto: u64, // Retransmission timeout in milliseconds +``` + +## Runtime Integration + +Per-tick additions to the `LocalRuntime` loop: + +1. `tcp_handler.process_established()` — already dispatched via `process_ipv4`/`process_ipv6`. Established branch filled in with receive logic. +2. `tcp_handler.poll_send(now)` — new. Builds and sends data segments from send ring buffers. Called after receive processing so fresh ACKs update windows before sending. +3. `tcp_handler.poll_timers(now)` — already exists. Extended to handle data retransmission RTO and fast retransmit. + +All timestamp operations use the `now: Instant` parameter passed from the single `coarsetime::Instant::now()` call at the top of the runtime tick. No clock reads in handlers or socket code. + +### Backpressure + +- User write stalls when send ring buffer is full (`TcpStream::write()` returns `Pending`). +- Send window closed: segments stop being built, send ring buffer fills, user stalls. +- Receive ring buffer full: `rcv_wnd` advertised as 0, sender stops (TCP flow control). + +## TcpStream API + +```rust +// Writing +TcpStream::write(&[u8]) -> Write<'_> // Pending if send buffer full + +// Reading +TcpStream::read(&mut [u8]) -> Read<'_> // Pending if no data available + +// Info (existing) +TcpStream::conn_id() -> &ConnectionId +TcpStream::local_addr() -> IpAddress +TcpStream::local_port() -> u16 +TcpStream::remote_addr() -> IpAddress +TcpStream::remote_port() -> u16 + +// Lifecycle (existing) +TcpStream::close() // Sends RST (proper FIN deferred) +``` + +### Configuration + +```rust +pub struct TcpConfig { + pub send_buffer_size: usize, // Default: 256KB, must be power of two + pub recv_buffer_size: usize, // Default: 256KB, must be power of two + pub backlog: usize, // Default: 128 (listener only) +} + +TcpListener::listen_with_config(addr, port, TcpConfig { .. }) +TcpStream::connect_with_config(local_addr, local_port, remote_addr, remote_port, TcpConfig { .. }) +``` + +## IP Fragmentation + +TCP does not use the `FragmentWriter`/`FragmentReader` infrastructure. TCP segments are sized to `eff_snd_mss` (negotiated during handshake). IPv4 packets are sent with DF (Don't Fragment) set. PMTU discovery is handled at the TCP layer by clamping MSS. + +## Decisions Summary + +| Decision | Choice | +|---|---| +| Send buffer | Pre-allocated ring buffer, power-of-two, default 256KB | +| Receive buffer | Pre-allocated ring buffer, same design | +| Send copies | 2: user to ring buffer, ring buffer to frame | +| Receive copies | 1: frame to ring buffer (frame returned immediately) | +| Out-of-order tracking | `BTreeMap` metadata only, no frames held | +| Retransmission | RTO + fast retransmit on 3 duplicate ACKs | +| Congestion control | Simple cwnd, slow start, halve-on-loss | +| ACKs | Cumulative only, no SACK, no delayed ACK | +| Timestamps | `now: Instant` from runtime tick, no clock reads in handlers | +| Frame safety | Never held by TCP, never dropped, returned within same tick | +| Configuration | Per-connection via `TcpConfig` | +| RTT estimation | RFC 6298 smoothed RTT + RTTVAR | +| API | `write`/`read` futures with backpressure | + +## Deferred + +- SACK (selective acknowledgment) +- Delayed ACKs +- Proper FIN teardown / TIME-WAIT +- Congestion control algorithms (Reno/Cubic/BBR) +- Nagle algorithm +- `split()` into read/write halves diff --git a/docs/plans/2026-03-06-tcp-data-transfer-impl.md b/docs/plans/2026-03-06-tcp-data-transfer-impl.md new file mode 100644 index 0000000..05e88f1 --- /dev/null +++ b/docs/plans/2026-03-06-tcp-data-transfer-impl.md @@ -0,0 +1,1732 @@ +# TCP Data Transfer Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Implement TCP data transfer (send/receive) for established connections with ring buffers, out-of-order reassembly, RTO + fast retransmit, and simple congestion control. + +**Architecture:** Ring buffers for both send and receive paths. Incoming frames are copied into the receive ring buffer and returned to `rx_return` immediately — TCP never holds frames. Send path copies from the ring buffer into fresh frames. Out-of-order segments are tracked via `BTreeMap` metadata. Congestion control uses a simple cwnd with slow start and halve-on-loss. + +**Tech Stack:** Rust (2024 edition), no new dependencies. Uses existing `coarsetime`, `Frame<'umem>`, `FrameBuffer`, `LocalQueue`, `SegmentBuilder` infrastructure. + +**Design doc:** `docs/plans/2026-03-06-tcp-data-transfer-design.md` + +--- + +### Task 1: RingBuffer — Core Data Structure + +**Files:** +- Create: `src/net/handler/tcp/ring_buffer.rs` +- Modify: `src/net/handler/tcp/mod.rs:1` (add `mod ring_buffer;`) + +**Step 1: Write the failing tests** + +In `src/net/handler/tcp/ring_buffer.rs`: + +```rust +pub struct RingBuffer { + buf: Vec, + head: usize, + tail: usize, + len: usize, + mask: usize, +} + +impl RingBuffer { + /// Create a new ring buffer. `capacity` must be a power of two. + pub fn new(capacity: usize) -> Self { + assert!(capacity.is_power_of_two(), "RingBuffer capacity must be a power of two"); + Self { + buf: vec![0u8; capacity], + head: 0, + tail: 0, + len: 0, + mask: capacity - 1, + } + } + + /// Number of bytes available to read. + #[inline] + pub fn available(&self) -> usize { + self.len + } + + /// Number of bytes available to write. + #[inline] + pub fn free_space(&self) -> usize { + self.buf.len() - self.len + } + + /// Total capacity. + #[inline] + pub fn capacity(&self) -> usize { + self.buf.len() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn new_ring_buffer() { + let rb = RingBuffer::new(1024); + assert_eq!(rb.capacity(), 1024); + assert_eq!(rb.available(), 0); + assert_eq!(rb.free_space(), 1024); + } + + #[test] + #[should_panic(expected = "power of two")] + fn non_power_of_two_panics() { + RingBuffer::new(1000); + } +} +``` + +**Step 2: Run tests to verify they pass** + +Run: `cargo test ring_buffer -- --nocapture` +Expected: PASS (both tests) + +**Step 3: Commit** + +```bash +git add src/net/handler/tcp/ring_buffer.rs src/net/handler/tcp/mod.rs +git commit -m "feat(tcp): add RingBuffer skeleton with new/capacity/available/free_space" +``` + +--- + +### Task 2: RingBuffer — Write and Read Operations + +**Files:** +- Modify: `src/net/handler/tcp/ring_buffer.rs` + +**Step 1: Write the failing tests** + +Add to the `tests` module in `ring_buffer.rs`: + +```rust +#[test] +fn write_and_read() { + let mut rb = RingBuffer::new(64); + let data = b"hello world"; + let written = rb.write(data); + assert_eq!(written, data.len()); + assert_eq!(rb.available(), data.len()); + assert_eq!(rb.free_space(), 64 - data.len()); + + let mut buf = [0u8; 64]; + let read = rb.read(&mut buf); + assert_eq!(read, data.len()); + assert_eq!(&buf[..read], data); + assert_eq!(rb.available(), 0); + assert_eq!(rb.free_space(), 64); +} + +#[test] +fn write_wraps_around() { + let mut rb = RingBuffer::new(16); + // Fill 12 bytes, read 12, then write 8 which wraps around the boundary. + rb.write(&[0xAA; 12]); + let mut discard = [0u8; 12]; + rb.read(&mut discard); + // head=12, tail=12. Write 8 bytes: 4 fit at end, 4 wrap to start. + let written = rb.write(&[0xBB; 8]); + assert_eq!(written, 8); + assert_eq!(rb.available(), 8); + + let mut buf = [0u8; 8]; + let read = rb.read(&mut buf); + assert_eq!(read, 8); + assert_eq!(buf, [0xBB; 8]); +} + +#[test] +fn write_when_full_returns_zero() { + let mut rb = RingBuffer::new(16); + let written = rb.write(&[0xFF; 16]); + assert_eq!(written, 16); + let written = rb.write(&[0xAA; 1]); + assert_eq!(written, 0); +} + +#[test] +fn partial_write_when_nearly_full() { + let mut rb = RingBuffer::new(16); + rb.write(&[0xFF; 12]); + let written = rb.write(&[0xAA; 8]); + assert_eq!(written, 4); // only 4 bytes free + assert_eq!(rb.available(), 16); +} +``` + +**Step 2: Run tests to verify they fail** + +Run: `cargo test ring_buffer -- --nocapture` +Expected: FAIL — `write` and `read` methods not defined. + +**Step 3: Implement write and read** + +Add to `impl RingBuffer`: + +```rust +/// Write bytes into the buffer at `tail`. Returns the number of bytes written. +/// Writes as many bytes as fit; returns less than `data.len()` if buffer is nearly full. +#[inline] +pub fn write(&mut self, data: &[u8]) -> usize { + let to_write = data.len().min(self.free_space()); + if to_write == 0 { + return 0; + } + let tail = self.tail & self.mask; + let first = to_write.min(self.buf.len() - tail); + self.buf[tail..tail + first].copy_from_slice(&data[..first]); + if first < to_write { + self.buf[..to_write - first].copy_from_slice(&data[first..to_write]); + } + self.tail = self.tail.wrapping_add(to_write); + self.len += to_write; + to_write +} + +/// Read bytes from the buffer starting at `head`. Returns the number of bytes read. +/// Advances `head` by the number of bytes read. +#[inline] +pub fn read(&mut self, buf: &mut [u8]) -> usize { + let to_read = buf.len().min(self.available()); + if to_read == 0 { + return 0; + } + let head = self.head & self.mask; + let first = to_read.min(self.buf.len() - head); + buf[..first].copy_from_slice(&self.buf[head..head + first]); + if first < to_read { + buf[first..to_read].copy_from_slice(&self.buf[..to_read - first]); + } + self.head = self.head.wrapping_add(to_read); + self.len -= to_read; + to_read +} +``` + +**Step 4: Run tests to verify they pass** + +Run: `cargo test ring_buffer -- --nocapture` +Expected: PASS + +**Step 5: Commit** + +```bash +git add src/net/handler/tcp/ring_buffer.rs +git commit -m "feat(tcp): RingBuffer write and read with wrap-around" +``` + +--- + +### Task 3: RingBuffer — write_at, peek_at, advance + +**Files:** +- Modify: `src/net/handler/tcp/ring_buffer.rs` + +**Step 1: Write the failing tests** + +Add to `tests` module: + +```rust +#[test] +fn write_at_and_read() { + let mut rb = RingBuffer::new(64); + // Write 4 bytes at offset 0 (like in-order). + rb.write_at(0, b"AAAA"); + // Write 4 bytes at offset 8 (out-of-order, gap at 4..8). + rb.write_at(8, b"CCCC"); + // Fill the gap. + rb.write_at(4, b"BBBB"); + // Now advance tail to cover all 12 bytes. + // (write_at for receive side — caller manages len/tail) + assert_eq!(rb.available(), 0); // write_at doesn't advance len +} + +#[test] +fn write_at_wraps() { + let mut rb = RingBuffer::new(16); + // Move head to position 12. + rb.write(&[0xAA; 12]); + let mut discard = [0u8; 12]; + rb.read(&mut discard); + // head=12. write_at offset 2 from head = position 14. Writing 4 bytes wraps. + rb.write_at(2, &[0xBB; 4]); + // Verify by peeking. + let mut buf = [0u8; 4]; + rb.peek_at(2, &mut buf); + assert_eq!(buf, [0xBB; 4]); +} + +#[test] +fn peek_at_does_not_advance() { + let mut rb = RingBuffer::new(64); + rb.write(b"hello"); + let mut buf = [0u8; 5]; + rb.peek_at(0, &mut buf); + assert_eq!(&buf, b"hello"); + assert_eq!(rb.available(), 5); // unchanged + // Peek again at offset 2. + let mut buf2 = [0u8; 3]; + rb.peek_at(2, &mut buf2); + assert_eq!(&buf2, b"llo"); +} + +#[test] +fn advance_frees_space() { + let mut rb = RingBuffer::new(64); + rb.write(b"hello world"); + rb.advance(5); + assert_eq!(rb.available(), 6); + assert_eq!(rb.free_space(), 64 - 6); + let mut buf = [0u8; 6]; + rb.read(&mut buf); + assert_eq!(&buf, b" world"); +} +``` + +**Step 2: Run tests to verify they fail** + +Run: `cargo test ring_buffer -- --nocapture` +Expected: FAIL — `write_at`, `peek_at`, `advance` not defined. + +**Step 3: Implement write_at, peek_at, advance** + +Add to `impl RingBuffer`: + +```rust +/// Write bytes at an arbitrary offset from `head`. Does NOT advance `tail` or `len`. +/// Used by the receive side for out-of-order segments — the caller is responsible +/// for tracking which ranges are filled and advancing `tail`/`len` when contiguous. +#[inline] +pub fn write_at(&mut self, offset: usize, data: &[u8]) { + let pos = (self.head.wrapping_add(offset)) & self.mask; + let first = data.len().min(self.buf.len() - pos); + self.buf[pos..pos + first].copy_from_slice(&data[..first]); + if first < data.len() { + self.buf[..data.len() - first].copy_from_slice(&data[first..]); + } +} + +/// Read bytes at an arbitrary offset from `head` without advancing `head`. +/// Used by the send side for retransmission. +#[inline] +pub fn peek_at(&self, offset: usize, buf: &mut [u8]) { + let pos = (self.head.wrapping_add(offset)) & self.mask; + let first = buf.len().min(self.buf.len() - pos); + buf[..first].copy_from_slice(&self.buf[pos..pos + first]); + if first < buf.len() { + buf[first..].copy_from_slice(&self.buf[..buf.len() - first]); + } +} + +/// Advance `head` by `n` bytes, freeing space. Used when bytes are ACKed (send) +/// or consumed by `TcpStream::read()` (receive). +#[inline] +pub fn advance(&mut self, n: usize) { + debug_assert!(n <= self.len); + self.head = self.head.wrapping_add(n); + self.len -= n; +} + +/// Advance `tail` and `len` to mark bytes as available for reading. +/// Used by the receive side when contiguous data is confirmed. +#[inline] +pub fn commit(&mut self, n: usize) { + debug_assert!(n <= self.free_space()); + self.tail = self.tail.wrapping_add(n); + self.len += n; +} +``` + +**Step 4: Run tests to verify they pass** + +Run: `cargo test ring_buffer -- --nocapture` +Expected: PASS + +**Step 5: Commit** + +```bash +git add src/net/handler/tcp/ring_buffer.rs +git commit -m "feat(tcp): RingBuffer write_at, peek_at, advance, commit" +``` + +--- + +### Task 4: TCB — Add Data Transfer State + +**Files:** +- Modify: `src/net/handler/tcp/tcb.rs` +- Modify: `src/net/handler/tcp/mod.rs` (update Tcb construction sites) + +**Step 1: Add new fields to Tcb** + +Add imports and fields to `src/net/handler/tcp/tcb.rs`: + +```rust +use std::collections::BTreeMap; +use super::ring_buffer::RingBuffer; +``` + +Add to the `Tcb` struct: + +```rust +// --- Data transfer buffers --- +/// Send ring buffer — user data is copied in, segments built from here. +pub send_buffer: RingBuffer, +/// Receive ring buffer — incoming payload copied here, user reads from here. +pub recv_buffer: RingBuffer, +/// Out-of-order receive ranges: seq -> byte_length (metadata only). +pub ooo_ranges: BTreeMap, + +// --- Congestion control --- +/// Congestion window in bytes. +pub cwnd: u32, +/// Slow start threshold. +pub ssthresh: u32, +/// Duplicate ACK counter for fast retransmit. +pub dup_ack_count: u8, + +// --- RTT estimation (RFC 6298) --- +/// Smoothed RTT in microseconds. +pub srtt: Option, +/// RTT variance in microseconds. +pub rttvar: u64, +/// Retransmission timeout in milliseconds (computed from srtt/rttvar). +pub rto: u64, +``` + +**Step 2: Add TcpConfig** + +Add to `src/net/handler/tcp/tcb.rs`: + +```rust +/// Configuration for TCP connections. +pub struct TcpConfig { + /// Send buffer size in bytes. Must be a power of two. Default: 256KB. + pub send_buffer_size: usize, + /// Receive buffer size in bytes. Must be a power of two. Default: 256KB. + pub recv_buffer_size: usize, + /// Listener backlog. Default: 128. + pub backlog: usize, +} + +impl Default for TcpConfig { + fn default() -> Self { + Self { + send_buffer_size: 256 * 1024, + recv_buffer_size: 256 * 1024, + backlog: 128, + } + } +} +``` + +**Step 3: Update all Tcb construction sites in `mod.rs`** + +There are three places where `Tcb { ... }` is constructed in `src/net/handler/tcp/mod.rs`: + +1. `connect()` (~line 131) — active open +2. `process_listen()` (~line 459) — passive open SYN-RECEIVED +3. No others. + +Add the new fields to each construction site with default values. Use `TcpConfig::default()` sizes for now. Initialize `cwnd` to `10 * eff_snd_mss` and `ssthresh` to `u32::MAX`. Set `rto` to `1000` (1 second initial RTO per RFC 6298). + +Example fields to add to each `Tcb { ... }`: + +```rust +send_buffer: RingBuffer::new(256 * 1024), +recv_buffer: RingBuffer::new(256 * 1024), +ooo_ranges: BTreeMap::new(), +cwnd: 10 * eff_snd_mss as u32, +ssthresh: u32::MAX, +dup_ack_count: 0, +srtt: None, +rttvar: 0, +rto: 1000, +``` + +Note: for the `connect()` site, `eff_snd_mss` is `DEFAULT_RCV_MSS` at creation time. For the `process_listen()` site, it's `peer_mss.min(DEFAULT_RCV_MSS)`. + +**Step 4: Run tests to verify nothing broke** + +Run: `cargo test tcp -- --nocapture` +Expected: PASS (all existing TCP tests still pass) + +**Step 5: Commit** + +```bash +git add src/net/handler/tcp/tcb.rs src/net/handler/tcp/mod.rs +git commit -m "feat(tcp): add data transfer state to TCB — ring buffers, cwnd, RTT" +``` + +--- + +### Task 5: SegmentBuilder — build_data() + +**Files:** +- Modify: `src/net/handler/tcp/segment.rs` + +**Step 1: Write the failing test** + +Add to the `segment.rs` file (create a `#[cfg(test)] mod tests` block if not present): + +```rust +#[cfg(test)] +mod tests { + use super::*; + use crate::xdp::frame::{BasicFrameBuffer, Frame}; + use crate::net::wire::ip::{Ipv4Address, IpAddress}; + use crate::net::wire::ethernet::MacAddress; + use crate::net::checksum::compute_tcp_checksum; + + fn alloc_free_frame(addr: u64) -> Frame<'static> { + let buf = Box::leak(vec![0u8; 2048].into_boxed_slice()); + Frame::new(addr, buf, 2048, false) + } + + #[test] + fn build_data_ipv4() { + let mut free = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + free.push(alloc_free_frame(100)); + + let payload = b"Hello, TCP!"; + let src_mac = MacAddress::new([0xAA; 6]); + let dst_mac = MacAddress::new([0xBB; 6]); + + SegmentBuilder::build_data( + IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])), + IpAddress::V4(Ipv4Address::new([10, 0, 0, 2])), + 8080, 80, + 1000, 500, + 65535, + payload, + src_mac, dst_mac, + false, &mut free, &mut tx, + ); + + assert_eq!(tx.num_frames(), 1); + assert_eq!(free.num_frames(), 0); + + let frame = tx.pop().unwrap(); + // ETH(14) + IPv4(20) + TCP(20) + payload(11) = 65 + assert_eq!(frame.len(), 65); + } +} +``` + +**Step 2: Run test to verify it fails** + +Run: `cargo test build_data_ipv4 -- --nocapture` +Expected: FAIL — `build_data` not defined. + +**Step 3: Implement build_data** + +Add to `impl SegmentBuilder` in `segment.rs`: + +```rust +/// Build a data segment with payload. +#[inline] +pub fn build_data<'umem>( + local_addr: IpAddress, + remote_addr: IpAddress, + local_port: u16, + remote_port: u16, + seq: u32, + ack: u32, + window: u16, + payload: &[u8], + src_mac: MacAddress, + dst_mac: MacAddress, + tx_offload: bool, + free_frames: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, +) { + match (local_addr, remote_addr) { + (IpAddress::V4(local_ip), IpAddress::V4(remote_ip)) => { + Self::build_ipv4_data_segment( + local_ip, remote_ip, + local_port, remote_port, + seq, ack, flags::ACK, window, + payload, + src_mac, dst_mac, + tx_offload, free_frames, tx_return, + ); + } + (IpAddress::V6(local_ip), IpAddress::V6(remote_ip)) => { + Self::build_ipv6_data_segment( + local_ip, remote_ip, + local_port, remote_port, + seq, ack, flags::ACK, window, + payload, + src_mac, dst_mac, + tx_offload, free_frames, tx_return, + ); + } + _ => {} + } +} +``` + +Add the IPv4 and IPv6 data segment builders. These are similar to the existing `build_ipv4_segment`/`build_ipv6_segment` but include a payload after the TCP header: + +```rust +#[inline] +fn build_ipv4_data_segment<'umem>( + src_ip: Ipv4Address, + dst_ip: Ipv4Address, + src_port: u16, + dst_port: u16, + seq: u32, + ack: u32, + tcp_flags: u8, + window: u16, + payload: &[u8], + src_mac: MacAddress, + dst_mac: MacAddress, + tx_offload: bool, + free_frames: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, +) { + let Some(mut frame) = free_frames.pop() else { + return; + }; + + let tcp_header_len = TCP_HEADER_LEN; // no options for data segments + let data_offset = (tcp_header_len / 4) as u8; + let total_ip_len = (IPV4_MIN_HEADER_LEN + tcp_header_len + payload.len()) as u16; + let frame_len = ETH_LEN + IPV4_MIN_HEADER_LEN + tcp_header_len + payload.len(); + + if frame.capacity() < frame_len { + free_frames.push(frame); + return; + } + + unsafe { frame.set_len(frame_len) }; + + write_ethernet_header(&mut frame, dst_mac, src_mac, EtherTypes::IPv4); + + { + let ip = &mut frame[ETH_LEN..ETH_LEN + IPV4_MIN_HEADER_LEN]; + ip.fill(0); + ip[0] = 0x45; + ip[2..4].copy_from_slice(&total_ip_len.to_be_bytes()); + ip[6] = 0x40; // DF + ip[8] = 64; // TTL + ip[9] = IpProtocols::Tcp; + let src_bytes: [u8; 4] = src_ip.into(); + ip[12..16].copy_from_slice(&src_bytes); + let dst_bytes: [u8; 4] = dst_ip.into(); + ip[16..20].copy_from_slice(&dst_bytes); + } + { + let ip = crate::net::wire::ip::Ipv4Header::from_bytes_mut(&mut frame); + ip.fill_checksum(); + } + + let tcp_offset = ETH_LEN + IPV4_MIN_HEADER_LEN; + Self::write_tcp_header( + &mut frame, tcp_offset, + src_port, dst_port, seq, ack, data_offset, tcp_flags, window, + &[], 0, + ); + + // Copy payload. + let payload_offset = tcp_offset + tcp_header_len; + frame[payload_offset..payload_offset + payload.len()].copy_from_slice(payload); + + if !tx_offload { + let tcp_bytes = &frame[tcp_offset..frame_len]; + let checksum = compute_tcp_checksum_from_parts(&src_ip, &dst_ip, &tcp_bytes[..TCP_HEADER_LEN], payload); + frame[tcp_offset + 16] = checksum[0]; + frame[tcp_offset + 17] = checksum[1]; + } + + tx_return.push(frame); +} + +#[inline] +fn build_ipv6_data_segment<'umem>( + src_ip: Ipv6Address, + dst_ip: Ipv6Address, + src_port: u16, + dst_port: u16, + seq: u32, + ack: u32, + tcp_flags: u8, + window: u16, + payload: &[u8], + src_mac: MacAddress, + dst_mac: MacAddress, + tx_offload: bool, + free_frames: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, +) { + let Some(mut frame) = free_frames.pop() else { + return; + }; + + let tcp_header_len = TCP_HEADER_LEN; + let data_offset = (tcp_header_len / 4) as u8; + let payload_len = (tcp_header_len + payload.len()) as u16; + let frame_len = ETH_LEN + IPV6_HEADER_LEN + tcp_header_len + payload.len(); + + if frame.capacity() < frame_len { + free_frames.push(frame); + return; + } + + unsafe { frame.set_len(frame_len) }; + + write_ethernet_header(&mut frame, dst_mac, src_mac, EtherTypes::IPv6); + + { + let ip = &mut frame[ETH_LEN..ETH_LEN + IPV6_HEADER_LEN]; + ip.fill(0); + ip[0] = 0x60; + ip[4..6].copy_from_slice(&payload_len.to_be_bytes()); + ip[6] = IpProtocols::Tcp; + ip[7] = 64; + let src_bytes: [u8; 16] = src_ip.into(); + ip[8..24].copy_from_slice(&src_bytes); + let dst_bytes: [u8; 16] = dst_ip.into(); + ip[24..40].copy_from_slice(&dst_bytes); + } + + let tcp_offset = ETH_LEN + IPV6_HEADER_LEN; + Self::write_tcp_header( + &mut frame, tcp_offset, + src_port, dst_port, seq, ack, data_offset, tcp_flags, window, + &[], 0, + ); + + let payload_offset = tcp_offset + tcp_header_len; + frame[payload_offset..payload_offset + payload.len()].copy_from_slice(payload); + + if !tx_offload { + let tcp_bytes = &frame[tcp_offset..frame_len]; + let checksum = compute_tcp_checksum_v6_from_parts(&src_ip, &dst_ip, &tcp_bytes[..TCP_HEADER_LEN], payload); + frame[tcp_offset + 16] = checksum[0]; + frame[tcp_offset + 17] = checksum[1]; + } + + tx_return.push(frame); +} +``` + +**Step 4: Run tests to verify they pass** + +Run: `cargo test build_data -- --nocapture` +Expected: PASS + +**Step 5: Commit** + +```bash +git add src/net/handler/tcp/segment.rs +git commit -m "feat(tcp): SegmentBuilder::build_data() for data segments" +``` + +--- + +### Task 6: TcpHandler — process_established() Receive Path + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs` + +**Step 1: Write the failing test** + +Add to the existing `tests` module in `mod.rs`: + +```rust +#[test] +fn established_receives_in_order_data() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(16); + let mut rx = BasicFrameBuffer::new(16); + let mut tx = BasicFrameBuffer::new(16); + + for i in 0..8 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete the handshake. + let accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1000, 0, flags::SYN, 65535, &[]); + let syn_len = syn_data.len(); + handler.process_ipv4(Frame::new(0, leak(syn_data), syn_len, false), &nh, &mut free, &mut rx, &mut tx); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1001, server_iss.wrapping_add(1), flags::ACK, 65535, &[]); + let ack_len = ack_data.len(); + handler.process_ipv4(Frame::new(1, leak(ack_data), ack_len, false), &nh, &mut free, &mut rx, &mut tx); + assert_eq!(handler.connections[0].state, TcpState::Established); + + // Clear tx from handshake. + while tx.pop().is_some() {} + + // Send a data segment. + let payload = b"Hello, TCP!"; + let data = build_tcp_frame_with_payload( + REMOTE_IP, LOCAL_IP, 12345, 80, + 1001, server_iss.wrapping_add(1), + flags::ACK, 65535, &[], payload, + ); + let data_len = data.len(); + handler.process_ipv4(Frame::new(2, leak(data), data_len, false), &nh, &mut free, &mut rx, &mut tx); + + // Verify: frame returned to rx_return, ACK generated on tx. + assert!(rx.num_frames() >= 1, "incoming frame returned to rx_return"); + assert_eq!(tx.num_frames(), 1, "ACK generated"); + + // Verify: data is in the receive ring buffer. + let tcb = &handler.connections[0]; + assert_eq!(tcb.recv_buffer.available(), payload.len()); + assert_eq!(tcb.rcv_nxt, 1001 + payload.len() as u32); +} +``` + +This requires a helper `build_tcp_frame_with_payload`. Add to test helpers: + +```rust +fn build_tcp_frame_with_payload( + src_ip: Ipv4Address, + dst_ip: Ipv4Address, + src_port: u16, + dst_port: u16, + seq: u32, + ack: u32, + tcp_flags: u8, + window: u16, + tcp_options: &[u8], + payload: &[u8], +) -> Vec { + let opt_padded_len = (tcp_options.len() + 3) & !3; + let tcp_header_len = TCP_HEADER_LEN + opt_padded_len; + let data_offset = (tcp_header_len / 4) as u8; + let total_ip_len = (IPV4_MIN_HEADER_LEN + tcp_header_len + payload.len()) as u16; + let mut buf = vec![0u8; ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN + tcp_header_len + payload.len()]; + + // Ethernet header. + buf[0..6].copy_from_slice(&[0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]); + buf[6..12].copy_from_slice(&[0x11, 0x22, 0x33, 0x44, 0x55, 0x66]); + buf[12] = 0x08; + buf[13] = 0x00; + + // IPv4 header. + let ip = &mut buf[ETH_HEADER_LEN..]; + ip[0] = 0x45; + ip[2..4].copy_from_slice(&total_ip_len.to_be_bytes()); + ip[6] = 0x40; + ip[8] = 64; + ip[9] = IpProtocols::Tcp; + let src_bytes: [u8; 4] = src_ip.into(); + ip[12..16].copy_from_slice(&src_bytes); + let dst_bytes: [u8; 4] = dst_ip.into(); + ip[16..20].copy_from_slice(&dst_bytes); + let cksum = compute_ipv4_checksum(&ip[..20]); + ip[10] = cksum[0]; + ip[11] = cksum[1]; + + // TCP header. + let tcp_off = ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN; + let hdr = TcpHeader::new( + src_port, dst_port, seq, ack, + data_offset, tcp_flags, window, + [0, 0], 0, + ); + let hdr_bytes = unsafe { + std::slice::from_raw_parts(&hdr as *const TcpHeader as *const u8, TCP_HEADER_LEN) + }; + buf[tcp_off..tcp_off + TCP_HEADER_LEN].copy_from_slice(hdr_bytes); + + // Options. + if !tcp_options.is_empty() { + buf[tcp_off + TCP_HEADER_LEN..tcp_off + TCP_HEADER_LEN + tcp_options.len()] + .copy_from_slice(tcp_options); + } + + // Payload. + let payload_off = tcp_off + tcp_header_len; + buf[payload_off..payload_off + payload.len()].copy_from_slice(payload); + + // TCP checksum (over header + payload). + let tcp_segment = &mut buf[tcp_off..]; + let cksum = compute_tcp_checksum(&src_ip, &dst_ip, tcp_segment); + buf[tcp_off + 16] = cksum[0]; + buf[tcp_off + 17] = cksum[1]; + + buf +} +``` + +**Step 2: Run test to verify it fails** + +Run: `cargo test established_receives_in_order_data -- --nocapture` +Expected: FAIL — currently the Established branch just does `rx_return.push(frame)`. + +**Step 3: Implement process_established** + +Replace the `TcpState::Established` match arm in `process_segment()` (~line 357) with: + +```rust +TcpState::Established => { + self.process_established( + idx, frame, seg_seq, seg_ack, seg_flags, seg_wnd, seg_len, + src_mac, dst_mac, + free_frames, rx_return, tx_return, + ); +} +``` + +Add the `process_established` method to `impl TcpHandler`. This handles: +- RST processing +- ACK processing (advance `snd_una`, update send window, duplicate ACK counting) +- In-order data: copy payload to recv_buffer, advance `rcv_nxt`, drain contiguous OOO ranges, send ACK +- Out-of-order data: copy payload to recv_buffer via `write_at`, record in `ooo_ranges`, send duplicate ACK +- Always return frame to `rx_return` + +The implementation is substantial — the full method should follow RFC 9293 Section 3.10.7.4 (ESTABLISHED state processing) for the segments it handles. Key logic: + +```rust +fn process_established<'umem>( + &mut self, + idx: usize, + frame: Frame<'umem>, + seg_seq: u32, + seg_ack: u32, + seg_flags: u8, + seg_wnd: u32, + seg_len: u32, + src_mac: crate::net::wire::ethernet::MacAddress, + dst_mac: crate::net::wire::ethernet::MacAddress, + free_frames: &mut impl FrameBuffer<'umem>, + rx_return: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, +) { + // Extract payload offset and length from frame before processing. + let ip_header_len = /* extract from frame based on IP version */; + let tcp_header_len = /* from data offset field already parsed */; + let payload_start = /* ETH + IP + TCP header */; + let payload_len = frame.len() - payload_start; + + let tcb = &self.connections[idx]; + let rcv_nxt = tcb.rcv_nxt; + let rcv_wnd = tcb.rcv_wnd; + + // Step 1: Check RST. + if seg_flags & flags::RST != 0 { + self.connections[idx].event_queue.push(TcpEvent::Reset); + self.connections.remove(idx); + rx_return.push(frame); + return; + } + + // Step 2: Check ACK. + if seg_flags & flags::ACK != 0 { + let tcb = &mut self.connections[idx]; + if crate::net::wire::tcp::seq_lt(tcb.snd_una, seg_ack) + && crate::net::wire::tcp::seq_le(seg_ack, tcb.snd_nxt) + { + // Valid new ACK. + let bytes_acked = seg_ack.wrapping_sub(tcb.snd_una) as usize; + tcb.snd_una = seg_ack; + tcb.send_buffer.advance(bytes_acked); + // Congestion control: slow start / congestion avoidance. + if tcb.cwnd < tcb.ssthresh { + tcb.cwnd += tcb.eff_snd_mss as u32; + } else { + tcb.cwnd += (tcb.eff_snd_mss as u32 * tcb.eff_snd_mss as u32) / tcb.cwnd; + } + tcb.dup_ack_count = 0; + // Update send window. + tcb.snd_wnd = seg_wnd; + tcb.snd_wl1 = seg_seq; + tcb.snd_wl2 = seg_ack; + } else if seg_ack == tcb.snd_una { + // Duplicate ACK. + if payload_len == 0 { + tcb.dup_ack_count += 1; + // Fast retransmit at 3 duplicate ACKs handled in poll_timers. + } + } + } + + // Step 3: Process data payload. + if payload_len > 0 { + let tcb = &mut self.connections[idx]; + if seg_seq == tcb.rcv_nxt { + // In-order: copy to recv buffer, advance rcv_nxt. + let payload = &frame[payload_start..payload_start + payload_len]; + tcb.recv_buffer.write(payload); + tcb.recv_buffer.commit(payload_len); // if using write_at path + // Actually for in-order, just use write() which advances tail. + tcb.rcv_nxt = tcb.rcv_nxt.wrapping_add(payload_len as u32); + + // Drain any now-contiguous OOO ranges. + while let Some((&ooo_seq, &ooo_len)) = tcb.ooo_ranges.iter().next() { + if ooo_seq == tcb.rcv_nxt { + tcb.rcv_nxt = tcb.rcv_nxt.wrapping_add(ooo_len); + tcb.recv_buffer.commit(ooo_len as usize); + tcb.ooo_ranges.remove(&ooo_seq); + } else { + break; + } + } + + // Send ACK. + let id = tcb.id; + let snd_nxt = tcb.snd_nxt; + let new_rcv_nxt = tcb.rcv_nxt; + let window = tcb.recv_buffer.free_space().min(u16::MAX as usize) as u16; + SegmentBuilder::build_ack( + id.local_addr, id.remote_addr, + id.local_port, id.remote_port, + snd_nxt, new_rcv_nxt, window, + src_mac, dst_mac, + self.tx_offload, free_frames, tx_return, + ); + } else if crate::net::wire::tcp::seq_lt(rcv_nxt, seg_seq) { + // Out-of-order: copy to recv buffer at offset, record range. + let offset = seg_seq.wrapping_sub(rcv_nxt) as usize; + let payload = &frame[payload_start..payload_start + payload_len]; + tcb.recv_buffer.write_at(offset, payload); + tcb.ooo_ranges.insert(seg_seq, payload_len as u32); + + // Send duplicate ACK. + let id = tcb.id; + let snd_nxt = tcb.snd_nxt; + let window = tcb.recv_buffer.free_space().min(u16::MAX as usize) as u16; + SegmentBuilder::build_ack( + id.local_addr, id.remote_addr, + id.local_port, id.remote_port, + snd_nxt, rcv_nxt, window, + src_mac, dst_mac, + self.tx_offload, free_frames, tx_return, + ); + } + // else: duplicate data (seg_seq < rcv_nxt), just ACK. + } + + // Always return frame. + rx_return.push(frame); +} +``` + +Note: the payload offset calculation needs to extract the IP header length and TCP header length from the already-parsed values. The `seg_len` and header offsets are already computed in `process_ipv4`/`process_ipv6` — you'll need to pass the payload offset and payload length through `process_segment` as additional parameters. + +**Step 4: Run tests to verify they pass** + +Run: `cargo test tcp -- --nocapture` +Expected: PASS (new test + all existing tests) + +**Step 5: Commit** + +```bash +git add src/net/handler/tcp/mod.rs +git commit -m "feat(tcp): process_established() receive path with in-order and OOO handling" +``` + +--- + +### Task 7: TcpHandler — process_established() Out-of-Order Test + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs` (tests only) + +**Step 1: Write the test** + +```rust +#[test] +fn established_out_of_order_reassembly() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1000, 0, flags::SYN, 65535, &[]); + let syn_len = syn_data.len(); + handler.process_ipv4(Frame::new(0, leak(syn_data), syn_len, false), &nh, &mut free, &mut rx, &mut tx); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1001, server_iss.wrapping_add(1), flags::ACK, 65535, &[]); + let ack_len = ack_data.len(); + handler.process_ipv4(Frame::new(1, leak(ack_data), ack_len, false), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + + // Send segment 2 first (out of order): seq=1006, 5 bytes "world". + let seg2 = build_tcp_frame_with_payload( + REMOTE_IP, LOCAL_IP, 12345, 80, + 1006, server_iss.wrapping_add(1), + flags::ACK, 65535, &[], b"world", + ); + let seg2_len = seg2.len(); + handler.process_ipv4(Frame::new(2, leak(seg2), seg2_len, false), &nh, &mut free, &mut rx, &mut tx); + assert_eq!(handler.connections[0].rcv_nxt, 1001, "rcv_nxt not advanced for OOO"); + assert_eq!(handler.connections[0].ooo_ranges.len(), 1); + + // Now send segment 1 (fills the gap): seq=1001, 5 bytes "hello". + let seg1 = build_tcp_frame_with_payload( + REMOTE_IP, LOCAL_IP, 12345, 80, + 1001, server_iss.wrapping_add(1), + flags::ACK, 65535, &[], b"hello", + ); + let seg1_len = seg1.len(); + handler.process_ipv4(Frame::new(3, leak(seg1), seg1_len, false), &nh, &mut free, &mut rx, &mut tx); + + // Both segments should now be contiguous. + assert_eq!(handler.connections[0].rcv_nxt, 1011, "rcv_nxt advanced past both segments"); + assert_eq!(handler.connections[0].ooo_ranges.len(), 0, "OOO ranges drained"); + assert_eq!(handler.connections[0].recv_buffer.available(), 10); + + // Read from recv buffer and verify contents. + let mut buf = [0u8; 10]; + handler.connections[0].recv_buffer.read(&mut buf); + assert_eq!(&buf, b"helloworld"); +} +``` + +**Step 2: Run test** + +Run: `cargo test established_out_of_order -- --nocapture` +Expected: PASS (if Task 6 is implemented correctly) + +**Step 3: Commit** + +```bash +git add src/net/handler/tcp/mod.rs +git commit -m "test(tcp): out-of-order segment reassembly test" +``` + +--- + +### Task 8: TcpHandler — poll_send() + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs` + +**Step 1: Write the failing test** + +```rust +#[test] +fn poll_send_builds_data_segment() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1000, 0, flags::SYN, 65535, &[]); + let syn_len = syn_data.len(); + handler.process_ipv4(Frame::new(0, leak(syn_data), syn_len, false), &nh, &mut free, &mut rx, &mut tx); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1001, server_iss.wrapping_add(1), flags::ACK, 65535, &[]); + let ack_len = ack_data.len(); + handler.process_ipv4(Frame::new(1, leak(ack_data), ack_len, false), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + + // Write data into the connection's send buffer. + let payload = b"Hello from server!"; + handler.connections[0].send_buffer.write(payload); + + // Set snd_wnd so the window allows sending. + handler.connections[0].snd_wnd = 65535; + + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + + assert_eq!(tx.num_frames(), 1, "data segment built"); + let tcb = &handler.connections[0]; + assert_eq!(tcb.snd_nxt, server_iss.wrapping_add(1).wrapping_add(payload.len() as u32)); + assert_eq!(tcb.send_buffer.available(), payload.len()); // still in buffer until ACKed +} +``` + +**Step 2: Run test to verify it fails** + +Run: `cargo test poll_send_builds_data_segment -- --nocapture` +Expected: FAIL — `poll_send` not defined. + +**Step 3: Implement poll_send** + +Add to `impl TcpHandler`: + +```rust +/// Poll established connections for outbound data segments. +/// Called each tick from the runtime loop after receive processing. +pub fn poll_send<'umem>( + &mut self, + now: Instant, + src_mac: crate::net::wire::ethernet::MacAddress, + neighbor_handler: &NeighborHandler, + free_frames: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, +) { + for tcb in &mut self.connections { + if tcb.state != TcpState::Established { + continue; + } + + // Compute how many bytes we can send. + let bytes_in_flight = tcb.snd_nxt.wrapping_sub(tcb.snd_una) as usize; + let send_window = (tcb.snd_wnd as usize).min(tcb.cwnd as usize); + let can_send = send_window.saturating_sub(bytes_in_flight); + let data_available = tcb.send_buffer.available() - bytes_in_flight; // unsent data + + if can_send == 0 || data_available == 0 { + continue; + } + + let to_send = can_send.min(data_available).min(tcb.eff_snd_mss as usize); + + // Peek the data from the send buffer (don't advance — held until ACKed). + let mut payload = vec![0u8; to_send]; // NOTE: consider stack buffer for MSS-sized chunks + tcb.send_buffer.peek_at(bytes_in_flight, &mut payload); + + let dst_mac = neighbor_handler + .lookup(now, &tcb.id.remote_addr) + .unwrap_or(crate::net::wire::ethernet::MacAddress::broadcast()); + + let window = tcb.recv_buffer.free_space().min(u16::MAX as usize) as u16; + + SegmentBuilder::build_data( + tcb.id.local_addr, tcb.id.remote_addr, + tcb.id.local_port, tcb.id.remote_port, + tcb.snd_nxt, tcb.rcv_nxt, window, + &payload, + src_mac, dst_mac, + self.tx_offload, free_frames, tx_return, + ); + + tcb.snd_nxt = tcb.snd_nxt.wrapping_add(to_send as u32); + + // Set retransmit timer if not already running. + if tcb.retransmit_deadline.is_none() { + tcb.retransmit_deadline = Some(now + coarsetime::Duration::from_millis(tcb.rto)); + } + } +} +``` + +Note: The `vec![0u8; to_send]` allocation is a concern. For the initial implementation this is acceptable because `to_send <= eff_snd_mss` (1460 bytes max). A future optimization can use a stack-allocated `[u8; 1460]` or a per-handler scratch buffer. Flag this with a `// TODO: use stack buffer to avoid allocation` comment. + +**Step 4: Run tests to verify they pass** + +Run: `cargo test tcp -- --nocapture` +Expected: PASS + +**Step 5: Commit** + +```bash +git add src/net/handler/tcp/mod.rs +git commit -m "feat(tcp): poll_send() builds data segments from send buffer" +``` + +--- + +### Task 9: TcpHandler — Data Retransmission (RTO + Fast Retransmit) + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs` (extend `poll_timers`) + +**Step 1: Write the failing test** + +```rust +#[test] +fn fast_retransmit_on_three_dup_acks() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake + establish. + let accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1000, 0, flags::SYN, 65535, &[]); + let syn_len = syn_data.len(); + handler.process_ipv4(Frame::new(0, leak(syn_data), syn_len, false), &nh, &mut free, &mut rx, &mut tx); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1001, server_iss.wrapping_add(1), flags::ACK, 65535, &[]); + let ack_len = ack_data.len(); + handler.process_ipv4(Frame::new(1, leak(ack_data), ack_len, false), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + + // Put data in send buffer and send it. + handler.connections[0].send_buffer.write(b"AAAA"); + handler.connections[0].snd_wnd = 65535; + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + while tx.pop().is_some() {} // consume sent segment + + // Send 3 duplicate ACKs (ACKing the old snd_una, not the new data). + let dup_ack_seq = server_iss.wrapping_add(1); // original snd_una + for i in 0..3u64 { + let dup = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1001, dup_ack_seq, flags::ACK, 65535, &[]); + let dup_len = dup.len(); + handler.process_ipv4(Frame::new(10 + i, leak(dup), dup_len, false), &nh, &mut free, &mut rx, &mut tx); + } + + assert_eq!(handler.connections[0].dup_ack_count, 3); + + // poll_timers should trigger fast retransmit. + handler.poll_timers(now, nh.local_mac(), &nh, &mut free, &mut tx); + assert!(tx.num_frames() >= 1, "retransmitted segment"); + + // cwnd should be halved. + let tcb = &handler.connections[0]; + assert!(tcb.cwnd < 10 * tcb.eff_snd_mss as u32, "cwnd reduced"); +} +``` + +**Step 2: Run test to verify it fails** + +Run: `cargo test fast_retransmit -- --nocapture` +Expected: FAIL — `dup_ack_count` never reaches 3 / retransmit logic not implemented. + +**Step 3: Extend poll_timers for data retransmission** + +In the existing `poll_timers` method, add handling for Established state connections: + +```rust +TcpState::Established => { + // Fast retransmit check. + if tcb.dup_ack_count >= 3 { + // Retransmit oldest unACKed segment. + let retransmit_len = tcb.send_buffer.available() + .min(tcb.eff_snd_mss as usize); + if retransmit_len > 0 { + let mut payload = vec![0u8; retransmit_len]; + tcb.send_buffer.peek_at(0, &mut payload); + let window = tcb.recv_buffer.free_space().min(u16::MAX as usize) as u16; + SegmentBuilder::build_data( + id.local_addr, id.remote_addr, + id.local_port, id.remote_port, + tcb.snd_una, tcb.rcv_nxt, window, + &payload, + src_mac, dst_mac, + self.tx_offload, free_frames, tx_return, + ); + } + tcb.ssthresh = (tcb.cwnd / 2).max(2 * tcb.eff_snd_mss as u32); + tcb.cwnd = tcb.ssthresh; + tcb.dup_ack_count = 0; + continue; + } + + // RTO retransmit. + // (same pattern: peek from send buffer, rebuild segment, backoff timer) + let retransmit_len = tcb.send_buffer.available().min(tcb.eff_snd_mss as usize); + if retransmit_len > 0 { + let mut payload = vec![0u8; retransmit_len]; + tcb.send_buffer.peek_at(0, &mut payload); + let window = tcb.recv_buffer.free_space().min(u16::MAX as usize) as u16; + SegmentBuilder::build_data( + id.local_addr, id.remote_addr, + id.local_port, id.remote_port, + tcb.snd_una, tcb.rcv_nxt, window, + &payload, + src_mac, dst_mac, + self.tx_offload, free_frames, tx_return, + ); + } + tcb.ssthresh = (tcb.cwnd / 2).max(2 * tcb.eff_snd_mss as u32); + tcb.cwnd = tcb.eff_snd_mss as u32; + tcb.rto_backoff += 1; + tcb.retransmit_deadline = Some(now + coarsetime::Duration::from_millis(tcb.rto << tcb.rto_backoff)); +} +``` + +**Step 4: Run tests to verify they pass** + +Run: `cargo test tcp -- --nocapture` +Expected: PASS + +**Step 5: Commit** + +```bash +git add src/net/handler/tcp/mod.rs +git commit -m "feat(tcp): data retransmission — RTO + fast retransmit on 3 dup ACKs" +``` + +--- + +### Task 10: TcpStream — write() and read() Futures + +**Files:** +- Modify: `src/net/socket/tcp.rs` + +**Step 1: Write the failing test** + +Add to the `tests` module (create one if needed — follow the pattern in `udp.rs` with `with_test_context`): + +```rust +#[cfg(test)] +mod tests { + use std::task::{Context, Poll}; + use std::pin::Pin; + use std::future::Future; + use crate::rt::context::{ContextDropGuard, RuntimeContext}; + use crate::rt::waker; + use super::*; + + // Reuse the with_test_context pattern from udp.rs tests. + + #[test] + fn write_returns_ready_when_buffer_has_space() { + with_test_context(|| { + // This test verifies the write future API exists and works. + // Full integration requires a connected TcpStream which needs + // handshake — so test the Write future directly against a + // handler with a pre-built connection. + }); + } +} +``` + +Note: Full TcpStream integration tests are complex because they require a completed handshake. The unit tests in `mod.rs` (Tasks 6-9) validate the handler logic. The socket-level tests here validate the future wiring. + +**Step 2: Implement Write and Read futures** + +Add to `src/net/socket/tcp.rs`: + +```rust +/// Future returned by [`TcpStream::write()`]. +pub struct TcpWrite<'stream> { + handler: &'stream Rc>, + conn_id: ConnectionId, + data: &'stream [u8], + written: usize, +} + +impl<'stream> Future for TcpWrite<'stream> { + type Output = usize; + + fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll { + let this = self.get_mut(); + let handler = unsafe { &mut *this.handler.get() }; + if let Some(tcb) = handler.get_connection_mut(&this.conn_id) { + let remaining = &this.data[this.written..]; + let n = tcb.send_buffer.write(remaining); + this.written += n; + if this.written == this.data.len() { + Poll::Ready(this.written) + } else { + Poll::Pending + } + } else { + Poll::Ready(0) // connection gone + } + } +} + +/// Future returned by [`TcpStream::read()`]. +pub struct TcpRead<'stream> { + handler: &'stream Rc>, + conn_id: ConnectionId, + buf: &'stream mut [u8], +} + +impl<'stream> Future for TcpRead<'stream> { + type Output = usize; + + fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll { + let this = self.get_mut(); + let handler = unsafe { &mut *this.handler.get() }; + if let Some(tcb) = handler.get_connection_mut(&this.conn_id) { + let n = tcb.recv_buffer.read(this.buf); + if n > 0 { + Poll::Ready(n) + } else { + Poll::Pending + } + } else { + Poll::Ready(0) // connection gone + } + } +} +``` + +Add methods to `TcpStream`: + +```rust +/// Write data to this connection. Returns a future that resolves when +/// all bytes are copied into the send buffer. +pub fn write<'a>(&'a self, data: &'a [u8]) -> TcpWrite<'a> { + TcpWrite { + handler: &self.handler, + conn_id: self.conn_id, + data, + written: 0, + } +} + +/// Read data from this connection. Returns a future that resolves when +/// data is available in the receive buffer. +pub fn read<'a>(&'a self, buf: &'a mut [u8]) -> TcpRead<'a> { + TcpRead { + handler: &self.handler, + conn_id: self.conn_id, + buf, + } +} +``` + +This also requires adding `get_connection_mut` to `TcpHandler`: + +```rust +/// Get a mutable reference to a connection by ConnectionId. +pub fn get_connection_mut(&mut self, id: &ConnectionId) -> Option<&mut Tcb> { + self.connections.iter_mut().find(|c| c.id == *id) +} +``` + +**Step 3: Update socket/mod.rs exports** + +Add `TcpWrite` and `TcpRead` to the public exports in `src/net/socket/mod.rs`. + +**Step 4: Run tests to verify nothing broke** + +Run: `cargo test -- --nocapture` +Expected: PASS + +**Step 5: Commit** + +```bash +git add src/net/socket/tcp.rs src/net/socket/mod.rs src/net/handler/tcp/mod.rs +git commit -m "feat(tcp): TcpStream::write() and read() futures" +``` + +--- + +### Task 11: Runtime Loop — Integrate poll_send + +**Files:** +- Modify: `src/rt/local.rs` + +**Step 1: Add poll_send to the runtime loop** + +In `LocalRuntime::run()`, after the `poll_timers` block (~line 327-336), add: + +```rust +// Drive TCP data segment transmission. +{ + // SAFETY: single-threaded, no reentrant handler calls. + let tcp_handler = unsafe { &mut *self.tcp_handler.get() }; + tcp_handler.poll_send( + now, + self.neighbor_handler.local_mac(), + &self.neighbor_handler, + &mut self.free_frames, + &mut self.tx_return, + ); +} +``` + +**Step 2: Run tests to verify nothing broke** + +Run: `cargo test -- --nocapture` +Expected: PASS + +**Step 3: Commit** + +```bash +git add src/rt/local.rs +git commit -m "feat(tcp): integrate poll_send into runtime loop" +``` + +--- + +### Task 12: TcpConfig — Wiring Through listen/connect + +**Files:** +- Modify: `src/net/handler/tcp/tcb.rs` (TcpConfig already added in Task 4) +- Modify: `src/net/handler/tcp/mod.rs` (accept config in connect/listen) +- Modify: `src/net/socket/tcp.rs` (expose config on TcpListener/TcpStream) + +**Step 1: Add config parameter to TcpHandler::connect and TcpHandler::listen** + +Modify `listen` to accept an optional `TcpConfig` parameter for buffer sizes. Modify `connect` similarly. The `TcpConfig` values flow through to the `RingBuffer::new()` calls in Tcb construction. + +**Step 2: Add `listen_with_config` and `connect_with_config` to TcpListener/TcpStream** + +These mirror the existing `listen` / `connect` but accept a `TcpConfig`. + +**Step 3: Run tests** + +Run: `cargo test tcp -- --nocapture` +Expected: PASS + +**Step 4: Commit** + +```bash +git add src/net/handler/tcp/mod.rs src/net/handler/tcp/tcb.rs src/net/socket/tcp.rs +git commit -m "feat(tcp): TcpConfig for per-connection buffer sizing" +``` + +--- + +### Task 13: RTT Estimation (RFC 6298) + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs` (update ACK processing in process_established) + +**Step 1: Write the test** + +```rust +#[test] +fn rtt_estimation_updates_rto() { + // After a segment is sent and ACKed, srtt/rttvar/rto should be updated. + // Setup: handshake, write data, poll_send, then process an ACK. + // Verify: tcb.srtt is Some, tcb.rto is reasonable (not the initial 1000ms). +} +``` + +**Step 2: Implement RTT update in ACK processing** + +In `process_established`, when a valid new ACK is received and `bytes_acked > 0`: + +```rust +// RTT measurement: use the time since the retransmit_deadline was set. +// Simplified: measure RTT from when the segment was sent (approximated +// by now - rto + remaining_deadline_time). For accuracy, store a +// `snd_nxt_timestamp` on the TCB when sending. +if let Some(send_time) = tcb.last_send_time { + let rtt_us = now.duration_since(send_time).as_micros(); + match tcb.srtt { + None => { + // First measurement (RFC 6298 §2.2). + tcb.srtt = Some(rtt_us); + tcb.rttvar = rtt_us / 2; + } + Some(srtt) => { + // Subsequent measurements (RFC 6298 §2.3). + let diff = if rtt_us > srtt { rtt_us - srtt } else { srtt - rtt_us }; + tcb.rttvar = (3 * tcb.rttvar + diff) / 4; + tcb.srtt = Some((7 * srtt + rtt_us) / 8); + } + } + let srtt = tcb.srtt.unwrap(); + let rto = srtt + 4 * tcb.rttvar; + tcb.rto = rto.max(1000).min(60_000) / 1000; // convert to ms, clamp 1s-60s +} +``` + +This requires adding a `last_send_time: Option` field to `Tcb`, set in `poll_send` when a segment is sent. + +**Step 3: Run tests** + +Run: `cargo test tcp -- --nocapture` +Expected: PASS + +**Step 4: Commit** + +```bash +git add src/net/handler/tcp/mod.rs src/net/handler/tcp/tcb.rs +git commit -m "feat(tcp): RTT estimation per RFC 6298 updates RTO on ACK" +``` + +--- + +### Task 14: Final Integration — Verify Frame Accounting + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs` (test only) + +**Step 1: Write comprehensive frame accounting test** + +```rust +#[test] +fn frame_accounting_through_data_transfer() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(64); + let mut rx = BasicFrameBuffer::new(64); + let mut tx = BasicFrameBuffer::new(64); + + for i in 0..32 { + free.push(alloc_free_frame(100 + i)); + } + + let initial_total = free.num_frames(); + + // Handshake. + handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1000, 0, flags::SYN, 65535, &[]); + let syn_len = syn.len(); + handler.process_ipv4(Frame::new(0, leak(syn), syn_len, false), &nh, &mut free, &mut rx, &mut tx); + let server_iss = handler.connections[0].iss; + let ack = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1001, server_iss.wrapping_add(1), flags::ACK, 65535, &[]); + let ack_len = ack.len(); + handler.process_ipv4(Frame::new(1, leak(ack), ack_len, false), &nh, &mut free, &mut rx, &mut tx); + + // Data segment. + let data = build_tcp_frame_with_payload( + REMOTE_IP, LOCAL_IP, 12345, 80, + 1001, server_iss.wrapping_add(1), + flags::ACK, 65535, &[], b"test data", + ); + let data_len = data.len(); + handler.process_ipv4(Frame::new(2, leak(data), data_len, false), &nh, &mut free, &mut rx, &mut tx); + + // All frames accounted for: free + rx + tx = initial + incoming frames. + let total = free.num_frames() + rx.num_frames() + tx.num_frames(); + // We started with initial_total free frames and injected 3 incoming frames. + assert_eq!(total, initial_total + 3, "all frames accounted for"); +} +``` + +**Step 2: Run test** + +Run: `cargo test frame_accounting_through_data -- --nocapture` +Expected: PASS + +**Step 3: Commit** + +```bash +git add src/net/handler/tcp/mod.rs +git commit -m "test(tcp): comprehensive frame accounting through data transfer" +``` + +--- + +## Task Dependency Graph + +``` +Task 1 (RingBuffer skeleton) + └─> Task 2 (write/read) + └─> Task 3 (write_at/peek_at/advance) + └─> Task 4 (TCB state additions) + ├─> Task 5 (SegmentBuilder::build_data) + │ └─> Task 8 (poll_send) + │ └─> Task 9 (retransmission) + │ └─> Task 11 (runtime integration) + │ └─> Task 12 (TcpConfig wiring) + │ └─> Task 13 (RTT estimation) + │ └─> Task 14 (frame accounting) + └─> Task 6 (process_established receive) + └─> Task 7 (OOO test) + └─> Task 10 (TcpStream write/read futures) +``` + +Tasks 5-6 can be parallelized after Task 4. Tasks 8 and 10 can be parallelized after their respective dependencies. diff --git a/docs/plans/2026-03-06-tcp-delayed-ack-nagle-design.md b/docs/plans/2026-03-06-tcp-delayed-ack-nagle-design.md new file mode 100644 index 0000000..ecbb972 --- /dev/null +++ b/docs/plans/2026-03-06-tcp-delayed-ack-nagle-design.md @@ -0,0 +1,109 @@ +# TCP Delayed ACK + Nagle Algorithm Design + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Reduce per-segment overhead with delayed ACKs and prevent small-packet floods with the Nagle algorithm. + +**Architecture:** Two complementary optimizations in the TCP handler. Delayed ACK defers ACKs up to 40ms or 2 segments, allowing piggybacking on data. Nagle gates small sends when data is in flight, coalescing into MSS-sized segments. + +**Tech Stack:** Rust, coarsetime for timers, existing TCP handler infrastructure. + +--- + +## Delayed ACK + +### Current Behavior + +Every in-order data segment in `process_established` triggers an immediate `SegmentBuilder::build_ack`. This sends one ACK per received segment — correct but wasteful when data is flowing bidirectionally (the ACK could piggyback on the next outbound data segment). + +### Design + +**New TCB fields:** +- `ack_pending: bool` (default `false`) — set when an ACK is owed but deferred +- `delayed_ack_deadline: Option` — when the deferred ACK must be sent +- `ack_delay_count: u8` (default `0`) — counts consecutive unACKed segments; flush at 2 + +**New TcpConfig field:** +- `delayed_ack_ms: u64` (default `40`) — max delay before flushing + +**Receive path (`process_established`):** +- In-order data: instead of `build_ack`, set `ack_pending = true`, set deadline if not already set, increment `ack_delay_count`. If `ack_delay_count >= 2`, send ACK immediately and reset. +- Out-of-order data: immediate ACK (needed for fast retransmit signaling) +- Duplicate data: immediate ACK +- FIN: immediate ACK + +**Send path (`poll_send`):** +- Data segments already carry the ACK flag with current `rcv_nxt`. After sending data, clear `ack_pending` and reset `ack_delay_count` (piggyback). + +**Timer path (`poll_timers`):** +- If `ack_pending && now >= delayed_ack_deadline`: send ACK, clear `ack_pending`, reset `ack_delay_count`. + +**Constants:** +- `DEFAULT_DELAYED_ACK_MS: u64 = 40` +- `MAX_DELAYED_ACK_COUNT: u8 = 2` + +### RFC Compliance + +RFC 9293 §4.2: "A TCP implementation SHOULD implement a delayed ACK" and "An ACK SHOULD NOT be excessively delayed; in particular, the delay MUST be less than 0.5 seconds." The 40ms default is well within bounds and matches common implementations (Linux uses 40ms). + +RFC 5681 §4.2: "An ACK SHOULD be generated for at least every second full-sized segment." The `ack_delay_count >= 2` flush satisfies this. + +## Nagle Algorithm + +### Current Behavior + +`poll_send` sends any available data immediately regardless of size. A 1-byte `TcpStream::write` produces a 1-byte TCP segment. + +### Design + +**New TCB field:** +- `nagle_enabled: bool` (default `true`) + +**New TcpConfig field:** +- `tcp_no_delay: bool` (default `false`) — when true, disables Nagle + +**Send gate in `poll_send`:** + +Before sending, check Nagle: +``` +if nagle_enabled && bytes_in_flight > 0 && to_send < eff_snd_mss: + skip sending (wait for outstanding ACK) +``` + +This means: +- First segment (nothing in flight): always send, any size +- Full MSS available: always send (no small-packet concern) +- Small data with outstanding segments: hold and coalesce +- `!nagle_enabled` (TCP_NODELAY): always send immediately + +**Socket API:** +- `TcpStream::set_nodelay(&self, nodelay: bool)` — flips `nagle_enabled` on the TCB + +### Nagle + Delayed ACK Interaction + +These features interact: if both sides use Nagle + delayed ACK, a write-write-read pattern can stall (sender waits for ACK, receiver delays ACK). This is the well-known "Nagle-delayed-ACK problem" and is standard TCP behavior. Applications that need low latency on small writes use `TCP_NODELAY`. The echo example works fine because the server echoes immediately, piggybacking the ACK. + +## Test Plan + +**Delayed ACK tests:** +1. In-order data does NOT produce immediate ACK (verify no segment in tx_return after process_established) +2. Timer expiry sends ACK (call poll_timers after deadline) +3. Second consecutive segment flushes ACK immediately (ack_delay_count >= 2) +4. Data send piggybacks ACK (poll_send clears ack_pending) +5. Out-of-order data still sends immediate ACK +6. FIN still sends immediate ACK + +**Nagle tests:** +7. Small data with bytes_in_flight > 0 is held (no segment built) +8. Full MSS-sized data always sends regardless of bytes_in_flight +9. bytes_in_flight == 0 always sends regardless of size +10. TCP_NODELAY: small data sends immediately even with bytes_in_flight > 0 + +## Scope + +**Modified files:** +- `src/net/handler/tcp/tcb.rs` — new fields on Tcb and TcpConfig +- `src/net/handler/tcp/mod.rs` — process_established, poll_send, poll_timers changes +- `src/net/socket/tcp.rs` — `set_nodelay` method on TcpStream + +**No new files.** No changes to segment builder, state machine, or ring buffer. diff --git a/docs/plans/2026-03-06-tcp-delayed-ack-nagle-impl.md b/docs/plans/2026-03-06-tcp-delayed-ack-nagle-impl.md new file mode 100644 index 0000000..f49e36b --- /dev/null +++ b/docs/plans/2026-03-06-tcp-delayed-ack-nagle-impl.md @@ -0,0 +1,747 @@ +# TCP Delayed ACK + Nagle Algorithm Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add delayed ACK and Nagle algorithm to reduce per-segment overhead and small-packet floods. + +**Architecture:** Four TCB fields (`ack_pending`, `delayed_ack_deadline`, `ack_delay_count`, `nagle_enabled`) gate ACK sending in `process_established` and data sending in `poll_send`. A new timer check in `poll_timers` flushes delayed ACKs. Config fields (`tcp_no_delay`, `delayed_ack_ms`) propagate through `TcpConfig` → `ListenEntry` → `Tcb`. + +**Tech Stack:** Rust, coarsetime for timers. + +--- + +### Dependency Graph + +``` +Task 1 (TCB + Config fields) ──> Task 2 (Delayed ACK) ──> Task 4 (Nagle) + ──> Task 3 (Delayed ACK timer) + ──> Task 5 (set_nodelay API) +``` + +Tasks 2 and 3 can be done in parallel after Task 1. Task 4 depends on Task 2 (needs `ack_pending` clearing in `poll_send`). Task 5 depends on Task 4. + +--- + +### Task 1: Add TCB and Config Fields + +**Files:** +- Modify: `src/net/handler/tcp/tcb.rs` +- Modify: `src/net/handler/tcp/mod.rs` (TCB initialization sites + ListenEntry) + +**Context:** + +The `Tcb` struct at `src/net/handler/tcp/tcb.rs:97` holds all per-connection state. There are two TCB initialization sites in `mod.rs`: +- Active open (connect): line ~175 +- Passive open (SYN-RECEIVED): line ~570 + +`TcpConfig` at `tcb.rs:74` holds per-connection configuration. `ListenEntry` at `mod.rs:39` stores config values that get copied into passive-open TCBs. + +**Step 1: Write the failing test** + +Add to the test module in `src/net/handler/tcp/mod.rs`: + +```rust +#[test] +fn new_connection_has_delayed_ack_fields() { + let (mut handler, mut free, mut rx, mut tx) = setup(); + let now = Instant::now(); + + // Set up listener and complete handshake. + let _accept = handler.listen( + IpAddress::V4(LOCAL_IP), LOCAL_PORT, 128, + ).unwrap(); + + let syn = build_syn_frame(REMOTE_IP, LOCAL_IP, REMOTE_PORT, LOCAL_PORT, 1000); + handler.process_ipv4_with_now(syn, now, &NH, &mut free, &mut rx, &mut tx); + let _ = tx.pop(); // SYN-ACK + + let ack = build_ack_frame(REMOTE_IP, LOCAL_IP, REMOTE_PORT, LOCAL_PORT, 1001, 1); + handler.process_ipv4_with_now(ack, now, &NH, &mut free, &mut rx, &mut tx); + + let id = ConnectionId { + local_addr: IpAddress::V4(LOCAL_IP), + local_port: LOCAL_PORT, + remote_addr: IpAddress::V4(REMOTE_IP), + remote_port: REMOTE_PORT, + }; + let tcb = handler.get_connection(&id).unwrap(); + assert!(!tcb.ack_pending); + assert!(tcb.delayed_ack_deadline.is_none()); + assert_eq!(tcb.ack_delay_count, 0); + assert!(tcb.nagle_enabled); +} +``` + +This test verifies the new fields exist and have correct defaults. It will fail because the fields don't exist yet. + +**Step 2: Run test to verify it fails** + +Run: `cargo test new_connection_has_delayed_ack_fields` +Expected: FAIL — no field `ack_pending` on type `Tcb` + +**Step 3: Implement the fields** + +In `src/net/handler/tcp/tcb.rs`: + +Add constants after line 71 (`DEFAULT_RCV_WSCALE`): + +```rust +/// Default delayed ACK timeout in milliseconds (RFC 9293 §4.2: < 500ms). +pub const DEFAULT_DELAYED_ACK_MS: u64 = 40; + +/// Maximum consecutive unACKed segments before flushing (RFC 5681 §4.2). +pub const MAX_DELAYED_ACK_COUNT: u8 = 2; +``` + +Add fields to `TcpConfig` (after `time_wait_duration_ms` at line 82): + +```rust + /// If true, disable Nagle algorithm (send small segments immediately). Default: false. + pub tcp_no_delay: bool, + /// Maximum delay for ACKs in milliseconds. Default: 40. + pub delayed_ack_ms: u64, +``` + +Update `TcpConfig::default()` to include: + +```rust + tcp_no_delay: false, + delayed_ack_ms: DEFAULT_DELAYED_ACK_MS, +``` + +Add fields to `Tcb` (after `time_wait_duration` at line 185): + +```rust + // --- Delayed ACK --- + /// True when an ACK is owed but deferred. + pub ack_pending: bool, + /// Deadline for sending the deferred ACK. + pub delayed_ack_deadline: Option, + /// Count of consecutive unACKed segments (flush at MAX_DELAYED_ACK_COUNT). + pub ack_delay_count: u8, + + // --- Nagle algorithm --- + /// When true, the Nagle algorithm gates small sends. Disabled by TCP_NODELAY. + pub nagle_enabled: bool, +``` + +In `src/net/handler/tcp/mod.rs`: + +Add to the import line 30 (the `use tcb::...` line), add `DEFAULT_DELAYED_ACK_MS`: + +```rust +use tcb::{ConnectionId, Tcb, TcpConfig, TcpEvent, DEFAULT_RCV_MSS, DEFAULT_RCV_WND, DEFAULT_RCV_WSCALE, DEFAULT_DELAYED_ACK_MS}; +``` + +Add fields to `ListenEntry` (after `time_wait_duration` at line 47): + +```rust + pub tcp_no_delay: bool, + pub delayed_ack_ms: u64, +``` + +Update `listen_with_config` (after `time_wait_duration: config.time_wait_duration_ms` at line 109): + +```rust + tcp_no_delay: config.tcp_no_delay, + delayed_ack_ms: config.delayed_ack_ms, +``` + +Update the active-open TCB init (connect, after `time_wait_duration` at line 210): + +```rust + ack_pending: false, + delayed_ack_deadline: None, + ack_delay_count: 0, + nagle_enabled: !config.tcp_no_delay, +``` + +Update the passive-open TCB init (SYN-RECEIVED, after `time_wait_duration` at line 605): + +```rust + ack_pending: false, + delayed_ack_deadline: None, + ack_delay_count: 0, + nagle_enabled: !listener.tcp_no_delay, +``` + +Note: For the passive-open TCB, the values come from `listener.tcp_no_delay` and `listener.delayed_ack_ms`. The `delayed_ack_ms` is used when setting the deadline (Task 2), not at init time. + +**Step 4: Run test to verify it passes** + +Run: `cargo test new_connection_has_delayed_ack_fields` +Expected: PASS + +**Step 5: Commit** + +```bash +git add src/net/handler/tcp/tcb.rs src/net/handler/tcp/mod.rs +git commit -m "feat: add delayed ACK and Nagle fields to TCB and TcpConfig" +``` + +--- + +### Task 2: Delayed ACK in process_established + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs` (process_established method + ListenEntry for delayed_ack_ms propagation) + +**Context:** + +Currently `process_established` (around line 860) sends an immediate ACK for every in-order data segment (line ~965). We need to change this to set `ack_pending` instead, and only send immediate ACKs for out-of-order data, duplicate data, and FIN. + +The `process_established` method receives `now: Instant` as a parameter, which we use to compute the deadline. + +The delayed ACK timeout value needs to be stored on the TCB so it can be checked in `poll_timers`. Add `delayed_ack_ms: u64` to the TCB, initialized from config/listener. + +**Step 1: Write the failing tests** + +Add to the test module in `src/net/handler/tcp/mod.rs`: + +```rust +#[test] +fn delayed_ack_defers_ack_for_in_order_data() { + let (mut handler, mut free, mut rx, mut tx) = setup(); + let now = Instant::now(); + + // Complete handshake. + let _accept = handler.listen(IpAddress::V4(LOCAL_IP), LOCAL_PORT, 128).unwrap(); + let syn = build_syn_frame(REMOTE_IP, LOCAL_IP, REMOTE_PORT, LOCAL_PORT, 1000); + handler.process_ipv4_with_now(syn, now, &NH, &mut free, &mut rx, &mut tx); + let _ = tx.pop(); // SYN-ACK + let ack = build_ack_frame(REMOTE_IP, LOCAL_IP, REMOTE_PORT, LOCAL_PORT, 1001, 1); + handler.process_ipv4_with_now(ack, now, &NH, &mut free, &mut rx, &mut tx); + + // Send in-order data — should NOT produce an immediate ACK. + let data = build_data_frame(REMOTE_IP, LOCAL_IP, REMOTE_PORT, LOCAL_PORT, 1001, 1, b"hello"); + handler.process_ipv4_with_now(data, now, &NH, &mut free, &mut rx, &mut tx); + + assert_eq!(tx.num_frames(), 0, "delayed ACK should not send immediately"); + + let id = ConnectionId { + local_addr: IpAddress::V4(LOCAL_IP), + local_port: LOCAL_PORT, + remote_addr: IpAddress::V4(REMOTE_IP), + remote_port: REMOTE_PORT, + }; + let tcb = handler.get_connection(&id).unwrap(); + assert!(tcb.ack_pending); + assert!(tcb.delayed_ack_deadline.is_some()); + assert_eq!(tcb.ack_delay_count, 1); +} + +#[test] +fn delayed_ack_flushes_on_second_segment() { + let (mut handler, mut free, mut rx, mut tx) = setup(); + let now = Instant::now(); + + // Complete handshake. + let _accept = handler.listen(IpAddress::V4(LOCAL_IP), LOCAL_PORT, 128).unwrap(); + let syn = build_syn_frame(REMOTE_IP, LOCAL_IP, REMOTE_PORT, LOCAL_PORT, 1000); + handler.process_ipv4_with_now(syn, now, &NH, &mut free, &mut rx, &mut tx); + let _ = tx.pop(); + let ack = build_ack_frame(REMOTE_IP, LOCAL_IP, REMOTE_PORT, LOCAL_PORT, 1001, 1); + handler.process_ipv4_with_now(ack, now, &NH, &mut free, &mut rx, &mut tx); + + // First data segment — deferred. + let data1 = build_data_frame(REMOTE_IP, LOCAL_IP, REMOTE_PORT, LOCAL_PORT, 1001, 1, b"hello"); + handler.process_ipv4_with_now(data1, now, &NH, &mut free, &mut rx, &mut tx); + assert_eq!(tx.num_frames(), 0); + + // Second data segment — triggers immediate ACK (ack_delay_count >= 2). + let data2 = build_data_frame(REMOTE_IP, LOCAL_IP, REMOTE_PORT, LOCAL_PORT, 1006, 1, b"world"); + handler.process_ipv4_with_now(data2, now, &NH, &mut free, &mut rx, &mut tx); + assert_eq!(tx.num_frames(), 1, "second segment should flush ACK"); + + let id = ConnectionId { + local_addr: IpAddress::V4(LOCAL_IP), + local_port: LOCAL_PORT, + remote_addr: IpAddress::V4(REMOTE_IP), + remote_port: REMOTE_PORT, + }; + let tcb = handler.get_connection(&id).unwrap(); + assert!(!tcb.ack_pending); + assert_eq!(tcb.ack_delay_count, 0); +} + +#[test] +fn out_of_order_data_sends_immediate_ack() { + let (mut handler, mut free, mut rx, mut tx) = setup(); + let now = Instant::now(); + + // Complete handshake. + let _accept = handler.listen(IpAddress::V4(LOCAL_IP), LOCAL_PORT, 128).unwrap(); + let syn = build_syn_frame(REMOTE_IP, LOCAL_IP, REMOTE_PORT, LOCAL_PORT, 1000); + handler.process_ipv4_with_now(syn, now, &NH, &mut free, &mut rx, &mut tx); + let _ = tx.pop(); + let ack = build_ack_frame(REMOTE_IP, LOCAL_IP, REMOTE_PORT, LOCAL_PORT, 1001, 1); + handler.process_ipv4_with_now(ack, now, &NH, &mut free, &mut rx, &mut tx); + + // Send out-of-order data (skip seq 1001, send 1006). + let data = build_data_frame(REMOTE_IP, LOCAL_IP, REMOTE_PORT, LOCAL_PORT, 1006, 1, b"world"); + handler.process_ipv4_with_now(data, now, &NH, &mut free, &mut rx, &mut tx); + + // Out-of-order MUST send immediate duplicate ACK (for fast retransmit). + assert_eq!(tx.num_frames(), 1, "OOO data must ACK immediately"); +} + +#[test] +fn fin_sends_immediate_ack() { + let (mut handler, mut free, mut rx, mut tx) = setup(); + let now = Instant::now(); + + // Complete handshake. + let _accept = handler.listen(IpAddress::V4(LOCAL_IP), LOCAL_PORT, 128).unwrap(); + let syn = build_syn_frame(REMOTE_IP, LOCAL_IP, REMOTE_PORT, LOCAL_PORT, 1000); + handler.process_ipv4_with_now(syn, now, &NH, &mut free, &mut rx, &mut tx); + let _ = tx.pop(); + let ack = build_ack_frame(REMOTE_IP, LOCAL_IP, REMOTE_PORT, LOCAL_PORT, 1001, 1); + handler.process_ipv4_with_now(ack, now, &NH, &mut free, &mut rx, &mut tx); + + // Send FIN. + let fin = build_fin_frame(REMOTE_IP, LOCAL_IP, REMOTE_PORT, LOCAL_PORT, 1001, 1); + handler.process_ipv4_with_now(fin, now, &NH, &mut free, &mut rx, &mut tx); + + // FIN must be ACKed immediately. + assert_eq!(tx.num_frames(), 1, "FIN must be ACKed immediately"); +} +``` + +**Important:** The test helpers `build_data_frame` and `build_fin_frame` should already exist in the test module (they were created during data transfer and teardown implementation). If `build_fin_frame` doesn't exist, you'll need to create it — it's the same as `build_ack_frame` but with the FIN flag set. Check the existing test helpers before adding new ones. + +**Step 2: Run tests to verify they fail** + +Run: `cargo test delayed_ack_defers_ack_for_in_order_data` +Expected: FAIL — ACK is still sent immediately (tx.num_frames() == 1, not 0) + +**Step 3: Implement delayed ACK** + +Add `delayed_ack_ms: u64` field to `Tcb` (after `nagle_enabled`): + +```rust + /// Delayed ACK timeout in milliseconds. + pub delayed_ack_ms: u64, +``` + +Add `delayed_ack_ms` to both TCB initialization sites: +- Active open: `delayed_ack_ms: DEFAULT_DELAYED_ACK_MS,` +- Passive open: `delayed_ack_ms: listener.delayed_ack_ms,` + +Import `MAX_DELAYED_ACK_COUNT` in the use line at the top of `mod.rs`: + +```rust +use tcb::{..., DEFAULT_DELAYED_ACK_MS, MAX_DELAYED_ACK_COUNT}; +``` + +In `process_established`, replace the in-order data ACK (around line 963-972) with: + +```rust + // Defer ACK (delayed ACK). + let tcb = &mut self.connections[idx]; + tcb.ack_delay_count += 1; + if tcb.ack_delay_count >= MAX_DELAYED_ACK_COUNT { + // Flush: ACK every other segment (RFC 5681 §4.2). + SegmentBuilder::build_ack( + tcb.id.local_addr, tcb.id.remote_addr, + tcb.id.local_port, tcb.id.remote_port, + tcb.snd_nxt, tcb.rcv_nxt, + DEFAULT_RCV_WND, + src_mac, dst_mac, + self.tx_offload, free_frames, tx_return, + ); + tcb.ack_pending = false; + tcb.ack_delay_count = 0; + tcb.delayed_ack_deadline = None; + } else { + tcb.ack_pending = true; + if tcb.delayed_ack_deadline.is_none() { + tcb.delayed_ack_deadline = Some(now + coarsetime::Duration::from_millis(tcb.delayed_ack_ms)); + } + } +``` + +Keep the out-of-order ACK (line ~982) and duplicate ACK (line ~993) as immediate — do NOT change those. + +Keep the FIN ACK (line ~1016) as immediate — do NOT change that. + +**Step 4: Run tests to verify they pass** + +Run: `cargo test delayed_ack` +Expected: All 4 delayed ACK tests PASS + +Run: `cargo test` +Expected: Some existing tests may fail because they expected immediate ACKs for in-order data. These tests need to be updated to either: +- Send two data segments (to trigger flush), or +- Call `poll_timers` with a future `now` to flush delayed ACKs, or +- Check `ack_pending` instead of `tx.num_frames()` + +Fix any broken tests by adjusting expectations. The most common fix is: where a test sent one data segment and expected an ACK in `tx_return`, either send a second segment or advance time past the deadline and call `poll_timers`. + +**Step 5: Commit** + +```bash +git add src/net/handler/tcp/mod.rs src/net/handler/tcp/tcb.rs +git commit -m "feat: implement delayed ACK for TCP" +``` + +--- + +### Task 3: Delayed ACK Timer in poll_timers + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs` (poll_timers method) + +**Context:** + +`poll_timers` (line ~1032) currently handles fast retransmit and RTO retransmit. We need to add a delayed ACK flush pass: for any connection with `ack_pending == true` and `now >= delayed_ack_deadline`, send an ACK and clear the flag. + +This should run BEFORE the fast retransmit and RTO passes, as it's a simpler check. + +**Step 1: Write the failing test** + +```rust +#[test] +fn delayed_ack_timer_flushes_pending_ack() { + let (mut handler, mut free, mut rx, mut tx) = setup(); + let now = Instant::now(); + + // Complete handshake. + let _accept = handler.listen(IpAddress::V4(LOCAL_IP), LOCAL_PORT, 128).unwrap(); + let syn = build_syn_frame(REMOTE_IP, LOCAL_IP, REMOTE_PORT, LOCAL_PORT, 1000); + handler.process_ipv4_with_now(syn, now, &NH, &mut free, &mut rx, &mut tx); + let _ = tx.pop(); + let ack = build_ack_frame(REMOTE_IP, LOCAL_IP, REMOTE_PORT, LOCAL_PORT, 1001, 1); + handler.process_ipv4_with_now(ack, now, &NH, &mut free, &mut rx, &mut tx); + + // Send one data segment — deferred. + let data = build_data_frame(REMOTE_IP, LOCAL_IP, REMOTE_PORT, LOCAL_PORT, 1001, 1, b"hello"); + handler.process_ipv4_with_now(data, now, &NH, &mut free, &mut rx, &mut tx); + assert_eq!(tx.num_frames(), 0); + + // Advance time past delayed ACK deadline (40ms). + let later = now + coarsetime::Duration::from_millis(50); + handler.poll_timers(later, SRC_MAC, &NH, &mut free, &mut tx); + + assert_eq!(tx.num_frames(), 1, "timer should flush delayed ACK"); + + let id = ConnectionId { + local_addr: IpAddress::V4(LOCAL_IP), + local_port: LOCAL_PORT, + remote_addr: IpAddress::V4(REMOTE_IP), + remote_port: REMOTE_PORT, + }; + let tcb = handler.get_connection(&id).unwrap(); + assert!(!tcb.ack_pending); + assert_eq!(tcb.ack_delay_count, 0); +} +``` + +**Step 2: Run test to verify it fails** + +Run: `cargo test delayed_ack_timer_flushes_pending_ack` +Expected: FAIL — tx.num_frames() == 0 (no delayed ACK flush logic yet) + +**Step 3: Implement delayed ACK timer** + +In `poll_timers`, add a delayed ACK pass at the very beginning (before the fast retransmit loop at line ~1042): + +```rust + // Delayed ACK pass — flush pending ACKs whose deadline has expired. + for tcb in &mut self.connections { + if !tcb.ack_pending { + continue; + } + if let Some(deadline) = tcb.delayed_ack_deadline { + if now >= deadline { + let id = tcb.id; + let dst_mac = neighbor_handler + .lookup(now, &id.remote_addr) + .unwrap_or(crate::net::wire::ethernet::MacAddress::broadcast()); + let window = tcb.recv_buffer.free_space().min(u16::MAX as usize) as u16; + + SegmentBuilder::build_ack( + id.local_addr, id.remote_addr, + id.local_port, id.remote_port, + tcb.snd_nxt, tcb.rcv_nxt, window, + src_mac, dst_mac, + self.tx_offload, free_frames, tx_return, + ); + tcb.ack_pending = false; + tcb.ack_delay_count = 0; + tcb.delayed_ack_deadline = None; + } + } + } +``` + +**Step 4: Run tests to verify they pass** + +Run: `cargo test delayed_ack_timer` +Expected: PASS + +Run: `cargo test` +Expected: All tests pass + +**Step 5: Commit** + +```bash +git add src/net/handler/tcp/mod.rs +git commit -m "feat: add delayed ACK timer flush in poll_timers" +``` + +--- + +### Task 4: Nagle Algorithm in poll_send + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs` (poll_send method) + +**Context:** + +`poll_send` (line ~1197) currently sends any available data immediately. We need to add the Nagle gate: if `nagle_enabled` and there are bytes in flight and the data to send is less than MSS, don't send — wait for the outstanding ACK. + +Also, when `poll_send` sends a data segment (which carries ACK flag), it should clear `ack_pending` since the ACK piggybacks on the data. + +**Step 1: Write the failing tests** + +```rust +#[test] +fn nagle_holds_small_data_when_bytes_in_flight() { + let (mut handler, mut free, mut rx, mut tx) = setup(); + let now = Instant::now(); + + // Complete handshake via active open. + let event_queue = handler.connect( + IpAddress::V4(LOCAL_IP), LOCAL_PORT, + IpAddress::V4(REMOTE_IP), REMOTE_PORT, + SRC_MAC, DST_MAC, + &mut free, &mut tx, + ).unwrap(); + let _ = tx.pop(); // SYN + + let syn_ack = build_syn_ack_frame(REMOTE_IP, LOCAL_IP, REMOTE_PORT, LOCAL_PORT, 2000, 1); + handler.process_ipv4_with_now(syn_ack, now, &NH, &mut free, &mut rx, &mut tx); + let _ = tx.pop(); // ACK of SYN-ACK + + let id = ConnectionId { + local_addr: IpAddress::V4(LOCAL_IP), + local_port: LOCAL_PORT, + remote_addr: IpAddress::V4(REMOTE_IP), + remote_port: REMOTE_PORT, + }; + + // Write small data (less than MSS). + { + let tcb = handler.get_connection_mut(&id).unwrap(); + tcb.send_buffer.write(b"hello"); + } + + // First poll_send: nothing in flight → sends immediately. + handler.poll_send(now, SRC_MAC, &NH, &mut free, &mut tx); + assert_eq!(tx.num_frames(), 1, "first send should go (nothing in flight)"); + let _ = tx.pop(); + + // Write more small data while first is still in flight (unACKed). + { + let tcb = handler.get_connection_mut(&id).unwrap(); + tcb.send_buffer.write(b"world"); + } + + // Second poll_send: bytes in flight + small data → Nagle holds it. + handler.poll_send(now, SRC_MAC, &NH, &mut free, &mut tx); + assert_eq!(tx.num_frames(), 0, "Nagle should hold small data when bytes in flight"); +} + +#[test] +fn nagle_allows_full_mss_even_with_bytes_in_flight() { + let (mut handler, mut free, mut rx, mut tx) = setup(); + let now = Instant::now(); + + // Complete handshake via active open. + let _event_queue = handler.connect( + IpAddress::V4(LOCAL_IP), LOCAL_PORT, + IpAddress::V4(REMOTE_IP), REMOTE_PORT, + SRC_MAC, DST_MAC, + &mut free, &mut tx, + ).unwrap(); + let _ = tx.pop(); // SYN + + let syn_ack = build_syn_ack_frame(REMOTE_IP, LOCAL_IP, REMOTE_PORT, LOCAL_PORT, 2000, 1); + handler.process_ipv4_with_now(syn_ack, now, &NH, &mut free, &mut rx, &mut tx); + let _ = tx.pop(); // ACK + + let id = ConnectionId { + local_addr: IpAddress::V4(LOCAL_IP), + local_port: LOCAL_PORT, + remote_addr: IpAddress::V4(REMOTE_IP), + remote_port: REMOTE_PORT, + }; + + // Write small data first to create bytes_in_flight. + { + let tcb = handler.get_connection_mut(&id).unwrap(); + tcb.send_buffer.write(b"small"); + } + handler.poll_send(now, SRC_MAC, &NH, &mut free, &mut tx); + let _ = tx.pop(); + + // Write MSS-worth of data. + let tcb = handler.get_connection_mut(&id).unwrap(); + let mss = tcb.eff_snd_mss as usize; + let big_data = vec![0xABu8; mss]; + tcb.send_buffer.write(&big_data); + drop(tcb); + + // poll_send: full MSS → always sends (Nagle allows it). + handler.poll_send(now, SRC_MAC, &NH, &mut free, &mut tx); + assert_eq!(tx.num_frames(), 1, "full MSS should always send"); +} + +#[test] +fn tcp_no_delay_sends_small_data_immediately() { + let (mut handler, mut free, mut rx, mut tx) = setup(); + let now = Instant::now(); + + let config = TcpConfig { + tcp_no_delay: true, + ..TcpConfig::default() + }; + + // Complete handshake via active open with config. + let _event_queue = handler.connect_with_config( + IpAddress::V4(LOCAL_IP), LOCAL_PORT, + IpAddress::V4(REMOTE_IP), REMOTE_PORT, + SRC_MAC, DST_MAC, + config, + &mut free, &mut tx, + ).unwrap(); + let _ = tx.pop(); // SYN + + let syn_ack = build_syn_ack_frame(REMOTE_IP, LOCAL_IP, REMOTE_PORT, LOCAL_PORT, 2000, 1); + handler.process_ipv4_with_now(syn_ack, now, &NH, &mut free, &mut rx, &mut tx); + let _ = tx.pop(); // ACK + + let id = ConnectionId { + local_addr: IpAddress::V4(LOCAL_IP), + local_port: LOCAL_PORT, + remote_addr: IpAddress::V4(REMOTE_IP), + remote_port: REMOTE_PORT, + }; + + // Write small data. + { + let tcb = handler.get_connection_mut(&id).unwrap(); + tcb.send_buffer.write(b"hello"); + } + handler.poll_send(now, SRC_MAC, &NH, &mut free, &mut tx); + let _ = tx.pop(); // First send goes (nothing in flight) + + // Write more small data while first in flight. + { + let tcb = handler.get_connection_mut(&id).unwrap(); + tcb.send_buffer.write(b"world"); + } + handler.poll_send(now, SRC_MAC, &NH, &mut free, &mut tx); + assert_eq!(tx.num_frames(), 1, "TCP_NODELAY should bypass Nagle"); +} +``` + +**Step 2: Run tests to verify they fail** + +Run: `cargo test nagle_holds_small_data` +Expected: FAIL — Nagle gate not implemented yet, small data sends immediately + +**Step 3: Implement Nagle gate and ACK piggybacking** + +In `poll_send`, after computing `to_send` (around line 1218), add the Nagle gate: + +```rust + if can_send > 0 && data_available > 0 { + let to_send = can_send.min(data_available).min(tcb.eff_snd_mss as usize); + + // Nagle algorithm: hold small segments when data is in flight. + if tcb.nagle_enabled && bytes_in_flight > 0 && to_send < tcb.eff_snd_mss as usize { + // Don't send — wait for outstanding ACK to come back. + } else { + // ... existing send logic (peek, build_data, advance snd_nxt, etc.) ... + + // Piggyback: data segment carries ACK, so clear delayed ACK state. + tcb.ack_pending = false; + tcb.ack_delay_count = 0; + tcb.delayed_ack_deadline = None; + } + } +``` + +Restructure: wrap the existing send body in the `else` branch and add the piggyback clearing after the `SegmentBuilder::build_data` call. + +**Step 4: Run tests to verify they pass** + +Run: `cargo test nagle` +Run: `cargo test tcp_no_delay` +Expected: All 3 Nagle tests PASS + +Run: `cargo test` +Expected: All tests pass + +**Step 5: Commit** + +```bash +git add src/net/handler/tcp/mod.rs +git commit -m "feat: implement Nagle algorithm with TCP_NODELAY support" +``` + +--- + +### Task 5: TcpStream::set_nodelay API + +**Files:** +- Modify: `src/net/socket/tcp.rs` + +**Context:** + +`TcpStream` at `src/net/socket/tcp.rs:161` holds a `conn_id` and a `handler: Rc>`. It already has methods like `close()` that access the handler mutably. + +We need `set_nodelay(&self, nodelay: bool)` that looks up the TCB and flips `nagle_enabled`. + +**Step 1: Write the method** + +Note: This is a socket-layer method. It cannot be unit-tested without the full runtime context (which requires root + real NIC). We verify it compiles and is consistent with existing methods. + +Add to `TcpStream` impl block (after `close` method, around line 349): + +```rust + /// Enable or disable the Nagle algorithm (TCP_NODELAY). + /// + /// When `nodelay` is `true`, small segments are sent immediately + /// without waiting for outstanding ACKs. Default is `false` (Nagle enabled). + pub fn set_nodelay(&self, nodelay: bool) { + let handler = unsafe { &mut *self.handler.get() }; + if let Some(tcb) = handler.get_connection_mut(&self.conn_id) { + tcb.nagle_enabled = !nodelay; + } + } + + /// Returns whether TCP_NODELAY is set (Nagle disabled). + pub fn nodelay(&self) -> bool { + let handler = unsafe { &*self.handler.get() }; + handler + .get_connection(&self.conn_id) + .map(|tcb| !tcb.nagle_enabled) + .unwrap_or(false) + } +``` + +Update the public exports in `src/net/socket/mod.rs` if needed (the existing `pub use tcp::...` line should already cover `TcpStream`). + +**Step 2: Verify it compiles** + +Run: `cargo build --features local` +Expected: Compiles successfully + +**Step 3: Commit** + +```bash +git add src/net/socket/tcp.rs +git commit -m "feat: add TcpStream::set_nodelay() API" +``` diff --git a/docs/plans/2026-03-06-tcp-echo-examples-design.md b/docs/plans/2026-03-06-tcp-echo-examples-design.md new file mode 100644 index 0000000..6401c7d --- /dev/null +++ b/docs/plans/2026-03-06-tcp-echo-examples-design.md @@ -0,0 +1,75 @@ +# TCP Echo Examples — End-to-End Validation Design + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Prove the TCP stack works end-to-end on real hardware by writing echo server and client examples. + +**Architecture:** Two standalone examples (`tcp-echo-server.rs`, `tcp-echo-client.rs`) mirroring the existing UDP example pattern. No library code changes — pure validation of existing TCP implementation. + +**Tech Stack:** Existing `libvoid` TCP socket API (`TcpListener`, `TcpStream`), `clap` for CLI args, `ctrlc` for signal handling. + +--- + +## TCP Echo Server (`examples/tcp-echo-server.rs`) + +**Args:** +- All `BaseArgs` (interface, queue, XDP options) +- `--local-addr` — listen address (default `[fc00:dead:cafe:1::1]:8080`) + +**Behavior:** +1. Build `LocalRuntime` from args +2. Set up Ctrl-C exit flag +3. Inside `runtime.run()`: + - `TcpListener::listen(addr, port)` + - Loop: `let stream = listener.accept().await` + - Per connection: read into buffer, echo back, track stats + - On read returning 0 (EOF): log disconnect, accept next connection +4. Single-connection handling (no concurrent task spawning) + +**Stats:** Reuse existing `Stats` struct — bytes echoed, periodic printing. + +## TCP Echo Client (`examples/tcp-echo-client.rs`) + +**Args:** +- All `BaseArgs` (interface, queue, XDP options) +- `--local-addr` — client bind address (default `[fc00:dead:cafe:1::2]:8080`) +- `--remote-addr` — server address (default `[fc00:dead:cafe:1::1]:8080`) +- `--message-size` — payload size in bytes (default 64) + +**Behavior:** +1. Build `LocalRuntime` from args +2. Set up Ctrl-C exit flag +3. Inside `runtime.run()`: + - `TcpStream::connect(local, local_port, remote, remote_port)?.await?` + - Loop: write fixed-size payload → read echo → verify length → update stats + - On Ctrl-C: stream drops, triggers graceful FIN close + +**Payload:** Fixed-size buffer allocated once. No content verification — length check only. + +**Stats:** Same `Stats` struct, tracking round-trips and bytes. + +## What This Validates + +**Exercised paths:** +- Full 3-way handshake (SYN → SYN-ACK → ACK) +- Bidirectional data transfer through ring buffers +- Flow control (send/receive windows) +- Congestion control (slow start ramp-up) +- RTT estimation under real latency +- Graceful teardown on drop (FIN/ACK exchange) +- Frame accounting (`debug_assert` in runtime loop) +- Checksum computation (or offload) on real NIC + +**Not exercised (deferred):** +- Retransmission under packet loss +- Out-of-order reassembly +- Multiple concurrent connections +- Half-close, keep-alive, linger + +## Scope + +Two new files only: +- `examples/tcp-echo-server.rs` +- `examples/tcp-echo-client.rs` + +No library code changes. diff --git a/docs/plans/2026-03-06-tcp-echo-examples-impl.md b/docs/plans/2026-03-06-tcp-echo-examples-impl.md new file mode 100644 index 0000000..62417ba --- /dev/null +++ b/docs/plans/2026-03-06-tcp-echo-examples-impl.md @@ -0,0 +1,356 @@ +# TCP Echo Examples Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Write TCP echo server and client examples to validate the TCP stack end-to-end on real hardware. + +**Architecture:** Two standalone example binaries mirroring the existing `udp-server.rs`/`udp-client.rs` pattern. Both use `BaseArgs` from `examples/common/mod.rs`, `LocalRuntime::builder()`, `ctrlc` for Ctrl-C, and the `libvoid` TCP socket API. No library code changes. + +**Tech Stack:** Rust, `clap` (CLI args), `ctrlc` (signal handling), `libvoid` (`TcpListener`, `TcpStream`, `LocalRuntime`) + +--- + +### Task 1: TCP Echo Server + +**Files:** +- Create: `examples/tcp-echo-server.rs` +- Reference: `examples/udp-server.rs` (pattern to follow) +- Reference: `examples/common/mod.rs` (`BaseArgs`, `Stats`) +- Reference: `src/net/socket/tcp.rs` (`TcpListener`, `TcpStream` API) +- Reference: `src/net/socket/mod.rs` (public exports) + +**Context:** + +The existing UDP server pattern is: +1. Parse `Args` with `#[derive(Parser)]` containing `#[command(flatten)] base: BaseArgs` plus custom args +2. Implement `Deref`/`DerefMut` to `BaseArgs` so `args.if_name`, `args.queue`, etc. work directly +3. Build `LocalRuntime` from all base args +4. Set up `ctrlc` handler with `Arc` +5. Call `runtime.run(exit, async move { ... })` + +The TCP socket API: +- `TcpListener::listen(addr: IpAddress, port: u16) -> Result` +- `listener.accept() -> Accept` (future resolving to `TcpStream`) +- `stream.read(buf: &mut [u8]) -> TcpRead` (future resolving to `usize`, 0 = EOF) +- `stream.write(data: &[u8]) -> TcpWrite` (future resolving to `usize`) +- `stream.remote_addr() -> IpAddress` and `stream.remote_port() -> u16` +- Stream drops trigger graceful FIN close automatically + +**Step 1: Write the server example** + +Create `examples/tcp-echo-server.rs` with this content: + +```rust +use std::{ + ops::{Deref, DerefMut}, + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, +}; + +use clap::Parser; +use coarsetime::Duration; + +use libvoid::net::{ + socket::TcpListener, + wire::ip::SocketAddr, +}; +use libvoid::rt::LocalRuntime; + +mod common; +use common::{BaseArgs, Stats}; + +#[derive(Parser)] +#[command(author, version, about = "TCP echo server", long_about = None)] +struct Args { + #[command(flatten)] + base: BaseArgs, + #[arg(short, long, default_value = "[fc00:dead:cafe:1::1]:8080")] + local_addr: SocketAddr, +} + +impl Deref for Args { + type Target = BaseArgs; + + fn deref(&self) -> &Self::Target { + &self.base + } +} + +impl DerefMut for Args { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.base + } +} + +fn main() { + let mut stats = Stats::new(); + let args = Args::parse(); + + let mut runtime = LocalRuntime::builder(&args.if_name, args.queue) + .arp_ttl(Duration::from_secs(1200)) + .attach_mode(args.attach_mode) + .enable_fragmentation(args.enable_fragmentation) + .completion_ring_size(args.completion_ring_size) + .fill_ring_size(args.fill_ring_size) + .frame_size(args.frame_size) + .busy_poll(args.busy_poll) + .busy_poll_batch_size(args.busy_poll_batch_size) + .busy_poll_timeout_us(args.busy_poll_timeout_us) + .huge_tables(args.huge_tables) + .unaligned(args.unaligned) + .rx_ring_size(args.rx_ring_size) + .tx_ring_size(args.tx_ring_size) + .copy_mode(args.copy_mode) + .build() + .expect("Failed to create runtime"); + + let exit = Arc::new(AtomicBool::new(false)); + ctrlc::set_handler({ + let exit = exit.clone(); + move || { + exit.store(true, Ordering::Relaxed); + } + }) + .expect("Error setting Ctrl-C handler"); + + runtime + .run(exit, async move { + let listener = TcpListener::listen(args.local_addr.ip, args.local_addr.port) + .expect("Failed to listen"); + println!("Listening on {}", args.local_addr); + + loop { + let stream = listener.accept().await; + println!( + "Accepted connection from {}:{}", + stream.remote_addr(), + stream.remote_port() + ); + + let mut buf = [0u8; 4096]; + loop { + let n = stream.read(&mut buf).await; + if n == 0 { + println!( + "Connection closed from {}:{}", + stream.remote_addr(), + stream.remote_port() + ); + break; + } + stream.write(&buf[..n]).await; + stats.update(n, false); + stats.maybe_print(); + } + } + }) + .expect("Failed to run runtime"); + + println!("Exiting..."); +} +``` + +**Step 2: Verify it compiles** + +Run: `cargo build --example tcp-echo-server --features local` +Expected: Compiles successfully (may have warnings, no errors) + +**Step 3: Commit** + +```bash +git add examples/tcp-echo-server.rs +git commit -m "feat: add TCP echo server example" +``` + +--- + +### Task 2: TCP Echo Client + +**Files:** +- Create: `examples/tcp-echo-client.rs` +- Reference: `examples/udp-client.rs` (pattern to follow) +- Reference: `examples/common/mod.rs` (`BaseArgs`, `Stats`) +- Reference: `src/net/socket/tcp.rs` (`TcpStream::connect` API) + +**Context:** + +The TCP connect API: +- `TcpStream::connect(local_addr, local_port, remote_addr, remote_port) -> Result` +- `Connect` is a future resolving to `Result` +- Once connected, use `stream.write()` and `stream.read()` as in the server + +**Step 1: Write the client example** + +Create `examples/tcp-echo-client.rs` with this content: + +```rust +use std::{ + ops::{Deref, DerefMut}, + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, +}; + +use clap::Parser; +use coarsetime::Duration; + +use libvoid::net::{ + socket::TcpStream, + wire::ip::SocketAddr, +}; +use libvoid::rt::LocalRuntime; + +mod common; +use common::{BaseArgs, Stats}; + +#[derive(Parser)] +#[command(author, version, about = "TCP echo client", long_about = None)] +struct Args { + #[command(flatten)] + base: BaseArgs, + #[arg(short, long, default_value = "[fc00:dead:cafe:1::2]:8080")] + local_addr: SocketAddr, + #[arg(short, long, default_value = "[fc00:dead:cafe:1::1]:8080")] + remote_addr: SocketAddr, + #[arg(short, long, default_value = "64")] + message_size: usize, +} + +impl Deref for Args { + type Target = BaseArgs; + + fn deref(&self) -> &Self::Target { + &self.base + } +} + +impl DerefMut for Args { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.base + } +} + +fn main() { + let mut stats = Stats::new(); + let args = Args::parse(); + + let mut runtime = LocalRuntime::builder(&args.if_name, args.queue) + .arp_ttl(Duration::from_secs(1200)) + .attach_mode(args.attach_mode) + .enable_fragmentation(args.enable_fragmentation) + .completion_ring_size(args.completion_ring_size) + .fill_ring_size(args.fill_ring_size) + .frame_size(args.frame_size) + .busy_poll(args.busy_poll) + .busy_poll_batch_size(args.busy_poll_batch_size) + .busy_poll_timeout_us(args.busy_poll_timeout_us) + .huge_tables(args.huge_tables) + .unaligned(args.unaligned) + .rx_ring_size(args.rx_ring_size) + .tx_ring_size(args.tx_ring_size) + .copy_mode(args.copy_mode) + .build() + .expect("Failed to create runtime"); + + let exit = Arc::new(AtomicBool::new(false)); + ctrlc::set_handler({ + let exit = exit.clone(); + move || { + exit.store(true, Ordering::Relaxed); + } + }) + .expect("Error setting Ctrl-C handler"); + + let message_size = args.message_size; + + runtime + .run(exit, async move { + println!( + "Connecting from {} to {}...", + args.local_addr, args.remote_addr + ); + + let stream = TcpStream::connect( + args.local_addr.ip, + args.local_addr.port, + args.remote_addr.ip, + args.remote_addr.port, + ) + .expect("Failed to initiate connection") + .await + .expect("Connection failed"); + + println!( + "Connected to {}:{}", + stream.remote_addr(), + stream.remote_port() + ); + + let payload = vec![0xABu8; message_size]; + let mut read_buf = vec![0u8; message_size]; + + loop { + stream.write(&payload).await; + + let mut total_read = 0; + while total_read < message_size { + let n = stream.read(&mut read_buf[total_read..]).await; + if n == 0 { + println!("Server closed connection"); + return; + } + total_read += n; + } + + stats.update(total_read, false); + stats.maybe_print(); + } + }) + .expect("Failed to run runtime"); + + println!("Exiting..."); +} +``` + +**Step 2: Verify it compiles** + +Run: `cargo build --example tcp-echo-client --features local` +Expected: Compiles successfully (may have warnings, no errors) + +**Step 3: Commit** + +```bash +git add examples/tcp-echo-client.rs +git commit -m "feat: add TCP echo client example" +``` + +--- + +### Dependency Graph + +``` +Task 1 (server) ──┐ + ├── independent, can be done in parallel +Task 2 (client) ──┘ +``` + +Both tasks are independent — no shared code changes, no ordering dependency. + +### How to Test End-to-End + +After both examples compile, test on two machines (or two network namespaces) connected via XDP-compatible interfaces: + +**Terminal 1 (server):** +```bash +cargo run --example tcp-echo-server --features local -- --if-name eth0 --queue 0 --local-addr "[fc00:dead:cafe:1::1]:8080" +``` + +**Terminal 2 (client):** +```bash +cargo run --example tcp-echo-client --features local -- --if-name eth0 --queue 0 --local-addr "[fc00:dead:cafe:1::2]:8080" --remote-addr "[fc00:dead:cafe:1::1]:8080" --message-size 64 +``` + +Expected: Client connects, sends 64-byte messages, receives echoes, prints stats periodically. Ctrl-C on either side triggers graceful close. diff --git a/docs/plans/2026-03-06-tcp-socket-api-completeness-design.md b/docs/plans/2026-03-06-tcp-socket-api-completeness-design.md new file mode 100644 index 0000000..7dad6a5 --- /dev/null +++ b/docs/plans/2026-03-06-tcp-socket-api-completeness-design.md @@ -0,0 +1,143 @@ +# TCP Socket API Completeness Design + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Complete the TCP socket API with half-close, keep-alive probes, and SO_LINGER support. + +**Architecture:** Three independent features added to the existing TCP handler and socket layer. Half-close adds a write-side shutdown without closing reads. Keep-alive adds periodic probes for idle connection liveness detection. Linger controls close behavior (graceful vs RST abort). + +**Tech Stack:** Rust, coarsetime for timers, existing TCP handler infrastructure. + +--- + +## Half-Close (`shutdown`) + +### Current Behavior + +`TcpStream::close()` sets `pending_fin = true` and `self.closed = true`. Once closed, both reads and writes stop. `Drop` calls `close()`. There is no way to send FIN while continuing to read. + +### Design + +**New TcpStream field:** +- `write_closed: bool` (default `false`) — tracks whether the write side is shut down + +**New method: `TcpStream::shutdown()`** +- Sets `write_closed = true` +- Calls `handler.initiate_close(&self.conn_id)` — triggers `pending_fin`, which sends FIN via `poll_send` +- Does NOT set `self.closed = true` — the stream stays alive for reads + +**TcpWrite::poll() change:** +- If the stream's write side is closed, return `Ready(0)` immediately (no more writes) +- Requires passing `write_closed` state to the future + +**TcpRead::poll() unchanged:** +- Reads continue working until remote sends FIN (returns 0 = EOF) or connection is removed + +**close() unchanged:** +- Full close on both sides. If `shutdown()` was already called, `initiate_close` is a no-op (pending_fin already set). + +**Drop unchanged:** +- Calls `close()`. If shutdown was already called, it's a no-op for the FIN. + +**State machine interaction:** +- `shutdown()` triggers Established → FinWait1 (same as close) +- Stream stays alive for reads through FinWait1 → FinWait2 → TimeWait +- When remote FINs back, TcpRead returns 0 (EOF) + +## TCP Keep-Alive + +### Current Behavior + +Idle connections have no liveness probing. A silently dead peer (power failure, network partition) leaves the connection in Established forever. + +### Design + +**New TCB fields:** +- `keep_alive_enabled: bool` (default `false`) +- `keep_alive_idle_ms: u64` (default `7_200_000` = 2 hours per RFC 9293) +- `keep_alive_interval_ms: u64` (default `75_000` = 75 seconds) +- `keep_alive_count: u8` (default `9` probes before abort) +- `last_activity: Instant` — updated on every data send/receive +- `keep_alive_probes_sent: u8` (default `0`) — current probe counter + +**New TcpConfig fields:** +- `keep_alive: bool` (default `false`) +- `keep_alive_idle_ms: u64` (default `7_200_000`) +- `keep_alive_interval_ms: u64` (default `75_000`) +- `keep_alive_count: u8` (default `9`) + +**Keep-alive probe format (RFC 9293 §3.8.4):** +- Segment with `seq = snd_una - 1`, no data, ACK flag +- Peer responds with ACK containing its current `rcv_nxt` + +**Timer logic in `poll_timers`:** +- For Established connections with `keep_alive_enabled`: + - Compute idle time: `now - last_activity` + - If no probes sent yet and `idle >= keep_alive_idle_ms`: send first probe + - If probes already sent and `idle >= idle + interval * probes_sent`: send next probe + - If `keep_alive_probes_sent > keep_alive_count`: push `TcpEvent::Timeout`, mark for removal +- On receiving any data or ACK in `process_established`: reset `last_activity = now`, `keep_alive_probes_sent = 0` +- On sending data in `poll_send`: reset `last_activity = now` + +**Socket API:** +- `TcpStream::set_keepalive(&self, enabled: bool)` +- `TcpStream::keepalive(&self) -> bool` + +## SO_LINGER + +### Current Behavior + +`close()` always initiates graceful close — sets `pending_fin`, data drains asynchronously, FIN sent by `poll_send`. Non-blocking. + +### Design + +SO_LINGER has two useful modes for our cooperative executor: + +**Linger off (`None`, default):** Current behavior — graceful close, data drains in background. + +**Linger(0):** Hard reset — `initiate_close` sends RST immediately, discards send buffer, removes connection. Used for abort semantics. + +**Linger(timeout > 0):** Graceful close with deadline — `initiate_close` sets `pending_fin` and `linger_deadline`. If `poll_send` hasn't completed the FIN by the deadline, it sends RST and removes the connection. + +**New TCB fields:** +- `linger: Option` (None = off, Some(0) = RST, Some(ms) = timeout) +- `linger_deadline: Option` — set when linger timeout close is initiated + +**New TcpConfig field:** +- `linger: Option` (default `None`) + +**Handler changes:** +- `initiate_close`: if `linger == Some(0)`, send RST immediately via existing `remove_connection` (which sends RST for synchronized states), skip setting `pending_fin`. If `linger == Some(ms)`, set `pending_fin = true` and `linger_deadline = Some(now + ms)`. +- `poll_send`: after checking `pending_fin`, also check `linger_deadline`. If deadline expired and data still not drained, mark connection for RST removal instead of continuing to wait. + +**Socket API:** +- `TcpStream::set_linger(&self, linger: Option)` +- `TcpStream::linger(&self) -> Option` + +## Test Plan + +**Half-close tests:** +1. `shutdown()` sets `pending_fin` and `write_closed` +2. Writes return 0 after shutdown +3. Reads still work after shutdown (data received before remote FIN) +4. Full lifecycle: shutdown → remote FIN → read returns 0 + +**Keep-alive tests:** +5. Probe sent after idle timeout expires +6. Activity resets probe timer +7. Connection killed after max probes exceeded +8. No probes when keep_alive_enabled is false + +**Linger tests:** +9. Linger(0): RST sent immediately, connection removed +10. Linger(timeout): FIN sent normally if data drains in time +11. Linger(timeout) expired: RST sent, connection removed + +## Scope + +**Modified files:** +- `src/net/handler/tcp/tcb.rs` — new fields on Tcb and TcpConfig +- `src/net/handler/tcp/mod.rs` — process_established, poll_send, poll_timers, initiate_close changes +- `src/net/socket/tcp.rs` — shutdown(), set_keepalive(), set_linger(), write_closed tracking + +**No new files.** diff --git a/docs/plans/2026-03-06-tcp-socket-api-completeness-impl.md b/docs/plans/2026-03-06-tcp-socket-api-completeness-impl.md new file mode 100644 index 0000000..668a422 --- /dev/null +++ b/docs/plans/2026-03-06-tcp-socket-api-completeness-impl.md @@ -0,0 +1,798 @@ +# TCP Socket API Completeness Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Complete the TCP socket API with half-close (`shutdown`), keep-alive probes, and SO_LINGER support. + +**Architecture:** Three independent features layered onto the existing TCP handler and socket. Half-close adds write-side shutdown without closing reads. Keep-alive adds periodic probes for idle connection liveness. Linger controls close behavior (graceful vs RST abort). + +**Tech Stack:** Rust, coarsetime for timers, existing TCP handler infrastructure. + +--- + +## Task 1: Add Keep-Alive and Linger Fields to Tcb and TcpConfig + +**Files:** +- Modify: `src/net/handler/tcp/tcb.rs:80-106` (TcpConfig) and `src/net/handler/tcp/tcb.rs:108-212` (Tcb) + +**Step 1: Add new fields to TcpConfig** + +Add after the `delayed_ack_ms` field (line 92): + +```rust +pub struct TcpConfig { + pub send_buffer_size: usize, + pub recv_buffer_size: usize, + pub backlog: usize, + pub time_wait_duration_ms: u64, + pub tcp_no_delay: bool, + pub delayed_ack_ms: u64, + // --- NEW --- + /// Enable TCP keep-alive probes. Default: false. + pub keep_alive: bool, + /// Idle time before first keep-alive probe in milliseconds. Default: 7200000 (2 hours). + pub keep_alive_idle_ms: u64, + /// Interval between keep-alive probes in milliseconds. Default: 75000 (75 seconds). + pub keep_alive_interval_ms: u64, + /// Max probes before aborting connection. Default: 9. + pub keep_alive_count: u8, + /// SO_LINGER setting. None = off (default), Some(0) = RST, Some(ms) = timeout. + pub linger: Option, +} +``` + +Update `Default for TcpConfig`: + +```rust +impl Default for TcpConfig { + fn default() -> Self { + Self { + send_buffer_size: 256 * 1024, + recv_buffer_size: 256 * 1024, + backlog: 128, + time_wait_duration_ms: 60_000, + tcp_no_delay: false, + delayed_ack_ms: DEFAULT_DELAYED_ACK_MS, + keep_alive: false, + keep_alive_idle_ms: 7_200_000, + keep_alive_interval_ms: 75_000, + keep_alive_count: 9, + linger: None, + } + } +} +``` + +**Step 2: Add new fields to Tcb** + +Add after the `nagle_enabled` field (line 211), before the closing `}`: + +```rust + // --- Keep-alive --- + /// Whether keep-alive probes are enabled. + pub keep_alive_enabled: bool, + /// Idle time before first probe in milliseconds. + pub keep_alive_idle_ms: u64, + /// Interval between probes in milliseconds. + pub keep_alive_interval_ms: u64, + /// Max probes before aborting. + pub keep_alive_count: u8, + /// Timestamp of last data activity (send or receive). + pub last_activity: Instant, + /// Number of keep-alive probes sent since last activity. + pub keep_alive_probes_sent: u8, + + // --- Linger --- + /// SO_LINGER setting. None = off, Some(0) = RST, Some(ms) = timeout. + pub linger: Option, + /// Deadline for linger timeout (set when close is initiated with linger > 0). + pub linger_deadline: Option, +``` + +**Step 3: Update TCB initialization sites** + +Update active open TCB init in `src/net/handler/tcp/mod.rs:179-220`: + +Add after `nagle_enabled: !config.tcp_no_delay,` (line 219): + +```rust + keep_alive_enabled: config.keep_alive, + keep_alive_idle_ms: config.keep_alive_idle_ms, + keep_alive_interval_ms: config.keep_alive_interval_ms, + keep_alive_count: config.keep_alive_count, + last_activity: Instant::now(), + keep_alive_probes_sent: 0, + linger: config.linger, + linger_deadline: None, +``` + +Update passive open TCB init in `src/net/handler/tcp/mod.rs:579-619`: + +Add after `nagle_enabled: !listener.tcp_no_delay,` (line 619): + +```rust + keep_alive_enabled: false, + keep_alive_idle_ms: 7_200_000, + keep_alive_interval_ms: 75_000, + keep_alive_count: 9, + last_activity: Instant::now(), + keep_alive_probes_sent: 0, + linger: None, + linger_deadline: None, +``` + +Update `ListenEntry` in `src/net/handler/tcp/mod.rs:39-50` — add fields: + +```rust + pub keep_alive: bool, + pub keep_alive_idle_ms: u64, + pub keep_alive_interval_ms: u64, + pub keep_alive_count: u8, + pub linger: Option, +``` + +Update all `ListenEntry` construction sites (`listen` and `listen_with_config` methods) to populate these fields from config. + +Update passive open TCB init to read from listener: + +```rust + keep_alive_enabled: listener.keep_alive, + keep_alive_idle_ms: listener.keep_alive_idle_ms, + keep_alive_interval_ms: listener.keep_alive_interval_ms, + keep_alive_count: listener.keep_alive_count, + linger: listener.linger, +``` + +**Step 4: Build and fix any compilation errors** + +Run: `cargo build 2>&1 | head -50` +Expected: Clean compilation (all new fields initialized at both TCB creation sites). + +**Step 5: Run tests** + +Run: `cargo test 2>&1 | tail -20` +Expected: All existing tests pass (446 unit + 1 integration + 5 doc-tests). + +**Step 6: Commit** + +```bash +git add src/net/handler/tcp/tcb.rs src/net/handler/tcp/mod.rs +git commit -m "feat(tcp): add keep-alive and linger fields to Tcb and TcpConfig" +``` + +--- + +## Task 2: Implement Half-Close (shutdown) + +**Files:** +- Modify: `src/net/socket/tcp.rs:161-376` (TcpStream, TcpWrite) +- Test: `src/net/handler/tcp/mod.rs` (existing test module) + +**Step 1: Write failing tests** + +Add to the test module in `src/net/handler/tcp/mod.rs`: + +```rust +#[test] +fn shutdown_sets_pending_fin_and_write_closed() { + // Create a connected TcpStream, call shutdown() + // Verify: write_closed == true, pending_fin == true on TCB + // Verify: stream is NOT fully closed (closed == false) +} + +#[test] +fn write_returns_zero_after_shutdown() { + // Create a connected TcpStream, call shutdown() + // Call write() — poll should return Ready(0) +} + +#[test] +fn read_works_after_shutdown() { + // Create a connected TcpStream, call shutdown() + // Put data in recv_buffer + // Call read() — should return data +} +``` + +Note: These tests need to construct `TcpStream` directly or use the handler test infrastructure to simulate connections. Follow the pattern of existing tests in the file. + +**Step 2: Add `write_closed` field to TcpStream** + +In `src/net/socket/tcp.rs`, modify the `TcpStream` struct (line 161): + +```rust +pub struct TcpStream { + conn_id: ConnectionId, + #[allow(dead_code)] + event_queue: LocalQueue, + handler: Rc>, + closed: bool, + write_closed: bool, +} +``` + +Update `from_accepted` (line 279) to initialize `write_closed: false`. + +Update `Connect::poll` (line 394) to initialize `write_closed: false` in the `TcpStream` construction. + +**Step 3: Add `shutdown()` method to TcpStream** + +Add after `close()` (line 349): + +```rust + /// Shut down the write side of this connection (half-close). + /// + /// Sends FIN to the remote peer but keeps the read side open. + /// Subsequent writes will return 0. Reads continue until remote FIN. + pub fn shutdown(&mut self) { + if self.write_closed || self.closed { + return; + } + self.write_closed = true; + let handler = unsafe { &mut *self.handler.get() }; + handler.initiate_close(&self.conn_id); + } +``` + +**Step 4: Modify TcpWrite to check write_closed** + +Change `TcpStream::write()` to pass `write_closed`: + +```rust + pub fn write<'a>(&'a self, data: &'a [u8]) -> TcpWrite<'a> { + TcpWrite { + handler: &self.handler, + conn_id: self.conn_id, + data, + written: 0, + write_closed: self.write_closed || self.closed, + } + } +``` + +Add field to `TcpWrite`: + +```rust +pub struct TcpWrite<'stream> { + handler: &'stream Rc>, + conn_id: ConnectionId, + data: &'stream [u8], + written: usize, + write_closed: bool, +} +``` + +Add early return in `TcpWrite::poll()`: + +```rust + fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll { + let this = self.get_mut(); + if this.write_closed { + return Poll::Ready(0); + } + // ... rest unchanged + } +``` + +**Step 5: Build and run tests** + +Run: `cargo build 2>&1 | head -50` +Run: `cargo test 2>&1 | tail -20` +Expected: All tests pass including the new half-close tests. + +**Step 6: Commit** + +```bash +git add src/net/socket/tcp.rs src/net/handler/tcp/mod.rs +git commit -m "feat(tcp): implement half-close shutdown()" +``` + +--- + +## Task 3: Implement Keep-Alive Probes in poll_timers + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs:1058-1226` (poll_timers) +- Test: `src/net/handler/tcp/mod.rs` (test module) + +**Step 1: Write failing tests** + +```rust +#[test] +fn keep_alive_probe_sent_after_idle_timeout() { + // Create established connection with keep_alive_enabled = true, short idle timeout (e.g., 100ms) + // Advance time past idle timeout + // Call poll_timers + // Verify: a probe segment was sent (seq = snd_una - 1, no data, ACK flag) + // Verify: keep_alive_probes_sent incremented to 1 +} + +#[test] +fn keep_alive_no_probe_when_disabled() { + // Create established connection with keep_alive_enabled = false + // Advance time well past any timeout + // Call poll_timers + // Verify: no probe sent +} + +#[test] +fn keep_alive_connection_aborted_after_max_probes() { + // Create established connection with keep_alive_enabled = true + // Set keep_alive_count = 2, short timeouts + // Send 3 probes without response + // Verify: TcpEvent::Timeout pushed, connection removed +} +``` + +**Step 2: Add keep-alive probe logic to poll_timers** + +In `poll_timers`, after the delayed ACK pass (line 1091) and before the fast retransmit pass (line 1093), add: + +```rust + // Keep-alive probe pass — send probes for idle established connections. + let mut keep_alive_removals: Vec = Vec::new(); + for (i, tcb) in self.connections.iter_mut().enumerate() { + if tcb.state != TcpState::Established || !tcb.keep_alive_enabled { + continue; + } + + let idle_ms = now.duration_since(tcb.last_activity).as_millis(); + + let probe_threshold = if tcb.keep_alive_probes_sent == 0 { + tcb.keep_alive_idle_ms + } else { + tcb.keep_alive_idle_ms + tcb.keep_alive_interval_ms * tcb.keep_alive_probes_sent as u64 + }; + + if idle_ms >= probe_threshold { + if tcb.keep_alive_probes_sent >= tcb.keep_alive_count { + // Max probes exceeded — abort connection. + tcb.event_queue.push(TcpEvent::Timeout); + keep_alive_removals.push(i); + continue; + } + + // Send keep-alive probe: seq = snd_una - 1, no data, ACK. + let id = tcb.id; + let dst_mac = neighbor_handler + .lookup(now, &id.remote_addr) + .unwrap_or(crate::net::wire::ethernet::MacAddress::broadcast()); + let window = tcb.recv_buffer.free_space().min(u16::MAX as usize) as u16; + + SegmentBuilder::build_ack( + id.local_addr, id.remote_addr, + id.local_port, id.remote_port, + tcb.snd_una.wrapping_sub(1), tcb.rcv_nxt, window, + src_mac, dst_mac, + self.tx_offload, free_frames, tx_return, + ); + + tcb.keep_alive_probes_sent += 1; + } + } + + // Remove connections that exceeded keep-alive probes (reverse order). + for idx in keep_alive_removals.into_iter().rev() { + let id = self.connections[idx].id; + self.decrement_syn_received(&id); + self.connections.remove(idx); + } +``` + +**Step 3: Build and run tests** + +Run: `cargo build 2>&1 | head -50` +Run: `cargo test 2>&1 | tail -20` +Expected: All tests pass. + +**Step 4: Commit** + +```bash +git add src/net/handler/tcp/mod.rs +git commit -m "feat(tcp): implement keep-alive probes in poll_timers" +``` + +--- + +## Task 4: Reset Keep-Alive Timer on Activity + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs:866-1053` (process_established) and `src/net/handler/tcp/mod.rs:1250-1310` (poll_send data path) +- Test: `src/net/handler/tcp/mod.rs` (test module) + +**Step 1: Write failing test** + +```rust +#[test] +fn keep_alive_activity_resets_probe_timer() { + // Create established connection with keep_alive_enabled = true, short idle (100ms) + // Set keep_alive_probes_sent = 2, last_activity = old time + // Receive an ACK (process_established) + // Verify: last_activity updated to now, keep_alive_probes_sent reset to 0 +} +``` + +**Step 2: Add activity reset in process_established** + +In `process_established`, after the valid new ACK processing (around line 905, after `tcb.snd_una = seg_ack;`), add: + +```rust + // Reset keep-alive timer on activity. + tcb.last_activity = now; + tcb.keep_alive_probes_sent = 0; +``` + +Also after data is copied into recv_buffer (in-order data processing), add the same reset: + +```rust + // Reset keep-alive timer on received data. + tcb.last_activity = now; + tcb.keep_alive_probes_sent = 0; +``` + +**Step 3: Add activity reset in poll_send** + +In `poll_send`, after data is successfully sent (line 1297, after `tcb.snd_nxt = ...`), add: + +```rust + tcb.last_activity = now; + tcb.keep_alive_probes_sent = 0; +``` + +**Step 4: Build and run tests** + +Run: `cargo build 2>&1 | head -50` +Run: `cargo test 2>&1 | tail -20` +Expected: All tests pass. + +**Step 5: Commit** + +```bash +git add src/net/handler/tcp/mod.rs +git commit -m "feat(tcp): reset keep-alive timer on data activity" +``` + +--- + +## Task 5: Implement SO_LINGER in initiate_close and poll_send + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs:1357-1363` (initiate_close) and `src/net/handler/tcp/mod.rs:1312-1350` (poll_send FIN section) +- Test: `src/net/handler/tcp/mod.rs` (test module) + +**Step 1: Write failing tests** + +```rust +#[test] +fn linger_zero_sends_rst_immediately() { + // Create established connection + // Set linger = Some(0) on TCB + // Call initiate_close + // Verify: connection removed, RST sent (check tx_return for RST segment) +} + +#[test] +fn linger_timeout_sets_deadline() { + // Create established connection + // Set linger = Some(1000) on TCB + // Call initiate_close + // Verify: pending_fin set, linger_deadline set to now + 1000ms +} + +#[test] +fn linger_deadline_expired_sends_rst() { + // Create established connection with pending_fin, linger_deadline in the past + // Data still in send_buffer + // Call poll_send + // Verify: RST sent, connection removed +} +``` + +**Step 2: Modify initiate_close for linger** + +Replace `initiate_close` (line 1357): + +```rust + pub fn initiate_close(&mut self, id: &ConnectionId) { + let idx = match self.connections.iter().position(|c| c.id == *id) { + Some(i) => i, + None => return, + }; + + let tcb = &mut self.connections[idx]; + if tcb.pending_fin || (tcb.state != TcpState::Established && tcb.state != TcpState::CloseWait) { + return; + } + + match tcb.linger { + Some(0) => { + // Linger(0): immediate RST, discard data, remove connection. + tcb.event_queue.push(TcpEvent::Reset); + self.connections.remove(idx); + } + Some(ms) => { + // Linger(timeout): graceful close with deadline. + tcb.pending_fin = true; + tcb.linger_deadline = Some(Instant::now() + coarsetime::Duration::from_millis(ms)); + } + None => { + // Default: graceful close, no deadline. + tcb.pending_fin = true; + } + } + } +``` + +Note: For `linger(0)`, we don't send RST here directly because `initiate_close` doesn't have access to frame buffers. Instead, we mark the connection for removal and let the caller handle it. Alternative: we can use `remove_connection` which already sends RST. However, `initiate_close` doesn't have the frame buffer parameters. The simplest approach: just remove the connection and push a Reset event. The next `poll_send` won't find it. If a proper RST is needed, we need to refactor. For now, just removing the connection is sufficient — the remote will eventually time out or we can add RST sending in a follow-up. + +Actually, looking at the existing `remove_connection` method (line 1409), it takes frame buffers. Since `initiate_close` doesn't have them, we need a different approach for linger(0): + +Set a flag on the TCB and let `poll_send` handle the RST: + +```rust + pub fn initiate_close(&mut self, id: &ConnectionId) { + if let Some(tcb) = self.connections.iter_mut().find(|c| c.id == *id) { + if tcb.pending_fin || (tcb.state != TcpState::Established && tcb.state != TcpState::CloseWait) { + return; + } + + match tcb.linger { + Some(0) => { + // Linger(0): mark for RST abort in poll_send. + tcb.pending_fin = true; + tcb.linger_deadline = Some(Instant::recent()); // already expired = immediate RST + // Clear send buffer so poll_send sees no data and checks deadline. + tcb.send_buffer.clear(); + } + Some(ms) => { + tcb.pending_fin = true; + tcb.linger_deadline = Some(Instant::now() + coarsetime::Duration::from_millis(ms)); + } + None => { + tcb.pending_fin = true; + } + } + } + } +``` + +Wait — `RingBuffer` may not have a `clear()` method. Check and add one if needed, or use a different approach. + +Simpler approach: In `poll_send`, check `linger_deadline` before the FIN section. If the deadline has expired, send RST and mark for removal instead of sending FIN. + +**Step 3: Modify poll_send for linger deadline** + +In `poll_send`, before the existing `if tcb.pending_fin` block (line 1312), add a linger deadline check: + +```rust + // Check linger deadline — if expired, abort with RST. + if tcb.pending_fin { + if let Some(deadline) = tcb.linger_deadline { + if now >= deadline { + let id = tcb.id; + let dst_mac = neighbor_handler + .lookup(now, &id.remote_addr) + .unwrap_or(crate::net::wire::ethernet::MacAddress::broadcast()); + + SegmentBuilder::build_rst_for_connection( + id.local_addr, id.remote_addr, + id.local_port, id.remote_port, + tcb.snd_nxt, tcb.rcv_nxt, + src_mac, dst_mac, + self.tx_offload, free_frames, tx_return, + ); + + tcb.event_queue.push(TcpEvent::Reset); + // Mark for removal — handled below. + tcb.state = TcpState::Closed; + tcb.pending_fin = false; + continue; + } + } + } +``` + +Note: `SegmentBuilder::build_rst_for_connection` may not exist. Check the existing RST building API. The existing `build_rst` takes incoming segment parameters. We may need a simpler RST builder or use the ACK-based RST format. The implementer should check `segment.rs` for the available RST builders and use the appropriate one, or build a simple RST with `seq = snd_nxt`, `ACK` flag, `ack = rcv_nxt`. + +After the `poll_send` loop, add cleanup for Closed connections: + +```rust + // Remove connections marked Closed by linger abort. + self.connections.retain(|tcb| tcb.state != TcpState::Closed); +``` + +**Step 4: Update initiate_close for linger** + +```rust + pub fn initiate_close(&mut self, id: &ConnectionId) { + if let Some(tcb) = self.connections.iter_mut().find(|c| c.id == *id) { + if tcb.pending_fin || (tcb.state != TcpState::Established && tcb.state != TcpState::CloseWait) { + return; + } + + tcb.pending_fin = true; + match tcb.linger { + Some(0) => { + // Set deadline to now — poll_send will send RST immediately. + tcb.linger_deadline = Some(Instant::recent()); + } + Some(ms) => { + tcb.linger_deadline = Some(Instant::now() + coarsetime::Duration::from_millis(ms)); + } + None => { + // No deadline — graceful close. + } + } + } + } +``` + +**Step 5: Build and run tests** + +Run: `cargo build 2>&1 | head -50` +Run: `cargo test 2>&1 | tail -20` +Expected: All tests pass. + +**Step 6: Commit** + +```bash +git add src/net/handler/tcp/mod.rs +git commit -m "feat(tcp): implement SO_LINGER in initiate_close and poll_send" +``` + +--- + +## Task 6: Add Socket API Methods (keepalive, linger, shutdown tests) + +**Files:** +- Modify: `src/net/socket/tcp.rs` (TcpStream methods) +- Test: `src/net/handler/tcp/mod.rs` (test module) + +**Step 1: Add set_keepalive / keepalive methods** + +In `src/net/socket/tcp.rs`, after `nodelay()` (line 369): + +```rust + /// Enable or disable TCP keep-alive probes. + pub fn set_keepalive(&self, enabled: bool) { + let handler = unsafe { &mut *self.handler.get() }; + if let Some(tcb) = handler.get_connection_mut(&self.conn_id) { + tcb.keep_alive_enabled = enabled; + } + } + + /// Returns whether TCP keep-alive is enabled. + pub fn keepalive(&self) -> bool { + let handler = unsafe { &*self.handler.get() }; + handler + .get_connection(&self.conn_id) + .map(|tcb| tcb.keep_alive_enabled) + .unwrap_or(false) + } + + /// Set the SO_LINGER option. + /// + /// - `None`: default graceful close + /// - `Some(0)`: hard RST on close + /// - `Some(ms)`: graceful close with timeout in milliseconds + pub fn set_linger(&self, linger: Option) { + let handler = unsafe { &mut *self.handler.get() }; + if let Some(tcb) = handler.get_connection_mut(&self.conn_id) { + tcb.linger = linger; + } + } + + /// Returns the current SO_LINGER setting. + pub fn linger(&self) -> Option { + let handler = unsafe { &*self.handler.get() }; + handler + .get_connection(&self.conn_id) + .and_then(|tcb| tcb.linger) + } +``` + +**Step 2: Build and run tests** + +Run: `cargo build 2>&1 | head -50` +Run: `cargo test 2>&1 | tail -20` +Expected: All tests pass. + +**Step 3: Commit** + +```bash +git add src/net/socket/tcp.rs +git commit -m "feat(tcp): add keepalive, linger, and shutdown socket API methods" +``` + +--- + +## Task 7: Integration Tests for All Three Features + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs` (test module) + +**Step 1: Write comprehensive integration-style tests** + +These tests exercise the features end-to-end through the handler: + +```rust +#[test] +fn half_close_full_lifecycle() { + // 1. Establish connection + // 2. shutdown() — sends FIN + // 3. Verify writes return 0 + // 4. Remote sends data — verify reads work + // 5. Remote sends FIN — verify read returns 0 (EOF) +} + +#[test] +fn keep_alive_full_lifecycle() { + // 1. Establish connection with keep_alive_enabled, short timeouts + // 2. Let connection sit idle past keep_alive_idle_ms + // 3. poll_timers — verify probe sent + // 4. Receive ACK — verify probes reset + // 5. Let idle again, exhaust probes — verify timeout event +} + +#[test] +fn linger_zero_aborts_connection() { + // 1. Establish connection, write data + // 2. Set linger = Some(0) + // 3. initiate_close + // 4. poll_send — verify RST sent, connection removed +} + +#[test] +fn linger_timeout_graceful_then_rst() { + // 1. Establish connection, write data that won't drain + // 2. Set linger = Some(100) + // 3. initiate_close — verify deadline set + // 4. poll_send before deadline — verify FIN behavior (waits for data to drain) + // 5. Advance past deadline, poll_send — verify RST sent +} +``` + +**Step 2: Run all tests** + +Run: `cargo test 2>&1 | tail -20` +Expected: All tests pass. + +**Step 3: Commit** + +```bash +git add src/net/handler/tcp/mod.rs +git commit -m "test(tcp): add integration tests for half-close, keep-alive, and linger" +``` + +--- + +## Implementation Notes + +### Key Line References (current state) +- `TcpConfig`: `src/net/handler/tcp/tcb.rs:80-106` +- `Tcb` struct: `src/net/handler/tcp/tcb.rs:108-212` +- `ListenEntry`: `src/net/handler/tcp/mod.rs:39-50` +- Active open TCB init: `src/net/handler/tcp/mod.rs:179-220` +- Passive open TCB init: `src/net/handler/tcp/mod.rs:579-619` +- `process_established`: `src/net/handler/tcp/mod.rs:866-1053` +- `poll_timers`: `src/net/handler/tcp/mod.rs:1058-1226` +- `poll_send`: `src/net/handler/tcp/mod.rs:1250-1351` +- `initiate_close`: `src/net/handler/tcp/mod.rs:1357-1363` +- `TcpStream` struct: `src/net/socket/tcp.rs:161-167` +- `TcpStream::close()`: `src/net/socket/tcp.rs:342-349` +- `TcpWrite`: `src/net/socket/tcp.rs:410-436` +- `TcpRead`: `src/net/socket/tcp.rs:439-464` + +### Dependencies Between Tasks +- Task 1 (fields) must be done first — all other tasks depend on it. +- Tasks 2, 3, 4, 5, 6 can be done in any order after Task 1, but the suggested order minimizes conflicts. +- Task 7 (integration tests) should be done last. + +### Test Command +Always use plain `cargo test` (no feature flags per project memory). + +### RST Building +The implementer needs to check `src/net/handler/tcp/segment.rs` for available RST segment builders. If no suitable one exists for sending RST on an active connection (vs responding to an incoming segment), create a minimal helper. The probe segments for keep-alive use `build_ack` with `seq = snd_una - 1`. + +### RingBuffer::clear() +If `RingBuffer` doesn't have a `clear()` method and linger(0) needs to discard the send buffer, the implementer can either add `clear()` or simply rely on the linger deadline approach (set deadline to now, let poll_send handle RST). diff --git a/docs/plans/2026-03-07-tcp-benchmarks-design.md b/docs/plans/2026-03-07-tcp-benchmarks-design.md new file mode 100644 index 0000000..ff4703b --- /dev/null +++ b/docs/plans/2026-03-07-tcp-benchmarks-design.md @@ -0,0 +1,68 @@ +# TCP Per-Connection Benchmarks Design + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Measure per-connection TCP throughput and latency over a veth pair using criterion, and validate data integrity under sustained load. + +**Architecture:** Two criterion benchmark files (`tcp_throughput`, `tcp_latency`), each using `iter_custom` to time only the data transfer phase. Each benchmark creates a `TestVethPair`, spawns two threads (server + client) with independent `LocalRuntime` instances, and coordinates via barriers and channels. + +**Tech Stack:** criterion 0.8, `TestVethPair`, `LocalRuntime`, `TcpListener`/`TcpStream` + +--- + +## Benchmarks + +### 1. Throughput (`benches/tcp_throughput.rs`) + +- Client sends 1 GB in 64 KB chunks over a single TCP connection +- Server reads into a 64 KB buffer, discards data, counts bytes +- Client calls `shutdown()` after sending; server reads until EOF +- Data integrity: client fills each chunk with `(byte_offset % 251) as u8`, server computes a running checksum, panics on mismatch +- Criterion config: 10 samples, 60s measurement time +- Reports: GB/s (criterion handles statistical analysis) + +### 2. Latency (`benches/tcp_latency.rs`) + +- Two parameter groups: 64 bytes and 1 KB payloads +- Server runs an echo loop (read N bytes, write N bytes back) +- Client sends a message, reads back full response, repeats 10,000 times per criterion sample +- Client verifies each response matches the sent payload +- Criterion config: 10 samples, 30s measurement time +- Reports: per-round-trip latency with min/avg/p50/p99/max (criterion built-in) + +## Thread Coordination + +- Each benchmark creates `TestVethPair` once per group (not per iteration) +- Server thread: `LocalRuntime::run()` with `Arc` exit flag +- Client thread: `LocalRuntime::run()` with the benchmark workload +- `std::sync::Barrier` ensures both runtimes are up before client connects +- After measurement, client signals server via exit flag + +## Cargo.toml + +```toml +[[bench]] +name = "tcp_throughput" +harness = false + +[[bench]] +name = "tcp_latency" +harness = false +``` + +## Running + +```bash +cargo bench --bench tcp_throughput +cargo bench --bench tcp_latency +``` + +Requires root (handled by `.cargo/config.toml` via `sudo -E`). + +## Out of Scope + +- Artificial loss injection (deferred) +- Comparison against kernel TCP +- Custom reporting dashboard +- Multi-connection benchmarks (deferred to connection management phase) +- `--json` output (criterion has its own HTML reports) diff --git a/docs/plans/2026-03-07-tcp-benchmarks-fix-design.md b/docs/plans/2026-03-07-tcp-benchmarks-fix-design.md new file mode 100644 index 0000000..dbf758a --- /dev/null +++ b/docs/plans/2026-03-07-tcp-benchmarks-fix-design.md @@ -0,0 +1,41 @@ +# TCP Benchmarks Fix Design + +## Problem + +Both `tcp_throughput.rs` and `tcp_latency.rs` benchmarks create and tear down the +full infrastructure (TestVethPair, LocalRuntime, TCP connection) on every criterion +iteration. The measured time is dominated by setup/teardown, not actual TCP +performance. These benchmarks produce meaningless numbers. + +## Solution: Iteration Inside Async Future + +Move the criterion iteration loop inside the async future passed to +`runtime.run()`. Create the veth pair once at benchmark group level. Each +`iter_custom` sample spawns threads and runtimes once, establishes one TCP +connection, then iterates over the actual measured work. + +### Throughput + +- `TestVethPair` created once, outside `bench_function` +- `AtomicU16` port counter avoids TIME_WAIT between samples +- Server: sink loop (read until EOF) +- Client async future: connect, then `for _ in 0..iters { send TRANSFER_SIZE }` +- Timing wraps only the send loop, not connection setup +- Remove integrity checking from throughput (CPU overhead pollutes measurement) +- Remove `WaitForFlag` — use exit flag from main thread after client joins + +### Latency + +- Same veth-outside, port-counter pattern +- Server: echo loop (read → write back) +- Client async future: connect, then `for _ in 0..iters { ROUND_TRIPS × (write → read) }` +- Return raw elapsed — criterion divides by `iters`, each iteration = ROUND_TRIPS round trips +- Keep echo integrity check (lightweight, validates correctness) +- Eliminate `run_latency_iteration()` entirely + +### Thread Coordination + +Both benchmarks use: +- `Barrier::new(2)` for server-ready synchronization +- `mpsc::channel` for elapsed time from client to main thread +- `Arc` exit flag for server shutdown (set after client joins) diff --git a/docs/plans/2026-03-07-tcp-benchmarks-fix-impl.md b/docs/plans/2026-03-07-tcp-benchmarks-fix-impl.md new file mode 100644 index 0000000..694112d --- /dev/null +++ b/docs/plans/2026-03-07-tcp-benchmarks-fix-impl.md @@ -0,0 +1,412 @@ +# TCP Benchmarks Fix Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Rewrite tcp_throughput and tcp_latency benchmarks so the iteration loop runs inside the async future over a single TCP connection, not per-iteration infrastructure teardown/rebuild. + +**Architecture:** Each benchmark creates a `TestVethPair` once at group level. Each `iter_custom` call spawns server/client threads with one `LocalRuntime` each, establishes one TCP connection, then loops `iters` times over the measured work inside the async future. An `AtomicU16` port counter prevents TIME_WAIT collisions between samples. + +**Tech Stack:** Rust, criterion, libvoid (TcpListener, TcpStream, LocalRuntime, TestVethPair) + +--- + +### Task 1: Rewrite `benches/tcp_throughput.rs` + +**Files:** +- Modify: `benches/tcp_throughput.rs` (full rewrite) + +**Step 1: Replace file contents** + +```rust +use std::{ + sync::{ + Arc, Barrier, + atomic::{AtomicBool, AtomicU16, Ordering}, + mpsc, + }, + thread, + time::{Duration, Instant}, +}; + +use criterion::{criterion_group, criterion_main, Criterion, Throughput}; + +use libvoid::net::socket::{TcpListener, TcpStream}; +use libvoid::net::wire::ip::{IpAddress, Ipv4Address}; +use libvoid::rt::LocalRuntime; +use libvoid::xdp::test_utils::TestVethPair; + +const TRANSFER_SIZE: u64 = 1_024 * 1_024 * 1_024; // 1 GB +const CHUNK_SIZE: usize = 64 * 1024; // 64 KB + +fn tcp_throughput(c: &mut Criterion) { + let veth = TestVethPair::new().expect("failed to create veth pair"); + let addrs = *veth.addresses(); + let server_if = veth.inner_name().to_owned(); + let client_if = veth.outer_name().to_owned(); + let base_port = 9000 + (veth.pair_id() as u16) * 100; + let port_counter = AtomicU16::new(0); + + let server_ip = IpAddress::V4(Ipv4Address::new(addrs.inner_ipv4.octets())); + let client_ip = IpAddress::V4(Ipv4Address::new(addrs.outer_ipv4.octets())); + + let mut group = c.benchmark_group("tcp_throughput"); + group.throughput(Throughput::Bytes(TRANSFER_SIZE)); + group.sample_size(10); + group.measurement_time(Duration::from_secs(300)); + + group.bench_function("1gb_transfer", |b| { + b.iter_custom(|iters| { + let port = base_port + port_counter.fetch_add(1, Ordering::Relaxed); + let exit = Arc::new(AtomicBool::new(false)); + let barrier = Arc::new(Barrier::new(2)); + + // --- Server thread (sink) --- + let server_exit = exit.clone(); + let server_barrier = barrier.clone(); + let server_if = server_if.clone(); + + let server_handle = thread::spawn(move || { + let mut runtime = LocalRuntime::builder(&server_if, 0) + .build() + .expect("server runtime build failed"); + + runtime + .run(server_exit, async move { + let listener = + TcpListener::listen(server_ip, port).expect("listen failed"); + + server_barrier.wait(); + + let stream = listener.accept().await; + let mut buf = vec![0u8; CHUNK_SIZE]; + + loop { + match stream.read(&mut buf).await { + Ok(0) | Err(_) => break, + Ok(_) => {} + } + } + }) + .expect("server runtime run failed"); + }); + + barrier.wait(); + thread::sleep(Duration::from_millis(50)); + + // --- Client thread (sender) --- + let client_if = client_if.clone(); + let (time_tx, time_rx) = mpsc::channel(); + let client_port = port + 50; + + let client_handle = thread::spawn(move || { + let mut runtime = LocalRuntime::builder(&client_if, 0) + .build() + .expect("client runtime build failed"); + + runtime + .run(Arc::new(AtomicBool::new(false)), async move { + let mut stream = TcpStream::connect( + client_ip, + client_port, + server_ip, + port, + ) + .expect("connect initiation failed") + .await + .expect("connect failed"); + + let send_buf = vec![0u8; CHUNK_SIZE]; + + let start = Instant::now(); + + for _ in 0..iters { + let mut bytes_sent: u64 = 0; + while bytes_sent < TRANSFER_SIZE { + let remaining = (TRANSFER_SIZE - bytes_sent) as usize; + let chunk = remaining.min(CHUNK_SIZE); + stream + .write(&send_buf[..chunk]) + .await + .expect("write failed"); + bytes_sent += chunk as u64; + } + } + + let elapsed = start.elapsed(); + time_tx.send(elapsed).unwrap(); + + stream.shutdown(); + }) + .expect("client runtime run failed"); + }); + + let elapsed = time_rx.recv().expect("failed to receive elapsed time"); + client_handle.join().expect("client thread panicked"); + + exit.store(true, Ordering::Relaxed); + server_handle.join().expect("server thread panicked"); + + elapsed + }); + }); + + group.finish(); +} + +criterion_group!(benches, tcp_throughput); +criterion_main!(benches); +``` + +Key changes from the broken version: +- Removed `fill_pattern`, `checksum_pattern`, `WaitForFlag`, integrity checking +- `for _ in 0..iters` loop is INSIDE the async block, after connect, wrapping only sends +- Single TCP connection serves all iterations within a sample +- `AtomicU16` port counter prevents TIME_WAIT collisions between samples +- Client future completes naturally after shutdown (no `WaitForFlag`) +- Client gets its own private exit flag (never set) — runtime exits when future completes +- `send_buf` is zeroed once — no `fill_pattern` overhead + +**Step 2: Verify it compiles** + +Run: `cargo build --bench tcp_throughput` +Expected: compiles without errors + +**Step 3: Commit** + +```bash +git add benches/tcp_throughput.rs +git commit -m "fix(bench): rewrite tcp_throughput to iterate inside async future + +Move the criterion iteration loop inside the async block so a single +TCP connection serves all iterations. Removes per-iteration veth/runtime +teardown that dominated measurement time." +``` + +--- + +### Task 2: Rewrite `benches/tcp_latency.rs` + +**Files:** +- Modify: `benches/tcp_latency.rs` (full rewrite) + +**Step 1: Replace file contents** + +```rust +use std::{ + sync::{ + Arc, Barrier, + atomic::{AtomicBool, AtomicU16, AtomicU64, Ordering}, + mpsc, + }, + thread, + time::{Duration, Instant}, +}; + +use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; + +use libvoid::net::socket::{TcpListener, TcpStream}; +use libvoid::net::wire::ip::{IpAddress, Ipv4Address}; +use libvoid::rt::LocalRuntime; +use libvoid::xdp::test_utils::TestVethPair; + +const ROUND_TRIPS: u64 = 10_000; + +/// Fill a buffer with a deterministic pattern. +fn fill_pattern(buf: &mut [u8], seed: u64) { + for (i, byte) in buf.iter_mut().enumerate() { + *byte = ((seed + i as u64) % 251) as u8; + } +} + +fn tcp_latency(c: &mut Criterion) { + let veth = TestVethPair::new().expect("failed to create veth pair"); + let addrs = *veth.addresses(); + let server_if = veth.inner_name().to_owned(); + let client_if = veth.outer_name().to_owned(); + let base_port = 9100 + (veth.pair_id() as u16) * 100; + let port_counter = AtomicU16::new(0); + + let server_ip = IpAddress::V4(Ipv4Address::new(addrs.inner_ipv4.octets())); + let client_ip = IpAddress::V4(Ipv4Address::new(addrs.outer_ipv4.octets())); + + let mut group = c.benchmark_group("tcp_latency"); + group.sample_size(10); + group.measurement_time(Duration::from_secs(300)); + + for payload_size in [64, 1024] { + group.bench_with_input( + BenchmarkId::new("rtt", payload_size), + &payload_size, + |b, &size| { + b.iter_custom(|iters| { + let port = base_port + port_counter.fetch_add(1, Ordering::Relaxed); + let exit = Arc::new(AtomicBool::new(false)); + let barrier = Arc::new(Barrier::new(2)); + let errors = Arc::new(AtomicU64::new(0)); + + // --- Server thread (echo loop) --- + let server_exit = exit.clone(); + let server_barrier = barrier.clone(); + let server_if = server_if.clone(); + + let server_handle = thread::spawn(move || { + let mut runtime = LocalRuntime::builder(&server_if, 0) + .build() + .expect("server runtime build failed"); + + runtime + .run(server_exit, async move { + let listener = TcpListener::listen(server_ip, port) + .expect("listen failed"); + + server_barrier.wait(); + + let stream = listener.accept().await; + let mut buf = vec![0u8; size]; + + loop { + let n = match stream.read(&mut buf).await { + Ok(0) | Err(_) => break, + Ok(n) => n, + }; + if stream.write(&buf[..n]).await.is_err() { + break; + } + } + }) + .expect("server runtime run failed"); + }); + + barrier.wait(); + thread::sleep(Duration::from_millis(50)); + + // --- Client thread (request-response loop) --- + let client_if = client_if.clone(); + let (time_tx, time_rx) = mpsc::channel(); + let client_port = port + 50; + let client_errors = errors.clone(); + + let client_handle = thread::spawn(move || { + let mut runtime = LocalRuntime::builder(&client_if, 0) + .build() + .expect("client runtime build failed"); + + runtime + .run(Arc::new(AtomicBool::new(false)), async move { + let mut stream = TcpStream::connect( + client_ip, + client_port, + server_ip, + port, + ) + .expect("connect initiation failed") + .await + .expect("connect failed"); + + let mut send_buf = vec![0u8; size]; + let mut recv_buf = vec![0u8; size]; + let total_rounds = iters * ROUND_TRIPS; + + let start = Instant::now(); + + for round in 0..total_rounds { + fill_pattern(&mut send_buf, round); + + stream.write(&send_buf).await.expect("write failed"); + + let mut received = 0; + while received < size { + let n = stream + .read(&mut recv_buf[received..]) + .await + .expect("read failed"); + if n == 0 { + break; + } + received += n; + } + + if recv_buf[..received] != send_buf[..received] { + client_errors.fetch_add(1, Ordering::Relaxed); + } + } + + let elapsed = start.elapsed(); + time_tx.send(elapsed).unwrap(); + + stream.shutdown(); + }) + .expect("client runtime run failed"); + }); + + let elapsed = time_rx.recv().expect("failed to receive elapsed time"); + client_handle.join().expect("client thread panicked"); + + exit.store(true, Ordering::Relaxed); + server_handle.join().expect("server thread panicked"); + + let errs = errors.load(Ordering::Relaxed); + assert_eq!( + errs, 0, + "echo integrity check failed: {} mismatches", + errs + ); + + elapsed + }); + }, + ); + } + + group.finish(); +} + +criterion_group!(benches, tcp_latency); +criterion_main!(benches); +``` + +Key changes from the broken version: +- Eliminated `run_latency_iteration()` — no per-iteration veth/runtime creation +- Removed `WaitForFlag` future +- `iters * ROUND_TRIPS` loop is INSIDE the async block over a single connection +- Raw elapsed returned — criterion divides by `iters` to get per-iteration (= per ROUND_TRIPS batch) time +- `AtomicU16` port counter prevents TIME_WAIT collisions +- Echo integrity check retained (lightweight validation) +- Client gets its own private exit flag — runtime exits when future completes + +**Step 2: Verify it compiles** + +Run: `cargo build --bench tcp_latency` +Expected: compiles without errors + +**Step 3: Commit** + +```bash +git add benches/tcp_latency.rs +git commit -m "fix(bench): rewrite tcp_latency to iterate inside async future + +Move the criterion iteration loop inside the async block so a single +TCP connection serves all iterations. Eliminates per-iteration veth/runtime +teardown that dominated measurement time." +``` + +--- + +### Task 3: Clean up stale example files + +**Files:** +- Delete: `examples/tcp-bench-debug.rs` (if related to old benchmark scaffolding) +- Delete: `examples/tcp-bench-test.rs` (if related to old benchmark scaffolding) + +**Step 1: Check if these files are benchmark scaffolding** + +Run: `head -20 examples/tcp-bench-debug.rs examples/tcp-bench-test.rs` + +If they are old benchmark test harnesses, delete them. If they serve a different purpose, leave them. + +**Step 2: Commit (if deleted)** + +```bash +git rm examples/tcp-bench-debug.rs examples/tcp-bench-test.rs +git commit -m "chore: remove stale benchmark scaffolding examples" +``` diff --git a/docs/plans/2026-03-07-tcp-benchmarks-impl.md b/docs/plans/2026-03-07-tcp-benchmarks-impl.md new file mode 100644 index 0000000..cb72971 --- /dev/null +++ b/docs/plans/2026-03-07-tcp-benchmarks-impl.md @@ -0,0 +1,572 @@ +# TCP Per-Connection Benchmarks Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Implement criterion-based TCP throughput and latency benchmarks over a veth pair, with data integrity validation. + +**Architecture:** Two benchmark files using `criterion::iter_custom` with `TestVethPair` + two `LocalRuntime` instances on separate threads. Each benchmark times only the data transfer phase, not setup/teardown. + +**Tech Stack:** criterion 0.8, `TestVethPair`, `LocalRuntime`, `TcpListener`/`TcpStream`, `std::thread`, `std::sync::Barrier` + +--- + +## Context for the Implementer + +### Key APIs + +**`TestVethPair`** (`src/xdp/test_utils.rs`): +- `TestVethPair::new()` creates a veth pair with unique names/addresses +- `.outer_name()` / `.inner_name()` — interface names for each end +- `.addresses()` returns `VethAddresses` with `.outer_ipv4`, `.inner_ipv4`, `.outer_ipv6`, `.inner_ipv6` +- Automatically cleaned up on `Drop` + +**`LocalRuntime`** (`src/rt/local.rs`): +- `LocalRuntime::builder(if_name, queue).build()` creates a runtime +- `.run(exit: Arc, fut: impl Future)` runs the event loop +- Returns `Ok(())` when the future completes or `exit` flag is set +- Must be called with root privileges + +**`TcpListener`** / **`TcpStream`** (`src/net/socket/tcp.rs`): +- `TcpListener::listen(addr, port)` — must be called inside `runtime.run()` +- `.accept().await` returns `TcpStream` +- `TcpStream::connect(local_addr, local_port, remote_addr, remote_port)?.await?` +- `.write(data).await -> Result` +- `.read(buf).await -> Result` +- `.shutdown()` — half-close (sends FIN, keeps read side open) +- `.close()` — full close + +**IP Addresses**: use IPv4 addresses from `VethAddresses` — `.outer_ipv4` and `.inner_ipv4` (e.g. `10.11.0.1`, `10.11.0.2`). Wrap them with `IpAddress::Ipv4(addr)`. + +**Import paths**: +```rust +use libvoid::xdp::test_utils::TestVethPair; +use libvoid::rt::LocalRuntime; +use libvoid::net::socket::{TcpListener, TcpStream}; +use libvoid::net::wire::ip::IpAddress; +``` + +### Criterion `iter_custom` pattern + +```rust +use criterion::{Criterion, criterion_group, criterion_main, BenchmarkId, Throughput}; +use std::time::Instant; + +// Inside a benchmark function: +group.bench_function("name", |b| { + b.iter_custom(|iters| { + // iters = number of iterations criterion wants + // Return total Duration for all iterations + let start = Instant::now(); + for _ in 0..iters { + // do the work + } + start.elapsed() + }); +}); +``` + +### Thread coordination pattern + +Each benchmark needs two `LocalRuntime` instances on separate threads because `LocalRuntime::run()` blocks. The pattern: + +1. Create `TestVethPair` (lives on the main/benchmark thread) +2. Create `Arc` exit flag and `std::sync::Barrier(2)` +3. Spawn server thread: builds runtime on one veth end, signals barrier, runs workload +4. In benchmark closure: build runtime on other veth end, signal barrier, run measurement +5. After measurement: set exit flag, join server thread + +**IMPORTANT**: `TestVethPair` is NOT `Send` — it must stay on the thread that created it. Pass `.outer_name().to_owned()` and `.inner_name().to_owned()` (as `String`) plus addresses to the spawned thread. + +**IMPORTANT**: Each `LocalRuntime` must be built on the thread that calls `.run()`. Do not build a runtime and move it across threads. + +### Running benchmarks + +```bash +cargo bench --bench tcp_throughput +cargo bench --bench tcp_latency +``` + +Requires root — `.cargo/config.toml` configures `sudo -E` as the runner. + +### Port allocation + +Use a fixed base port and add the veth pair_id to avoid conflicts if multiple benchmarks run: +```rust +let port = 9000 + veth.pair_id() as u16; +``` + +--- + +## Task 1: Add bench entries to Cargo.toml + +**Files:** +- Modify: `Cargo.toml` + +**Step 1: Add the bench entries** + +Add at the end of `Cargo.toml`: + +```toml +[[bench]] +name = "tcp_throughput" +harness = false + +[[bench]] +name = "tcp_latency" +harness = false +``` + +**Step 2: Create empty bench files** + +Create `benches/tcp_throughput.rs`: +```rust +use criterion::{Criterion, criterion_group, criterion_main}; + +fn tcp_throughput(_c: &mut Criterion) { + // TODO: implement +} + +criterion_group!(benches, tcp_throughput); +criterion_main!(benches); +``` + +Create `benches/tcp_latency.rs`: +```rust +use criterion::{Criterion, criterion_group, criterion_main}; + +fn tcp_latency(_c: &mut Criterion) { + // TODO: implement +} + +criterion_group!(benches, tcp_latency); +criterion_main!(benches); +``` + +**Step 3: Verify it compiles** + +Run: `cargo bench --bench tcp_throughput --no-run && cargo bench --bench tcp_latency --no-run` +Expected: compiles successfully (no tests run) + +**Step 4: Commit** + +```bash +git add Cargo.toml benches/tcp_throughput.rs benches/tcp_latency.rs +git commit -m "feat(bench): add skeleton criterion bench files for TCP throughput and latency" +``` + +--- + +## Task 2: Implement throughput benchmark + +**Files:** +- Modify: `benches/tcp_throughput.rs` + +**Step 1: Write the full benchmark** + +Replace `benches/tcp_throughput.rs` with: + +```rust +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::sync::{Arc, Barrier}; +use std::time::Instant; + +use criterion::{Criterion, Throughput, criterion_group, criterion_main}; + +use libvoid::net::socket::TcpListener; +use libvoid::net::socket::TcpStream; +use libvoid::net::wire::ip::IpAddress; +use libvoid::rt::LocalRuntime; +use libvoid::xdp::test_utils::TestVethPair; + +const TRANSFER_SIZE: u64 = 1_024 * 1_024 * 1_024; // 1 GB +const CHUNK_SIZE: usize = 64 * 1024; // 64 KB + +/// Fill a buffer with a deterministic pattern for integrity checking. +/// Uses (byte_offset % 251) — 251 is prime to avoid alignment tricks. +fn fill_pattern(buf: &mut [u8], offset: u64) { + for (i, byte) in buf.iter_mut().enumerate() { + *byte = ((offset + i as u64) % 251) as u8; + } +} + +/// Compute a simple checksum over a buffer starting at a given byte offset. +/// Returns the running sum. +fn checksum_pattern(buf: &[u8], offset: u64) -> u64 { + let mut bad = 0u64; + for (i, &byte) in buf.iter().enumerate() { + let expected = ((offset + i as u64) % 251) as u8; + if byte != expected { + bad += 1; + } + } + bad +} + +fn tcp_throughput(c: &mut Criterion) { + let veth = TestVethPair::new().expect("failed to create veth pair"); + let addrs = *veth.addresses(); + let server_if = veth.inner_name().to_owned(); + let client_if = veth.outer_name().to_owned(); + let port = 9000 + veth.pair_id() as u16; + + let server_ip = IpAddress::Ipv4(addrs.inner_ipv4); + let client_ip = IpAddress::Ipv4(addrs.outer_ipv4); + + let mut group = c.benchmark_group("tcp_throughput"); + group.throughput(Throughput::Bytes(TRANSFER_SIZE)); + group.sample_size(10); + group.measurement_time(std::time::Duration::from_secs(120)); + + group.bench_function("1GB_transfer", |b| { + b.iter_custom(|iters| { + let mut total_elapsed = std::time::Duration::ZERO; + + for _ in 0..iters { + let exit = Arc::new(AtomicBool::new(false)); + let barrier = Arc::new(Barrier::new(2)); + let integrity_errors = Arc::new(AtomicU64::new(0)); + + // Server thread: receive and verify data. + let server_exit = exit.clone(); + let server_barrier = barrier.clone(); + let server_errors = integrity_errors.clone(); + let server_if = server_if.clone(); + + let server_handle = std::thread::spawn(move || { + let mut runtime = LocalRuntime::builder(&server_if, 0) + .build() + .expect("server runtime build failed"); + + runtime + .run(server_exit, async move { + let listener = TcpListener::listen(server_ip, port) + .expect("listen failed"); + + server_barrier.wait(); + + let stream = listener.accept().await; + let mut buf = vec![0u8; CHUNK_SIZE]; + let mut total_received: u64 = 0; + let mut errors: u64 = 0; + + loop { + let n = match stream.read(&mut buf).await { + Ok(0) => break, + Ok(n) => n, + Err(_) => break, + }; + errors += checksum_pattern(&buf[..n], total_received); + total_received += n as u64; + } + + server_errors.store(errors, Ordering::Relaxed); + }) + .expect("server runtime run failed"); + }); + + // Wait for server to be listening. + barrier.wait(); + + // Give server a moment to set up the listen socket. + std::thread::sleep(std::time::Duration::from_millis(50)); + + // Client: connect, send 1 GB, measure time. + let client_exit = exit.clone(); + let client_if = client_if.clone(); + + let elapsed = { + let (tx, rx) = std::sync::mpsc::channel(); + let client_handle = std::thread::spawn(move || { + let mut runtime = LocalRuntime::builder(&client_if, 0) + .build() + .expect("client runtime build failed"); + + runtime + .run(client_exit, async move { + let mut stream = TcpStream::connect( + client_ip, port, server_ip, port, + ) + .expect("connect initiation failed") + .await + .expect("connect failed"); + + let mut send_buf = vec![0u8; CHUNK_SIZE]; + let mut bytes_sent: u64 = 0; + + let start = Instant::now(); + + while bytes_sent < TRANSFER_SIZE { + let remaining = (TRANSFER_SIZE - bytes_sent) as usize; + let chunk = remaining.min(CHUNK_SIZE); + fill_pattern(&mut send_buf[..chunk], bytes_sent); + + stream.write(&send_buf[..chunk]).await + .expect("write failed"); + bytes_sent += chunk as u64; + } + + stream.shutdown(); + + let elapsed = start.elapsed(); + tx.send(elapsed).unwrap(); + }) + .expect("client runtime run failed"); + }); + + let elapsed = rx.recv().expect("failed to receive elapsed time"); + client_handle.join().expect("client thread panicked"); + elapsed + }; + + // Wait for server to finish. + exit.store(true, Ordering::Relaxed); + server_handle.join().expect("server thread panicked"); + + let errors = integrity_errors.load(Ordering::Relaxed); + assert_eq!(errors, 0, "data integrity check failed: {} byte mismatches", errors); + + total_elapsed += elapsed; + } + + total_elapsed + }); + }); + + group.finish(); +} + +criterion_group!(benches, tcp_throughput); +criterion_main!(benches); +``` + +**Step 2: Verify it compiles** + +Run: `cargo bench --bench tcp_throughput --no-run` +Expected: compiles successfully + +**Step 3: Run the benchmark** + +Run: `cargo bench --bench tcp_throughput` +Expected: criterion runs 10 samples, reports throughput in GB/s or MB/s, no integrity errors + +**Step 4: Commit** + +```bash +git add benches/tcp_throughput.rs +git commit -m "feat(bench): implement 1GB TCP throughput benchmark with integrity check" +``` + +--- + +## Task 3: Implement latency benchmark + +**Files:** +- Modify: `benches/tcp_latency.rs` + +**Step 1: Write the full benchmark** + +Replace `benches/tcp_latency.rs` with: + +```rust +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Barrier}; +use std::time::Instant; + +use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; + +use libvoid::net::socket::TcpListener; +use libvoid::net::socket::TcpStream; +use libvoid::net::wire::ip::IpAddress; +use libvoid::rt::LocalRuntime; +use libvoid::xdp::test_utils::TestVethPair; + +const ROUND_TRIPS: u64 = 10_000; + +fn tcp_latency(c: &mut Criterion) { + let veth = TestVethPair::new().expect("failed to create veth pair"); + let addrs = *veth.addresses(); + let server_if = veth.inner_name().to_owned(); + let client_if = veth.outer_name().to_owned(); + let port = 9100 + veth.pair_id() as u16; + + let server_ip = IpAddress::Ipv4(addrs.inner_ipv4); + let client_ip = IpAddress::Ipv4(addrs.outer_ipv4); + + let mut group = c.benchmark_group("tcp_latency"); + group.sample_size(10); + group.measurement_time(std::time::Duration::from_secs(60)); + + for msg_size in [64usize, 1024] { + group.bench_with_input( + BenchmarkId::new("round_trip", format!("{}B", msg_size)), + &msg_size, + |b, &msg_size| { + b.iter_custom(|iters| { + let mut total_elapsed = std::time::Duration::ZERO; + + for _ in 0..iters { + let exit = Arc::new(AtomicBool::new(false)); + let barrier = Arc::new(Barrier::new(2)); + + // Server thread: echo loop. + let server_exit = exit.clone(); + let server_barrier = barrier.clone(); + let server_if = server_if.clone(); + + let server_handle = std::thread::spawn(move || { + let mut runtime = LocalRuntime::builder(&server_if, 0) + .build() + .expect("server runtime build failed"); + + runtime + .run(server_exit, async move { + let listener = TcpListener::listen(server_ip, port) + .expect("listen failed"); + + server_barrier.wait(); + + let stream = listener.accept().await; + let mut buf = vec![0u8; msg_size]; + + loop { + // Read exactly msg_size bytes. + let mut total_read = 0; + while total_read < msg_size { + let n = match stream.read(&mut buf[total_read..]).await { + Ok(0) => return, + Ok(n) => n, + Err(_) => return, + }; + total_read += n; + } + + // Echo back. + if stream.write(&buf[..msg_size]).await.is_err() { + return; + } + } + }) + .expect("server runtime run failed"); + }); + + // Wait for server to be listening. + barrier.wait(); + std::thread::sleep(std::time::Duration::from_millis(50)); + + // Client: connect, do round-trips, measure time. + let client_exit = exit.clone(); + let client_if = client_if.clone(); + + let elapsed = { + let (tx, rx) = std::sync::mpsc::channel(); + + let client_handle = std::thread::spawn(move || { + let mut runtime = LocalRuntime::builder(&client_if, 0) + .build() + .expect("client runtime build failed"); + + runtime + .run(client_exit, async move { + let mut stream = TcpStream::connect( + client_ip, port, server_ip, port, + ) + .expect("connect initiation failed") + .await + .expect("connect failed"); + + let send_buf = vec![0xABu8; msg_size]; + let mut recv_buf = vec![0u8; msg_size]; + + let start = Instant::now(); + + for _ in 0..ROUND_TRIPS { + stream.write(&send_buf).await + .expect("write failed"); + + let mut total_read = 0; + while total_read < msg_size { + let n = stream.read(&mut recv_buf[total_read..]).await + .expect("read failed"); + assert!(n > 0, "unexpected EOF"); + total_read += n; + } + + assert_eq!( + &recv_buf[..msg_size], + &send_buf[..msg_size], + "echo response mismatch" + ); + } + + let elapsed = start.elapsed(); + stream.shutdown(); + tx.send(elapsed).unwrap(); + }) + .expect("client runtime run failed"); + }); + + let elapsed = rx.recv().expect("failed to receive elapsed time"); + client_handle.join().expect("client thread panicked"); + elapsed + }; + + exit.store(true, Ordering::Relaxed); + server_handle.join().expect("server thread panicked"); + + total_elapsed += elapsed; + } + + total_elapsed + }); + }, + ); + } + + group.finish(); +} + +criterion_group!(benches, tcp_latency); +criterion_main!(benches); +``` + +**Step 2: Verify it compiles** + +Run: `cargo bench --bench tcp_latency --no-run` +Expected: compiles successfully + +**Step 3: Run the benchmark** + +Run: `cargo bench --bench tcp_latency` +Expected: criterion runs 10 samples for each payload size (64B, 1KB), reports per-round-trip latency stats + +**Step 4: Commit** + +```bash +git add benches/tcp_latency.rs +git commit -m "feat(bench): implement TCP request-response latency benchmark at 64B and 1KB" +``` + +--- + +## Task 4: Run full test suite and both benchmarks + +**Files:** None (verification only) + +**Step 1: Run tests** + +Run: `cargo test` +Expected: all tests pass (546+ unit, 1 integration, 5 doctests) + +**Step 2: Run throughput benchmark** + +Run: `cargo bench --bench tcp_throughput` +Expected: completes without errors, reports throughput numbers + +**Step 3: Run latency benchmark** + +Run: `cargo bench --bench tcp_latency` +Expected: completes without errors, reports latency numbers for both 64B and 1KB + +**Step 4: Commit (if any fixes were needed)** + +Only commit if you had to fix something. Otherwise, this task is just verification. diff --git a/docs/plans/2026-03-07-tcp-congestion-recovery-design.md b/docs/plans/2026-03-07-tcp-congestion-recovery-design.md new file mode 100644 index 0000000..bf9d3c5 --- /dev/null +++ b/docs/plans/2026-03-07-tcp-congestion-recovery-design.md @@ -0,0 +1,254 @@ +# TCP Congestion Control & Loss Recovery Design + +**Goal:** Replace basic Reno congestion control with CUBIC (RFC 9438) and add proper SACK-based loss recovery (RFC 6675), PRR (RFC 6937), Limited Transmit (RFC 3042), and F-RTO (RFC 5682). + +**Scope:** Single-connection focus. No benchmarking infrastructure in this round. No connection table changes (defer HashMap until multi-connection). + +--- + +## Approach + +Concrete structs, no traits. Extract congestion and recovery logic into separate modules while keeping integration in `mod.rs`. No dynamic dispatch — if/when BBR is added, the interface will differ enough to warrant redesign. + +## File Structure + +``` +src/net/handler/tcp/ +├── congestion.rs # CubicState +├── recovery.rs # SackRecovery + PrrState + FRtoState +├── mod.rs # existing — calls into congestion/recovery +├── tcb.rs # replace cwnd/ssthresh/dup_ack_count with new structs +└── ...existing... +``` + +**TCB changes:** Replace `cwnd: u32, ssthresh: u32, dup_ack_count: u8` with `cubic: CubicState`, `recovery: SackRecovery`, `prr: PrrState`, `frto: FRtoState`. + +--- + +## CubicState (`congestion.rs`) + +### Fields + +- `cwnd: u32` — congestion window (bytes) +- `ssthresh: u32` — slow start threshold +- `w_max: u32` — window before last reduction +- `w_max_prev: u32` — previous w_max (for fast convergence) +- `epoch_start: Option` — time of last loss event +- `k: f64` — time to reach w_max (seconds) +- `origin_point: u32` — W_max used in current epoch +- `tcp_cwnd: u32` — Reno-friendly estimate (TCP friendliness) +- `ack_count: u32` — bytes ACKed in current congestion avoidance round +- `eff_mss: u16` — cached MSS for calculations +- `cwnd_before_rto: Option` — saved for F-RTO restoration +- `ssthresh_before_rto: Option` — saved for F-RTO restoration + +### Methods + +- `on_ack(bytes_acked: u32, now: Instant, rtt: u64)` — slow start or CUBIC window update +- `on_loss()` — multiplicative decrease (beta=0.7), save w_max, compute K +- `on_rto()` — save cwnd/ssthresh for F-RTO, ssthresh = cwnd * 0.7, cwnd = 1 MSS +- `on_ecn()` — same as on_loss() +- `restore_after_spurious_rto()` — restore cwnd/ssthresh from saved values + +### CUBIC Algorithm (RFC 9438 section 5) + +**Window function:** `W_cubic(t) = C * (t - K)^3 + W_max` +- `C = 0.4`, `beta = 0.7` +- `K = cbrt(W_max * (1 - beta) / C)` = `cbrt(W_max * 0.3 / 0.4)` + +**On loss/ECN:** +- `W_max = cwnd` +- `ssthresh = cwnd * 0.7` +- `cwnd = ssthresh` +- Compute K, reset epoch_start + +**On ACK (not in recovery):** +- If `cwnd < ssthresh`: slow start — `cwnd += MSS` per ACK +- Else: compute `W_cubic(t)` and `W_est(t)`, `cwnd = max(W_cubic, W_est)` +- `W_est = W_max * beta + 3 * (1 - beta) / (1 + beta) * t / RTT` + +**On RTO:** +- Save cwnd/ssthresh for F-RTO +- `ssthresh = cwnd * 0.7`, `cwnd = 1 MSS` +- Clear epoch + +**Fast convergence (section 5.8):** If `W_max < W_max_prev`, set `W_max = W_max * (1 + beta) / 2`. Helps flows converge to fairness. + +**Implementation notes:** +- Cube root via `f64::cbrt()` — called once per loss, not per ACK +- Time from `now: Instant` parameter, never `Instant::now()` +- All window values in bytes +- `cwnd` floor: 1 MSS + +--- + +## SackRecovery (`recovery.rs`) + +### Fields + +- `in_recovery: bool` +- `recovery_point: u32` — snd_nxt at entry +- `pipe: u32` — estimated bytes in network +- `dup_ack_count: u8` + +### Methods + +- `enter(snd_nxt: u32)` — set in_recovery, recovery_point +- `exit()` — clear state +- `on_ack(seg_ack: u32) -> bool` — returns true if recovery exits (seg_ack >= recovery_point) +- `is_lost(seq: u32, sack_scoreboard: &BTreeMap, eff_mss: u16) -> bool` — RFC 6675 section 4 +- `set_pipe(snd_una: u32, snd_nxt: u32, sack_scoreboard: &BTreeMap, eff_mss: u16)` — RFC 6675 section 4.1 + +### IsLost Predicate (RFC 6675 section 4) + +A segment at `seq` is lost if: +- 3+ segments with higher sequence numbers are SACKed, OR +- The segment is `3 * MSS` bytes below the highest SACKed sequence + +### SetPipe (RFC 6675 section 4.1) + +Iterate MSS-sized blocks from snd_una to snd_nxt: +- If SACKed: skip +- If IsLost: skip (will retransmit) +- Else: pipe += MSS + +### Recovery Loop (RFC 6675 section 5.1) + +Runs in poll_timers and on each ACK during recovery: +1. SetPipe() +2. While pipe < cwnd: + - If lost segment exists: retransmit, pipe += MSS + - Else if new unsent data: send, pipe += MSS + - Else: break + +### Partial ACK Handling + +ACK advances snd_una but below recovery_point — stay in recovery, SetPipe again, continue retransmitting. + +--- + +## PrrState (`recovery.rs`) + +### Fields + +- `prr_delivered: u32` +- `prr_out: u32` +- `recover_fs: u32` — FlightSize at recovery entry + +### Methods + +- `enter(bytes_in_flight: u32)` — set recover_fs, zero counters +- `on_ack(bytes_newly_delivered: u32, pipe: u32, ssthresh: u32, eff_mss: u16) -> u32` — returns snd_cnt + +### Algorithm (RFC 6937) + +On each ACK during recovery: +- `prr_delivered += bytes_newly_acked + bytes_newly_sacked` +- If `pipe > ssthresh`: `snd_cnt = ceil(prr_delivered * ssthresh / recover_fs) - prr_out` +- If `pipe <= ssthresh`: `snd_cnt = min(ssthresh - pipe, prr_delivered - prr_out + MSS)` +- Floor: `snd_cnt = max(snd_cnt, 0)` +- After sending: `prr_out += bytes_sent` + +--- + +## FRtoState (`recovery.rs`) + +### Fields + +- `state: FRtoPhase` — enum { Disabled, Step1, Step2 } +- `snd_una_at_rto: u32` + +### State Machine (RFC 5682) + +``` +Disabled -> Step1 (on RTO retransmit) +Step1 -> Step2 (first ACK advances snd_una: send new data, not retransmits) +Step1 -> Disabled (first ACK is dup: genuine loss) +Step2 -> Disabled (second ACK advances snd_una: spurious RTO, restore cwnd) +Step2 -> Disabled (second ACK is dup: genuine loss) +``` + +On spurious detection: call `cubic.restore_after_spurious_rto()`. + +--- + +## Integration Points in mod.rs + +### ACK Processing (~line 1468) + +**New ACK (advances snd_una):** +1. `recovery.on_ack(seg_ack)` — check if exiting recovery +2. If in recovery: `prr.on_ack(...)` for send budget, `recovery.set_pipe(...)` to update pipe +3. If not in recovery: `cubic.on_ack(bytes_acked, now)` — replaces lines 1486-1492 +4. RTT measurement unchanged +5. SACK scoreboard parsing unchanged + +**Duplicate ACK:** +1. `recovery.dup_ack_count += 1` +2. Limited Transmit: on count 1 or 2, allow 1 MSS new data in poll_send +3. On count == 3: `recovery.enter(snd_nxt)`, `prr.enter(bytes_in_flight)`, `cubic.on_loss()` + +**ECN:** `cubic.on_ecn()` replaces inline halving (~line 1567). + +### Fast Retransmit (~line 1948) + +Replace current block: +1. Check `recovery.in_recovery` instead of `dup_ack_count >= 3` +2. `recovery.is_lost(seq, scoreboard)` to pick retransmit target +3. Gate on `recovery.pipe < cubic.cwnd` +4. After sending: `recovery.pipe += bytes`, `prr.prr_out += bytes` +5. Send new data if pipe < cwnd and no more lost segments + +### RTO Retransmit (~line 2029) + +1. `frto.enter(snd_una)` +2. `cubic.on_rto()` — saves cwnd/ssthresh, resets +3. `recovery.exit()` — clear recovery state and scoreboard +4. First ACK after RTO: F-RTO decides genuine vs spurious + +### poll_send (~line 2229) + +- In recovery: use `min(prr.snd_cnt, cwnd - pipe)` as send budget +- Not in recovery: `send_window = min(snd_wnd, cubic.cwnd)` (same as current but from cubic) +- Limited Transmit: if dup_ack_count in [1, 2], add `dup_ack_count * MSS` to effective budget + +--- + +## Testing + +All tests use existing `build_tcp_frame` / `TcpHandler` unit test infrastructure in `mod.rs`. + +### CUBIC (unit tests on CubicState) + +- `cubic_slow_start` — cwnd += MSS per ACK when below ssthresh +- `cubic_on_loss` — W_max saved, ssthresh = cwnd * 0.7, cwnd = ssthresh +- `cubic_on_rto` — cwnd = 1 MSS, ssthresh = cwnd * 0.7 +- `cubic_congestion_avoidance` — after loss, cwnd follows cubic function, eventually exceeds W_max +- `cubic_tcp_friendliness` — cwnd >= W_est +- `cubic_fast_convergence` — W_max reduced when below previous W_max +- `cubic_beta_07_not_05` — verify beta = 0.7 + +### RFC 6675 (integration via handler) + +- `sack_recovery_entry_on_3_dup_acks` — enters recovery, sets recovery_point +- `sack_recovery_is_lost_3_sacked_above` — IsLost correct with 3 SACKed segments beyond +- `sack_recovery_pipe_calculation` — SetPipe counts only in-flight bytes +- `sack_recovery_partial_ack_stays_in_recovery` — ACK advances but below recovery_point +- `sack_recovery_exit_on_full_ack` — ACK >= recovery_point exits recovery +- `sack_recovery_retransmits_lost_before_new` — lost segments prioritized + +### PRR + +- `prr_proportional_when_pipe_above_ssthresh` — snd_cnt tracks delivery rate +- `prr_slow_start_reduction_when_pipe_below` — converges to ssthresh + +### Limited Transmit + +- `limited_transmit_sends_on_first_dup_ack` — 1 MSS on dup_ack 1 +- `limited_transmit_sends_on_second_dup_ack` — 1 MSS on dup_ack 2 +- `limited_transmit_stops_at_third` — no extra send on 3rd + +### F-RTO + +- `frto_spurious_rto_restores_cwnd` — RTO -> advancing ACKs -> cwnd restored +- `frto_genuine_loss_keeps_reduced_cwnd` — RTO -> dup ACK -> normal loss behavior diff --git a/docs/plans/2026-03-07-tcp-congestion-recovery-impl.md b/docs/plans/2026-03-07-tcp-congestion-recovery-impl.md new file mode 100644 index 0000000..9861358 --- /dev/null +++ b/docs/plans/2026-03-07-tcp-congestion-recovery-impl.md @@ -0,0 +1,1547 @@ +# TCP Congestion Control & Loss Recovery — Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Replace basic Reno with CUBIC (RFC 9438), add SACK-based loss recovery (RFC 6675), PRR (RFC 6937), Limited Transmit (RFC 3042), and F-RTO (RFC 5682). + +**Architecture:** New `congestion.rs` and `recovery.rs` modules with concrete structs (no traits). TCB replaces `cwnd/ssthresh/dup_ack_count` with `CubicState`, `SackRecovery`, `PrrState`, `FRtoState`. Integration into existing ACK processing, fast retransmit, RTO, and poll_send paths in `mod.rs`. + +**Tech Stack:** Rust, coarsetime (for timestamps — always use supplied `now: Instant`, never `Instant::now()`), existing TCP handler + wire module infrastructure. + +**Design doc:** `docs/plans/2026-03-07-tcp-congestion-recovery-design.md` + +--- + +### Task 1: Create CubicState with slow start and loss response + +**Files:** +- Create: `src/net/handler/tcp/congestion.rs` +- Modify: `src/net/handler/tcp/mod.rs:1` (add `pub(crate) mod congestion;`) + +**Step 1: Write the failing tests** + +Add unit tests in `congestion.rs`: + +```rust +use coarsetime::Instant; + +/// CUBIC constants (RFC 9438 §5). +const CUBIC_C: f64 = 0.4; +const CUBIC_BETA: f64 = 0.7; + +pub struct CubicState { + pub cwnd: u32, + pub ssthresh: u32, + w_max: u32, + w_max_prev: u32, + epoch_start: Option, + k: f64, + origin_point: u32, + tcp_cwnd: u32, + ack_count: u32, + eff_mss: u16, + cwnd_before_rto: Option, + ssthresh_before_rto: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn slow_start_increases_cwnd_by_mss_per_ack() { + let mut cubic = CubicState::new(1460); + // IW = 10 * MSS = 14600, ssthresh = MAX => slow start. + let now = Instant::now(); + cubic.on_ack(1460, now, 100); + assert_eq!(cubic.cwnd, 14600 + 1460); + cubic.on_ack(1460, now, 100); + assert_eq!(cubic.cwnd, 14600 + 2 * 1460); + } + + #[test] + fn on_loss_sets_ssthresh_to_cwnd_times_beta() { + let mut cubic = CubicState::new(1460); + cubic.cwnd = 100_000; + cubic.on_loss(); + assert_eq!(cubic.ssthresh, 70_000); // 100_000 * 0.7 + assert_eq!(cubic.cwnd, 70_000); + assert_eq!(cubic.w_max, 100_000); + } + + #[test] + fn on_rto_resets_cwnd_to_one_mss() { + let mut cubic = CubicState::new(1460); + cubic.cwnd = 100_000; + cubic.ssthresh = 50_000; + cubic.on_rto(); + assert_eq!(cubic.cwnd, 1460); + assert_eq!(cubic.ssthresh, 70_000); // 100_000 * 0.7 + assert_eq!(cubic.cwnd_before_rto, Some(100_000)); + assert_eq!(cubic.ssthresh_before_rto, Some(50_000)); + } + + #[test] + fn beta_is_07_not_05() { + let mut cubic = CubicState::new(1460); + cubic.cwnd = 10_000; + cubic.on_loss(); + // Reno would be 5000, CUBIC should be 7000. + assert_eq!(cubic.ssthresh, 7000); + } + + #[test] + fn fast_convergence_reduces_w_max() { + let mut cubic = CubicState::new(1460); + // First loss at cwnd=100_000. + cubic.cwnd = 100_000; + cubic.on_loss(); + // w_max_prev = 100_000. + // Second loss at cwnd=80_000 (below w_max_prev). + cubic.cwnd = 80_000; + cubic.on_loss(); + // Fast convergence: w_max = 80_000 * (1 + 0.7) / 2 = 68_000. + assert_eq!(cubic.w_max, 68_000); + } +} +``` + +**Step 2: Run tests to verify they fail** + +Run: `cargo test -p voidnet congestion::tests` +Expected: FAIL — `CubicState::new`, `on_ack`, `on_loss`, `on_rto` not implemented + +**Step 3: Implement CubicState::new, on_loss, on_rto, and slow start portion of on_ack** + +```rust +impl CubicState { + pub fn new(eff_mss: u16) -> Self { + Self { + cwnd: 10 * eff_mss as u32, + ssthresh: u32::MAX, + w_max: 0, + w_max_prev: 0, + epoch_start: None, + k: 0.0, + origin_point: 0, + tcp_cwnd: 0, + ack_count: 0, + eff_mss, + cwnd_before_rto: None, + ssthresh_before_rto: None, + } + } + + /// Update eff_mss after MSS negotiation completes. + pub fn set_mss(&mut self, eff_mss: u16) { + self.eff_mss = eff_mss; + } + + /// Called on each new ACK (that advances snd_una). NOT called during recovery. + /// `rtt_ms` is the current smoothed RTT estimate in milliseconds. + pub fn on_ack(&mut self, bytes_acked: u32, now: Instant, rtt_ms: u64) { + let mss = self.eff_mss as u32; + if self.cwnd < self.ssthresh { + // Slow start: increase by MSS per ACK. + self.cwnd += mss; + } else { + // Congestion avoidance: CUBIC window update (Task 2). + self.cubic_update(bytes_acked, now, rtt_ms); + } + } + + /// Called on packet loss detected via 3 dup ACKs / SACK. + pub fn on_loss(&mut self) { + self.epoch_start = None; + let mss = self.eff_mss as u32; + + // Fast convergence (RFC 9438 §5.8). + if self.cwnd < self.w_max_prev { + self.w_max_prev = self.cwnd; + self.w_max = (self.cwnd as f64 * (1.0 + CUBIC_BETA) / 2.0) as u32; + } else { + self.w_max_prev = self.cwnd; + self.w_max = self.cwnd; + } + + self.ssthresh = (self.cwnd as f64 * CUBIC_BETA) as u32; + self.ssthresh = self.ssthresh.max(2 * mss); + self.cwnd = self.ssthresh; + } + + /// Called on ECN congestion signal. Same as loss per RFC 9438. + pub fn on_ecn(&mut self) { + self.on_loss(); + } + + /// Called on RTO expiry. Saves state for F-RTO, resets to 1 MSS. + pub fn on_rto(&mut self) { + let mss = self.eff_mss as u32; + self.cwnd_before_rto = Some(self.cwnd); + self.ssthresh_before_rto = Some(self.ssthresh); + self.epoch_start = None; + + self.w_max_prev = self.w_max; + self.w_max = self.cwnd; + self.ssthresh = (self.cwnd as f64 * CUBIC_BETA) as u32; + self.ssthresh = self.ssthresh.max(2 * mss); + self.cwnd = mss; + } + + /// Restore cwnd/ssthresh after F-RTO determines RTO was spurious. + pub fn restore_after_spurious_rto(&mut self) { + if let (Some(cwnd), Some(ssthresh)) = (self.cwnd_before_rto, self.ssthresh_before_rto) { + self.cwnd = cwnd; + self.ssthresh = ssthresh; + self.cwnd_before_rto = None; + self.ssthresh_before_rto = None; + } + } + + // Stub for Task 2. + fn cubic_update(&mut self, _bytes_acked: u32, _now: Instant, _rtt_ms: u64) { + // Placeholder — Reno-like until Task 2. + let mss = self.eff_mss as u32; + self.ack_count += _bytes_acked; + if self.ack_count >= self.cwnd { + self.cwnd += mss; + self.ack_count -= self.cwnd - mss; + } + } +} +``` + +Also add `pub(crate) mod congestion;` to `src/net/handler/tcp/mod.rs` after line 5 (after `pub(crate) mod tcb;`). + +**Step 4: Run tests to verify they pass** + +Run: `cargo test -p voidnet congestion::tests` +Expected: All 5 tests PASS + +**Step 5: Commit** + +```bash +git add src/net/handler/tcp/congestion.rs src/net/handler/tcp/mod.rs +git commit -m "feat(tcp): add CubicState with slow start, loss, and RTO response" +``` + +--- + +### Task 2: Implement CUBIC congestion avoidance window function + +**Files:** +- Modify: `src/net/handler/tcp/congestion.rs` (replace `cubic_update` stub) + +**Step 1: Write the failing tests** + +Add to `congestion.rs` tests: + +```rust +#[test] +fn congestion_avoidance_grows_past_w_max() { + let mut cubic = CubicState::new(1460); + // Simulate loss at cwnd=100_000 to set w_max. + cubic.cwnd = 100_000; + cubic.on_loss(); + // Now cwnd = 70_000, ssthresh = 70_000, w_max = 100_000. + + let start = Instant::now(); + let rtt_ms = 50; + // Simulate ~400 ACKs over several seconds. + for i in 0..400 { + let elapsed_ms = (i as u64) * rtt_ms; + let now = start + coarsetime::Duration::from_millis(elapsed_ms); + cubic.on_ack(1460, now, rtt_ms); + } + // After enough time, cwnd should exceed w_max. + assert!( + cubic.cwnd > 100_000, + "cwnd {} should exceed w_max 100_000", + cubic.cwnd + ); +} + +#[test] +fn tcp_friendliness_cwnd_at_least_reno() { + let mut cubic = CubicState::new(1460); + // Loss at 50_000. + cubic.cwnd = 50_000; + cubic.on_loss(); + // cwnd = 35_000, w_max = 50_000. + + let start = Instant::now(); + let rtt_ms = 100; + let mut reno_cwnd = 35_000u32; + let mss = 1460u32; + for i in 0..200 { + let now = start + coarsetime::Duration::from_millis(i * rtt_ms); + cubic.on_ack(1460, now, rtt_ms); + // Reno: cwnd += MSS^2 / cwnd per ACK. + reno_cwnd += (mss * mss) / reno_cwnd; + } + assert!( + cubic.cwnd >= reno_cwnd - mss, + "CUBIC cwnd {} should be >= Reno cwnd {} (within 1 MSS)", + cubic.cwnd, + reno_cwnd + ); +} +``` + +**Step 2: Run tests to verify they fail** + +Run: `cargo test -p voidnet congestion::tests` +Expected: FAIL — `cubic_update` stub doesn't follow cubic function + +**Step 3: Implement the CUBIC window function** + +Replace the `cubic_update` stub: + +```rust +fn cubic_update(&mut self, bytes_acked: u32, now: Instant, rtt_ms: u64) { + let mss = self.eff_mss as u32; + + // Initialize epoch on first ACK in congestion avoidance. + if self.epoch_start.is_none() { + self.epoch_start = Some(now); + // If cwnd was less than w_max (e.g. after timeout), adjust K. + if self.cwnd < self.w_max { + self.k = ((self.w_max - self.cwnd) as f64 / CUBIC_C).cbrt(); + self.origin_point = self.w_max; + } else { + self.k = 0.0; + self.origin_point = self.cwnd; + } + self.ack_count = 0; + self.tcp_cwnd = self.cwnd; + } + + let epoch_start = self.epoch_start.unwrap(); + let t = now.duration_since(epoch_start).as_millis() as f64 / 1000.0; // seconds + + // W_cubic(t) = C * (t - K)^3 + origin_point. + let t_minus_k = t - self.k; + let w_cubic = (CUBIC_C * t_minus_k * t_minus_k * t_minus_k) as i64 + + self.origin_point as i64; + let w_cubic = (w_cubic.max(mss as i64)) as u32; + + // TCP-friendly estimate: W_est = origin_point * beta + 3 * (1-beta)/(1+beta) * t/RTT * MSS. + if rtt_ms > 0 { + let rtt_sec = rtt_ms as f64 / 1000.0; + let acks_since_epoch = t / rtt_sec; + let reno_inc = (3.0 * (1.0 - CUBIC_BETA) / (1.0 + CUBIC_BETA)) * acks_since_epoch; + self.tcp_cwnd = ((self.origin_point as f64 * CUBIC_BETA) + reno_inc * mss as f64) as u32; + } + + // Take the larger of CUBIC and TCP-friendly. + let target = w_cubic.max(self.tcp_cwnd); + + if target > self.cwnd { + // Increase cwnd. Scale by bytes_acked to handle byte counting. + let delta = target - self.cwnd; + // Increase by (delta * MSS / cwnd) per ACK for smoothness. + let inc = ((delta as u64 * mss as u64) / self.cwnd as u64) as u32; + self.cwnd += inc.max(1); + } + // If target <= cwnd, hold steady (plateau near w_max before concave growth). +} +``` + +**Step 4: Run tests to verify they pass** + +Run: `cargo test -p voidnet congestion::tests` +Expected: All 7 tests PASS + +**Step 5: Commit** + +```bash +git add src/net/handler/tcp/congestion.rs +git commit -m "feat(tcp): implement CUBIC congestion avoidance window function (RFC 9438)" +``` + +--- + +### Task 3: Create SackRecovery with IsLost, SetPipe, and entry/exit + +**Files:** +- Create: `src/net/handler/tcp/recovery.rs` +- Modify: `src/net/handler/tcp/mod.rs:1` (add `pub(crate) mod recovery;`) + +**Step 1: Write the failing tests** + +```rust +use std::collections::BTreeMap; + +const DUP_THRESH: u32 = 3; + +pub struct SackRecovery { + pub in_recovery: bool, + pub recovery_point: u32, + pub pipe: u32, + pub dup_ack_count: u8, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn is_lost_with_3_sacked_above() { + let recovery = SackRecovery::new(); + let mut scoreboard = BTreeMap::new(); + // SACKed: [2000..3000], [3000..4000], [4000..5000] — 3 blocks above seq 1000. + scoreboard.insert(2000u32, 1000u32); + scoreboard.insert(3000u32, 1000u32); + scoreboard.insert(4000u32, 1000u32); + assert!(recovery.is_lost(1000, &scoreboard, 1000)); + } + + #[test] + fn is_lost_with_large_gap() { + let recovery = SackRecovery::new(); + let mut scoreboard = BTreeMap::new(); + // Single SACK at [5000..6000] — 4000 bytes above seq 1000, > 3*MSS. + scoreboard.insert(5000u32, 1000u32); + assert!(recovery.is_lost(1000, &scoreboard, 1000)); + } + + #[test] + fn not_lost_with_insufficient_sacks() { + let recovery = SackRecovery::new(); + let mut scoreboard = BTreeMap::new(); + // Only 2 blocks above. + scoreboard.insert(2000u32, 1000u32); + scoreboard.insert(3000u32, 1000u32); + assert!(!recovery.is_lost(1000, &scoreboard, 1000)); + } + + #[test] + fn set_pipe_counts_in_flight() { + let mut recovery = SackRecovery::new(); + let mut scoreboard = BTreeMap::new(); + // snd_una=1000, snd_nxt=5000 => 4 MSS-sized blocks. + // Block [2000..3000] SACKed, block [1000..2000] is lost (3+ SACKed above). + scoreboard.insert(2000u32, 1000u32); + scoreboard.insert(3000u32, 1000u32); + scoreboard.insert(4000u32, 1000u32); + // [1000..2000]: lost => not counted. + // [2000..3000]: SACKed => not counted. + // [3000..4000]: SACKed => not counted. + // [4000..5000]: SACKed => not counted. + recovery.set_pipe(1000, 5000, &scoreboard, 1000); + assert_eq!(recovery.pipe, 0); + } + + #[test] + fn set_pipe_counts_unacked_unsacked_as_in_flight() { + let mut recovery = SackRecovery::new(); + let mut scoreboard = BTreeMap::new(); + // snd_una=1000, snd_nxt=6000 => 5 MSS blocks. + // Only [2000..3000] SACKed. [1000..2000] has only 1 SACK above — not lost. + scoreboard.insert(2000u32, 1000u32); + // [1000..2000]: not SACKed, only 1 block SACKed above (2000) — not lost => in flight. + // [2000..3000]: SACKed. + // [3000..4000]: not SACKed, 0 blocks above => in flight. + // [4000..5000]: not SACKed, 0 blocks above => in flight. + // [5000..6000]: not SACKed, 0 blocks above => in flight. + recovery.set_pipe(1000, 6000, &scoreboard, 1000); + assert_eq!(recovery.pipe, 4000); // 4 blocks in flight + } + + #[test] + fn enter_and_exit_recovery() { + let mut recovery = SackRecovery::new(); + recovery.enter(5000); + assert!(recovery.in_recovery); + assert_eq!(recovery.recovery_point, 5000); + + // Partial ACK — doesn't exit. + assert!(!recovery.on_ack(3000)); + assert!(recovery.in_recovery); + + // Full ACK — exits. + assert!(recovery.on_ack(5000)); + assert!(!recovery.in_recovery); + } +} +``` + +**Step 2: Run tests to verify they fail** + +Run: `cargo test -p voidnet recovery::tests` +Expected: FAIL — `SackRecovery` methods not implemented + +**Step 3: Implement SackRecovery** + +```rust +use std::collections::BTreeMap; +use crate::net::wire::tcp::{seq_le, seq_lt}; + +const DUP_THRESH: u32 = 3; + +pub struct SackRecovery { + pub in_recovery: bool, + pub recovery_point: u32, + pub pipe: u32, + pub dup_ack_count: u8, +} + +impl SackRecovery { + pub fn new() -> Self { + Self { + in_recovery: false, + recovery_point: 0, + pipe: 0, + dup_ack_count: 0, + } + } + + /// Enter SACK recovery. `snd_nxt` is the recovery point. + pub fn enter(&mut self, snd_nxt: u32) { + self.in_recovery = true; + self.recovery_point = snd_nxt; + } + + /// Exit recovery, reset state. + pub fn exit(&mut self) { + self.in_recovery = false; + self.dup_ack_count = 0; + self.pipe = 0; + } + + /// Process a new ACK during recovery. Returns true if recovery is complete. + pub fn on_ack(&mut self, seg_ack: u32) -> bool { + if seq_le(self.recovery_point, seg_ack) { + self.exit(); + true + } else { + false + } + } + + /// RFC 6675 §4: IsLost predicate. A segment starting at `seq` is lost if + /// DupThresh segments above it have been SACKed or the byte distance exceeds + /// DupThresh * MSS. + pub fn is_lost(&self, seq: u32, scoreboard: &BTreeMap, eff_mss: u16) -> bool { + let mss = eff_mss as u32; + let mut sacked_segments_above = 0u32; + let mut highest_sacked = seq; + + for (&start, &len) in scoreboard { + let end = start.wrapping_add(len); + if seq_lt(seq, start) { + // Count MSS-sized segments in this SACK block. + let block_segments = (len + mss - 1) / mss; + sacked_segments_above += block_segments; + if seq_lt(highest_sacked, end) { + highest_sacked = end; + } + } + } + + // Lost if 3+ segments SACKed above, or distance > 3*MSS. + let byte_distance = highest_sacked.wrapping_sub(seq); + sacked_segments_above >= DUP_THRESH || byte_distance >= DUP_THRESH * mss + } + + /// RFC 6675 §4.1: SetPipe — estimate bytes in the network. + /// Iterates MSS-sized blocks from snd_una to snd_nxt. + pub fn set_pipe( + &mut self, + snd_una: u32, + snd_nxt: u32, + scoreboard: &BTreeMap, + eff_mss: u16, + ) { + let mss = eff_mss as u32; + let mut pipe = 0u32; + let mut seq = snd_una; + + while seq_lt(seq, snd_nxt) { + let block_end = seq.wrapping_add(mss); + let is_sacked = self.is_sacked(seq, scoreboard); + let is_lost = self.is_lost(seq, scoreboard, eff_mss); + + if !is_sacked && !is_lost { + pipe += mss; + } + + seq = block_end; + } + + self.pipe = pipe; + } + + /// Check if a sequence number falls within a SACKed range. + fn is_sacked(&self, seq: u32, scoreboard: &BTreeMap) -> bool { + for (&start, &len) in scoreboard { + let end = start.wrapping_add(len); + if seq_le(start, seq) && seq_lt(seq, end) { + return true; + } + } + false + } + + /// Find the lowest lost sequence number >= `from` for retransmission. + pub fn next_lost_segment( + &self, + from: u32, + snd_nxt: u32, + scoreboard: &BTreeMap, + eff_mss: u16, + ) -> Option { + let mss = eff_mss as u32; + let mut seq = from; + while seq_lt(seq, snd_nxt) { + if !self.is_sacked(seq, scoreboard) && self.is_lost(seq, scoreboard, eff_mss) { + return Some(seq); + } + seq = seq.wrapping_add(mss); + } + None + } +} +``` + +Also add `pub(crate) mod recovery;` to `src/net/handler/tcp/mod.rs` after the congestion module line. + +**Step 4: Run tests to verify they pass** + +Run: `cargo test -p voidnet recovery::tests` +Expected: All 6 tests PASS + +**Step 5: Commit** + +```bash +git add src/net/handler/tcp/recovery.rs src/net/handler/tcp/mod.rs +git commit -m "feat(tcp): add SackRecovery with IsLost, SetPipe, entry/exit (RFC 6675)" +``` + +--- + +### Task 4: Add PrrState to recovery.rs + +**Files:** +- Modify: `src/net/handler/tcp/recovery.rs` + +**Step 1: Write the failing tests** + +Add to `recovery.rs` tests: + +```rust +#[test] +fn prr_proportional_when_pipe_above_ssthresh() { + let mut prr = PrrState::new(); + prr.enter(10_000); // recover_fs = 10_000. + let ssthresh = 7_000u32; + let mss = 1460u16; + + // Simulate ACK delivering 1460 bytes. + let snd_cnt = prr.on_ack(1460, 8_000, ssthresh, mss); + // pipe=8000 > ssthresh=7000 => proportional. + // snd_cnt = ceil(1460 * 7000 / 10000) - 0 = ceil(1022) = 1022. + assert_eq!(snd_cnt, 1022); + + // After "sending" 1022 bytes. + prr.on_sent(1022); + // Second ACK. + let snd_cnt = prr.on_ack(1460, 7_000, ssthresh, mss); + // snd_cnt = ceil(2920 * 7000 / 10000) - 1022 = ceil(2044) - 1022 = 1022. + assert_eq!(snd_cnt, 1022); +} + +#[test] +fn prr_slow_start_reduction_when_pipe_below_ssthresh() { + let mut prr = PrrState::new(); + prr.enter(10_000); + let ssthresh = 7_000u32; + let mss = 1460u16; + + // pipe=5000 < ssthresh. + let snd_cnt = prr.on_ack(1460, 5_000, ssthresh, mss); + // snd_cnt = min(7000 - 5000, 1460 - 0 + 1460) = min(2000, 2920) = 2000. + assert_eq!(snd_cnt, 2000); +} +``` + +**Step 2: Run tests to verify they fail** + +Run: `cargo test -p voidnet recovery::tests::prr` +Expected: FAIL — `PrrState` not defined + +**Step 3: Implement PrrState** + +Add to `recovery.rs`: + +```rust +pub struct PrrState { + pub prr_delivered: u32, + pub prr_out: u32, + pub recover_fs: u32, +} + +impl PrrState { + pub fn new() -> Self { + Self { + prr_delivered: 0, + prr_out: 0, + recover_fs: 0, + } + } + + /// Enter recovery. `bytes_in_flight` = snd_nxt - snd_una at entry. + pub fn enter(&mut self, bytes_in_flight: u32) { + self.recover_fs = bytes_in_flight; + self.prr_delivered = 0; + self.prr_out = 0; + } + + /// Reset on recovery exit. + pub fn exit(&mut self) { + self.prr_delivered = 0; + self.prr_out = 0; + self.recover_fs = 0; + } + + /// Called on each ACK during recovery. Returns snd_cnt (bytes allowed to send). + /// `bytes_newly_delivered` = bytes_acked + bytes_newly_sacked. + /// `pipe` = current pipe estimate. `ssthresh` = target cwnd. + pub fn on_ack(&mut self, bytes_newly_delivered: u32, pipe: u32, ssthresh: u32, eff_mss: u16) -> u32 { + self.prr_delivered += bytes_newly_delivered; + + if pipe > ssthresh { + // Proportional: snd_cnt = ceil(prr_delivered * ssthresh / recover_fs) - prr_out. + let numer = self.prr_delivered as u64 * ssthresh as u64; + let target = ((numer + self.recover_fs as u64 - 1) / self.recover_fs as u64) as u32; + target.saturating_sub(self.prr_out) + } else { + // Slow start reduction bound. + let limit = self.prr_delivered.saturating_sub(self.prr_out) + eff_mss as u32; + ssthresh.saturating_sub(pipe).min(limit) + } + } + + /// Called after sending bytes during recovery. + pub fn on_sent(&mut self, bytes_sent: u32) { + self.prr_out += bytes_sent; + } +} +``` + +**Step 4: Run tests to verify they pass** + +Run: `cargo test -p voidnet recovery::tests` +Expected: All 8 tests PASS + +**Step 5: Commit** + +```bash +git add src/net/handler/tcp/recovery.rs +git commit -m "feat(tcp): add PRR for smooth sending during recovery (RFC 6937)" +``` + +--- + +### Task 5: Add FRtoState to recovery.rs + +**Files:** +- Modify: `src/net/handler/tcp/recovery.rs` + +**Step 1: Write the failing tests** + +Add to `recovery.rs` tests: + +```rust +#[test] +fn frto_spurious_rto_detected() { + let mut frto = FRtoState::new(); + frto.enter(1000); // snd_una at RTO = 1000. + + // First ACK advances snd_una. + let result = frto.on_ack(2000); + assert_eq!(result, FRtoAction::SendNewData); + + // Second ACK advances snd_una again => spurious. + let result = frto.on_ack(3000); + assert_eq!(result, FRtoAction::SpuriousRto); +} + +#[test] +fn frto_genuine_loss_on_first_dup_ack() { + let mut frto = FRtoState::new(); + frto.enter(1000); + + // First ACK is dup (snd_una doesn't advance). + let result = frto.on_ack(1000); + assert_eq!(result, FRtoAction::GenuineLoss); +} + +#[test] +fn frto_genuine_loss_on_second_dup_ack() { + let mut frto = FRtoState::new(); + frto.enter(1000); + + // First ACK advances. + let result = frto.on_ack(2000); + assert_eq!(result, FRtoAction::SendNewData); + + // Second ACK is dup. + let result = frto.on_ack(2000); + assert_eq!(result, FRtoAction::GenuineLoss); +} +``` + +**Step 2: Run tests to verify they fail** + +Run: `cargo test -p voidnet recovery::tests::frto` +Expected: FAIL — `FRtoState` not defined + +**Step 3: Implement FRtoState** + +Add to `recovery.rs`: + +```rust +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FRtoPhase { + Disabled, + Step1, + Step2, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FRtoAction { + /// No F-RTO active — proceed normally. + None, + /// Send new data (not retransmissions) to probe. + SendNewData, + /// Spurious RTO detected — restore cwnd. + SpuriousRto, + /// Genuine loss confirmed — keep reduced cwnd. + GenuineLoss, +} + +pub struct FRtoState { + pub phase: FRtoPhase, + snd_una_at_rto: u32, + snd_una_last: u32, +} + +impl FRtoState { + pub fn new() -> Self { + Self { + phase: FRtoPhase::Disabled, + snd_una_at_rto: 0, + snd_una_last: 0, + } + } + + /// Arm F-RTO on RTO retransmit. + pub fn enter(&mut self, snd_una: u32) { + self.phase = FRtoPhase::Step1; + self.snd_una_at_rto = snd_una; + self.snd_una_last = snd_una; + } + + /// Process ACK during F-RTO. `new_snd_una` is the updated snd_una after this ACK. + /// Returns the action to take. + pub fn on_ack(&mut self, new_snd_una: u32) -> FRtoAction { + match self.phase { + FRtoPhase::Disabled => FRtoAction::None, + FRtoPhase::Step1 => { + if seq_lt(self.snd_una_last, new_snd_una) { + // ACK advances — move to Step2, send new data. + self.snd_una_last = new_snd_una; + self.phase = FRtoPhase::Step2; + FRtoAction::SendNewData + } else { + // Dup ACK — genuine loss. + self.phase = FRtoPhase::Disabled; + FRtoAction::GenuineLoss + } + } + FRtoPhase::Step2 => { + if seq_lt(self.snd_una_last, new_snd_una) { + // Second advancing ACK — spurious RTO. + self.phase = FRtoPhase::Disabled; + FRtoAction::SpuriousRto + } else { + // Dup ACK — genuine loss. + self.phase = FRtoPhase::Disabled; + FRtoAction::GenuineLoss + } + } + } + } + + /// Check if F-RTO is active (in Step1 or Step2). + pub fn is_active(&self) -> bool { + self.phase != FRtoPhase::Disabled + } +} +``` + +**Step 4: Run tests to verify they pass** + +Run: `cargo test -p voidnet recovery::tests` +Expected: All 11 tests PASS + +**Step 5: Commit** + +```bash +git add src/net/handler/tcp/recovery.rs +git commit -m "feat(tcp): add F-RTO spurious RTO detection (RFC 5682)" +``` + +--- + +### Task 6: Update TCB to use new congestion/recovery structs + +**Files:** +- Modify: `src/net/handler/tcp/tcb.rs` — replace `cwnd`, `ssthresh`, `dup_ack_count` with new structs +- Modify: `src/net/handler/tcp/mod.rs` — update all TCB construction sites and field accesses + +This is a refactoring task. The goal is to swap fields in the TCB and update all references. No new behavior yet — the integration comes in Tasks 7-10. + +**Step 1: Update TCB struct** + +In `src/net/handler/tcp/tcb.rs`: +- Add imports: `use super::congestion::CubicState;` and `use super::recovery::{SackRecovery, PrrState, FRtoState};` +- Remove fields: `cwnd: u32`, `ssthresh: u32`, `dup_ack_count: u8` +- Add fields: `pub cubic: CubicState`, `pub recovery: SackRecovery`, `pub prr: PrrState`, `pub frto: FRtoState` +- Update `advertised_window()` — currently doesn't reference cwnd, so no change needed there. + +**Step 2: Update TCB construction sites in mod.rs** + +There are 2 TCB construction sites: +1. Active open (connect): `src/net/handler/tcp/mod.rs:229-293` — replace `cwnd: 10 * DEFAULT_RCV_MSS as u32, ssthresh: u32::MAX, dup_ack_count: 0` with `cubic: CubicState::new(DEFAULT_RCV_MSS), recovery: SackRecovery::new(), prr: PrrState::new(), frto: FRtoState::new()` +2. Passive open (SYN-RECEIVED): `src/net/handler/tcp/mod.rs:806-872` — same replacement, use `CubicState::new(peer_mss.min(DEFAULT_RCV_MSS))` + +**Step 3: Update all field accesses** + +Search for `tcb.cwnd`, `tcb.ssthresh`, `tcb.dup_ack_count` and replace: +- `tcb.cwnd` → `tcb.cubic.cwnd` +- `tcb.ssthresh` → `tcb.cubic.ssthresh` +- `tcb.dup_ack_count` → `tcb.recovery.dup_ack_count` + +Key locations (approximate lines, may shift after earlier tasks): +- ACK processing (~1486-1492): `tcb.cwnd` / `tcb.ssthresh` → `tcb.cubic.cwnd` / `tcb.cubic.ssthresh` +- Dup ACK counter (~1602): `tcb.dup_ack_count` → `tcb.recovery.dup_ack_count` +- Fast retransmit (~1951): `tcb.dup_ack_count` → `tcb.recovery.dup_ack_count` +- Fast recovery cwnd halving (~2023-2026): `tcb.cwnd` / `tcb.ssthresh` → `tcb.cubic.cwnd` / `tcb.cubic.ssthresh` +- RTO cwnd reset (~2152-2154): same +- poll_send (~2244): `tcb.cwnd` → `tcb.cubic.cwnd` +- ECN response (~1567-1570): `tcb.cwnd` / `tcb.ssthresh` → `tcb.cubic.cwnd` / `tcb.cubic.ssthresh` +- All test assertions referencing `tcb.cwnd`, `tcb.ssthresh`, `tcb.dup_ack_count` + +Also update the `make_tcb` helper in `tcb.rs` tests to use the new structs. + +**Step 4: Run all tests to verify nothing is broken** + +Run: `cargo test` +Expected: All tests PASS (behavior unchanged, just field access through structs) + +**Step 5: Commit** + +```bash +git add src/net/handler/tcp/tcb.rs src/net/handler/tcp/mod.rs +git commit -m "refactor(tcp): replace cwnd/ssthresh/dup_ack_count with CubicState/SackRecovery/PrrState/FRtoState" +``` + +--- + +### Task 7: Integrate CUBIC into ACK processing + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs` — ACK processing path + +Replace the inline Reno congestion control with calls to `CubicState`. + +**Step 1: Write integration test** + +Add to `mod.rs` tests: + +```rust +#[test] +fn cubic_congestion_control_on_new_ack() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + for i in 0..16 { free.push(alloc_free_frame(100 + i)); } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1000, 0, flags::SYN, 65535, &[]); + let syn_len = syn_data.len(); + handler.process_ipv4(Frame::new(0, leak(syn_data), syn_len, false), &nh, &mut free, &mut rx, &mut tx); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1001, server_iss.wrapping_add(1), flags::ACK, 65535, &[]); + let ack_len = ack_data.len(); + handler.process_ipv4(Frame::new(1, leak(ack_data), ack_len, false), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + + let cwnd_before = handler.connections[0].cubic.cwnd; + let mss = handler.connections[0].eff_snd_mss; + + // Send data and get it ACKed. + handler.connections[0].send_buffer.write(&[0xAA; 1460]); + handler.connections[0].snd_wnd = 65535; + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + while tx.pop().is_some() {} + + // ACK the data. + let snd_nxt = handler.connections[0].snd_nxt; + let ack = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1001, snd_nxt, flags::ACK, 65535, &[]); + let ack_len = ack.len(); + handler.process_ipv4(Frame::new(2, leak(ack), ack_len, false), &nh, &mut free, &mut rx, &mut tx); + + // In slow start: cwnd should increase by MSS. + let cwnd_after = handler.connections[0].cubic.cwnd; + assert_eq!(cwnd_after, cwnd_before + mss as u32, "slow start: cwnd += MSS"); +} +``` + +**Step 2: Run test to verify it fails** + +Run: `cargo test -p voidnet cubic_congestion_control_on_new_ack` +Expected: FAIL — ACK processing still uses inline Reno + +**Step 3: Replace inline Reno with CubicState calls** + +In `process_established` ACK processing (~line 1484-1492), replace: + +```rust +// OLD: +let eff_mss = tcb.eff_snd_mss as u32; +if tcb.cwnd < tcb.ssthresh { + tcb.cwnd += eff_mss; +} else { + tcb.cwnd += (eff_mss * eff_mss) / tcb.cwnd; +} +tcb.dup_ack_count = 0; +``` + +With: + +```rust +// NEW: +if !tcb.recovery.in_recovery { + let rtt_ms = tcb.srtt.unwrap_or(tcb.rto); + tcb.cubic.on_ack(bytes_acked as u32, now, rtt_ms); +} +tcb.recovery.dup_ack_count = 0; +``` + +Also replace the ECN response (~line 1567-1570): + +```rust +// OLD: +tcb.ssthresh = (tcb.cwnd / 2).max(2 * tcb.eff_snd_mss as u32); +tcb.cwnd = tcb.ssthresh; + +// NEW: +tcb.cubic.on_ecn(); +``` + +**Step 4: Run tests to verify they pass** + +Run: `cargo test` +Expected: All tests PASS + +**Step 5: Commit** + +```bash +git add src/net/handler/tcp/mod.rs +git commit -m "feat(tcp): integrate CUBIC into ACK processing, replacing inline Reno" +``` + +--- + +### Task 8: Integrate SACK recovery into fast retransmit + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs` — fast retransmit path in `poll_timers` and dup ACK handling + +**Step 1: Write integration test** + +Add to `mod.rs` tests: + +```rust +#[test] +fn sack_recovery_enters_on_3_dup_acks() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(64); + let mut rx = BasicFrameBuffer::new(64); + let mut tx = BasicFrameBuffer::new(64); + for i in 0..32 { free.push(alloc_free_frame(100 + i)); } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1000, 0, flags::SYN, 65535, &[]); + let syn_len = syn_data.len(); + handler.process_ipv4(Frame::new(0, leak(syn_data), syn_len, false), &nh, &mut free, &mut rx, &mut tx); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1001, server_iss.wrapping_add(1), flags::ACK, 65535, &[]); + let ack_len = ack_data.len(); + handler.process_ipv4(Frame::new(1, leak(ack_data), ack_len, false), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + + // Send 4 segments. + handler.connections[0].send_buffer.write(&[0xAA; 5840]); // 4 * 1460 + handler.connections[0].snd_wnd = 65535; + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + while tx.pop().is_some() {} + + let snd_una = handler.connections[0].snd_una; + + // Send 3 dup ACKs (ACK for snd_una, no data). + for i in 0..3 { + let dup = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1001, snd_una, flags::ACK, 65535, &[]); + let dup_len = dup.len(); + handler.process_ipv4(Frame::new(10 + i, leak(dup), dup_len, false), &nh, &mut free, &mut rx, &mut tx); + } + + assert!(handler.connections[0].recovery.in_recovery, "should be in recovery"); + assert_eq!(handler.connections[0].recovery.dup_ack_count, 3); +} + +#[test] +fn sack_recovery_partial_ack_stays_in_recovery() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(64); + let mut rx = BasicFrameBuffer::new(64); + let mut tx = BasicFrameBuffer::new(64); + for i in 0..32 { free.push(alloc_free_frame(100 + i)); } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1000, 0, flags::SYN, 65535, &[]); + let syn_len = syn_data.len(); + handler.process_ipv4(Frame::new(0, leak(syn_data), syn_len, false), &nh, &mut free, &mut rx, &mut tx); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1001, server_iss.wrapping_add(1), flags::ACK, 65535, &[]); + let ack_len = ack_data.len(); + handler.process_ipv4(Frame::new(1, leak(ack_data), ack_len, false), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + + // Send 4 segments. + handler.connections[0].send_buffer.write(&[0xAA; 5840]); + handler.connections[0].snd_wnd = 65535; + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + while tx.pop().is_some() {} + + let snd_una = handler.connections[0].snd_una; + let recovery_point = handler.connections[0].snd_nxt; + + // 3 dup ACKs → enter recovery. + for i in 0..3 { + let dup = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1001, snd_una, flags::ACK, 65535, &[]); + let dup_len = dup.len(); + handler.process_ipv4(Frame::new(10 + i, leak(dup), dup_len, false), &nh, &mut free, &mut rx, &mut tx); + } + assert!(handler.connections[0].recovery.in_recovery); + + // Partial ACK — advances snd_una but doesn't reach recovery_point. + let partial_ack_seq = snd_una.wrapping_add(1460); + let partial = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1001, partial_ack_seq, flags::ACK, 65535, &[]); + let partial_len = partial.len(); + handler.process_ipv4(Frame::new(20, leak(partial), partial_len, false), &nh, &mut free, &mut rx, &mut tx); + + assert!(handler.connections[0].recovery.in_recovery, "should still be in recovery after partial ACK"); + assert_eq!(handler.connections[0].snd_una, partial_ack_seq); +} +``` + +**Step 2: Run tests to verify they fail** + +Run: `cargo test -p voidnet sack_recovery_enters_on_3_dup_acks sack_recovery_partial_ack_stays_in_recovery` +Expected: FAIL — dup ACK path doesn't enter recovery + +**Step 3: Integrate recovery into dup ACK and fast retransmit paths** + +In the **dup ACK branch** (~line 1599-1633), after incrementing dup_ack_count, add recovery entry: + +```rust +// After: tcb.recovery.dup_ack_count += 1; +// Add: +if tcb.recovery.dup_ack_count == 3 && !tcb.recovery.in_recovery { + let bytes_in_flight = tcb.snd_nxt.wrapping_sub(tcb.snd_una); + tcb.recovery.enter(tcb.snd_nxt); + tcb.prr.enter(bytes_in_flight); + tcb.cubic.on_loss(); +} +``` + +In the **new ACK branch** (~line 1473), add recovery exit check: + +```rust +// After updating snd_una, before congestion control: +if tcb.recovery.in_recovery { + if tcb.recovery.on_ack(seg_ack) { + // Exited recovery. + tcb.prr.exit(); + } +} +``` + +Replace the **fast retransmit block** in `poll_timers` (~line 1948-2027) with RFC 6675 recovery loop: + +```rust +// Replace: if tcb.state != TcpState::Established || tcb.dup_ack_count < 3 { continue; } +// With: if tcb.state != TcpState::Established || !tcb.recovery.in_recovery { continue; } +``` + +Then use `recovery.set_pipe()` + `recovery.next_lost_segment()` to pick what to retransmit, gate on `pipe < cwnd`, and track PRR. + +Replace the cwnd halving: +```rust +// OLD: +tcb.ssthresh = (tcb.cwnd / 2).max(2 * tcb.eff_snd_mss as u32); +tcb.cwnd = tcb.ssthresh; +tcb.dup_ack_count = 0; + +// REMOVE — cubic.on_loss() already called at recovery entry. +``` + +**Step 4: Run tests to verify they pass** + +Run: `cargo test` +Expected: All tests PASS + +**Step 5: Commit** + +```bash +git add src/net/handler/tcp/mod.rs +git commit -m "feat(tcp): integrate RFC 6675 SACK recovery with PRR into fast retransmit" +``` + +--- + +### Task 9: Integrate F-RTO into RTO path + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs` — RTO retransmit path and ACK processing + +**Step 1: Write integration test** + +Add to `mod.rs` tests: + +```rust +#[test] +fn frto_spurious_rto_restores_cwnd() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(64); + let mut rx = BasicFrameBuffer::new(64); + let mut tx = BasicFrameBuffer::new(64); + for i in 0..32 { free.push(alloc_free_frame(100 + i)); } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1000, 0, flags::SYN, 65535, &[]); + let syn_len = syn_data.len(); + handler.process_ipv4(Frame::new(0, leak(syn_data), syn_len, false), &nh, &mut free, &mut rx, &mut tx); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1001, server_iss.wrapping_add(1), flags::ACK, 65535, &[]); + let ack_len = ack_data.len(); + handler.process_ipv4(Frame::new(1, leak(ack_data), ack_len, false), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + + // Send data. + handler.connections[0].send_buffer.write(&[0xAA; 2920]); + handler.connections[0].snd_wnd = 65535; + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + while tx.pop().is_some() {} + + let cwnd_before_rto = handler.connections[0].cubic.cwnd; + + // Simulate RTO by setting deadline in the past and calling poll_timers. + handler.connections[0].retransmit_deadline = Some(now); + let rto_time = now + coarsetime::Duration::from_millis(1100); + handler.poll_timers(rto_time, nh.local_mac(), &nh, &mut free, &mut tx); + while tx.pop().is_some() {} + + // F-RTO should be in Step1. + assert!(handler.connections[0].frto.is_active()); + assert_eq!(handler.connections[0].cubic.cwnd, handler.connections[0].eff_snd_mss as u32); + + // First ACK advances snd_una. + let snd_una = handler.connections[0].snd_una; + let ack1 = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1001, snd_una.wrapping_add(1460), flags::ACK, 65535, &[]); + let ack1_len = ack1.len(); + handler.process_ipv4(Frame::new(10, leak(ack1), ack1_len, false), &nh, &mut free, &mut rx, &mut tx); + + // Second ACK advances snd_una again => spurious. + let snd_una = handler.connections[0].snd_una; + let ack2 = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1001, snd_una.wrapping_add(1460), flags::ACK, 65535, &[]); + let ack2_len = ack2.len(); + handler.process_ipv4(Frame::new(11, leak(ack2), ack2_len, false), &nh, &mut free, &mut rx, &mut tx); + + // cwnd should be restored. + assert_eq!(handler.connections[0].cubic.cwnd, cwnd_before_rto, "cwnd restored after spurious RTO"); + assert!(!handler.connections[0].frto.is_active()); +} +``` + +**Step 2: Run test to verify it fails** + +Run: `cargo test -p voidnet frto_spurious_rto_restores_cwnd` +Expected: FAIL — F-RTO not integrated + +**Step 3: Integrate F-RTO** + +In the **RTO Established** path in `poll_timers` (~line 2123-2159), add F-RTO entry: + +```rust +// After the existing RTO handling: +tcb.frto.enter(tcb.snd_una); +tcb.cubic.on_rto(); +tcb.recovery.exit(); +tcb.sack_scoreboard.clear(); +``` + +Replace the inline cwnd reset: +```rust +// OLD: +tcb.ssthresh = (tcb.cwnd / 2).max(2 * tcb.eff_snd_mss as u32); +tcb.cwnd = tcb.eff_snd_mss as u32; +// REMOVE — cubic.on_rto() handles this. +``` + +In **ACK processing** (new ACK branch), add F-RTO check before normal congestion control: + +```rust +// Before the existing congestion control: +if tcb.frto.is_active() { + let action = tcb.frto.on_ack(seg_ack); + match action { + FRtoAction::SpuriousRto => { + tcb.cubic.restore_after_spurious_rto(); + } + FRtoAction::GenuineLoss => { + // Keep reduced cwnd, proceed normally. + } + FRtoAction::SendNewData => { + // F-RTO Step1→Step2: prefer sending new data in poll_send. + // No cwnd changes needed here. + } + FRtoAction::None => {} + } +} +``` + +**Step 4: Run tests to verify they pass** + +Run: `cargo test` +Expected: All tests PASS + +**Step 5: Commit** + +```bash +git add src/net/handler/tcp/mod.rs +git commit -m "feat(tcp): integrate F-RTO into RTO path for spurious timeout detection (RFC 5682)" +``` + +--- + +### Task 10: Integrate Limited Transmit and PRR into poll_send + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs` — `poll_send` method + +**Step 1: Write integration tests** + +Add to `mod.rs` tests: + +```rust +#[test] +fn limited_transmit_sends_on_first_dup_ack() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(64); + let mut rx = BasicFrameBuffer::new(64); + let mut tx = BasicFrameBuffer::new(64); + for i in 0..32 { free.push(alloc_free_frame(100 + i)); } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1000, 0, flags::SYN, 65535, &[]); + let syn_len = syn_data.len(); + handler.process_ipv4(Frame::new(0, leak(syn_data), syn_len, false), &nh, &mut free, &mut rx, &mut tx); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1001, server_iss.wrapping_add(1), flags::ACK, 65535, &[]); + let ack_len = ack_data.len(); + handler.process_ipv4(Frame::new(1, leak(ack_data), ack_len, false), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + + // Fill send buffer with plenty of data, shrink cwnd to limit sending. + let mss = handler.connections[0].eff_snd_mss as usize; + handler.connections[0].send_buffer.write(&vec![0xAA; mss * 6]); + handler.connections[0].snd_wnd = 65535; + // Set cwnd to exactly 3*MSS so we can only send 3 segments. + handler.connections[0].cubic.cwnd = (mss * 3) as u32; + + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + while tx.pop().is_some() {} + // 3 segments sent, 3*MSS in flight. + + let snd_una = handler.connections[0].snd_una; + let snd_nxt_before = handler.connections[0].snd_nxt; + + // First dup ACK. + let dup = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1001, snd_una, flags::ACK, 65535, &[]); + let dup_len = dup.len(); + handler.process_ipv4(Frame::new(10, leak(dup), dup_len, false), &nh, &mut free, &mut rx, &mut tx); + + // poll_send should allow 1 MSS of new data (limited transmit). + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + let snd_nxt_after = handler.connections[0].snd_nxt; + assert_eq!( + snd_nxt_after.wrapping_sub(snd_nxt_before) as usize, + mss, + "limited transmit: 1 MSS sent on first dup ACK" + ); +} + +#[test] +fn prr_gates_sending_during_recovery() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(64); + let mut rx = BasicFrameBuffer::new(64); + let mut tx = BasicFrameBuffer::new(64); + for i in 0..32 { free.push(alloc_free_frame(100 + i)); } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1000, 0, flags::SYN, 65535, &[]); + let syn_len = syn_data.len(); + handler.process_ipv4(Frame::new(0, leak(syn_data), syn_len, false), &nh, &mut free, &mut rx, &mut tx); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1001, server_iss.wrapping_add(1), flags::ACK, 65535, &[]); + let ack_len = ack_data.len(); + handler.process_ipv4(Frame::new(1, leak(ack_data), ack_len, false), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + + let mss = handler.connections[0].eff_snd_mss as usize; + handler.connections[0].send_buffer.write(&vec![0xAA; mss * 10]); + handler.connections[0].snd_wnd = 65535; + handler.connections[0].cubic.cwnd = (mss * 10) as u32; + + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + while tx.pop().is_some() {} + + let snd_una = handler.connections[0].snd_una; + + // 3 dup ACKs → enter recovery. + for i in 0..3 { + let dup = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1001, snd_una, flags::ACK, 65535, &[]); + let dup_len = dup.len(); + handler.process_ipv4(Frame::new(10 + i, leak(dup), dup_len, false), &nh, &mut free, &mut rx, &mut tx); + } + + assert!(handler.connections[0].recovery.in_recovery); + // cwnd should be reduced (CUBIC beta=0.7). + let cwnd = handler.connections[0].cubic.cwnd; + assert!(cwnd < (mss * 10) as u32, "cwnd reduced after loss"); +} +``` + +**Step 2: Run tests to verify they fail** + +Run: `cargo test -p voidnet limited_transmit_sends prr_gates_sending` +Expected: FAIL + +**Step 3: Integrate Limited Transmit and PRR into poll_send** + +In `poll_send` (~line 2237), update the send budget calculation: + +```rust +// Compute how many bytes we can send. +let bytes_in_flight = tcb.snd_nxt.wrapping_sub(tcb.snd_una) as usize; + +let can_send = if tcb.recovery.in_recovery { + // During recovery: use pipe + PRR to gate sending. + tcb.recovery.set_pipe( + tcb.snd_una, + tcb.snd_nxt, + &tcb.sack_scoreboard, + tcb.eff_snd_mss, + ); + let pipe = tcb.recovery.pipe as usize; + let cwnd = tcb.cubic.cwnd as usize; + if pipe < cwnd { + // PRR determines actual budget. + // Note: prr.on_ack was already called in ACK processing. + (cwnd - pipe).min(tcb.eff_snd_mss as usize) + } else { + 0 + } +} else { + let send_window = (tcb.snd_wnd as usize).min(tcb.cubic.cwnd as usize); + let mut budget = send_window.saturating_sub(bytes_in_flight); + // Limited Transmit (RFC 3042): on 1st/2nd dup ACK, allow extra MSS. + if tcb.recovery.dup_ack_count > 0 + && tcb.recovery.dup_ack_count <= 2 + && !tcb.recovery.in_recovery + { + budget += tcb.recovery.dup_ack_count as usize * tcb.eff_snd_mss as usize; + } + budget +}; +``` + +**Step 4: Run tests to verify they pass** + +Run: `cargo test` +Expected: All tests PASS + +**Step 5: Commit** + +```bash +git add src/net/handler/tcp/mod.rs +git commit -m "feat(tcp): integrate Limited Transmit (RFC 3042) and PRR (RFC 6937) into poll_send" +``` + +--- + +### Task 11: Final pass — update MSS after negotiation and verify all tests + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs` — SYN-ACK processing (where eff_snd_mss is finalized) + +After MSS negotiation completes (SYN-ACK received in SynSent, or ACK in SynReceived), call `tcb.cubic.set_mss(tcb.eff_snd_mss)` to ensure CUBIC uses the negotiated MSS. + +**Step 1: Find MSS negotiation sites** + +Search for `eff_snd_mss` assignment in `mod.rs` — it's set during TCB construction and potentially updated when processing the SYN-ACK. Add `cubic.set_mss()` call after each site where `eff_snd_mss` is finalized. + +**Step 2: Add the set_mss calls** + +At each site where `tcb.eff_snd_mss` is set post-construction: + +```rust +tcb.cubic.set_mss(tcb.eff_snd_mss); +``` + +**Step 3: Run full test suite** + +Run: `cargo test` +Expected: All tests PASS + +**Step 4: Commit** + +```bash +git add src/net/handler/tcp/mod.rs +git commit -m "fix(tcp): sync CUBIC MSS after negotiation completes" +``` diff --git a/docs/plans/2026-03-07-tcp-correctness-ecn-socket-design.md b/docs/plans/2026-03-07-tcp-correctness-ecn-socket-design.md new file mode 100644 index 0000000..a15803c --- /dev/null +++ b/docs/plans/2026-03-07-tcp-correctness-ecn-socket-design.md @@ -0,0 +1,167 @@ +# TCP Protocol Correctness, ECN, and Socket API Design + +**Goal:** Improve RFC compliance (PSH flag, ECN, simultaneous open), add IPv6 fragment visibility, and surface TCP events as errors in the socket API. + +**Architecture:** Five independent features that can be implemented in any order. PSH and simultaneous open are small correctness fixes. ECN is the largest piece (negotiation + send/receive + congestion response). IPv6 fragment visibility is a logging change. Socket API error surfacing connects the existing event_queue to read/write futures. + +**Tech Stack:** Rust, coarsetime for timers, existing TCP handler infrastructure. + +--- + +## 1. PSH Flag on Outgoing Segments + +### Problem + +PSH flag is never set on outgoing data segments. Peers that buffer received data until PSH may experience unnecessary latency. + +### Design + +Set `PSH | ACK` (instead of just `ACK`) on the last data segment in a burst from `poll_send`. "Last in burst" means: after sending this segment, either the send buffer has no more data beyond what's in flight, or the send window is exhausted. + +**Detection in poll_send:** After computing `to_send`, check if `bytes_in_flight + to_send >= send_buffer.available()` or `to_send >= can_send` (window exhausted). If either is true, this is the last segment — set PSH. + +**Implementation:** `build_data_from_slices` currently hardcodes `flags::ACK`. Add a `tcp_flags: u8` parameter (or a `psh: bool` that gets OR'd). Callers in `poll_send` pass `flags::ACK | flags::PSH` when appropriate, `flags::ACK` otherwise. Retransmit paths (fast retransmit, RTO) don't set PSH — they're resending, not originating. + +--- + +## 2. IPv6 Fragment Visibility + +### Problem + +`src/net/handler/ipv6.rs` silently drops fragmented IPv6 TCP segments with a comment. This makes debugging MTU issues impossible. + +### Design + +Replace the silent drop with: +1. Increment an atomic counter (`ipv6_fragments_dropped: AtomicU64`) on the handler +2. Log at debug level on first occurrence (avoid log spam) +3. Do NOT implement full reassembly — PMTU discovery should prevent fragmentation in practice + +**Optional:** If we have the peer's PMTU cached, we could send ICMPv6 Packet Too Big. But this is complex (need to construct ICMPv6) and the peer should already be doing PMTU discovery. Skip for now — just make it visible. + +**Where:** Add counter field to `Ipv6Handler`, increment on fragment drop, expose via a getter. + +--- + +## 3. ECN (RFC 3168) + +### Negotiation (in SYN/SYN-ACK) + +**Active open (connect):** Set ECE + CWR flags in SYN segment to indicate ECN capability. + +**Passive open (listen):** When incoming SYN has ECE + CWR, set ECE flag (only) in SYN-ACK to confirm ECN support. + +**Completion:** After handshake, set `tcb.ecn_enabled = true` if both sides agreed. If peer's SYN-ACK lacks ECE, disable. + +**New TcpConfig field:** `ecn: bool` (default `true`). Controls whether we offer ECN in SYN. + +### Sending (IP header ECT codepoint) + +When `ecn_enabled`, set ECT(0) codepoint (`0b10`) in IPv4 ToS / IPv6 Traffic Class field on all outgoing data segments. Do NOT set on pure ACKs or retransmissions (RFC 3168 says retransmits should not be ECT-marked). + +**Where:** `build_ipv4_data_segment_slices` / `build_ipv6_data_segment_slices` — add `ecn_ect: bool` parameter. When true, set the 2 ECN bits in the IP header to `10` (ECT(0)). + +### Receiving CE (Congestion Experienced) + +When an incoming data segment has CE codepoint (`0b11`) in IP header: +1. Set `tcb.ecn_ce_received = true` +2. On next outgoing ACK/data, set ECE flag + +Continue sending ECE on every ACK until sender acknowledges with CWR. + +**Where:** `process_established` — after parsing IP header, check ECN bits. If CE, set flag. In segment builder calls, pass ECE flag when `ecn_ce_received`. + +### Receiving ECE (from peer) + +When we receive an ACK with ECE flag set: +1. Halve cwnd (same as loss response): `ssthresh = cwnd / 2; cwnd = ssthresh` +2. Set CWR flag on next outgoing data segment +3. Set `ecn_cwr_sent = true` to avoid reacting multiple times per RTT +4. Reset `ecn_cwr_sent` when new data is ACKed (new RTT) + +**Where:** `process_established` ACK processing — check ECE flag. `poll_send` — set CWR on next data if needed. + +### Receiving CWR (from peer) + +When we receive a segment with CWR flag: +1. Clear `ecn_ce_received` — stop sending ECE + +**Where:** `process_established` — check CWR flag before data processing. + +### New TCB Fields + +- `ecn_enabled: bool` — negotiated during handshake +- `ecn_ce_received: bool` — CE mark seen, send ECE until CWR received +- `ecn_cwr_sent: bool` — we sent CWR, don't react to ECE again this RTT + +### New TcpConfig Field + +- `ecn: bool` (default `true`) + +--- + +## 4. Socket API — Errors from read/write + +### Problem + +`TcpEvent` variants (Reset, Timeout) are pushed to `event_queue` on the TCB but the socket layer never reads them. `read()` returns 0 for both EOF and error conditions. + +### Design + +**Change return types:** +- `TcpStream::read()` → returns `Result` instead of `usize` +- `TcpStream::write()` → returns `Result` instead of `usize` + +**Event mapping:** +- `TcpEvent::Reset` → `Err(TcpError::Reset)` +- `TcpEvent::Timeout` → `Err(TcpError::Timeout)` +- `TcpEvent::RemoteClose` → `Ok(0)` on read (EOF), `Err(TcpError::NotConnected)` on write +- `TcpEvent::ConnectionRefused` → Already handled by connect future + +**Implementation in TcpRead/TcpWrite futures:** +Before checking the recv/send buffer, poll the event_queue. If an error event is present, return immediately with the corresponding error. + +**TcpError** already exists in `tcb.rs`: +```rust +pub enum TcpError { + ConnectionRefused, + Timeout, + Reset, + NotConnected, +} +``` + +**Impact on examples:** `tcp-echo-server.rs` and `tcp-echo-client.rs` call `.await` on read/write which return `usize`. They'll need to handle `Result`. Update to use `.await?` or `.await.unwrap()`. + +--- + +## 5. Simultaneous Open Verification + +### Problem + +Simultaneous open (both sides send SYN before receiving peer's SYN) is a rare but RFC-required scenario. The handler has code for it in `process_syn_sent` (transitions to SynReceived on bare SYN), but it's never been tested. + +### Design + +**Verify existing code:** Read `process_syn_sent` and trace the SYN → SynReceived → Established path when both sides are in SynSent. + +**Add integration test:** Two connections in SynSent, each receives the other's SYN. Verify both reach Established. + +**Fix if needed:** If the code path has bugs, fix them. If it works, just add the test. + +**Where:** `mod.rs` process_syn_sent (existing code), new test. + +--- + +## Scope + +**Modified files:** +- `src/net/handler/tcp/mod.rs` — PSH in poll_send, ECN in handshake/established, simultaneous open test +- `src/net/handler/tcp/tcb.rs` — ECN fields, TcpConfig ecn field +- `src/net/handler/tcp/segment.rs` — flags parameter for build_data_from_slices, ECT in IP header +- `src/net/socket/tcp.rs` — read/write return Result, event consumption +- `src/net/handler/ipv6.rs` — fragment drop counter +- `examples/tcp-echo-server.rs` — handle Result from read/write +- `examples/tcp-echo-client.rs` — handle Result from read/write + +**No new files.** diff --git a/docs/plans/2026-03-07-tcp-correctness-ecn-socket-impl.md b/docs/plans/2026-03-07-tcp-correctness-ecn-socket-impl.md new file mode 100644 index 0000000..f562fe7 --- /dev/null +++ b/docs/plans/2026-03-07-tcp-correctness-ecn-socket-impl.md @@ -0,0 +1,773 @@ +# TCP Correctness, ECN, and Socket API Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add PSH flag on outgoing segments, IPv6 fragment drop visibility, full ECN (RFC 3168), error-returning socket API, and simultaneous open verification. + +**Architecture:** Five independent features. PSH and simultaneous open are small. ECN is multi-step (TCB fields → negotiation → send/receive → congestion response). Socket API changes read/write return types from `usize` to `Result`. IPv6 fragment visibility is a counter addition. + +**Tech Stack:** Rust, coarsetime for timers, existing TCP handler infrastructure in `src/net/handler/tcp/`. + +**Testing:** `cargo test` (no feature flags, no `--all-features`). Tests run under `sudo -E` via `.cargo/config.toml`. + +**Key files reference:** +- Wire format: `src/net/wire/tcp.rs` (flags: ECE=0x40, CWR=0x80, PSH=0x08) +- TCB/config: `src/net/handler/tcp/tcb.rs` +- Handler: `src/net/handler/tcp/mod.rs` +- Segment builder: `src/net/handler/tcp/segment.rs` +- Socket layer: `src/net/socket/tcp.rs` +- IPv6 handler: `src/net/handler/ipv6.rs` +- Examples: `examples/tcp-echo-server.rs`, `examples/tcp-echo-client.rs` + +--- + +## Task 1: PSH Flag on Outgoing Data Segments + +**Files:** +- Modify: `src/net/handler/tcp/segment.rs:548-616` (build_data_from_slices) +- Modify: `src/net/handler/tcp/mod.rs:2003-2050` (poll_send data path) + +**Context:** `build_data_from_slices` hardcodes `flags::ACK` at lines 584 and 603 when calling the internal IPv4/IPv6 helpers. The internal helpers (`build_ipv4_data_segment_slices`, `build_ipv6_data_segment_slices`) already accept a `tcp_flags: u8` parameter — the public method just never passes anything other than ACK. In `poll_send`, after computing `to_send`, we can detect "last segment in burst" by checking if all available data will be sent. + +**Step 1: Add `tcp_flags` parameter to `build_data_from_slices`** + +In `src/net/handler/tcp/segment.rs`, change the signature of `build_data_from_slices` (line 548) to add a `tcp_flags: u8` parameter after `window`: + +```rust +pub fn build_data_from_slices<'umem>( + local_addr: IpAddress, + remote_addr: IpAddress, + local_port: u16, + remote_port: u16, + seq: u32, + ack: u32, + window: u16, + tcp_flags: u8, // NEW — was hardcoded to flags::ACK + payload: (&[u8], &[u8]), + timestamp: Option<(u32, u32)>, + src_mac: MacAddress, + dst_mac: MacAddress, + tx_offload: bool, + free_frames: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, +) +``` + +Then replace the hardcoded `flags::ACK` at lines 584 and 603 with `tcp_flags`. + +**Step 2: Update all call sites in mod.rs** + +There are 3 call sites for `build_data_from_slices` in mod.rs: + +1. **poll_send data path** (~line 2029): Change to pass `flags::ACK` (for now — we'll add PSH logic in step 3). +2. **Fast retransmit** (~line 1797): Pass `flags::ACK` (retransmits don't set PSH). +3. **RTO retransmit** (~line 1922): Pass `flags::ACK` (retransmits don't set PSH). + +Search for `build_data_from_slices` in mod.rs to find all 3 sites. + +**Step 3: Add PSH flag logic in poll_send** + +In `poll_send`, before the `build_data_from_slices` call, determine if this is the last segment in the burst: + +```rust +// Set PSH on last segment in burst (all available data sent or window exhausted). +let remaining_after_send = data_available.saturating_sub(to_send); +let data_flags = if remaining_after_send == 0 || to_send >= can_send { + flags::ACK | flags::PSH +} else { + flags::ACK +}; +``` + +Then pass `data_flags` to `build_data_from_slices` instead of `flags::ACK`. + +Add `use crate::net::wire::tcp::flags;` if not already in scope (it's likely already available via the segment builder imports). + +**Step 4: Write test** + +Add a test in the `#[cfg(test)]` section of mod.rs: + +```rust +#[test] +fn poll_send_sets_psh_on_last_segment() { + // Setup: established connection with small amount of data (< MSS). + // This is the last (and only) segment, so PSH should be set. + // After poll_send, inspect the emitted frame's TCP flags. + // The TCP flags byte is at offset ETH_LEN + IPV4_MIN_HEADER_LEN + 13. + // Verify: flags & PSH != 0. +} +``` + +Follow existing test patterns (use `establish_connection` helper). Build the frame, call `poll_send`, pop from tx_return, check TCP flags byte in the frame. The TCP flags offset in an IPv6 frame is `ETH_LEN + IPV6_HEADER_LEN + 13`. For IPv4: `ETH_LEN + IPV4_MIN_HEADER_LEN + 13`. + +Also add a test that sends more than 1 MSS of data — the first segment should NOT have PSH, but after poll_send loops or on the next poll_send when remaining data fits, PSH should be set. + +**Step 5: Update segment.rs test** + +The existing test `build_data_from_slices_produces_frame` needs updating to pass the new `tcp_flags` parameter. Add `flags::ACK` to the existing call. + +**Step 6: Run tests** + +Run: `cargo test` +Expected: All pass + +**Step 7: Commit** + +```bash +git add src/net/handler/tcp/segment.rs src/net/handler/tcp/mod.rs +git commit -m "feat(tcp): set PSH flag on last data segment in burst" +``` + +--- + +## Task 2: IPv6 Fragment Drop Visibility + +**Files:** +- Modify: `src/net/handler/ipv6.rs:248-250` + +**Context:** The IPv6 handler at line 249 silently drops fragmented TCP segments with `rx_return.push(frame)`. We want to make this visible with a counter. + +**Step 1: Add counter to Ipv6Handler** + +Find the `Ipv6Handler` struct definition in `src/net/handler/ipv6.rs`. Add a field: + +```rust +pub ipv6_tcp_fragments_dropped: u64, +``` + +Initialize to 0 in the constructor/default. + +**Step 2: Increment counter on drop** + +Replace the fragment drop code (line 249): + +```rust +} else if frag_next_header == IpProtocols::Tcp { + // TCP does not yet support fragment reassembly; drop to rx_return. + self.ipv6_tcp_fragments_dropped += 1; + rx_return.push(frame); +} +``` + +**Step 3: Write test** + +Test that processing an IPv6 fragmented TCP frame increments the counter. Follow existing test patterns in the file. If there's no existing test infrastructure for IPv6 fragments, a simple unit test that constructs a minimal fragmented IPv6 frame and processes it is sufficient. + +**Step 4: Run tests** + +Run: `cargo test` +Expected: All pass + +**Step 5: Commit** + +```bash +git add src/net/handler/ipv6.rs +git commit -m "feat(ipv6): count dropped TCP fragments instead of silent discard" +``` + +--- + +## Task 3: ECN — TCB Fields and TcpConfig + +**Files:** +- Modify: `src/net/handler/tcp/tcb.rs` + +**Context:** Add ECN-related fields to TCB and TcpConfig before implementing the protocol logic. + +**Step 1: Add fields to TcpConfig** + +In `src/net/handler/tcp/tcb.rs`, add to `TcpConfig` struct (after `sack: bool`): + +```rust +pub ecn: bool, +``` + +In `impl Default for TcpConfig`, add: + +```rust +ecn: true, +``` + +**Step 2: Add fields to Tcb** + +In the `Tcb` struct, add after the SACK fields: + +```rust +// ECN (RFC 3168) +pub ecn_enabled: bool, +pub ecn_ce_received: bool, +pub ecn_cwr_sent: bool, +``` + +In the `Tcb` constructor (wherever `Tcb` is created — likely in `process_listen` and `connect`), initialize: + +```rust +ecn_enabled: false, +ecn_ce_received: false, +ecn_cwr_sent: false, +``` + +**Step 3: Run tests** + +Run: `cargo test` +Expected: All pass (fields added but unused — may get dead_code warnings, which is fine) + +**Step 4: Commit** + +```bash +git add src/net/handler/tcp/tcb.rs +git commit -m "feat(tcp): add ECN fields to TCB and TcpConfig" +``` + +--- + +## Task 4: ECN — Negotiation in SYN/SYN-ACK + +**Files:** +- Modify: `src/net/handler/tcp/segment.rs` (build_syn, build_syn_ack) +- Modify: `src/net/handler/tcp/mod.rs` (connect, process_listen, process_syn_sent, process_syn_received) + +**Context:** ECN negotiation per RFC 3168 §6.1.1: +- **Active open:** SYN with ECE+CWR flags → "I support ECN" +- **Passive open:** SYN-ACK with ECE flag (no CWR) → "I confirm ECN" +- Both sides must agree; if SYN-ACK lacks ECE, disable ECN. + +**Step 1: Modify build_syn to accept ecn parameter** + +In `src/net/handler/tcp/segment.rs`, find `build_syn` (around line 104). Add an `ecn: bool` parameter. When `ecn` is true, set flags to `flags::SYN | flags::ECE | flags::CWR` instead of just `flags::SYN`. The internal call to `build_ipv4_segment`/`build_ipv6_segment` passes the `tcp_flags` parameter — just change what's passed. + +```rust +let syn_flags = if ecn { flags::SYN | flags::ECE | flags::CWR } else { flags::SYN }; +``` + +**Step 2: Modify build_syn_ack to accept ecn parameter** + +Find `build_syn_ack` (around line 181). Add `ecn: bool` parameter. When true, set flags to `flags::SYN | flags::ACK | flags::ECE` instead of `flags::SYN | flags::ACK`. + +```rust +let syn_ack_flags = if ecn { flags::SYN | flags::ACK | flags::ECE } else { flags::SYN | flags::ACK }; +``` + +**Step 3: Update connect() to pass ecn flag** + +In `mod.rs`, in `connect()` and `connect_with_config()`, pass `config.ecn` to `build_syn`. Also set `tcb.ecn_enabled` provisionally (will be confirmed when SYN-ACK arrives). + +Find the `build_syn` call in connect (around line 288). Add the ecn parameter. + +**Step 4: Update process_listen to negotiate ECN** + +In `process_listen`, when processing an incoming SYN: +1. Check if incoming SYN has both ECE and CWR flags set: `seg_flags & (flags::ECE | flags::CWR) == (flags::ECE | flags::CWR)` +2. If yes AND our config allows ECN, set `tcb.ecn_enabled = true` on the new connection +3. Pass `tcb.ecn_enabled` to `build_syn_ack` + +Find where `build_syn_ack` is called in `process_listen` (~line 859) and the SYN-ACK retransmit in `poll_timers` for SynReceived. + +**Step 5: Update process_syn_sent to confirm ECN** + +In `process_syn_sent`, when processing SYN-ACK: +1. Check if SYN-ACK has ECE flag: `seg_flags & flags::ECE != 0` +2. If yes and we offered ECN (tcb.ecn_enabled was provisionally set), confirm: keep `ecn_enabled = true` +3. If no ECE in SYN-ACK: `tcb.ecn_enabled = false` + +Find the SYN-ACK processing section in `process_syn_sent` (the `seg_flags & flags::ACK != 0` branch). + +**Step 6: Update all build_syn/build_syn_ack call sites** + +Search for `build_syn(` and `build_syn_ack(` in mod.rs. There are calls in: +- `connect()` / `connect_with_config()` +- `process_listen()` (SYN-ACK) +- `poll_timers()` (SYN retransmit for SynSent, SYN-ACK retransmit for SynReceived) + +All need the new `ecn` parameter. For retransmits, use `tcb.ecn_enabled` (already stored). + +**Step 7: Write tests** + +```rust +#[test] +fn ecn_negotiated_when_both_sides_support() { + // Connect with ecn=true. Verify SYN has ECE+CWR. + // Send SYN-ACK with ECE. Verify ecn_enabled=true after handshake. +} + +#[test] +fn ecn_disabled_when_peer_doesnt_support() { + // Connect with ecn=true. Send SYN-ACK without ECE. + // Verify ecn_enabled=false after handshake. +} + +#[test] +fn ecn_negotiated_on_passive_open() { + // Listen. Send SYN with ECE+CWR. Verify SYN-ACK has ECE. + // Complete handshake. Verify ecn_enabled=true. +} +``` + +**Step 8: Run tests** + +Run: `cargo test` +Expected: All pass + +**Step 9: Commit** + +```bash +git add src/net/handler/tcp/segment.rs src/net/handler/tcp/mod.rs +git commit -m "feat(tcp): negotiate ECN in SYN/SYN-ACK handshake (RFC 3168)" +``` + +--- + +## Task 5: ECN — ECT Codepoint on Outgoing Data and CE Detection + +**Files:** +- Modify: `src/net/handler/tcp/segment.rs` (build_ipv4_data_segment_slices, build_ipv6_data_segment_slices, build_data_from_slices) +- Modify: `src/net/handler/tcp/mod.rs` (poll_send, process_established) + +**Context:** When ECN is enabled: +- **Outgoing:** Set ECT(0) codepoint (`0b10`) in IPv4 ToS byte (`ip[1]`) / IPv6 Traffic Class on data segments. NOT on retransmits per RFC 3168. +- **Incoming:** Check for CE codepoint (`0b11`) in IP header. If CE, set `ecn_ce_received = true`. + +The IPv4 ToS byte is at `ip[1]` (offset 1 in IP header). Currently `ip.fill(0)` zeros it. ECN bits are the two low-order bits of the ToS byte. ECT(0) = `0b10` = 0x02. + +For IPv6, Traffic Class spans bytes 0-1 of the IPv6 header (4 bits version + 8 bits TC + 20 bits flow label). The ECN bits are bits 6-7 of the Traffic Class (the two low-order bits). In the IPv6 header bytes: `ip[0]` has version(4 bits) + TC high 4 bits, `ip[1]` has TC low 4 bits + flow label high 4 bits. ECN bits are in the low 2 bits of the TC field, which map to bits 5-4 of `ip[1]`. So: `ip[1] = (ip[1] & 0xCF) | (ecn_value << 4)`. Actually, let's be more precise: the 8-bit TC field is `(ip[0] & 0x0F) << 4 | (ip[1] >> 4)`. ECN is the low 2 bits of TC. To set ECT(0)=0b10: `ip[1] = (ip[1] & 0xCF) | (0x02 << 4)` = `ip[1] | 0x20`. + +**Step 1: Add `ecn_ect: bool` parameter to build_data_from_slices** + +```rust +pub fn build_data_from_slices<'umem>( + ... + tcp_flags: u8, + payload: (&[u8], &[u8]), + timestamp: Option<(u32, u32)>, + ecn_ect: bool, // NEW — set ECT(0) in IP header + ... +) +``` + +Pass it through to `build_ipv4_data_segment_slices` / `build_ipv6_data_segment_slices`. + +**Step 2: Set ECT(0) in IP header** + +In `build_ipv4_data_segment_slices`, after the IP header setup (after `ip.fill(0); ip[0] = 0x45;`), add: + +```rust +if ecn_ect { + ip[1] = 0x02; // ECT(0) in ToS byte +} +``` + +In `build_ipv6_data_segment_slices`, similarly set the ECN bits in the IPv6 Traffic Class field. After the IPv6 header is initialized: + +```rust +if ecn_ect { + ip[1] |= 0x20; // ECT(0) in Traffic Class low bits +} +``` + +**Step 3: Update call sites in mod.rs** + +- **poll_send data path:** Pass `tcb.ecn_enabled` as `ecn_ect`. +- **Fast retransmit:** Pass `false` (retransmits don't get ECT marking per RFC 3168 §6.1.5). +- **RTO retransmit:** Pass `false`. +- **Zero-window probe:** Uses `build_data` not `build_data_from_slices` — leave as-is (probes don't need ECT). + +**Step 4: Detect CE on incoming segments** + +In `process_established`, at the top (before PAWS check), extract the ECN bits from the incoming IP header. The frame has already been parsed for IP — check how `process_ipv4_with_now` extracts the IP header. + +For IPv4: ToS byte is at `frame[ETH_LEN + 1]`. CE = `(tos & 0x03) == 0x03`. +For IPv6: TC byte split across `frame[ETH_LEN]` and `frame[ETH_LEN + 1]`. ECN bits: `(frame[ETH_LEN + 1] >> 4) & 0x03`. CE = value == 0x03. + +Since `process_established` doesn't currently receive the IP header ECN bits, you'll need to pass them through. Add an `ecn_bits: u8` parameter to `process_established` (and `process_ipv4_with_now` / `process_ipv6_with_now` extract it from the IP header and pass it). + +In `process_established`, if `tcb.ecn_enabled && ecn_bits == 0x03`: +```rust +tcb.ecn_ce_received = true; +``` + +**Step 5: Write tests** + +```rust +#[test] +fn ecn_ect_set_on_outgoing_data() { + // Established connection with ecn_enabled=true. + // Write data, call poll_send. + // Check outgoing frame's IP ToS byte has ECT(0) (0x02). +} + +#[test] +fn ecn_ect_not_set_on_retransmit() { + // Trigger RTO retransmit with ecn_enabled=true. + // Check outgoing frame's IP ToS byte is 0x00. +} + +#[test] +fn ecn_ce_detected_on_incoming() { + // Established connection with ecn_enabled=true. + // Send a data segment with CE mark in IP header. + // Verify ecn_ce_received=true on the TCB. +} +``` + +**Step 6: Run tests** + +Run: `cargo test` +Expected: All pass + +**Step 7: Commit** + +```bash +git add src/net/handler/tcp/segment.rs src/net/handler/tcp/mod.rs +git commit -m "feat(tcp): set ECT(0) on outgoing data, detect CE on incoming (RFC 3168)" +``` + +--- + +## Task 6: ECN — ECE/CWR Flag Signaling and Congestion Response + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs` (process_established, poll_send) + +**Context:** When CE is detected, receiver must signal sender via ECE flag. Sender responds by halving cwnd and sending CWR. + +**Step 1: Send ECE flag when ecn_ce_received is true** + +In all ACK-sending paths within `process_established` (there are several `build_ack` / `build_ack_with_sack` calls), when `tcb.ecn_ce_received` is true, OR the ECE flag into the ACK's TCP flags. + +The simplest approach: modify `build_ack` and `build_ack_with_sack` to accept a `tcp_flags: u8` parameter (similar to what we did for `build_data_from_slices`). Default callers pass `flags::ACK`, but when `ecn_ce_received`, pass `flags::ACK | flags::ECE`. + +Alternatively, add ECE into all outgoing segments (including data piggybacked ACKs) when `ecn_ce_received` is true — this means also adding ECE to `build_data_from_slices` calls in `poll_send`. This is the correct behavior: keep sending ECE until CWR is received. + +**Step 2: React to incoming ECE (sender side)** + +In `process_established` ACK processing (the valid new ACK branch), check for ECE flag: + +```rust +if tcb.ecn_enabled && seg_flags & flags::ECE != 0 && !tcb.ecn_cwr_sent { + // Congestion response: halve cwnd (same as loss). + tcb.ssthresh = (tcb.cwnd / 2).max(2 * tcb.eff_snd_mss as u32); + tcb.cwnd = tcb.ssthresh; + tcb.ecn_cwr_sent = true; // Will send CWR on next data segment +} +``` + +**Step 3: Send CWR flag on next data segment** + +In `poll_send`, when `tcb.ecn_cwr_sent` is true, OR the CWR flag into the data segment's TCP flags: + +```rust +let mut data_flags = if remaining_after_send == 0 || to_send >= can_send { + flags::ACK | flags::PSH +} else { + flags::ACK +}; +if tcb.ecn_cwr_sent { + data_flags |= flags::CWR; +} +``` + +After sending the CWR-marked segment, clear the flag: + +```rust +tcb.ecn_cwr_sent = false; +``` + +**Step 4: Clear ecn_ce_received when CWR received** + +In `process_established`, before data processing, check for CWR: + +```rust +if tcb.ecn_enabled && seg_flags & flags::CWR != 0 { + tcb.ecn_ce_received = false; // Stop sending ECE +} +``` + +**Step 5: Reset ecn_cwr_sent on new ACK** + +In the valid new ACK branch, after advancing `snd_una`: + +```rust +// Reset ECN CWR tracking for new RTT. +if tcb.ecn_cwr_sent { + // CWR was already sent; new ACK means sender saw it. + // (Actually we clear ecn_cwr_sent when the segment is sent in poll_send) +} +``` + +This is already handled in Step 3 (cleared after sending). No additional code needed. + +**Step 6: Write tests** + +```rust +#[test] +fn ecn_ece_sent_when_ce_received() { + // Established with ecn_enabled. Set ecn_ce_received=true. + // Trigger an ACK (e.g., by receiving data). + // Check outgoing ACK has ECE flag set. +} + +#[test] +fn ecn_cwnd_halved_on_ece() { + // Established with ecn_enabled. Set cwnd to known value. + // Send ACK with ECE flag. + // Verify cwnd halved and ecn_cwr_sent=true. +} + +#[test] +fn ecn_cwr_sent_on_next_data() { + // Established with ecn_enabled and ecn_cwr_sent=true. + // Write data, poll_send. + // Verify outgoing segment has CWR flag. + // Verify ecn_cwr_sent cleared after send. +} + +#[test] +fn ecn_ce_received_cleared_on_cwr() { + // Established with ecn_enabled and ecn_ce_received=true. + // Receive segment with CWR flag. + // Verify ecn_ce_received=false. +} +``` + +**Step 7: Run tests** + +Run: `cargo test` +Expected: All pass + +**Step 8: Commit** + +```bash +git add src/net/handler/tcp/mod.rs src/net/handler/tcp/segment.rs +git commit -m "feat(tcp): ECN congestion signaling with ECE/CWR flags (RFC 3168)" +``` + +--- + +## Task 7: Socket API — read/write Return Result + +**Files:** +- Modify: `src/net/socket/tcp.rs` (TcpRead, TcpWrite futures, TcpStream methods) +- Modify: `examples/tcp-echo-server.rs` +- Modify: `examples/tcp-echo-client.rs` + +**Context:** `TcpRead` and `TcpWrite` futures currently return `usize`. They need to return `Result` to surface Reset/Timeout events. `TcpError` already exists in `tcb.rs`. + +**Step 1: Change TcpRead::Output to Result** + +In `src/net/socket/tcp.rs`, find `impl Future for TcpRead` (around line 497). Change: + +```rust +impl<'stream> Future for TcpRead<'stream> { + type Output = Result; +``` + +In the `poll` method: +- Before checking recv_buffer, check event_queue for error events: + ```rust + if let Some(event) = handler.check_event(&self.conn_id) { + match event { + TcpEvent::Reset => return Poll::Ready(Err(TcpError::Reset)), + TcpEvent::Timeout => return Poll::Ready(Err(TcpError::Timeout)), + _ => {} + } + } + ``` +- When connection is gone (not found): `return Poll::Ready(Err(TcpError::NotConnected))` +- When data available: `return Poll::Ready(Ok(n))` +- When remote closed (state is CloseWait/LastAck/TimeWait/Closed and no data): `return Poll::Ready(Ok(0))` + +Note: You'll need to check how the event_queue is accessed. The TCB has `event_queue: LocalQueue`. The socket layer holds a clone of this queue. Check `TcpStream` struct fields — it has `event_queue: LocalQueue` at line 164. + +**Step 2: Change TcpWrite::Output to Result** + +Find `impl Future for TcpWrite` (around line 464). Change: + +```rust +impl<'stream> Future for TcpWrite<'stream> { + type Output = Result; +``` + +In the `poll` method: +- Check event_queue for errors before writing +- When `write_closed`: `return Poll::Ready(Err(TcpError::NotConnected))` +- When connection gone: `return Poll::Ready(Err(TcpError::NotConnected))` +- When data written: `return Poll::Ready(Ok(n))` + +**Step 3: Update TcpStream::read and TcpStream::write signatures** + +These methods return the future types, so their return types change automatically. But check if there are any `async fn` wrappers or if callers expect `usize` directly. + +```rust +pub fn read<'a>(&'a self, buf: &'a mut [u8]) -> TcpRead<'a> { ... } +pub fn write<'a>(&'a self, data: &'a [u8]) -> TcpWrite<'a> { ... } +``` + +These return `TcpRead` / `TcpWrite` which now resolve to `Result`. The `.await` callers will get `Result`. + +**Step 4: Update TcpListener::accept if needed** + +Check if `accept()` returns a future that depends on TcpRead/TcpWrite. It likely returns a `TcpStream` directly (via the accept queue). Probably no change needed, but verify. + +**Step 5: Update tcp-echo-server.rs** + +```rust +// Before: +let n = stream.read(&mut buf).await; +if n == 0 { break; } +stream.write(&buf[..n]).await; + +// After: +let n = match stream.read(&mut buf).await { + Ok(0) => break, + Ok(n) => n, + Err(e) => { + println!("Read error: {:?}", e); + break; + } +}; +if let Err(e) = stream.write(&buf[..n]).await { + println!("Write error: {:?}", e); + break; +} +``` + +**Step 6: Update tcp-echo-client.rs** + +```rust +// Before: +stream.write(&payload).await; +let n = stream.read(&mut read_buf[total_read..]).await; +if n == 0 { break; } + +// After: +if let Err(e) = stream.write(&payload).await { + println!("Write error: {:?}", e); + return; +} +let n = match stream.read(&mut read_buf[total_read..]).await { + Ok(0) => { + println!("Server closed connection"); + return; + } + Ok(n) => n, + Err(e) => { + println!("Read error: {:?}", e); + return; + } +}; +``` + +**Step 7: Update any other callers** + +Search for `.read(` and `.write(` on TcpStream across the codebase. Check tests in mod.rs or socket tests that use TcpStream. + +**Step 8: Write tests** + +```rust +#[test] +fn read_returns_error_on_reset() { + // Setup TcpStream with a connection. + // Push TcpEvent::Reset to event_queue. + // Call read() — should return Err(TcpError::Reset). +} + +#[test] +fn write_returns_error_on_timeout() { + // Setup TcpStream with a connection. + // Push TcpEvent::Timeout to event_queue. + // Call write() — should return Err(TcpError::Timeout). +} +``` + +**Step 9: Run tests** + +Run: `cargo test` +Expected: All pass + +**Step 10: Commit** + +```bash +git add src/net/socket/tcp.rs examples/tcp-echo-server.rs examples/tcp-echo-client.rs +git commit -m "feat(tcp): return Result from TcpStream read/write, surface Reset/Timeout errors" +``` + +--- + +## Task 8: Simultaneous Open Verification and Test + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs` (test section only, possibly small fix) + +**Context:** Simultaneous open (both sides SYN at same time) transitions SynSent → SynReceived when a bare SYN (no ACK) arrives. Then the SynReceived side receives the peer's SYN-ACK or ACK and transitions to Established. The code exists in `process_syn_sent` (~line 1155-1199) but has never been tested. + +**Step 1: Read and trace the simultaneous open path** + +Read `process_syn_sent` carefully. Trace: +1. Both sides call `connect()` → both in `SynSent` +2. Side A receives Side B's SYN (no ACK) → A transitions to `SynReceived`, sends SYN-ACK +3. Side B receives Side A's SYN (no ACK) → B transitions to `SynReceived`, sends SYN-ACK +4. Side A receives Side B's SYN-ACK → A transitions to `Established` +5. Side B receives Side A's SYN-ACK → B transitions to `Established` + +Step 4-5 happens in `process_syn_received` — verify it handles SYN-ACK correctly (the ACK for our SYN). + +**Step 2: Write test** + +```rust +#[test] +fn simultaneous_open_both_reach_established() { + // Create two TcpHandlers (or one handler with two connections). + // Both sides in SynSent state. + // + // Step 1: Feed Side A's SYN to Side B's handler. + // → B transitions to SynReceived, emits SYN-ACK. + // + // Step 2: Feed Side B's SYN to Side A's handler. + // → A transitions to SynReceived, emits SYN-ACK. + // + // Step 3: Feed B's SYN-ACK to A's handler. + // → A transitions to Established. + // + // Step 4: Feed A's SYN-ACK to B's handler. + // → B transitions to Established. + // + // Verify both connections are in Established state. +} +``` + +This test requires constructing SYN frames manually (use `build_tcp_frame` helper) and processing them through the handler. Follow existing test patterns. + +**Step 3: Fix any issues found** + +If the simultaneous open path fails (e.g., process_syn_received doesn't handle SYN-ACK correctly, or the ACK number is wrong), fix it. If it works, the test is the deliverable. + +**Step 4: Run tests** + +Run: `cargo test` +Expected: All pass + +**Step 5: Commit** + +```bash +git add src/net/handler/tcp/mod.rs +git commit -m "test(tcp): verify simultaneous open reaches Established on both sides" +``` + +--- + +## Summary + +| Task | Feature | Key Files | Dependencies | +|------|---------|-----------|--------------| +| 1 | PSH flag on outgoing segments | segment.rs, mod.rs | None | +| 2 | IPv6 fragment drop counter | ipv6.rs | None | +| 3 | ECN TCB fields | tcb.rs | None | +| 4 | ECN negotiation in SYN/SYN-ACK | segment.rs, mod.rs | Task 3 | +| 5 | ECN ECT marking + CE detection | segment.rs, mod.rs | Task 4 | +| 6 | ECN ECE/CWR signaling | mod.rs, segment.rs | Task 5 | +| 7 | Socket API Result returns | tcp.rs, examples | None | +| 8 | Simultaneous open test | mod.rs | None | + +**Independent groups:** Tasks 1, 2, 7, 8 are fully independent. Tasks 3→4→5→6 are sequential (ECN chain). + +**Recommended execution order:** 1, 2, 3, 4, 5, 6, 7, 8 (ECN chain in the middle, bookended by simpler tasks). diff --git a/docs/plans/2026-03-07-tcp-handler-refactor-design.md b/docs/plans/2026-03-07-tcp-handler-refactor-design.md new file mode 100644 index 0000000..fecbf54 --- /dev/null +++ b/docs/plans/2026-03-07-tcp-handler-refactor-design.md @@ -0,0 +1,80 @@ +# TCP Handler Refactor Design + +## Problem + +`src/net/handler/tcp/mod.rs` is 11,300 lines (3,098 implementation + 8,202 tests). It contains the `TcpHandler` struct, listener management, active open, all state machine handlers, timer polling, transmit path, and 70+ tests. Every other `mod.rs` in the handler layer is a pure export shim. The supporting files (`tcb.rs`, `state.rs`, `congestion.rs`, `recovery.rs`, `ring_buffer.rs`, `isn.rs`, `segment.rs`) are already well-factored. + +## Design + +### File Structure + +``` +src/net/handler/tcp/ + mod.rs # Pure export shim (~15 lines) + handler.rs # TcpHandler struct + new() + lookup helpers + evict_stale + listener.rs # ListenEntry struct + listen/unlisten/find_listener + connection.rs # connect/connect_with_config + initiate_close + inbound.rs # process_ipv4/v6, process_segment, per-state handlers + timers.rs # poll_timers (delayed ACK, keep-alive, RTO, persist, SACK) + transmit.rs # poll_send (data segmentation, congestion window, Nagle) + tcb.rs # unchanged + state.rs # unchanged + congestion.rs # unchanged + recovery.rs # unchanged + ring_buffer.rs # unchanged + isn.rs # unchanged + segment.rs # unchanged + tests/ + mod.rs # Shared test helpers + mod declarations + handshake.rs # 3-way handshake, window scale, backlog (~12 tests) + data_transfer.rs # In-order, out-of-order delivery (~2 tests) + retransmission.rs # RTO, fast retransmit, RTT estimation (~5 tests) + teardown.rs # FIN, TIME_WAIT, simultaneous close (~12 tests) + delayed_ack.rs # Deferral, flushing, timer (~6 tests) + nagle.rs # Small segments, TCP_NODELAY (~5 tests) + keepalive.rs # Idle probes, exhaustion, linger (~11 tests) + timestamps.rs # Timestamp negotiation, PAWS (~7 tests) + persist.rs # Zero-window probing (~4 tests) + sack.rs # SACK recovery, scoreboard (~9 tests) + ecn.rs # ECN negotiation, CE, CWR (~11 tests) + edge_cases.rs # RST validation, challenge ACK, SWS (~10 tests) + congestion.rs # CUBIC slow start, F-RTO, limited transmit (~3 tests) +``` + +### Module Responsibilities + +**`mod.rs`** — `mod` declarations and `pub use` re-exports only. + +**`handler.rs`** (~80 lines) — `TcpHandler` struct definition with `pub(super)` fields (connections, listeners, isn_generator, rx/tx_offload). Constructor, `get_connection()`, `get_connection_mut()`, `remove_connection()`, `evict_stale()`. + +**`listener.rs`** (~120 lines) — `ListenEntry` struct. `impl TcpHandler` block: `listen()`, `listen_with_config()`, `unlisten()`, `find_listener()`, `push_to_accept_queue()`, `decrement_syn_received()`. + +**`connection.rs`** (~150 lines) — `impl TcpHandler` block: `connect()`, `connect_with_config()`, `initiate_close()`. + +**`inbound.rs`** (~1,600 lines) — `is_segment_acceptable()` free function. `impl TcpHandler` block: `process_ipv4()`, `process_ipv6()`, `process_ipv4_with_now()`, `process_ipv6_with_now()`, `process_segment()`, `process_listen()`, `process_syn_sent()`, `process_syn_received()`, `process_established()`, `process_teardown()`. + +**`timers.rs`** (~380 lines) — `impl TcpHandler` block: `poll_timers()`. + +**`transmit.rs`** (~300 lines) — `impl TcpHandler` block: `poll_send()`. + +### Key Pattern + +`handler.rs` owns the struct with `pub(super)` fields. Sibling files extend it with `impl TcpHandler` blocks. No circular dependencies. + +### Test Organization + +`tests/mod.rs` contains shared helpers (creating handlers, building frames, extracting headers). Each category file uses `use super::*` for helpers. Tests move as-is with no rewriting. + +### Migration Strategy + +Purely mechanical — no behavior or API changes. Each step is a standalone commit that compiles and passes tests: + +1. Extract `listener.rs` (ListenEntry + listener methods) +2. Extract `connection.rs` (connect + close methods) +3. Extract `transmit.rs` (poll_send) +4. Extract `timers.rs` (poll_timers + evict_stale) +5. Extract `handler.rs` (TcpHandler struct + new() + lookup helpers) +6. Extract `inbound.rs` (remaining process_* + state handlers) +7. Reduce `mod.rs` to pure export shim +8. Extract tests into `tests/` (one category at a time) +9. Verify `cargo test` passes after each step diff --git a/docs/plans/2026-03-07-tcp-handler-refactor.md b/docs/plans/2026-03-07-tcp-handler-refactor.md new file mode 100644 index 0000000..c8e168f --- /dev/null +++ b/docs/plans/2026-03-07-tcp-handler-refactor.md @@ -0,0 +1,680 @@ +# TCP Handler Refactor Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Decompose the 11,300-line `src/net/handler/tcp/mod.rs` into focused modules with categorized test files. + +**Architecture:** `handler.rs` owns the `TcpHandler` struct with `pub(super)` fields. Sibling files (`listener.rs`, `connection.rs`, `inbound.rs`, `timers.rs`, `transmit.rs`) extend it with `impl TcpHandler` blocks. Tests move to a `tests/` subdirectory with one file per category. `mod.rs` becomes a pure export shim. + +**Tech Stack:** Rust, no new dependencies. Purely mechanical refactor — no behavior or API changes. + +**Reference:** Design doc at `docs/plans/2026-03-07-tcp-handler-refactor-design.md` + +--- + +## Important Notes + +- This is a **move-only refactor**. Do not change any logic, rename functions, or alter APIs. +- After each task, run `cargo test` (no feature flags) to verify nothing broke. +- Each task produces one commit. +- Fields on `TcpHandler` need `pub(super)` visibility so sibling `impl` blocks can access them. +- `ListenEntry` fields need `pub(super)` visibility for the same reason. +- Use `use super::*` or specific imports in new files to access sibling module items. +- The `listeners` field type changes from `Vec` to `Vec` in `handler.rs`. + +## Source Line Ranges Reference + +Current `mod.rs` structure (11,301 lines total): +- Lines 1-7: Module declarations +- Lines 9-40: Imports and `use` statements +- Lines 42-46: Constants (`INITIAL_RTO_MS`, `SYN_R2_THRESHOLD_MS`) +- Lines 48-68: `ListenEntry` struct +- Lines 70-89: `is_segment_acceptable()` free function +- Lines 91-101: `TcpHandler` struct definition +- Lines 103-112: `TcpHandler::new()` +- Lines 114-176: Listener methods (`listen`, `listen_with_config`, `unlisten`) +- Lines 178-327: Connect methods (`connect`, `connect_with_config`) +- Lines 329-553: IPv4/IPv6 process entry points (`process_ipv4`, `process_ipv4_with_now`, `process_ipv6`, `process_ipv6_with_now`) +- Lines 555-726: `process_segment()` dispatcher +- Lines 728-922: `process_listen()` +- Lines 924-1077: `process_syn_received()` +- Lines 1079-1275: `process_syn_sent()` +- Lines 1277-1874: `process_established()` +- Lines 1876-2257: `poll_timers()` +- Lines 2260-2270: `evict_stale()` +- Lines 2272-2563: `poll_send()` +- Lines 2565-2593: `initiate_close()` +- Lines 2595-2602: `find_listener()` +- Lines 2605-2614: `push_to_accept_queue()` +- Lines 2617-2626: `decrement_syn_received()` +- Lines 2629-2631: `get_connection()` +- Lines 2634-2636: `get_connection_mut()` +- Lines 2639-2669: `remove_connection()` +- Lines 2671-3096: `process_teardown()` +- Lines 3099-3098: `#[cfg(test)]` marker +- Lines 3099-11300: Test module + +--- + +### Task 1: Extract `listener.rs` + +**Files:** +- Create: `src/net/handler/tcp/listener.rs` +- Modify: `src/net/handler/tcp/mod.rs` + +**Step 1: Create `listener.rs`** + +Move the following from `mod.rs` into a new `listener.rs` file: +- `ListenEntry` struct (lines 48-68) — change field visibility from `pub` to `pub(super)` +- `listen()` method (lines 115-126) — as `impl TcpHandler` block +- `listen_with_config()` method (lines 129-163) — same block +- `unlisten()` method (lines 166-176) — same block +- `find_listener()` method (lines 2598-2602) — same block +- `push_to_accept_queue()` method (lines 2605-2614) — same block +- `decrement_syn_received()` method (lines 2617-2626) — same block + +The file needs these imports: +```rust +use crate::net::{ + handler::udp::BindError, + socket::LocalQueue, + wire::ip::IpAddress, +}; +use super::handler::TcpHandler; +use super::state::TcpState; +use super::tcb::{ConnectionId, TcpConfig}; +``` + +Note: `find_listener`, `push_to_accept_queue`, and `decrement_syn_received` are called from `inbound.rs` (process_listen), so they need `pub(super)` visibility. + +**Step 2: Update `mod.rs`** + +- Add `pub(super) mod listener;` to module declarations +- Remove the moved code (ListenEntry struct, all listener methods) +- Replace internal references to `ListenEntry` with `listener::ListenEntry` +- Update the `TcpHandler` struct's `listeners` field type to `Vec` + +**Step 3: Verify** + +Run: `cargo test` +Expected: All tests pass unchanged. + +**Step 4: Commit** + +``` +refactor(tcp): extract listener.rs from mod.rs +``` + +--- + +### Task 2: Extract `connection.rs` + +**Files:** +- Create: `src/net/handler/tcp/connection.rs` +- Modify: `src/net/handler/tcp/mod.rs` + +**Step 1: Create `connection.rs`** + +Move from `mod.rs`: +- `connect()` method (lines 181-203) +- `connect_with_config()` method (lines 206-327) +- `initiate_close()` method (lines 2569-2593) + +These go in an `impl TcpHandler` block. Imports needed: +```rust +use coarsetime::Instant; +use crate::{ + net::{ + handler::udp::BindError, + socket::LocalQueue, + wire::{ethernet::MacAddress, ip::IpAddress}, + }, + xdp::frame::FrameBuffer, +}; +use super::handler::TcpHandler; +use super::isn::IsnGenerator; +use super::ring_buffer::RingBuffer; +use super::segment::SegmentBuilder; +use super::state::TcpState; +use super::tcb::{ConnectionId, Tcb, TcpConfig, TcpEvent}; +use super::congestion::CubicState; +``` + +Note: `connect_with_config` creates a `Tcb`, builds a SYN via `SegmentBuilder`, and pushes to `self.connections`. Check all field accesses and ensure the imports cover every type used. + +**Step 2: Update `mod.rs`** + +- Add `pub(super) mod connection;` to module declarations +- Remove the moved methods + +**Step 3: Verify** + +Run: `cargo test` +Expected: All tests pass unchanged. + +**Step 4: Commit** + +``` +refactor(tcp): extract connection.rs from mod.rs +``` + +--- + +### Task 3: Extract `transmit.rs` + +**Files:** +- Create: `src/net/handler/tcp/transmit.rs` +- Modify: `src/net/handler/tcp/mod.rs` + +**Step 1: Create `transmit.rs`** + +Move from `mod.rs`: +- `poll_send()` method (lines 2276-2563) + +This includes all data segmentation logic, congestion window management, Nagle algorithm, persist timer probing, SACK retransmission, linger/RST handling. It's a single large method. + +Imports needed will include: `coarsetime::Instant`, wire types, `FrameBuffer`, `Frame`, `SegmentBuilder`, `TcpState`, `TcpEvent`, `seq_lt`/`seq_le`, recovery types, and congestion types. Copy the exact imports used by `poll_send` from mod.rs. + +**Step 2: Update `mod.rs`** + +- Add `pub(super) mod transmit;` +- Remove `poll_send` method + +**Step 3: Verify** + +Run: `cargo test` +Expected: All tests pass unchanged. + +**Step 4: Commit** + +``` +refactor(tcp): extract transmit.rs from mod.rs +``` + +--- + +### Task 4: Extract `timers.rs` + +**Files:** +- Create: `src/net/handler/tcp/timers.rs` +- Modify: `src/net/handler/tcp/mod.rs` + +**Step 1: Create `timers.rs`** + +Move from `mod.rs`: +- `poll_timers()` method (lines 1879-2257) +- `evict_stale()` method (lines 2260-2270) + +`poll_timers` handles: delayed ACK flushing, keep-alive probes, SACK recovery timers, RTO expiry/retransmission, SYN retransmission, linger deadline, and persist timer. It calls `SegmentBuilder` methods to build ACKs, keep-alive probes, retransmissions, and RSTs. + +**Step 2: Update `mod.rs`** + +- Add `pub(super) mod timers;` +- Remove `poll_timers` and `evict_stale` methods + +**Step 3: Verify** + +Run: `cargo test` +Expected: All tests pass unchanged. + +**Step 4: Commit** + +``` +refactor(tcp): extract timers.rs from mod.rs +``` + +--- + +### Task 5: Extract `handler.rs` + +**Files:** +- Create: `src/net/handler/tcp/handler.rs` +- Modify: `src/net/handler/tcp/mod.rs` + +**Step 1: Create `handler.rs`** + +Move from `mod.rs`: +- `TcpHandler` struct definition (lines 91-101) — change all fields to `pub(super)` +- `TcpHandler::new()` (lines 103-112) +- `get_connection()` (lines 2629-2631) +- `get_connection_mut()` (lines 2634-2636) +- `remove_connection()` (lines 2639-2669) +- Constants: `INITIAL_RTO_MS` (line 43), `SYN_R2_THRESHOLD_MS` (line 45) — make `pub(super)` so `inbound.rs` and `timers.rs` can use them + +The struct fields become: +```rust +pub struct TcpHandler { + pub(super) connections: Vec, + pub(super) listeners: Vec, + pub(super) isn_generator: IsnGenerator, + pub(super) rx_offload: bool, + pub(super) tx_offload: bool, +} +``` + +**Step 2: Update `mod.rs`** + +- Add `pub(super) mod handler;` (or `pub(crate)` if needed by socket layer) +- Add `pub use handler::TcpHandler;` for re-export +- Remove struct definition, `new()`, lookup helpers, constants + +**Step 3: Verify** + +Run: `cargo test` +Expected: All tests pass unchanged. + +**Step 4: Commit** + +``` +refactor(tcp): extract handler.rs from mod.rs +``` + +--- + +### Task 6: Extract `inbound.rs` + +**Files:** +- Create: `src/net/handler/tcp/inbound.rs` +- Modify: `src/net/handler/tcp/mod.rs` + +**Step 1: Create `inbound.rs`** + +Move ALL remaining implementation code from `mod.rs`: +- `is_segment_acceptable()` free function (lines 72-89) +- `process_ipv4()` (lines 332-348) +- `process_ipv4_with_now()` (lines 351-442) +- `process_ipv6()` (lines 445-463) +- `process_ipv6_with_now()` (lines 466-553) +- `process_segment()` (lines 555-726) +- `process_listen()` (lines 728-922) +- `process_syn_received()` (lines 924-1077) +- `process_syn_sent()` (lines 1079-1275) +- `process_established()` (lines 1277-1874) +- `process_teardown()` (lines 2671-3096) + +This is the largest extraction (~2,400 lines). It will need the most imports since the state machine handlers touch everything: wire parsing, segment building, congestion control, recovery, TCB fields, timestamps, SACK, ECN. + +The state handlers call `self.find_listener()`, `self.push_to_accept_queue()`, `self.decrement_syn_received()` from `listener.rs` — these must be `pub(super)`. + +**Step 2: Reduce `mod.rs` to export shim** + +After this extraction, `mod.rs` should contain ONLY: +```rust +pub(crate) mod congestion; +mod connection; +mod handler; +mod inbound; +mod isn; +pub(crate) mod listener; +pub(crate) mod recovery; +pub(crate) mod ring_buffer; +pub(crate) mod segment; +pub(crate) mod state; +pub(crate) mod tcb; +mod timers; +mod transmit; + +pub use handler::TcpHandler; + +#[cfg(test)] +mod tests; +``` + +Adjust visibility (`pub(crate)` vs `pub(super)` vs private) based on what the socket layer and other modules need to access. Currently `mod.rs` declares `congestion`, `recovery`, `ring_buffer`, `segment`, `state`, `tcb` as `pub(crate)`. + +**Step 3: Verify** + +Run: `cargo test` +Expected: All tests pass unchanged. + +**Step 4: Commit** + +``` +refactor(tcp): extract inbound.rs, mod.rs is now export shim +``` + +--- + +### Task 7: Extract test helpers into `tests/mod.rs` + +**Files:** +- Create: `src/net/handler/tcp/tests/mod.rs` +- Modify: `src/net/handler/tcp/mod.rs` (remove `#[cfg(test)] mod tests` block) + +**Step 1: Create the tests directory and `tests/mod.rs`** + +Move from the current test module (lines 3099-11300): +- All `use` statements from the test module +- Helper functions: `new_handler` (3119-3121), `new_neighbor_handler` (3123-3126), `build_tcp_frame` (3128-3198), `build_tcp_frame_with_payload` (3200-3278), `leak` (3280-3282), `alloc_free_frame` (3284-3286) +- Any other shared helpers like `build_ts_option` (7543-7551) and `establish_connection` (7890-7940) and `establish_connection_with_sack` (8178-8234) +- `mod` declarations for all test category submodules (added in subsequent tasks) + +Make helpers `pub(super)` so test submodules can use them. + +**Step 2: Start with one test category to validate the structure** + +Move the segment acceptability tests (lines 7857-7888, 4 tests) into `tests/mod.rs` temporarily to validate the test infrastructure works. + +**Step 3: Verify** + +Run: `cargo test` +Expected: The moved tests pass. + +**Step 4: Commit** + +``` +refactor(tcp): create tests/ directory with shared helpers +``` + +--- + +### Task 8: Extract `tests/handshake.rs` + +**Files:** +- Create: `src/net/handler/tcp/tests/handshake.rs` +- Modify: `src/net/handler/tcp/tests/mod.rs` + +**Step 1: Move handshake tests** + +Move these tests (passive open handshake): +- `syn_to_listener_generates_syn_ack` (3403) +- `handshake_completes_on_ack` (3441) +- `rst_in_syn_received_removes_connection` (3495) +- `backlog_limits_syn_received` (3546) +- `window_scale_negotiation` (3611) +- `simultaneous_open_both_reach_established` (9826) + +And active open handshake: +- `active_open_handshake` (6094) +- `active_open_handshake_with_config` (6152) + +**Step 2: Add `mod handshake;` to `tests/mod.rs`** + +**Step 3: Verify** + +Run: `cargo test handshake` +Expected: All handshake tests pass. + +**Step 4: Commit** + +``` +refactor(tcp): extract handshake tests +``` + +--- + +### Task 9: Extract `tests/data_transfer.rs` + +**Files:** +- Create: `src/net/handler/tcp/tests/data_transfer.rs` + +Move: +- `established_receives_in_order_data` (3651) +- `established_out_of_order_reassembly` (3780) +- `poll_send_builds_data_segment` (3902) +- `frame_accounting_through_data_transfer` (4274) +- `poll_send_sets_psh_on_last_segment` (8846) +- `poll_send_no_psh_on_first_segment_when_more_data` (8883) +- `rcv_nxt_advances_only_by_bytes_written_to_recv_buffer` (10006) + +Commit: `refactor(tcp): extract data transfer tests` + +--- + +### Task 10: Extract `tests/retransmission.rs` + +**Files:** +- Create: `src/net/handler/tcp/tests/retransmission.rs` + +Move: +- `fast_retransmit_on_three_dup_acks` (3978) +- `rto_retransmit_on_timer_expiry` (4081) +- `rtt_estimation_updates_rto` (4167) +- `limited_transmit_sends_on_first_dup_ack` (11197) + +Commit: `refactor(tcp): extract retransmission tests` + +--- + +### Task 11: Extract `tests/teardown.rs` + +**Files:** +- Create: `src/net/handler/tcp/tests/teardown.rs` + +Move: +- `established_receives_fin_transitions_to_close_wait` (4380) +- `established_receives_fin_with_data` (4461) +- `poll_send_sends_fin_when_pending` (4551) +- `poll_send_drains_data_before_fin` (4620) +- `active_close_fin_wait1_to_fin_wait2` (4693) +- `fin_wait2_receives_fin_to_time_wait` (4780) +- `simultaneous_close_closing_to_time_wait` (4889) +- `passive_close_last_ack_removes_connection` (4999) +- `time_wait_ignores_rst` (5111) +- `time_wait_evicted_after_deadline` (5231) +- `full_active_close_lifecycle` (5296) +- `full_passive_close_lifecycle` (5464) +- `shutdown_sets_pending_fin` (6439) +- `half_close_writes_blocked_reads_continue` (7116) +- `close_wait_processes_ack_for_sent_data` (10657) +- `fin_retransmitted_in_fin_wait1` (10438) +- `out_of_order_fin_does_not_transition_to_close_wait` (10121) + +Commit: `refactor(tcp): extract teardown tests` + +--- + +### Task 12: Extract `tests/delayed_ack.rs` + +**Files:** +- Create: `src/net/handler/tcp/tests/delayed_ack.rs` + +Move: +- `delayed_ack_defers_ack_for_in_order_data` (5577) +- `delayed_ack_flushes_on_second_segment` (5666) +- `out_of_order_data_sends_immediate_ack` (5775) +- `fin_sends_immediate_ack` (5859) +- `new_connection_has_delayed_ack_fields` (5939) +- `delayed_ack_timer_flushes_pending_ack` (6006) +- `data_send_clears_delayed_ack` (6334) + +Commit: `refactor(tcp): extract delayed ACK tests` + +--- + +### Task 13: Extract `tests/nagle.rs` + +**Files:** +- Create: `src/net/handler/tcp/tests/nagle.rs` + +Move: +- `nagle_holds_small_data_when_bytes_in_flight` (6209) +- `nagle_allows_full_mss_even_with_bytes_in_flight` (6247) +- `tcp_no_delay_sends_small_data_immediately` (6283) + +Commit: `refactor(tcp): extract Nagle tests` + +--- + +### Task 14: Extract `tests/keepalive.rs` + +**Files:** +- Create: `src/net/handler/tcp/tests/keepalive.rs` + +Move: +- `keep_alive_activity_resets_probe_timer` (6518) +- `keep_alive_probe_sent_after_idle_timeout` (6615) +- `keep_alive_no_probe_when_disabled` (6697) +- `keep_alive_connection_aborted_after_max_probes` (6769) +- `linger_zero_sends_rst_on_poll_send` (6860) +- `linger_timeout_sets_deadline` (6956) +- `linger_none_normal_close` (7040) +- `keep_alive_probe_and_recovery` (7207) +- `keep_alive_exhaustion_removes_connection` (7337) +- `linger_zero_immediate_rst` (7445) + +Commit: `refactor(tcp): extract keep-alive and linger tests` + +--- + +### Task 15: Extract `tests/timestamps.rs` + +**Files:** +- Create: `src/net/handler/tcp/tests/timestamps.rs` + +Move: +- `build_ts_option` helper (7543-7551) — if not already in `tests/mod.rs` +- `paws_rejects_old_timestamp` (7552) +- `paws_drops_rst_with_old_timestamp` (7651) +- `paws_accepts_stale_ts_recent` (7747) + +Commit: `refactor(tcp): extract timestamp/PAWS tests` + +--- + +### Task 16: Extract `tests/persist.rs` + +**Files:** +- Create: `src/net/handler/tcp/tests/persist.rs` + +Move: +- `establish_connection` helper (7890-7940) — if not already in `tests/mod.rs` +- `persist_timer_activates_on_zero_window` (7941) +- `persist_probe_sent_when_deadline_expires` (7972) +- `persist_timer_clears_when_window_reopens` (8013) + +Commit: `refactor(tcp): extract persist timer tests` + +--- + +### Task 17: Extract `tests/sack.rs` + +**Files:** +- Create: `src/net/handler/tcp/tests/sack.rs` + +Move: +- `establish_connection_with_sack` helper (8178-8234) — if not already in `tests/mod.rs` +- `ooo_data_sends_sack_blocks_in_dup_ack` (8067) +- `sack_blocks_update_scoreboard_on_ack` (8235) +- `sack_scoreboard_pruned_on_cumulative_ack_advance` (8301) +- `sack_blocks_updated_on_dup_ack` (8392) +- `sack_scoreboard_cleared_on_rto` (8445) +- `fast_retransmit_uses_sack_gap` (8507) +- `fast_retransmit_fallback_when_scoreboard_empty` (8570) +- `sack_recovery_enters_on_3_dup_acks` (8621) +- `sack_recovery_partial_ack_stays_in_recovery` (8724) + +Commit: `refactor(tcp): extract SACK recovery tests` + +--- + +### Task 18: Extract `tests/ecn.rs` + +**Files:** +- Create: `src/net/handler/tcp/tests/ecn.rs` + +Move: +- `ecn_negotiated_when_both_sides_support` (8944) +- `ecn_disabled_when_peer_doesnt_support` (9032) +- `ecn_negotiated_on_passive_open` (9099) +- `ecn_ect_set_on_outgoing_data` (9192) +- `ecn_ect_not_set_on_retransmit` (9265) +- `ecn_ce_detected_on_incoming` (9340) +- `ecn_ece_sent_when_ce_received` (9437) +- `ecn_cwnd_halved_on_ece` (9539) +- `ecn_cwr_sent_on_next_data` (9657) +- `ecn_ce_received_cleared_on_cwr` (9738) + +Commit: `refactor(tcp): extract ECN tests` + +--- + +### Task 19: Extract `tests/edge_cases.rs` + +**Files:** +- Create: `src/net/handler/tcp/tests/edge_cases.rs` + +Move: +- `unmatched_syn_generates_rst` (3288) +- `rst_to_unbound_port_silently_dropped` (3319) +- `invalid_checksum_dropped` (3337) +- `truncated_tcp_header_dropped` (3369) +- `frame_accounting_after_rst` (3581) +- `rst_outside_window_is_dropped` (10239) +- `rst_in_window_but_not_exact_sends_challenge_ack` (10283) +- `syn_in_established_sends_challenge_ack` (10332) +- `segment_without_ack_is_dropped` (10387) +- `ack_beyond_snd_nxt_sends_ack_and_drops` (10500) +- `stale_segment_does_not_regress_window` (10577) +- `sender_sws_avoidance_holds_small_sends` (10797) +- `sender_sws_allows_send_when_all_data_fits` (10884) +- `segment_acceptability_*` tests (7857-7888) + +Commit: `refactor(tcp): extract edge case tests` + +--- + +### Task 20: Extract `tests/congestion.rs` + +**Files:** +- Create: `src/net/handler/tcp/tests/congestion_tests.rs` (name avoids collision with `congestion.rs` module) + +Move: +- `cubic_slow_start_on_new_ack` (10957) +- `frto_restores_cwnd_on_spurious_rto` (11052) + +Commit: `refactor(tcp): extract congestion control tests` + +--- + +### Task 21: Final cleanup and verification + +**Step 1: Verify `mod.rs` is a pure export shim** + +It should look approximately like: +```rust +pub(crate) mod congestion; +mod connection; +mod handler; +mod inbound; +mod isn; +pub(super) mod listener; +pub(crate) mod recovery; +pub(crate) mod ring_buffer; +pub(crate) mod segment; +pub(crate) mod state; +pub(crate) mod tcb; +mod timers; +mod transmit; + +pub use handler::TcpHandler; + +#[cfg(test)] +mod tests; +``` + +**Step 2: Verify all tests still pass** + +Run: `cargo test` +Expected: All 70+ tests pass with zero failures. + +**Step 3: Verify line counts are reasonable** + +Run: `wc -l src/net/handler/tcp/*.rs src/net/handler/tcp/tests/*.rs` + +Expected approximate sizes: +- `mod.rs`: ~15 lines +- `handler.rs`: ~80 lines +- `listener.rs`: ~120 lines +- `connection.rs`: ~150 lines +- `inbound.rs`: ~1,600 lines +- `timers.rs`: ~380 lines +- `transmit.rs`: ~300 lines +- `tests/mod.rs`: ~200 lines (helpers) +- Individual test files: 200-1200 lines each + +**Step 4: Commit** + +``` +refactor(tcp): final cleanup after mod.rs decomposition +``` diff --git a/docs/plans/2026-03-07-tcp-reliability-performance-design.md b/docs/plans/2026-03-07-tcp-reliability-performance-design.md new file mode 100644 index 0000000..9f75946 --- /dev/null +++ b/docs/plans/2026-03-07-tcp-reliability-performance-design.md @@ -0,0 +1,254 @@ +# TCP Reliability & Performance Design + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Fix window scaling bugs, add segment validation, timestamps, zero-window probing, full SACK, and zero-copy send path. + +**Architecture:** Six features ordered by dependency: window scaling fix first (everything depends on correct windows), then segment acceptability, timestamps (PAWS gates acceptability), zero-window probing, full SACK, and zero-copy send path. + +**Tech Stack:** Rust, coarsetime for timers, existing TCP handler infrastructure. + +--- + +## 1. Window Scaling Fix + +### Problem + +`snd_wscale` is negotiated during the handshake but never applied. `seg_wnd` from the wire is stored raw into `tcb.snd_wnd`. With `snd_wscale=7`, a peer advertising window=512 means 65536 bytes, but we treat it as 512. Advertised `rcv_wnd` in outgoing segments is also never downscaled by `rcv_wscale`. + +### Design + +**Receiving (apply snd_wscale to incoming window):** +- After SYN exchange completes (Established state onward), apply shift: `tcb.snd_wnd = (seg_wnd as u32) << tcb.snd_wscale` +- During SYN/SYN-ACK processing, window is unscaled per RFC 7323 section 2.2 -- no change there +- Affected sites in `process_established`: line ~1176 (`tcb.snd_wnd = seg_wnd`) +- Affected sites in `process_teardown` FinWait1: line ~1939 (`tcb.snd_wnd = seg_wnd`) + +**Sending (downscale rcv_wnd in outgoing segments):** +- When advertising our window in ACKs/data/FIN-ACK segments, compute: `(recv_buffer.free_space() >> rcv_wscale).min(u16::MAX as usize) as u16` +- Only apply scaling after handshake (when `wscale_enabled == true`). During SYN/SYN-ACK, continue using unscaled `DEFAULT_RCV_WND` +- Affected: all `SegmentBuilder::build_ack`, `build_data`, `build_fin_ack` calls in `process_established`, `process_teardown`, `poll_timers`, and `poll_send` + +**Helper:** Add `fn advertised_window(&self) -> u16` on Tcb to centralize the computation. + +--- + +## 2. Segment Acceptability Checks + +### Problem + +No validation that incoming segments fall within the receive window. Segments with bogus sequence numbers are processed, which is a correctness and security issue. + +### Design (RFC 9293 section 3.10.7.4) + +Four cases based on SEG.LEN and RCV.WND: + +| SEG.LEN | RCV.WND | Acceptable if | +|---------|---------|---------------| +| 0 | 0 | `SEG.SEQ == RCV.NXT` | +| 0 | > 0 | `RCV.NXT <= SEG.SEQ < RCV.NXT + RCV.WND` | +| > 0 | 0 | Not acceptable | +| > 0 | > 0 | `RCV.NXT <= SEG.SEQ < RCV.NXT + RCV.WND` OR `RCV.NXT <= SEG.SEQ + SEG.LEN - 1 < RCV.NXT + RCV.WND` | + +**Implementation:** +- Add `fn is_segment_acceptable(seg_seq: u32, seg_len: u32, rcv_nxt: u32, rcv_wnd: u32) -> bool` helper function +- Call at top of `process_established` and `process_teardown` before any processing +- If not acceptable and not RST: send ACK and drop segment +- If not acceptable and RST: drop silently +- `seg_len` computed as payload_len + SYN/FIN contribution (using existing `Tcb::seg_len`) + +--- + +## 3. Timestamps (RFC 7323) + +### Wire Format + +Option Kind=8, Length=10. Contains TSval (4 bytes, sender's timestamp) and TSecr (4 bytes, echo of received TSval). + +### Negotiation + +Both sides must include TS option in SYN. If either omits it, timestamps are disabled. We always offer if `TcpConfig::timestamps` is true; disable if peer doesn't reciprocate. + +### New Constants and Wire Functions + +In `src/net/wire/tcp.rs`: +- `options::TIMESTAMP = 8` +- `options::SACK_PERMITTED = 4` (added here, used later by SACK) +- `options::SACK = 5` (added here, used later by SACK) +- `parse_timestamp(options: &[u8]) -> Option<(u32, u32)>` -- returns (TSval, TSecr) +- `write_timestamp_option(buf: &mut [u8], tsval: u32, tsecr: u32) -> usize` -- writes 10 bytes + +### New TCB Fields + +- `ts_enabled: bool` -- negotiated during handshake +- `ts_recent: u32` -- most recent TSval received from peer +- `ts_recent_age: Instant` -- when ts_recent was updated +- `ts_offset: Instant` -- base instant for deriving our monotonic u32 clock + +### New TcpConfig Field + +- `timestamps: bool` (default `true`) + +### Timestamp Clock + +Derive u32 millisecond counter from the `now: Instant` parameter already passed through the call chain: `now.duration_since(ts_offset).as_millis() as u32`. Wraps every ~49 days, which is fine per RFC 7323. Never calls `Instant::now()`. + +### RTTM (RFC 7323 section 4.1) + +On every valid ACK received with TSecr != 0, compute RTT: `rtt = our_current_ts - TSecr`. This replaces the single-sample `last_send_time` approach with per-ACK measurement. The existing SRTT/RTTVAR/RTO computation (RFC 6298) remains unchanged -- just fed more samples. + +### PAWS (RFC 7323 section 5) + +Before segment acceptability check in `process_established` and `process_teardown`, if `ts_enabled`: +- If `SEG.TSval < ts_recent` and segment is not RST: drop segment, send ACK +- Exception: if `ts_recent_age > 24 days`, accept anyway (stale timestamp invalidation) +- On acceptable segment with data or SYN: update `ts_recent = SEG.TSval`, `ts_recent_age = now` + +### Segment Size Impact + +Every non-SYN segment grows by 12 bytes (NOP + NOP + 10-byte TS option for alignment). `build_ack`, `build_data`, `build_fin_ack` all accept optional `ts: Option<(u32, u32)>` parameter. SYN/SYN-ACK option buffers grow to accommodate TS. + +**SYN option layout (when all options enabled):** +MSS(4) + NOP(1) + WSCALE(3) + NOP(1) + NOP(1) + TS(10) + SACK_PERMITTED(2) = 22 bytes, padded to 24. + +--- + +## 4. Zero-Window Probing (Persist Timer) + +### Problem + +When the peer advertises `window=0`, the sender stops. If the peer's window update ACK is lost, the connection deadlocks. + +### New TCB Fields + +- `persist_deadline: Option` -- when to send next zero-window probe +- `persist_backoff: u8` -- exponential backoff counter (caps at 6, max ~64s) + +### Logic (in `poll_send`) + +**Detection:** When `send_window == 0` and there's data to send, set `persist_deadline` if not already set. Initial deadline: `now + rto`. + +**Probing:** When `persist_deadline` is set and `now >= persist_deadline`: +- Send 1-byte probe segment: peek 1 byte from send buffer at `bytes_in_flight` offset +- Advance `snd_nxt` by 1 +- Set next deadline: `now + rto << persist_backoff` (capped at 60s) +- Increment `persist_backoff` (cap at 6) + +**Recovery:** When any ACK arrives with `snd_wnd > 0` (in `process_established` ACK handling): clear `persist_deadline`, reset `persist_backoff = 0`. + +**Why poll_send:** The persist timer is tightly coupled with the send path -- it needs send buffer state, flight tracking, and segment building. Keeping it in `poll_send` avoids duplicating context. + +--- + +## 5. Full SACK (RFC 2018 / RFC 6675) + +### 5a. Negotiation + +**SACK Permitted option** (Kind=4, Length=2) sent in SYN and SYN-ACK. Both sides must include it. We always offer if `TcpConfig::sack` is true; disable if peer doesn't reciprocate. + +**New TCB field:** `sack_enabled: bool` + +**New TcpConfig field:** `sack: bool` (default `true`) + +**New wire functions:** +- `parse_sack_permitted(options: &[u8]) -> bool` +- `write_sack_permitted_option(buf: &mut [u8]) -> usize` -- 2 bytes +- `parse_sack_blocks(options: &[u8]) -> ([Option<(u32, u32)>; 4], usize)` -- fixed-size array, returns count to avoid allocation +- `write_sack_option(buf: &mut [u8], blocks: &[(u32, u32)]) -> usize` -- variable length + +### 5b. Sending SACK Blocks (Receiver Side) + +When OOO data arrives, report our `ooo_ranges` as SACK blocks in the duplicate ACK. + +**Option space budget (40 bytes max):** +- Timestamps enabled: 12 bytes used, 28 remaining -> 3 SACK blocks max (2 + 8*3 = 26) +- Timestamps disabled: 40 bytes -> 4 SACK blocks max (2 + 8*4 = 34) + +**Block ordering:** Most recently received range first (per RFC 2018 section 3). + +**Implementation:** Convert `ooo_ranges` entries to SACK blocks in the OOO data path of `process_established` (line ~1257). Build a new `SegmentBuilder::build_ack_with_options` or extend `build_ack` to accept optional SACK blocks and timestamp. + +### 5c. Using Received SACK Blocks (Sender Side) + +**New TCB field:** +- `sack_scoreboard: BTreeMap` -- byte ranges peer has confirmed (left_edge -> right_edge) + +**On receiving ACK with SACK blocks:** +1. Parse SACK blocks from options +2. Insert/merge ranges into scoreboard +3. On cumulative ACK advance (`snd_una` moves forward), remove scoreboard entries below `snd_una` + +**Selective retransmission (in `poll_timers` fast retransmit path):** +- When `dup_ack_count >= 3` and `sack_enabled`: + - Find first gap in scoreboard between `snd_una` and `snd_nxt` + - Retransmit data from that gap (1 MSS) + - Track retransmitted ranges to avoid re-sending same gap in one recovery episode + +**On RTO retransmit:** Clear scoreboard entirely -- fall back to full retransmit per RFC 6675. + +--- + +## 6. Zero-Copy Send Path + +### Problem + +`poll_send` and `poll_timers` allocate `vec![0u8; to_send]` for every data segment, copying from the ring buffer into a temporary buffer before passing to `build_data`. Unnecessary when ring buffer data doesn't wrap, and avoidable even when it does. + +### Design + +**Ring buffer change:** Add `peek_slices(offset: usize, len: usize) -> (&[u8], &[u8])` to `RingBuffer`. Returns two slices -- first covers head-to-end-of-buffer, second covers wrap-around portion. Second slice is empty if data doesn't wrap. + +**SegmentBuilder change:** `build_data` takes `payload: (&[u8], &[u8])` instead of `&[u8]`. Writes both slices contiguously into the frame. Checksum computation handles two slices (already works over arbitrary byte ranges). + +**Affected call sites:** `poll_send` data path, `poll_timers` fast retransmit, `poll_timers` RTO retransmit -- all three drop the heap allocation and pass two slices directly. + +--- + +## Test Plan + +### Window Scaling Tests +1. Incoming `seg_wnd` is scaled by `snd_wscale` in Established state +2. Outgoing window is downscaled by `rcv_wscale` +3. SYN/SYN-ACK windows remain unscaled +4. Window scaling disabled when peer doesn't negotiate + +### Segment Acceptability Tests +5. In-window segment accepted +6. Out-of-window segment dropped with ACK sent +7. Zero-length segment with zero window: only exact match accepted +8. RST outside window dropped silently + +### Timestamp Tests +9. TS option negotiated when both sides offer +10. TS disabled when peer doesn't offer +11. RTTM: RTT computed from TSecr on each ACK +12. PAWS: segment with old TSval dropped +13. PAWS: stale ts_recent (>24 days) allows segment through + +### Zero-Window Probing Tests +14. Persist probe sent when peer window is 0 +15. Backoff increases between probes +16. Probing stops when window reopens + +### SACK Tests +17. SACK Permitted negotiated in SYN exchange +18. SACK blocks sent in duplicate ACKs for OOO data +19. SACK blocks parsed and scoreboard updated +20. Selective retransmit: only gap data resent, not already-SACKed data +21. Scoreboard cleared on RTO + +### Zero-Copy Send Tests +22. `peek_slices` returns single slice when data doesn't wrap +23. `peek_slices` returns two slices when data wraps +24. `build_data` with two-slice payload produces correct frame + +## Scope + +**Modified files:** +- `src/net/wire/tcp.rs` -- new option constants, parse/write functions for timestamps, SACK permitted, SACK blocks +- `src/net/handler/tcp/tcb.rs` -- new fields on Tcb and TcpConfig +- `src/net/handler/tcp/mod.rs` -- window scaling fix, segment acceptability, PAWS, timestamps in established/teardown, persist timer, SACK block generation/processing, selective retransmit +- `src/net/handler/tcp/segment.rs` -- build_data signature change (two slices), timestamp option in ACK/data/FIN-ACK builders +- `src/net/handler/tcp/ring_buffer.rs` -- peek_slices method + +**No new files.** diff --git a/docs/plans/2026-03-07-tcp-reliability-performance-impl.md b/docs/plans/2026-03-07-tcp-reliability-performance-impl.md new file mode 100644 index 0000000..4d00ca0 --- /dev/null +++ b/docs/plans/2026-03-07-tcp-reliability-performance-impl.md @@ -0,0 +1,1817 @@ +# TCP Reliability & Performance Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Fix window scaling bugs, add segment acceptability checks, TCP timestamps (RTTM/PAWS), zero-window probing, full SACK support, and zero-copy send path. + +**Architecture:** Six features implemented bottom-up by dependency: window scaling fix (prerequisite for correct windows), segment acceptability (correctness gate), timestamps with PAWS (gates segment processing), zero-window probing (needs correct windows), full SACK (needs correct validation), zero-copy send (independent optimization). Each feature adds wire-level option support, TCB state, handler logic, and tests. + +**Tech Stack:** Rust, coarsetime for timers, existing TCP handler infrastructure in `src/net/handler/tcp/`. + +**Testing:** `cargo test` (no feature flags, no `--all-features`). Tests run under `sudo -E` via `.cargo/config.toml`. + +**Key files reference:** +- Wire format: `src/net/wire/tcp.rs` +- TCB/config: `src/net/handler/tcp/tcb.rs` +- Handler: `src/net/handler/tcp/mod.rs` +- Segment builder: `src/net/handler/tcp/segment.rs` +- Ring buffer: `src/net/handler/tcp/ring_buffer.rs` +- Socket layer: `src/net/socket/tcp.rs` +- Checksum: `src/net/checksum/compute.rs` + +--- + +## Task 1: Window Scaling Fix — TCB Helper and Incoming Window + +**Files:** +- Modify: `src/net/handler/tcp/tcb.rs` +- Modify: `src/net/handler/tcp/mod.rs` + +**Context:** `snd_wscale` is negotiated but never applied to incoming `seg_wnd`. Every `tcb.snd_wnd = seg_wnd` stores the raw 16-bit value instead of `seg_wnd << snd_wscale`. Per RFC 7323 §2.2, scaling applies only after the SYN exchange completes (Established state onward). + +**Step 1: Add `advertised_window` helper to Tcb** + +In `src/net/handler/tcp/tcb.rs`, add to the `impl Tcb` block: + +```rust +/// Compute the window value to advertise in outgoing segments. +/// Downscales by `rcv_wscale` if window scaling is enabled. +#[inline] +pub fn advertised_window(&self) -> u16 { + let free = self.recv_buffer.free_space(); + if self.wscale_enabled { + (free >> self.rcv_wscale as usize).min(u16::MAX as usize) as u16 + } else { + free.min(u16::MAX as usize) as u16 + } +} +``` + +**Step 2: Add `scale_incoming_window` helper** + +In `src/net/handler/tcp/tcb.rs`, add to the `impl Tcb` block: + +```rust +/// Scale an incoming window value by `snd_wscale`. +/// Only call after SYN exchange (Established onward). +#[inline] +pub fn scale_incoming_window(&self, raw_wnd: u32) -> u32 { + if self.wscale_enabled { + raw_wnd << self.snd_wscale as u32 + } else { + raw_wnd + } +} +``` + +**Step 3: Write tests for the helpers** + +Add to the `#[cfg(test)]` section at the bottom of `src/net/handler/tcp/tcb.rs` (create the module if it doesn't exist): + +```rust +#[cfg(test)] +mod tests { + use super::*; + use crate::net::handler::tcp::ring_buffer::RingBuffer; + + fn make_test_tcb() -> Tcb { + // Minimal Tcb for testing helpers — only relevant fields matter. + Tcb { + id: ConnectionId { + local_addr: crate::net::wire::ip::IpAddress::V4( + crate::net::wire::ip::Ipv4Address::new([10, 0, 0, 1]), + ), + local_port: 8080, + remote_addr: crate::net::wire::ip::IpAddress::V4( + crate::net::wire::ip::Ipv4Address::new([10, 0, 0, 2]), + ), + remote_port: 80, + }, + state: TcpState::Established, + from_passive_open: false, + iss: 0, + snd_una: 0, + snd_nxt: 0, + snd_wnd: 0, + snd_wl1: 0, + snd_wl2: 0, + irs: 0, + rcv_nxt: 0, + rcv_wnd: 0, + snd_mss: 1460, + rcv_mss: 1460, + eff_snd_mss: 1460, + snd_wscale: 0, + rcv_wscale: 0, + wscale_enabled: false, + retransmit_deadline: None, + rto_backoff: 0, + event_queue: crate::net::socket::LocalQueue::new(), + send_buffer: RingBuffer::new(1024), + recv_buffer: RingBuffer::new(1024), + ooo_ranges: std::collections::BTreeMap::new(), + cwnd: 65535, + ssthresh: 65535, + dup_ack_count: 0, + srtt: None, + rttvar: 0, + rto: 1000, + last_send_time: None, + pending_fin: false, + fin_seq: None, + time_wait_deadline: None, + time_wait_duration: 60_000, + ack_pending: false, + delayed_ack_deadline: None, + ack_delay_count: 0, + delayed_ack_ms: 40, + nagle_enabled: true, + keep_alive_enabled: false, + keep_alive_idle_ms: 7_200_000, + keep_alive_interval_ms: 75_000, + keep_alive_count: 9, + last_activity: Instant::now(), + keep_alive_probes_sent: 0, + linger: None, + linger_deadline: None, + } + } + + #[test] + fn advertised_window_no_scaling() { + let tcb = make_test_tcb(); + // recv_buffer is 1024 bytes, all free + assert_eq!(tcb.advertised_window(), 1024); + } + + #[test] + fn advertised_window_with_scaling() { + let mut tcb = make_test_tcb(); + tcb.wscale_enabled = true; + tcb.rcv_wscale = 7; // divide by 128 + // 1024 >> 7 = 8 + assert_eq!(tcb.advertised_window(), 8); + } + + #[test] + fn scale_incoming_window_no_scaling() { + let tcb = make_test_tcb(); + assert_eq!(tcb.scale_incoming_window(512), 512); + } + + #[test] + fn scale_incoming_window_with_scaling() { + let mut tcb = make_test_tcb(); + tcb.wscale_enabled = true; + tcb.snd_wscale = 7; // multiply by 128 + assert_eq!(tcb.scale_incoming_window(512), 512 * 128); + } +} +``` + +**Step 4: Apply `scale_incoming_window` to all `snd_wnd` assignments in Established+ states** + +In `src/net/handler/tcp/mod.rs`, find every `tcb.snd_wnd = seg_wnd` that occurs after the SYN exchange: + +1. In `process_established` (valid new ACK branch): change `tcb.snd_wnd = seg_wnd;` to `tcb.snd_wnd = tcb.scale_incoming_window(seg_wnd);` +2. In `process_teardown` FinWait1 ACK branch: same change. +3. Do NOT change the assignments in `process_syn_received` or `process_syn_sent` — those are during handshake where window is unscaled. + +**Step 5: Run tests** + +Run: `cargo test` +Expected: All tests pass. + +**Step 6: Commit** + +```bash +git add src/net/handler/tcp/tcb.rs src/net/handler/tcp/mod.rs +git commit -m "fix: apply window scaling to incoming seg_wnd and outgoing rcv_wnd" +``` + +--- + +## Task 2: Window Scaling Fix — Outgoing Window in All Segments + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs` + +**Context:** All `build_ack`, `build_data`, `build_fin_ack` calls pass either `DEFAULT_RCV_WND` (hardcoded 65535) or `recv_buffer.free_space().min(u16::MAX as usize) as u16` for the window parameter. Both are wrong when window scaling is enabled — they need to use `tcb.advertised_window()`. + +**Step 1: Write a test verifying outgoing window is scaled** + +Add to the test module in `src/net/handler/tcp/mod.rs`: + +```rust +#[test] +fn outgoing_window_uses_wscale() { + // Set up a connection with wscale_enabled and rcv_wscale = 7. + // Send data to trigger an ACK, then verify the window field in the ACK + // is downscaled by rcv_wscale. + let mut handler = TcpHandler::new(false, false); + let config = TcpConfig { + recv_buffer_size: 1024, // power of 2 + send_buffer_size: 1024, + ..TcpConfig::default() + }; + let local_addr = IpAddress::V4(crate::net::wire::ip::Ipv4Address::new([10, 0, 0, 1])); + let remote_addr = IpAddress::V4(crate::net::wire::ip::Ipv4Address::new([10, 0, 0, 2])); + let local_port = 80u16; + let remote_port = 12345u16; + + // Create a connection directly in Established state with wscale. + let now = Instant::now(); + let conn_id = ConnectionId { + local_addr, + local_port, + remote_addr, + remote_port, + }; + + // Use listen + inject SYN with wscale to establish connection properly. + // For simplicity, manually create TCB: + handler.connections.push(Tcb { + id: conn_id, + state: TcpState::Established, + from_passive_open: true, + iss: 100, + snd_una: 101, + snd_nxt: 101, + snd_wnd: 65535, + snd_wl1: 0, + snd_wl2: 0, + irs: 200, + rcv_nxt: 201, + rcv_wnd: 0, + snd_mss: 1460, + rcv_mss: 1460, + eff_snd_mss: 1460, + snd_wscale: 7, + rcv_wscale: 7, + wscale_enabled: true, + retransmit_deadline: None, + rto_backoff: 0, + event_queue: crate::net::socket::LocalQueue::new(), + send_buffer: RingBuffer::new(config.send_buffer_size), + recv_buffer: RingBuffer::new(config.recv_buffer_size), + ooo_ranges: std::collections::BTreeMap::new(), + cwnd: 65535, + ssthresh: 65535, + dup_ack_count: 0, + srtt: None, + rttvar: 0, + rto: 1000, + last_send_time: None, + pending_fin: false, + fin_seq: None, + time_wait_deadline: None, + time_wait_duration: 60_000, + ack_pending: false, + delayed_ack_deadline: None, + ack_delay_count: 0, + delayed_ack_ms: 40, + nagle_enabled: true, + keep_alive_enabled: false, + keep_alive_idle_ms: 7_200_000, + keep_alive_interval_ms: 75_000, + keep_alive_count: 9, + last_activity: now, + keep_alive_probes_sent: 0, + linger: None, + linger_deadline: None, + }); + + // Trigger a delayed ACK flush to get an outgoing segment. + handler.connections[0].ack_pending = true; + handler.connections[0].delayed_ack_deadline = Some(now); // already expired + + let src_mac = crate::net::wire::ethernet::MacAddress::new([0xAA; 6]); + let neighbor = NeighborHandler::new(src_mac); + let mut free = crate::xdp::frame::BasicFrameBuffer::new(4); + let mut tx = crate::xdp::frame::BasicFrameBuffer::new(4); + + let buf = Box::leak(vec![0u8; 2048].into_boxed_slice()); + let frame = crate::xdp::frame::Frame::new(0, buf, 2048, false); + free.push(frame); + + handler.poll_timers(now, src_mac, &neighbor, &mut free, &mut tx); + + assert_eq!(tx.num_frames(), 1); + let out_frame = tx.pop().unwrap(); + let tcp_offset = 14 + 20; // ETH + IPv4 + let tcp = unsafe { TcpHeader::from_bytes_at(&out_frame, tcp_offset) }; + // 1024 bytes free >> 7 = 8 + assert_eq!(tcp.window(), 8); +} +``` + +**Step 2: Run test to verify it fails** + +Run: `cargo test outgoing_window_uses_wscale` +Expected: FAIL — window will be 1024 (unscaled). + +**Step 3: Replace all window computations with `advertised_window()`** + +In `src/net/handler/tcp/mod.rs`, replace every occurrence of: +- `DEFAULT_RCV_WND` used as a window argument in ACK/data segments in Established+ states +- `tcb.recv_buffer.free_space().min(u16::MAX as usize) as u16` and similar + +With `tcb.advertised_window()`. + +Specifically update these call sites: +1. `process_established` — delayed ACK flush `build_ack` call (line ~1231 area) +2. `process_established` — OOO duplicate ACK `build_ack` call +3. `process_established` — duplicate data ACK `build_ack` call +4. `process_established` — FIN handling `build_ack` call +5. `poll_timers` — delayed ACK pass `build_ack` call +6. `poll_timers` — keep-alive probe `build_ack` call +7. `poll_timers` — fast retransmit `build_data` call +8. `poll_timers` — RTO retransmit `build_data` call +9. `poll_send` — data `build_data` call +10. `poll_send` — FIN `build_fin_ack` call +11. `process_teardown` — all `build_ack` calls in FinWait1, FinWait2, TimeWait states +12. `process_syn_received` — challenge ACK (this one should stay as `DEFAULT_RCV_WND` since it's pre-Established) + +**Important:** Keep `DEFAULT_RCV_WND` in `build_syn`, `build_syn_ack`, and the challenge ACK in `process_syn_received`. These are pre-Established. + +**Step 4: Run tests** + +Run: `cargo test` +Expected: All tests pass. + +**Step 5: Commit** + +```bash +git add src/net/handler/tcp/mod.rs +git commit -m "fix: use advertised_window() for all outgoing segments" +``` + +--- + +## Task 3: Segment Acceptability Checks + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs` + +**Context:** RFC 9293 §3.10.7.4 requires checking that incoming segments fall within the receive window before processing. Currently no such check exists. + +**Step 1: Write the acceptability helper and tests** + +Add to `src/net/handler/tcp/mod.rs` (inside `impl TcpHandler` or as a free function): + +```rust +/// Check segment acceptability per RFC 9293 §3.10.7.4. +/// +/// Returns `true` if the segment is within the receive window. +#[inline] +fn is_segment_acceptable(seg_seq: u32, seg_len: u32, rcv_nxt: u32, rcv_wnd: u32) -> bool { + use crate::net::wire::tcp::{seq_le, seq_lt}; + + if seg_len == 0 { + if rcv_wnd == 0 { + seg_seq == rcv_nxt + } else { + // RCV.NXT <= SEG.SEQ < RCV.NXT + RCV.WND + seq_le(rcv_nxt, seg_seq) && seq_lt(seg_seq, rcv_nxt.wrapping_add(rcv_wnd)) + } + } else { + if rcv_wnd == 0 { + false + } else { + let seg_end = seg_seq.wrapping_add(seg_len - 1); + let wnd_end = rcv_nxt.wrapping_add(rcv_wnd); + // Start or end of segment in window + (seq_le(rcv_nxt, seg_seq) && seq_lt(seg_seq, wnd_end)) + || (seq_le(rcv_nxt, seg_end) && seq_lt(seg_end, wnd_end)) + } + } +} +``` + +Add tests: + +```rust +#[test] +fn segment_acceptability_zero_len_zero_wnd() { + // Only exact match accepted + assert!(is_segment_acceptable(100, 0, 100, 0)); + assert!(!is_segment_acceptable(101, 0, 100, 0)); +} + +#[test] +fn segment_acceptability_zero_len_nonzero_wnd() { + // SEG.SEQ must be in [RCV.NXT, RCV.NXT + RCV.WND) + assert!(is_segment_acceptable(100, 0, 100, 1000)); + assert!(is_segment_acceptable(1099, 0, 100, 1000)); + assert!(!is_segment_acceptable(1100, 0, 100, 1000)); + assert!(!is_segment_acceptable(99, 0, 100, 1000)); +} + +#[test] +fn segment_acceptability_nonzero_len_zero_wnd() { + // Never acceptable + assert!(!is_segment_acceptable(100, 10, 100, 0)); +} + +#[test] +fn segment_acceptability_nonzero_len_nonzero_wnd() { + // Start in window + assert!(is_segment_acceptable(100, 10, 100, 1000)); + // End in window (start before) + assert!(is_segment_acceptable(95, 10, 100, 1000)); + // Completely outside + assert!(!is_segment_acceptable(1200, 10, 100, 1000)); + // Completely before + assert!(!is_segment_acceptable(80, 10, 100, 1000)); +} +``` + +**Step 2: Add the check at the top of `process_established`** + +After the RST check (step 1 in process_established), before ACK processing, add: + +```rust +// Segment acceptability check (RFC 9293 §3.10.7.4). +{ + let tcb = &self.connections[idx]; + let seg_len = Tcb::seg_len(payload_len, seg_flags); + let rcv_wnd = if tcb.wscale_enabled { + tcb.recv_buffer.free_space() as u32 + } else { + tcb.recv_buffer.free_space().min(u16::MAX as usize) as u32 + }; + if !is_segment_acceptable(seg_seq, seg_len, tcb.rcv_nxt, rcv_wnd) { + if seg_flags & flags::RST == 0 { + // Send ACK for out-of-window segment. + SegmentBuilder::build_ack( + tcb.id.local_addr, + tcb.id.remote_addr, + tcb.id.local_port, + tcb.id.remote_port, + tcb.snd_nxt, + tcb.rcv_nxt, + tcb.advertised_window(), + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); + } + rx_return.push(frame); + return; + } +} +``` + +**Step 3: Add similar check at the top of `process_teardown`** + +After the RST check in `process_teardown`, add the same acceptability check (before the `match state` block). The check is the same code — extract the rcv_wnd from the TCB and call `is_segment_acceptable`. + +**Step 4: Run tests** + +Run: `cargo test` +Expected: All tests pass. Some existing tests may need adjustment if they were sending segments with bogus sequence numbers that now get rejected. + +**Step 5: Commit** + +```bash +git add src/net/handler/tcp/mod.rs +git commit -m "feat: add segment acceptability checks per RFC 9293" +``` + +--- + +## Task 4: TCP Options — Timestamp and SACK Permitted Wire Format + +**Files:** +- Modify: `src/net/wire/tcp.rs` + +**Context:** Add option constants, parsing, and writing functions for Timestamps (Kind=8) and SACK Permitted (Kind=4). SACK block parsing (Kind=5) is also added here for later use. + +**Step 1: Add option constants** + +In `src/net/wire/tcp.rs`, extend the `options` module: + +```rust +pub mod options { + pub const END: u8 = 0; + pub const NOP: u8 = 1; + pub const MSS: u8 = 2; + pub const WINDOW_SCALE: u8 = 3; + pub const SACK_PERMITTED: u8 = 4; + pub const SACK: u8 = 5; + pub const TIMESTAMP: u8 = 8; +} +``` + +**Step 2: Add timestamp parsing and writing** + +```rust +/// Parse Timestamp option (Kind=8, Len=10). +/// Returns (TSval, TSecr) or None if not present. +pub fn parse_timestamp(options: &[u8]) -> Option<(u32, u32)> { + let mut i = 0; + while i < options.len() { + match options[i] { + options::END => break, + options::NOP => { i += 1; } + options::TIMESTAMP => { + if i + 10 > options.len() { return None; } + if options[i + 1] != 10 { return None; } + let tsval = u32::from_be_bytes([ + options[i + 2], options[i + 3], options[i + 4], options[i + 5], + ]); + let tsecr = u32::from_be_bytes([ + options[i + 6], options[i + 7], options[i + 8], options[i + 9], + ]); + return Some((tsval, tsecr)); + } + _ => { + if i + 1 >= options.len() { return None; } + let len = options[i + 1] as usize; + if len < 2 || i + len > options.len() { return None; } + i += len; + } + } + } + None +} + +/// Write a 10-byte Timestamp option. Returns 10. +pub fn write_timestamp_option(buf: &mut [u8], tsval: u32, tsecr: u32) -> usize { + buf[0] = options::TIMESTAMP; + buf[1] = 10; + buf[2..6].copy_from_slice(&tsval.to_be_bytes()); + buf[6..10].copy_from_slice(&tsecr.to_be_bytes()); + 10 +} +``` + +**Step 3: Add SACK Permitted parsing and writing** + +```rust +/// Check if SACK Permitted option (Kind=4, Len=2) is present. +pub fn parse_sack_permitted(options: &[u8]) -> bool { + let mut i = 0; + while i < options.len() { + match options[i] { + options::END => break, + options::NOP => { i += 1; } + options::SACK_PERMITTED => { + if i + 2 > options.len() { return false; } + if options[i + 1] != 2 { return false; } + return true; + } + _ => { + if i + 1 >= options.len() { return false; } + let len = options[i + 1] as usize; + if len < 2 || i + len > options.len() { return false; } + i += len; + } + } + } + false +} + +/// Write a 2-byte SACK Permitted option. Returns 2. +pub fn write_sack_permitted_option(buf: &mut [u8]) -> usize { + buf[0] = options::SACK_PERMITTED; + buf[1] = 2; + 2 +} +``` + +**Step 4: Add SACK block parsing and writing** + +```rust +/// Parse SACK blocks (Kind=5, variable length). +/// Returns up to 4 blocks as (left_edge, right_edge) pairs and the count. +pub fn parse_sack_blocks(options: &[u8]) -> ([Option<(u32, u32)>; 4], usize) { + let mut blocks = [None; 4]; + let mut i = 0; + while i < options.len() { + match options[i] { + options::END => break, + options::NOP => { i += 1; } + options::SACK => { + if i + 2 > options.len() { return (blocks, 0); } + let len = options[i + 1] as usize; + if len < 2 || i + len > options.len() { return (blocks, 0); } + let data_len = len - 2; + let num_blocks = data_len / 8; + let count = num_blocks.min(4); + for b in 0..count { + let off = i + 2 + b * 8; + let left = u32::from_be_bytes([ + options[off], options[off + 1], options[off + 2], options[off + 3], + ]); + let right = u32::from_be_bytes([ + options[off + 4], options[off + 5], options[off + 6], options[off + 7], + ]); + blocks[b] = Some((left, right)); + } + return (blocks, count); + } + _ => { + if i + 1 >= options.len() { return (blocks, 0); } + let len = options[i + 1] as usize; + if len < 2 || i + len > options.len() { return (blocks, 0); } + i += len; + } + } + } + (blocks, 0) +} + +/// Write SACK blocks option. Returns bytes written (2 + 8*N). +/// `blocks` should contain up to 4 (left_edge, right_edge) pairs. +pub fn write_sack_option(buf: &mut [u8], blocks: &[(u32, u32)]) -> usize { + let count = blocks.len().min(4); + let len = 2 + count * 8; + buf[0] = options::SACK; + buf[1] = len as u8; + for (i, &(left, right)) in blocks.iter().take(count).enumerate() { + let off = 2 + i * 8; + buf[off..off + 4].copy_from_slice(&left.to_be_bytes()); + buf[off + 4..off + 8].copy_from_slice(&right.to_be_bytes()); + } + len +} +``` + +**Step 5: Write tests for all new functions** + +```rust +#[test] +fn parse_timestamp_valid() { + let opts = [options::TIMESTAMP, 10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00]; + assert_eq!(parse_timestamp(&opts), Some((0x00010000, 0x00020000))); +} + +#[test] +fn parse_timestamp_absent() { + let opts = [options::MSS, 4, 0x05, 0xB4]; + assert_eq!(parse_timestamp(&opts), None); +} + +#[test] +fn write_timestamp_option_roundtrip() { + let mut buf = [0u8; 10]; + let written = write_timestamp_option(&mut buf, 12345, 67890); + assert_eq!(written, 10); + assert_eq!(parse_timestamp(&buf), Some((12345, 67890))); +} + +#[test] +fn parse_sack_permitted_valid() { + let opts = [options::SACK_PERMITTED, 2]; + assert!(parse_sack_permitted(&opts)); +} + +#[test] +fn parse_sack_permitted_absent() { + let opts = [options::MSS, 4, 0x05, 0xB4]; + assert!(!parse_sack_permitted(&opts)); +} + +#[test] +fn write_sack_permitted_roundtrip() { + let mut buf = [0u8; 2]; + let written = write_sack_permitted_option(&mut buf); + assert_eq!(written, 2); + assert!(parse_sack_permitted(&buf)); +} + +#[test] +fn parse_sack_blocks_valid() { + let mut opts = [0u8; 18]; // 2 header + 2 blocks * 8 + opts[0] = options::SACK; + opts[1] = 18; + opts[2..6].copy_from_slice(&100u32.to_be_bytes()); + opts[6..10].copy_from_slice(&200u32.to_be_bytes()); + opts[10..14].copy_from_slice(&300u32.to_be_bytes()); + opts[14..18].copy_from_slice(&400u32.to_be_bytes()); + let (blocks, count) = parse_sack_blocks(&opts); + assert_eq!(count, 2); + assert_eq!(blocks[0], Some((100, 200))); + assert_eq!(blocks[1], Some((300, 400))); +} + +#[test] +fn write_sack_option_roundtrip() { + let mut buf = [0u8; 34]; + let sack_blocks = [(100, 200), (300, 400)]; + let written = write_sack_option(&mut buf, &sack_blocks); + assert_eq!(written, 18); + let (blocks, count) = parse_sack_blocks(&buf[..written]); + assert_eq!(count, 2); + assert_eq!(blocks[0], Some((100, 200))); + assert_eq!(blocks[1], Some((300, 400))); +} + +#[test] +fn parse_all_options_mixed() { + // MSS + NOP + WSCALE + NOP + NOP + TIMESTAMP + SACK_PERMITTED + let mut opts = [0u8; 24]; + let mut i = 0; + i += write_mss_option(&mut opts[i..], 1460); // 4 + opts[i] = options::NOP; i += 1; // 5 + i += write_window_scale_option(&mut opts[i..], 7); // 8 + opts[i] = options::NOP; i += 1; // 9 + opts[i] = options::NOP; i += 1; // 10 + i += write_timestamp_option(&mut opts[i..], 1000, 2000); // 20 + i += write_sack_permitted_option(&mut opts[i..]); // 22 + + assert_eq!(parse_mss(&opts[..i]), Some(1460)); + assert_eq!(parse_window_scale(&opts[..i]), Some(7)); + assert_eq!(parse_timestamp(&opts[..i]), Some((1000, 2000))); + assert!(parse_sack_permitted(&opts[..i])); +} +``` + +**Step 6: Run tests** + +Run: `cargo test` +Expected: All tests pass. + +**Step 7: Commit** + +```bash +git add src/net/wire/tcp.rs +git commit -m "feat: add timestamp, SACK permitted, and SACK block option parsing/writing" +``` + +--- + +## Task 5: Timestamp and SACK TCB Fields + +**Files:** +- Modify: `src/net/handler/tcp/tcb.rs` + +**Context:** Add fields to `TcpConfig` and `Tcb` for timestamp and SACK state. + +**Step 1: Add TcpConfig fields** + +In `TcpConfig` struct, add after `linger`: + +```rust +/// Enable TCP timestamps (RFC 7323). Default: true. +pub timestamps: bool, +/// Enable SACK (RFC 2018). Default: true. +pub sack: bool, +``` + +In `Default for TcpConfig`, add: + +```rust +timestamps: true, +sack: true, +``` + +**Step 2: Add Tcb fields** + +In `Tcb` struct, add after the linger section: + +```rust +// --- Timestamps (RFC 7323) --- +/// Whether timestamps were negotiated. +pub ts_enabled: bool, +/// Most recent TSval received from peer. +pub ts_recent: u32, +/// When ts_recent was last updated. +pub ts_recent_age: Instant, +/// Base instant for deriving our monotonic timestamp clock. +pub ts_offset: Instant, + +// --- SACK --- +/// Whether SACK was negotiated. +pub sack_enabled: bool, +/// Scoreboard: byte ranges the peer has confirmed receiving (left_edge -> right_edge). +pub sack_scoreboard: BTreeMap, + +// --- Zero-window probing --- +/// Deadline for next zero-window probe. +pub persist_deadline: Option, +/// Exponential backoff counter for persist probes (cap at 6). +pub persist_backoff: u8, +``` + +**Step 3: Update all Tcb construction sites** + +In `src/net/handler/tcp/mod.rs`, find every place where a `Tcb { ... }` is constructed (active open and passive open paths) and add the new fields with defaults: + +```rust +ts_enabled: false, +ts_recent: 0, +ts_recent_age: now, +ts_offset: now, +sack_enabled: false, +sack_scoreboard: BTreeMap::new(), +persist_deadline: None, +persist_backoff: 0, +``` + +Also update `ListenEntry` to include `timestamps` and `sack` fields (carried from TcpConfig). + +**Step 4: Update test TCB construction** + +Update `make_test_tcb()` and any inline Tcb construction in tests to include the new fields. + +**Step 5: Run tests** + +Run: `cargo test` +Expected: All tests pass. + +**Step 6: Commit** + +```bash +git add src/net/handler/tcp/tcb.rs src/net/handler/tcp/mod.rs +git commit -m "feat: add timestamp, SACK, and persist timer TCB fields" +``` + +--- + +## Task 6: Timestamp Negotiation in SYN/SYN-ACK + +**Files:** +- Modify: `src/net/handler/tcp/segment.rs` +- Modify: `src/net/handler/tcp/mod.rs` + +**Context:** SYN and SYN-ACK must include the Timestamp option when `TcpConfig::timestamps` is true. SACK Permitted is also included when `TcpConfig::sack` is true. Update `build_syn` and `build_syn_ack` to accept optional timestamp and SACK permitted parameters. + +**Step 1: Update `build_syn` signature and option buffer** + +Change `build_syn` to accept additional parameters: + +```rust +pub fn build_syn<'umem>( + local_addr: IpAddress, + remote_addr: IpAddress, + local_port: u16, + remote_port: u16, + iss: u32, + window: u16, + mss: u16, + wscale: u8, + timestamp: Option<(u32, u32)>, // NEW: (TSval, TSecr=0) + sack_permitted: bool, // NEW + src_mac: MacAddress, + dst_mac: MacAddress, + tx_offload: bool, + free_frames: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, +) +``` + +Update the option buffer to 24 bytes and build options: + +```rust +let mut opt_buf = [0u8; 24]; +let mut opt_len = write_mss_option(&mut opt_buf, mss); +opt_buf[opt_len] = options::NOP; +opt_len += 1; +opt_len += write_window_scale_option(&mut opt_buf[opt_len..], wscale); +if let Some((tsval, tsecr)) = timestamp { + opt_buf[opt_len] = options::NOP; + opt_len += 1; + opt_buf[opt_len] = options::NOP; + opt_len += 1; + opt_len += write_timestamp_option(&mut opt_buf[opt_len..], tsval, tsecr); +} +if sack_permitted { + opt_len += write_sack_permitted_option(&mut opt_buf[opt_len..]); +} +``` + +**Step 2: Update `build_syn_ack` similarly** + +Add `timestamp: Option<(u32, u32)>` and `sack_permitted: bool` parameters. Build options in same order as SYN. + +**Step 3: Update all `build_syn` and `build_syn_ack` call sites** + +In `src/net/handler/tcp/mod.rs`: +- Active open `connect`/`connect_with_config`: pass `timestamp` based on config, `sack_permitted` based on config +- Passive open (SYN-RECEIVED creation in `process_listen`): pass timestamp/sack based on listener config +- SYN retransmit in `poll_timers`: pass the stored config values +- SYN-ACK retransmit in `poll_timers`: pass stored config values +- Simultaneous open SYN-ACK in `process_syn_sent`: pass stored values + +For the timestamp TSval, compute from `now`: `now.duration_since(tcb.ts_offset).as_millis() as u32` +For SYN, TSecr is always 0. For SYN-ACK, TSecr should echo the peer's TSval (stored in `ts_recent` after parsing the SYN). + +**Step 4: Parse peer options in SYN/SYN-ACK processing** + +In `process_listen` (when receiving SYN): +- Call `parse_timestamp(options)` — if present AND our config allows timestamps, set `tcb.ts_enabled = true`, `tcb.ts_recent = peer_tsval` +- Call `parse_sack_permitted(options)` — if present AND our config allows SACK, set `tcb.sack_enabled = true` + +In `process_syn_sent` (when receiving SYN-ACK): +- Same timestamp/SACK negotiation logic + +**Step 5: Write test for timestamp negotiation** + +```rust +#[test] +fn timestamp_negotiated_in_handshake() { + // Build a SYN with timestamps, verify option is present in the frame. + let mut free = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + let buf = Box::leak(vec![0u8; 2048].into_boxed_slice()); + free.push(Frame::new(0, buf, 2048, false)); + + SegmentBuilder::build_syn( + IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])), + IpAddress::V4(Ipv4Address::new([10, 0, 0, 2])), + 8080, 80, 1000, + 65535, 1460, 7, + Some((12345, 0)), // timestamp + true, // sack_permitted + MacAddress::new([0xAA; 6]), + MacAddress::new([0xBB; 6]), + false, + &mut free, &mut tx, + ); + + assert_eq!(tx.num_frames(), 1); + let frame = tx.pop().unwrap(); + let tcp_offset = 14 + 20; + let tcp = unsafe { TcpHeader::from_bytes_at(&frame, tcp_offset) }; + // Data offset should be > 5 (options present) + assert!(tcp.data_offset() > 5); + + // Parse options from the frame + let opt_start = tcp_offset + TCP_HEADER_LEN; + let opt_end = tcp_offset + tcp.header_len(); + let options = &frame[opt_start..opt_end]; + assert_eq!(parse_timestamp(options).map(|(v, _)| v), Some(12345)); + assert!(parse_sack_permitted(options)); +} +``` + +**Step 6: Run tests** + +Run: `cargo test` +Expected: All tests pass. + +**Step 7: Commit** + +```bash +git add src/net/handler/tcp/segment.rs src/net/handler/tcp/mod.rs +git commit -m "feat: negotiate timestamps and SACK permitted in SYN/SYN-ACK" +``` + +--- + +## Task 7: Timestamps in Data/ACK Segments + RTTM + +**Files:** +- Modify: `src/net/handler/tcp/segment.rs` +- Modify: `src/net/handler/tcp/mod.rs` + +**Context:** When timestamps are negotiated, every segment must include the TS option (12 bytes with NOP padding). ACKs use TSecr to echo the peer's timestamp for RTT measurement. On receiving ACKs with TSecr, compute RTT and feed into the existing SRTT/RTTVAR/RTO estimator. + +**Step 1: Update `build_ack`, `build_data`, `build_fin_ack` to accept optional timestamp** + +Add `timestamp: Option<(u32, u32)>` parameter to each. When `Some`, include 12 bytes of options (NOP + NOP + TS option). For `build_ack` and `build_fin_ack`, pass the options to `build_ipv4_segment`/`build_ipv6_segment`. For `build_data`, change `build_ipv4_data_segment`/`build_ipv6_data_segment` to accept `tcp_options: &[u8]`. + +For `build_data`, the internal helpers need to handle both options AND payload. Update `build_ipv4_data_segment` and `build_ipv6_data_segment` to accept a `tcp_options: &[u8]` parameter (currently hardcoded as `&[]`). Adjust header size calculation: `tcp_header_len = TCP_HEADER_LEN + opt_padded_len`. + +**Step 2: Update all call sites to pass timestamps** + +Wherever `build_ack`, `build_data`, `build_fin_ack` are called, compute the timestamp tuple if `tcb.ts_enabled`: + +```rust +let ts = if tcb.ts_enabled { + let tsval = now.duration_since(tcb.ts_offset).as_millis() as u32; + Some((tsval, tcb.ts_recent)) +} else { + None +}; +``` + +Pass `ts` to the builder call. + +**Step 3: RTTM — update RTT from TSecr on ACK** + +In `process_established`, in the valid new ACK branch, after advancing `snd_una`, if `ts_enabled`: + +```rust +if tcb.ts_enabled { + // Parse timestamp from incoming segment options. + if let Some((_tsval, tsecr)) = parse_timestamp(options) { + if tsecr != 0 { + let our_ts = now.duration_since(tcb.ts_offset).as_millis() as u32; + let rtt_ms = our_ts.wrapping_sub(tsecr) as u64; + // Feed into RFC 6298 estimator (same logic, just more samples). + match tcb.srtt { + None => { + tcb.srtt = Some(rtt_ms); + tcb.rttvar = rtt_ms / 2; + } + Some(srtt) => { + let diff = rtt_ms.abs_diff(srtt); + tcb.rttvar = (3 * tcb.rttvar + diff) / 4; + tcb.srtt = Some((7 * srtt + rtt_ms) / 8); + } + } + tcb.rto = (tcb.srtt.unwrap() + 4 * tcb.rttvar).clamp(1000, 60_000); + } + } +} +``` + +When timestamps are enabled, skip the old `last_send_time`-based RTT measurement (it's superseded by RTTM). + +**Step 4: Update ts_recent on incoming segments** + +In `process_established`, after acceptability check, when receiving data or ACK: + +```rust +if tcb.ts_enabled { + if let Some((tsval, _)) = parse_timestamp(options) { + tcb.ts_recent = tsval; + tcb.ts_recent_age = now; + } +} +``` + +**Step 5: Pass options through to process_established and process_teardown** + +Currently `process_established` doesn't receive the TCP options. Add an `options: &[u8]` parameter to `process_established` and `process_teardown`. Parse it from the frame in `process_segment` (the options are at `tcp_offset + TCP_HEADER_LEN .. tcp_offset + header_len`) and pass to the state handlers. + +**Step 6: Write test for RTTM** + +```rust +#[test] +fn timestamp_rttm_updates_rto() { + // Set up connection with ts_enabled. + // Send data (writes TSval into segment). + // Receive ACK echoing our TSval. + // Verify srtt/rto updated. + // (Details: create TCB with ts_enabled, manually call process_established + // with a crafted ACK containing TSecr matching a known TSval) +} +``` + +**Step 7: Run tests** + +Run: `cargo test` +Expected: All tests pass. + +**Step 8: Commit** + +```bash +git add src/net/handler/tcp/segment.rs src/net/handler/tcp/mod.rs +git commit -m "feat: include timestamps in data/ACK segments, implement RTTM" +``` + +--- + +## Task 8: PAWS (Protection Against Wrapped Sequences) + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs` + +**Context:** RFC 7323 §5 — before segment acceptability check, reject segments with old timestamps. This prevents wrapped sequence numbers from being accepted on high-bandwidth connections. + +**Step 1: Add PAWS check in `process_established`** + +After RST check, before segment acceptability check, add: + +```rust +// PAWS check (RFC 7323 §5). +if self.connections[idx].ts_enabled { + if let Some((tsval, _)) = parse_timestamp(options) { + let tcb = &self.connections[idx]; + // Check if TSval is older than ts_recent. + // Use signed comparison for wraparound. + let ts_diff = tsval.wrapping_sub(tcb.ts_recent) as i32; + if ts_diff < 0 && seg_flags & flags::RST == 0 { + // Check staleness: if ts_recent is older than 24 days, accept anyway. + let staleness = now.duration_since(tcb.ts_recent_age).as_millis(); + if staleness < 24 * 24 * 60 * 60 * 1000 { + // Reject: send ACK and drop. + let tcb = &self.connections[idx]; + SegmentBuilder::build_ack( + tcb.id.local_addr, + tcb.id.remote_addr, + tcb.id.local_port, + tcb.id.remote_port, + tcb.snd_nxt, + tcb.rcv_nxt, + tcb.advertised_window(), + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); + rx_return.push(frame); + return; + } + } + } +} +``` + +**Step 2: Add same PAWS check in `process_teardown`** + +Same logic, placed after RST check and before state-specific processing. + +**Step 3: Write tests** + +```rust +#[test] +fn paws_rejects_old_timestamp() { + // Create connection with ts_enabled and ts_recent = 1000. + // Send segment with TSval = 999 (older). + // Verify segment is dropped and ACK is sent. +} + +#[test] +fn paws_accepts_rst_with_old_timestamp() { + // RST segments bypass PAWS. + // Create connection, send RST with old TSval. + // Verify connection is removed (RST processed). +} + +#[test] +fn paws_accepts_stale_ts_recent() { + // If ts_recent_age > 24 days, accept even with old TSval. + // (Hard to test with real time, but can set ts_recent_age to a very old Instant.) +} +``` + +**Step 4: Run tests** + +Run: `cargo test` +Expected: All tests pass. + +**Step 5: Commit** + +```bash +git add src/net/handler/tcp/mod.rs +git commit -m "feat: implement PAWS timestamp validation" +``` + +--- + +## Task 9: Zero-Window Probing + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs` + +**Context:** When the peer advertises window=0, the sender must periodically probe with 1-byte segments to detect when the window reopens. Without this, connections deadlock if the window update ACK is lost. + +**Step 1: Add persist timer logic to `poll_send`** + +In `poll_send`, after computing `send_window` and `can_send`, when `send_window == 0` and `data_available > 0`: + +```rust +// Zero-window probing (persist timer). +if send_window == 0 && data_available > 0 { + if tcb.persist_deadline.is_none() { + // Start persist timer. + tcb.persist_deadline = Some(now + coarsetime::Duration::from_millis(tcb.rto)); + } else if let Some(deadline) = tcb.persist_deadline { + if now >= deadline { + // Send 1-byte probe. + let mut probe_byte = [0u8; 1]; + tcb.send_buffer.peek_at(bytes_in_flight, &mut probe_byte); + + let dst_mac = neighbor_handler + .lookup(now, &tcb.id.remote_addr) + .unwrap_or(crate::net::wire::ethernet::MacAddress::broadcast()); + + let ts = if tcb.ts_enabled { + let tsval = now.duration_since(tcb.ts_offset).as_millis() as u32; + Some((tsval, tcb.ts_recent)) + } else { + None + }; + + SegmentBuilder::build_data( + tcb.id.local_addr, + tcb.id.remote_addr, + tcb.id.local_port, + tcb.id.remote_port, + tcb.snd_nxt, + tcb.rcv_nxt, + tcb.advertised_window(), + &probe_byte, + src_mac, + dst_mac, + self.tx_offload, + ts, + free_frames, + tx_return, + ); + + tcb.snd_nxt = tcb.snd_nxt.wrapping_add(1); + + // Exponential backoff, capped at ~60s. + let backoff_rto = (tcb.rto << tcb.persist_backoff).min(60_000); + tcb.persist_deadline = Some(now + coarsetime::Duration::from_millis(backoff_rto)); + if tcb.persist_backoff < 6 { + tcb.persist_backoff += 1; + } + } + } + continue; // Skip normal data sending when window is 0. +} +``` + +**Step 2: Reset persist timer when window reopens** + +In `process_established`, in the valid new ACK branch, after updating `snd_wnd`, add: + +```rust +// Reset persist timer if window reopened. +if tcb.snd_wnd > 0 { + tcb.persist_deadline = None; + tcb.persist_backoff = 0; +} +``` + +**Step 3: Write tests** + +```rust +#[test] +fn persist_probe_sent_on_zero_window() { + // Set up established connection with snd_wnd = 0 and data in send buffer. + // Call poll_send — should set persist_deadline but not send yet. + // Advance time past deadline, call poll_send again. + // Verify 1-byte probe segment emitted. +} + +#[test] +fn persist_backoff_increases() { + // After first probe, persist_backoff should be 1. + // After second probe, persist_backoff should be 2. + // Verify deadline intervals increase. +} + +#[test] +fn persist_timer_cleared_on_window_reopen() { + // Set up connection with persist_deadline set. + // Process an ACK with snd_wnd > 0. + // Verify persist_deadline is None and persist_backoff is 0. +} +``` + +**Step 4: Run tests** + +Run: `cargo test` +Expected: All tests pass. + +**Step 5: Commit** + +```bash +git add src/net/handler/tcp/mod.rs +git commit -m "feat: implement zero-window probing with persist timer" +``` + +--- + +## Task 10: SACK — Sending SACK Blocks in Duplicate ACKs + +**Files:** +- Modify: `src/net/handler/tcp/segment.rs` +- Modify: `src/net/handler/tcp/mod.rs` + +**Context:** When OOO data arrives and SACK is enabled, the duplicate ACK should include SACK blocks reporting which byte ranges we have. This allows the sender to selectively retransmit only missing segments. + +**Step 1: Add `build_ack_with_options` to SegmentBuilder** + +Rather than modifying `build_ack` further, add a new builder that accepts pre-built option bytes: + +```rust +/// Build an ACK segment with TCP options (timestamps, SACK blocks, etc.). +pub fn build_ack_with_options<'umem>( + local_addr: IpAddress, + remote_addr: IpAddress, + local_port: u16, + remote_port: u16, + seq: u32, + ack: u32, + window: u16, + tcp_options: &[u8], + src_mac: MacAddress, + dst_mac: MacAddress, + tx_offload: bool, + free_frames: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, +) { + // Delegate to build_ipv4_segment / build_ipv6_segment with options + match (local_addr, remote_addr) { + (IpAddress::V4(local_ip), IpAddress::V4(remote_ip)) => { + Self::build_ipv4_segment( + local_ip, remote_ip, local_port, remote_port, + seq, ack, flags::ACK, window, + tcp_options, src_mac, dst_mac, tx_offload, + free_frames, tx_return, + ); + } + (IpAddress::V6(local_ip), IpAddress::V6(remote_ip)) => { + Self::build_ipv6_segment( + local_ip, remote_ip, local_port, remote_port, + seq, ack, flags::ACK, window, + tcp_options, src_mac, dst_mac, tx_offload, + free_frames, tx_return, + ); + } + _ => {} + } +} +``` + +**Step 2: Build SACK blocks from ooo_ranges in OOO data path** + +In `process_established`, in the OOO data branch (where duplicate ACK is sent), replace the plain `build_ack` with SACK-aware logic: + +```rust +} else if seq_lt(rcv_nxt, seg_seq) { + // Out-of-order data. + let offset = seg_seq.wrapping_sub(rcv_nxt) as usize; + let payload = &frame[payload_offset..payload_offset + payload_len]; + let tcb = &mut self.connections[idx]; + tcb.recv_buffer.write_at(offset, payload); + tcb.ooo_ranges.insert(seg_seq, payload_len as u32); + + // Build SACK blocks from ooo_ranges (most recent first). + let tcb = &self.connections[idx]; + let mut opt_buf = [0u8; 40]; + let mut opt_len = 0; + + // Add timestamp if enabled. + if tcb.ts_enabled { + opt_buf[opt_len] = options::NOP; opt_len += 1; + opt_buf[opt_len] = options::NOP; opt_len += 1; + let tsval = now.duration_since(tcb.ts_offset).as_millis() as u32; + opt_len += write_timestamp_option(&mut opt_buf[opt_len..], tsval, tcb.ts_recent); + } + + if tcb.sack_enabled { + // Collect SACK blocks: most recent first, up to space limit. + let max_blocks = if tcb.ts_enabled { 3 } else { 4 }; + let mut sack_blocks = Vec::new(); + // The range just received goes first. + sack_blocks.push((seg_seq, seg_seq.wrapping_add(payload_len as u32))); + // Then other ranges. + for (&seq, &len) in tcb.ooo_ranges.iter().rev() { + if seq == seg_seq { continue; } // already added + if sack_blocks.len() >= max_blocks { break; } + sack_blocks.push((seq, seq.wrapping_add(len))); + } + opt_len += write_sack_option(&mut opt_buf[opt_len..], &sack_blocks); + } + + SegmentBuilder::build_ack_with_options( + tcb.id.local_addr, + tcb.id.remote_addr, + tcb.id.local_port, + tcb.id.remote_port, + tcb.snd_nxt, + tcb.rcv_nxt, + tcb.advertised_window(), + &opt_buf[..opt_len], + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); +} +``` + +**Step 3: Write test** + +```rust +#[test] +fn sack_blocks_sent_on_ooo_data() { + // Set up established connection with sack_enabled. + // Send in-order data at seq=201. + // Send OOO data at seq=301 (gap at 211-300). + // Verify the duplicate ACK contains a SACK block for [301, 301+len). +} +``` + +**Step 4: Run tests** + +Run: `cargo test` +Expected: All tests pass. + +**Step 5: Commit** + +```bash +git add src/net/handler/tcp/segment.rs src/net/handler/tcp/mod.rs +git commit -m "feat: send SACK blocks in duplicate ACKs for OOO data" +``` + +--- + +## Task 11: SACK — Scoreboard and Selective Retransmission + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs` + +**Context:** When we receive ACKs with SACK blocks from the peer, update the scoreboard. Use the scoreboard during fast retransmit to selectively retransmit only missing segments instead of retransmitting from `snd_una`. + +**Step 1: Parse SACK blocks on incoming ACKs** + +In `process_established`, in the valid new ACK branch, after updating `snd_una`: + +```rust +// Update SACK scoreboard. +if tcb.sack_enabled { + // Remove entries below new snd_una. + let una = tcb.snd_una; + tcb.sack_scoreboard.retain(|&left, _| !seq_lt(left, una)); + + // Parse new SACK blocks from incoming segment. + let (blocks, count) = parse_sack_blocks(options); + for i in 0..count { + if let Some((left, right)) = blocks[i] { + tcb.sack_scoreboard.insert(left, right); + } + } +} +``` + +Also handle SACK blocks in the duplicate ACK branch: + +```rust +} else if seg_ack == snd_una && payload_len == 0 { + // Duplicate ACK. + let tcb = &mut self.connections[idx]; + tcb.dup_ack_count += 1; + // Keep-alive probe response handling... + + // Update SACK scoreboard on duplicate ACK. + if tcb.sack_enabled { + let (blocks, count) = parse_sack_blocks(options); + for i in 0..count { + if let Some((left, right)) = blocks[i] { + tcb.sack_scoreboard.insert(left, right); + } + } + } +} +``` + +**Step 2: Use scoreboard in fast retransmit** + +In `poll_timers`, in the fast retransmit pass, when `sack_enabled`: + +```rust +if tcb.sack_enabled && !tcb.sack_scoreboard.is_empty() { + // Find first gap: scan from snd_una looking for the first byte + // NOT covered by a SACK block. + let mut retransmit_start = tcb.snd_una; + for (&left, &right) in &tcb.sack_scoreboard { + if seq_le(left, retransmit_start) && seq_lt(retransmit_start, right) { + // retransmit_start is inside a SACKed range — skip past it. + retransmit_start = right; + } + } + + if seq_lt(retransmit_start, tcb.snd_nxt) { + // Retransmit from the gap. + let offset = retransmit_start.wrapping_sub(tcb.snd_una) as usize; + let gap_len = // find end of gap (next SACK left edge or snd_nxt) + tcb.sack_scoreboard.iter() + .find(|(&left, _)| seq_lt(retransmit_start, left)) + .map(|(&left, _)| left.wrapping_sub(retransmit_start) as usize) + .unwrap_or((tcb.snd_nxt.wrapping_sub(retransmit_start)) as usize); + let retransmit_len = gap_len.min(tcb.eff_snd_mss as usize); + + let mut payload = vec![0u8; retransmit_len]; + tcb.send_buffer.peek_at(offset, &mut payload); + // ... build_data with payload, same as current fast retransmit + } +} else { + // Original fast retransmit from snd_una. + // ... existing code +} +``` + +**Step 3: Clear scoreboard on RTO** + +In `poll_timers` RTO retransmit path for Established state, add: + +```rust +tcb.sack_scoreboard.clear(); +``` + +**Step 4: Write tests** + +```rust +#[test] +fn sack_scoreboard_updated_on_ack() { + // Set up connection with sack_enabled. + // Send 3 segments. + // Receive ACK with SACK blocks for segments 2 and 3. + // Verify scoreboard has the right entries. +} + +#[test] +fn sack_selective_retransmit() { + // Set up connection with data in flight at snd_una..snd_nxt. + // Populate scoreboard with SACKed ranges (middle segment). + // Trigger fast retransmit (dup_ack_count >= 3). + // Verify retransmit is for the gap, not from snd_una. +} + +#[test] +fn sack_scoreboard_cleared_on_rto() { + // Populate scoreboard, trigger RTO. + // Verify scoreboard is empty after RTO retransmit. +} +``` + +**Step 5: Run tests** + +Run: `cargo test` +Expected: All tests pass. + +**Step 6: Commit** + +```bash +git add src/net/handler/tcp/mod.rs +git commit -m "feat: SACK scoreboard tracking and selective retransmission" +``` + +--- + +## Task 12: Zero-Copy Send Path — Ring Buffer peek_slices + +**Files:** +- Modify: `src/net/handler/tcp/ring_buffer.rs` + +**Context:** Add `peek_slices` method that returns two slices instead of copying into a buffer. This eliminates heap allocation in the data send path. + +**Step 1: Write failing tests** + +```rust +#[test] +fn peek_slices_no_wrap() { + let mut rb = RingBuffer::new(64); + rb.write(b"hello world"); + let (a, b) = rb.peek_slices(0, 5); + assert_eq!(a, b"hello"); + assert!(b.is_empty()); + assert_eq!(rb.available(), 11); // unchanged +} + +#[test] +fn peek_slices_with_wrap() { + let mut rb = RingBuffer::new(16); + rb.write(&[0xAA; 12]); + let mut discard = [0u8; 12]; + rb.read(&mut discard); + // head=12, tail=12. Write 8 bytes wrapping. + rb.write(&[0xBB; 8]); + // Data spans 12..15 (4 bytes) and 0..3 (4 bytes). + let (a, b) = rb.peek_slices(0, 8); + assert_eq!(a, &[0xBB; 4]); + assert_eq!(b, &[0xBB; 4]); +} + +#[test] +fn peek_slices_with_offset() { + let mut rb = RingBuffer::new(64); + rb.write(b"hello world"); + let (a, b) = rb.peek_slices(6, 5); + assert_eq!(a, b"world"); + assert!(b.is_empty()); +} +``` + +**Step 2: Run tests to verify they fail** + +Run: `cargo test peek_slices` +Expected: FAIL — method doesn't exist. + +**Step 3: Implement `peek_slices`** + +```rust +/// Return two slices covering `len` bytes starting at `offset` from head, +/// without advancing head. Second slice is empty if data doesn't wrap. +#[inline] +pub fn peek_slices(&self, offset: usize, len: usize) -> (&[u8], &[u8]) { + let pos = (self.head.wrapping_add(offset)) & self.mask; + let first_len = len.min(self.buf.len() - pos); + if first_len >= len { + (&self.buf[pos..pos + len], &[]) + } else { + let second_len = len - first_len; + (&self.buf[pos..pos + first_len], &self.buf[..second_len]) + } +} +``` + +**Step 4: Run tests** + +Run: `cargo test` +Expected: All tests pass. + +**Step 5: Commit** + +```bash +git add src/net/handler/tcp/ring_buffer.rs +git commit -m "feat: add peek_slices for zero-copy send path" +``` + +--- + +## Task 13: Zero-Copy Send Path — Two-Slice build_data + +**Files:** +- Modify: `src/net/handler/tcp/segment.rs` +- Modify: `src/net/handler/tcp/mod.rs` + +**Context:** Change `build_data` to accept `(&[u8], &[u8])` instead of `&[u8]`, write both slices into the frame, and update all call sites to use `peek_slices`. + +**Step 1: Update `build_data` signature** + +Change `payload: &[u8]` to `payload: (&[u8], &[u8])`. Update the internal helpers `build_ipv4_data_segment` and `build_ipv6_data_segment` similarly: + +```rust +pub fn build_data<'umem>( + // ... same params ... + payload: (&[u8], &[u8]), // CHANGED + // ... same params ... +) +``` + +In the internal helpers, compute total payload length as `payload.0.len() + payload.1.len()`. Copy both slices contiguously: + +```rust +let payload_offset = tcp_offset + tcp_header_len; +frame[payload_offset..payload_offset + payload.0.len()].copy_from_slice(payload.0); +let second_start = payload_offset + payload.0.len(); +frame[second_start..second_start + payload.1.len()].copy_from_slice(payload.1); +``` + +For checksum, the existing approach computes over `&frame[tcp_offset..frame_len]` after writing — this already covers both slices since they're in the frame. + +**Step 2: Update all call sites in `mod.rs`** + +Replace all `build_data` calls: + +1. **poll_send data path:** Replace the `vec![0u8; to_send]` + `peek_at` with: +```rust +let (slice_a, slice_b) = tcb.send_buffer.peek_slices(bytes_in_flight, to_send); +// ... pass (slice_a, slice_b) to build_data +``` + +2. **poll_timers fast retransmit:** Same replacement. + +3. **poll_timers RTO retransmit:** Same replacement. + +4. **Zero-window probe** (Task 9): The 1-byte probe can use `(&probe_byte, &[])`. + +**Step 3: Update existing `build_data` test** + +The existing `build_data_ipv4` test passes `payload` as `&[u8]` — change to pass `(payload, &[])`. + +**Step 4: Write test for two-slice payload** + +```rust +#[test] +fn build_data_two_slices() { + let mut free = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + let buf = Box::leak(vec![0u8; 2048].into_boxed_slice()); + free.push(Frame::new(100, buf, 2048, false)); + + let part1 = b"Hello"; + let part2 = b", TCP!"; + + SegmentBuilder::build_data( + IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])), + IpAddress::V4(Ipv4Address::new([10, 0, 0, 2])), + 8080, 80, 1000, 500, 65535, + (part1, part2), + MacAddress::new([0xAA; 6]), + MacAddress::new([0xBB; 6]), + false, + None, // no timestamp + &mut free, &mut tx, + ); + + assert_eq!(tx.num_frames(), 1); + let frame = tx.pop().unwrap(); + let payload_start = 14 + 20 + 20; // ETH + IPv4 + TCP + assert_eq!(&frame[payload_start..frame.len()], b"Hello, TCP!"); +} +``` + +**Step 5: Run tests** + +Run: `cargo test` +Expected: All tests pass. + +**Step 6: Commit** + +```bash +git add src/net/handler/tcp/segment.rs src/net/handler/tcp/mod.rs +git commit -m "feat: zero-copy send path with two-slice build_data and peek_slices" +``` + +--- + +## Task 14: Socket Layer Updates + +**Files:** +- Modify: `src/net/socket/tcp.rs` + +**Context:** Add `set_timestamps()` and `set_sack()` methods to `TcpStream` for runtime configuration, matching the existing `set_nodelay()` / `set_keepalive()` pattern. + +**Step 1: Add socket API methods** + +After the `linger()` method in `TcpStream`: + +```rust +/// Enable or disable TCP timestamps (RFC 7323). +pub fn set_timestamps(&self, enabled: bool) { + let handler = unsafe { &mut *self.handler.get() }; + if let Some(tcb) = handler.get_connection_mut(&self.conn_id) { + tcb.ts_enabled = enabled; + } +} + +/// Returns whether TCP timestamps are enabled. +pub fn timestamps(&self) -> bool { + let handler = unsafe { &*self.handler.get() }; + handler + .get_connection(&self.conn_id) + .map(|tcb| tcb.ts_enabled) + .unwrap_or(false) +} + +/// Enable or disable SACK. +pub fn set_sack(&self, enabled: bool) { + let handler = unsafe { &mut *self.handler.get() }; + if let Some(tcb) = handler.get_connection_mut(&self.conn_id) { + tcb.sack_enabled = enabled; + } +} + +/// Returns whether SACK is enabled. +pub fn sack(&self) -> bool { + let handler = unsafe { &*self.handler.get() }; + handler + .get_connection(&self.conn_id) + .map(|tcb| tcb.sack_enabled) + .unwrap_or(false) +} +``` + +**Step 2: Run tests** + +Run: `cargo test` +Expected: All tests pass. + +**Step 3: Commit** + +```bash +git add src/net/socket/tcp.rs +git commit -m "feat: add timestamps and SACK socket API methods" +``` + +--- + +## Task 15: Integration — Verify All Features Work Together + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs` (test section) + +**Step 1: Write integration test combining all features** + +```rust +#[test] +fn full_lifecycle_with_timestamps_sack_wscale() { + // End-to-end test: + // 1. Listen with timestamps + SACK + wscale enabled. + // 2. Active open with same config. + // 3. Complete handshake (verify options negotiated). + // 4. Send data, receive ACK with correct scaled window. + // 5. Simulate OOO data, verify SACK blocks sent. + // 6. Simulate zero-window, verify persist probe sent. + // 7. Graceful close. +} +``` + +**Step 2: Run all tests** + +Run: `cargo test` +Expected: All tests pass. + +**Step 3: Commit** + +```bash +git add src/net/handler/tcp/mod.rs +git commit -m "test: integration test for timestamps, SACK, wscale, and persist timer" +``` diff --git a/docs/plans/2026-03-07-tcp-remaining-phase6-impl.md b/docs/plans/2026-03-07-tcp-remaining-phase6-impl.md new file mode 100644 index 0000000..e7d9708 --- /dev/null +++ b/docs/plans/2026-03-07-tcp-remaining-phase6-impl.md @@ -0,0 +1,965 @@ +# TCP Phase 6 Remaining Features — Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Implement zero-window probing, full SACK (send/receive/selective retransmit), and zero-copy send path — the 5 remaining Phase 6 features. + +**Architecture:** Zero-window probing in `poll_send` prevents deadlocks when peer advertises window=0. SACK block sending lets the receiver report OOO ranges. SACK block receiving lets the sender track what the peer has. Selective retransmit uses the scoreboard to retransmit only gaps. Zero-copy send eliminates heap allocation by passing ring buffer slices directly to the segment builder. + +**Tech Stack:** Rust, coarsetime for timers, existing TCP handler infrastructure in `src/net/handler/tcp/`. + +**Testing:** `cargo test` (no feature flags, no `--all-features`). Tests run under `sudo -E` via `.cargo/config.toml`. + +**Key files reference:** +- Wire format: `src/net/wire/tcp.rs` +- TCB/config: `src/net/handler/tcp/tcb.rs` +- Handler: `src/net/handler/tcp/mod.rs` +- Segment builder: `src/net/handler/tcp/segment.rs` +- Ring buffer: `src/net/handler/tcp/ring_buffer.rs` + +**What's already done (do NOT re-implement):** +- Window scaling fix (advertised_window, scale_incoming_window) +- Segment acceptability checks (is_segment_acceptable) +- Timestamp negotiation, RTTM, PAWS +- SACK Permitted negotiation in SYN/SYN-ACK +- Wire functions: parse_sack_blocks, write_sack_option, parse_timestamp, write_timestamp_option +- TCB fields: persist_deadline, persist_backoff, sack_enabled, sack_scoreboard, ooo_ranges — all exist but are unused + +--- + +## Task 1: Zero-Window Probing — Persist Timer in poll_send + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs:1896-2065` (poll_send) +- Modify: `src/net/handler/tcp/mod.rs:1323-1406` (process_established ACK processing) + +**Context:** When `snd_wnd == 0` and there's data to send, the sender must probe periodically to detect window reopening. TCB already has `persist_deadline: Option` and `persist_backoff: u8` — both initialized to None/0 and never used. + +**Step 1: Write failing test for persist timer activation** + +In `src/net/handler/tcp/mod.rs`, add to the `#[cfg(test)]` section: + +```rust +#[test] +fn persist_timer_activates_on_zero_window() { + // Setup: Established connection with peer window = 0 and data in send buffer. + let (mut handler, idx) = make_established_handler(); + let tcb = &mut handler.connections[idx]; + tcb.snd_wnd = 0; + tcb.send_buffer.write(b"hello"); + let now = Instant::recent(); + + // poll_send should NOT send data (window is 0) but should set persist_deadline. + let mut free = make_frame_buffer(4); + let mut tx = make_frame_buffer(0); + handler.poll_send(now, SRC_MAC, &make_neighbor_handler(), &mut free, &mut tx); + + let tcb = &handler.connections[idx]; + assert!(tcb.persist_deadline.is_some(), "persist timer should be set"); + assert_eq!(tx.len(), 0, "no data should be sent with zero window"); +} +``` + +**Step 2: Run test to verify it fails** + +Run: `cargo test persist_timer_activates_on_zero_window` +Expected: FAIL — persist_deadline is never set. + +**Step 3: Implement persist timer detection in poll_send** + +In `src/net/handler/tcp/mod.rs` inside `poll_send`, after the `if can_send > 0 && data_available > 0` block (around line 1973), add a new block: + +```rust +// Zero-window probing: when send window is 0 and we have data, start persist timer. +if send_window == 0 && data_available > 0 && tcb.persist_deadline.is_none() { + tcb.persist_deadline = Some(now + coarsetime::Duration::from_millis(tcb.rto)); +} +``` + +**Step 4: Run test to verify it passes** + +Run: `cargo test persist_timer_activates_on_zero_window` +Expected: PASS + +**Step 5: Write failing test for probe sending** + +```rust +#[test] +fn persist_timer_sends_probe() { + let (mut handler, idx) = make_established_handler(); + let tcb = &mut handler.connections[idx]; + tcb.snd_wnd = 0; + tcb.send_buffer.write(b"hello"); + tcb.rto = 1000; + // Set persist deadline in the past so it fires. + tcb.persist_deadline = Some(Instant::recent() - coarsetime::Duration::from_millis(1)); + let now = Instant::recent(); + + let mut free = make_frame_buffer(4); + let mut tx = make_frame_buffer(0); + handler.poll_send(now, SRC_MAC, &make_neighbor_handler(), &mut free, &mut tx); + + assert_eq!(tx.len(), 1, "one probe segment should be sent"); + let tcb = &handler.connections[idx]; + assert_eq!(tcb.persist_backoff, 1, "backoff should increment"); + assert!(tcb.persist_deadline.is_some(), "next probe should be scheduled"); +} +``` + +**Step 6: Run test to verify it fails** + +Run: `cargo test persist_timer_sends_probe` +Expected: FAIL + +**Step 7: Implement probe sending in poll_send** + +In `poll_send`, after the persist timer detection block, add: + +```rust +// Send zero-window probe if deadline expired. +if let Some(deadline) = tcb.persist_deadline { + if now >= deadline && send_window == 0 && data_available > 0 { + // Probe: send 1 byte from the send buffer at bytes_in_flight offset. + let probe_len = 1; + let mut probe_buf = [0u8; 1]; + tcb.send_buffer.peek_at(bytes_in_flight, &mut probe_buf); + + let dst_mac = neighbor_handler + .lookup(now, &tcb.id.remote_addr) + .unwrap_or(crate::net::wire::ethernet::MacAddress::broadcast()); + + let ts = if tcb.ts_enabled { + let tsval = now.duration_since(tcb.ts_offset).as_millis() as u32; + Some((tsval, tcb.ts_recent)) + } else { + None + }; + SegmentBuilder::build_data( + tcb.id.local_addr, + tcb.id.remote_addr, + tcb.id.local_port, + tcb.id.remote_port, + tcb.snd_nxt, + tcb.rcv_nxt, + tcb.advertised_window(), + &probe_buf[..probe_len], + ts, + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); + + tcb.snd_nxt = tcb.snd_nxt.wrapping_add(probe_len as u32); + + // Schedule next probe with exponential backoff (cap at 60s). + let backoff_rto = (tcb.rto << tcb.persist_backoff).min(60_000); + tcb.persist_deadline = Some(now + coarsetime::Duration::from_millis(backoff_rto)); + tcb.persist_backoff = tcb.persist_backoff.saturating_add(1).min(6); + } +} +``` + +**Step 8: Run test to verify it passes** + +Run: `cargo test persist_timer_sends_probe` +Expected: PASS + +**Step 9: Write failing test for persist timer recovery** + +```rust +#[test] +fn persist_timer_clears_on_window_reopen() { + let (mut handler, idx) = make_established_handler(); + let tcb = &mut handler.connections[idx]; + tcb.persist_deadline = Some(Instant::recent()); + tcb.persist_backoff = 3; + // Simulate receiving an ACK that opens the window. + // This happens in process_established when snd_wnd becomes > 0. + tcb.snd_wnd = 65535; + tcb.persist_deadline = None; + tcb.persist_backoff = 0; + + assert!(tcb.persist_deadline.is_none()); + assert_eq!(tcb.persist_backoff, 0); +} +``` + +**Step 10: Implement persist timer recovery in process_established** + +In `process_established`, in the ACK processing section (around line 1394 where `tcb.snd_wnd` is updated), add after the window update: + +```rust +// Clear persist timer when window reopens. +if tcb.snd_wnd > 0 && tcb.persist_deadline.is_some() { + tcb.persist_deadline = None; + tcb.persist_backoff = 0; +} +``` + +**Step 11: Run all persist tests** + +Run: `cargo test persist` +Expected: All PASS + +**Step 12: Commit** + +```bash +git add src/net/handler/tcp/mod.rs +git commit -m "feat(tcp): implement zero-window probing with persist timer" +``` + +--- + +## Task 2: SACK Block Sending (Receiver Side) + +**Files:** +- Modify: `src/net/handler/tcp/segment.rs:259-323` (build_ack — add SACK block support) +- Modify: `src/net/handler/tcp/mod.rs:1473-1502` (OOO data path — generate SACK blocks) + +**Context:** When OOO data arrives, the receiver should include SACK blocks in the duplicate ACK reporting which ranges it has received. Wire functions `write_sack_option` and `parse_sack_blocks` already exist. `build_ack` currently accepts only `timestamp: Option<(u32, u32)>`. + +**Step 1: Write failing test for build_ack with SACK blocks** + +In `src/net/handler/tcp/segment.rs`, add to the `#[cfg(test)]` section: + +```rust +#[test] +fn build_ack_with_sack_blocks() { + let mut free = make_test_frames(2); + let mut tx = Vec::new(); + + let sack_blocks = vec![(1000u32, 1500u32), (2000u32, 2500u32)]; + + SegmentBuilder::build_ack_with_sack( + IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])), + IpAddress::V4(Ipv4Address::new([10, 0, 0, 2])), + 1234, + 80, + 100, // seq + 200, // ack + 65535, // window + None, // no timestamp + &sack_blocks, + MacAddress::broadcast(), + MacAddress::broadcast(), + false, + &mut free, + &mut tx, + ); + + assert_eq!(tx.len(), 1, "should produce one frame"); +} +``` + +**Step 2: Run test to verify it fails** + +Run: `cargo test build_ack_with_sack_blocks` +Expected: FAIL — `build_ack_with_sack` doesn't exist. + +**Step 3: Implement build_ack_with_sack in segment.rs** + +Add a new method to `SegmentBuilder` after `build_ack` (after line 323): + +```rust +/// Build an ACK segment with optional SACK blocks. +/// Used when sending duplicate ACKs for out-of-order data. +#[inline] +pub fn build_ack_with_sack<'umem>( + local_addr: IpAddress, + remote_addr: IpAddress, + local_port: u16, + remote_port: u16, + seq: u32, + ack: u32, + window: u16, + timestamp: Option<(u32, u32)>, + sack_blocks: &[(u32, u32)], + src_mac: MacAddress, + dst_mac: MacAddress, + tx_offload: bool, + free_frames: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, +) { + // Build options buffer: timestamp (12 bytes) + SACK (2 + 8*N bytes). + // Max TCP option space = 40 bytes. + let mut opt_buf = [0u8; 40]; + let mut opt_len = 0; + + if let Some((tsval, tsecr)) = timestamp { + opt_buf[0] = options::NOP; + opt_buf[1] = options::NOP; + write_timestamp_option(&mut opt_buf[2..], tsval, tsecr); + opt_len = 12; + } + + if !sack_blocks.is_empty() { + let sack_written = + crate::net::wire::tcp::write_sack_option(&mut opt_buf[opt_len..], sack_blocks); + opt_len += sack_written; + } + + let tcp_options = &opt_buf[..opt_len]; + + match (local_addr, remote_addr) { + (IpAddress::V4(local_ip), IpAddress::V4(remote_ip)) => { + Self::build_ipv4_segment( + local_ip, remote_ip, local_port, remote_port, + seq, ack, flags::ACK, window, tcp_options, + src_mac, dst_mac, tx_offload, free_frames, tx_return, + ); + } + (IpAddress::V6(local_ip), IpAddress::V6(remote_ip)) => { + Self::build_ipv6_segment( + local_ip, remote_ip, local_port, remote_port, + seq, ack, flags::ACK, window, tcp_options, + src_mac, dst_mac, tx_offload, free_frames, tx_return, + ); + } + _ => {} + } +} +``` + +Add `write_sack_option` to the imports at the top of `segment.rs` (line 9): + +```rust +use crate::net::wire::tcp::{ + TCP_HEADER_LEN, TcpHeader, flags, options, write_mss_option, + write_sack_option, write_sack_permitted_option, write_timestamp_option, + write_window_scale_option, +}; +``` + +**Step 4: Run test to verify it passes** + +Run: `cargo test build_ack_with_sack_blocks` +Expected: PASS + +**Step 5: Write failing test for SACK blocks in OOO duplicate ACK** + +In `src/net/handler/tcp/mod.rs` tests: + +```rust +#[test] +fn ooo_data_sends_sack_blocks() { + // Setup: established connection, send OOO segment (gap at rcv_nxt). + let (mut handler, idx) = make_established_handler(); + let tcb = &mut handler.connections[idx]; + tcb.sack_enabled = true; + let rcv_nxt = tcb.rcv_nxt; + + // Send segment starting 100 bytes after rcv_nxt (creates gap). + // The duplicate ACK should include SACK block (rcv_nxt+100, rcv_nxt+200). + // We verify by checking that a frame was emitted (the dup ACK with SACK). + let ooo_seq = rcv_nxt.wrapping_add(100); + let payload = vec![0xAA; 100]; + + // Build an inbound frame with OOO data and process it. + // ... (use existing test helper to build inbound TCP segment) + // After processing, verify ooo_ranges has the entry and a dup ACK was sent. + let tcb = &handler.connections[idx]; + assert!(tcb.ooo_ranges.contains_key(&ooo_seq)); +} +``` + +Note: The exact test construction depends on existing test helpers (`make_established_handler`, inbound frame builders). The implementer should follow the patterns already used in existing tests like `poll_send_builds_data_segment` (line 3301). + +**Step 6: Implement SACK block generation in process_established OOO path** + +In `src/net/handler/tcp/mod.rs`, in the OOO data branch (around line 1473-1502), replace the `SegmentBuilder::build_ack` call with `build_ack_with_sack`: + +```rust +} else if seq_lt(rcv_nxt, seg_seq) { + // Out-of-order data. + let offset = seg_seq.wrapping_sub(rcv_nxt) as usize; + let payload = &frame[payload_offset..payload_offset + payload_len]; + let tcb = &mut self.connections[idx]; + tcb.recv_buffer.write_at(offset, payload); + tcb.ooo_ranges.insert(seg_seq, payload_len as u32); + + // Generate SACK blocks from ooo_ranges (most recent first, per RFC 2018 §3). + // Max blocks: 3 with timestamps, 4 without. + let max_blocks = if tcb.ts_enabled { 3 } else { 4 }; + let mut sack_blocks: Vec<(u32, u32)> = Vec::new(); + + if tcb.sack_enabled { + // Most recently inserted range first. + sack_blocks.push((seg_seq, seg_seq.wrapping_add(payload_len as u32))); + for (&start, &len) in tcb.ooo_ranges.iter().rev() { + if sack_blocks.len() >= max_blocks { + break; + } + let end = start.wrapping_add(len); + if start != seg_seq { + sack_blocks.push((start, end)); + } + } + } + + // Send duplicate ACK (with current rcv_nxt) and SACK blocks. + let ts = if tcb.ts_enabled { + let tsval = now.duration_since(tcb.ts_offset).as_millis() as u32; + Some((tsval, tcb.ts_recent)) + } else { + None + }; + SegmentBuilder::build_ack_with_sack( + tcb.id.local_addr, + tcb.id.remote_addr, + tcb.id.local_port, + tcb.id.remote_port, + tcb.snd_nxt, + tcb.rcv_nxt, + tcb.advertised_window(), + ts, + &sack_blocks, + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); +} +``` + +Add `SegmentBuilder::build_ack_with_sack` to the imports if needed — it's on `SegmentBuilder` which is already imported. + +**Step 7: Run tests** + +Run: `cargo test ooo_data_sends_sack` +Expected: PASS + +**Step 8: Commit** + +```bash +git add src/net/handler/tcp/segment.rs src/net/handler/tcp/mod.rs +git commit -m "feat(tcp): send SACK blocks in duplicate ACKs for OOO data" +``` + +--- + +## Task 3: SACK Block Receiving (Sender Scoreboard) + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs:1323-1406` (process_established ACK processing) + +**Context:** When receiving ACKs with SACK blocks, the sender should parse them and update `sack_scoreboard` (BTreeMap). On cumulative ACK advance, prune entries below snd_una. On RTO, clear the scoreboard entirely. + +**Step 1: Write failing test for scoreboard update** + +```rust +#[test] +fn sack_blocks_update_scoreboard() { + let (mut handler, idx) = make_established_handler(); + let tcb = &mut handler.connections[idx]; + tcb.sack_enabled = true; + + // Simulate receiving an ACK with SACK blocks. + // After processing, sack_scoreboard should contain the reported ranges. + // (Build inbound ACK frame with SACK options and process it.) + // Verify: handler.connections[idx].sack_scoreboard contains expected entries. + assert!(tcb.sack_scoreboard.is_empty()); // initially empty +} +``` + +**Step 2: Run test to verify it fails** + +Run: `cargo test sack_blocks_update_scoreboard` +Expected: FAIL (or trivially passes since scoreboard is empty — adjust test to send actual SACK-bearing ACK) + +**Step 3: Implement SACK block parsing in process_established ACK path** + +In `process_established`, inside the valid new ACK branch (around line 1329-1396), after the window update (line 1396), add: + +```rust +// Parse and merge SACK blocks into scoreboard. +if tcb.sack_enabled { + let (blocks, count) = crate::net::wire::tcp::parse_sack_blocks(options); + for i in 0..count { + if let Some((left, right)) = blocks[i] { + tcb.sack_scoreboard.insert(left, right.wrapping_sub(left)); + } + } + // Prune scoreboard entries below snd_una. + let snd_una = tcb.snd_una; + tcb.sack_scoreboard.retain(|&start, _| seq_le(snd_una, start)); +} +``` + +Also add SACK parsing in the **duplicate ACK** branch (around line 1397-1406): + +```rust +} else if seg_ack == snd_una && payload_len == 0 { + // Duplicate ACK. + let tcb = &mut self.connections[idx]; + tcb.dup_ack_count += 1; + + // Parse SACK blocks on duplicate ACKs too. + if tcb.sack_enabled { + let (blocks, count) = crate::net::wire::tcp::parse_sack_blocks(options); + for i in 0..count { + if let Some((left, right)) = blocks[i] { + tcb.sack_scoreboard.insert(left, right.wrapping_sub(left)); + } + } + } + + // Keep-alive probe responses arrive as duplicate ACKs — reset timer. + if tcb.keep_alive_enabled && tcb.keep_alive_probes_sent > 0 { + tcb.last_activity = now; + tcb.keep_alive_probes_sent = 0; + } +} +``` + +**Step 4: Clear scoreboard on RTO retransmit** + +In `poll_timers`, in the RTO retransmit path for `TcpState::Established` (around line 1822-1856), add after the cwnd reset: + +```rust +// Clear SACK scoreboard on RTO — fall back to full retransmit. +tcb.sack_scoreboard.clear(); +``` + +**Step 5: Run tests** + +Run: `cargo test sack` +Expected: All PASS + +**Step 6: Commit** + +```bash +git add src/net/handler/tcp/mod.rs +git commit -m "feat(tcp): parse received SACK blocks and maintain sender scoreboard" +``` + +--- + +## Task 4: Selective Retransmit Using SACK Scoreboard + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs:1681-1728` (fast retransmit in poll_timers) + +**Context:** Currently fast retransmit always retransmits from `snd_una`. With SACK scoreboard, we can find the first gap (un-SACKed range) and retransmit only that, avoiding redundant retransmission of data the peer already has. + +**Step 1: Write failing test for selective retransmit** + +```rust +#[test] +fn fast_retransmit_uses_sack_scoreboard() { + let (mut handler, idx) = make_established_handler(); + let tcb = &mut handler.connections[idx]; + tcb.sack_enabled = true; + tcb.dup_ack_count = 3; // trigger fast retransmit + + // Write 3 MSS worth of data to send buffer. + let mss = tcb.eff_snd_mss as usize; + let data = vec![0xAA; mss * 3]; + tcb.send_buffer.write(&data); + tcb.snd_nxt = tcb.snd_una.wrapping_add((mss * 3) as u32); + + // Peer has SACKed the 2nd MSS-worth of data. + // Gap is: [snd_una, snd_una + mss) — this is what should be retransmitted. + let sack_start = tcb.snd_una.wrapping_add(mss as u32); + tcb.sack_scoreboard.insert(sack_start, mss as u32); + + let now = Instant::recent(); + let mut free = make_frame_buffer(4); + let mut tx = make_frame_buffer(0); + handler.poll_timers(now, SRC_MAC, &make_neighbor_handler(), &mut free, &mut tx); + + // Should retransmit 1 MSS from snd_una (the gap). + assert_eq!(tx.len(), 1, "should retransmit one segment"); + let tcb = &handler.connections[idx]; + assert_eq!(tcb.dup_ack_count, 0, "dup_ack_count should be reset"); +} +``` + +**Step 2: Run test to verify it fails** + +Run: `cargo test fast_retransmit_uses_sack_scoreboard` +Expected: FAIL (retransmits from offset 0, not using scoreboard) + +**Step 3: Implement selective retransmit in fast retransmit path** + +Replace the fast retransmit logic in `poll_timers` (lines 1681-1728) with: + +```rust +// Fast retransmit pass — triggered by 3 duplicate ACKs. +for tcb in &mut self.connections { + if tcb.state != TcpState::Established || tcb.dup_ack_count < 3 { + continue; + } + + // Determine what to retransmit. + let (retransmit_offset, retransmit_len) = if tcb.sack_enabled && !tcb.sack_scoreboard.is_empty() + { + // Find first gap in scoreboard between snd_una and snd_nxt. + let mut gap_start = tcb.snd_una; + let mut found_gap = None; + + for (&sack_start, &sack_len) in &tcb.sack_scoreboard { + if seq_lt(gap_start, sack_start) { + // Gap: [gap_start, sack_start) + let gap_len = sack_start.wrapping_sub(gap_start) as usize; + let len = gap_len.min(tcb.eff_snd_mss as usize); + let offset = gap_start.wrapping_sub(tcb.snd_una) as usize; + found_gap = Some((offset, len)); + break; + } + // Move past this SACKed range. + let sack_end = sack_start.wrapping_add(sack_len); + if seq_lt(gap_start, sack_end) { + gap_start = sack_end; + } + } + + found_gap.unwrap_or_else(|| { + // No gap found before scoreboard — retransmit from snd_una. + let len = tcb.send_buffer.available().min(tcb.eff_snd_mss as usize); + (0, len) + }) + } else { + // No SACK — retransmit from snd_una. + let len = tcb.send_buffer.available().min(tcb.eff_snd_mss as usize); + (0, len) + }; + + if retransmit_len == 0 { + continue; + } + + let id = tcb.id; + let dst_mac = neighbor_handler + .lookup(now, &id.remote_addr) + .unwrap_or(crate::net::wire::ethernet::MacAddress::broadcast()); + + let mut payload = vec![0u8; retransmit_len]; + tcb.send_buffer.peek_at(retransmit_offset, &mut payload); + + let seq = tcb.snd_una.wrapping_add(retransmit_offset as u32); + + let ts = if tcb.ts_enabled { + let tsval = now.duration_since(tcb.ts_offset).as_millis() as u32; + Some((tsval, tcb.ts_recent)) + } else { + None + }; + SegmentBuilder::build_data( + id.local_addr, + id.remote_addr, + id.local_port, + id.remote_port, + seq, + tcb.rcv_nxt, + tcb.advertised_window(), + &payload, + ts, + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); + + // Fast recovery: halve cwnd. + tcb.ssthresh = (tcb.cwnd / 2).max(2 * tcb.eff_snd_mss as u32); + tcb.cwnd = tcb.ssthresh; + tcb.dup_ack_count = 0; +} +``` + +Note: `seq_lt` and `seq_le` are already imported at the top of `process_established`; ensure they are accessible in `poll_timers` scope (they're module-level functions in `wire::tcp`). + +**Step 4: Run tests** + +Run: `cargo test fast_retransmit` +Expected: All PASS (both old and new tests) + +**Step 5: Commit** + +```bash +git add src/net/handler/tcp/mod.rs +git commit -m "feat(tcp): selective retransmit using SACK scoreboard" +``` + +--- + +## Task 5: Zero-Copy Send Path + +**Files:** +- Modify: `src/net/handler/tcp/ring_buffer.rs` (add peek_slices) +- Modify: `src/net/handler/tcp/segment.rs` (add build_data_from_slices) +- Modify: `src/net/handler/tcp/mod.rs` (update poll_send and poll_timers call sites) + +### Step 1: Write failing test for peek_slices + +In `src/net/handler/tcp/ring_buffer.rs`, add to tests: + +```rust +#[test] +fn peek_slices_no_wrap() { + let mut rb = RingBuffer::new(64); + rb.write(b"hello world"); + let (a, b) = rb.peek_slices(0, 5); + assert_eq!(a, b"hello"); + assert!(b.is_empty()); + assert_eq!(rb.available(), 11); // unchanged +} + +#[test] +fn peek_slices_with_wrap() { + let mut rb = RingBuffer::new(16); + // Move head near the end. + rb.write(&[0xAA; 12]); + let mut discard = [0u8; 12]; + rb.read(&mut discard); + // head=12. Write 8 bytes: 4 at end [12..16], 4 wrap [0..4]. + rb.write(&[0xBB; 8]); + + let (a, b) = rb.peek_slices(0, 8); + assert_eq!(a, &[0xBB; 4]); // [12..16] + assert_eq!(b, &[0xBB; 4]); // [0..4] +} + +#[test] +fn peek_slices_with_offset() { + let mut rb = RingBuffer::new(64); + rb.write(b"hello world"); + let (a, b) = rb.peek_slices(6, 5); + assert_eq!(a, b"world"); + assert!(b.is_empty()); +} +``` + +### Step 2: Run test to verify it fails + +Run: `cargo test peek_slices` +Expected: FAIL — method doesn't exist. + +### Step 3: Implement peek_slices + +In `src/net/handler/tcp/ring_buffer.rs`, add to `impl RingBuffer` (after `peek_at`): + +```rust +/// Return two slices covering `len` bytes starting at `offset` from `head`, +/// without advancing `head`. The first slice covers data up to the end of +/// the backing buffer; the second covers the wrap-around portion (empty if +/// no wrap occurs). Used for zero-copy segment building. +#[inline] +pub fn peek_slices(&self, offset: usize, len: usize) -> (&[u8], &[u8]) { + debug_assert!(offset + len <= self.len); + let pos = (self.head.wrapping_add(offset)) & self.mask; + let first = len.min(self.buf.len() - pos); + if first >= len { + (&self.buf[pos..pos + len], &[]) + } else { + (&self.buf[pos..], &self.buf[..len - first]) + } +} +``` + +### Step 4: Run test to verify it passes + +Run: `cargo test peek_slices` +Expected: PASS + +### Step 5: Write failing test for build_data_from_slices + +In `src/net/handler/tcp/segment.rs` tests: + +```rust +#[test] +fn build_data_from_slices_two_parts() { + let mut free = make_test_frames(2); + let mut tx = Vec::new(); + + let part1 = b"hel"; + let part2 = b"lo"; + + SegmentBuilder::build_data_from_slices( + IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])), + IpAddress::V4(Ipv4Address::new([10, 0, 0, 2])), + 1234, + 80, + 100, + 200, + 65535, + (part1.as_slice(), part2.as_slice()), + None, + MacAddress::broadcast(), + MacAddress::broadcast(), + false, + &mut free, + &mut tx, + ); + + assert_eq!(tx.len(), 1, "should produce one frame"); +} +``` + +### Step 6: Run test to verify it fails + +Run: `cargo test build_data_from_slices` +Expected: FAIL — method doesn't exist. + +### Step 7: Implement build_data_from_slices + +In `segment.rs`, add two new internal helpers `build_ipv4_data_segment_slices` and `build_ipv6_data_segment_slices` that accept `(&[u8], &[u8])` instead of `&[u8]`. These are copies of the existing `build_ipv4_data_segment`/`build_ipv6_data_segment` but write two payload slices contiguously: + +First, add the public method after `build_data`: + +```rust +/// Build a data segment from two contiguous slices (zero-copy from ring buffer). +/// The two slices represent data that may wrap in the ring buffer. +#[inline] +pub fn build_data_from_slices<'umem>( + local_addr: IpAddress, + remote_addr: IpAddress, + local_port: u16, + remote_port: u16, + seq: u32, + ack: u32, + window: u16, + payload: (&[u8], &[u8]), + timestamp: Option<(u32, u32)>, + src_mac: MacAddress, + dst_mac: MacAddress, + tx_offload: bool, + free_frames: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, +) { + let mut ts_buf = [0u8; 12]; + let tcp_options: &[u8] = if let Some((tsval, tsecr)) = timestamp { + ts_buf[0] = options::NOP; + ts_buf[1] = options::NOP; + write_timestamp_option(&mut ts_buf[2..], tsval, tsecr); + &ts_buf + } else { + &[] + }; + + match (local_addr, remote_addr) { + (IpAddress::V4(local_ip), IpAddress::V4(remote_ip)) => { + Self::build_ipv4_data_segment_slices( + local_ip, remote_ip, local_port, remote_port, + seq, ack, flags::ACK, window, payload, + tcp_options, src_mac, dst_mac, tx_offload, + free_frames, tx_return, + ); + } + (IpAddress::V6(local_ip), IpAddress::V6(remote_ip)) => { + Self::build_ipv6_data_segment_slices( + local_ip, remote_ip, local_port, remote_port, + seq, ack, flags::ACK, window, payload, + tcp_options, src_mac, dst_mac, tx_offload, + free_frames, tx_return, + ); + } + _ => {} + } +} +``` + +Then add the internal helpers. These follow the same pattern as existing `build_ipv4_data_segment` / `build_ipv6_data_segment` but accept `payload: (&[u8], &[u8])` and compute `payload_len = payload.0.len() + payload.1.len()`. When writing the payload to the frame, write both slices sequentially: + +```rust +// In the frame writing section, replace: +// frame[payload_start..payload_start + payload.len()].copy_from_slice(payload); +// With: +// frame[payload_start..payload_start + payload.0.len()].copy_from_slice(payload.0); +// if !payload.1.is_empty() { +// let p2_start = payload_start + payload.0.len(); +// frame[p2_start..p2_start + payload.1.len()].copy_from_slice(payload.1); +// } +``` + +The implementer should copy `build_ipv4_data_segment` (find it by searching for `fn build_ipv4_data_segment`) and modify the payload parameter and copy logic. Same for the IPv6 variant. The checksum functions already work on byte ranges within the frame, so they need no changes. + +### Step 8: Run test to verify it passes + +Run: `cargo test build_data_from_slices` +Expected: PASS + +### Step 9: Update poll_send to use zero-copy path + +In `src/net/handler/tcp/mod.rs`, in `poll_send` (around lines 1924-1953), replace: + +```rust +// OLD: +let mut payload = vec![0u8; to_send]; +tcb.send_buffer.peek_at(bytes_in_flight, &mut payload); +// ... build_data(..., &payload, ...) +``` + +With: + +```rust +// NEW: zero-copy from ring buffer. +let payload = tcb.send_buffer.peek_slices(bytes_in_flight, to_send); +// ... build_data_from_slices(..., payload, ...) +``` + +Update the `SegmentBuilder::build_data` call to `SegmentBuilder::build_data_from_slices` and pass the tuple directly. + +### Step 10: Update poll_timers fast retransmit to use zero-copy path + +In `poll_timers`, in the fast retransmit path, replace: + +```rust +let mut payload = vec![0u8; retransmit_len]; +tcb.send_buffer.peek_at(retransmit_offset, &mut payload); +// ... build_data(..., &payload, ...) +``` + +With: + +```rust +let payload = tcb.send_buffer.peek_slices(retransmit_offset, retransmit_len); +// ... build_data_from_slices(..., payload, ...) +``` + +### Step 11: Update poll_timers RTO retransmit to use zero-copy path + +In `poll_timers`, in the RTO retransmit path for `TcpState::Established` (around line 1822-1848), replace: + +```rust +let mut payload = vec![0u8; retransmit_len]; +tcb.send_buffer.peek_at(0, &mut payload); +// ... build_data(..., &payload, ...) +``` + +With: + +```rust +let payload = tcb.send_buffer.peek_slices(0, retransmit_len); +// ... build_data_from_slices(..., payload, ...) +``` + +### Step 12: Run all tests + +Run: `cargo test` +Expected: All PASS + +### Step 13: Commit + +```bash +git add src/net/handler/tcp/ring_buffer.rs src/net/handler/tcp/segment.rs src/net/handler/tcp/mod.rs +git commit -m "feat(tcp): zero-copy send path with peek_slices and build_data_from_slices" +``` + +--- + +## Summary + +| Task | Feature | Key Files | +|------|---------|-----------| +| 1 | Zero-window probing (persist timer) | mod.rs (poll_send, process_established) | +| 2 | SACK block sending (receiver) | segment.rs (build_ack_with_sack), mod.rs (OOO path) | +| 3 | SACK block receiving (sender) | mod.rs (ACK processing, RTO path) | +| 4 | Selective retransmit | mod.rs (fast retransmit in poll_timers) | +| 5 | Zero-copy send path | ring_buffer.rs, segment.rs, mod.rs | + +**Dependencies:** Task 1 is independent. Tasks 2 → 3 → 4 build on each other. Task 5 is independent but should be done last since Tasks 1-4 modify the same call sites. diff --git a/docs/plans/2026-03-07-tcp-rfc9293-compliance-design.md b/docs/plans/2026-03-07-tcp-rfc9293-compliance-design.md new file mode 100644 index 0000000..884fc87 --- /dev/null +++ b/docs/plans/2026-03-07-tcp-rfc9293-compliance-design.md @@ -0,0 +1,182 @@ +# TCP RFC 9293 Compliance Fixes — Design + +**Goal:** Fix the 10 highest-priority RFC 9293 compliance issues found during a full audit of the TCP implementation against the RFC and referenced standards (RFC 5961, 6298, 5681). + +**Architecture:** All fixes are localized to the TCP handler (`mod.rs`), TCB (`tcb.rs`), and segment builder (`segment.rs`). No new files needed. Most fixes are small, surgical changes to existing processing logic. + +--- + +## 1. recv_buffer.write() return value — data corruption fix + +### Problem +`mod.rs:~1535`: `rcv_nxt` advances by full `payload.len()` even when `recv_buffer.write()` returns fewer bytes (buffer nearly full). This desynchronizes `rcv_nxt` from actual buffered data, causing silent data loss. + +### Fix +Capture the return value of `recv_buffer.write(payload)`. Only advance `rcv_nxt` by the number of bytes actually written. If partial write occurs, the unwritten portion will be retransmitted by the peer (since we won't ACK past what we buffered). + +```rust +let written = tcb.recv_buffer.write(payload); +tcb.rcv_nxt = tcb.rcv_nxt.wrapping_add(written as u32); +``` + +Also audit `write_at()` for OOO data — ensure offset + data doesn't exceed buffer capacity. + +--- + +## 2. Out-of-order FIN processing — data loss fix + +### Problem +`mod.rs:~1665`: FIN is processed whenever the flag is set, regardless of whether all preceding data has been received. An out-of-order segment with FIN transitions to CLOSE-WAIT before prior data arrives. + +### Fix +Only process FIN when the segment's data ends exactly at `rcv_nxt`. After data processing (Step 7), check: `seg_seq + payload_len == rcv_nxt` (accounting for wrapping). If FIN is set but this condition is false, defer FIN processing — store a flag (`pending_remote_fin: bool`) and re-check when OOO gaps are filled. + +--- + +## 3. RST ordering + challenge ACK (RFC 5961) — security fix + +### Problem +Two issues combined: +1. RST is checked before sequence number validation (`mod.rs:~1296`), accepting out-of-window RSTs. +2. No challenge ACK for in-window but non-exact RST (RFC 5961 MUST). + +### Fix +Reorder `process_established` to match RFC §3.10.7.4 step order: +1. **Step 1:** PAWS check, then segment acceptability check (already exists, just move before RST) +2. **Step 2:** RST check — only AFTER acceptability. Add RFC 5961 logic: + - `SEG.SEQ == RCV.NXT` → reset connection + - In-window but not exact → send challenge ACK, drop segment + - Out-of-window → already rejected by Step 1 + +Apply the same reordering to `process_teardown`. + +--- + +## 4. SYN check in synchronized states (RFC 5961) — security fix + +### Problem +Neither `process_established` nor `process_teardown` check for the SYN flag. SYN in ESTABLISHED should trigger a challenge ACK. + +### Fix +After Step 3 (security/precedence — skipped) and before Step 5 (ACK), add Step 4: if `seg_flags & SYN != 0`, send a challenge ACK and drop the segment. This applies to all synchronized states: ESTABLISHED, FIN-WAIT-1/2, CLOSE-WAIT, CLOSING, LAST-ACK, TIME-WAIT. + +--- + +## 5. FIN retransmission in teardown states + +### Problem +`poll_timers` RTO match only handles SynSent, SynReceived, Established. FIN-WAIT-1, Closing, and LAST-ACK hit `_ => continue`. Lost FINs are never retransmitted. + +### Fix +Add cases for `FinWait1`, `Closing`, and `LastAck` in the `poll_timers` retransmit match. These should retransmit the FIN segment (rebuild from TCB state: `build_fin_ack` with `seq = snd_nxt - 1`, `ack = rcv_nxt`). Apply the same exponential backoff and R2 threshold logic. + +--- + +## 6. "ACK bit off" drop + +### Problem +Segments without ACK flag in ESTABLISHED/teardown states fall through to data/FIN processing instead of being dropped. + +### Fix +In `process_established`, after the RST check (Step 2) and SYN check (Step 4), add Step 5 preamble: +```rust +if seg_flags & flags::ACK == 0 { + return; // drop segment +} +``` + +Same check needed at the top of `process_teardown` ACK-dependent processing. + +--- + +## 7. SEG.ACK > SND.NXT handling + +### Problem +ACKs acknowledging data not yet sent are silently ignored. RFC requires: send ACK, drop segment, return. + +### Fix +In the ACK processing section of `process_established`, after the `snd_una < seg_ack <= snd_nxt` check, add: +```rust +if seq_lt(tcb.snd_nxt, seg_ack) { + // ACK for unsent data — send ACK and drop + tcb.ack_pending = true; + tcb.ack_delay_count = u8::MAX; // force immediate + return; +} +``` + +--- + +## 8. Window update WL1/WL2 guard + +### Problem +`snd_wnd`/`snd_wl1`/`snd_wl2` are unconditionally updated on every new ACK. Stale out-of-order segments can regress the send window. + +### Fix +Guard the window update with the RFC condition: +```rust +if seq_lt(tcb.snd_wl1, seg_seq) + || (tcb.snd_wl1 == seg_seq && seq_le(tcb.snd_wl2, seg_ack)) +{ + tcb.snd_wnd = tcb.scale_incoming_window(seg_wnd as u32); + tcb.snd_wl1 = seg_seq; + tcb.snd_wl2 = seg_ack; +} +``` + +Apply to both new-ACK and duplicate-ACK window update paths. + +--- + +## 9. CLOSE-WAIT ACK processing + +### Problem +CLOSE-WAIT does nothing with ACKs. The local side can still send data in CLOSE-WAIT, but ACKs for that data are never processed. This breaks half-close. + +### Fix +Add ESTABLISHED-equivalent ACK processing for CLOSE-WAIT in `process_teardown`: +- Advance `snd_una` when `snd_una < seg_ack <= snd_nxt` +- Update send window (with WL1/WL2 guard) +- Handle `seg_ack > snd_nxt` (send ACK, drop) +- Process congestion control (cwnd updates) + +This can share code with `process_established` by extracting common ACK processing into a helper, or by inlining the logic. + +--- + +## 10. SWS avoidance (sender + receiver) + +### Problem +**Sender:** `poll_send` sends whenever `can_send > 0`. No check for usable window size. +**Receiver:** `advertised_window()` returns raw free space. Even 1 freed byte opens the window. + +### Fix + +**Sender (MUST-38):** In `poll_send`, add SWS check before sending sub-MSS data: +```rust +let usable_window = can_send; +if usable_window < eff_snd_mss && usable_window < max_snd_wnd / 2 && data_available > usable_window { + // SWS: don't send unless Nagle allows it + continue; +} +``` +Where `max_snd_wnd` tracks the largest window ever advertised by the peer (new TCB field). + +**Receiver (MUST-39):** In `advertised_window()`, only increase the window when free space >= `min(eff_rcv_mss, recv_buffer.capacity() / 2)`. Track `last_advertised_wnd_edge` to prevent shrinkage (also fixes MUST-34, window right edge moving left). + +New TCB fields: +- `max_snd_wnd: u32` — largest send window seen from peer +- `last_advertised_right_edge: u32` — `rcv_nxt + advertised_wnd` at time of last ACK sent + +--- + +## Scope + +**Modified files:** +- `src/net/handler/tcp/mod.rs` — fixes 1-9 (reorder steps, add checks, FIN retransmit) +- `src/net/handler/tcp/tcb.rs` — fix 10 (new SWS fields), fix 2 (`pending_remote_fin`) +- `src/net/handler/tcp/segment.rs` — no changes expected + +**No new files.** + +**Testing:** Each fix gets a unit test exercising the specific scenario (out-of-order FIN, blind RST, ACK > SND.NXT, etc.). Existing tests must continue to pass. diff --git a/docs/plans/2026-03-07-tcp-rfc9293-compliance-impl.md b/docs/plans/2026-03-07-tcp-rfc9293-compliance-impl.md new file mode 100644 index 0000000..b713712 --- /dev/null +++ b/docs/plans/2026-03-07-tcp-rfc9293-compliance-impl.md @@ -0,0 +1,655 @@ +# TCP RFC 9293 Compliance Fixes — Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Fix the 10 highest-priority RFC 9293 compliance issues: data corruption, security vulnerabilities, and protocol violations in the TCP handler. + +**Architecture:** All changes are in the TCP handler module (`mod.rs`, `tcb.rs`). Each task is independent after Task 1 (foundational). Test-first approach using the existing `build_tcp_frame` test infrastructure in `mod.rs`. + +**Tech Stack:** Rust, coarsetime, existing TCP handler + wire module infrastructure. + +--- + +### Task 1: Fix recv_buffer.write() return value — data corruption + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs:1531-1532` (in-order data write) +- Modify: `src/net/handler/tcp/mod.rs:2563-2564` (FinWait1 data write) +- Modify: `src/net/handler/tcp/mod.rs:2629-2630` (FinWait2 data write) +- Test: `src/net/handler/tcp/mod.rs` (add test in `mod tests`) + +**Step 1: Write the failing test** + +Add a unit test in `mod.rs` tests that establishes a connection, fills the recv buffer nearly full, sends a segment larger than remaining space, and verifies `rcv_nxt` only advances by the amount actually written. + +```rust +#[test] +fn recv_buffer_partial_write_advances_rcv_nxt_correctly() { + // Setup: create handler, connect, fill recv buffer to near capacity. + // Send a data segment larger than remaining free space. + // Verify rcv_nxt advanced by bytes_written (not payload_len). +} +``` + +The test should construct a scenario where `recv_buffer.free_space() < payload_len` and verify `rcv_nxt` advances by the written amount only. + +**Step 2: Run test to verify it fails** + +Run: `cargo test --test sync recv_buffer_partial_write` or unit test via `cargo test -p voidnet recv_buffer_partial_write` +Expected: FAIL — `rcv_nxt` advances by full `payload_len` + +**Step 3: Fix the three data write sites** + +In `process_established` (~line 1531): +```rust +// Before: +tcb.recv_buffer.write(payload); +tcb.rcv_nxt = rcv_nxt.wrapping_add(payload_len as u32); + +// After: +let written = tcb.recv_buffer.write(payload); +tcb.rcv_nxt = rcv_nxt.wrapping_add(written as u32); +``` + +Apply the same fix in `process_teardown` FinWait1 (~line 2563) and FinWait2 (~line 2629): +```rust +let written = tcb.recv_buffer.write(payload); +tcb.rcv_nxt = tcb.rcv_nxt.wrapping_add(written as u32); +``` + +**Step 4: Run test to verify it passes** + +Run: `cargo test` +Expected: PASS + +**Step 5: Commit** + +``` +fix(tcp): use recv_buffer.write() return value for rcv_nxt advancement + +rcv_nxt was advanced by the full payload length even when the recv buffer +was nearly full and only a partial write occurred. This caused sequence +number desynchronization and silent data loss (RFC 9293 §3.10.7.4 Step 7). +``` + +--- + +### Task 2: Fix out-of-order FIN processing — data loss + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs:1665-1698` (FIN check in process_established) +- Test: `src/net/handler/tcp/mod.rs` + +**Step 1: Write the failing test** + +Test that sends a FIN segment with `seg_seq > rcv_nxt` (out-of-order). Verify the connection does NOT transition to CloseWait — it should remain Established until the gap is filled. + +**Step 2: Run test to verify it fails** + +Expected: FAIL — connection transitions to CloseWait prematurely + +**Step 3: Guard FIN processing with in-order check** + +Replace the FIN check at ~line 1665: +```rust +// Before: +if seg_flags & flags::FIN != 0 { + +// After: +if seg_flags & flags::FIN != 0 && seg_seq.wrapping_add(payload_len as u32) == self.connections[idx].rcv_nxt { +``` + +This ensures FIN is only processed when all preceding data has been received (the FIN's sequence position matches `rcv_nxt` after data processing). + +Note: this means if FIN arrives out-of-order, the peer will retransmit it. We could add a `pending_remote_fin` flag for optimization later, but for correctness the simple guard is sufficient — the peer's retransmission will deliver the FIN once the gap is filled. + +**Step 4: Run tests** + +Run: `cargo test` +Expected: PASS + +**Step 5: Commit** + +``` +fix(tcp): only process FIN when all preceding data received + +Out-of-order segments with FIN set would transition to CloseWait before +all prior data arrived, causing data loss. Now FIN is only processed when +seg_seq + payload_len == rcv_nxt (RFC 9293 §3.10.7.4 Step 8). +``` + +--- + +### Task 3: Fix RST processing order + RFC 5961 challenge ACK + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs:1291-1375` (reorder process_established steps) +- Modify: `src/net/handler/tcp/mod.rs:2446-2527` (reorder process_teardown steps) +- Test: `src/net/handler/tcp/mod.rs` + +**Step 1: Write two failing tests** + +1. `rst_outside_window_is_silently_dropped` — send RST with `seg_seq` outside receive window. Verify connection is NOT reset. +2. `rst_in_window_but_not_exact_sends_challenge_ack` — send RST with `seg_seq` in window but != `rcv_nxt`. Verify connection survives and an ACK is sent. + +**Step 2: Run tests to verify they fail** + +Expected: FAIL — connection is reset in both cases + +**Step 3: Reorder process_established** + +Move RST check AFTER PAWS + segment acceptability. Then add RFC 5961 logic: + +```rust +// After segment acceptability check passes: + +// Step 2: RST check (RFC 5961). +if seg_flags & flags::RST != 0 { + if seg_seq == self.connections[idx].rcv_nxt { + // Exact match: reset connection. + self.connections[idx].event_queue.push(TcpEvent::Reset); + let id = self.connections[idx].id; + self.decrement_syn_received(&id); + self.connections.remove(idx); + rx_return.push(frame); + return; + } + // In-window but not exact: send challenge ACK, drop segment. + let tcb = &self.connections[idx]; + // ... build_ack (challenge ACK) ... + rx_return.push(frame); + return; +} +``` + +Apply similar reordering in `process_teardown` — move RST check after acceptability, add RFC 5961 challenge ACK for non-exact in-window RST. Keep the TIME-WAIT RST ignore behavior. + +**Step 4: Run tests** + +Run: `cargo test` +Expected: PASS + +**Step 5: Commit** + +``` +fix(tcp): reorder RST check after sequence validation, add RFC 5961 challenge ACK + +RST was checked before segment acceptability, allowing out-of-window RST +to reset connections (blind RST attack). Now: (1) out-of-window RST is +dropped by acceptability check, (2) in-window non-exact RST gets challenge +ACK, (3) only exact SEG.SEQ == RCV.NXT resets (RFC 5961 §3.2). +``` + +--- + +### Task 4: Add SYN check in synchronized states (RFC 5961) + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs` (process_established, after RST check) +- Modify: `src/net/handler/tcp/mod.rs` (process_teardown, after RST check) +- Test: `src/net/handler/tcp/mod.rs` + +**Step 1: Write failing test** + +`syn_in_established_sends_challenge_ack` — send a segment with SYN flag set in Established state. Verify: (1) connection is NOT reset, (2) a challenge ACK is sent, (3) segment data is NOT processed. + +**Step 2: Run test to verify it fails** + +Expected: FAIL — SYN is silently ignored, data is processed + +**Step 3: Add SYN check after RST check** + +In `process_established`, after the RST check block and before ACK processing: + +```rust +// Step 4: SYN check (RFC 5961 — challenge ACK for SYN in synchronized state). +if seg_flags & flags::SYN != 0 { + let tcb = &self.connections[idx]; + // Send challenge ACK. + let ts = if tcb.ts_enabled { + let tsval = now.duration_since(tcb.ts_offset).as_millis() as u32; + Some((tsval, tcb.ts_recent)) + } else { + None + }; + SegmentBuilder::build_ack( + tcb.id.local_addr, tcb.id.remote_addr, + tcb.id.local_port, tcb.id.remote_port, + tcb.snd_nxt, tcb.rcv_nxt, + tcb.advertised_window(), ack_flags, ts, + src_mac, dst_mac, self.tx_offload, + free_frames, tx_return, + ); + rx_return.push(frame); + return; +} +``` + +Add the same check in `process_teardown`, after the RST check and before the state match. + +**Step 4: Run tests** + +Run: `cargo test` +Expected: PASS + +**Step 5: Commit** + +``` +fix(tcp): send challenge ACK for SYN in synchronized states (RFC 5961) + +SYN flag was not checked in ESTABLISHED or teardown states, allowing +blind SYN attacks. Now sends challenge ACK and drops the segment per +RFC 5961 §4. +``` + +--- + +### Task 5: Add FIN retransmission for teardown states + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs:2040` (`poll_timers` RTO match) +- Test: `src/net/handler/tcp/mod.rs` + +**Step 1: Write failing test** + +`fin_retransmitted_in_fin_wait1` — put a connection into FinWait1 with a retransmit deadline in the past. Call `poll_timers`. Verify a FIN-ACK segment is sent and the retransmit deadline is rescheduled. + +**Step 2: Run test to verify it fails** + +Expected: FAIL — `poll_timers` skips FinWait1 (`_ => continue`) + +**Step 3: Add FIN retransmission cases** + +In the `poll_timers` RTO match (~line 2040), before `_ => continue`: + +```rust +TcpState::FinWait1 | TcpState::Closing | TcpState::LastAck => { + // Retransmit FIN-ACK. + let ts = if tcb.ts_enabled { + let tsval = now.duration_since(tcb.ts_offset).as_millis() as u32; + Some((tsval, tcb.ts_recent)) + } else { + None + }; + if let Some(fin_seq) = tcb.fin_seq { + SegmentBuilder::build_fin_ack( + id.local_addr, id.remote_addr, + id.local_port, id.remote_port, + fin_seq, tcb.rcv_nxt, + tcb.advertised_window(), ts, + src_mac, dst_mac, + self.tx_offload, free_frames, tx_return, + ); + } + tcb.rto_backoff += 1; + tcb.retransmit_deadline = + Some(now + coarsetime::Duration::from_millis(tcb.rto << tcb.rto_backoff)); +} +``` + +**Step 4: Run tests** + +Run: `cargo test` +Expected: PASS + +**Step 5: Commit** + +``` +feat(tcp): retransmit FIN in FinWait1, Closing, and LastAck states + +Lost FIN segments were never retransmitted because poll_timers only +handled SynSent, SynReceived, and Established. Connections would hang +in teardown states until the R2 timeout (RFC 9293 §3.10.8). +``` + +--- + +### Task 6: Drop segments without ACK flag + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs` (process_established, before ACK processing) +- Modify: `src/net/handler/tcp/mod.rs` (process_teardown, after SYN check) +- Test: `src/net/handler/tcp/mod.rs` + +**Step 1: Write failing test** + +`segment_without_ack_is_dropped` — send a data segment with ACK flag cleared in Established state. Verify: no data is written to recv_buffer, segment is dropped. + +**Step 2: Run test to verify it fails** + +Expected: FAIL — data is processed despite missing ACK + +**Step 3: Add ACK-off guard** + +In `process_established`, after the SYN check (Task 4) and before ACK processing: + +```rust +// Step 5 preamble: if ACK bit is off, drop segment and return. +if seg_flags & flags::ACK == 0 { + rx_return.push(frame); + return; +} +``` + +In `process_teardown`, add the same check after the SYN check and before the state match block. + +**Step 4: Run tests** + +Run: `cargo test` +Expected: PASS + +**Step 5: Commit** + +``` +fix(tcp): drop segments without ACK flag in synchronized states + +Segments without ACK were falling through to data/FIN processing +instead of being dropped (RFC 9293 §3.10.7.4 Step 5). +``` + +--- + +### Task 7: Handle SEG.ACK > SND.NXT + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs:1394` (ACK processing in process_established) +- Test: `src/net/handler/tcp/mod.rs` + +**Step 1: Write failing test** + +`ack_beyond_snd_nxt_sends_ack_and_drops` — send a segment with `seg_ack > snd_nxt`. Verify: (1) an ACK is sent in response, (2) no state changes occur, (3) segment data is not processed. + +**Step 2: Run test to verify it fails** + +Expected: FAIL — segment falls through silently + +**Step 3: Add future-ACK check** + +In `process_established` ACK processing, after checking `seq_lt(snd_una, seg_ack) && seq_le(seg_ack, snd_nxt)`, add an else-if before the duplicate ACK branch: + +```rust +} else if seq_lt(snd_nxt, seg_ack) { + // ACK for unsent data — send ACK and drop (RFC 9293 §3.10.7.4 Step 5). + let tcb = &self.connections[idx]; + let ts = if tcb.ts_enabled { + let tsval = now.duration_since(tcb.ts_offset).as_millis() as u32; + Some((tsval, tcb.ts_recent)) + } else { + None + }; + SegmentBuilder::build_ack( + tcb.id.local_addr, tcb.id.remote_addr, + tcb.id.local_port, tcb.id.remote_port, + tcb.snd_nxt, tcb.rcv_nxt, + tcb.advertised_window(), ack_flags, ts, + src_mac, dst_mac, self.tx_offload, + free_frames, tx_return, + ); + rx_return.push(frame); + return; +} else if seg_ack == snd_una && payload_len == 0 { +``` + +**Step 4: Run tests** + +Run: `cargo test` +Expected: PASS + +**Step 5: Commit** + +``` +fix(tcp): send ACK and drop segment when SEG.ACK > SND.NXT + +ACKs acknowledging data not yet sent were silently ignored. Now sends +an ACK and drops the segment per RFC 9293 §3.10.7.4 Step 5. +``` + +--- + +### Task 8: Add window update WL1/WL2 guard + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs:1459-1461` (new-ACK window update) +- Modify: `src/net/handler/tcp/mod.rs:1500-1503` (dup-ACK window update) +- Test: `src/net/handler/tcp/mod.rs` + +**Step 1: Write failing test** + +`stale_segment_does_not_regress_window` — process two ACK segments out of order (segment B with higher seq first, then segment A with lower seq but different window). Verify the window is NOT overwritten by the stale segment A. + +**Step 2: Run test to verify it fails** + +Expected: FAIL — window is unconditionally updated + +**Step 3: Add WL1/WL2 guard** + +Replace the unconditional window update at ~line 1459: + +```rust +// Before: +tcb.snd_wnd = tcb.scale_incoming_window(seg_wnd); +tcb.snd_wl1 = seg_seq; +tcb.snd_wl2 = seg_ack; + +// After: +if seq_lt(tcb.snd_wl1, seg_seq) + || (tcb.snd_wl1 == seg_seq && seq_le(tcb.snd_wl2, seg_ack)) +{ + tcb.snd_wnd = tcb.scale_incoming_window(seg_wnd); + tcb.snd_wl1 = seg_seq; + tcb.snd_wl2 = seg_ack; +} +``` + +For the duplicate ACK window update (~line 1500), apply the same guard: + +```rust +// Before: +if new_wnd != tcb.snd_wnd { + tcb.snd_wnd = new_wnd; + tcb.snd_wl1 = seg_seq; + tcb.snd_wl2 = seg_ack; +} + +// After: +if seq_lt(tcb.snd_wl1, seg_seq) + || (tcb.snd_wl1 == seg_seq && seq_le(tcb.snd_wl2, seg_ack)) +{ + tcb.snd_wnd = new_wnd; + tcb.snd_wl1 = seg_seq; + tcb.snd_wl2 = seg_ack; +} +``` + +**Step 4: Run tests** + +Run: `cargo test` +Expected: PASS + +**Step 5: Commit** + +``` +fix(tcp): guard window updates with SND.WL1/WL2 ordering check + +Send window was unconditionally updated on every ACK. Stale out-of-order +segments could regress the window. Now uses the RFC 9293 §3.10.7.4 Step 5 +guard: SND.WL1 < SEG.SEQ || (SND.WL1 == SEG.SEQ && SND.WL2 <= SEG.ACK). +``` + +--- + +### Task 9: Add ACK processing in CLOSE-WAIT + +**Files:** +- Modify: `src/net/handler/tcp/mod.rs:2740-2744` (CloseWait branch in process_teardown) +- Test: `src/net/handler/tcp/mod.rs` + +**Step 1: Write failing test** + +`close_wait_processes_ack_for_sent_data` — put a connection in CloseWait with unacknowledged data in the send buffer. Send an ACK for that data. Verify `snd_una` advances and `send_buffer` is drained. + +**Step 2: Run test to verify it fails** + +Expected: FAIL — CloseWait does nothing with ACKs + +**Step 3: Add ACK processing to CloseWait** + +Replace the CloseWait handler: + +```rust +TcpState::CloseWait => { + // Process ACKs — local side can still send data. + if seg_flags & flags::ACK != 0 { + let tcb = &mut self.connections[idx]; + let snd_una = tcb.snd_una; + let snd_nxt = tcb.snd_nxt; + + if crate::net::wire::tcp::seq_lt(snd_una, seg_ack) + && crate::net::wire::tcp::seq_le(seg_ack, snd_nxt) + { + let bytes_acked = seg_ack.wrapping_sub(snd_una) as usize; + tcb.snd_una = seg_ack; + let buf_advance = bytes_acked.min(tcb.send_buffer.available()); + tcb.send_buffer.advance(buf_advance); + + // Window update with WL1/WL2 guard. + if crate::net::wire::tcp::seq_lt(tcb.snd_wl1, seg_seq) + || (tcb.snd_wl1 == seg_seq + && crate::net::wire::tcp::seq_le(tcb.snd_wl2, seg_ack)) + { + tcb.snd_wnd = tcb.scale_incoming_window(seg_wnd); + tcb.snd_wl1 = seg_seq; + tcb.snd_wl2 = seg_ack; + } + } + } + rx_return.push(frame); +} +``` + +**Step 4: Run tests** + +Run: `cargo test` +Expected: PASS + +**Step 5: Commit** + +``` +fix(tcp): process ACKs in CLOSE-WAIT for half-close data transfer + +CLOSE-WAIT ignored all ACKs, breaking half-close scenarios where the +local side sends data after the remote closes. Now advances snd_una and +updates the send window (RFC 9293 §3.10.7.4 Step 5 — "Do the same +processing as for the ESTABLISHED state"). +``` + +--- + +### Task 10: Implement SWS avoidance (sender + receiver) + +**Files:** +- Modify: `src/net/handler/tcp/tcb.rs` (add `max_snd_wnd` and `last_advertised_right_edge` fields) +- Modify: `src/net/handler/tcp/tcb.rs:297-304` (`advertised_window()`) +- Modify: `src/net/handler/tcp/mod.rs` (update `max_snd_wnd` on window updates, sender SWS check in `poll_send`) +- Test: `src/net/handler/tcp/tcb.rs`, `src/net/handler/tcp/mod.rs` + +**Step 1: Add new TCB fields** + +In `tcb.rs`, add to the `Tcb` struct: + +```rust +/// Largest send window ever advertised by peer (for sender SWS avoidance). +pub max_snd_wnd: u32, +/// Right edge of last advertised receive window: rcv_nxt + wnd at ACK send time. +pub last_advertised_right_edge: u32, +``` + +Initialize both to 0 in `make_tcb()` test helper and all TCB construction sites. + +**Step 2: Write failing test for receiver SWS** + +Test that `advertised_window()` returns 0 when free space is less than `min(eff_snd_mss, capacity/2)` and the right edge would shrink. + +**Step 3: Implement receiver SWS avoidance** + +In `tcb.rs`, modify `advertised_window()`: + +```rust +pub fn advertised_window(&self) -> u16 { + let free = self.recv_buffer.free_space(); + let threshold = (self.rcv_mss as usize).min(self.recv_buffer.capacity() / 2); + + // SWS avoidance (MUST-39): don't open the window until we can + // advertise at least min(MSS, buffer/2) of new space. + let right_edge = self.rcv_nxt.wrapping_add(free as u32); + let prev_right_edge = self.last_advertised_right_edge; + let new_space = right_edge.wrapping_sub(prev_right_edge) as usize; + + let effective_free = if new_space < threshold && prev_right_edge != 0 { + // Clamp to previous right edge to avoid shrinking. + prev_right_edge.wrapping_sub(self.rcv_nxt).max(0) as usize + } else { + free + }; + + if self.wscale_enabled { + (effective_free >> self.rcv_wscale as usize).min(u16::MAX as usize) as u16 + } else { + effective_free.min(u16::MAX as usize) as u16 + } +} +``` + +Update `last_advertised_right_edge` wherever an ACK is sent (after calling `advertised_window()`). This requires setting it in `process_established` and `poll_timers` after every ACK build call. + +**Step 4: Write failing test for sender SWS** + +Test that `poll_send` does NOT send a sub-MSS segment when the usable window is less than `eff_snd_mss` and less than `max_snd_wnd / 2`. + +**Step 5: Implement sender SWS avoidance** + +In `poll_send`, after computing `can_send`: + +```rust +// Sender SWS avoidance (MUST-38): don't use a small window unless +// we can send a full segment or at least half the max window. +let sws_threshold = (tcb.eff_snd_mss as usize).max(tcb.max_snd_wnd as usize / 2); +if can_send < sws_threshold && can_send < data_available { + // Usable window too small — wait. + // (Still allow if all remaining data fits in the window.) +} else if can_send > 0 && data_available > 0 { + // ... existing send logic ... +} +``` + +Update `max_snd_wnd` wherever `snd_wnd` is updated in ACK processing: + +```rust +tcb.max_snd_wnd = tcb.max_snd_wnd.max(tcb.snd_wnd); +``` + +**Step 6: Run all tests** + +Run: `cargo test` +Expected: PASS + +**Step 7: Commit** + +``` +feat(tcp): implement SWS avoidance for sender and receiver (MUST-38, MUST-39) + +Sender: don't send sub-MSS data unless usable window >= eff_snd_mss or +>= max_snd_wnd/2. Receiver: don't open window until free space >= +min(MSS, buffer/2), preventing right edge from shrinking (also fixes +MUST-34). Tracks max_snd_wnd and last_advertised_right_edge in the TCB. +``` + +--- + +## Execution Order and Dependencies + +Tasks 1-9 are independent of each other and can be implemented in any order. However, the recommended order follows the priority ranking from the design doc. + +Task 10 (SWS) depends on Task 8 (WL1/WL2 guard) being done first since SWS also touches window update code. + +After all 10 tasks: run `cargo test` to verify no regressions. diff --git a/docs/plans/2026-03-08-tcp-performance-design.md b/docs/plans/2026-03-08-tcp-performance-design.md new file mode 100644 index 0000000..5c0303c --- /dev/null +++ b/docs/plans/2026-03-08-tcp-performance-design.md @@ -0,0 +1,45 @@ +# TCP Performance Optimization — Design + +**Date:** 2026-03-08 +**Goal:** Improve TCP echo throughput from ~400K pkt/s to millions of pkt/s +**Approach:** Eliminate per-packet syscalls, redundant computations, and heap allocations in hot paths + +## Current Bottlenecks + +1. **`Instant::now()` per packet** — `process_ipv4()` and `process_ipv6()` in `inbound.rs:68,183` call `Instant::now()` on every incoming TCP segment. The runtime loop already caches `now` and passes it to IPv4/IPv6 handlers, but those handlers call the no-`now` TCP variants instead of threading it through. + +2. **Repeated `duration_since().as_millis()` computation** — ~20+ identical calls to `now.duration_since(tcb.ts_offset).as_millis() as u32` throughout the TCP state machine for timestamp option generation. Each packet computes this multiple times across branches. + +3. **Vec allocations in hot paths** — `timers.rs:77,219` allocate `Vec` for removal indices. `inbound.rs:1498` allocates `Vec<(u32, u32)>` for SACK blocks (max 4 per RFC). + +4. **Connection creation calls `Instant::now()` 4 times** — `connection.rs:99,127,133,134` for retransmit_deadline, last_activity, ts_recent_age, ts_offset. + +## Changes + +### 1. Pass `now` through IPv4/IPv6 → TCP dispatch +- `ipv4.rs:109` — call `process_ipv4_with_now(..., now, ...)` instead of `process_ipv4(...)` +- `ipv6.rs:222` — call `process_ipv6_with_now(..., now, ...)` instead of `process_ipv6(...)` + +### 2. Rename `_with_now` to primary API +- Rename `process_ipv4_with_now` → `process_ipv4`, `process_ipv6_with_now` → `process_ipv6` +- Remove the old no-`now` wrappers (or keep only for test convenience) + +### 3. Pre-compute `tsval` once per segment +- After connection lookup in `process_segment()`, compute `tsval` once +- Thread pre-computed value into `process_established`, `process_syn_received`, etc. +- Replace all ~20 inline `now.duration_since(tcb.ts_offset).as_millis() as u32` calls + +### 4. Stack arrays for small collections +- `timers.rs:77` — delayed ACK removals: `[usize; 64]` + count +- `timers.rs:219` — RTO removals: `[usize; 64]` + count +- `inbound.rs:1498` — SACK blocks: `[(u32, u32); 4]` + count (RFC max is 4) + +### 5. Pass `now` into connection constructors +- Add `now: Instant` parameter to `new_client` and passive-open constructor +- Replace 4x `Instant::now()` with the passed-in value +- Update callers in `inbound.rs` and `connect()` paths + +## Out of Scope +- Connection lookup optimization (Vec → HashMap) — deferred +- Ring buffer copies — unavoidable for payload transfer +- Checksum offload — already wired in for both RX and TX diff --git a/docs/plans/2026-03-08-tcp-performance-impl.md b/docs/plans/2026-03-08-tcp-performance-impl.md new file mode 100644 index 0000000..13ff90c --- /dev/null +++ b/docs/plans/2026-03-08-tcp-performance-impl.md @@ -0,0 +1,484 @@ +# TCP Performance Optimization — Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Eliminate per-packet syscalls, redundant timestamp computations, and heap allocations from TCP hot paths to reach millions of pkt/s for TCP echo. + +**Architecture:** Pure refactoring — no behavioral changes. Thread the runtime loop's cached `now` through the dispatch chain, pre-compute repeated timestamp values, replace Vec allocations with stack arrays. All existing tests must continue to pass unchanged. + +**Tech Stack:** Rust, coarsetime::Instant, AF_XDP (libvoid) + +--- + +### Task 1: Wire `now` through IPv4/IPv6 → TCP dispatch + +**Files:** +- Modify: `src/net/handler/ipv4.rs:107-110` +- Modify: `src/net/handler/ipv6.rs:221-224` + +The IPv4/IPv6 handlers already receive `now` from the runtime loop but call the no-`now` TCP variants. Fix both dispatch sites. + +**Step 1: Update IPv4 handler to pass `now`** + +In `src/net/handler/ipv4.rs`, change line 108-110 from: + +```rust +IpProtocols::Tcp => { + tcp_handler.process_ipv4(frame, neighbor_handler, free_frames, rx_return, tx_return) +} +``` + +to: + +```rust +IpProtocols::Tcp => { + tcp_handler.process_ipv4_with_now(frame, now, neighbor_handler, free_frames, rx_return, tx_return) +} +``` + +**Step 2: Update IPv6 handler to pass `now`** + +In `src/net/handler/ipv6.rs`, change lines 222-224 from: + +```rust +IpProtocols::Tcp => { + tcp_handler.process_ipv6( + frame, payload_offset, neighbor_handler, free_frames, rx_return, tx_return, + ); +} +``` + +to: + +```rust +IpProtocols::Tcp => { + tcp_handler.process_ipv6_with_now( + frame, payload_offset, now, neighbor_handler, free_frames, rx_return, tx_return, + ); +} +``` + +**Step 3: Run tests** + +```bash +cargo test +``` + +Expected: all tests pass (behavioral no-op — same `Instant` type, just sourced from caller instead of inline). + +**Step 4: Commit** + +```bash +git add src/net/handler/ipv4.rs src/net/handler/ipv6.rs +git commit -m "perf(tcp): pass cached now through IPv4/IPv6 dispatch to TCP" +``` + +--- + +### Task 2: Remove no-`now` wrappers, rename `_with_now` to primary API + +**Files:** +- Modify: `src/net/handler/tcp/inbound.rs:57-74` (remove `process_ipv4`) +- Modify: `src/net/handler/tcp/inbound.rs:76-85` (rename `process_ipv4_with_now` → `process_ipv4`) +- Modify: `src/net/handler/tcp/inbound.rs:170-189` (remove `process_ipv6`) +- Modify: `src/net/handler/tcp/inbound.rs:191-200` (rename `process_ipv6_with_now` → `process_ipv6`) +- Modify: `src/net/handler/ipv4.rs` (update call from `process_ipv4_with_now` → `process_ipv4`) +- Modify: `src/net/handler/ipv6.rs` (update call from `process_ipv6_with_now` → `process_ipv6`) +- Modify: any test files that call `process_ipv4_with_now` or `process_ipv6_with_now` + +**Step 1: Remove the no-`now` `process_ipv4` wrapper (lines 57-74)** + +Delete the entire `process_ipv4` method that calls `Instant::now()`. + +**Step 2: Rename `process_ipv4_with_now` → `process_ipv4`** + +Change the method name and doc comment. The signature stays the same (with `now: Instant`). + +**Step 3: Remove the no-`now` `process_ipv6` wrapper (lines 170-189)** + +Delete the entire `process_ipv6` method that calls `Instant::now()`. + +**Step 4: Rename `process_ipv6_with_now` → `process_ipv6`** + +Change the method name and doc comment. The signature stays the same (with `now: Instant`). + +**Step 5: Update callers** + +- `src/net/handler/ipv4.rs`: `process_ipv4_with_now(` → `process_ipv4(` +- `src/net/handler/ipv6.rs`: `process_ipv6_with_now(` → `process_ipv6(` +- Search all test files under `src/net/handler/tcp/tests/` for `process_ipv4_with_now` and `process_ipv6_with_now` and rename to `process_ipv4` / `process_ipv6`. + +**Step 6: Run tests** + +```bash +cargo test +``` + +Expected: all pass. + +**Step 7: Commit** + +```bash +git add src/net/handler/tcp/inbound.rs src/net/handler/ipv4.rs src/net/handler/ipv6.rs src/net/handler/tcp/tests/ +git commit -m "refactor(tcp): rename process_*_with_now to process_*, remove wrappers" +``` + +--- + +### Task 3: Add `Tcb::ts_option()` helper, pre-compute tsval in inbound.rs + +**Files:** +- Modify: `src/net/handler/tcp/tcb.rs` (add helper method) +- Modify: `src/net/handler/tcp/inbound.rs` (replace ~21 inline tsval blocks) + +The pattern `let ts = if tcb.ts_enabled { let tsval = now.duration_since(tcb.ts_offset).as_millis() as u32; Some((tsval, tcb.ts_recent)) } else { None };` appears 21 times in inbound.rs alone. The `tsval` portion is constant for a given `(now, tcb.ts_offset)` pair. `ts_recent` varies as the segment is processed so it must be read at the point of use. + +**Step 1: Add `ts_option` helper to Tcb** + +In `src/net/handler/tcp/tcb.rs`, add to the `impl Tcb` block: + +```rust +/// Build the timestamp option tuple using a pre-computed tsval. +/// `ts_recent` is read at call time since it may change during segment processing. +#[inline(always)] +pub(super) fn ts_option(&self, tsval: u32) -> Option<(u32, u32)> { + if self.ts_enabled { + Some((tsval, self.ts_recent)) + } else { + None + } +} +``` + +**Step 2: Pre-compute tsval after connection lookup in process_segment** + +In `src/net/handler/tcp/inbound.rs`, after the connection lookup at line 314 (`if let Some(idx) = self.connections.iter().position(...)`), compute tsval once: + +```rust +let tsval = if self.connections[idx].ts_enabled { + now.duration_since(self.connections[idx].ts_offset).as_millis() as u32 +} else { + 0 +}; +``` + +Then thread `tsval` into `process_established`, `process_syn_sent`, `process_syn_received`, and `process_teardown` as an additional parameter. + +**Step 3: Replace all inline tsval blocks in inbound.rs** + +In each sub-function, replace every occurrence of: + +```rust +let ts = if tcb.ts_enabled { + let tsval = now.duration_since(tcb.ts_offset).as_millis() as u32; + Some((tsval, tcb.ts_recent)) +} else { + None +}; +``` + +with: + +```rust +let ts = tcb.ts_option(tsval); +``` + +Also replace the one variant at line 1274: + +```rust +let our_ts = now.duration_since(tcb.ts_offset).as_millis() as u32; +``` + +with just `tsval` (already computed). + +There are 21 occurrences in inbound.rs to replace. The exact lines are: 680-685, 730-733, 926-929, 967-970, 1062-1065, 1101-1104, 1141-1144, 1170-1173, 1274, 1346-1349, 1448-1451, 1490-1494, 1533-1537, 1574-1577, 1641-1644, 1680-1683, 1723-1726, 1752-1755, 1851-1854, 1901-1904, 1966-1969. + +**Step 4: Update sub-function signatures** + +Add `tsval: u32` parameter to: +- `process_established()` +- `process_syn_sent()` +- `process_syn_received()` +- `process_teardown()` + +**Step 5: Run tests** + +```bash +cargo test +``` + +Expected: all pass. + +**Step 6: Commit** + +```bash +git add src/net/handler/tcp/tcb.rs src/net/handler/tcp/inbound.rs +git commit -m "perf(tcp): pre-compute tsval once per segment, add Tcb::ts_option()" +``` + +--- + +### Task 4: Pre-compute tsval in transmit.rs and timers.rs + +**Files:** +- Modify: `src/net/handler/tcp/transmit.rs` (3 occurrences) +- Modify: `src/net/handler/tcp/timers.rs` (7 occurrences) + +**Step 1: Pre-compute tsval per-connection in transmit.rs poll_send** + +In the `for tcb in &mut self.connections` loop (line 23), compute tsval once at the top of the loop body after the state check: + +```rust +let tsval = if tcb.ts_enabled { + now.duration_since(tcb.ts_offset).as_millis() as u32 +} else { + 0 +}; +``` + +Replace the 3 inline computations at lines 96, 182, 259 with `tcb.ts_option(tsval)`. + +**Step 2: Pre-compute tsval per-connection in timers.rs poll_timers** + +In each timer pass loop (`for tcb in &mut self.connections` or `for (i, tcb) in ...`), compute tsval once at the top after the `continue` guard: + +```rust +let tsval = if tcb.ts_enabled { + now.duration_since(tcb.ts_offset).as_millis() as u32 +} else { + 0 +}; +``` + +Replace the 7 inline computations at lines 43, 106, 184, 256, 287, 317, 353 with `tcb.ts_option(tsval)`. + +**Step 3: Run tests** + +```bash +cargo test +``` + +Expected: all pass. + +**Step 4: Commit** + +```bash +git add src/net/handler/tcp/transmit.rs src/net/handler/tcp/timers.rs +git commit -m "perf(tcp): pre-compute tsval in transmit and timer loops" +``` + +--- + +### Task 5: Replace Vec allocations with stack arrays + +**Files:** +- Modify: `src/net/handler/tcp/inbound.rs:1498` (SACK blocks) +- Modify: `src/net/handler/tcp/timers.rs:77` (keep-alive removals) +- Modify: `src/net/handler/tcp/timers.rs:219` (RTO removals) + +**Step 1: Replace SACK blocks Vec with stack array** + +In `src/net/handler/tcp/inbound.rs`, replace at line 1497-1511: + +```rust +let max_blocks = if tcb.ts_enabled { 3 } else { 4 }; +let mut sack_blocks: Vec<(u32, u32)> = Vec::new(); +if tcb.sack_enabled { + sack_blocks.push((seg_seq, seg_seq.wrapping_add(payload_len as u32))); + for (&start, &len) in tcb.ooo_ranges.iter().rev() { + if sack_blocks.len() >= max_blocks { + break; + } + let end = start.wrapping_add(len); + if start != seg_seq { + sack_blocks.push((start, end)); + } + } +} +``` + +with: + +```rust +let max_blocks = if tcb.ts_enabled { 3 } else { 4 }; +let mut sack_buf = [(0u32, 0u32); 4]; +let mut sack_count = 0usize; +if tcb.sack_enabled { + sack_buf[0] = (seg_seq, seg_seq.wrapping_add(payload_len as u32)); + sack_count = 1; + for (&start, &len) in tcb.ooo_ranges.iter().rev() { + if sack_count >= max_blocks { + break; + } + let end = start.wrapping_add(len); + if start != seg_seq { + sack_buf[sack_count] = (start, end); + sack_count += 1; + } + } +} +let sack_blocks = &sack_buf[..sack_count]; +``` + +Update the `build_ack_with_sack` call to use `sack_blocks` (it already takes `&[(u32, u32)]` so this is a drop-in replacement). + +**Step 2: Replace keep-alive removals Vec with stack array** + +In `src/net/handler/tcp/timers.rs`, replace line 77: + +```rust +let mut keep_alive_removals: Vec = Vec::new(); +``` + +with: + +```rust +let mut keep_alive_removals = [0usize; 64]; +let mut keep_alive_removal_count = 0usize; +``` + +Replace `keep_alive_removals.push(i)` (line 96) with: + +```rust +if keep_alive_removal_count < 64 { + keep_alive_removals[keep_alive_removal_count] = i; + keep_alive_removal_count += 1; +} +``` + +Replace the removal loop (line 138) from: + +```rust +for idx in keep_alive_removals.into_iter().rev() { +``` + +to: + +```rust +for &idx in keep_alive_removals[..keep_alive_removal_count].iter().rev() { +``` + +**Step 3: Replace RTO removals Vec with stack array** + +In `src/net/handler/tcp/timers.rs`, replace line 219: + +```rust +let mut to_remove = Vec::new(); +``` + +with: + +```rust +let mut to_remove = [0usize; 64]; +let mut to_remove_count = 0usize; +``` + +Replace `to_remove.push(idx)` (line 243) with: + +```rust +if to_remove_count < 64 { + to_remove[to_remove_count] = idx; + to_remove_count += 1; +} +``` + +Find the removal loop that iterates `to_remove` in reverse and update it to use `to_remove[..to_remove_count].iter().rev()`. + +**Step 4: Run tests** + +```bash +cargo test +``` + +Expected: all pass. + +**Step 5: Commit** + +```bash +git add src/net/handler/tcp/inbound.rs src/net/handler/tcp/timers.rs +git commit -m "perf(tcp): replace Vec allocations with stack arrays in hot paths" +``` + +--- + +### Task 6: Pass `now` into active-open connection constructor + +**Files:** +- Modify: `src/net/handler/tcp/connection.rs:52-63` (add `now` parameter to `connect_with_config`) +- Modify: `src/net/handler/tcp/connection.rs:27-49` (add `now` parameter to `connect`) +- Modify: `src/net/socket/tcp.rs:183-221` (pass `now` from caller) + +**Step 1: Add `now: Instant` parameter to `connect_with_config`** + +In `src/net/handler/tcp/connection.rs`, add `now: Instant` after `dst_mac` in the `connect_with_config` signature. + +Replace the 4 `Instant::now()` calls inside: +- Line 99: `Instant::now() + coarsetime::Duration::from_millis(INITIAL_RTO_MS)` → `now + coarsetime::Duration::from_millis(INITIAL_RTO_MS)` +- Line 127: `last_activity: Instant::now()` → `last_activity: now` +- Line 133: `ts_recent_age: Instant::now()` → `ts_recent_age: now` +- Line 134: `ts_offset: Instant::now()` → `ts_offset: now` + +**Step 2: Add `now: Instant` parameter to `connect` wrapper** + +Pass it through to `connect_with_config`. + +**Step 3: Update callers in `src/net/socket/tcp.rs`** + +In `TcpStream::connect()` (line 190), change `Instant::now()` to a single `let now = Instant::now();` and pass it to both `neighbor_handler.lookup(now, ...)` and `handler.connect(..., now, ...)`. + +Same for `TcpStream::connect_with_config()` (line 244). + +Note: These are called once at connection setup, not per-packet. The `Instant::now()` here is acceptable but we consolidate to a single call for consistency. + +**Step 4: Run tests** + +```bash +cargo test +``` + +Expected: all pass. + +**Step 5: Commit** + +```bash +git add src/net/handler/tcp/connection.rs src/net/socket/tcp.rs +git commit -m "perf(tcp): pass now into active-open constructor, eliminate 4x Instant::now()" +``` + +--- + +### Task 7: Final verification + +**Step 1: Run full test suite** + +```bash +cargo test +``` + +Expected: all pass with zero behavioral changes. + +**Step 2: Verify no remaining hot-path `Instant::now()` calls** + +```bash +# Should only find test files and the ISN generator (cold path) +grep -rn "Instant::now()" src/net/handler/tcp/ --include="*.rs" | grep -v tests | grep -v isn.rs +``` + +Expected: only `connection.rs` `initiate_close` (cold path, linger timeout) should remain. + +**Step 3: Commit any final cleanup** + +--- + +## Summary of Eliminated Overhead + +| What | Before | After | +|------|--------|-------| +| `Instant::now()` per packet | 1 call per TCP segment | 0 (uses runtime cached `now`) | +| `duration_since().as_millis()` per segment | ~21 calls in inbound.rs | 1 per segment | +| `duration_since().as_millis()` in transmit | 3 per connection per tick | 1 per connection per tick | +| `duration_since().as_millis()` in timers | 7 per connection per tick | 1 per connection per tick | +| Vec allocation (SACK blocks) | 1 per OOO segment | 0 (stack `[_; 4]`) | +| Vec allocation (keep-alive removals) | 1 per timer tick | 0 (stack `[_; 64]`) | +| Vec allocation (RTO removals) | 1 per timer tick | 0 (stack `[_; 64]`) | +| `Instant::now()` per connect | 4 calls | 0 (uses passed `now`) | diff --git a/docs/plans/2026-03-08-tcp-throughput-design.md b/docs/plans/2026-03-08-tcp-throughput-design.md new file mode 100644 index 0000000..f26d8d3 --- /dev/null +++ b/docs/plans/2026-03-08-tcp-throughput-design.md @@ -0,0 +1,206 @@ +# TCP Throughput Optimization — Design + +**Date:** 2026-03-08 +**Goal:** Improve TCP echo throughput from ~560K pkt/s toward 2–5M pkt/s (64B payload over VETH pair) +**Context:** UDP echo achieves 11M pkt/s on the same setup. TCP will never match UDP due to +reliability guarantees, but a 20x gap indicates structural inefficiency. + +## Root Cause Analysis + +### Data Copy Overhead (4 copies per echo vs 0 for UDP) + +UDP echo calls `swap_addresses()` in-place and pushes the same frame to TX — zero copies. +TCP echo does 4 memcpys per round trip: + +1. **RX frame → recv_buffer** (`recv_buffer.write(payload)` in `process_established`) +2. **recv_buffer → user buf** (`recv_buffer.read(buf)` in `TcpRead::poll`) +3. **user buf → send_buffer** (`send_buffer.write(data)` in `TcpWrite::poll`) +4. **send_buffer → TX frame** (via `peek_slices` → `build_data_from_slices` in `poll_send`) + +Copies #2 and #3 are redundant for echo — data goes from one ring buffer to user stack and +immediately back to another ring buffer. + +### Redundant TCP Options Parsing + +`parse_timestamp(options)` is called 3 times in `process_established`: +- Line 998: PAWS check +- Line 1138: `ts_recent` update +- Line 1203: RTT measurement + +Each call re-scans the options byte array. Should be parsed once. + +### Instruction Cache Pressure + +`process_established` is ~556 lines with many rarely-taken branches (RST handling, SYN-in- +established, PAWS rejection, OOO data, SACK recovery). The hot path (in-order data + valid +ACK) executes ~20% of the code but the entire function must be loaded into L1i cache. + +### Connection Lookup in TcpRead/TcpWrite + +Every `TcpRead::poll` and `TcpWrite::poll` calls `get_connection_mut(&self.conn_id)` which +does a linear scan of `Vec`. For echo, this means 2 extra O(n) lookups per packet on +top of the inbound lookup. With n=1 the cost is small, but it's still unnecessary work. + +### Tcb Cache Line Layout + +The `Tcb` struct is ~500+ bytes (metadata, not counting ring buffer backing). Hot fields +(`rcv_nxt`, `snd_nxt`, `snd_una`, `ack_pending`, buffer metadata) are scattered across +multiple cache lines. Each access to a cold field evicts hot data from L1d. + +## Proposed Changes + +### 1. Consolidated TCP Options Parsing + +Parse all TCP options once at the top of `process_segment` after connection lookup, before +dispatching to `process_established`/etc. Store results in a stack struct: + +```rust +struct ParsedOptions { + timestamp: Option<(u32, u32)>, // (tsval, tsecr) + mss: Option, + window_scale: Option, + sack_permitted: bool, + sack_blocks: ([(u32, u32); 4], usize), // (blocks, count) +} +``` + +Pass `&ParsedOptions` to all state handlers. Eliminates 2 redundant `parse_timestamp` +calls per established-state packet. + +### 2. Fast-Path for Established State + +Add a fast-path branch at the top of `process_established` for the common case: +- ACK set, no RST/SYN/FIN flags +- In-order data (seg_seq == rcv_nxt) +- Valid new ACK (snd_una < seg_ack <= snd_nxt) +- No PAWS rejection +- Not in recovery + +The fast path combines ACK advancement + data write + delayed ACK logic in a single +streamlined block, skipping the 11+ branch checks in the slow path. The slow path +remains for correctness on rare cases. + +Estimated savings: ~100–150ns per packet. + +### 3. RingBuffer::transfer() for Splice + +Add a method to transfer data directly between ring buffers: + +```rust +impl RingBuffer { + /// Transfer up to `max_len` bytes from self to `dst` without intermediate copy. + /// Returns the number of bytes transferred. + pub fn transfer(&mut self, dst: &mut RingBuffer, max_len: usize) -> usize; +} +``` + +And a corresponding `TcpStream::splice()` API: + +```rust +impl TcpStream { + /// Transfer data from recv_buffer directly to send_buffer. + /// Eliminates 2 copies vs read() + write(). + pub fn splice(&self, max_len: usize) -> Splice<'_>; +} +``` + +This eliminates copies #2 and #3 for echo-like workloads. The echo server becomes: +```rust +loop { + let n = stream.splice(65535).await?; + stats.update(n, false); +} +``` + +### 4. Tcb Hot/Cold Field Splitting + +Reorder `Tcb` fields to group frequently-accessed fields in the first 2 cache lines (128B): + +**Cache line 1 (hot — accessed every packet):** +`rcv_nxt`, `snd_nxt`, `snd_una`, `snd_wnd`, `state`, `ack_pending`, `ack_delay_count`, +`ts_enabled`, `sack_enabled`, `ecn_enabled`, `eff_snd_mss` + +**Cache line 2 (warm — accessed most packets):** +`recv_buffer` metadata (head, tail, len, mask — not the backing Vec ptr), +`send_buffer` metadata, `id` (for equality checks), `ts_recent`, `delayed_ack_deadline` + +**Remaining (cold — rarely accessed):** +Keep-alive fields, linger, time_wait, congestion control state, recovery state, +SACK scoreboard, OOO ranges, F-RTO state + +### 5. Eliminate Connection Lookup in Socket API + +Cache a connection index in `TcpStream` and validate on access: + +```rust +struct TcpStream { + conn_id: ConnectionId, + cached_idx: Cell, // cached index, revalidated on use + // ... +} +``` + +On `TcpRead::poll` / `TcpWrite::poll`, check if `connections[cached_idx].id == conn_id`. +If yes, skip the linear scan. If not (connection moved due to removal), fall back to +linear scan and update cache. For echo with n=1, this saves 2 linear scans per packet. + +### 6. Separate Hot/Cold Paths with #[inline] Hints + +Mark rarely-taken branches in `process_established` with `#[cold]` or extract them into +`#[inline(never)]` helper functions: + +- RST handling → `handle_rst_in_established()` +- SYN-in-established (challenge ACK) → `handle_syn_in_established()` +- PAWS rejection → `handle_paws_rejection()` +- Out-of-order data → `handle_ooo_data()` +- Duplicate data → `handle_duplicate_data()` +- SACK recovery entry → `enter_sack_recovery()` + +The hot path (in-order data + valid ACK) stays inline. Cold paths are outlined to reduce +instruction cache pressure. + +### 7. Pre-compute Immutable Per-Connection Values + +Several values are recomputed on every packet but don't change within a connection's +established state: +- `ack_flags` (depends on `ecn_ce_received` which changes rarely) +- `id` fields (never change) +- Timestamp option offset in the options buffer (fixed after SYN exchange) + +Cache these in the Tcb or compute once per `process_segment` call. + +## Performance Budget + +At 2M pkt/s per side: 500ns per packet. Minus XDP baseline (~45ns): **455ns for TCP.** + +| Component | Current (est.) | After optimization | Savings | +|-----------|---------------|-------------------|---------| +| process_established | 300–400ns | 150–200ns | 150ns | +| Ring buffer copies (4→2) | 60ns | 30ns | 30ns | +| Options parsing (3→1) | 15ns | 5ns | 10ns | +| Socket API lookups (2→0) | 10ns | 2ns | 8ns | +| poll_send | 96ns | 80ns | 16ns | +| Segment building | 100ns | 90ns | 10ns | +| Tcb cache improvement | — | — | 50–100ns | +| **Total** | **~805ns** | **~400–460ns** | **~360ns** | + +Expected result: **1.5–2.5M pkt/s** (3–4.5x improvement). + +## Out of Scope + +- **Connection lookup optimization (Vec → HashMap)** — Deferred; n=1 for echo benchmark. +- **Zero-copy frame echo** — Would require rewriting headers in-place on the RX frame + and resubmitting it, breaking the frame ownership model. Not worth the complexity. +- **Async runtime overhead** — The no-op waker + poll pattern is already minimal. +- **CUBIC optimization** — Floating-point arithmetic only runs in congestion avoidance; + echo benchmark stays in slow start (no losses on VETH). + +## Implementation Order + +1. Consolidated options parsing (low risk, clear improvement) — DONE +2. Fast-path in process_established (medium risk, biggest improvement) +3. RingBuffer::transfer() + TcpStream::splice() (medium risk, API addition) +4. Tcb field reordering (low risk, cache improvement) +5. Connection index caching in TcpStream (low risk) +6. Cold path extraction with #[inline(never)] (low risk) +7. Pre-compute immutable values (low risk) diff --git a/docs/plans/2026-03-08-tcp-throughput-impl.md b/docs/plans/2026-03-08-tcp-throughput-impl.md new file mode 100644 index 0000000..eb1b299 --- /dev/null +++ b/docs/plans/2026-03-08-tcp-throughput-impl.md @@ -0,0 +1,731 @@ +# TCP Throughput Optimization — Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Improve TCP echo throughput from ~560K pkt/s toward 2–5M pkt/s over VETH + +**Architecture:** Consolidate option parsing, add fast-path for established state, splice +recv↔send buffers, skip checksums on VETH, reorder Tcb for cache locality, cache connection +indices, outline cold paths. + +**Tech Stack:** Rust, AF_XDP, coarsetime, zero-copy frame buffers + +**Design:** See `docs/plans/2026-03-08-tcp-throughput-design.md` + +--- + +### Task 1: Consolidated TCP Options Parsing + +**Files:** +- Create: `src/net/handler/tcp/options.rs` +- Modify: `src/net/handler/tcp/mod.rs` +- Modify: `src/net/handler/tcp/inbound.rs` +- Test: `src/net/handler/tcp/tests/options.rs` + +**Context:** +`parse_timestamp(options)` is called 3 times in `process_established` (PAWS check at ~998, +ts_recent update at ~1138, RTT measurement at ~1203). Each re-scans the options bytes. +SACK blocks are parsed separately in the ACK processing section. All option parsing should +happen once. + +**Step 1: Create the ParsedOptions struct** + +Create `src/net/handler/tcp/options.rs`: + +```rust +use crate::net::wire::tcp::{parse_timestamp, parse_mss, parse_window_scale, parse_sack_permitted, parse_sack_blocks}; + +/// Pre-parsed TCP options. Computed once per segment, passed by reference. +#[derive(Debug, Clone, Copy)] +pub struct ParsedOptions { + pub timestamp: Option<(u32, u32)>, + pub mss: Option, + pub window_scale: Option, + pub sack_permitted: bool, + pub sack_blocks: ([Option<(u32, u32)>; 4], usize), +} + +impl ParsedOptions { + /// Parse all TCP options from a raw options byte slice. + #[inline] + pub fn parse(options: &[u8]) -> Self { + Self { + timestamp: parse_timestamp(options), + mss: parse_mss(options), + window_scale: parse_window_scale(options), + sack_permitted: parse_sack_permitted(options), + sack_blocks: parse_sack_blocks(options), + } + } + + /// Fast parse — only extracts timestamp (for established state where + /// MSS/wscale/sack_permitted are already negotiated). + #[inline] + pub fn parse_established(options: &[u8]) -> Self { + Self { + timestamp: parse_timestamp(options), + mss: None, + window_scale: None, + sack_permitted: false, + sack_blocks: parse_sack_blocks(options), + } + } +} +``` + +Add `pub(crate) mod options;` to `mod.rs`. + +**Step 2: Write tests** + +Create `src/net/handler/tcp/tests/options.rs` with unit tests for `ParsedOptions::parse()`: +- Empty options → all None/false/empty +- Options with timestamp → timestamp is Some +- Options with SACK blocks → sack_blocks populated +- Options with all fields → all populated + +**Step 3: Thread ParsedOptions through process_segment** + +In `inbound.rs`, change `process_segment` to parse options once: + +```rust +// After connection lookup, before state dispatch: +let opts = if state == TcpState::Established || state.is_teardown() { + ParsedOptions::parse_established(options) +} else { + ParsedOptions::parse(options) +}; +``` + +Pass `&opts` (instead of `options: &[u8]`) to `process_established`, `process_syn_sent`, +`process_syn_received`, `process_teardown`. + +**Step 4: Replace all inline parse calls in process_established** + +Replace every `parse_timestamp(options)` call with `opts.timestamp`. Replace +`parse_sack_blocks(options)` calls with `opts.sack_blocks`. The `options: &[u8]` parameter +is no longer needed in these functions. + +**Step 5: Update test files** + +Update all test files that call `process_established`, `process_syn_sent`, etc. to pass +`ParsedOptions` instead of raw `&[u8]`. + +**Step 6: Verify and commit** + +Run: `cargo test` +Expected: All tests pass. + +```bash +git add src/net/handler/tcp/options.rs src/net/handler/tcp/mod.rs src/net/handler/tcp/inbound.rs src/net/handler/tcp/tests/ +git commit -m "perf(tcp): consolidate option parsing into ParsedOptions struct" +``` + +--- + +### Task 2: Checksum Bypass for VETH/Loopback + +**Files:** +- Modify: `src/rt/local.rs` (LocalRuntimeBuilder + LocalRuntime) +- Modify: `src/net/handler/tcp/handler.rs` (TcpHandler) +- Modify: `src/net/handler/tcp/inbound.rs` (process_ipv4, process_ipv6) + +**Context:** +VETH pairs report checksum offload as disabled, causing software checksum verification on +every RX packet and computation on every TX packet. VETH frames can't be corrupted, so RX +verification is pure waste. TX checksums can optionally be skipped when both endpoints are +ours (benchmarking). + +**Step 1: Add skip_rx_checksum flag to LocalRuntimeBuilder** + +In `src/rt/local.rs`, add a `skip_rx_checksum: bool` field to `LocalRuntimeBuilder` and a +builder method: + +```rust +pub fn skip_rx_checksum(mut self, skip: bool) -> Self { + self.skip_rx_checksum = skip; + self +} +``` + +Thread this through to `TcpHandler::new()` (and the IPv4/IPv6 handlers if they also verify +TCP checksums). The flag overrides `rx_offload` for TCP — when `skip_rx_checksum` is true, +TCP checksum verification is skipped regardless of hardware capability. + +**Step 2: Plumb through TcpHandler** + +In `handler.rs`, TcpHandler already has `rx_offload: bool`. When `skip_rx_checksum` is true, +set `rx_offload = true` effectively skipping verification. + +Alternative (cleaner): add a separate `skip_rx_checksum` field and check +`self.rx_offload || self.skip_rx_checksum` in the checksum verification paths of +`process_ipv4` and `process_ipv6`. + +**Step 3: Update echo examples** + +Add `.skip_rx_checksum(true)` to both `tcp-echo-server.rs` and `tcp-echo-client.rs` runtime +builders (or add it as a CLI flag `--skip-rx-checksum`). + +**Step 4: Verify and commit** + +Run: `cargo test` +Expected: All tests pass. + +```bash +git add src/rt/local.rs src/net/handler/tcp/handler.rs src/net/handler/tcp/inbound.rs examples/ +git commit -m "perf(tcp): add skip_rx_checksum option for VETH/loopback" +``` + +--- + +### Task 3: Fast-Path in process_established + +**Files:** +- Modify: `src/net/handler/tcp/inbound.rs` + +**Context:** +`process_established` is ~556 lines with 11+ branch checks for rare cases (RST, SYN, +PAWS rejection, OOO data). The common case for echo is: ACK set, no special flags, in-order +data, valid new ACK. A fast-path at the top of the function handles this case with minimal +branches. + +**Step 1: Add fast-path check at top of process_established** + +After the existing ECN check (lines 978–994), add: + +```rust +// Fast-path: common case — in-order data + valid new ACK, no special flags. +let fast_path = { + let tcb = &self.connections[idx]; + (seg_flags & (flags::RST | flags::SYN | flags::FIN)) == 0 + && seg_flags & flags::ACK != 0 + && payload_len > 0 + && seg_seq == tcb.rcv_nxt + && seq_lt(tcb.snd_una, seg_ack) + && seq_le(seg_ack, tcb.snd_nxt) + && !tcb.recovery.in_recovery + && tcb.ooo_ranges.is_empty() // no pending OOO reassembly +}; +``` + +**Step 2: Implement the fast-path body** + +When `fast_path` is true, execute a streamlined version that combines: + +1. **PAWS check** (inline, using pre-parsed timestamp from `opts`): + - If ts_enabled and timestamp present, check `tsval >= ts_recent` (signed comparison) + - On PAWS failure, fall through to slow path + - On success, update `ts_recent` and `ts_recent_age` + +2. **Segment acceptability** (simplified — seg_seq == rcv_nxt and we know payload_len > 0): + - Just check `rcv_nxt + rcv_wnd > seg_seq` (always true if window > 0) + +3. **ACK advancement**: + - `bytes_acked = seg_ack - snd_una` + - `snd_una = seg_ack` + - `send_buffer.advance(bytes_acked)` + - Reset keep-alive timer + - Slow-start cwnd update: `cwnd += mss` (if cwnd < ssthresh) + - Reset dup_ack_count + - RTT measurement (using pre-parsed timestamp) + - Window update + - Retransmit timer: if `snd_una == snd_nxt`, clear deadline + +4. **Data write**: + - `recv_buffer.write(payload)` — in-order, no OOO handling needed + - `rcv_nxt += written` + +5. **Delayed ACK**: + - Increment ack_delay_count + - If >= MAX_DELAYED_ACK_COUNT, send ACK immediately + - Otherwise set ack_pending + deadline + +6. **Return** (skip slow path entirely) + +If PAWS fails or any other condition isn't met, fall through to the existing slow path +(the entire current function body). + +**Step 3: Extract slow-path helpers** + +Move rarely-taken branches into `#[inline(never)]` helper functions to reduce code size +of the hot path: + +```rust +#[inline(never)] +fn handle_rst_in_established(&mut self, idx: usize, ...) { ... } + +#[inline(never)] +fn handle_syn_in_established(&mut self, idx: usize, ...) { ... } + +#[inline(never)] +fn handle_ooo_data(&mut self, idx: usize, ...) { ... } +``` + +**Step 4: Verify and commit** + +Run: `cargo test` +Expected: All tests pass. + +```bash +git add src/net/handler/tcp/inbound.rs +git commit -m "perf(tcp): add fast-path for common established-state processing" +``` + +--- + +### Task 4: RingBuffer::transfer() and TcpStream::splice() + +**Files:** +- Modify: `src/net/handler/tcp/ring_buffer.rs` +- Modify: `src/net/socket/tcp.rs` +- Modify: `examples/tcp-echo-server.rs` +- Test: `src/net/handler/tcp/tests/` (ring_buffer tests) + +**Context:** +The echo server does `read()` → `write()` which copies data through a user buffer (2 extra +copies). `RingBuffer::transfer()` moves data directly between ring buffers. + +**Step 1: Implement RingBuffer::transfer()** + +In `ring_buffer.rs`: + +```rust +/// Transfer up to `max_len` bytes from self (as source) to `dst` (as destination). +/// Reads from self's head, writes to dst's tail. Returns bytes transferred. +/// This is equivalent to `read()` + `write()` but avoids the intermediate buffer. +#[inline] +pub fn transfer(&mut self, dst: &mut RingBuffer, max_len: usize) -> usize { + let available = self.available().min(max_len); + let to_transfer = available.min(dst.free_space()); + if to_transfer == 0 { + return 0; + } + // Get source slices (may wrap). + let (s1, s2) = self.peek_slices(0, to_transfer); + // Write to destination. + let wrote1 = dst.write(s1); + let wrote2 = if !s2.is_empty() { dst.write(s2) } else { 0 }; + let total = wrote1 + wrote2; + // Advance source. + self.advance(total); + total +} +``` + +**Step 2: Write tests for transfer()** + +Add tests to the ring_buffer tests: +- Transfer between empty/full buffers +- Transfer with wrap-around on source +- Transfer with wrap-around on destination +- Transfer with both wrapping +- Partial transfer when destination is nearly full + +**Step 3: Add TcpStream::splice()** + +In `tcp.rs`: + +```rust +/// Transfer data from recv_buffer directly to send_buffer, avoiding the +/// intermediate user buffer copy. Returns a future that resolves when at +/// least 1 byte has been transferred. +pub fn splice(&self, max_len: usize) -> TcpSplice<'_> { + TcpSplice { + handler: &self.handler, + conn_id: self.conn_id, + event_queue: &self.event_queue, + max_len, + } +} +``` + +```rust +pub struct TcpSplice<'stream> { + handler: &'stream Rc>, + conn_id: ConnectionId, + event_queue: &'stream LocalQueue, + max_len: usize, +} + +impl<'stream> Future for TcpSplice<'stream> { + type Output = Result; + + fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll { + let this = self.get_mut(); + // Check errors + while let Some(event) = this.event_queue.pop() { + match event { + TcpEvent::Reset => return Poll::Ready(Err(TcpError::Reset)), + TcpEvent::Timeout => return Poll::Ready(Err(TcpError::Timeout)), + TcpEvent::RemoteClose => { + // Check if there's still data to drain before returning EOF + } + _ => {} + } + } + let handler = unsafe { &mut *this.handler.get() }; + if let Some(tcb) = handler.get_connection_mut(&this.conn_id) { + let n = tcb.recv_buffer.transfer(&mut tcb.send_buffer, this.max_len); + if n > 0 { + Poll::Ready(Ok(n)) + } else if tcb.state.is_remote_closed() { + Poll::Ready(Ok(0)) + } else { + Poll::Pending + } + } else { + Poll::Ready(Err(TcpError::NotConnected)) + } + } +} +``` + +**Step 4: Update echo server example** + +Change `tcp-echo-server.rs` to use splice: + +```rust +loop { + match stream.splice(65535).await { + Ok(0) => { + println!("Disconnected from {}:{}", stream.remote_addr(), stream.remote_port()); + break; + } + Ok(n) => { + stats.update(n, false); + stats.maybe_print(); + } + Err(e) => { + println!("Splice error: {:?}", e); + break; + } + } +} +``` + +**Step 5: Verify and commit** + +Run: `cargo test` +Expected: All tests pass. + +```bash +git add src/net/handler/tcp/ring_buffer.rs src/net/socket/tcp.rs examples/tcp-echo-server.rs +git commit -m "perf(tcp): add RingBuffer::transfer() and TcpStream::splice() for zero-copy echo" +``` + +--- + +### Task 5: Tcb Field Reordering for Cache Locality + +**Files:** +- Modify: `src/net/handler/tcp/tcb.rs` + +**Context:** +The `Tcb` struct is ~500+ bytes. Hot fields are scattered across cache lines. Reorder fields +so the most-accessed ones share cache lines. + +**Step 1: Reorder Tcb fields** + +Group fields by access frequency: + +```rust +pub struct Tcb { + // === Cache line 1: accessed every packet === + pub rcv_nxt: u32, + pub snd_nxt: u32, + pub snd_una: u32, + pub snd_wnd: u32, + pub state: TcpState, + pub ack_pending: bool, + pub ack_delay_count: u8, + pub ts_enabled: bool, + pub sack_enabled: bool, + pub ecn_enabled: bool, + pub ecn_ce_received: bool, + pub wscale_enabled: bool, + pub eff_snd_mss: u16, + pub snd_wscale: u8, + pub rcv_wscale: u8, + pub rcv_mss: u16, + pub nagle_enabled: bool, + pub from_passive_open: bool, + _pad1: [u8; 2], // align to 64 bytes + + // === Cache line 2: accessed most packets === + pub id: ConnectionId, + pub ts_recent: u32, + pub delayed_ack_deadline: Option, + pub delayed_ack_ms: u64, + + // === Cache line 3+: buffers (accessed for data) === + pub recv_buffer: RingBuffer, + pub send_buffer: RingBuffer, + + // === Warm: accessed on ACKs === + pub snd_wl1: u32, + pub snd_wl2: u32, + pub max_snd_wnd: u32, + pub last_advertised_right_edge: u32, + pub iss: u32, + pub irs: u32, + pub rcv_wnd: u32, + pub snd_mss: u16, + + // === Congestion/recovery (accessed on ACKs, not data-only) === + pub cubic: CubicState, + pub recovery: SackRecovery, + pub prr: PrrState, + pub frto: FRtoState, + + // === RTT (accessed on ACKs with timestamps) === + pub srtt: Option, + pub rttvar: u64, + pub rto: u64, + pub last_send_time: Option, + pub ts_recent_age: Instant, + pub ts_offset: Instant, + + // === Cold: rarely accessed === + pub retransmit_deadline: Option, + pub rto_backoff: u8, + pub event_queue: LocalQueue, + pub ooo_ranges: BTreeMap, + pub sack_scoreboard: BTreeMap, + pub ecn_cwr_sent: bool, + pub pending_fin: bool, + pub fin_seq: Option, + pub time_wait_deadline: Option, + pub time_wait_duration: u64, + pub persist_deadline: Option, + pub persist_backoff: u8, + pub keep_alive_enabled: bool, + pub keep_alive_idle_ms: u64, + pub keep_alive_interval_ms: u64, + pub keep_alive_count: u8, + pub last_activity: Instant, + pub keep_alive_probes_sent: u8, + pub linger: Option, + pub linger_deadline: Option, +} +``` + +Note: exact layout depends on alignment and padding. Use `#[repr(C)]` to control layout, +or verify with `std::mem::offset_of!` that hot fields share cache lines. + +**Step 2: Update all Tcb construction sites** + +Update `make_tcb()` in `tcb.rs` tests, `connect` / `connect_with_config` in `connection.rs`, +passive-open constructor in `inbound.rs`, and all test helpers to use the new field order. + +**Step 3: Verify and commit** + +Run: `cargo test` +Expected: All tests pass. + +```bash +git add src/net/handler/tcp/tcb.rs src/net/handler/tcp/connection.rs src/net/handler/tcp/inbound.rs src/net/handler/tcp/tests/ +git commit -m "perf(tcp): reorder Tcb fields for cache locality" +``` + +--- + +### Task 6: Cache Connection Index in TcpStream + +**Files:** +- Modify: `src/net/socket/tcp.rs` +- Modify: `src/net/handler/tcp/handler.rs` + +**Context:** +`TcpRead::poll` and `TcpWrite::poll` each call `get_connection_mut(&conn_id)` which does +O(n) linear scan. For echo, that's 2 extra scans per packet. Cache the index and validate. + +**Step 1: Add index-based lookup to TcpHandler** + +In `handler.rs`: + +```rust +/// Get a mutable reference by index, validating the connection ID matches. +/// Returns None if index is out of bounds or ID doesn't match. +#[inline] +pub fn get_connection_by_idx(&mut self, idx: usize, id: &ConnectionId) -> Option<&mut Tcb> { + if let Some(tcb) = self.connections.get_mut(idx) { + if tcb.id == *id { + return Some(tcb); + } + } + // Fallback to linear scan. + self.get_connection_mut(id) +} + +/// Find the index of a connection by ID. +pub fn find_connection_idx(&self, id: &ConnectionId) -> Option { + self.connections.iter().position(|c| c.id == *id) +} +``` + +**Step 2: Add cached index to TcpStream** + +```rust +use std::cell::Cell; + +pub struct TcpStream { + conn_id: ConnectionId, + event_queue: LocalQueue, + handler: Rc>, + cached_idx: Cell, + closed: bool, + write_closed: bool, +} +``` + +Initialize `cached_idx` from `find_connection_idx` in `from_accepted` and `Connect::poll`. + +**Step 3: Use cached index in TcpRead/TcpWrite** + +```rust +// In TcpRead::poll: +let handler = unsafe { &mut *this.handler.get() }; +let idx = this.cached_idx.get(); +if let Some(tcb) = handler.get_connection_by_idx(idx, &this.conn_id) { + // ... use tcb +} else if let Some(new_idx) = handler.find_connection_idx(&this.conn_id) { + this.cached_idx.set(new_idx); + // ... use connections[new_idx] +} else { + return Poll::Ready(Err(TcpError::NotConnected)); +} +``` + +Wait — `TcpRead` borrows `&self` from `TcpStream` but `cached_idx` is a `Cell`. +We need to pass the Cell by reference. Add `cached_idx: &'stream Cell` to +`TcpRead` and `TcpWrite`. + +**Step 4: Verify and commit** + +Run: `cargo test` +Expected: All tests pass. + +```bash +git add src/net/socket/tcp.rs src/net/handler/tcp/handler.rs +git commit -m "perf(tcp): cache connection index in TcpStream to avoid linear scan" +``` + +--- + +### Task 7: Cold Path Extraction + +**Files:** +- Modify: `src/net/handler/tcp/inbound.rs` + +**Context:** +Rarely-taken branches in `process_established` pollute the instruction cache. Extract them +into `#[inline(never)]` functions. + +**Step 1: Extract cold helpers** + +Create these `#[inline(never)]` methods on `TcpHandler`: + +```rust +/// Handle RST in established state (RFC 5961). +#[inline(never)] +fn handle_rst_in_established<'umem>( + &mut self, idx: usize, seg_seq: u32, tsval: u32, ack_flags: u8, + src_mac: MacAddress, dst_mac: MacAddress, + free_frames: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, +) -> bool; // returns true if connection was removed + +/// Handle SYN in synchronized state (challenge ACK). +#[inline(never)] +fn send_challenge_ack<'umem>( + &mut self, idx: usize, tsval: u32, ack_flags: u8, + src_mac: MacAddress, dst_mac: MacAddress, + free_frames: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, +); + +/// Handle out-of-order data segment. +#[inline(never)] +fn handle_ooo_data<'umem>( + &mut self, idx: usize, frame: &Frame<'umem>, payload_offset: usize, + payload_len: usize, seg_seq: u32, tsval: u32, ack_flags: u8, + src_mac: MacAddress, dst_mac: MacAddress, + free_frames: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, +); + +/// Enter SACK recovery on 3rd duplicate ACK. +#[inline(never)] +fn enter_sack_recovery(&mut self, idx: usize); +``` + +**Step 2: Replace inline code with helper calls** + +In `process_established`, replace the RST handling block (~30 lines) with: +```rust +if seg_flags & flags::RST != 0 { + if self.handle_rst_in_established(idx, seg_seq, tsval, ack_flags, src_mac, dst_mac, free_frames, tx_return) { + rx_return.push(frame); + return; + } + // Challenge ACK already sent by handler + rx_return.push(frame); + return; +} +``` + +Similarly for SYN check, OOO data, and SACK recovery entry. + +**Step 3: Verify and commit** + +Run: `cargo test` +Expected: All tests pass. + +```bash +git add src/net/handler/tcp/inbound.rs +git commit -m "perf(tcp): extract cold paths to #[inline(never)] functions" +``` + +--- + +### Task 8: Verification and Benchmarking + +**Files:** +- No code changes — verification only + +**Step 1: Run full test suite** + +```bash +cargo test +``` + +Expected: All tests pass. + +**Step 2: Check for remaining hot-path inefficiencies** + +Search for any remaining `parse_timestamp` or `parse_sack_blocks` calls in inbound.rs +that should use `ParsedOptions` instead. + +Search for any `Instant::now()` calls in hot paths. + +Search for any heap allocations (`Vec::new`, `Box`, `BTreeMap::new`) in hot paths. + +**Step 3: Benchmark** + +Run the TCP echo benchmark: + +Terminal 1 (server namespace): +```bash +cargo run --release --example tcp-echo-server -- --if-name veth-server --queue 0 +``` + +Terminal 2 (client namespace): +```bash +cargo run --release --example tcp-echo-client -- --if-name veth-client --queue 0 -m 64 +``` + +Record pkt/s and compare against the 560K baseline. + +**Step 4: Document results** + +If pkt/s improved significantly, note the result. If not at target (2M+), identify +the next bottleneck for a follow-up optimization pass. diff --git a/examples/common/mod.rs b/examples/common/mod.rs index 170dbb2..5eeaee0 100644 --- a/examples/common/mod.rs +++ b/examples/common/mod.rs @@ -18,7 +18,7 @@ use pnet::{ }, util::MacAddr, }; -use rand::RngCore; +use rand::Rng; pub struct Stats { pub id: Option, @@ -29,6 +29,7 @@ pub struct Stats { pub last_fragments_received: u64, pub last_bytes_received: u64, pub last_time: u64, + pub packets_per_print: u64, } impl Stats { @@ -45,8 +46,17 @@ impl Stats { .duration_since(UNIX_EPOCH) .unwrap() .as_nanos() as u64, + packets_per_print: 20_000_000, } } + + pub fn new_with_packets_per_print(packets_per_print: u64) -> Self { + Self { + packets_per_print, + ..Self::new() + } + } + pub fn new_with_id(id: usize) -> Self { Self { id: Some(id), @@ -71,8 +81,7 @@ impl Stats { #[inline(always)] pub fn maybe_print(&mut self) { - const PACKETS_PER_PRINT: u64 = 20_000_000; - if self.packets_received - self.last_packets_received < PACKETS_PER_PRINT { + if self.packets_received - self.last_packets_received < self.packets_per_print { return; } @@ -125,6 +134,7 @@ impl Stats { } } +#[inline(always)] pub fn swap_addresses(frame: &mut [u8]) -> Option<()> { let mut ether = MutableEthernetPacket::new(frame)?; @@ -309,7 +319,7 @@ pub struct BaseArgs { pub unaligned: bool, #[arg( long, - default_value = "2048", + default_value = "4096", help = "The number of slots in the RX ring in the socket." )] pub rx_ring_size: u32, diff --git a/examples/std-tcp-echo-client.rs b/examples/std-tcp-echo-client.rs new file mode 100644 index 0000000..0fad81f --- /dev/null +++ b/examples/std-tcp-echo-client.rs @@ -0,0 +1,147 @@ +use std::{ + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, + time::{SystemTime, UNIX_EPOCH}, +}; + +use clap::Parser; +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::TcpStream, +}; + +#[derive(Parser)] +#[command(author, version, about = "Standard TCP echo client baseline")] +struct Args { + #[arg(short, long, default_value = "[::1]:8080")] + remote_addr: String, + #[arg(short, long, default_value = "64")] + message_size: usize, +} + +struct Stats { + bytes: u64, + packets: u64, + last_bytes: u64, + last_packets: u64, + last_time: u64, + packets_per_print: u64, +} + +impl Stats { + fn new(packets_per_print: u64) -> Self { + Self { + bytes: 0, + packets: 0, + last_bytes: 0, + last_packets: 0, + last_time: SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() as u64, + packets_per_print, + } + } + + #[inline(always)] + fn update(&mut self, bytes: usize) { + self.bytes += bytes as u64; + self.packets += 1; + } + + #[inline(always)] + fn maybe_print(&mut self) { + if self.packets - self.last_packets < self.packets_per_print { + return; + } + + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() as u64; + let elapsed = (now - self.last_time) as f64 / 1_000_000_000.0; + + let pps = (self.packets - self.last_packets) as f64 / elapsed; + let bps = (self.bytes - self.last_bytes) as f64 / elapsed; + + println!( + "Packets: {}K | Bytes: {:.2}GiB | Packet rate: {:.2} Kpps | Byte rate: {:.2} Gbps", + self.packets / 1_000, + self.bytes as f64 / 1024.0 / 1024.0 / 1024.0, + pps / 1_000.0, + (bps / 1024.0 / 1024.0 / 1024.0) * 8.0 + ); + + self.last_packets = self.packets; + self.last_bytes = self.bytes; + self.last_time = now; + } +} + +fn main() { + let args = Args::parse(); + + let exit = Arc::new(AtomicBool::new(false)); + ctrlc::set_handler({ + let exit = exit.clone(); + move || { + exit.store(true, Ordering::Relaxed); + } + }) + .expect("Error setting Ctrl-C handler"); + + let rt = tokio::runtime::Builder::new_current_thread() + .enable_io() + .build() + .expect("Failed to create tokio runtime"); + + rt.block_on(async move { + println!("Connecting to {}...", args.remote_addr); + + let mut stream = TcpStream::connect(&args.remote_addr) + .await + .expect("Failed to connect"); + + stream.set_nodelay(true).ok(); + + println!("Connected to {}", args.remote_addr); + + let payload = vec![0xABu8; args.message_size]; + let mut read_buf = vec![0u8; args.message_size]; + let mut stats = Stats::new(100_000); + + loop { + if exit.load(Ordering::Relaxed) { + break; + } + + if let Err(e) = stream.write_all(&payload).await { + println!("Write error: {:?}", e); + break; + } + + let mut total_read = 0; + while total_read < args.message_size { + let n = match stream.read(&mut read_buf[total_read..]).await { + Ok(0) => { + println!("Server closed connection"); + return; + } + Ok(n) => n, + Err(e) => { + println!("Read error: {:?}", e); + return; + } + }; + total_read += n; + } + + stats.update(total_read); + stats.maybe_print(); + } + }); + + println!("Exiting..."); +} diff --git a/examples/std-tcp-echo-server.rs b/examples/std-tcp-echo-server.rs new file mode 100644 index 0000000..8e7aedd --- /dev/null +++ b/examples/std-tcp-echo-server.rs @@ -0,0 +1,140 @@ +use std::{ + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, + time::{SystemTime, UNIX_EPOCH}, +}; + +use clap::Parser; +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::TcpListener, +}; + +#[derive(Parser)] +#[command(author, version, about = "Standard TCP echo server baseline")] +struct Args { + #[arg(short, long, default_value = "[::1]:8080")] + listen_addr: String, +} + +struct Stats { + bytes: u64, + packets: u64, + last_bytes: u64, + last_packets: u64, + last_time: u64, + packets_per_print: u64, +} + +impl Stats { + fn new(packets_per_print: u64) -> Self { + Self { + bytes: 0, + packets: 0, + last_bytes: 0, + last_packets: 0, + last_time: SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() as u64, + packets_per_print, + } + } + + #[inline(always)] + fn update(&mut self, bytes: usize) { + self.bytes += bytes as u64; + self.packets += 1; + } + + #[inline(always)] + fn maybe_print(&mut self) { + if self.packets - self.last_packets < self.packets_per_print { + return; + } + + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() as u64; + let elapsed = (now - self.last_time) as f64 / 1_000_000_000.0; + + let pps = (self.packets - self.last_packets) as f64 / elapsed; + let bps = (self.bytes - self.last_bytes) as f64 / elapsed; + + println!( + "Packets: {}K | Bytes: {:.2}GiB | Packet rate: {:.2} Kpps | Byte rate: {:.2} Gbps", + self.packets / 1_000, + self.bytes as f64 / 1024.0 / 1024.0 / 1024.0, + pps / 1_000.0, + (bps / 1024.0 / 1024.0 / 1024.0) * 8.0 + ); + + self.last_packets = self.packets; + self.last_bytes = self.bytes; + self.last_time = now; + } +} + +fn main() { + let args = Args::parse(); + + let exit = Arc::new(AtomicBool::new(false)); + ctrlc::set_handler({ + let exit = exit.clone(); + move || { + exit.store(true, Ordering::Relaxed); + } + }) + .expect("Error setting Ctrl-C handler"); + + let rt = tokio::runtime::Builder::new_current_thread() + .enable_io() + .build() + .expect("Failed to create tokio runtime"); + + rt.block_on(async move { + let listener = TcpListener::bind(&args.listen_addr) + .await + .expect("Failed to bind"); + println!("Listening on {}", args.listen_addr); + + listener.set_ttl(64).ok(); + + while !exit.load(Ordering::Relaxed) { + let (mut stream, addr) = listener.accept().await.expect("Failed to accept"); + println!("Accepted connection from {}", addr); + + stream.set_nodelay(true).ok(); + + let mut stats = Stats::new(100_000); + let mut buf = vec![0u8; 65535]; + + loop { + let n = match stream.read(&mut buf).await { + Ok(0) => { + println!("Disconnected from {}", addr); + break; + } + Ok(n) => n, + Err(e) => { + println!("Read error: {:?}", e); + break; + } + }; + + if let Err(e) = stream.write_all(&buf[..n]).await { + println!("Write error: {:?}", e); + break; + } + + stats.update(n); + stats.maybe_print(); + } + } + }); + + println!("Exiting..."); +} diff --git a/examples/sync-echo.rs b/examples/sync-echo.rs index 2701c0b..2e3789b 100644 --- a/examples/sync-echo.rs +++ b/examples/sync-echo.rs @@ -107,7 +107,8 @@ fn main() { // Process the frame buffer, this will consume the entire buffer and submit them to the fill queue. let mut frames = umem.init_buffer::().unwrap(); - umem.process_fill_queue(&mut frames); + umem.process_fill_queue(&mut frames) + .expect("Failed to process fill queue"); // Loop forever reading packets from the socket. while !exit.load(Ordering::Relaxed) { @@ -124,10 +125,10 @@ fn main() { debug_assert_eq!(frames.num_frames(), received as usize); // For each received frame, attempt to swap the addresses. - for mut frame in frames.iter_frames_mut() { + for frame in frames.iter_frames_mut() { stats.update(frame.len(), frame.is_fragment()); - swap_addresses(&mut frame).unwrap(); + swap_addresses(frame).unwrap(); } // Send the updated frames to the socket. @@ -137,13 +138,14 @@ fn main() { debug_assert_eq!(frames.num_frames(), 0); // So we "sent" the packets but now we need to actually drive the completion of those sends. - while let Err(_) = umem.process_completion_queue(&mut frames) { + while umem.process_completion_queue(&mut frames).is_err() { socket.maybe_wake().expect("Failed to wake tx queue"); } // Now give back all our frames to the kernel by means of the fill queue. umem.maybe_wake_fill_queue(socket.fd()).unwrap(); - umem.process_fill_queue(&mut frames); + umem.process_fill_queue(&mut frames) + .expect("Failed to process fill queue"); stats.maybe_print(); } diff --git a/examples/sync-rx-ms.rs b/examples/sync-rx-ms.rs index 455cc02..6bcdf14 100644 --- a/examples/sync-rx-ms.rs +++ b/examples/sync-rx-ms.rs @@ -82,7 +82,12 @@ fn worker_thread<'umem>( // Now we need to give back the frames to the kernel so hand them back to the main umem frame stack, // the Umem thread will handle the heavy lifting of submitting them to the fill queue. - frame_stack.lock().unwrap().extend(frames.drain(..)); + { + let mut guard = frame_stack.lock().unwrap(); + for frame in frames.drain(..) { + guard.push(frame); + } + } stats.maybe_print(); } @@ -101,7 +106,8 @@ fn umem_thread<'umem>( { let guard = frame_stack.lock().unwrap(); - umem.process_fill_queue(guard); + umem.process_fill_queue(guard) + .expect("Failed to process fill queue"); } } } @@ -153,7 +159,8 @@ fn main() { // For reads to work we need to hand some buffers to the kernel, so it can start reading data into them. // Process the frame buffer, this will consume the entire buffer and submit them to the fill queue. let mut frames = umem.init_buffer::().unwrap(); - umem.process_fill_queue(&mut frames); + umem.process_fill_queue(&mut frames) + .expect("Failed to process fill queue"); // Since we are going to be using multiple threads, we need to wrap up our frame stack in a arc/mutex to // share it between the workers and umem threads diff --git a/examples/sync-rx.rs b/examples/sync-rx.rs index 0fd9e2f..92989e7 100644 --- a/examples/sync-rx.rs +++ b/examples/sync-rx.rs @@ -105,7 +105,8 @@ fn main() { // Process the frame buffer, this will consume the entire buffer and submit them to the fill queue. let mut frames = umem.init_buffer::().unwrap(); - umem.process_fill_queue(&mut frames); + umem.process_fill_queue(&mut frames) + .expect("Failed to process fill queue"); // Loop forever reading packets from the socket. while !exit.load(Ordering::Relaxed) { @@ -139,7 +140,8 @@ fn main() { umem.maybe_wake_fill_queue(socket.fd()).unwrap(); // Process the frame buffer, this will consume the entire buffer and submit them to the fill queue. - umem.process_fill_queue(&mut frames); + umem.process_fill_queue(&mut frames) + .expect("Failed to process fill queue"); stats.maybe_print(); } diff --git a/examples/sync-tx.rs b/examples/sync-tx.rs index 9175a22..dc865e7 100644 --- a/examples/sync-tx.rs +++ b/examples/sync-tx.rs @@ -137,7 +137,7 @@ fn main() { // Process any outstanding descriptors on the completion queue retrieving the sent frames. // // This should be a loop because the kernel can only transmit a limited number of frames at a time. - while let Err(_) = umem.process_completion_queue(&mut write_frames) { + while umem.process_completion_queue(&mut write_frames).is_err() { socket.maybe_wake().expect("Failed to wake tx queue"); } diff --git a/examples/tcp-echo-client.rs b/examples/tcp-echo-client.rs new file mode 100644 index 0000000..78df92a --- /dev/null +++ b/examples/tcp-echo-client.rs @@ -0,0 +1,133 @@ +use std::{ + ops::{Deref, DerefMut}, + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, +}; + +use clap::Parser; +use coarsetime::Duration; + +use libvoid::net::{socket::TcpStream, wire::ip::SocketAddr}; +use libvoid::rt::LocalRuntime; + +mod common; +use common::{BaseArgs, Stats}; + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +struct Args { + #[command(flatten)] + base: BaseArgs, + #[arg(short, long, default_value = "[fc00:dead:cafe:1::2]:8080")] + local_addr: SocketAddr, + #[arg(short, long, default_value = "[fc00:dead:cafe:1::1]:8080")] + remote_addr: SocketAddr, + #[arg(short, long, default_value = "64")] + message_size: usize, +} + +impl Deref for Args { + type Target = BaseArgs; + + fn deref(&self) -> &Self::Target { + &self.base + } +} + +impl DerefMut for Args { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.base + } +} + +fn main() { + let mut stats = Stats::new_with_packets_per_print(1_000_000); + let args = Args::parse(); + + let mut runtime = LocalRuntime::builder(&args.if_name, args.queue) + .arp_ttl(Duration::from_secs(1200)) + .attach_mode(args.attach_mode) + .enable_fragmentation(args.enable_fragmentation) + .completion_ring_size(args.completion_ring_size) + .fill_ring_size(args.fill_ring_size) + .frame_size(args.frame_size) + .busy_poll(args.busy_poll) + .busy_poll_batch_size(args.busy_poll_batch_size) + .busy_poll_timeout_us(args.busy_poll_timeout_us) + .huge_tables(args.huge_tables) + .unaligned(args.unaligned) + .rx_ring_size(args.rx_ring_size) + .tx_ring_size(args.tx_ring_size) + .copy_mode(args.copy_mode) + .build() + .expect("Failed to create runtime"); + + let exit = Arc::new(AtomicBool::new(false)); + ctrlc::set_handler({ + let exit = exit.clone(); + move || { + exit.store(true, Ordering::Relaxed); + } + }) + .expect("Error setting Ctrl-C handler"); + + let message_size = args.message_size; + + runtime + .run(exit, async move { + println!( + "Connecting from {} to {}...", + args.local_addr, args.remote_addr + ); + + let stream = TcpStream::connect( + args.local_addr.ip, + args.local_addr.port, + args.remote_addr.ip, + args.remote_addr.port, + ) + .expect("Failed to initiate connection") + .await + .expect("Connection failed"); + + println!( + "Connected to {}:{}", + stream.remote_addr(), + stream.remote_port() + ); + + let payload = vec![0xABu8; message_size]; + let mut read_buf = vec![0u8; message_size]; + + loop { + if let Err(e) = stream.write(&payload).await { + println!("Write error: {:?}", e); + break; + } + + let mut total_read = 0; + while total_read < message_size { + let n = match stream.read(&mut read_buf[total_read..]).await { + Ok(0) => { + println!("Server closed connection"); + return; + } + Ok(n) => n, + Err(e) => { + println!("Read error: {:?}", e); + return; + } + }; + total_read += n; + } + + stats.update(total_read, false); + stats.maybe_print(); + } + }) + .expect("Failed to run runtime"); + + println!("Exiting..."); +} diff --git a/examples/tcp-echo-server.rs b/examples/tcp-echo-server.rs new file mode 100644 index 0000000..f24e4ee --- /dev/null +++ b/examples/tcp-echo-server.rs @@ -0,0 +1,111 @@ +use std::{ + ops::{Deref, DerefMut}, + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, +}; + +use clap::Parser; +use coarsetime::Duration; + +use libvoid::net::{socket::TcpListener, wire::ip::SocketAddr}; +use libvoid::rt::LocalRuntime; + +mod common; +use common::{BaseArgs, Stats}; + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +struct Args { + #[command(flatten)] + base: BaseArgs, + #[arg(short, long, default_value = "[fc00:dead:cafe:1::1]:8080")] + local_addr: SocketAddr, +} + +impl Deref for Args { + type Target = BaseArgs; + + fn deref(&self) -> &Self::Target { + &self.base + } +} + +impl DerefMut for Args { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.base + } +} + +fn main() { + let mut stats = Stats::new_with_packets_per_print(1_000_000); + let args = Args::parse(); + + let mut runtime = LocalRuntime::builder(&args.if_name, args.queue) + .arp_ttl(Duration::from_secs(1200)) + .attach_mode(args.attach_mode) + .enable_fragmentation(args.enable_fragmentation) + .completion_ring_size(args.completion_ring_size) + .fill_ring_size(args.fill_ring_size) + .frame_size(args.frame_size) + .busy_poll(args.busy_poll) + .busy_poll_batch_size(args.busy_poll_batch_size) + .busy_poll_timeout_us(args.busy_poll_timeout_us) + .huge_tables(args.huge_tables) + .unaligned(args.unaligned) + .rx_ring_size(args.rx_ring_size) + .tx_ring_size(args.tx_ring_size) + .copy_mode(args.copy_mode) + .build() + .expect("Failed to create runtime"); + + let exit = Arc::new(AtomicBool::new(false)); + ctrlc::set_handler({ + let exit = exit.clone(); + move || { + exit.store(true, Ordering::Relaxed); + } + }) + .expect("Error setting Ctrl-C handler"); + + runtime + .run(exit, async move { + let listener = TcpListener::listen(args.local_addr.ip, args.local_addr.port) + .expect("Failed to listen"); + println!("Listening on {}", args.local_addr); + + loop { + let stream = listener.accept().await; + println!( + "Accepted connection from {}:{}", + stream.remote_addr(), + stream.remote_port() + ); + + loop { + match stream.splice(65535).await { + Ok(0) => { + println!( + "Disconnected from {}:{}", + stream.remote_addr(), + stream.remote_port() + ); + break; + } + Ok(n) => { + stats.update(n, false); + stats.maybe_print(); + } + Err(e) => { + println!("Splice error: {:?}", e); + break; + } + } + } + } + }) + .expect("Failed to run runtime"); + + println!("Exiting..."); +} diff --git a/examples/tokio-rx-ms.rs b/examples/tokio-rx-ms.rs index 806cb94..18155b5 100644 --- a/examples/tokio-rx-ms.rs +++ b/examples/tokio-rx-ms.rs @@ -83,7 +83,9 @@ async fn worker_task<'umem>( // - Return the data to the kernel by means of the fill queue. Which is what will be doing here. { let mut guard = frame_stack.lock().await; - guard.extend(frames.drain(..)); + for frame in frames.drain(..) { + guard.push(frame); + } } // Maybe print the stats for this iteration. diff --git a/examples/tokio.rs b/examples/tokio.rs new file mode 100644 index 0000000..223d8e8 --- /dev/null +++ b/examples/tokio.rs @@ -0,0 +1,48 @@ +use std::ops::{Deref, DerefMut}; + +use clap::Parser; +use tokio::net::UdpSocket; + +mod common; +use common::{BaseArgs, Stats}; + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +struct Args { + #[command(flatten)] + base: BaseArgs, +} + +impl Deref for Args { + type Target = BaseArgs; + + fn deref(&self) -> &Self::Target { + &self.base + } +} + +impl DerefMut for Args { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.base + } +} + +#[tokio::main(flavor = "current_thread")] +async fn main() { + let mut stats = Stats::new_with_packets_per_print(1); + + let udp = UdpSocket::bind("[fc00:dead:cafe:1::2]:8080") + .await + .expect("Failed to bind UDP socket"); + + println!("Listening on [fc00:dead:cafe:1::2]:8080"); + let mut buf = [0; 1024]; + loop { + let (n, _) = udp + .recv_from(&mut buf) + .await + .expect("Failed to receive packet"); + stats.update(n, false); + stats.maybe_print(); + } +} diff --git a/examples/udp-client.rs b/examples/udp-client.rs new file mode 100644 index 0000000..d7c5e88 --- /dev/null +++ b/examples/udp-client.rs @@ -0,0 +1,92 @@ +use std::{ + ops::{Deref, DerefMut}, + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, +}; + +use clap::Parser; +use coarsetime::Duration; + +use libvoid::net::{socket::UdpSocket, wire::ip::SocketAddr}; +use libvoid::rt::LocalRuntime; + +mod common; +use common::{BaseArgs, Stats}; + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +struct Args { + #[command(flatten)] + base: BaseArgs, + #[arg(short, long, default_value = "[fc00:dead:cafe:1::2]:8080")] + local_addr: SocketAddr, + #[arg(short, long, default_value = "[fc00:dead:cafe:1::1]:8080")] + remote_addr: SocketAddr, +} + +impl Deref for Args { + type Target = BaseArgs; + + fn deref(&self) -> &Self::Target { + &self.base + } +} + +impl DerefMut for Args { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.base + } +} + +fn main() { + let mut stats = Stats::new(); + let args = Args::parse(); + + let mut runtime = LocalRuntime::builder(&args.if_name, args.queue) + .arp_ttl(Duration::from_secs(1200)) + .attach_mode(args.attach_mode) + .enable_fragmentation(args.enable_fragmentation) + .completion_ring_size(args.completion_ring_size) + .fill_ring_size(args.fill_ring_size) + .frame_size(args.frame_size) + .busy_poll(args.busy_poll) + .busy_poll_batch_size(args.busy_poll_batch_size) + .busy_poll_timeout_us(args.busy_poll_timeout_us) + .huge_tables(args.huge_tables) + .unaligned(args.unaligned) + .rx_ring_size(args.rx_ring_size) + .tx_ring_size(args.tx_ring_size) + .copy_mode(args.copy_mode) + .build() + .expect("Failed to create runtime"); + + let exit = Arc::new(AtomicBool::new(false)); + ctrlc::set_handler({ + let exit = exit.clone(); + move || { + exit.store(true, Ordering::Relaxed); + } + }) + .expect("Error setting Ctrl-C handler"); + + runtime + .run(exit, async move { + let mut socket = + UdpSocket::new(args.local_addr.ip, args.local_addr.port).expect("Failed to bind"); + println!("Listening on {}", args.local_addr); + + let buf = b"Hello, world!"; + loop { + let sent = socket + .send_to(args.remote_addr.ip, args.remote_addr.port, buf) + .await; + stats.update(sent as usize, false); + stats.maybe_print(); + } + }) + .expect("Failed to run runtime"); + + println!("Exiting..."); +} diff --git a/examples/udp-server.rs b/examples/udp-server.rs new file mode 100644 index 0000000..2e04566 --- /dev/null +++ b/examples/udp-server.rs @@ -0,0 +1,97 @@ +use std::{ + ops::{Deref, DerefMut}, + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, +}; + +use clap::Parser; +use coarsetime::Duration; + +use futures_util::StreamExt; +use libvoid::net::{socket::UdpSocket, wire::ip::SocketAddr}; +use libvoid::rt::LocalRuntime; + +mod common; +use common::{BaseArgs, Stats}; + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +struct Args { + #[command(flatten)] + base: BaseArgs, + #[arg(short, long, default_value = "[fc00:dead:cafe:1::1]:8080")] + local_addr: SocketAddr, + #[arg(short, long, default_value = "false")] + echo: bool, +} + +impl Deref for Args { + type Target = BaseArgs; + + fn deref(&self) -> &Self::Target { + &self.base + } +} + +impl DerefMut for Args { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.base + } +} + +fn main() { + let mut stats = Stats::new(); + let args = Args::parse(); + + let mut runtime = LocalRuntime::builder(&args.if_name, args.queue) + .arp_ttl(Duration::from_secs(1200)) + .attach_mode(args.attach_mode) + .enable_fragmentation(args.enable_fragmentation) + .completion_ring_size(args.completion_ring_size) + .fill_ring_size(args.fill_ring_size) + .frame_size(args.frame_size) + .busy_poll(args.busy_poll) + .busy_poll_batch_size(args.busy_poll_batch_size) + .busy_poll_timeout_us(args.busy_poll_timeout_us) + .huge_tables(args.huge_tables) + .unaligned(args.unaligned) + .rx_ring_size(args.rx_ring_size) + .tx_ring_size(args.tx_ring_size) + .copy_mode(args.copy_mode) + .build() + .expect("Failed to create runtime"); + + let exit = Arc::new(AtomicBool::new(false)); + ctrlc::set_handler({ + let exit = exit.clone(); + move || { + exit.store(true, Ordering::Relaxed); + } + }) + .expect("Error setting Ctrl-C handler"); + + runtime + .run(exit, async move { + let mut socket = + UdpSocket::new(args.local_addr.ip, args.local_addr.port).expect("Failed to bind"); + println!("Listening on {}", args.local_addr); + + let (recv, mut send) = socket.split(); + let mut recv_stream = recv.recv_stream(); + while let Some(mut packet) = recv_stream.next().await { + stats.update(packet.packet.len(), false); + stats.maybe_print(); + if args.echo { + packet.swap_addresses(); + send.echo_immediate(packet); + } else { + send.discard(packet); + } + } + }) + .expect("Failed to run runtime"); + + println!("Exiting..."); +} diff --git a/src/lib.rs b/src/lib.rs index 307b234..8530125 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1 +1,6 @@ +#![allow(clippy::too_many_arguments)] +#![allow(clippy::module_inception)] + +pub mod net; +pub mod rt; pub mod xdp; diff --git a/src/net/checksum/common.rs b/src/net/checksum/common.rs new file mode 100644 index 0000000..455ed85 --- /dev/null +++ b/src/net/checksum/common.rs @@ -0,0 +1,258 @@ +use crate::net::wire::ip::{Ipv4Address, Ipv6Address}; + +/// Sum all 16-bit words in `data`, handling trailing bytes. +/// +/// Uses a `u64` accumulator and processes 32 bytes (sixteen 16-bit words) +/// per iteration to reduce loop overhead and let the CPU pipeline loads. +#[inline] +pub(crate) fn sum_words(data: &[u8]) -> u64 { + let (mut sum, pending) = sum_words_carry(data, 0, None); + if let Some(hi) = pending { + sum += (hi as u64) << 8; + } + sum +} + +/// Sum 16-bit words across a slice, carrying a pending odd byte in/out. +/// +/// This is the building block for checksumming fragmented (multi-frame) +/// packets without heap-allocating a `Vec` of slices. Call it once per +/// fragment and thread the `(sum, pending)` state through. +#[inline] +pub(crate) fn sum_words_carry(data: &[u8], mut sum: u64, pending: Option) -> (u64, Option) { + let len = data.len(); + let mut i = 0; + + if let Some(hi) = pending { + if len > 0 { + sum += ((hi as u64) << 8) | (data[0] as u64); + i = 1; + } else { + return (sum, Some(hi)); + } + } + + // Process 32 bytes per iteration using 4x u64 wide reads. + // Each u64 is split into 4 u16 words via shifts — safe for unaligned data + // because from_be_bytes copies rather than casting pointers. + while i + 31 < len { + let w0 = u64::from_be_bytes([ + data[i], + data[i + 1], + data[i + 2], + data[i + 3], + data[i + 4], + data[i + 5], + data[i + 6], + data[i + 7], + ]); + let w1 = u64::from_be_bytes([ + data[i + 8], + data[i + 9], + data[i + 10], + data[i + 11], + data[i + 12], + data[i + 13], + data[i + 14], + data[i + 15], + ]); + let w2 = u64::from_be_bytes([ + data[i + 16], + data[i + 17], + data[i + 18], + data[i + 19], + data[i + 20], + data[i + 21], + data[i + 22], + data[i + 23], + ]); + let w3 = u64::from_be_bytes([ + data[i + 24], + data[i + 25], + data[i + 26], + data[i + 27], + data[i + 28], + data[i + 29], + data[i + 30], + data[i + 31], + ]); + sum += (w0 >> 48) + ((w0 >> 32) & 0xFFFF) + ((w0 >> 16) & 0xFFFF) + (w0 & 0xFFFF); + sum += (w1 >> 48) + ((w1 >> 32) & 0xFFFF) + ((w1 >> 16) & 0xFFFF) + (w1 & 0xFFFF); + sum += (w2 >> 48) + ((w2 >> 32) & 0xFFFF) + ((w2 >> 16) & 0xFFFF) + (w2 & 0xFFFF); + sum += (w3 >> 48) + ((w3 >> 32) & 0xFFFF) + ((w3 >> 16) & 0xFFFF) + (w3 & 0xFFFF); + i += 32; + } + + // Handle remaining 4 bytes at a time. + while i + 3 < len { + sum += ((data[i] as u64) << 8) | (data[i + 1] as u64); + sum += ((data[i + 2] as u64) << 8) | (data[i + 3] as u64); + i += 4; + } + + if i + 1 < len { + sum += ((data[i] as u64) << 8) | (data[i + 1] as u64); + i += 2; + } + + if i < len { + return (sum, Some(data[i])); + } + + (sum, None) +} + +/// Fold 64-bit running sum to 16 bits, then one's-complement. +#[inline] +pub(crate) fn fold_checksum(mut sum: u64) -> u16 { + while (sum >> 16) != 0 { + sum = (sum & 0xFFFF) + (sum >> 16); + } + !(sum as u16) +} + +/// Fold and check for 0xFFFF (verification path). +#[inline] +pub(crate) fn fold_and_verify(mut sum: u64, actual: u16) -> bool { + while (sum >> 16) != 0 { + sum = (sum & 0xFFFF) + (sum >> 16); + } + !(sum as u16) == actual +} + +/// Build the IPv4 pseudo-header sum: src IP + dst IP + protocol + length. +#[inline] +pub(crate) fn pseudo_header_sum_v4( + src_addr: &Ipv4Address, + dst_addr: &Ipv4Address, + protocol: u8, + len: u16, +) -> u64 { + sum_words(&src_addr.octets) + sum_words(&dst_addr.octets) + protocol as u64 + len as u64 +} + +/// Build the IPv6 pseudo-header sum: src IP + dst IP + length (u32) + next header. +#[inline] +pub(crate) fn pseudo_header_sum_v6( + src_addr: &Ipv6Address, + dst_addr: &Ipv6Address, + protocol: u8, + len: u32, +) -> u64 { + sum_words(&src_addr.octets) + + sum_words(&dst_addr.octets) + + (len >> 16) as u64 + + (len & 0xFFFF) as u64 + + protocol as u64 +} + +/// Convert a folded checksum to wire bytes, mapping zero to 0xFFFF per RFC 768. +#[inline] +pub(crate) fn checksum_to_bytes(checksum: u16) -> [u8; 2] { + if checksum == 0 { + [0xFF, 0xFF] + } else { + checksum.to_be_bytes() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn sum_words_carry_even_boundary() { + let src = Ipv4Address::new([192, 168, 1, 1]); + let dst = Ipv4Address::new([10, 0, 0, 1]); + let mut segment = [ + 0x12, 0x34, 0x00, 0x35, 0x00, 0x0C, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, + ]; + + // Compute checksum using the full compute path + let sum = pseudo_header_sum_v4(&src, &dst, 17, segment.len() as u16) + sum_words(&segment); + let cksum = checksum_to_bytes(fold_checksum(sum)); + segment[6] = cksum[0]; + segment[7] = cksum[1]; + + // Sum across two slices split at an even boundary + let (a, b) = segment.split_at(8); + let mut sum2 = pseudo_header_sum_v4(&src, &dst, 17, segment.len() as u16); + let pending; + (sum2, pending) = sum_words_carry(a, sum2, None); + (sum2, _) = sum_words_carry(b, sum2, pending); + assert!(fold_and_verify(sum2, 0x0000)); + } + + #[test] + fn sum_words_carry_odd_boundary() { + let src = Ipv4Address::new([192, 168, 1, 1]); + let dst = Ipv4Address::new([10, 0, 0, 1]); + let mut segment = [ + 0x12, 0x34, 0x00, 0x35, 0x00, 0x0D, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, + ]; + + let sum = pseudo_header_sum_v4(&src, &dst, 17, segment.len() as u16) + sum_words(&segment); + let cksum = checksum_to_bytes(fold_checksum(sum)); + segment[6] = cksum[0]; + segment[7] = cksum[1]; + + // Split at an odd boundary (9 bytes + 4 bytes) + let (a, b) = segment.split_at(9); + let mut sum2 = pseudo_header_sum_v4(&src, &dst, 17, segment.len() as u16); + let pending; + (sum2, pending) = sum_words_carry(a, sum2, None); + let trailing; + (sum2, trailing) = sum_words_carry(b, sum2, pending); + if let Some(hi) = trailing { + sum2 += (hi as u64) << 8; + } + assert!(fold_and_verify(sum2, 0x0000)); + } + + #[test] + fn sum_words_carry_empty_slice() { + let (sum, pending) = sum_words_carry(&[], 42, Some(0xAB)); + assert_eq!(sum, 42); + assert_eq!(pending, Some(0xAB)); + } + + #[test] + fn fold_checksum_basic() { + // Known value: all zeros should fold to 0xFFFF + assert_eq!(fold_checksum(0), 0xFFFF); + } + + #[test] + fn pseudo_header_v4_matches_legacy() { + let src = Ipv4Address::new([192, 168, 1, 1]); + let dst = Ipv4Address::new([10, 0, 0, 1]); + // Legacy UDP pseudo-header used protocol=17 hardcoded + let legacy = sum_words(&src.octets) + sum_words(&dst.octets) + 17u64 + 12u64; + let new = pseudo_header_sum_v4(&src, &dst, 17, 12); + assert_eq!(legacy, new); + } + + #[test] + fn pseudo_header_v6_matches_legacy() { + let src = Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + let dst = Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]); + // Legacy UDP pseudo-header used protocol=17 hardcoded + let legacy = sum_words(&src.octets) + + sum_words(&dst.octets) + + (12u32 >> 16) as u64 + + (12u32 & 0xFFFF) as u64 + + 17u64; + let new = pseudo_header_sum_v6(&src, &dst, 17, 12); + assert_eq!(legacy, new); + } + + #[test] + fn checksum_to_bytes_zero_maps_to_ffff() { + assert_eq!(checksum_to_bytes(0), [0xFF, 0xFF]); + } + + #[test] + fn checksum_to_bytes_nonzero() { + assert_eq!(checksum_to_bytes(0x1234), [0x12, 0x34]); + } +} diff --git a/src/net/checksum/compute.rs b/src/net/checksum/compute.rs new file mode 100644 index 0000000..df9b74a --- /dev/null +++ b/src/net/checksum/compute.rs @@ -0,0 +1,321 @@ +use crate::net::wire::ip::{IpProtocols, Ipv4Address, Ipv6Address}; + +use super::{ + checksum_to_bytes, fold_checksum, pseudo_header_sum_v4, pseudo_header_sum_v6, sum_words, +}; + +/// Computes the IPv4 header checksum per RFC 1071. +/// +/// `header_bytes` must contain the full header with the checksum field +/// set to zero. Returns the two-byte checksum in network byte order. +#[inline] +pub fn compute_ipv4_checksum(header_bytes: &[u8]) -> [u8; 2] { + let checksum = fold_checksum(sum_words(header_bytes)); + checksum.to_be_bytes() +} + +/// Compute IPv4 UDP checksum without allocating (from port/payload parts). +#[inline] +pub fn compute_udp_checksum_from_parts( + src_addr: &Ipv4Address, + dst_addr: &Ipv4Address, + src_port: u16, + dst_port: u16, + udp_len: u16, + payload: &[u8], +) -> [u8; 2] { + let sum = pseudo_header_sum_v4(src_addr, dst_addr, IpProtocols::Udp, udp_len) + + src_port as u64 + + dst_port as u64 + + udp_len as u64 + // checksum field is zero, contributes nothing + + sum_words(payload); + checksum_to_bytes(fold_checksum(sum)) +} + +/// Compute IPv6 UDP checksum without allocating (from port/payload parts). +#[inline] +pub fn compute_udp_checksum_v6_from_parts( + src_addr: &Ipv6Address, + dst_addr: &Ipv6Address, + src_port: u16, + dst_port: u16, + udp_len: u16, + payload: &[u8], +) -> [u8; 2] { + let sum = pseudo_header_sum_v6(src_addr, dst_addr, IpProtocols::Udp, udp_len as u32) + + src_port as u64 + + dst_port as u64 + + udp_len as u64 + // checksum field is zero, contributes nothing + + sum_words(payload); + checksum_to_bytes(fold_checksum(sum)) +} + +/// Compute IPv4 TCP checksum without allocating (from header/payload parts). +#[inline] +pub fn compute_tcp_checksum_from_parts( + src_addr: &Ipv4Address, + dst_addr: &Ipv4Address, + tcp_header_bytes: &[u8], + payload: &[u8], +) -> [u8; 2] { + let tcp_len = (tcp_header_bytes.len() + payload.len()) as u16; + let sum = pseudo_header_sum_v4(src_addr, dst_addr, IpProtocols::Tcp, tcp_len) + + sum_words(tcp_header_bytes) + + sum_words(payload); + checksum_to_bytes(fold_checksum(sum)) +} + +/// Compute IPv6 TCP checksum without allocating (from header/payload parts). +#[inline] +pub fn compute_tcp_checksum_v6_from_parts( + src_addr: &Ipv6Address, + dst_addr: &Ipv6Address, + tcp_header_bytes: &[u8], + payload: &[u8], +) -> [u8; 2] { + let tcp_len = (tcp_header_bytes.len() + payload.len()) as u32; + let sum = pseudo_header_sum_v6(src_addr, dst_addr, IpProtocols::Tcp, tcp_len) + + sum_words(tcp_header_bytes) + + sum_words(payload); + checksum_to_bytes(fold_checksum(sum)) +} + +/// Computes the ICMPv6 checksum per RFC 4443 s2.3. +/// +/// The checksum covers an IPv6 pseudo-header (source address, +/// destination address, upper-layer packet length, next header = 58) +/// followed by the ICMPv6 message data. +/// +/// When computing a fresh checksum, zero the checksum field in +/// `icmpv6_data` first. When verifying, pass the data as-is and +/// check for a `[0x00, 0x00]` result. +#[inline] +pub fn compute_icmpv6_checksum( + src_addr: &Ipv6Address, + dst_addr: &Ipv6Address, + icmpv6_data: &[u8], +) -> [u8; 2] { + let sum = pseudo_header_sum_v6( + src_addr, + dst_addr, + IpProtocols::IcmpV6, + icmpv6_data.len() as u32, + ) + sum_words(icmpv6_data); + let checksum = fold_checksum(sum); + checksum.to_be_bytes() +} + +#[cfg(test)] +mod tests { + use crate::net::{checksum::test_utils::*, wire::udp::UDP_HEADER_LEN}; + + use super::*; + + #[test] + fn ipv4_checksum_compute_and_verify() { + let bytes: [u8; 20] = [ + 0x45, 0x00, 0x00, 0x28, 0x00, 0x01, 0x40, 0x00, 0x40, 0x11, 0x00, 0x00, 0xC0, 0xA8, + 0x01, 0x01, 0x0A, 0x00, 0x00, 0x01, + ]; + let checksum = compute_ipv4_checksum(&bytes); + assert_eq!(checksum, [0x6F, 0x1A]); + + let mut with_checksum = bytes; + with_checksum[10] = checksum[0]; + with_checksum[11] = checksum[1]; + // Recomputing over the full header (including checksum) should yield [0, 0] + assert_eq!(compute_ipv4_checksum(&with_checksum), [0x00, 0x00]); + } + + #[test] + fn udp_checksum_v4_compute_and_verify() { + let src = Ipv4Address::new([192, 168, 1, 1]); + let dst = Ipv4Address::new([10, 0, 0, 1]); + let segment = [ + 0x12, 0x34, // src port + 0x00, 0x35, // dst port + 0x00, 0x0C, // length = 12 + 0x00, 0x00, // checksum (zeroed) + 0x01, 0x02, 0x03, 0x04, // payload + ]; + + let checksum = compute_udp_checksum(&src, &dst, &segment); + assert_eq!(checksum, [0x1D, 0xBD]); + } + + #[test] + fn udp_checksum_v6_compute() { + let src = Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + let dst = Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]); + let segment = [ + 0x12, 0x34, // src port + 0x00, 0x35, // dst port + 0x00, 0x0C, // length = 12 + 0x00, 0x00, // checksum (zeroed) + 0x01, 0x02, 0x03, 0x04, // payload + ]; + + let checksum = compute_udp_checksum_v6(&src, &dst, &segment); + assert_eq!(checksum, [0xEC, 0x62]); + } + + #[test] + fn from_parts_v4_matches_segment() { + let src = Ipv4Address::new([192, 168, 1, 1]); + let dst = Ipv4Address::new([10, 0, 0, 1]); + let payload = [0x01, 0x02, 0x03, 0x04]; + let src_port: u16 = 0x1234; + let dst_port: u16 = 53; + let udp_len = (UDP_HEADER_LEN + payload.len()) as u16; + + let segment = [ + 0x12, 0x34, 0x00, 0x35, 0x00, 0x0C, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, + ]; + let expected = compute_udp_checksum(&src, &dst, &segment); + let actual = + compute_udp_checksum_from_parts(&src, &dst, src_port, dst_port, udp_len, &payload); + assert_eq!(actual, expected); + } + + #[test] + fn from_parts_v6_matches_segment() { + let src = Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + let dst = Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]); + let payload = [0x01, 0x02, 0x03, 0x04]; + let src_port: u16 = 0x1234; + let dst_port: u16 = 53; + let udp_len = (UDP_HEADER_LEN + payload.len()) as u16; + + let segment = [ + 0x12, 0x34, 0x00, 0x35, 0x00, 0x0C, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, + ]; + let expected = compute_udp_checksum_v6(&src, &dst, &segment); + let actual = + compute_udp_checksum_v6_from_parts(&src, &dst, src_port, dst_port, udp_len, &payload); + assert_eq!(actual, expected); + } + + #[test] + fn from_parts_v4_odd_payload() { + let src = Ipv4Address::new([10, 0, 0, 1]); + let dst = Ipv4Address::new([10, 0, 0, 2]); + let payload = [0x01, 0x02, 0x03, 0x04, 0x05]; // odd + let src_port: u16 = 8000; + let dst_port: u16 = 9000; + let udp_len = (UDP_HEADER_LEN + payload.len()) as u16; + + let mut segment = Vec::with_capacity(UDP_HEADER_LEN + payload.len()); + segment.extend_from_slice(&src_port.to_be_bytes()); + segment.extend_from_slice(&dst_port.to_be_bytes()); + segment.extend_from_slice(&udp_len.to_be_bytes()); + segment.extend_from_slice(&[0u8; 2]); + segment.extend_from_slice(&payload); + + let expected = compute_udp_checksum(&src, &dst, &segment); + let actual = + compute_udp_checksum_from_parts(&src, &dst, src_port, dst_port, udp_len, &payload); + assert_eq!(actual, expected); + } + + #[test] + fn from_parts_v6_odd_payload() { + let src = Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + let dst = Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]); + let payload = [0x01, 0x02, 0x03, 0x04, 0x05]; // odd + let src_port: u16 = 8000; + let dst_port: u16 = 9000; + let udp_len = (UDP_HEADER_LEN + payload.len()) as u16; + + let mut segment = Vec::with_capacity(UDP_HEADER_LEN + payload.len()); + segment.extend_from_slice(&src_port.to_be_bytes()); + segment.extend_from_slice(&dst_port.to_be_bytes()); + segment.extend_from_slice(&udp_len.to_be_bytes()); + segment.extend_from_slice(&[0u8; 2]); + segment.extend_from_slice(&payload); + + let expected = compute_udp_checksum_v6(&src, &dst, &segment); + let actual = + compute_udp_checksum_v6_from_parts(&src, &dst, src_port, dst_port, udp_len, &payload); + assert_eq!(actual, expected); + } + + #[test] + fn tcp_checksum_v4_compute() { + let src = Ipv4Address::new([192, 168, 1, 1]); + let dst = Ipv4Address::new([10, 0, 0, 1]); + let segment = [ + 0x12, 0x34, // src port + 0x00, 0x50, // dst port (80) + 0x00, 0x00, 0x00, 0x01, // seq + 0x00, 0x00, 0x00, 0x00, // ack + 0x50, 0x02, // data offset=5, SYN + 0x72, 0x10, // window + 0x00, 0x00, // checksum (zeroed) + 0x00, 0x00, // urgent + ]; + + let checksum = compute_tcp_checksum(&src, &dst, &segment); + assert_ne!(checksum, [0x00, 0x00]); + } + + #[test] + fn tcp_checksum_v6_compute() { + let src = Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + let dst = Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]); + let segment = [ + 0x12, 0x34, // src port + 0x00, 0x50, // dst port + 0x00, 0x00, 0x00, 0x01, // seq + 0x00, 0x00, 0x00, 0x00, // ack + 0x50, 0x02, // data offset=5, SYN + 0x72, 0x10, // window + 0x00, 0x00, // checksum (zeroed) + 0x00, 0x00, // urgent + ]; + + let checksum = compute_tcp_checksum_v6(&src, &dst, &segment); + assert_ne!(checksum, [0x00, 0x00]); + } + + #[test] + fn icmpv6_checksum_echo_request() { + let src = Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + let dst = Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]); + // ICMPv6 Echo Request: type=128, code=0, checksum=0, id=1, seq=1 + let data = [0x80, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01]; + let checksum = compute_icmpv6_checksum(&src, &dst, &data); + assert_eq!(checksum, [0x82, 0xB6]); + } + + #[test] + fn icmpv6_checksum_verify_roundtrip() { + let src = Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + let dst = Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]); + let mut data = [0x80, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01]; + + let checksum = compute_icmpv6_checksum(&src, &dst, &data); + data[2] = checksum[0]; + data[3] = checksum[1]; + + // Recomputing over data with correct checksum should yield [0, 0] + let verify = compute_icmpv6_checksum(&src, &dst, &data); + assert_eq!(verify, [0x00, 0x00]); + } + + #[test] + fn icmpv6_checksum_odd_length_data() { + let src = Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + let dst = Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]); + // 9 bytes: echo request header + 1 byte payload (odd length) + let mut data = [0x80, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0xAB]; + + let checksum = compute_icmpv6_checksum(&src, &dst, &data); + data[2] = checksum[0]; + data[3] = checksum[1]; + + let verify = compute_icmpv6_checksum(&src, &dst, &data); + assert_eq!(verify, [0x00, 0x00]); + } +} diff --git a/src/net/checksum/mod.rs b/src/net/checksum/mod.rs new file mode 100644 index 0000000..c4c02a6 --- /dev/null +++ b/src/net/checksum/mod.rs @@ -0,0 +1,23 @@ +mod common; +mod compute; +mod verify; + +#[cfg(test)] +mod test_utils; + +pub(crate) use common::{ + checksum_to_bytes, fold_and_verify, fold_checksum, pseudo_header_sum_v4, pseudo_header_sum_v6, + sum_words, sum_words_carry, +}; +pub(crate) use compute::{ + compute_icmpv6_checksum, compute_ipv4_checksum, compute_tcp_checksum_from_parts, + compute_tcp_checksum_v6_from_parts, compute_udp_checksum_from_parts, + compute_udp_checksum_v6_from_parts, +}; +pub(crate) use verify::{ + verify_ipv4_checksum, verify_tcp_checksum, verify_tcp_checksum_v6, verify_udp_checksum, + verify_udp_checksum_v6, +}; + +#[cfg(test)] +pub(crate) use test_utils::{compute_tcp_checksum, compute_udp_checksum, compute_udp_checksum_v6}; diff --git a/src/net/checksum/test_utils.rs b/src/net/checksum/test_utils.rs new file mode 100644 index 0000000..3350f9c --- /dev/null +++ b/src/net/checksum/test_utils.rs @@ -0,0 +1,97 @@ +use crate::net::wire::ip::{IpProtocols, Ipv4Address, Ipv6Address}; + +use super::{ + checksum_to_bytes, fold_checksum, pseudo_header_sum_v4, pseudo_header_sum_v6, sum_words, +}; + +/// Computes the UDP checksum over the IPv4 pseudo-header and full UDP segment. +/// +/// Per RFC 768, the checksum covers a pseudo-header (src IP, dst IP, +/// zero, protocol, UDP length) concatenated with the UDP header and data. +/// +/// `udp_segment` must contain the full UDP header + payload with the +/// checksum field set to zero. +/// +/// Returns the two-byte checksum in network byte order. If the computed +/// checksum is zero, returns `[0xFF, 0xFF]` per RFC 768 (a transmitted +/// checksum of zero means "no checksum"). +#[inline] +pub fn compute_udp_checksum( + src_addr: &Ipv4Address, + dst_addr: &Ipv4Address, + udp_segment: &[u8], +) -> [u8; 2] { + let sum = pseudo_header_sum_v4( + src_addr, + dst_addr, + IpProtocols::Udp, + udp_segment.len() as u16, + ) + sum_words(udp_segment); + checksum_to_bytes(fold_checksum(sum)) +} + +/// Computes the UDP checksum over the IPv6 pseudo-header and full UDP segment. +/// +/// Per RFC 2460 s8.1, the pseudo-header for IPv6 contains: +/// source address (16), destination address (16), UDP length as u32 (4), +/// three zero bytes (3), and next header = 17 (1) -- totalling 40 bytes. +/// +/// Unlike IPv4, the UDP checksum is **mandatory** for IPv6 -- a zero +/// checksum is not permitted on transmit. +/// +/// `udp_segment` must contain the full UDP header + payload with the +/// checksum field set to zero. +/// +/// Returns the two-byte checksum in network byte order. +#[inline] +pub fn compute_udp_checksum_v6( + src_addr: &Ipv6Address, + dst_addr: &Ipv6Address, + udp_segment: &[u8], +) -> [u8; 2] { + let sum = pseudo_header_sum_v6( + src_addr, + dst_addr, + IpProtocols::Udp, + udp_segment.len() as u32, + ) + sum_words(udp_segment); + checksum_to_bytes(fold_checksum(sum)) +} + +/// Computes the TCP checksum over the IPv4 pseudo-header and full TCP segment. +/// +/// `tcp_segment` must contain the full TCP header + payload with the +/// checksum field set to zero. +#[inline] +pub fn compute_tcp_checksum( + src_addr: &Ipv4Address, + dst_addr: &Ipv4Address, + tcp_segment: &[u8], +) -> [u8; 2] { + let sum = pseudo_header_sum_v4( + src_addr, + dst_addr, + IpProtocols::Tcp, + tcp_segment.len() as u16, + ) + sum_words(tcp_segment); + checksum_to_bytes(fold_checksum(sum)) +} + +/// Computes the TCP checksum over the IPv6 pseudo-header and full TCP segment. +/// +/// `tcp_segment` must contain the full TCP header + payload with the +/// checksum field set to zero. +#[inline] +pub fn compute_tcp_checksum_v6( + src_addr: &Ipv6Address, + dst_addr: &Ipv6Address, + tcp_segment: &[u8], +) -> [u8; 2] { + let sum = pseudo_header_sum_v6( + src_addr, + dst_addr, + IpProtocols::Tcp, + tcp_segment.len() as u32, + ) + sum_words(tcp_segment); + checksum_to_bytes(fold_checksum(sum)) +} diff --git a/src/net/checksum/verify.rs b/src/net/checksum/verify.rs new file mode 100644 index 0000000..a857192 --- /dev/null +++ b/src/net/checksum/verify.rs @@ -0,0 +1,314 @@ +use crate::net::wire::{ + ip::{IpProtocols, Ipv4Address, Ipv6Address}, + tcp::TCP_HEADER_LEN, + udp::UDP_HEADER_LEN, +}; + +use super::{ + common::{fold_and_verify, pseudo_header_sum_v4, pseudo_header_sum_v6, sum_words}, + compute::compute_ipv4_checksum, +}; + +/// Verifies the IPv4 header checksum. +/// +/// Returns `true` if the checksum is valid (the one's complement sum +/// of the entire header including the checksum field yields zero). +#[inline] +pub fn verify_ipv4_checksum(header_bytes: &[u8]) -> bool { + compute_ipv4_checksum(header_bytes) == [0x00, 0x00] +} + +/// Verifies the UDP checksum for an IPv4 packet. +/// +/// Returns `true` if the checksum field is zero (no checksum, per RFC 768) +/// or if the one's complement sum of the pseudo-header and full UDP segment +/// yields the expected result. +#[inline] +pub fn verify_udp_checksum( + src_addr: &Ipv4Address, + dst_addr: &Ipv4Address, + udp_segment: &[u8], +) -> bool { + if udp_segment.len() < UDP_HEADER_LEN { + return false; + } + if udp_segment[6] == 0 && udp_segment[7] == 0 { + return true; + } + let sum = pseudo_header_sum_v4( + src_addr, + dst_addr, + IpProtocols::Udp, + udp_segment.len() as u16, + ) + sum_words(udp_segment); + fold_and_verify(sum, 0x0000) +} + +/// Verifies the UDP checksum for an IPv6 packet. +/// +/// Returns `true` if the one's complement sum of the pseudo-header and +/// full UDP segment yields the expected result. Unlike IPv4, a zero +/// checksum field is **invalid** for IPv6 and will cause this to return +/// `false`. +#[inline] +pub fn verify_udp_checksum_v6( + src_addr: &Ipv6Address, + dst_addr: &Ipv6Address, + udp_segment: &[u8], +) -> bool { + if udp_segment.len() < UDP_HEADER_LEN { + return false; + } + if udp_segment[6] == 0 && udp_segment[7] == 0 { + return false; + } + let sum = pseudo_header_sum_v6( + src_addr, + dst_addr, + IpProtocols::Udp, + udp_segment.len() as u32, + ) + sum_words(udp_segment); + fold_and_verify(sum, 0x0000) +} + +/// Verifies the TCP checksum for an IPv4 packet. +/// +/// Returns `true` if the one's complement sum of the pseudo-header and +/// full TCP segment yields the expected result. A zero checksum field +/// is **invalid** for TCP and will cause this to return `false`. +#[inline] +pub fn verify_tcp_checksum( + src_addr: &Ipv4Address, + dst_addr: &Ipv4Address, + tcp_segment: &[u8], +) -> bool { + if tcp_segment.len() < TCP_HEADER_LEN { + return false; + } + if tcp_segment[16] == 0 && tcp_segment[17] == 0 { + return false; + } + let sum = pseudo_header_sum_v4( + src_addr, + dst_addr, + IpProtocols::Tcp, + tcp_segment.len() as u16, + ) + sum_words(tcp_segment); + fold_and_verify(sum, 0x0000) +} + +/// Verifies the TCP checksum for an IPv6 packet. +/// +/// Returns `true` if the one's complement sum of the pseudo-header and +/// full TCP segment yields the expected result. A zero checksum field +/// is **invalid** and will cause this to return `false`. +#[inline] +pub fn verify_tcp_checksum_v6( + src_addr: &Ipv6Address, + dst_addr: &Ipv6Address, + tcp_segment: &[u8], +) -> bool { + if tcp_segment.len() < TCP_HEADER_LEN { + return false; + } + if tcp_segment[16] == 0 && tcp_segment[17] == 0 { + return false; + } + let sum = pseudo_header_sum_v6( + src_addr, + dst_addr, + IpProtocols::Tcp, + tcp_segment.len() as u32, + ) + sum_words(tcp_segment); + fold_and_verify(sum, 0x0000) +} + +#[cfg(test)] +mod tests { + use crate::net::checksum::test_utils::*; + + use super::*; + + #[test] + fn ipv4_checksum_verify_valid() { + let mut bytes: [u8; 20] = [ + 0x45, 0x00, 0x00, 0x28, 0x00, 0x01, 0x40, 0x00, 0x40, 0x11, 0x00, 0x00, 0xC0, 0xA8, + 0x01, 0x01, 0x0A, 0x00, 0x00, 0x01, + ]; + let checksum = compute_ipv4_checksum(&bytes); + bytes[10] = checksum[0]; + bytes[11] = checksum[1]; + assert!(verify_ipv4_checksum(&bytes)); + } + + #[test] + fn ipv4_checksum_verify_rejects_bad() { + let bytes: [u8; 20] = [ + 0x45, 0x00, 0x00, 0x28, 0x00, 0x01, 0x40, 0x00, 0x40, 0x11, 0xFF, 0xFF, 0xC0, 0xA8, + 0x01, 0x01, 0x0A, 0x00, 0x00, 0x01, + ]; + assert!(!verify_ipv4_checksum(&bytes)); + } + + #[test] + fn udp_v4_verify_valid() { + let src = Ipv4Address::new([192, 168, 1, 1]); + let dst = Ipv4Address::new([10, 0, 0, 1]); + let mut segment = [ + 0x12, 0x34, 0x00, 0x35, 0x00, 0x0C, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, + ]; + let checksum = compute_udp_checksum(&src, &dst, &segment); + segment[6] = checksum[0]; + segment[7] = checksum[1]; + assert!(verify_udp_checksum(&src, &dst, &segment)); + } + + #[test] + fn udp_v4_zero_means_no_checksum() { + let src = Ipv4Address::new([0; 4]); + let dst = Ipv4Address::new([0; 4]); + let segment = [0u8; 8]; + assert!(verify_udp_checksum(&src, &dst, &segment)); + } + + #[test] + fn udp_v4_too_short() { + let src = Ipv4Address::new([0; 4]); + let dst = Ipv4Address::new([0; 4]); + assert!(!verify_udp_checksum(&src, &dst, &[0; 7])); + } + + #[test] + fn udp_v4_rejects_bad_checksum() { + let src = Ipv4Address::new([192, 168, 1, 1]); + let dst = Ipv4Address::new([10, 0, 0, 1]); + let segment = [ + 0x12, 0x34, 0x00, 0x35, 0x00, 0x0C, 0xFF, 0xFF, 0x01, 0x02, 0x03, 0x04, + ]; + assert!(!verify_udp_checksum(&src, &dst, &segment)); + } + + #[test] + fn udp_v6_verify_valid() { + let src = Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + let dst = Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]); + let mut segment = [ + 0x12, 0x34, 0x00, 0x35, 0x00, 0x0C, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, + ]; + let checksum = compute_udp_checksum_v6(&src, &dst, &segment); + segment[6] = checksum[0]; + segment[7] = checksum[1]; + assert!(verify_udp_checksum_v6(&src, &dst, &segment)); + } + + #[test] + fn udp_v6_zero_is_invalid() { + let src = Ipv6Address::new([0; 16]); + let dst = Ipv6Address::new([0; 16]); + let segment = [0u8; 8]; + assert!(!verify_udp_checksum_v6(&src, &dst, &segment)); + } + + #[test] + fn udp_v6_too_short() { + let src = Ipv6Address::new([0; 16]); + let dst = Ipv6Address::new([0; 16]); + assert!(!verify_udp_checksum_v6(&src, &dst, &[0; 7])); + } + + #[test] + fn udp_v6_odd_length_payload() { + let src = Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + let dst = Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]); + let mut segment = [ + 0x12, 0x34, 0x00, 0x35, 0x00, 0x0D, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, + ]; + let checksum = compute_udp_checksum_v6(&src, &dst, &segment); + segment[6] = checksum[0]; + segment[7] = checksum[1]; + assert!(verify_udp_checksum_v6(&src, &dst, &segment)); + } + + #[test] + fn tcp_v4_verify_valid() { + let src = Ipv4Address::new([192, 168, 1, 1]); + let dst = Ipv4Address::new([10, 0, 0, 1]); + let mut segment = [ + 0x12, 0x34, 0x00, 0x50, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x50, 0x02, + 0x72, 0x10, 0x00, 0x00, 0x00, 0x00, + ]; + let checksum = compute_tcp_checksum(&src, &dst, &segment); + segment[16] = checksum[0]; + segment[17] = checksum[1]; + assert!(verify_tcp_checksum(&src, &dst, &segment)); + } + + #[test] + fn tcp_v4_rejects_zero() { + let src = Ipv4Address::new([0; 4]); + let dst = Ipv4Address::new([0; 4]); + let segment = [0u8; 20]; + assert!(!verify_tcp_checksum(&src, &dst, &segment)); + } + + #[test] + fn tcp_v4_rejects_bad() { + let src = Ipv4Address::new([192, 168, 1, 1]); + let dst = Ipv4Address::new([10, 0, 0, 1]); + let segment = [ + 0x12, 0x34, 0x00, 0x50, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x50, 0x02, + 0x72, 0x10, 0xFF, 0xFF, 0x00, 0x00, + ]; + assert!(!verify_tcp_checksum(&src, &dst, &segment)); + } + + #[test] + fn tcp_v4_too_short() { + let src = Ipv4Address::new([0; 4]); + let dst = Ipv4Address::new([0; 4]); + assert!(!verify_tcp_checksum(&src, &dst, &[0; 19])); + } + + #[test] + fn tcp_v6_verify_valid() { + let src = Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + let dst = Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]); + let mut segment = [ + 0x12, 0x34, 0x00, 0x50, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x50, 0x02, + 0x72, 0x10, 0x00, 0x00, 0x00, 0x00, + ]; + let checksum = compute_tcp_checksum_v6(&src, &dst, &segment); + segment[16] = checksum[0]; + segment[17] = checksum[1]; + assert!(verify_tcp_checksum_v6(&src, &dst, &segment)); + } + + #[test] + fn tcp_v6_rejects_zero() { + let src = Ipv6Address::new([0; 16]); + let dst = Ipv6Address::new([0; 16]); + let segment = [0u8; 20]; + assert!(!verify_tcp_checksum_v6(&src, &dst, &segment)); + } + + #[test] + fn tcp_v6_too_short() { + let src = Ipv6Address::new([0; 16]); + let dst = Ipv6Address::new([0; 16]); + assert!(!verify_tcp_checksum_v6(&src, &dst, &[0; 19])); + } + + #[test] + fn tcp_v6_odd_length_payload() { + let src = Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + let dst = Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]); + let mut segment = [ + 0x12, 0x34, 0x00, 0x50, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x50, 0x18, + 0x72, 0x10, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, + ]; + let checksum = compute_tcp_checksum_v6(&src, &dst, &segment); + segment[16] = checksum[0]; + segment[17] = checksum[1]; + assert!(verify_tcp_checksum_v6(&src, &dst, &segment)); + } +} diff --git a/src/net/fragment/id.rs b/src/net/fragment/id.rs new file mode 100644 index 0000000..bf078c5 --- /dev/null +++ b/src/net/fragment/id.rs @@ -0,0 +1,38 @@ +use std::sync::atomic::{AtomicU16, AtomicU32, Ordering}; + +/// Global atomic counter for IPv4 fragment identification field. +static FRAGMENT_IPV4_ID: AtomicU16 = AtomicU16::new(1); + +/// Global atomic counter for IPv6 fragment identification field. +static FRAGMENT_IPV6_ID: AtomicU32 = AtomicU32::new(1); + +/// Atomically obtain the next IPv4 fragment identification value. +#[inline] +pub(crate) fn next_ipv4_id() -> u16 { + FRAGMENT_IPV4_ID.fetch_add(1, Ordering::Relaxed) +} + +/// Atomically obtain the next IPv6 fragment identification value. +#[inline] +pub(crate) fn next_ipv6_id() -> u32 { + FRAGMENT_IPV6_ID.fetch_add(1, Ordering::Relaxed) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn ipv4_id_increments() { + let a = next_ipv4_id(); + let b = next_ipv4_id(); + assert_eq!(b, a.wrapping_add(1)); + } + + #[test] + fn ipv6_id_increments() { + let a = next_ipv6_id(); + let b = next_ipv6_id(); + assert_eq!(b, a.wrapping_add(1)); + } +} diff --git a/src/net/fragment/mod.rs b/src/net/fragment/mod.rs new file mode 100644 index 0000000..101afa4 --- /dev/null +++ b/src/net/fragment/mod.rs @@ -0,0 +1,11 @@ +mod id; +mod plan; +mod pkt; +mod reader; +mod transport; +mod writer; + +pub use pkt::Packet; +pub use reader::{FragmentReader, ReassembledPacket}; +pub use transport::TransportHeader; +pub use writer::FragmentWriter; diff --git a/src/net/fragment/pkt.rs b/src/net/fragment/pkt.rs new file mode 100644 index 0000000..a03dd5a --- /dev/null +++ b/src/net/fragment/pkt.rs @@ -0,0 +1,321 @@ +use crate::xdp::frame::{Frame, FrameBuffer}; + +/// A packet is a collection of frames that are part of a single packet. +#[derive(Debug, Default)] +pub enum Packet<'umem> { + #[default] + Empty, + Single(Frame<'umem>), + Multi(Vec>), +} + +impl<'umem> Packet<'umem> { + /// Returns the number of frames in this packet. + pub fn num_frames(&self) -> usize { + match self { + Packet::Empty => 0, + Packet::Single(_) => 1, + Packet::Multi(frames) => frames.len(), + } + } + + /// Returns the total length of the packet in bytes. + /// + /// Note for fragmented packets this iterates over the entire packet use sparingly. + pub fn len(&self) -> usize { + match self { + Packet::Empty => 0, + Packet::Single(frame) => frame.len(), + Packet::Multi(frames) => frames.iter().map(|f| f.len()).sum(), + } + } + + /// Returns true if the packet is empty. + /// + /// Note for fragmented packets this iterates over the entire packet use sparingly. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Returns a borrowing iterator over the frames in this packet. + pub fn frames(&self) -> PacketFrameIter<'_, 'umem> { + match self { + Packet::Empty => PacketFrameIter::Empty, + Packet::Single(frame) => PacketFrameIter::Single(std::iter::once(frame)), + Packet::Multi(frames) => PacketFrameIter::Multi(frames.iter()), + } + } + + /// Returns a mutable borrowing iterator over the frames in this packet. + pub fn frames_mut(&mut self) -> PacketFrameIterMut<'_, 'umem> { + match self { + Packet::Empty => PacketFrameIterMut::Empty, + Packet::Single(frame) => PacketFrameIterMut::Single(std::iter::once(frame)), + Packet::Multi(frames) => PacketFrameIterMut::Multi(frames.iter_mut()), + } + } + + /// Consume this packet, pushing all frames into `buf`. + pub fn drain_to(self, buf: &mut impl FrameBuffer<'umem>) { + for frame in self.into_frames() { + buf.push(frame); + } + } + + /// Returns a consuming iterator over the frames in this packet. + pub fn into_frames(self) -> PacketIntoIter<'umem> { + match self { + Packet::Empty => PacketIntoIter::Empty, + Packet::Single(frame) => PacketIntoIter::Single(std::iter::once(frame)), + Packet::Multi(frames) => PacketIntoIter::Multi(frames.into_iter()), + } + } +} + +impl<'umem, T: Iterator> + ExactSizeIterator> From for Packet<'umem> { + fn from(mut iter: T) -> Self { + if iter.len() == 0 { + Packet::Empty + } else if iter.len() == 1 { + Packet::Single(iter.next().unwrap()) + } else { + Packet::Multi(iter.collect()) + } + } +} + +/// Borrowing iterator over frames in a [`Packet`]. +pub enum PacketFrameIter<'a, 'umem> { + Empty, + Single(std::iter::Once<&'a Frame<'umem>>), + Multi(std::slice::Iter<'a, Frame<'umem>>), +} + +impl<'a, 'umem> Iterator for PacketFrameIter<'a, 'umem> { + type Item = &'a Frame<'umem>; + + fn next(&mut self) -> Option { + match self { + PacketFrameIter::Empty => None, + PacketFrameIter::Single(iter) => iter.next(), + PacketFrameIter::Multi(iter) => iter.next(), + } + } + + fn size_hint(&self) -> (usize, Option) { + match self { + PacketFrameIter::Empty => (0, Some(0)), + PacketFrameIter::Single(iter) => iter.size_hint(), + PacketFrameIter::Multi(iter) => iter.size_hint(), + } + } +} + +impl<'a, 'umem> ExactSizeIterator for PacketFrameIter<'a, 'umem> {} + +/// Mutable borrowing iterator over frames in a [`Packet`]. +pub enum PacketFrameIterMut<'a, 'umem> { + Empty, + Single(std::iter::Once<&'a mut Frame<'umem>>), + Multi(std::slice::IterMut<'a, Frame<'umem>>), +} + +impl<'a, 'umem> Iterator for PacketFrameIterMut<'a, 'umem> { + type Item = &'a mut Frame<'umem>; + + fn next(&mut self) -> Option { + match self { + PacketFrameIterMut::Empty => None, + PacketFrameIterMut::Single(iter) => iter.next(), + PacketFrameIterMut::Multi(iter) => iter.next(), + } + } + + fn size_hint(&self) -> (usize, Option) { + match self { + PacketFrameIterMut::Empty => (0, Some(0)), + PacketFrameIterMut::Single(iter) => iter.size_hint(), + PacketFrameIterMut::Multi(iter) => iter.size_hint(), + } + } +} + +impl<'a, 'umem> ExactSizeIterator for PacketFrameIterMut<'a, 'umem> {} + +/// Consuming iterator over frames in a [`Packet`]. +pub enum PacketIntoIter<'umem> { + Empty, + Single(std::iter::Once>), + Multi(std::vec::IntoIter>), +} + +impl<'umem> Iterator for PacketIntoIter<'umem> { + type Item = Frame<'umem>; + + fn next(&mut self) -> Option { + match self { + PacketIntoIter::Empty => None, + PacketIntoIter::Single(iter) => iter.next(), + PacketIntoIter::Multi(iter) => iter.next(), + } + } + + fn size_hint(&self) -> (usize, Option) { + match self { + PacketIntoIter::Empty => (0, Some(0)), + PacketIntoIter::Single(iter) => iter.size_hint(), + PacketIntoIter::Multi(iter) => iter.size_hint(), + } + } +} + +impl<'umem> ExactSizeIterator for PacketIntoIter<'umem> {} + +#[cfg(test)] +mod tests { + use crate::xdp::frame::Frame; + + use super::*; + + fn make_frame(buf: &mut [u8], len: usize) -> Frame<'_> { + Frame::new(0, buf, len, false) + } + + #[test] + fn single_num_frames() { + let mut buf = [0u8; 64]; + let pkt = Packet::Single(make_frame(&mut buf, 10)); + assert_eq!(pkt.num_frames(), 1); + } + + #[test] + fn multi_num_frames() { + let mut bufs = [[0u8; 64]; 3]; + let frames: Vec<_> = bufs.iter_mut().map(|b| make_frame(b, 10)).collect(); + let pkt = Packet::Multi(frames); + assert_eq!(pkt.num_frames(), 3); + } + + #[test] + fn frames_iter_single() { + let mut buf = [0u8; 64]; + buf[0] = 0xAA; + let pkt = Packet::Single(make_frame(&mut buf, 10)); + let collected: Vec<_> = pkt.frames().collect(); + assert_eq!(collected.len(), 1); + assert_eq!(collected[0][0], 0xAA); + } + + #[test] + fn frames_iter_multi() { + let mut bufs = [[0u8; 64]; 3]; + for (i, b) in bufs.iter_mut().enumerate() { + b[0] = i as u8; + } + let frames: Vec<_> = bufs.iter_mut().map(|b| make_frame(b, 10)).collect(); + let pkt = Packet::Multi(frames); + let collected: Vec<_> = pkt.frames().collect(); + assert_eq!(collected.len(), 3); + for (i, f) in collected.iter().enumerate() { + assert_eq!(f[0], i as u8); + } + } + + #[test] + fn frames_mut_single() { + let mut buf = [0u8; 64]; + buf[0] = 0xAA; + let mut pkt = Packet::Single(make_frame(&mut buf, 10)); + for frame in pkt.frames_mut() { + frame[0] = 0xBB; + } + let collected: Vec<_> = pkt.frames().collect(); + assert_eq!(collected[0][0], 0xBB); + } + + #[test] + fn frames_mut_multi() { + let mut bufs = [[0u8; 64]; 3]; + for (i, b) in bufs.iter_mut().enumerate() { + b[0] = i as u8; + } + let frames: Vec<_> = bufs.iter_mut().map(|b| make_frame(b, 10)).collect(); + let mut pkt = Packet::Multi(frames); + for frame in pkt.frames_mut() { + frame[0] += 10; + } + let collected: Vec<_> = pkt.frames().collect(); + for (i, f) in collected.iter().enumerate() { + assert_eq!(f[0], (i as u8) + 10); + } + } + + #[test] + fn frames_mut_exact_size() { + let mut buf = [0u8; 64]; + let mut pkt = Packet::Single(make_frame(&mut buf, 10)); + let iter = pkt.frames_mut(); + assert_eq!(iter.len(), 1); + assert_eq!(iter.size_hint(), (1, Some(1))); + + let mut bufs = [[0u8; 64]; 3]; + let frames: Vec<_> = bufs.iter_mut().map(|b| make_frame(b, 10)).collect(); + let mut pkt = Packet::Multi(frames); + let iter = pkt.frames_mut(); + assert_eq!(iter.len(), 3); + assert_eq!(iter.size_hint(), (3, Some(3))); + } + + #[test] + fn into_frames_single() { + let mut buf = [0u8; 64]; + buf[0] = 0xBB; + let pkt = Packet::Single(make_frame(&mut buf, 10)); + let collected: Vec<_> = pkt.into_frames().collect(); + assert_eq!(collected.len(), 1); + assert_eq!(collected[0][0], 0xBB); + } + + #[test] + fn into_frames_multi() { + let mut bufs = [[0u8; 64]; 2]; + bufs[0][0] = 0x01; + bufs[1][0] = 0x02; + let frames: Vec<_> = bufs.iter_mut().map(|b| make_frame(b, 10)).collect(); + let pkt = Packet::Multi(frames); + let collected: Vec<_> = pkt.into_frames().collect(); + assert_eq!(collected.len(), 2); + assert_eq!(collected[0][0], 0x01); + assert_eq!(collected[1][0], 0x02); + } + + #[test] + fn frames_iter_exact_size() { + let mut buf = [0u8; 64]; + let pkt = Packet::Single(make_frame(&mut buf, 10)); + let iter = pkt.frames(); + assert_eq!(iter.len(), 1); + assert_eq!(iter.size_hint(), (1, Some(1))); + + let mut bufs = [[0u8; 64]; 3]; + let frames: Vec<_> = bufs.iter_mut().map(|b| make_frame(b, 10)).collect(); + let pkt = Packet::Multi(frames); + let iter = pkt.frames(); + assert_eq!(iter.len(), 3); + assert_eq!(iter.size_hint(), (3, Some(3))); + } + + #[test] + fn into_frames_exact_size() { + let mut buf = [0u8; 64]; + let pkt = Packet::Single(make_frame(&mut buf, 10)); + let iter = pkt.into_frames(); + assert_eq!(iter.len(), 1); + + let mut bufs = [[0u8; 64]; 4]; + let frames: Vec<_> = bufs.iter_mut().map(|b| make_frame(b, 10)).collect(); + let pkt = Packet::Multi(frames); + let iter = pkt.into_frames(); + assert_eq!(iter.len(), 4); + } +} diff --git a/src/net/fragment/plan.rs b/src/net/fragment/plan.rs new file mode 100644 index 0000000..229f51e --- /dev/null +++ b/src/net/fragment/plan.rs @@ -0,0 +1,143 @@ +/// Pre-computed fragment sizing plan for IP fragmentation. +/// +/// Accepts a `transport_header_len` parameter instead of hardcoding +/// `UDP_HEADER_LEN`, making it usable with any transport protocol. +pub(crate) struct FragmentPlan { + /// Maximum data bytes per fragment, 8-byte aligned: `(mtu - ip_overhead) & !7`. + pub max_frag_data: usize, + /// Data bytes in the first fragment after the transport header: + /// `max_frag_data - transport_header_len`. + pub first_chunk: usize, + /// Total number of frames needed for the entire datagram. + pub num_frames: usize, +} + +impl FragmentPlan { + /// Compute a fragmentation plan. + /// + /// - `ip_header_overhead`: total IP-layer overhead per fragment + /// (e.g. 20 for IPv4, 48 for IPv6 + fragment ext). + /// - `transport_header_len`: bytes consumed by the transport header + /// in the first fragment (e.g. 8 for UDP). + /// - `pmtu`: path MTU in bytes. + /// - `payload_len`: application payload length (excluding transport header). + pub fn new( + ip_header_overhead: usize, + transport_header_len: usize, + pmtu: u32, + payload_len: usize, + ) -> Self { + let max_frag_data = (pmtu as usize - ip_header_overhead) & !7; + let first_chunk = max_frag_data - transport_header_len; + let remaining = payload_len.saturating_sub(first_chunk); + let subsequent_chunks = if remaining == 0 { + 0 + } else { + remaining.div_ceil(max_frag_data) + }; + FragmentPlan { + max_frag_data, + first_chunk, + num_frames: 1 + subsequent_chunks, + } + } + + /// Returns `(ip_payload_len, data_to_copy)` for fragment `i`. + /// + /// - `transport_header_len`: size of the transport header (only relevant for i==0). + /// - `payload_offset`: current offset into the application payload. + /// - `payload_len`: total application payload length. + pub fn fragment_sizes( + &self, + i: usize, + transport_header_len: usize, + payload_offset: usize, + payload_len: usize, + ) -> (usize, usize) { + if i == 0 { + let chunk = self.first_chunk.min(payload_len); + (transport_header_len + chunk, chunk) + } else if i == self.num_frames - 1 { + let remaining = payload_len - payload_offset; + (remaining, remaining) + } else { + (self.max_frag_data, self.max_frag_data) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn ipv4_typical() { + // IPv4: ip_overhead=20, transport=8 (UDP), MTU=1500, payload=3000 + let plan = FragmentPlan::new(20, 8, 1500, 3000); + assert_eq!(plan.max_frag_data, 1480); + assert_eq!(plan.first_chunk, 1472); + assert_eq!(plan.num_frames, 3); + } + + #[test] + fn ipv6_typical() { + // IPv6 + frag ext: ip_overhead=48, transport=8, MTU=1500, payload=3000 + let plan = FragmentPlan::new(48, 8, 1500, 3000); + assert_eq!(plan.max_frag_data, 1448); + assert_eq!(plan.first_chunk, 1440); + assert_eq!(plan.num_frames, 3); + } + + #[test] + fn two_fragments() { + let plan = FragmentPlan::new(20, 8, 1500, 1473); + assert_eq!(plan.first_chunk, 1472); + assert_eq!(plan.num_frames, 2); + } + + #[test] + fn exact_first_chunk() { + let plan = FragmentPlan::new(20, 8, 1500, 1472); + assert_eq!(plan.num_frames, 1); + } + + #[test] + fn eight_byte_alignment() { + let plan = FragmentPlan::new(20, 8, 1505, 3000); + assert_eq!(plan.max_frag_data % 8, 0); + assert_eq!(plan.max_frag_data, 1480); + } + + #[test] + fn fragment_sizes_three() { + let plan = FragmentPlan::new(20, 8, 1500, 3000); + assert_eq!(plan.fragment_sizes(0, 8, 0, 3000), (1480, 1472)); + assert_eq!(plan.fragment_sizes(1, 8, 1472, 3000), (1480, 1480)); + assert_eq!(plan.fragment_sizes(2, 8, 2952, 3000), (48, 48)); + } + + #[test] + fn fragment_sizes_two() { + let plan = FragmentPlan::new(20, 8, 1500, 1473); + assert_eq!(plan.fragment_sizes(0, 8, 0, 1473), (1480, 1472)); + assert_eq!(plan.fragment_sizes(1, 8, 1472, 1473), (1, 1)); + } + + #[test] + fn large_payload() { + let plan = FragmentPlan::new(20, 8, 1500, 65000); + assert_eq!(plan.max_frag_data, 1480); + assert_eq!(plan.first_chunk, 1472); + assert_eq!(plan.num_frames, 44); + } + + #[test] + fn custom_transport_header_len() { + // Transport header = 20 bytes (e.g. TCP), IPv4 overhead = 20 + let plan = FragmentPlan::new(20, 20, 1500, 3000); + assert_eq!(plan.max_frag_data, 1480); + assert_eq!(plan.first_chunk, 1460); // 1480 - 20 + // remaining = 3000 - 1460 = 1540, subsequent = ceil(1540/1480) = 2 + assert_eq!(plan.num_frames, 3); + } +} diff --git a/src/net/fragment/reader.rs b/src/net/fragment/reader.rs new file mode 100644 index 0000000..0013804 --- /dev/null +++ b/src/net/fragment/reader.rs @@ -0,0 +1,755 @@ +use std::{ + collections::{BTreeMap, btree_map::Entry}, + hash::Hash, +}; + +use coarsetime::{Duration, Instant}; +use rustc_hash::FxHashMap; + +use crate::{ + net::wire::ip::{ + FRAGMENT_EXT_LEN, Ipv4Address, Ipv4Header, Ipv6Address, Ipv6FragmentHeader, Ipv6Header, + }, + xdp::frame::{Frame, FrameBuffer}, +}; + +use super::pkt::Packet; + +/// A fully reassembled IP datagram, protocol-agnostic. +/// +/// Does not parse transport-layer metadata — the caller is responsible +/// for interpreting the payload based on `protocol`. +pub struct ReassembledPacket<'umem> { + /// The reassembled frames (original fragment frames, sorted by offset). + pub packet: Packet<'umem>, + /// IP protocol number from the fragment headers (e.g. 17 for UDP, 6 for TCP). + pub protocol: u8, +} + +#[derive(Clone, PartialEq, Eq, Hash)] +struct Ipv4FragmentKey { + src_addr: Ipv4Address, + dst_addr: Ipv4Address, + protocol: u8, + identification: u16, +} + +#[derive(Clone, PartialEq, Eq, Hash)] +struct Ipv6FragmentKey { + src_addr: Ipv6Address, + dst_addr: Ipv6Address, + identification: u32, +} + +struct ReassemblyEntry<'umem> { + /// Fragments keyed by byte offset, kept sorted via BTreeMap. + fragments: BTreeMap>, + /// Known when the final fragment (MF=0) arrives. + total_len: Option, + /// Sum of all received fragment data lengths. + received_len: usize, + first_received: Instant, +} + +impl<'umem> ReassemblyEntry<'umem> { + fn new() -> Self { + Self { + fragments: BTreeMap::new(), + total_len: None, + received_len: 0, + first_received: Instant::now(), + } + } + + /// Insert a fragment by offset. Returns `Some(frame)` if a + /// duplicate offset is already present. + fn insert( + &mut self, + offset: usize, + data_len: usize, + more_fragments: bool, + frame: Frame<'umem>, + ) -> Option> { + match self.fragments.entry(offset) { + Entry::Occupied(_) => Some(frame), // duplicate + Entry::Vacant(entry) => { + entry.insert(frame); + self.received_len += data_len; + if !more_fragments { + self.total_len = Some(offset + data_len); + } + None + } + } + } + + fn is_complete(&self) -> bool { + match self.total_len { + Some(total) => self.received_len == total, + None => false, + } + } + + fn into_packet(self) -> Packet<'umem> { + Packet::Multi(self.fragments.into_values().collect()) + } +} + +/// Evict stale reassembly entries from a map, returning held frames. +fn evict_map<'umem, K: Eq + Hash>( + map: &mut FxHashMap>, + now: Instant, + timeout: Duration, + rx_return: &mut impl FrameBuffer<'umem>, +) { + map.retain(|_, entry| { + if now.duration_since(entry.first_received) > timeout { + for (_, frame) in std::mem::take(&mut entry.fragments) { + rx_return.push(frame); + } + false + } else { + true + } + }); +} + +/// Protocol-generic IP fragment reassembly engine. +/// +/// Does not parse transport-layer headers or extract port numbers. It +/// operates purely at the IP fragmentation level, making it usable for +/// any IP protocol. +pub struct FragmentReader<'umem> { + ipv4_reassembly: FxHashMap>, + ipv6_reassembly: FxHashMap>, + max_entries: usize, +} + +impl<'umem> FragmentReader<'umem> { + /// Create a new reader with the given maximum number of concurrent + /// reassembly entries (across both IPv4 and IPv6). + pub fn new(max_entries: usize) -> Self { + Self { + ipv4_reassembly: FxHashMap::with_capacity_and_hasher(max_entries, Default::default()), + ipv6_reassembly: FxHashMap::with_capacity_and_hasher(max_entries, Default::default()), + max_entries, + } + } + + fn has_capacity_for_new_entry(&self) -> bool { + self.ipv4_reassembly.len() + self.ipv6_reassembly.len() < self.max_entries + } + + /// Process an IPv4 fragment. + /// + /// The frame must contain a valid Ethernet + IPv4 header with fragment + /// fields set (MF and/or non-zero offset). Non-fragment frames should + /// not be passed here. + /// + /// Returns `Some(ReassembledPacket)` when all fragments of a datagram + /// have been received, `None` otherwise. Duplicate fragments are pushed + /// to `rx_return`. + pub fn process_ipv4( + &mut self, + frame: Frame<'umem>, + rx_return: &mut impl FrameBuffer<'umem>, + ) -> Option> { + let ip = Ipv4Header::from_bytes(&frame); + let src_addr = ip.src_addr; + let dst_addr = ip.dst_addr; + let protocol = ip.protocol; + let identification = ip.identification(); + let frag_offset_bytes = ip.fragment_offset() as usize * 8; + let more_fragments = ip.more_fragments(); + let payload_len = ip.payload_len(); + + // Single-fragment datagram: offset=0 and MF=0 means the entire + // datagram is in this one frame. Return immediately without + // touching the reassembly map. + if frag_offset_bytes == 0 && !more_fragments { + return Some(ReassembledPacket { + packet: Packet::Single(frame), + protocol, + }); + } + + let key = Ipv4FragmentKey { + src_addr, + dst_addr, + protocol, + identification, + }; + + if !self.ipv4_reassembly.contains_key(&key) && !self.has_capacity_for_new_entry() { + rx_return.push(frame); + return None; + } + + let remove_key = key.clone(); + let entry = self + .ipv4_reassembly + .entry(key) + .or_insert_with(ReassemblyEntry::new); + + if let Some(dup_frame) = entry.insert(frag_offset_bytes, payload_len, more_fragments, frame) + { + rx_return.push(dup_frame); + return None; + } + + if entry.is_complete() { + let entry = self.ipv4_reassembly.remove(&remove_key).unwrap(); + let packet = entry.into_packet(); + return Some(ReassembledPacket { packet, protocol }); + } + + None + } + + /// Process an IPv6 fragment. + /// + /// `frag_ext_offset` is the byte offset (from frame start) where the + /// IPv6 Fragment Extension Header begins. The frame must contain at + /// least `frag_ext_offset + FRAGMENT_EXT_LEN` bytes. + /// + /// Returns `Some(ReassembledPacket)` when all fragments of a datagram + /// have been received, `None` otherwise. + pub fn process_ipv6( + &mut self, + frame: Frame<'umem>, + frag_ext_offset: usize, + rx_return: &mut impl FrameBuffer<'umem>, + ) -> Option> { + if frame.len() < frag_ext_offset + FRAGMENT_EXT_LEN { + rx_return.push(frame); + return None; + } + + let frag_hdr = Ipv6FragmentHeader::from_bytes_at(&frame, frag_ext_offset); + let protocol = frag_hdr.next_header; + let frag_offset_bytes = frag_hdr.fragment_offset() as usize * 8; + let more_fragments = frag_hdr.more_fragments(); + let identification = frag_hdr.identification(); + + // Single-fragment datagram (atomic fragment): offset=0 and MF=0 + // means the entire datagram is in this one frame. Return + // immediately without touching the reassembly map. + if frag_offset_bytes == 0 && !more_fragments { + return Some(ReassembledPacket { + packet: Packet::Single(frame), + protocol, + }); + } + + let ip = Ipv6Header::from_bytes(&frame); + let src_addr = ip.src_addr; + let dst_addr = ip.dst_addr; + + let data_start = frag_ext_offset + FRAGMENT_EXT_LEN; + let data_len = if frame.len() > data_start { + frame.len() - data_start + } else { + 0 + }; + + let key = Ipv6FragmentKey { + src_addr, + dst_addr, + identification, + }; + + if !self.ipv6_reassembly.contains_key(&key) && !self.has_capacity_for_new_entry() { + rx_return.push(frame); + return None; + } + + let remove_key = key.clone(); + let entry = self + .ipv6_reassembly + .entry(key) + .or_insert_with(ReassemblyEntry::new); + + if let Some(dup_frame) = entry.insert(frag_offset_bytes, data_len, more_fragments, frame) { + rx_return.push(dup_frame); + return None; + } + + if entry.is_complete() { + let entry = self.ipv6_reassembly.remove(&remove_key).unwrap(); + let packet = entry.into_packet(); + return Some(ReassembledPacket { packet, protocol }); + } + + None + } + + /// Evict reassembly entries older than `timeout`, returning held frames. + pub fn evict_stale( + &mut self, + now: Instant, + timeout: Duration, + rx_return: &mut impl FrameBuffer<'umem>, + ) { + evict_map(&mut self.ipv4_reassembly, now, timeout, rx_return); + evict_map(&mut self.ipv6_reassembly, now, timeout, rx_return); + } + + /// Number of in-progress reassembly entries (IPv4 + IPv6). + pub fn pending_entries(&self) -> usize { + self.ipv4_reassembly.len() + self.ipv6_reassembly.len() + } +} + +#[cfg(test)] +mod tests { + use crate::{ + net::{ + fragment::FragmentWriter, + wire::{ + ethernet::{EthernetFrame, MacAddress}, + ip::{IPV4_MIN_HEADER_LEN, IPV6_HEADER_LEN, IpProtocols}, + udp::UdpHeader, + }, + }, + xdp::frame::{BasicFrameBuffer, Frame}, + }; + + use super::*; + + const ETH_LEN: usize = size_of::(); + const SRC_V4: [u8; 4] = [192, 168, 1, 1]; + const DST_V4: [u8; 4] = [10, 0, 0, 1]; + const SRC_V6: [u8; 16] = [0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]; + const DST_V6: [u8; 16] = [0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]; + + /// Build an IPv4 fragment frame. + fn build_ipv4_fragment( + buf: &mut [u8], + id: u16, + protocol: u8, + frag_offset_8: u16, + more: bool, + data: &[u8], + ) -> usize { + let ip_total = IPV4_MIN_HEADER_LEN + data.len(); + let frame_len = ETH_LEN + ip_total; + buf[12..14].copy_from_slice(&[0x08, 0x00]); + buf[14] = 0x45; + buf[16..18].copy_from_slice(&(ip_total as u16).to_be_bytes()); + buf[18..20].copy_from_slice(&id.to_be_bytes()); + let flags = if more { + 0x2000 | frag_offset_8 + } else { + frag_offset_8 + }; + buf[20..22].copy_from_slice(&flags.to_be_bytes()); + buf[23] = protocol; + buf[26..30].copy_from_slice(&SRC_V4); + buf[30..34].copy_from_slice(&DST_V4); + let p = ETH_LEN + IPV4_MIN_HEADER_LEN; + buf[p..p + data.len()].copy_from_slice(data); + frame_len + } + + /// Build an IPv6 fragment frame. + fn build_ipv6_fragment( + buf: &mut [u8], + id: u32, + next_header: u8, + frag_offset_8: u16, + more: bool, + data: &[u8], + ) -> usize { + let payload_len = FRAGMENT_EXT_LEN + data.len(); + let frame_len = ETH_LEN + IPV6_HEADER_LEN + payload_len; + buf[12..14].copy_from_slice(&[0x86, 0xDD]); + buf[14] = 0x60; + buf[18..20].copy_from_slice(&(payload_len as u16).to_be_bytes()); + buf[20] = 44; // Fragment extension header + buf[22..38].copy_from_slice(&SRC_V6); + buf[38..54].copy_from_slice(&DST_V6); + let f = ETH_LEN + IPV6_HEADER_LEN; + buf[f] = next_header; + let frag_offset_mf = (frag_offset_8 << 3) | if more { 1 } else { 0 }; + buf[f + 2..f + 4].copy_from_slice(&frag_offset_mf.to_be_bytes()); + buf[f + 4..f + 8].copy_from_slice(&id.to_be_bytes()); + let d = f + FRAGMENT_EXT_LEN; + buf[d..d + data.len()].copy_from_slice(data); + frame_len + } + + const FRAG_OFF: usize = ETH_LEN + IPV6_HEADER_LEN; + + #[test] + fn ipv4_two_fragment_reassembly() { + let mut reader = FragmentReader::new(16); + let mut rx = BasicFrameBuffer::new(16); + + // First fragment: 16 bytes data, MF=1, offset=0 + let mut buf1 = [0u8; 256]; + let len1 = build_ipv4_fragment(&mut buf1, 42, IpProtocols::Udp, 0, true, &[0xAA; 16]); + let f1 = Frame::new(0, &mut buf1, len1, false); + assert!(reader.process_ipv4(f1, &mut rx).is_none()); + assert_eq!(reader.pending_entries(), 1); + + // Last fragment: 8 bytes data, MF=0, offset=2 (2*8=16 bytes in) + let mut buf2 = [0u8; 256]; + let len2 = build_ipv4_fragment(&mut buf2, 42, IpProtocols::Udp, 2, false, &[0xBB; 8]); + let f2 = Frame::new(1, &mut buf2, len2, false); + let result = reader.process_ipv4(f2, &mut rx).expect("should complete"); + assert_eq!(result.protocol, IpProtocols::Udp); + assert_eq!(result.packet.num_frames(), 2); + assert_eq!(reader.pending_entries(), 0); + } + + #[test] + fn ipv4_out_of_order() { + let mut reader = FragmentReader::new(16); + let mut rx = BasicFrameBuffer::new(16); + + // Send last fragment first + let mut buf2 = [0u8; 256]; + let len2 = build_ipv4_fragment(&mut buf2, 77, IpProtocols::Udp, 2, false, &[0u8; 8]); + let f2 = Frame::new(1, &mut buf2, len2, false); + assert!(reader.process_ipv4(f2, &mut rx).is_none()); + + // Then first fragment + let mut buf1 = [0u8; 256]; + let len1 = build_ipv4_fragment(&mut buf1, 77, IpProtocols::Udp, 0, true, &[0u8; 16]); + let f1 = Frame::new(0, &mut buf1, len1, false); + let result = reader.process_ipv4(f1, &mut rx).expect("should complete"); + assert_eq!(result.protocol, IpProtocols::Udp); + assert_eq!(result.packet.num_frames(), 2); + } + + #[test] + fn ipv4_duplicate_returned() { + let mut reader = FragmentReader::new(16); + let mut rx = BasicFrameBuffer::new(16); + + let mut buf1 = [0u8; 256]; + let len = build_ipv4_fragment(&mut buf1, 99, IpProtocols::Udp, 0, true, &[0u8; 16]); + let f1 = Frame::new(0, &mut buf1, len, false); + reader.process_ipv4(f1, &mut rx); + + // Duplicate offset + let mut buf2 = [0u8; 256]; + let len = build_ipv4_fragment(&mut buf2, 99, IpProtocols::Udp, 0, true, &[0u8; 16]); + let f2 = Frame::new(1, &mut buf2, len, false); + assert!(reader.process_ipv4(f2, &mut rx).is_none()); + assert_eq!(rx.num_frames(), 1); // dup returned + } + + #[test] + fn ipv4_capacity_exceeded() { + let mut reader = FragmentReader::new(1); + let mut rx = BasicFrameBuffer::new(16); + + let mut buf1 = [0u8; 256]; + let len = build_ipv4_fragment(&mut buf1, 1, IpProtocols::Udp, 0, true, &[0u8; 16]); + let f1 = Frame::new(0, &mut buf1, len, false); + reader.process_ipv4(f1, &mut rx); + assert_eq!(reader.pending_entries(), 1); + + // Different id → new entry rejected + let mut buf2 = [0u8; 256]; + let len = build_ipv4_fragment(&mut buf2, 2, IpProtocols::Udp, 0, true, &[0u8; 16]); + let f2 = Frame::new(1, &mut buf2, len, false); + assert!(reader.process_ipv4(f2, &mut rx).is_none()); + assert_eq!(rx.num_frames(), 1); + assert_eq!(reader.pending_entries(), 1); + } + + #[test] + fn ipv4_protocol_in_key() { + // Fragments with same id but different protocols are different datagrams. + let mut reader = FragmentReader::new(16); + let mut rx = BasicFrameBuffer::new(16); + + let mut buf1 = [0u8; 256]; + let len = build_ipv4_fragment(&mut buf1, 42, IpProtocols::Udp, 0, true, &[0u8; 16]); + let f1 = Frame::new(0, &mut buf1, len, false); + reader.process_ipv4(f1, &mut rx); + + let mut buf2 = [0u8; 256]; + let len = build_ipv4_fragment(&mut buf2, 42, IpProtocols::Tcp, 0, true, &[0u8; 16]); + let f2 = Frame::new(1, &mut buf2, len, false); + reader.process_ipv4(f2, &mut rx); + + assert_eq!(reader.pending_entries(), 2); // two separate entries + } + + #[test] + fn ipv6_two_fragment_reassembly() { + let mut reader = FragmentReader::new(16); + let mut rx = BasicFrameBuffer::new(16); + + let mut buf1 = [0u8; 256]; + let len1 = build_ipv6_fragment(&mut buf1, 100, IpProtocols::Udp, 0, true, &[0xAA; 16]); + let f1 = Frame::new(0, &mut buf1, len1, false); + assert!(reader.process_ipv6(f1, FRAG_OFF, &mut rx).is_none()); + assert_eq!(reader.pending_entries(), 1); + + let mut buf2 = [0u8; 256]; + let len2 = build_ipv6_fragment(&mut buf2, 100, IpProtocols::Udp, 2, false, &[0xBB; 8]); + let f2 = Frame::new(1, &mut buf2, len2, false); + let result = reader + .process_ipv6(f2, FRAG_OFF, &mut rx) + .expect("should complete"); + assert_eq!(result.protocol, IpProtocols::Udp); + assert_eq!(result.packet.num_frames(), 2); + assert_eq!(reader.pending_entries(), 0); + } + + #[test] + fn ipv6_out_of_order() { + let mut reader = FragmentReader::new(16); + let mut rx = BasicFrameBuffer::new(16); + + // Last fragment first + let mut buf2 = [0u8; 256]; + let len2 = build_ipv6_fragment(&mut buf2, 200, IpProtocols::Udp, 2, false, &[0u8; 8]); + let f2 = Frame::new(1, &mut buf2, len2, false); + assert!(reader.process_ipv6(f2, FRAG_OFF, &mut rx).is_none()); + + // Then first + let mut buf1 = [0u8; 256]; + let len1 = build_ipv6_fragment(&mut buf1, 200, IpProtocols::Udp, 0, true, &[0u8; 16]); + let f1 = Frame::new(0, &mut buf1, len1, false); + let result = reader + .process_ipv6(f1, FRAG_OFF, &mut rx) + .expect("should complete"); + assert_eq!(result.protocol, IpProtocols::Udp); + assert_eq!(result.packet.num_frames(), 2); + } + + #[test] + fn ipv6_duplicate_returned() { + let mut reader = FragmentReader::new(16); + let mut rx = BasicFrameBuffer::new(16); + + let mut buf1 = [0u8; 256]; + let len = build_ipv6_fragment(&mut buf1, 50, IpProtocols::Udp, 0, true, &[0u8; 16]); + let f1 = Frame::new(0, &mut buf1, len, false); + reader.process_ipv6(f1, FRAG_OFF, &mut rx); + + let mut buf2 = [0u8; 256]; + let len = build_ipv6_fragment(&mut buf2, 50, IpProtocols::Udp, 0, true, &[0u8; 16]); + let f2 = Frame::new(1, &mut buf2, len, false); + assert!(reader.process_ipv6(f2, FRAG_OFF, &mut rx).is_none()); + assert_eq!(rx.num_frames(), 1); + } + + #[test] + fn ipv6_capacity_exceeded() { + let mut reader = FragmentReader::new(1); + let mut rx = BasicFrameBuffer::new(16); + + let mut buf1 = [0u8; 256]; + let len = build_ipv6_fragment(&mut buf1, 1, IpProtocols::Udp, 0, true, &[0u8; 16]); + let f1 = Frame::new(0, &mut buf1, len, false); + reader.process_ipv6(f1, FRAG_OFF, &mut rx); + assert_eq!(reader.pending_entries(), 1); + + let mut buf2 = [0u8; 256]; + let len = build_ipv6_fragment(&mut buf2, 2, IpProtocols::Udp, 0, true, &[0u8; 16]); + let f2 = Frame::new(1, &mut buf2, len, false); + assert!(reader.process_ipv6(f2, FRAG_OFF, &mut rx).is_none()); + assert_eq!(rx.num_frames(), 1); + } + + #[test] + fn ipv6_too_short_for_frag_ext() { + let mut reader = FragmentReader::new(16); + let mut rx = BasicFrameBuffer::new(16); + let mut buf = [0u8; 256]; + buf[14] = 0x60; + buf[20] = 44; + buf[22..38].copy_from_slice(&SRC_V6); + buf[38..54].copy_from_slice(&DST_V6); + // Frame too short for fragment ext + let frame = Frame::new(0, &mut buf, FRAG_OFF + 2, false); + assert!(reader.process_ipv6(frame, FRAG_OFF, &mut rx).is_none()); + assert_eq!(rx.num_frames(), 1); + } + + #[test] + fn pending_entries_counts_both() { + let mut reader = FragmentReader::new(16); + let mut rx = BasicFrameBuffer::new(16); + assert_eq!(reader.pending_entries(), 0); + + let mut buf1 = [0u8; 256]; + let len = build_ipv4_fragment(&mut buf1, 1, IpProtocols::Udp, 0, true, &[0u8; 16]); + let f1 = Frame::new(0, &mut buf1, len, false); + reader.process_ipv4(f1, &mut rx); + + let mut buf2 = [0u8; 256]; + let len = build_ipv6_fragment(&mut buf2, 1, IpProtocols::Udp, 0, true, &[0u8; 16]); + let f2 = Frame::new(1, &mut buf2, len, false); + reader.process_ipv6(f2, FRAG_OFF, &mut rx); + + assert_eq!(reader.pending_entries(), 2); + } + + #[test] + fn evict_stale_returns_frames() { + let mut reader = FragmentReader::new(16); + let mut rx = BasicFrameBuffer::new(16); + + let mut buf = [0u8; 256]; + let len = build_ipv4_fragment(&mut buf, 1, IpProtocols::Udp, 0, true, &[0u8; 16]); + let frame = Frame::new(0, &mut buf, len, false); + reader.process_ipv4(frame, &mut rx); + assert_eq!(reader.pending_entries(), 1); + + std::thread::sleep(std::time::Duration::from_millis(5)); + reader.evict_stale(Instant::now(), Duration::from_ticks(0), &mut rx); + assert_eq!(reader.pending_entries(), 0); + assert_eq!(rx.num_frames(), 1); + } + + #[test] + fn evict_stale_keeps_fresh() { + let mut reader = FragmentReader::new(16); + let mut rx = BasicFrameBuffer::new(16); + + let mut buf = [0u8; 256]; + let len = build_ipv4_fragment(&mut buf, 1, IpProtocols::Udp, 0, true, &[0u8; 16]); + let frame = Frame::new(0, &mut buf, len, false); + reader.process_ipv4(frame, &mut rx); + + reader.evict_stale(Instant::now(), Duration::from_secs(3600), &mut rx); + assert_eq!(reader.pending_entries(), 1); + assert_eq!(rx.num_frames(), 0); + } + + #[test] + fn ipv4_roundtrip() { + let src_mac = MacAddress::new([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]); + let dst_mac = MacAddress::new([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]); + let src_ip = Ipv4Address::new(SRC_V4); + let dst_ip = Ipv4Address::new(DST_V4); + + let payload = vec![0x42u8; 3000]; + let transport = UdpHeader::new(12345, 53, (8 + payload.len()) as u16, [0xAB, 0xCD]); + + let mut bufs: Vec> = (0..32).map(|_| vec![0u8; 2048]).collect(); + let mut free = BasicFrameBuffer::new(64); + for (i, buf) in bufs.iter_mut().enumerate() { + free.push(Frame::new(i as u64, buf.as_mut_slice(), 1, false)); + } + + let packet = FragmentWriter::fragment_ipv4( + src_mac, dst_mac, src_ip, dst_ip, 64, &transport, &payload, 1500, false, &mut free, + ) + .unwrap(); + + // Now reassemble. + let mut reader = FragmentReader::new(16); + let mut rx = BasicFrameBuffer::new(16); + let mut result = None; + for frame in packet.into_frames() { + if let Some(r) = reader.process_ipv4(frame, &mut rx) { + result = Some(r); + } + } + let reassembled = result.expect("should complete reassembly"); + assert_eq!(reassembled.protocol, IpProtocols::Udp); + + // Verify payload from reassembled frames. + let mut data = Vec::new(); + for (i, frame) in reassembled.packet.frames().enumerate() { + let ip = Ipv4Header::from_bytes(frame); + let data_start = ip.payload_offset(); + if i == 0 { + // First fragment includes transport header. + data.extend_from_slice(&frame[data_start + 8..frame.len()]); + } else { + data.extend_from_slice(&frame[data_start..frame.len()]); + } + } + assert_eq!(data, payload); + } + + #[test] + fn ipv6_roundtrip() { + let src_mac = MacAddress::new([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]); + let dst_mac = MacAddress::new([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]); + let src_ip = Ipv6Address::new(SRC_V6); + let dst_ip = Ipv6Address::new(DST_V6); + + let payload = vec![0x77u8; 3000]; + let transport = UdpHeader::new(12345, 53, (8 + payload.len()) as u16, [0xAB, 0xCD]); + + let mut bufs: Vec> = (0..32).map(|_| vec![0u8; 2048]).collect(); + let mut free = BasicFrameBuffer::new(64); + for (i, buf) in bufs.iter_mut().enumerate() { + free.push(Frame::new(i as u64, buf.as_mut_slice(), 1, false)); + } + + let packet = FragmentWriter::fragment_ipv6( + src_mac, dst_mac, src_ip, dst_ip, 64, &transport, &payload, 1500, &mut free, + ) + .unwrap(); + + // Now reassemble. + let frag_ext_offset = ETH_LEN + IPV6_HEADER_LEN; + let mut reader = FragmentReader::new(16); + let mut rx = BasicFrameBuffer::new(16); + let mut result = None; + for frame in packet.into_frames() { + if let Some(r) = reader.process_ipv6(frame, frag_ext_offset, &mut rx) { + result = Some(r); + } + } + let reassembled = result.expect("should complete reassembly"); + assert_eq!(reassembled.protocol, IpProtocols::Udp); + + // Verify payload. + let mut data = Vec::new(); + for (i, frame) in reassembled.packet.frames().enumerate() { + let data_start = frag_ext_offset + FRAGMENT_EXT_LEN; + if i == 0 { + data.extend_from_slice(&frame[data_start + 8..frame.len()]); + } else { + data.extend_from_slice(&frame[data_start..frame.len()]); + } + } + assert_eq!(data, payload); + } + + #[test] + fn ipv4_single_frame_roundtrip() { + let src_mac = MacAddress::new([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]); + let dst_mac = MacAddress::new([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]); + let src_ip = Ipv4Address::new(SRC_V4); + let dst_ip = Ipv4Address::new(DST_V4); + + let payload = b"small"; + let transport = UdpHeader::new(1234, 5678, (8 + payload.len()) as u16, [0x00, 0x00]); + + let mut bufs: Vec> = (0..4).map(|_| vec![0u8; 2048]).collect(); + let mut free = BasicFrameBuffer::new(8); + for (i, buf) in bufs.iter_mut().enumerate() { + free.push(Frame::new(i as u64, buf.as_mut_slice(), 1, false)); + } + + let packet = FragmentWriter::fragment_ipv4( + src_mac, dst_mac, src_ip, dst_ip, 64, &transport, payload, 1500, false, &mut free, + ) + .unwrap(); + + // Single frame → not a fragment, returned directly as Packet::Single. + // Just verify it's a single frame with correct length. + assert_eq!(packet.num_frames(), 1); + match &packet { + Packet::Single(frame) => { + let ip = Ipv4Header::from_bytes(frame); + assert!(ip.dont_fragment()); + assert!(!ip.is_fragment()); + } + _ => panic!("expected Single"), + } + } +} diff --git a/src/net/fragment/transport.rs b/src/net/fragment/transport.rs new file mode 100644 index 0000000..e30ff43 --- /dev/null +++ b/src/net/fragment/transport.rs @@ -0,0 +1,111 @@ +use crate::net::wire::{ + ip::IpProtocols, + tcp::{TCP_HEADER_LEN, TcpHeader}, + udp::{UDP_HEADER_LEN, UdpHeader}, +}; + +/// Describes a transport-layer header for use with generic IP fragmentation. +/// +/// Implementations provide the protocol number, header size, and serialization +/// so that `FragmentWriter` can place the transport header in the first fragment +/// without being coupled to any particular protocol. +pub trait TransportHeader { + /// IP protocol number (e.g. 17 for UDP, 6 for TCP). + fn protocol(&self) -> u8; + + /// Serialized header length in bytes. + fn header_len(&self) -> usize; + + /// Write the header bytes into `buf[0..header_len()]`. + /// + /// The caller guarantees `buf.len() >= header_len()`. + fn write_to(&self, buf: &mut [u8]); +} + +impl TransportHeader for TcpHeader { + #[inline] + fn protocol(&self) -> u8 { + IpProtocols::Tcp + } + + #[inline] + fn header_len(&self) -> usize { + TCP_HEADER_LEN + } + + #[inline] + fn write_to(&self, buf: &mut [u8]) { + let bytes = + unsafe { std::slice::from_raw_parts(self as *const Self as *const u8, TCP_HEADER_LEN) }; + buf[..TCP_HEADER_LEN].copy_from_slice(bytes); + } +} + +impl TransportHeader for UdpHeader { + #[inline] + fn protocol(&self) -> u8 { + IpProtocols::Udp + } + + #[inline] + fn header_len(&self) -> usize { + UDP_HEADER_LEN + } + + #[inline] + fn write_to(&self, buf: &mut [u8]) { + let bytes = + unsafe { std::slice::from_raw_parts(self as *const Self as *const u8, UDP_HEADER_LEN) }; + buf[..UDP_HEADER_LEN].copy_from_slice(bytes); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn udp_header_as_transport_protocol() { + let hdr = UdpHeader::new(1234, 5678, 100, [0xAB, 0xCD]); + assert_eq!(hdr.protocol(), 17); + } + + #[test] + fn udp_header_as_transport_len() { + let hdr = UdpHeader::new(0, 0, 0, [0; 2]); + assert_eq!(hdr.header_len(), 8); + } + + #[test] + fn udp_header_as_transport_write() { + let hdr = UdpHeader::new(0x3039, 0x0035, 0x000C, [0xAB, 0xCD]); + let mut buf = [0u8; 8]; + hdr.write_to(&mut buf); + assert_eq!(buf, [0x30, 0x39, 0x00, 0x35, 0x00, 0x0C, 0xAB, 0xCD]); + } + + /// Custom transport header to verify trait generality. + struct FakeTransport; + + impl TransportHeader for FakeTransport { + fn protocol(&self) -> u8 { + 99 + } + fn header_len(&self) -> usize { + 4 + } + fn write_to(&self, buf: &mut [u8]) { + buf[0..4].copy_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]); + } + } + + #[test] + fn custom_transport_header() { + let hdr = FakeTransport; + assert_eq!(hdr.protocol(), 99); + assert_eq!(hdr.header_len(), 4); + let mut buf = [0u8; 4]; + hdr.write_to(&mut buf); + assert_eq!(buf, [0xDE, 0xAD, 0xBE, 0xEF]); + } +} diff --git a/src/net/fragment/writer.rs b/src/net/fragment/writer.rs new file mode 100644 index 0000000..d86c290 --- /dev/null +++ b/src/net/fragment/writer.rs @@ -0,0 +1,692 @@ +use crate::{ + net::wire::{ + ethernet::{self, EtherTypes, EthernetFrame, MacAddress}, + ip::{ + FRAGMENT_EXT_LEN, IPV4_MIN_HEADER_LEN, IPV6_HEADER_LEN, Ipv4Address, Ipv4Header, + Ipv6Address, Ipv6FragmentHeader, Ipv6Header, + }, + }, + xdp::{ + error::{NonBlocking, WouldBlock}, + frame::{Frame, FrameBuffer}, + }, +}; + +use super::{id, pkt::Packet, plan::FragmentPlan, transport::TransportHeader}; + +const ETH_HEADER_LEN: usize = size_of::(); + +/// Stateless outbound IP fragmentation. +/// +/// Provides static-like methods that fragment a payload into one or more +/// UMEM-backed frames, ready for transmission. +pub struct FragmentWriter; + +impl FragmentWriter { + /// Fragment a payload into IPv4 frames. + /// + /// - Writes Ethernet + IPv4 headers per fragment. + /// - Fragment 0 includes the transport header (via `transport.write_to()`). + /// - Non-last fragments have 8-byte-aligned data lengths. + /// - Returns `Packet::Single` if the payload fits in one frame, `Packet::Multi` otherwise. + /// - Returns `Err(WouldBlock)` if `free_frames` has insufficient frames. + pub fn fragment_ipv4<'umem>( + src_mac: MacAddress, + dst_mac: MacAddress, + src_ip: Ipv4Address, + dst_ip: Ipv4Address, + ttl: u8, + transport: &impl TransportHeader, + payload: &[u8], + mtu: u32, + tx_offload: bool, + free_frames: &mut impl FrameBuffer<'umem>, + ) -> NonBlocking> { + let transport_header_len = transport.header_len(); + let max_payload = mtu as usize - IPV4_MIN_HEADER_LEN - transport_header_len; + + if payload.len() <= max_payload { + let protocol = transport.protocol(); + let ip_total_len = (IPV4_MIN_HEADER_LEN + transport_header_len + payload.len()) as u16; + let identification = id::next_ipv4_id(); + + return Self::build_single( + transport, + payload, + IPV4_MIN_HEADER_LEN, + free_frames, + |frame| { + ethernet::write_ethernet_header( + &mut *frame, + dst_mac, + src_mac, + EtherTypes::IPv4, + ); + + let ip = Ipv4Header::from_bytes_mut(frame); + ip.version_ihl = 0x45; + ip.dscp_ecn = 0; + ip.total_length = ip_total_len.to_be_bytes(); + ip.identification = identification.to_be_bytes(); + ip.flags_fragment_offset = [0x40, 0x00]; // DF set + ip.ttl = ttl; + ip.protocol = protocol; + ip.header_checksum = [0, 0]; + ip.src_addr = src_ip; + ip.dst_addr = dst_ip; + if !tx_offload { + ip.fill_checksum(); + } + }, + ); + } + + let plan = FragmentPlan::new( + IPV4_MIN_HEADER_LEN, + transport_header_len, + mtu, + payload.len(), + ); + + if free_frames.num_frames() < plan.num_frames { + return Err(WouldBlock); + } + + let identification = id::next_ipv4_id(); + + Self::fragment_loop( + transport, + payload, + IPV4_MIN_HEADER_LEN, + &plan, + free_frames, + |frame, frag_data_len, frag_byte_offset, is_last| { + ethernet::write_ethernet_header(&mut *frame, dst_mac, src_mac, EtherTypes::IPv4); + + let ip_total_len = (IPV4_MIN_HEADER_LEN + frag_data_len) as u16; + let frag_offset_units = (frag_byte_offset / 8) as u16; + let mf: u8 = if is_last { 0 } else { 0x20 }; + let flags_frag_hi = mf | ((frag_offset_units >> 8) as u8 & 0x1F); + let flags_frag_lo = frag_offset_units as u8; + + let ip = Ipv4Header::from_bytes_mut(frame); + ip.version_ihl = 0x45; + ip.dscp_ecn = 0; + ip.total_length = ip_total_len.to_be_bytes(); + ip.identification = identification.to_be_bytes(); + ip.flags_fragment_offset = [flags_frag_hi, flags_frag_lo]; + ip.ttl = ttl; + ip.protocol = transport.protocol(); + ip.header_checksum = [0, 0]; + ip.src_addr = src_ip; + ip.dst_addr = dst_ip; + if !tx_offload { + ip.fill_checksum(); + } + }, + ) + } + + /// Fragment a payload into IPv6 frames. + /// + /// - Writes Ethernet + IPv6 + Fragment Extension headers per fragment. + /// - Fragment 0 includes the transport header (via `transport.write_to()`). + /// - Non-last fragments have 8-byte-aligned data lengths. + /// - Returns `Packet::Single` if the payload fits in one frame (no fragment ext header), `Packet::Multi` otherwise. + /// - Returns `Err(WouldBlock)` if `free_frames` has insufficient frames. + pub fn fragment_ipv6<'umem>( + src_mac: MacAddress, + dst_mac: MacAddress, + src_ip: Ipv6Address, + dst_ip: Ipv6Address, + hop_limit: u8, + transport: &impl TransportHeader, + payload: &[u8], + mtu: u32, + free_frames: &mut impl FrameBuffer<'umem>, + ) -> NonBlocking> { + let transport_header_len = transport.header_len(); + let max_payload = mtu as usize - IPV6_HEADER_LEN - transport_header_len; + + if payload.len() <= max_payload { + let protocol = transport.protocol(); + let ipv6_payload_len = (transport_header_len + payload.len()) as u16; + + return Self::build_single(transport, payload, IPV6_HEADER_LEN, free_frames, |frame| { + ethernet::write_ethernet_header(&mut *frame, dst_mac, src_mac, EtherTypes::IPv6); + + let ip = Ipv6Header::from_bytes_mut(frame); + ip.version_tc_fl = [0x60, 0x00, 0x00, 0x00]; + ip.payload_length = ipv6_payload_len.to_be_bytes(); + ip.next_header = protocol; + ip.hop_limit = hop_limit; + ip.src_addr = src_ip; + ip.dst_addr = dst_ip; + }); + } + + let ip_overhead = IPV6_HEADER_LEN + FRAGMENT_EXT_LEN; + let plan = FragmentPlan::new(ip_overhead, transport_header_len, mtu, payload.len()); + + if free_frames.num_frames() < plan.num_frames { + return Err(WouldBlock); + } + + let identification = id::next_ipv6_id(); + + Self::fragment_loop( + transport, + payload, + ip_overhead, + &plan, + free_frames, + |frame, frag_data_len, frag_byte_offset, is_last| { + ethernet::write_ethernet_header(&mut *frame, dst_mac, src_mac, EtherTypes::IPv6); + + let ipv6_payload_len = (FRAGMENT_EXT_LEN + frag_data_len) as u16; + { + let ip = Ipv6Header::from_bytes_mut(frame); + ip.version_tc_fl = [0x60, 0x00, 0x00, 0x00]; + ip.payload_length = ipv6_payload_len.to_be_bytes(); + ip.next_header = 44; + ip.hop_limit = hop_limit; + ip.src_addr = src_ip; + ip.dst_addr = dst_ip; + } + + let frag_ext_offset = ETH_HEADER_LEN + IPV6_HEADER_LEN; + let frag_hdr = Ipv6FragmentHeader::from_bytes_at_mut(frame, frag_ext_offset); + frag_hdr.next_header = transport.protocol(); + frag_hdr.reserved = 0; + let frag_offset_units = (frag_byte_offset / 8) as u16; + frag_hdr.set_fragment_offset_mf(frag_offset_units, !is_last); + frag_hdr.identification = identification.to_be_bytes(); + }, + ) + } + + /// Common fragment loop shared by IPv4 and IPv6 fragmented paths. + /// + /// `ip_overhead` is the total bytes between the ethernet header and the + /// fragment data start (e.g. 20 for IPv4, 48 for IPv6 + fragment ext). + /// + /// `write_headers` is called per fragment with `(frame, frag_data_len, + /// frag_byte_offset, is_last)` and must write all protocol-specific headers. + fn fragment_loop<'umem>( + transport: &impl TransportHeader, + payload: &[u8], + ip_overhead: usize, + plan: &FragmentPlan, + free_frames: &mut impl FrameBuffer<'umem>, + mut write_headers: impl FnMut(&mut Frame<'umem>, usize, usize, bool), + ) -> NonBlocking> { + let transport_header_len = transport.header_len(); + let mut frames = Vec::with_capacity(plan.num_frames); + let mut payload_offset = 0usize; + let mut frag_byte_offset = 0usize; + + for i in 0..plan.num_frames { + let mut frame = free_frames.pop().ok_or(WouldBlock)?; + let is_last = i == plan.num_frames - 1; + + let (frag_data_len, data_to_copy) = + plan.fragment_sizes(i, transport_header_len, payload_offset, payload.len()); + + let frame_len = ETH_HEADER_LEN + ip_overhead + frag_data_len; + unsafe { frame.set_len(frame_len) }; + + write_headers(&mut frame, frag_data_len, frag_byte_offset, is_last); + + let data_start = ETH_HEADER_LEN + ip_overhead; + + if i == 0 { + transport.write_to(&mut frame[data_start..data_start + transport_header_len]); + + let chunk = plan.first_chunk.min(payload.len()); + frame[data_start + transport_header_len..data_start + transport_header_len + chunk] + .copy_from_slice(&payload[..chunk]); + payload_offset = chunk; + frag_byte_offset = transport_header_len + chunk; + } else { + frame[data_start..data_start + data_to_copy] + .copy_from_slice(&payload[payload_offset..payload_offset + data_to_copy]); + payload_offset += data_to_copy; + frag_byte_offset += data_to_copy; + } + + frames.push(frame); + } + + debug_assert!(frames.len() >= 2); + Ok(Packet::Multi(frames)) + } + + /// Build a single (non-fragmented) frame. + /// + /// `ip_header_len` is the bytes between the ethernet header and the + /// transport data (e.g. 20 for IPv4, 40 for IPv6). + /// + /// `write_headers` must write the ethernet and IP headers into the frame. + fn build_single<'umem>( + transport: &impl TransportHeader, + payload: &[u8], + ip_header_len: usize, + free_frames: &mut impl FrameBuffer<'umem>, + write_headers: impl FnOnce(&mut Frame<'umem>), + ) -> NonBlocking> { + let transport_header_len = transport.header_len(); + let mut frame = free_frames.pop().ok_or(WouldBlock)?; + let frame_len = ETH_HEADER_LEN + ip_header_len + transport_header_len + payload.len(); + unsafe { frame.set_len(frame_len) }; + + write_headers(&mut frame); + + let data_start = ETH_HEADER_LEN + ip_header_len; + transport.write_to(&mut frame[data_start..data_start + transport_header_len]); + frame[data_start + transport_header_len..frame_len].copy_from_slice(payload); + + Ok(Packet::Single(frame)) + } +} + +#[cfg(test)] +mod tests { + use crate::{ + net::{ + checksum::compute_ipv4_checksum, + wire::{ip::IpProtocols, udp::UdpHeader}, + }, + xdp::frame::{BasicFrameBuffer, Frame}, + }; + + use super::*; + + const SRC_MAC: MacAddress = MacAddress::new([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]); + const DST_MAC: MacAddress = MacAddress::new([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]); + const SRC_V4: Ipv4Address = Ipv4Address::new([192, 168, 1, 1]); + const DST_V4: Ipv4Address = Ipv4Address::new([10, 0, 0, 1]); + const SRC_V6: Ipv6Address = + Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + const DST_V6: Ipv6Address = + Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]); + + fn make_free_frames(bufs: &mut [Vec], n: usize) -> BasicFrameBuffer<'_> { + let mut fb = BasicFrameBuffer::new(n * 2); + for (i, buf) in bufs.iter_mut().enumerate() { + fb.push(Frame::new(i as u64, buf.as_mut_slice(), 1, false)); + } + fb + } + + fn make_udp_transport(payload_len: usize) -> UdpHeader { + UdpHeader::new(12345, 53, (8 + payload_len) as u16, [0xAB, 0xCD]) + } + + // --- IPv4 single frame --- + + #[test] + fn ipv4_single_frame() { + let mut bufs: Vec> = (0..4).map(|_| vec![0u8; 2048]).collect(); + let mut free = make_free_frames(&mut bufs, 4); + let payload = b"Hello, World!"; + let transport = make_udp_transport(payload.len()); + + let result = FragmentWriter::fragment_ipv4( + SRC_MAC, DST_MAC, SRC_V4, DST_V4, 64, &transport, payload, 1500, false, &mut free, + ); + + let packet = result.expect("should succeed"); + match packet { + Packet::Single(frame) => { + let expected_len = ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN + 8 + payload.len(); + assert_eq!(frame.len(), expected_len); + + let ip = Ipv4Header::from_bytes(&frame); + assert_eq!(ip.version(), 4); + assert_eq!(ip.ihl(), 5); + assert_eq!(ip.ttl, 64); + assert_eq!(ip.protocol, IpProtocols::Udp); + assert_eq!(ip.src_addr, SRC_V4); + assert_eq!(ip.dst_addr, DST_V4); + assert!(ip.dont_fragment()); + assert!(!ip.is_fragment()); + + // Verify IPv4 checksum. + let ip_bytes = &frame[ETH_HEADER_LEN..ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN]; + assert_eq!(compute_ipv4_checksum(ip_bytes), [0x00, 0x00]); + + // Verify transport header was written. + let data_start = ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN; + assert_eq!( + u16::from_be_bytes([frame[data_start], frame[data_start + 1]]), + 12345 + ); + assert_eq!( + u16::from_be_bytes([frame[data_start + 2], frame[data_start + 3]]), + 53 + ); + + // Verify payload. + assert_eq!(&frame[data_start + 8..frame.len()], payload); + } + _ => panic!("expected Single"), + } + } + + // --- IPv4 fragmented --- + + #[test] + fn ipv4_fragmented() { + let mut bufs: Vec> = (0..32).map(|_| vec![0u8; 2048]).collect(); + let mut free = make_free_frames(&mut bufs, 32); + let payload = vec![0xAB; 3000]; + let transport = make_udp_transport(payload.len()); + + let result = FragmentWriter::fragment_ipv4( + SRC_MAC, DST_MAC, SRC_V4, DST_V4, 64, &transport, &payload, 1500, false, &mut free, + ); + + let packet = result.expect("should succeed"); + match packet { + Packet::Multi(frames) => { + assert!(frames.len() >= 2); + + let first_id = Ipv4Header::from_bytes(&frames[0]).identification(); + for f in &frames { + let ip = Ipv4Header::from_bytes(f); + assert_eq!(ip.identification(), first_id); + assert_eq!(ip.protocol, IpProtocols::Udp); + assert_eq!(ip.src_addr, SRC_V4); + assert_eq!(ip.dst_addr, DST_V4); + assert!(!ip.dont_fragment()); + + // Verify IPv4 checksum. + let ip_bytes = &f[ETH_HEADER_LEN..ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN]; + assert_eq!(compute_ipv4_checksum(ip_bytes), [0x00, 0x00]); + } + + // First fragment: MF set, offset 0. + let first_ip = Ipv4Header::from_bytes(&frames[0]); + assert!(first_ip.more_fragments()); + assert_eq!(first_ip.fragment_offset(), 0); + + // Last fragment: MF clear. + let last_ip = Ipv4Header::from_bytes(frames.last().unwrap()); + assert!(!last_ip.more_fragments()); + + // Non-last fragments: 8-byte aligned payload. + for (i, f) in frames.iter().enumerate() { + let ip = Ipv4Header::from_bytes(f); + if i < frames.len() - 1 { + assert_eq!(ip.payload_len() % 8, 0, "non-last fragment not 8-aligned"); + } + } + + // Reassemble payload. + let mut reassembled = Vec::new(); + for (i, f) in frames.iter().enumerate() { + let data_start = ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN; + if i == 0 { + reassembled.extend_from_slice(&f[data_start + 8..f.len()]); + } else { + reassembled.extend_from_slice(&f[data_start..f.len()]); + } + } + assert_eq!(reassembled, payload); + } + _ => panic!("expected Multi"), + } + } + + // --- IPv6 single frame --- + + #[test] + fn ipv6_single_frame() { + let mut bufs: Vec> = (0..4).map(|_| vec![0u8; 2048]).collect(); + let mut free = make_free_frames(&mut bufs, 4); + let payload = b"Hello, IPv6!"; + let transport = make_udp_transport(payload.len()); + + let result = FragmentWriter::fragment_ipv6( + SRC_MAC, DST_MAC, SRC_V6, DST_V6, 64, &transport, payload, 1500, &mut free, + ); + + let packet = result.expect("should succeed"); + match packet { + Packet::Single(frame) => { + let expected_len = ETH_HEADER_LEN + IPV6_HEADER_LEN + 8 + payload.len(); + assert_eq!(frame.len(), expected_len); + + let ip = Ipv6Header::from_bytes(&frame); + assert_eq!(ip.version(), 6); + assert_eq!(ip.hop_limit, 64); + assert_eq!(ip.next_header, IpProtocols::Udp); + assert_eq!(ip.src_addr, SRC_V6); + assert_eq!(ip.dst_addr, DST_V6); + + // Verify payload. + let data_start = ETH_HEADER_LEN + IPV6_HEADER_LEN; + assert_eq!(&frame[data_start + 8..frame.len()], payload); + } + _ => panic!("expected Single"), + } + } + + // --- IPv6 fragmented --- + + #[test] + fn ipv6_fragmented() { + let mut bufs: Vec> = (0..32).map(|_| vec![0u8; 2048]).collect(); + let mut free = make_free_frames(&mut bufs, 32); + let payload = vec![0xCD; 3000]; + let transport = make_udp_transport(payload.len()); + + let result = FragmentWriter::fragment_ipv6( + SRC_MAC, DST_MAC, SRC_V6, DST_V6, 64, &transport, &payload, 1500, &mut free, + ); + + let packet = result.expect("should succeed"); + match packet { + Packet::Multi(frames) => { + assert!(frames.len() >= 2); + + let frag_ext_offset = ETH_HEADER_LEN + IPV6_HEADER_LEN; + let first_frag = Ipv6FragmentHeader::from_bytes_at(&frames[0], frag_ext_offset); + let first_id = first_frag.identification(); + + for f in &frames { + let ip = Ipv6Header::from_bytes(f); + assert_eq!(ip.next_header, 44); // Fragment ext header + assert_eq!(ip.src_addr, SRC_V6); + assert_eq!(ip.dst_addr, DST_V6); + + let frag_hdr = Ipv6FragmentHeader::from_bytes_at(f, frag_ext_offset); + assert_eq!(frag_hdr.identification(), first_id); + assert_eq!(frag_hdr.next_header, IpProtocols::Udp); + } + + // First fragment: MF set, offset 0. + assert!(first_frag.more_fragments()); + assert_eq!(first_frag.fragment_offset(), 0); + + // Last fragment: MF clear. + let last_frag = + Ipv6FragmentHeader::from_bytes_at(frames.last().unwrap(), frag_ext_offset); + assert!(!last_frag.more_fragments()); + + // Reassemble payload. + let mut reassembled = Vec::new(); + for (i, f) in frames.iter().enumerate() { + let data_start = frag_ext_offset + FRAGMENT_EXT_LEN; + if i == 0 { + reassembled.extend_from_slice(&f[data_start + 8..f.len()]); + } else { + reassembled.extend_from_slice(&f[data_start..f.len()]); + } + } + assert_eq!(reassembled, payload); + } + _ => panic!("expected Multi"), + } + } + + // --- WouldBlock on insufficient frames --- + + #[test] + fn ipv4_would_block_no_frames() { + let mut free = BasicFrameBuffer::new(16); + let transport = make_udp_transport(13); + + let result = FragmentWriter::fragment_ipv4( + SRC_MAC, + DST_MAC, + SRC_V4, + DST_V4, + 64, + &transport, + b"Hello, World!", + 1500, + false, + &mut free, + ); + assert_eq!(result.unwrap_err(), WouldBlock); + } + + #[test] + fn ipv4_would_block_insufficient_frames_for_fragmentation() { + // Need 3 frames for 3000-byte payload, only provide 2. + let mut bufs: Vec> = (0..2).map(|_| vec![0u8; 2048]).collect(); + let mut free = make_free_frames(&mut bufs, 2); + let payload = vec![0xAB; 3000]; + let transport = make_udp_transport(payload.len()); + + let result = FragmentWriter::fragment_ipv4( + SRC_MAC, DST_MAC, SRC_V4, DST_V4, 64, &transport, &payload, 1500, false, &mut free, + ); + assert_eq!(result.unwrap_err(), WouldBlock); + } + + #[test] + fn ipv6_would_block_no_frames() { + let mut free = BasicFrameBuffer::new(16); + let transport = make_udp_transport(12); + + let result = FragmentWriter::fragment_ipv6( + SRC_MAC, + DST_MAC, + SRC_V6, + DST_V6, + 64, + &transport, + b"Hello, IPv6!", + 1500, + &mut free, + ); + assert_eq!(result.unwrap_err(), WouldBlock); + } + + // --- Custom transport header --- + + struct FakeTransport { + protocol: u8, + header: [u8; 4], + } + + impl TransportHeader for FakeTransport { + fn protocol(&self) -> u8 { + self.protocol + } + fn header_len(&self) -> usize { + 4 + } + fn write_to(&self, buf: &mut [u8]) { + buf[0..4].copy_from_slice(&self.header); + } + } + + #[test] + fn ipv4_custom_transport() { + let mut bufs: Vec> = (0..4).map(|_| vec![0u8; 2048]).collect(); + let mut free = make_free_frames(&mut bufs, 4); + let transport = FakeTransport { + protocol: 99, + header: [0xDE, 0xAD, 0xBE, 0xEF], + }; + let payload = b"test"; + + let result = FragmentWriter::fragment_ipv4( + SRC_MAC, DST_MAC, SRC_V4, DST_V4, 128, &transport, payload, 1500, false, &mut free, + ); + + match result.unwrap() { + Packet::Single(frame) => { + let ip = Ipv4Header::from_bytes(&frame); + assert_eq!(ip.protocol, 99); + assert_eq!(ip.ttl, 128); + + let data_start = ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN; + assert_eq!( + &frame[data_start..data_start + 4], + &[0xDE, 0xAD, 0xBE, 0xEF] + ); + assert_eq!(&frame[data_start + 4..frame.len()], b"test"); + } + _ => panic!("expected Single"), + } + } + + // --- Empty payload --- + + #[test] + fn ipv4_empty_payload() { + let mut bufs: Vec> = (0..4).map(|_| vec![0u8; 2048]).collect(); + let mut free = make_free_frames(&mut bufs, 4); + let transport = make_udp_transport(0); + + let result = FragmentWriter::fragment_ipv4( + SRC_MAC, + DST_MAC, + SRC_V4, + DST_V4, + 64, + &transport, + &[], + 1500, + false, + &mut free, + ); + + match result.unwrap() { + Packet::Single(frame) => { + assert_eq!(frame.len(), ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN + 8); + } + _ => panic!("expected Single"), + } + } + + #[test] + fn ipv6_empty_payload() { + let mut bufs: Vec> = (0..4).map(|_| vec![0u8; 2048]).collect(); + let mut free = make_free_frames(&mut bufs, 4); + let transport = make_udp_transport(0); + + let result = FragmentWriter::fragment_ipv6( + SRC_MAC, + DST_MAC, + SRC_V6, + DST_V6, + 64, + &transport, + &[], + 1500, + &mut free, + ); + + match result.unwrap() { + Packet::Single(frame) => { + assert_eq!(frame.len(), ETH_HEADER_LEN + IPV6_HEADER_LEN + 8); + } + _ => panic!("expected Single"), + } + } +} diff --git a/src/net/handler/ethernet.rs b/src/net/handler/ethernet.rs new file mode 100644 index 0000000..c9eda5b --- /dev/null +++ b/src/net/handler/ethernet.rs @@ -0,0 +1,310 @@ +use coarsetime::Instant; + +use crate::{ + net::{ + NeighborHandler, PmtuCache, + handler::{ipv4::Ipv4Handler, ipv6::Ipv6Handler, tcp::TcpHandler, udp::UdpHandler}, + wire::ethernet::{EtherTypes, EthernetFrame}, + }, + xdp::frame::{Frame, FrameBuffer}, +}; + +pub struct EthernetHandler; + +impl EthernetHandler { + pub fn handle<'umem>( + &mut self, + frame: Frame<'umem>, + ipv4_handler: &mut Ipv4Handler, + ipv6_handler: &mut Ipv6Handler, + udp_handler: &mut UdpHandler<'umem>, + tcp_handler: &mut TcpHandler, + neighbor_handler: &NeighborHandler, + pmtu: &PmtuCache, + now: Instant, + free_frames: &mut impl FrameBuffer<'umem>, + rx_return: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) { + let ethernet_frame = EthernetFrame::from_bytes(&frame); + match ethernet_frame.ether_type { + EtherTypes::IPv4 => { + ipv4_handler.handle( + frame, + udp_handler, + tcp_handler, + neighbor_handler, + pmtu, + now, + free_frames, + rx_return, + tx_return, + ); + } + EtherTypes::IPv6 => { + ipv6_handler.handle( + frame, + neighbor_handler, + udp_handler, + tcp_handler, + pmtu, + now, + free_frames, + rx_return, + tx_return, + ); + } + EtherTypes::Arp => { + neighbor_handler.handle_arp(now, frame, rx_return, tx_return); + } + _ => { + rx_return.push(frame); + } + } + } +} + +#[cfg(test)] +mod tests { + use coarsetime::Duration; + + use crate::{ + net::{ + checksum::{compute_ipv4_checksum, compute_udp_checksum}, + wire::{ + ethernet::{EtherType, MacAddress, write_ethernet_header}, + ip::{IPV4_MIN_HEADER_LEN, Ipv4Address}, + udp::UDP_HEADER_LEN, + }, + }, + xdp::frame::BasicFrameBuffer, + }; + + use super::*; + + const SRC_IP: Ipv4Address = Ipv4Address::new([10, 0, 0, 1]); + const DST_IP: Ipv4Address = Ipv4Address::new([10, 0, 0, 2]); + const ETH_LEN: usize = size_of::(); + + fn new_handlers<'umem>() -> ( + EthernetHandler, + Ipv4Handler, + Ipv6Handler, + UdpHandler<'umem>, + TcpHandler, + NeighborHandler, + PmtuCache, + ) { + let eth = EthernetHandler; + let ipv4 = Ipv4Handler::new(false, false); + let ipv6 = Ipv6Handler::new(false, false); + let udp = UdpHandler::new(256, false); + let tcp = TcpHandler::new(false, false); + let neighbor = NeighborHandler::new("test0", Duration::from_secs(60)).unwrap(); + let pmtu = PmtuCache::new(); + (eth, ipv4, ipv6, udp, tcp, neighbor, pmtu) + } + + fn new_buffers<'umem>() -> ( + BasicFrameBuffer<'umem>, + BasicFrameBuffer<'umem>, + BasicFrameBuffer<'umem>, + ) { + ( + BasicFrameBuffer::new(4), + BasicFrameBuffer::new(4), + BasicFrameBuffer::new(4), + ) + } + + /// Builds a minimal Ethernet frame with only the 14-byte header set. + fn build_eth_frame(ether_type: EtherType) -> Vec { + let mut buf = vec![0u8; ETH_LEN + 46]; // min ethernet payload + write_ethernet_header( + &mut buf, + MacAddress::broadcast(), + MacAddress::zero(), + ether_type, + ); + buf + } + + /// Builds a valid Ethernet + IPv4 + UDP frame with correct checksums. + fn build_ipv4_udp_frame() -> Vec { + let udp_payload = build_udp_bytes(&SRC_IP, &DST_IP); + let total_ip_len = (IPV4_MIN_HEADER_LEN + udp_payload.len()) as u16; + let mut buf = vec![0u8; ETH_LEN + IPV4_MIN_HEADER_LEN + udp_payload.len()]; + + // Ethernet header + buf[12] = 0x08; + buf[13] = 0x00; + + // IPv4 header + let ip = &mut buf[ETH_LEN..]; + ip[0] = 0x45; // version=4, ihl=5 + ip[2..4].copy_from_slice(&total_ip_len.to_be_bytes()); + ip[6] = 0x40; // don't fragment + ip[8] = 64; // TTL + ip[9] = 17; // UDP protocol + let src_bytes: [u8; 4] = SRC_IP.into(); + ip[12..16].copy_from_slice(&src_bytes); + let dst_bytes: [u8; 4] = DST_IP.into(); + ip[16..20].copy_from_slice(&dst_bytes); + let cksum = compute_ipv4_checksum(&ip[..20]); + ip[10] = cksum[0]; + ip[11] = cksum[1]; + + // UDP payload + buf[ETH_LEN + IPV4_MIN_HEADER_LEN..].copy_from_slice(&udp_payload); + buf + } + + /// Builds a minimal valid UDP segment with correct checksum. + fn build_udp_bytes(src: &Ipv4Address, dst: &Ipv4Address) -> Vec { + let mut buf = vec![0u8; UDP_HEADER_LEN]; + let udp_len = UDP_HEADER_LEN as u16; + buf[4..6].copy_from_slice(&udp_len.to_be_bytes()); + let cksum = compute_udp_checksum(src, dst, &buf); + buf[6] = cksum[0]; + buf[7] = cksum[1]; + buf + } + + #[test] + fn unknown_ether_type_returns_frame_to_rx() { + let (mut eth, mut ipv4, mut ipv6, mut udp, mut tcp, neighbor, pmtu) = new_handlers(); + let (mut free, mut rx, mut tx) = new_buffers(); + let now = Instant::now(); + + let unknown_type = EtherType { + octets: [0xFF, 0xFF], + }; + let mut data = build_eth_frame(unknown_type); + let len = data.len(); + let frame = Frame::new(0, &mut data, len, false); + + eth.handle( + frame, &mut ipv4, &mut ipv6, &mut udp, &mut tcp, &neighbor, &pmtu, now, &mut free, + &mut rx, &mut tx, + ); + + assert_eq!(rx.num_frames(), 1); + assert_eq!(tx.num_frames(), 0); + } + + #[test] + fn ipv4_frame_dispatches_to_ipv4_handler() { + let (mut eth, mut ipv4, mut ipv6, mut udp, mut tcp, neighbor, pmtu) = new_handlers(); + let (mut free, mut rx, mut tx) = new_buffers(); + let now = Instant::now(); + + let mut data = build_ipv4_udp_frame(); + let len = data.len(); + let frame = Frame::new(0, &mut data, len, false); + + eth.handle( + frame, &mut ipv4, &mut ipv6, &mut udp, &mut tcp, &neighbor, &pmtu, now, &mut free, + &mut rx, &mut tx, + ); + + // Frame was consumed by ipv4_handler. No UDP binding so it ends up + // in rx_return after full IPv4 processing (not the unknown-type path). + assert_eq!(rx.num_frames(), 1); + assert_eq!(tx.num_frames(), 0); + } + + #[test] + fn ipv4_short_frame_dispatches_to_ipv4_handler() { + let (mut eth, mut ipv4, mut ipv6, mut udp, mut tcp, neighbor, pmtu) = new_handlers(); + let (mut free, mut rx, mut tx) = new_buffers(); + let now = Instant::now(); + + // Ethernet header says IPv4 but the IP payload is too short. + let mut data = build_eth_frame(EtherTypes::IPv4); + let len = data.len(); + let frame = Frame::new(0, &mut data, len, false); + + eth.handle( + frame, &mut ipv4, &mut ipv6, &mut udp, &mut tcp, &neighbor, &pmtu, now, &mut free, + &mut rx, &mut tx, + ); + + // ipv4_handler rejects the short frame back to rx_return. + assert_eq!(rx.num_frames(), 1); + assert_eq!(tx.num_frames(), 0); + } + + #[test] + fn ipv6_short_frame_dispatches_to_ipv6_handler() { + let (mut eth, mut ipv4, mut ipv6, mut udp, mut tcp, neighbor, pmtu) = new_handlers(); + let (mut free, mut rx, mut tx) = new_buffers(); + let now = Instant::now(); + + // Ethernet header says IPv6 but the payload is too short for a valid + // IPv6 header (needs 54 bytes: 14 eth + 40 ipv6). + let mut data = build_eth_frame(EtherTypes::IPv6); + let len = data.len(); + let frame = Frame::new(0, &mut data, len, false); + + eth.handle( + frame, &mut ipv4, &mut ipv6, &mut udp, &mut tcp, &neighbor, &pmtu, now, &mut free, + &mut rx, &mut tx, + ); + + // ipv6_handler rejects the short frame back to rx_return. + assert_eq!(rx.num_frames(), 1); + assert_eq!(tx.num_frames(), 0); + } + + #[test] + fn arp_frame_dispatches_to_neighbor_handler() { + let (mut eth, mut ipv4, mut ipv6, mut udp, mut tcp, neighbor, pmtu) = new_handlers(); + let (mut free, mut rx, mut tx) = new_buffers(); + let now = Instant::now(); + + let mut data = build_eth_frame(EtherTypes::Arp); + let len = data.len(); + let frame = Frame::new(0, &mut data, len, false); + + eth.handle( + frame, &mut ipv4, &mut ipv6, &mut udp, &mut tcp, &neighbor, &pmtu, now, &mut free, + &mut rx, &mut tx, + ); + + // neighbor_handler processes (and discards) the malformed ARP. + // Frame ends up in one of the return buffers. + let total = rx.num_frames() + tx.num_frames(); + assert_eq!(total, 1); + } + + #[test] + fn frame_always_consumed() { + let now = Instant::now(); + + // Every ether_type path must consume the frame exactly once. + let types = [ + EtherTypes::IPv4, + EtherTypes::IPv6, + EtherTypes::Arp, + EtherType { + octets: [0xDE, 0xAD], + }, + ]; + + for etype in types { + let (mut eth, mut ipv4, mut ipv6, mut udp, mut tcp, neighbor, pmtu) = new_handlers(); + let (mut free, mut rx, mut tx) = new_buffers(); + let mut data = build_eth_frame(etype); + let len = data.len(); + let frame = Frame::new(0, &mut data, len, false); + + eth.handle( + frame, &mut ipv4, &mut ipv6, &mut udp, &mut tcp, &neighbor, &pmtu, now, + &mut free, &mut rx, &mut tx, + ); + + let total = free.num_frames() + rx.num_frames() + tx.num_frames(); + assert!(total >= 1, "frame lost for ether_type {:?}", etype); + } + } +} diff --git a/src/net/handler/icmpv4.rs b/src/net/handler/icmpv4.rs new file mode 100644 index 0000000..22dfc82 --- /dev/null +++ b/src/net/handler/icmpv4.rs @@ -0,0 +1,787 @@ +use coarsetime::Instant; + +use crate::{ + net::{ + PmtuCache, + checksum::compute_ipv4_checksum, + wire::{ + ethernet::EthernetFrame, + icmpv4::{ + ICMPV4_HEADER_LEN, Icmpv4Codes, Icmpv4Frame, Icmpv4Header, Icmpv4Types, + is_icmp_error, + }, + ip::{IPV4_MIN_HEADER_LEN, IpProtocols, Ipv4Address, Ipv4Header}, + }, + }, + xdp::frame::{Frame, FrameBuffer}, +}; + +/// Processes an incoming ICMPv4 packet. +/// +/// The frame must already be validated as a proper IPv4 packet with +/// `protocol = ICMP` by the [`Ipv4Handler`]. +/// +/// Currently handles: +/// * Echo Request (type 8) -> Echo Reply (type 0), pushed to `tx_return`. +/// * All other types are passed to `rx_return`. +pub fn handle_icmpv4<'umem>( + mut frame: Frame<'umem>, + pmtu: &PmtuCache, + now: Instant, + rx_offload: bool, + tx_offload: bool, + rx_return: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, +) { + let ip = Ipv4Header::from_bytes(&frame); + let payload_offset = ip.payload_offset(); + let payload_len = ip.payload_len(); + let dst_addr = ip.dst_addr; + + if payload_len < ICMPV4_HEADER_LEN { + rx_return.push(frame); + return; + } + + let icmp_end = payload_offset + payload_len; + + if !rx_offload && compute_ipv4_checksum(&frame[payload_offset..icmp_end]) != [0x00, 0x00] { + rx_return.push(frame); + return; + } + + let icmp = Icmpv4Header::from_bytes_at(&frame, payload_offset); + let icmp_type = icmp.icmp_type; + let icmp_code = icmp.code; + + match icmp_type { + Icmpv4Types::EchoRequest => { + // RFC 1122 §3.2.2.6: silently discard echo requests to + // broadcast/multicast (prevents amplification attacks). + if dst_addr.is_broadcast() || dst_addr.is_multicast() { + rx_return.push(frame); + return; + } + + // Swap Ethernet MACs. + let eth = EthernetFrame::from_bytes_mut(&mut frame); + std::mem::swap(&mut eth.dst_mac, &mut eth.src_mac); + + // Swap IPv4 addresses and reset TTL. + let ip = Ipv4Header::from_bytes_mut(&mut frame); + std::mem::swap(&mut ip.src_addr, &mut ip.dst_addr); + ip.ttl = 64; + if !tx_offload { + ip.fill_checksum(); + } + + // Set ICMP type to Echo Reply and recompute checksum. + let icmp = Icmpv4Header::from_bytes_at_mut(&mut frame, payload_offset); + icmp.icmp_type = Icmpv4Types::EchoReply; + icmp.checksum = [0, 0]; + if !tx_offload { + let cksum = compute_ipv4_checksum(&frame[payload_offset..icmp_end]); + Icmpv4Header::from_bytes_at_mut(&mut frame, payload_offset).checksum = cksum; + } + + tx_return.push(frame); + } + Icmpv4Types::DestinationUnreachable if icmp_code == Icmpv4Codes::FragmentationNeeded => { + // Extract next-hop MTU from the ICMP header. + let icmp = Icmpv4Header::from_bytes_at(&frame, payload_offset); + let mtu = icmp.next_hop_mtu() as u32; + + // Extract original destination IP from embedded IP header. + // The embedded IP header starts at payload_offset + ICMPV4_HEADER_LEN. + // The destination address is at offset 16 within the IP header. + let dst_offset = payload_offset + ICMPV4_HEADER_LEN + 16; + if dst_offset + 4 <= frame.len() { + let dst_ip = Ipv4Address::new([ + frame[dst_offset], + frame[dst_offset + 1], + frame[dst_offset + 2], + frame[dst_offset + 3], + ]); + pmtu.update(now, dst_ip.into(), mtu); + } + + rx_return.push(frame); + } + _ => rx_return.push(frame), + } +} + +/// Transforms a frame into an ICMPv4 Destination Unreachable response. +/// +/// Per RFC 792, the ICMP error payload contains the original IPv4 +/// header plus the first 8 bytes of the original datagram's data. +/// +/// Per RFC 1122: +/// * MUST NOT send in response to a broadcast/multicast packet. +/// * MUST NOT send in response to an ICMP error message. +/// * MUST NOT send if the source address is not unicast. +/// * Source address of the response is the original packet's +/// destination (our address on the receiving interface). +/// +/// If the response cannot be generated the frame goes to `rx_return`. +pub fn send_destination_unreachable<'umem>( + mut frame: Frame<'umem>, + code: u8, + next_hop_mtu: u16, + tx_offload: bool, + rx_return: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, +) { + let eth_len = size_of::(); + + let ip = Ipv4Header::from_bytes(&frame); + let src_addr = ip.src_addr; + let dst_addr = ip.dst_addr; + let orig_header_len = ip.header_len(); + let orig_protocol = ip.protocol; + let orig_payload_offset = ip.payload_offset(); + + // RFC 1122: MUST NOT send ICMP error for broadcast/multicast. + if dst_addr.is_broadcast() || dst_addr.is_multicast() { + rx_return.push(frame); + return; + } + + // RFC 1122: MUST NOT send if source is not unicast. + if src_addr.is_broadcast() || src_addr.is_multicast() || src_addr.is_unspecified() { + rx_return.push(frame); + return; + } + + // RFC 792: MUST NOT send ICMP error in response to an ICMP error. + if orig_protocol == IpProtocols::Icmp + && frame.len() > orig_payload_offset + && is_icmp_error(frame[orig_payload_offset]) + { + rx_return.push(frame); + return; + } + + // Save the original IP header + first 8 bytes of payload. + // Max IP header is 60 bytes (IHL=15) plus 8 data bytes = 68 bytes. + let orig_data_avail = frame.len().saturating_sub(eth_len + orig_header_len); + let orig_data_bytes = orig_data_avail.min(8); + let save_len = orig_header_len + orig_data_bytes; + let mut saved = [0u8; 68]; + saved[..save_len].copy_from_slice(&frame[eth_len..eth_len + save_len]); + + // Compute new frame dimensions. + let icmp_total_len = ICMPV4_HEADER_LEN + save_len; + let new_ip_total_len = IPV4_MIN_HEADER_LEN + icmp_total_len; + let new_frame_len = eth_len + new_ip_total_len; + + if new_frame_len > frame.capacity() { + rx_return.push(frame); + return; + } + + // Extend frame to the new length so we can write into it. + unsafe { + frame.set_len(new_frame_len); + } + + // Build Ethernet + IPv4 + ICMP headers via struct overlay. + let icmp_start = eth_len + IPV4_MIN_HEADER_LEN; + { + let pkt = Icmpv4Frame::from_bytes_mut(&mut frame); + + // Swap Ethernet MACs. + std::mem::swap(&mut pkt.ethernet.dst_mac, &mut pkt.ethernet.src_mac); + + // Build the new IPv4 header (always 20 bytes, no options). + pkt.ipv4.version_ihl = 0x45; + pkt.ipv4.dscp_ecn = 0; + pkt.ipv4.total_length = (new_ip_total_len as u16).to_be_bytes(); + pkt.ipv4.identification = [0, 0]; + pkt.ipv4.flags_fragment_offset = [0x40, 0x00]; // DF=1 + pkt.ipv4.ttl = 64; + pkt.ipv4.protocol = IpProtocols::Icmp; + pkt.ipv4.header_checksum = [0, 0]; + pkt.ipv4.src_addr = dst_addr; // our address + pkt.ipv4.dst_addr = src_addr; // original sender + if !tx_offload { + pkt.ipv4.fill_checksum(); + } + + // Build the ICMP header. + pkt.icmpv4.icmp_type = Icmpv4Types::DestinationUnreachable; + pkt.icmpv4.code = code; + pkt.icmpv4.checksum = [0, 0]; + if code == Icmpv4Codes::FragmentationNeeded { + pkt.icmpv4.rest_of_header = [0, 0, (next_hop_mtu >> 8) as u8, next_hop_mtu as u8]; + } else { + pkt.icmpv4.rest_of_header = [0, 0, 0, 0]; + } + } + + // Copy the saved original data into the ICMP payload. + let payload_start = icmp_start + ICMPV4_HEADER_LEN; + frame[payload_start..payload_start + save_len].copy_from_slice(&saved[..save_len]); + + // Compute and write ICMP checksum. + if !tx_offload { + let icmp_end = icmp_start + icmp_total_len; + let cksum = compute_ipv4_checksum(&frame[icmp_start..icmp_end]); + Icmpv4Header::from_bytes_at_mut(&mut frame, icmp_start).checksum = cksum; + } + + tx_return.push(frame); +} + +#[cfg(test)] +mod tests { + use crate::{ + net::wire::{ethernet::MacAddress, ip::IpAddress}, + xdp::frame::BasicFrameBuffer, + }; + + use super::*; + + const SRC_MAC: [u8; 6] = [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x01]; + const DST_MAC: [u8; 6] = [0x11, 0x22, 0x33, 0x44, 0x55, 0x02]; + const REMOTE_IP: Ipv4Address = Ipv4Address::new([10, 0, 0, 2]); + const LOCAL_IP: Ipv4Address = Ipv4Address::new([192, 168, 1, 1]); + + /// Builds a complete Ethernet + IPv4 + ICMP Echo Request frame. + fn build_echo_request( + src_mac: [u8; 6], + dst_mac: [u8; 6], + src_ip: Ipv4Address, + dst_ip: Ipv4Address, + id: u16, + seq: u16, + data: &[u8], + ) -> Vec { + let eth_len = size_of::(); + let icmp_len = ICMPV4_HEADER_LEN + data.len(); + let ip_total_len = (IPV4_MIN_HEADER_LEN + icmp_len) as u16; + let frame_len = eth_len + IPV4_MIN_HEADER_LEN + icmp_len; + let mut buf = vec![0u8; frame_len]; + + buf[0..6].copy_from_slice(&dst_mac); + buf[6..12].copy_from_slice(&src_mac); + buf[12] = 0x08; + buf[13] = 0x00; + + let ip = &mut buf[14..]; + ip[0] = 0x45; + ip[2..4].copy_from_slice(&ip_total_len.to_be_bytes()); + ip[6] = 0x40; + ip[8] = 64; + ip[9] = IpProtocols::Icmp; + let src_bytes: [u8; 4] = src_ip.into(); + ip[12..16].copy_from_slice(&src_bytes); + let dst_bytes: [u8; 4] = dst_ip.into(); + ip[16..20].copy_from_slice(&dst_bytes); + let cksum = compute_ipv4_checksum(&ip[..20]); + ip[10] = cksum[0]; + ip[11] = cksum[1]; + + let icmp_start = eth_len + IPV4_MIN_HEADER_LEN; + buf[icmp_start] = Icmpv4Types::EchoRequest; + buf[icmp_start + 4] = (id >> 8) as u8; + buf[icmp_start + 5] = id as u8; + buf[icmp_start + 6] = (seq >> 8) as u8; + buf[icmp_start + 7] = seq as u8; + buf[icmp_start + 8..].copy_from_slice(data); + + let icmp_end = icmp_start + icmp_len; + let cksum = compute_ipv4_checksum(&buf[icmp_start..icmp_end]); + buf[icmp_start + 2] = cksum[0]; + buf[icmp_start + 3] = cksum[1]; + + buf + } + + /// Builds a generic Ethernet + IPv4 frame. + fn build_ipv4_frame( + src_mac: [u8; 6], + dst_mac: [u8; 6], + src_ip: Ipv4Address, + dst_ip: Ipv4Address, + protocol: u8, + payload: &[u8], + ) -> Vec { + let eth_len = size_of::(); + let ip_total_len = (IPV4_MIN_HEADER_LEN + payload.len()) as u16; + let frame_len = eth_len + IPV4_MIN_HEADER_LEN + payload.len(); + let mut buf = vec![0u8; frame_len]; + + buf[0..6].copy_from_slice(&dst_mac); + buf[6..12].copy_from_slice(&src_mac); + buf[12] = 0x08; + buf[13] = 0x00; + + let ip = &mut buf[14..]; + ip[0] = 0x45; + ip[2..4].copy_from_slice(&ip_total_len.to_be_bytes()); + ip[6] = 0x40; + ip[8] = 64; + ip[9] = protocol; + let src_bytes: [u8; 4] = src_ip.into(); + ip[12..16].copy_from_slice(&src_bytes); + let dst_bytes: [u8; 4] = dst_ip.into(); + ip[16..20].copy_from_slice(&dst_bytes); + let cksum = compute_ipv4_checksum(&ip[..20]); + ip[10] = cksum[0]; + ip[11] = cksum[1]; + + buf[eth_len + IPV4_MIN_HEADER_LEN..].copy_from_slice(payload); + buf + } + + /// Helper: asserts frame was rejected (goes to rx, not tx). + fn assert_rejected(rx: &BasicFrameBuffer, tx: &BasicFrameBuffer) { + assert_eq!(rx.num_frames(), 1, "expected frame in rx_return"); + assert_eq!(tx.num_frames(), 0, "expected nothing in tx_return"); + } + + #[test] + fn echo_reply_complete_response() { + let now = Instant::now(); + let data = [0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE]; + let echo = build_echo_request(SRC_MAC, DST_MAC, REMOTE_IP, LOCAL_IP, 0x1234, 0x0005, &data); + let mut buf = vec![0u8; 256]; + buf[..echo.len()].copy_from_slice(&echo); + let frame = Frame::new(0, &mut buf, echo.len(), false); + + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + handle_icmpv4( + frame, + &PmtuCache::new(), + now, + false, + false, + &mut rx, + &mut tx, + ); + + assert_eq!(rx.num_frames(), 0); + assert_eq!(tx.num_frames(), 1); + + let reply = tx.pop().unwrap(); + let eth_len = size_of::(); + let icmp_offset = eth_len + IPV4_MIN_HEADER_LEN; + + // Type and code + assert_eq!(reply[icmp_offset], Icmpv4Types::EchoReply); + assert_eq!(reply[icmp_offset + 1], 0); + + // Ethernet MACs swapped + let eth = EthernetFrame::from_bytes(&reply); + assert_eq!(eth.src_mac, MacAddress::from(DST_MAC)); + assert_eq!(eth.dst_mac, MacAddress::from(SRC_MAC)); + + // IPv4 addresses swapped, TTL reset + let ip = Ipv4Header::from_bytes(&reply); + assert_eq!(ip.src_addr, LOCAL_IP); + assert_eq!(ip.dst_addr, REMOTE_IP); + assert_eq!(ip.ttl, 64); + + // Identifier and sequence preserved + assert_eq!(reply[icmp_offset + 4], 0x12); + assert_eq!(reply[icmp_offset + 5], 0x34); + assert_eq!(reply[icmp_offset + 6], 0x00); + assert_eq!(reply[icmp_offset + 7], 0x05); + assert_eq!(&reply[icmp_offset + 8..icmp_offset + 16], &data); + + // Valid IPv4 header checksum + assert_eq!( + compute_ipv4_checksum(&reply[eth_len..eth_len + IPV4_MIN_HEADER_LEN]), + [0, 0] + ); + + // Valid ICMP checksum + let icmp_start = ip.payload_offset(); + let icmp_end = icmp_start + ip.payload_len(); + assert_eq!(compute_ipv4_checksum(&reply[icmp_start..icmp_end]), [0, 0]); + } + + #[test] + fn echo_request_bad_checksum_rejected() { + let now = Instant::now(); + let mut echo = build_echo_request(SRC_MAC, DST_MAC, REMOTE_IP, LOCAL_IP, 1, 1, &[0; 8]); + let icmp_start = size_of::() + IPV4_MIN_HEADER_LEN; + echo[icmp_start + 2] ^= 0xFF; + let mut buf = vec![0u8; 256]; + buf[..echo.len()].copy_from_slice(&echo); + let frame = Frame::new(0, &mut buf, echo.len(), false); + + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + handle_icmpv4( + frame, + &PmtuCache::new(), + now, + false, + false, + &mut rx, + &mut tx, + ); + assert_rejected(&rx, &tx); + } + + #[test] + fn echo_request_too_short_rejected() { + let now = Instant::now(); + let data = build_ipv4_frame( + SRC_MAC, + DST_MAC, + REMOTE_IP, + LOCAL_IP, + IpProtocols::Icmp, + &[0; 4], + ); + let mut buf = vec![0u8; 256]; + buf[..data.len()].copy_from_slice(&data); + let frame = Frame::new(0, &mut buf, data.len(), false); + + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + handle_icmpv4( + frame, + &PmtuCache::new(), + now, + false, + false, + &mut rx, + &mut tx, + ); + assert_rejected(&rx, &tx); + } + + #[test] + fn echo_request_broadcast_multicast_rejected() { + let now = Instant::now(); + // Broadcast destination + let echo = build_echo_request( + SRC_MAC, + DST_MAC, + REMOTE_IP, + Ipv4Address::broadcast(), + 1, + 1, + &[0; 8], + ); + let mut buf = vec![0u8; 256]; + buf[..echo.len()].copy_from_slice(&echo); + let frame = Frame::new(0, &mut buf, echo.len(), false); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + handle_icmpv4( + frame, + &PmtuCache::new(), + now, + false, + false, + &mut rx, + &mut tx, + ); + assert_rejected(&rx, &tx); + + // Multicast destination + let echo = build_echo_request( + SRC_MAC, + DST_MAC, + REMOTE_IP, + Ipv4Address::new([224, 0, 0, 1]), + 1, + 1, + &[0; 8], + ); + let mut buf = vec![0u8; 256]; + buf[..echo.len()].copy_from_slice(&echo); + let frame = Frame::new(0, &mut buf, echo.len(), false); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + handle_icmpv4( + frame, + &PmtuCache::new(), + now, + false, + false, + &mut rx, + &mut tx, + ); + assert_rejected(&rx, &tx); + } + + #[test] + fn non_echo_goes_to_rx() { + let now = Instant::now(); + let mut icmp_payload = [0u8; 36]; + icmp_payload[0] = Icmpv4Types::DestinationUnreachable; + let cksum = compute_ipv4_checksum(&icmp_payload); + icmp_payload[2] = cksum[0]; + icmp_payload[3] = cksum[1]; + + let data = build_ipv4_frame( + SRC_MAC, + DST_MAC, + REMOTE_IP, + LOCAL_IP, + IpProtocols::Icmp, + &icmp_payload, + ); + let mut buf = vec![0u8; 256]; + buf[..data.len()].copy_from_slice(&data); + let frame = Frame::new(0, &mut buf, data.len(), false); + + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + handle_icmpv4( + frame, + &PmtuCache::new(), + now, + false, + false, + &mut rx, + &mut tx, + ); + assert_rejected(&rx, &tx); + } + + #[test] + fn dest_unreachable_complete_response() { + let payload = [0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA]; + let data = build_ipv4_frame(SRC_MAC, DST_MAC, REMOTE_IP, LOCAL_IP, 99, &payload); + let mut buf = vec![0u8; 256]; + buf[..data.len()].copy_from_slice(&data); + + let eth_len = size_of::(); + let orig_ip_hdr: Vec = data[eth_len..eth_len + IPV4_MIN_HEADER_LEN].to_vec(); + let orig_first8: Vec = payload[..8].to_vec(); + + let frame = Frame::new(0, &mut buf, data.len(), false); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + send_destination_unreachable( + frame, + Icmpv4Codes::ProtocolUnreachable, + 0, + false, + &mut rx, + &mut tx, + ); + + assert_eq!(rx.num_frames(), 0); + assert_eq!(tx.num_frames(), 1); + + let reply = tx.pop().unwrap(); + let icmp_start = eth_len + IPV4_MIN_HEADER_LEN; + + // Type and code + assert_eq!(reply[icmp_start], Icmpv4Types::DestinationUnreachable); + assert_eq!(reply[icmp_start + 1], Icmpv4Codes::ProtocolUnreachable); + + // Addresses swapped + let eth = EthernetFrame::from_bytes(&reply); + assert_eq!(eth.src_mac, MacAddress::from(DST_MAC)); + assert_eq!(eth.dst_mac, MacAddress::from(SRC_MAC)); + let ip = Ipv4Header::from_bytes(&reply); + assert_eq!(ip.src_addr, LOCAL_IP); + assert_eq!(ip.dst_addr, REMOTE_IP); + assert_eq!(ip.protocol, IpProtocols::Icmp); + + // Original header + first 8 bytes preserved + let icmp_payload_start = icmp_start + ICMPV4_HEADER_LEN; + assert_eq!( + &reply[icmp_payload_start..icmp_payload_start + IPV4_MIN_HEADER_LEN], + &orig_ip_hdr[..] + ); + let data_start = icmp_payload_start + IPV4_MIN_HEADER_LEN; + assert_eq!(&reply[data_start..data_start + 8], &orig_first8[..]); + + // Valid IPv4 header checksum + assert_eq!( + compute_ipv4_checksum(&reply[eth_len..eth_len + IPV4_MIN_HEADER_LEN]), + [0, 0] + ); + + // Valid ICMP checksum + let icmp_end = ip.payload_offset() + ip.payload_len(); + assert_eq!(compute_ipv4_checksum(&reply[icmp_start..icmp_end]), [0, 0]); + } + + #[test] + fn dest_unreachable_fragmentation_needed_includes_mtu() { + let data = build_ipv4_frame(SRC_MAC, DST_MAC, REMOTE_IP, LOCAL_IP, 17, &[0; 32]); + let mut buf = vec![0u8; 256]; + buf[..data.len()].copy_from_slice(&data); + let frame = Frame::new(0, &mut buf, data.len(), false); + + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + send_destination_unreachable( + frame, + Icmpv4Codes::FragmentationNeeded, + 1280, + false, + &mut rx, + &mut tx, + ); + + let reply = tx.pop().unwrap(); + let icmp_start = size_of::() + IPV4_MIN_HEADER_LEN; + let mtu = u16::from_be_bytes([reply[icmp_start + 6], reply[icmp_start + 7]]); + assert_eq!(mtu, 1280); + } + + #[test] + fn dest_unreachable_rfc1122_restrictions() { + // Helper to test that send_destination_unreachable rejects a frame. + let assert_not_sent = |data: Vec| { + let mut buf = vec![0u8; 256]; + buf[..data.len()].copy_from_slice(&data); + let frame = Frame::new(0, &mut buf, data.len(), false); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + send_destination_unreachable( + frame, + Icmpv4Codes::ProtocolUnreachable, + 0, + false, + &mut rx, + &mut tx, + ); + assert_eq!(rx.num_frames(), 1, "expected rejection"); + assert_eq!(tx.num_frames(), 0, "expected no response"); + }; + + // Broadcast destination + assert_not_sent(build_ipv4_frame( + SRC_MAC, + DST_MAC, + REMOTE_IP, + Ipv4Address::broadcast(), + 99, + &[0; 32], + )); + // Multicast destination + assert_not_sent(build_ipv4_frame( + SRC_MAC, + DST_MAC, + REMOTE_IP, + Ipv4Address::new([224, 0, 0, 1]), + 99, + &[0; 32], + )); + // Broadcast source + assert_not_sent(build_ipv4_frame( + SRC_MAC, + DST_MAC, + Ipv4Address::broadcast(), + LOCAL_IP, + 99, + &[0; 32], + )); + // Multicast source + assert_not_sent(build_ipv4_frame( + SRC_MAC, + DST_MAC, + Ipv4Address::new([224, 0, 0, 1]), + LOCAL_IP, + 99, + &[0; 32], + )); + // Unspecified source + assert_not_sent(build_ipv4_frame( + SRC_MAC, + DST_MAC, + Ipv4Address::unspecified(), + LOCAL_IP, + 99, + &[0; 32], + )); + // ICMP error as trigger + let mut icmp_err = [0u8; 36]; + icmp_err[0] = Icmpv4Types::DestinationUnreachable; + let cksum = compute_ipv4_checksum(&icmp_err); + icmp_err[2] = cksum[0]; + icmp_err[3] = cksum[1]; + assert_not_sent(build_ipv4_frame( + SRC_MAC, + DST_MAC, + REMOTE_IP, + LOCAL_IP, + IpProtocols::Icmp, + &icmp_err, + )); + } + + #[test] + fn dest_unreachable_allowed_for_non_error() { + let echo = build_echo_request(SRC_MAC, DST_MAC, REMOTE_IP, LOCAL_IP, 1, 1, &[0; 8]); + let mut buf = vec![0u8; 256]; + buf[..echo.len()].copy_from_slice(&echo); + let frame = Frame::new(0, &mut buf, echo.len(), false); + + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + send_destination_unreachable( + frame, + Icmpv4Codes::ProtocolUnreachable, + 0, + false, + &mut rx, + &mut tx, + ); + assert_eq!(rx.num_frames(), 0); + assert_eq!(tx.num_frames(), 1); + } + + #[test] + fn fragmentation_needed_updates_pmtu() { + let now = Instant::now(); + // Build a Fragmentation Needed ICMP message carrying an embedded IPv4 header. + let dest_ip = Ipv4Address::new([172, 16, 0, 1]); + + // Build embedded original IPv4 header (20 bytes) + 8 bytes data. + let mut embedded = [0u8; 28]; + embedded[0] = 0x45; + embedded[9] = IpProtocols::Udp; + let src_bytes: [u8; 4] = REMOTE_IP.into(); + embedded[12..16].copy_from_slice(&src_bytes); + let dst_bytes: [u8; 4] = dest_ip.into(); + embedded[16..20].copy_from_slice(&dst_bytes); + + // Build ICMP Dest Unreachable / Fragmentation Needed with MTU=1280. + let mut icmp_payload = vec![0u8; ICMPV4_HEADER_LEN + embedded.len()]; + icmp_payload[0] = Icmpv4Types::DestinationUnreachable; + icmp_payload[1] = Icmpv4Codes::FragmentationNeeded; + icmp_payload[6] = (1280u16 >> 8) as u8; + icmp_payload[7] = 1280u16 as u8; + icmp_payload[ICMPV4_HEADER_LEN..].copy_from_slice(&embedded); + let cksum = compute_ipv4_checksum(&icmp_payload); + icmp_payload[2] = cksum[0]; + icmp_payload[3] = cksum[1]; + + let data = build_ipv4_frame( + SRC_MAC, + DST_MAC, + REMOTE_IP, + LOCAL_IP, + IpProtocols::Icmp, + &icmp_payload, + ); + let mut buf = vec![0u8; 256]; + buf[..data.len()].copy_from_slice(&data); + let frame = Frame::new(0, &mut buf, data.len(), false); + + let pmtu = PmtuCache::new(); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + handle_icmpv4(frame, &pmtu, now, false, false, &mut rx, &mut tx); + + // Frame goes to rx (not an echo request) + assert_eq!(rx.num_frames(), 1); + // PMTU cache should be updated for the embedded destination IP + assert_eq!(pmtu.get(now, &IpAddress::V4(dest_ip)), 1280); + } +} diff --git a/src/net/handler/icmpv6.rs b/src/net/handler/icmpv6.rs new file mode 100644 index 0000000..0ec3c49 --- /dev/null +++ b/src/net/handler/icmpv6.rs @@ -0,0 +1,911 @@ +use std::mem::size_of; + +use coarsetime::Instant; + +use crate::{ + net::{ + NeighborHandler, PmtuCache, + checksum::compute_icmpv6_checksum, + wire::{ + ethernet::EthernetFrame, + icmpv6::{ + ICMPV6_HEADER_LEN, Icmpv6Codes, Icmpv6Frame, Icmpv6Header, Icmpv6Types, + MAX_ERROR_PAYLOAD, is_icmpv6_error, + }, + ip::{IPV6_HEADER_LEN, IpProtocols, Ipv6Address, Ipv6Header}, + }, + }, + xdp::frame::{Frame, FrameBuffer}, +}; + +/// Processes an incoming ICMPv6 packet. +/// +/// The frame must already be validated as a proper IPv6 packet with +/// an upper-layer protocol of ICMPv6 by the [`Ipv6Handler`]. +/// +/// `icmpv6_offset` is the byte offset from the start of the frame +/// to the ICMPv6 header (accounting for any extension headers). +/// `icmpv6_len` is the number of bytes in the ICMPv6 message. +/// +/// Currently handles: +/// * Echo Request (type 128) -> Echo Reply (type 129), pushed to +/// `tx_return`. +/// * All other types (including NDP 133--137) pass to `rx_return`. +pub fn handle_icmpv6<'umem>( + mut frame: Frame<'umem>, + icmpv6_offset: usize, + icmpv6_len: usize, + neighbor_handler: &NeighborHandler, + pmtu: &PmtuCache, + now: Instant, + rx_offload: bool, + tx_offload: bool, + rx_return: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, +) { + if icmpv6_len < ICMPV6_HEADER_LEN { + rx_return.push(frame); + return; + } + + let ip = Ipv6Header::from_bytes(&frame); + let src_addr = ip.src_addr; + let dst_addr = ip.dst_addr; + + let icmpv6_end = icmpv6_offset + icmpv6_len; + + if !rx_offload + && compute_icmpv6_checksum(&src_addr, &dst_addr, &frame[icmpv6_offset..icmpv6_end]) + != [0x00, 0x00] + { + rx_return.push(frame); + return; + } + + let icmpv6 = Icmpv6Header::from_bytes_at(&frame, icmpv6_offset); + let icmpv6_type = icmpv6.icmp_type; + + match icmpv6_type { + Icmpv6Types::EchoRequest => { + // Silently discard echo requests to multicast destinations. + // We cannot form a correct reply (source must be unicast, and + // we do not track our own unicast address here). + if dst_addr.is_multicast() { + rx_return.push(frame); + return; + } + + // Swap Ethernet MACs. + let eth = EthernetFrame::from_bytes_mut(&mut frame); + std::mem::swap(&mut eth.dst_mac, &mut eth.src_mac); + + // Swap IPv6 addresses and reset hop limit. + let ip = Ipv6Header::from_bytes_mut(&mut frame); + std::mem::swap(&mut ip.src_addr, &mut ip.dst_addr); + ip.hop_limit = 64; + + // Set ICMPv6 type to Echo Reply and recompute checksum. + let icmpv6 = Icmpv6Header::from_bytes_at_mut(&mut frame, icmpv6_offset); + icmpv6.icmp_type = Icmpv6Types::EchoReply; + icmpv6.checksum = [0, 0]; + if !tx_offload { + let cksum = compute_icmpv6_checksum( + &dst_addr, // new src = old dst + &src_addr, // new dst = old src + &frame[icmpv6_offset..icmpv6_end], + ); + Icmpv6Header::from_bytes_at_mut(&mut frame, icmpv6_offset).checksum = cksum; + } + + tx_return.push(frame); + } + Icmpv6Types::PacketTooBig => { + // Extract MTU from ICMPv6 header body. + let icmpv6 = Icmpv6Header::from_bytes_at(&frame, icmpv6_offset); + let mtu = icmpv6.body_as_u32(); + + // Extract original destination IP from embedded IPv6 header. + // The embedded IPv6 header starts at icmpv6_offset + ICMPV6_HEADER_LEN. + // The destination address is at offset 24 within the IPv6 header. + let dst_offset = icmpv6_offset + ICMPV6_HEADER_LEN + 24; + if dst_offset + 16 <= frame.len() { + let mut octets = [0u8; 16]; + octets.copy_from_slice(&frame[dst_offset..dst_offset + 16]); + let dst_ip = Ipv6Address::new(octets); + pmtu.update(now, dst_ip.into(), mtu); + } + + rx_return.push(frame); + } + Icmpv6Types::RouterAdvertisement + | Icmpv6Types::NeighborSolicitation + | Icmpv6Types::NeighborAdvertisement => { + neighbor_handler.handle_ndp( + now, + frame, + icmpv6_offset, + icmpv6_len, + rx_return, + tx_return, + ); + } + _ => rx_return.push(frame), + } +} + +/// Builds and sends an ICMPv6 error message by transforming the +/// incoming frame in-place. +/// +/// Per RFC 4443, the error payload contains as much of the original +/// IPv6 packet as possible without exceeding the minimum IPv6 MTU +/// (1280 bytes). +/// +/// RFC 4443 §2.4 restrictions: +/// * MUST NOT send in response to an ICMPv6 error message. +/// * MUST NOT send in response to a packet destined to a multicast +/// address (exceptions: Packet Too Big and Parameter Problem code 2 +/// are allowed). +/// * MUST NOT send if the source address does not uniquely identify +/// a single node (multicast, unspecified). +/// +/// `orig_upper_protocol` and `orig_upper_offset` identify the +/// upper-layer protocol and its byte offset from the frame start, +/// used to detect ICMPv6-error-in-response-to-error. +/// +/// If the response cannot be generated the frame goes to `rx_return`. +pub fn send_icmpv6_error<'umem>( + mut frame: Frame<'umem>, + icmpv6_type: u8, + code: u8, + body: [u8; 4], + orig_upper_protocol: u8, + orig_upper_offset: usize, + tx_offload: bool, + rx_return: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, +) { + let eth_len = size_of::(); + + let ip = Ipv6Header::from_bytes(&frame); + let src_addr = ip.src_addr; + let dst_addr = ip.dst_addr; + let payload_length = ip.payload_length() as usize; + + // RFC 4443 §2.4(e): MUST NOT send if source is not unicast. + if src_addr.is_multicast() || src_addr.is_unspecified() { + rx_return.push(frame); + return; + } + + // RFC 4443 §2.4(e): MUST NOT send for multicast dst, with + // exceptions for Packet Too Big and Parameter Problem code 2. + if dst_addr.is_multicast() { + let allowed = icmpv6_type == Icmpv6Types::PacketTooBig + || (icmpv6_type == Icmpv6Types::ParameterProblem && code == Icmpv6Codes::BeyondScope); + if !allowed { + rx_return.push(frame); + return; + } + } + + // RFC 4443 §2.4(e): MUST NOT send in response to an ICMPv6 error. + if orig_upper_protocol == IpProtocols::IcmpV6 + && frame.len() > orig_upper_offset + && is_icmpv6_error(frame[orig_upper_offset]) + { + rx_return.push(frame); + return; + } + + // Compute how much of the original IPv6 packet to include. + let orig_ipv6_len = IPV6_HEADER_LEN + payload_length; + let save_len = orig_ipv6_len.min(MAX_ERROR_PAYLOAD); + + // New frame dimensions. + let icmpv6_msg_len = ICMPV6_HEADER_LEN + save_len; + let new_ipv6_payload_len = icmpv6_msg_len; + let new_frame_len = eth_len + IPV6_HEADER_LEN + new_ipv6_payload_len; + + if new_frame_len > frame.capacity() { + rx_return.push(frame); + return; + } + + // Set frame length to accommodate both the read (source data) and + // the write (destination). The source data lives at eth_len..eth_len+save_len, + // the destination starts at eth_len+IPV6_HEADER_LEN+ICMPV6_HEADER_LEN. + let working_len = frame.len().max(new_frame_len); + unsafe { + frame.set_len(working_len); + } + + // Shift the original IPv6 packet data into the ICMP payload area. + let icmp_payload_start = eth_len + IPV6_HEADER_LEN + ICMPV6_HEADER_LEN; + frame.copy_within(eth_len..eth_len + save_len, icmp_payload_start); + + // Build all headers via struct overlay. + let icmp_start = eth_len + IPV6_HEADER_LEN; + { + let pkt = Icmpv6Frame::from_bytes_mut(&mut frame); + + // Swap Ethernet MACs. + std::mem::swap(&mut pkt.ethernet.dst_mac, &mut pkt.ethernet.src_mac); + + // Build the new IPv6 header. + pkt.ipv6.version_tc_fl = [0x60, 0x00, 0x00, 0x00]; + pkt.ipv6.payload_length = (new_ipv6_payload_len as u16).to_be_bytes(); + pkt.ipv6.next_header = IpProtocols::IcmpV6; + pkt.ipv6.hop_limit = 64; + pkt.ipv6.src_addr = dst_addr; // our address + pkt.ipv6.dst_addr = src_addr; // original sender + + // Build the ICMPv6 header. + pkt.icmpv6.icmp_type = icmpv6_type; + pkt.icmpv6.code = code; + pkt.icmpv6.checksum = [0, 0]; + pkt.icmpv6.body = body; + } + + // Truncate to the correct length before computing checksum. + unsafe { + frame.set_len(new_frame_len); + } + + // Compute and write ICMPv6 checksum (over pseudo-header + message). + if !tx_offload { + let icmp_end = icmp_start + icmpv6_msg_len; + let cksum = compute_icmpv6_checksum( + &dst_addr, // new src + &src_addr, // new dst + &frame[icmp_start..icmp_end], + ); + Icmpv6Header::from_bytes_at_mut(&mut frame, icmp_start).checksum = cksum; + } + + tx_return.push(frame); +} + +#[cfg(test)] +mod tests { + use coarsetime::Duration; + + use crate::{ + net::wire::{ethernet::MacAddress, ip::IpAddress}, + xdp::frame::BasicFrameBuffer, + }; + + use super::*; + + const SRC_MAC: [u8; 6] = MacAddress::zero().octets; + const DST_MAC: [u8; 6] = [0x11, 0x22, 0x33, 0x44, 0x55, 0x02]; + const REMOTE_IP: Ipv6Address = + Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]); + const LOCAL_IP: Ipv6Address = + Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + const MCAST: Ipv6Address = + Ipv6Address::new([0xFF, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + + /// Builds a complete Ethernet + IPv6 + ICMPv6 Echo Request frame. + fn build_echo_request( + src_mac: [u8; 6], + dst_mac: [u8; 6], + src_ip: Ipv6Address, + dst_ip: Ipv6Address, + id: u16, + seq: u16, + data: &[u8], + ) -> Vec { + let eth_len = size_of::(); + let icmpv6_len = ICMPV6_HEADER_LEN + data.len(); + let frame_len = eth_len + IPV6_HEADER_LEN + icmpv6_len; + let mut buf = vec![0u8; frame_len]; + + buf[0..6].copy_from_slice(&dst_mac); + buf[6..12].copy_from_slice(&src_mac); + buf[12] = 0x86; + buf[13] = 0xDD; + + buf[14] = 0x60; + buf[18..20].copy_from_slice(&(icmpv6_len as u16).to_be_bytes()); + buf[20] = IpProtocols::IcmpV6; + buf[21] = 64; + let src_bytes: [u8; 16] = src_ip.into(); + buf[22..38].copy_from_slice(&src_bytes); + let dst_bytes: [u8; 16] = dst_ip.into(); + buf[38..54].copy_from_slice(&dst_bytes); + + let icmp_off = eth_len + IPV6_HEADER_LEN; + buf[icmp_off] = Icmpv6Types::EchoRequest; + buf[icmp_off + 1] = 0; + buf[icmp_off + 4] = (id >> 8) as u8; + buf[icmp_off + 5] = id as u8; + buf[icmp_off + 6] = (seq >> 8) as u8; + buf[icmp_off + 7] = seq as u8; + buf[icmp_off + 8..].copy_from_slice(data); + + buf[icmp_off + 2] = 0; + buf[icmp_off + 3] = 0; + let cksum = compute_icmpv6_checksum(&src_ip, &dst_ip, &buf[icmp_off..]); + buf[icmp_off + 2] = cksum[0]; + buf[icmp_off + 3] = cksum[1]; + + buf + } + + /// Builds a generic Ethernet + IPv6 frame. + fn build_ipv6_frame( + src_mac: [u8; 6], + dst_mac: [u8; 6], + src_ip: Ipv6Address, + dst_ip: Ipv6Address, + next_header: u8, + payload: &[u8], + ) -> Vec { + let eth_len = size_of::(); + let frame_len = eth_len + IPV6_HEADER_LEN + payload.len(); + let mut buf = vec![0u8; frame_len]; + + buf[0..6].copy_from_slice(&dst_mac); + buf[6..12].copy_from_slice(&src_mac); + buf[12] = 0x86; + buf[13] = 0xDD; + + buf[14] = 0x60; + buf[18..20].copy_from_slice(&(payload.len() as u16).to_be_bytes()); + buf[20] = next_header; + buf[21] = 64; + let src_bytes: [u8; 16] = src_ip.into(); + buf[22..38].copy_from_slice(&src_bytes); + let dst_bytes: [u8; 16] = dst_ip.into(); + buf[38..54].copy_from_slice(&dst_bytes); + + buf[eth_len + IPV6_HEADER_LEN..].copy_from_slice(payload); + buf + } + + /// Helper: asserts frame was rejected (goes to rx, not tx). + fn assert_rejected(rx: &BasicFrameBuffer, tx: &BasicFrameBuffer) { + assert_eq!(rx.num_frames(), 1, "expected frame in rx_return"); + assert_eq!(tx.num_frames(), 0, "expected nothing in tx_return"); + } + + // -- Echo Reply tests -- + + #[test] + fn echo_reply_complete_response() { + let now = Instant::now(); + let data = [0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE]; + let echo = build_echo_request(SRC_MAC, DST_MAC, REMOTE_IP, LOCAL_IP, 0x1234, 0x0005, &data); + let mut buf = vec![0u8; 512]; + buf[..echo.len()].copy_from_slice(&echo); + let frame = Frame::new(0, &mut buf, echo.len(), false); + + let icmpv6_offset = size_of::() + IPV6_HEADER_LEN; + let icmpv6_len = echo.len() - icmpv6_offset; + + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + let neighbor_handler = NeighborHandler::new("test", Duration::from_secs(60)).unwrap(); + handle_icmpv6( + frame, + icmpv6_offset, + icmpv6_len, + &neighbor_handler, + &PmtuCache::new(), + now, + false, + false, + &mut rx, + &mut tx, + ); + + assert_eq!(rx.num_frames(), 0); + assert_eq!(tx.num_frames(), 1); + + let reply = tx.pop().unwrap(); + + // Type and code + assert_eq!(reply[icmpv6_offset], Icmpv6Types::EchoReply); + assert_eq!(reply[icmpv6_offset + 1], 0); + + // Ethernet MACs swapped + let eth = EthernetFrame::from_bytes(&reply); + assert_eq!(eth.src_mac, MacAddress::from(DST_MAC)); + assert_eq!(eth.dst_mac, MacAddress::from(SRC_MAC)); + + // IPv6 addresses swapped, hop limit reset + let ip = Ipv6Header::from_bytes(&reply); + assert_eq!(ip.src_addr, LOCAL_IP); + assert_eq!(ip.dst_addr, REMOTE_IP); + assert_eq!(ip.hop_limit, 64); + + // Identifier, sequence, and data preserved + assert_eq!(reply[icmpv6_offset + 4], 0x12); + assert_eq!(reply[icmpv6_offset + 5], 0x34); + assert_eq!(reply[icmpv6_offset + 6], 0x00); + assert_eq!(reply[icmpv6_offset + 7], 0x05); + assert_eq!(&reply[icmpv6_offset + 8..icmpv6_offset + 16], &data); + + // Valid ICMPv6 checksum + let cksum = compute_icmpv6_checksum( + &ip.src_addr, + &ip.dst_addr, + &reply[icmpv6_offset..icmpv6_offset + icmpv6_len], + ); + assert_eq!(cksum, [0x00, 0x00]); + } + + #[test] + fn echo_request_bad_checksum_rejected() { + let now = Instant::now(); + let mut echo = build_echo_request(SRC_MAC, DST_MAC, REMOTE_IP, LOCAL_IP, 1, 1, &[0; 8]); + let icmpv6_offset = size_of::() + IPV6_HEADER_LEN; + echo[icmpv6_offset + 2] ^= 0xFF; + let mut buf = vec![0u8; 512]; + buf[..echo.len()].copy_from_slice(&echo); + let frame = Frame::new(0, &mut buf, echo.len(), false); + + let icmpv6_len = echo.len() - icmpv6_offset; + + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + let neighbor_handler = NeighborHandler::new("test", Duration::from_secs(60)).unwrap(); + handle_icmpv6( + frame, + icmpv6_offset, + icmpv6_len, + &neighbor_handler, + &PmtuCache::new(), + now, + false, + false, + &mut rx, + &mut tx, + ); + assert_rejected(&rx, &tx); + } + + #[test] + fn echo_request_too_short_rejected() { + let now = Instant::now(); + let data = build_ipv6_frame( + SRC_MAC, + DST_MAC, + REMOTE_IP, + LOCAL_IP, + IpProtocols::IcmpV6, + &[0; 4], + ); + let mut buf = vec![0u8; 512]; + buf[..data.len()].copy_from_slice(&data); + let frame = Frame::new(0, &mut buf, data.len(), false); + + let icmpv6_offset = size_of::() + IPV6_HEADER_LEN; + + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + let neighbor_handler = NeighborHandler::new("test", Duration::from_secs(60)).unwrap(); + handle_icmpv6( + frame, + icmpv6_offset, + 4, + &neighbor_handler, + &PmtuCache::new(), + now, + false, + false, + &mut rx, + &mut tx, + ); + assert_rejected(&rx, &tx); + } + + #[test] + fn echo_request_to_multicast_rejected() { + let now = Instant::now(); + let echo = build_echo_request(SRC_MAC, DST_MAC, REMOTE_IP, MCAST, 1, 1, &[0; 8]); + let mut buf = vec![0u8; 512]; + buf[..echo.len()].copy_from_slice(&echo); + let frame = Frame::new(0, &mut buf, echo.len(), false); + + let icmpv6_offset = size_of::() + IPV6_HEADER_LEN; + let icmpv6_len = echo.len() - icmpv6_offset; + + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + let neighbor_handler = NeighborHandler::new("test", Duration::from_secs(60)).unwrap(); + handle_icmpv6( + frame, + icmpv6_offset, + icmpv6_len, + &neighbor_handler, + &PmtuCache::new(), + now, + false, + false, + &mut rx, + &mut tx, + ); + assert_rejected(&rx, &tx); + } + + #[test] + fn non_echo_request_goes_to_rx() { + let now = Instant::now(); + let mut icmpv6_payload = [0u8; 48]; + icmpv6_payload[0] = Icmpv6Types::DestinationUnreachable; + let cksum = compute_icmpv6_checksum(&REMOTE_IP, &LOCAL_IP, &icmpv6_payload); + icmpv6_payload[2] = cksum[0]; + icmpv6_payload[3] = cksum[1]; + + let data = build_ipv6_frame( + SRC_MAC, + DST_MAC, + REMOTE_IP, + LOCAL_IP, + IpProtocols::IcmpV6, + &icmpv6_payload, + ); + let mut buf = vec![0u8; 512]; + buf[..data.len()].copy_from_slice(&data); + let frame = Frame::new(0, &mut buf, data.len(), false); + + let icmpv6_offset = size_of::() + IPV6_HEADER_LEN; + + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + let neighbor_handler = NeighborHandler::new("test", Duration::from_secs(60)).unwrap(); + handle_icmpv6( + frame, + icmpv6_offset, + icmpv6_payload.len(), + &neighbor_handler, + &PmtuCache::new(), + now, + false, + false, + &mut rx, + &mut tx, + ); + assert_rejected(&rx, &tx); + } + + #[test] + fn packet_too_big_updates_pmtu() { + let now = Instant::now(); + let dest_ip = Ipv6Address::new([ + 0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x99, + ]); + let eth_len = size_of::(); + + // Build an ICMPv6 Packet Too Big message with embedded IPv6 header. + // ICMPv6 header (8 bytes): type=2, code=0, checksum=0, MTU=1280 + // Embedded IPv6 header (40 bytes): src=REMOTE_IP, dst=dest_ip + let mut icmpv6_msg = vec![0u8; ICMPV6_HEADER_LEN + IPV6_HEADER_LEN]; + icmpv6_msg[0] = Icmpv6Types::PacketTooBig; + icmpv6_msg[1] = 0; + // MTU = 1280 in body bytes [4..8] + icmpv6_msg[4..8].copy_from_slice(&1280u32.to_be_bytes()); + // Embedded IPv6 header at offset 8 + icmpv6_msg[8] = 0x60; // version + icmpv6_msg[8 + 6] = IpProtocols::Udp; // next header + icmpv6_msg[8 + 7] = 64; // hop limit + let remote_bytes: [u8; 16] = REMOTE_IP.into(); + icmpv6_msg[8 + 8..8 + 24].copy_from_slice(&remote_bytes); // src + let dest_bytes: [u8; 16] = dest_ip.into(); + icmpv6_msg[8 + 24..8 + 40].copy_from_slice(&dest_bytes); // dst + + // Compute ICMPv6 checksum over the whole message + let cksum = compute_icmpv6_checksum(&REMOTE_IP, &LOCAL_IP, &icmpv6_msg); + icmpv6_msg[2] = cksum[0]; + icmpv6_msg[3] = cksum[1]; + + let data = build_ipv6_frame( + SRC_MAC, + DST_MAC, + REMOTE_IP, + LOCAL_IP, + IpProtocols::IcmpV6, + &icmpv6_msg, + ); + let mut buf = vec![0u8; 512]; + buf[..data.len()].copy_from_slice(&data); + let frame = Frame::new(0, &mut buf, data.len(), false); + + let icmpv6_offset = eth_len + IPV6_HEADER_LEN; + + let pmtu = PmtuCache::new(); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + let neighbor_handler = NeighborHandler::new("test", Duration::from_secs(60)).unwrap(); + handle_icmpv6( + frame, + icmpv6_offset, + icmpv6_msg.len(), + &neighbor_handler, + &pmtu, + now, + false, + false, + &mut rx, + &mut tx, + ); + + // Frame goes to rx (informational, not echo) + assert_eq!(rx.num_frames(), 1); + // PMTU cache should be updated for the embedded destination IP + assert_eq!(pmtu.get(now, &IpAddress::V6(dest_ip)), 1280); + } + + // -- send_icmpv6_error tests -- + + #[test] + fn error_complete_response() { + let payload = [0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]; + let data = build_ipv6_frame(SRC_MAC, DST_MAC, REMOTE_IP, LOCAL_IP, 99, &payload); + let mut buf = vec![0u8; 2048]; + buf[..data.len()].copy_from_slice(&data); + + let eth_len = size_of::(); + let orig_ipv6_packet: Vec = data[eth_len..].to_vec(); + + let frame = Frame::new(0, &mut buf, data.len(), false); + let upper_offset = eth_len + IPV6_HEADER_LEN; + + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + send_icmpv6_error( + frame, + Icmpv6Types::DestinationUnreachable, + Icmpv6Codes::PortUnreachable, + [0; 4], + 99, + upper_offset, + false, + &mut rx, + &mut tx, + ); + + assert_eq!(rx.num_frames(), 0); + assert_eq!(tx.num_frames(), 1); + + let reply = tx.pop().unwrap(); + let icmp_start = eth_len + IPV6_HEADER_LEN; + + // Type and code + assert_eq!(reply[icmp_start], Icmpv6Types::DestinationUnreachable); + assert_eq!(reply[icmp_start + 1], Icmpv6Codes::PortUnreachable); + + // Addresses swapped + let eth = EthernetFrame::from_bytes(&reply); + assert_eq!(eth.src_mac, MacAddress::from(DST_MAC)); + assert_eq!(eth.dst_mac, MacAddress::from(SRC_MAC)); + let ip = Ipv6Header::from_bytes(&reply); + assert_eq!(ip.src_addr, LOCAL_IP); + assert_eq!(ip.dst_addr, REMOTE_IP); + assert_eq!(ip.next_header, IpProtocols::IcmpV6); + + // Original IPv6 header + payload preserved in ICMP payload + let icmp_payload_start = icmp_start + ICMPV6_HEADER_LEN; + let icmp_payload_end = icmp_payload_start + orig_ipv6_packet.len(); + assert_eq!( + &reply[icmp_payload_start..icmp_payload_end], + &orig_ipv6_packet[..] + ); + + // Valid ICMPv6 checksum + let icmp_end = icmp_start + ip.payload_length() as usize; + let cksum = + compute_icmpv6_checksum(&ip.src_addr, &ip.dst_addr, &reply[icmp_start..icmp_end]); + assert_eq!(cksum, [0x00, 0x00]); + } + + #[test] + fn error_packet_too_big_includes_mtu() { + let data = build_ipv6_frame(SRC_MAC, DST_MAC, REMOTE_IP, LOCAL_IP, 17, &[0; 32]); + let mut buf = vec![0u8; 2048]; + buf[..data.len()].copy_from_slice(&data); + let frame = Frame::new(0, &mut buf, data.len(), false); + let upper_offset = size_of::() + IPV6_HEADER_LEN; + + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + send_icmpv6_error( + frame, + Icmpv6Types::PacketTooBig, + 0, + 1280u32.to_be_bytes(), + 17, + upper_offset, + false, + &mut rx, + &mut tx, + ); + + let reply = tx.pop().unwrap(); + let icmp_start = size_of::() + IPV6_HEADER_LEN; + let body_mtu = u32::from_be_bytes([ + reply[icmp_start + 4], + reply[icmp_start + 5], + reply[icmp_start + 6], + reply[icmp_start + 7], + ]); + assert_eq!(body_mtu, 1280); + } + + #[test] + fn error_parameter_problem_includes_pointer() { + let data = build_ipv6_frame(SRC_MAC, DST_MAC, REMOTE_IP, LOCAL_IP, 99, &[0; 32]); + let mut buf = vec![0u8; 2048]; + buf[..data.len()].copy_from_slice(&data); + let frame = Frame::new(0, &mut buf, data.len(), false); + let upper_offset = size_of::() + IPV6_HEADER_LEN; + + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + send_icmpv6_error( + frame, + Icmpv6Types::ParameterProblem, + Icmpv6Codes::UnrecognizedNextHeader, + 6u32.to_be_bytes(), + 99, + upper_offset, + false, + &mut rx, + &mut tx, + ); + + let reply = tx.pop().unwrap(); + let icmp_start = size_of::() + IPV6_HEADER_LEN; + let body_ptr = u32::from_be_bytes([ + reply[icmp_start + 4], + reply[icmp_start + 5], + reply[icmp_start + 6], + reply[icmp_start + 7], + ]); + assert_eq!(body_ptr, 6); + } + + #[test] + fn error_rfc4443_restrictions() { + let upper_offset = size_of::() + IPV6_HEADER_LEN; + + let assert_not_sent = |data: Vec, proto: u8| { + let mut buf = vec![0u8; 2048]; + buf[..data.len()].copy_from_slice(&data); + let frame = Frame::new(0, &mut buf, data.len(), false); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + send_icmpv6_error( + frame, + Icmpv6Types::DestinationUnreachable, + Icmpv6Codes::NoRouteToDestination, + [0; 4], + proto, + upper_offset, + false, + &mut rx, + &mut tx, + ); + assert_rejected(&rx, &tx); + }; + + // Multicast destination + assert_not_sent( + build_ipv6_frame(SRC_MAC, DST_MAC, REMOTE_IP, MCAST, 99, &[0; 32]), + 99, + ); + // Multicast source + assert_not_sent( + build_ipv6_frame(SRC_MAC, DST_MAC, MCAST, LOCAL_IP, 99, &[0; 32]), + 99, + ); + // Unspecified source + assert_not_sent( + build_ipv6_frame( + SRC_MAC, + DST_MAC, + Ipv6Address::unspecified(), + LOCAL_IP, + 99, + &[0; 32], + ), + 99, + ); + // ICMPv6 error as trigger + let mut icmpv6_err = [0u8; 48]; + icmpv6_err[0] = Icmpv6Types::DestinationUnreachable; + let cksum = compute_icmpv6_checksum(&REMOTE_IP, &LOCAL_IP, &icmpv6_err); + icmpv6_err[2] = cksum[0]; + icmpv6_err[3] = cksum[1]; + assert_not_sent( + build_ipv6_frame( + SRC_MAC, + DST_MAC, + REMOTE_IP, + LOCAL_IP, + IpProtocols::IcmpV6, + &icmpv6_err, + ), + IpProtocols::IcmpV6, + ); + } + + #[test] + fn error_packet_too_big_allowed_for_multicast_dst() { + let data = build_ipv6_frame(SRC_MAC, DST_MAC, REMOTE_IP, MCAST, 17, &[0; 32]); + let mut buf = vec![0u8; 2048]; + buf[..data.len()].copy_from_slice(&data); + let frame = Frame::new(0, &mut buf, data.len(), false); + let upper_offset = size_of::() + IPV6_HEADER_LEN; + + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + send_icmpv6_error( + frame, + Icmpv6Types::PacketTooBig, + 0, + 1280u32.to_be_bytes(), + 17, + upper_offset, + false, + &mut rx, + &mut tx, + ); + + assert_eq!(rx.num_frames(), 0); + assert_eq!(tx.num_frames(), 1); + } + + #[test] + fn error_parameter_problem_code2_allowed_for_multicast_dst() { + let data = build_ipv6_frame(SRC_MAC, DST_MAC, REMOTE_IP, MCAST, 99, &[0; 32]); + let mut buf = vec![0u8; 2048]; + buf[..data.len()].copy_from_slice(&data); + let frame = Frame::new(0, &mut buf, data.len(), false); + let upper_offset = size_of::() + IPV6_HEADER_LEN; + + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + send_icmpv6_error( + frame, + Icmpv6Types::ParameterProblem, + Icmpv6Codes::BeyondScope, + 6u32.to_be_bytes(), + 99, + upper_offset, + false, + &mut rx, + &mut tx, + ); + + assert_eq!(rx.num_frames(), 0); + assert_eq!(tx.num_frames(), 1); + } + + #[test] + fn error_allowed_for_icmpv6_non_error() { + let echo = build_echo_request(SRC_MAC, DST_MAC, REMOTE_IP, LOCAL_IP, 1, 1, &[0; 8]); + let mut buf = vec![0u8; 2048]; + buf[..echo.len()].copy_from_slice(&echo); + let frame = Frame::new(0, &mut buf, echo.len(), false); + let upper_offset = size_of::() + IPV6_HEADER_LEN; + + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + send_icmpv6_error( + frame, + Icmpv6Types::DestinationUnreachable, + Icmpv6Codes::PortUnreachable, + [0; 4], + IpProtocols::IcmpV6, + upper_offset, + false, + &mut rx, + &mut tx, + ); + + assert_eq!(rx.num_frames(), 0); + assert_eq!(tx.num_frames(), 1); + } +} diff --git a/src/net/handler/ipv4.rs b/src/net/handler/ipv4.rs new file mode 100644 index 0000000..6d86859 --- /dev/null +++ b/src/net/handler/ipv4.rs @@ -0,0 +1,640 @@ +use coarsetime::Instant; + +use crate::{ + net::{ + PmtuCache, + checksum::verify_ipv4_checksum, + wire::{ + ethernet::EthernetFrame, + icmpv4::Icmpv4Codes, + ip::{IPV4_MIN_FRAME_LEN, IpProtocols, Ipv4Header}, + }, + }, + xdp::frame::{Frame, FrameBuffer}, +}; + +use super::{icmpv4, tcp::TcpHandler, udp::UdpHandler}; + +/// Layer-3 handler for incoming IPv4 frames. +/// +/// Validates the IPv4 header (version, length, checksum) and dispatches +/// to protocol-specific handlers based on the protocol field. +/// +/// The frame is always consumed and pushed to exactly one buffer: +/// +/// * `rx_return` -- validation failures or received data for upper layers. +/// * `tx_return` -- responses generated by protocol handlers (e.g. +/// ICMP echo reply, Destination Unreachable). +pub struct Ipv4Handler { + rx_offload: bool, + tx_offload: bool, +} + +impl Ipv4Handler { + pub fn new(rx_offload: bool, tx_offload: bool) -> Self { + Self { + rx_offload, + tx_offload, + } + } + + /// Processes an incoming IPv4 frame. + /// + /// Validates the header and dispatches to the appropriate protocol + /// handler. The frame is always consumed. + pub fn handle<'umem>( + &mut self, + frame: Frame<'umem>, + udp_handler: &mut UdpHandler<'umem>, + tcp_handler: &mut TcpHandler, + neighbor_handler: &crate::net::NeighborHandler, + pmtu: &PmtuCache, + now: Instant, + free_frames: &mut impl FrameBuffer<'umem>, + rx_return: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) { + if frame.len() < IPV4_MIN_FRAME_LEN { + rx_return.push(frame); + return; + } + + let ip = Ipv4Header::from_bytes(&frame); + + if ip.version() != 4 { + rx_return.push(frame); + return; + } + + if ip.ihl() < 5 { + rx_return.push(frame); + return; + } + + let total_length = ip.total_length() as usize; + let header_len = ip.header_len(); + + if total_length < header_len { + rx_return.push(frame); + return; + } + + let eth_len = size_of::(); + if frame.len() < eth_len + total_length { + rx_return.push(frame); + return; + } + + if !self.rx_offload { + let ip_bytes = &frame[eth_len..eth_len + header_len]; + if !verify_ipv4_checksum(ip_bytes) { + rx_return.push(frame); + return; + } + } + + let protocol = ip.protocol; + match protocol { + IpProtocols::Icmp => icmpv4::handle_icmpv4( + frame, + pmtu, + now, + self.rx_offload, + self.tx_offload, + rx_return, + tx_return, + ), + IpProtocols::Udp => udp_handler.process_ipv4(frame, rx_return), + IpProtocols::Tcp => { + tcp_handler.process_ipv4(frame, now, neighbor_handler, free_frames, rx_return, tx_return) + } + _ => icmpv4::send_destination_unreachable( + frame, + Icmpv4Codes::ProtocolUnreachable, + 0, + self.tx_offload, + rx_return, + tx_return, + ), + } + } +} + +#[cfg(test)] +mod tests { + use crate::{ + net::{ + NeighborHandler, + checksum::{compute_ipv4_checksum, compute_udp_checksum}, + handler::tcp::TcpHandler, + wire::{ + ip::{IPV4_MIN_HEADER_LEN, IpAddress, Ipv4Address}, + udp::UDP_HEADER_LEN, + }, + }, + xdp::frame::BasicFrameBuffer, + }; + + use super::*; + + const REMOTE_IP: Ipv4Address = Ipv4Address::new([10, 0, 0, 2]); + const LOCAL_IP: Ipv4Address = Ipv4Address::new([192, 168, 1, 1]); + + fn new_handler() -> Ipv4Handler { + Ipv4Handler::new(false, false) + } + + fn new_udp_handler<'umem>() -> UdpHandler<'umem> { + UdpHandler::new(256, false) + } + + fn new_tcp_handler() -> TcpHandler { + TcpHandler::new(false, false) + } + + fn new_neighbor_handler() -> NeighborHandler { + NeighborHandler::new("test0", coarsetime::Duration::from_secs(60)).unwrap() + } + + /// Builds a minimal valid UDP segment (header only, no payload) with a + /// correct IPv4 checksum. Ports are zero, length is 8. + fn build_udp_bytes_v4(src: &Ipv4Address, dst: &Ipv4Address) -> Vec { + let mut buf = vec![0u8; UDP_HEADER_LEN]; + let udp_len = UDP_HEADER_LEN as u16; + buf[4..6].copy_from_slice(&udp_len.to_be_bytes()); + let cksum = compute_udp_checksum(src, dst, &buf); + buf[6] = cksum[0]; + buf[7] = cksum[1]; + buf + } + + fn new_udp_handler_with_socket<'umem>( + port: u16, + ) -> ( + UdpHandler<'umem>, + crate::net::socket::LocalQueue>, + ) { + let mut udp = UdpHandler::new(256, false); + let rx_queue = udp.bind(IpAddress::V4(LOCAL_IP), port, 256).unwrap(); + (udp, rx_queue) + } + + /// Builds a valid Ethernet + IPv4 frame. + fn build_ipv4_frame( + src: Ipv4Address, + dst: Ipv4Address, + protocol: u8, + ttl: u8, + payload: &[u8], + ) -> Vec { + let total_ip_len = (IPV4_MIN_HEADER_LEN + payload.len()) as u16; + let eth_len = size_of::(); + let mut buf = vec![0u8; eth_len + IPV4_MIN_HEADER_LEN + payload.len()]; + + buf[12] = 0x08; + buf[13] = 0x00; + + let ip = &mut buf[14..]; + ip[0] = 0x45; + ip[2..4].copy_from_slice(&total_ip_len.to_be_bytes()); + ip[6] = 0x40; + ip[8] = ttl; + ip[9] = protocol; + let src_bytes: [u8; 4] = src.into(); + ip[12..16].copy_from_slice(&src_bytes); + let dst_bytes: [u8; 4] = dst.into(); + ip[16..20].copy_from_slice(&dst_bytes); + + let cksum = compute_ipv4_checksum(&ip[..20]); + ip[10] = cksum[0]; + ip[11] = cksum[1]; + + buf[eth_len + IPV4_MIN_HEADER_LEN..].copy_from_slice(payload); + buf + } + + /// Builds a fragmented IPv4 frame. + fn build_fragment_frame( + src: Ipv4Address, + dst: Ipv4Address, + protocol: u8, + frag_offset: u16, + more_fragments: bool, + payload: &[u8], + ) -> Vec { + let total_ip_len = (IPV4_MIN_HEADER_LEN + payload.len()) as u16; + let eth_len = size_of::(); + let mut buf = vec![0u8; eth_len + IPV4_MIN_HEADER_LEN + payload.len()]; + + buf[12] = 0x08; + buf[13] = 0x00; + + let ip = &mut buf[14..]; + ip[0] = 0x45; + ip[2..4].copy_from_slice(&total_ip_len.to_be_bytes()); + + let mf = if more_fragments { 0x20u8 } else { 0 }; + let hi = mf | ((frag_offset >> 8) as u8 & 0x1F); + let lo = frag_offset as u8; + ip[6] = hi; + ip[7] = lo; + + ip[8] = 64; + ip[9] = protocol; + let src_bytes: [u8; 4] = src.into(); + ip[12..16].copy_from_slice(&src_bytes); + let dst_bytes: [u8; 4] = dst.into(); + ip[16..20].copy_from_slice(&dst_bytes); + + let cksum = compute_ipv4_checksum(&ip[..20]); + ip[10] = cksum[0]; + ip[11] = cksum[1]; + + buf[eth_len + IPV4_MIN_HEADER_LEN..].copy_from_slice(payload); + buf + } + + #[test] + fn frame_too_short_goes_to_rx() { + let mut data = [0u8; 30]; + let mut handler = new_handler(); + let mut udp = new_udp_handler(); + let mut free = BasicFrameBuffer::new(4); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let frame = Frame::new(0, &mut data, 30, false); + + let mut tcp = new_tcp_handler(); + let nh = new_neighbor_handler(); + handler.handle( + frame, + &mut udp, + &mut tcp, + &nh, + &PmtuCache::new(), + Instant::now(), + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(rx.num_frames(), 1); + assert_eq!(tx.num_frames(), 0); + } + + #[test] + fn wrong_version_goes_to_rx() { + let mut data = build_ipv4_frame(REMOTE_IP, LOCAL_IP, 17, 64, &[0; 8]); + data[14] = 0x65; // version 6 instead of 4 + let cksum = compute_ipv4_checksum(&data[14..34]); + data[24] = cksum[0]; + data[25] = cksum[1]; + + let mut handler = new_handler(); + let mut udp = new_udp_handler(); + let mut free = BasicFrameBuffer::new(4); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let len = data.len(); + let frame = Frame::new(0, &mut data, len, false); + + let mut tcp = new_tcp_handler(); + let nh = new_neighbor_handler(); + handler.handle( + frame, + &mut udp, + &mut tcp, + &nh, + &PmtuCache::new(), + Instant::now(), + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(rx.num_frames(), 1); + assert_eq!(tx.num_frames(), 0); + } + + #[test] + fn ihl_too_small_goes_to_rx() { + let mut data = build_ipv4_frame(REMOTE_IP, LOCAL_IP, 17, 64, &[0; 8]); + data[14] = 0x44; // IHL = 4 (< 5) + + let mut handler = new_handler(); + let mut udp = new_udp_handler(); + let mut free = BasicFrameBuffer::new(4); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let len = data.len(); + let frame = Frame::new(0, &mut data, len, false); + + let mut tcp = new_tcp_handler(); + let nh = new_neighbor_handler(); + handler.handle( + frame, + &mut udp, + &mut tcp, + &nh, + &PmtuCache::new(), + Instant::now(), + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(rx.num_frames(), 1); + assert_eq!(tx.num_frames(), 0); + } + + #[test] + fn bad_checksum_goes_to_rx() { + let mut data = build_ipv4_frame(REMOTE_IP, LOCAL_IP, 17, 64, &[0; 8]); + data[24] ^= 0xFF; // corrupt checksum + + let mut handler = new_handler(); + let mut udp = new_udp_handler(); + let mut free = BasicFrameBuffer::new(4); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let len = data.len(); + let frame = Frame::new(0, &mut data, len, false); + + let mut tcp = new_tcp_handler(); + let nh = new_neighbor_handler(); + handler.handle( + frame, + &mut udp, + &mut tcp, + &nh, + &PmtuCache::new(), + Instant::now(), + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(rx.num_frames(), 1); + assert_eq!(tx.num_frames(), 0); + } + + #[test] + fn total_length_shorter_than_header_goes_to_rx() { + let mut data = build_ipv4_frame(REMOTE_IP, LOCAL_IP, 17, 64, &[0; 8]); + // Set total_length to 10 (less than 20-byte header) + data[16] = 0x00; + data[17] = 0x0A; + // Recompute checksum with corrupted total_length + data[24] = 0; + data[25] = 0; + let cksum = compute_ipv4_checksum(&data[14..34]); + data[24] = cksum[0]; + data[25] = cksum[1]; + + let mut handler = new_handler(); + let mut udp = new_udp_handler(); + let mut free = BasicFrameBuffer::new(4); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let len = data.len(); + let frame = Frame::new(0, &mut data, len, false); + + let mut tcp = new_tcp_handler(); + let nh = new_neighbor_handler(); + handler.handle( + frame, + &mut udp, + &mut tcp, + &nh, + &PmtuCache::new(), + Instant::now(), + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(rx.num_frames(), 1); + assert_eq!(tx.num_frames(), 0); + } + + #[test] + fn udp_fragment_goes_to_udp_handler() { + let mut data = + build_fragment_frame(REMOTE_IP, LOCAL_IP, IpProtocols::Udp, 0, true, &[0; 8]); + + let mut handler = new_handler(); + let mut udp = new_udp_handler(); + let mut free = BasicFrameBuffer::new(4); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let len = data.len(); + let frame = Frame::new(0, &mut data, len, false); + + let mut tcp = new_tcp_handler(); + let nh = new_neighbor_handler(); + handler.handle( + frame, + &mut udp, + &mut tcp, + &nh, + &PmtuCache::new(), + Instant::now(), + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(rx.num_frames(), 0); + assert_eq!(tx.num_frames(), 0); + assert_eq!(udp.pending_reassembly(), 1); + } + + #[test] + fn non_udp_fragment_goes_to_rx() { + // TCP fragment — dispatched via tcp_handler.process_ipv4; the + // all-zeros payload will fail TCP header validation and the frame + // ends up in rx_return. + let mut data = + build_fragment_frame(REMOTE_IP, LOCAL_IP, IpProtocols::Tcp, 0, true, &[0; 20]); + + let mut handler = new_handler(); + let mut udp = new_udp_handler(); + let mut free = BasicFrameBuffer::new(4); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let len = data.len(); + let frame = Frame::new(0, &mut data, len, false); + + let mut tcp = new_tcp_handler(); + let nh = new_neighbor_handler(); + handler.handle( + frame, + &mut udp, + &mut tcp, + &nh, + &PmtuCache::new(), + Instant::now(), + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(rx.num_frames(), 1); + assert_eq!(tx.num_frames(), 0); + assert_eq!(udp.pending_reassembly(), 0); + } + + #[test] + fn icmp_echo_request_generates_reply() { + let eth_len = size_of::(); + let icmp_len = 8 + 8; + let ip_total = IPV4_MIN_HEADER_LEN + icmp_len; + let frame_len = eth_len + ip_total; + let mut data = vec![0u8; 256]; + + data[12] = 0x08; + data[13] = 0x00; + data[14] = 0x45; + data[16..18].copy_from_slice(&(ip_total as u16).to_be_bytes()); + data[20] = 0x40; + data[22] = 64; + data[23] = IpProtocols::Icmp; + let src_bytes: [u8; 4] = REMOTE_IP.into(); + data[26..30].copy_from_slice(&src_bytes); + let dst_bytes: [u8; 4] = LOCAL_IP.into(); + data[30..34].copy_from_slice(&dst_bytes); + let cksum = compute_ipv4_checksum(&data[14..34]); + data[24] = cksum[0]; + data[25] = cksum[1]; + + let icmp_off = eth_len + IPV4_MIN_HEADER_LEN; + data[icmp_off] = 8; // Echo Request + let cksum = compute_ipv4_checksum(&data[icmp_off..icmp_off + icmp_len]); + data[icmp_off + 2] = cksum[0]; + data[icmp_off + 3] = cksum[1]; + + let mut handler = new_handler(); + let mut udp = new_udp_handler(); + let mut free = BasicFrameBuffer::new(4); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let frame = Frame::new(0, &mut data, frame_len, false); + let mut tcp = new_tcp_handler(); + let nh = new_neighbor_handler(); + handler.handle( + frame, + &mut udp, + &mut tcp, + &nh, + &PmtuCache::new(), + Instant::now(), + &mut free, + &mut rx, + &mut tx, + ); + + assert_eq!(rx.num_frames(), 0); + assert_eq!(tx.num_frames(), 1); + } + + #[test] + fn valid_tcp_accepted() { + let mut data = build_ipv4_frame(REMOTE_IP, LOCAL_IP, IpProtocols::Tcp, 64, &[0; 20]); + + let mut handler = new_handler(); + let mut udp = new_udp_handler(); + let mut free = BasicFrameBuffer::new(4); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let len = data.len(); + let frame = Frame::new(0, &mut data, len, false); + + let mut tcp = new_tcp_handler(); + let nh = new_neighbor_handler(); + handler.handle( + frame, + &mut udp, + &mut tcp, + &nh, + &PmtuCache::new(), + Instant::now(), + &mut free, + &mut rx, + &mut tx, + ); + // TCP payload is all-zeros; no valid SYN so the frame ends up in + // rx_return (validation failure or no matching listener). + let total = rx.num_frames() + tx.num_frames(); + assert!( + total >= 1, + "frame must be consumed: rx={} tx={}", + rx.num_frames(), + tx.num_frames() + ); + } + + #[test] + fn valid_udp_routed_to_socket() { + let udp_bytes = build_udp_bytes_v4(&REMOTE_IP, &LOCAL_IP); + let mut data = build_ipv4_frame(REMOTE_IP, LOCAL_IP, IpProtocols::Udp, 64, &udp_bytes); + + let mut handler = new_handler(); + let (mut udp, rx_queue) = new_udp_handler_with_socket(0); + let mut free = BasicFrameBuffer::new(4); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let len = data.len(); + let frame = Frame::new(0, &mut data, len, false); + + let mut tcp = new_tcp_handler(); + let nh = new_neighbor_handler(); + handler.handle( + frame, + &mut udp, + &mut tcp, + &nh, + &PmtuCache::new(), + Instant::now(), + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(rx.num_frames(), 0); + assert_eq!(tx.num_frames(), 0); + assert_eq!(rx_queue.len(), 1); + } + + #[test] + fn unknown_protocol_sends_dest_unreachable() { + let raw = build_ipv4_frame(REMOTE_IP, LOCAL_IP, 255, 64, &[0; 8]); + let mut data = vec![0u8; 256]; + data[..raw.len()].copy_from_slice(&raw); + + let mut handler = new_handler(); + let mut udp = new_udp_handler(); + let mut free = BasicFrameBuffer::new(4); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let frame = Frame::new(0, &mut data, raw.len(), false); + + let mut tcp = new_tcp_handler(); + let nh = new_neighbor_handler(); + handler.handle( + frame, + &mut udp, + &mut tcp, + &nh, + &PmtuCache::new(), + Instant::now(), + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(rx.num_frames(), 0); + assert_eq!(tx.num_frames(), 1); + } +} diff --git a/src/net/handler/ipv6.rs b/src/net/handler/ipv6.rs new file mode 100644 index 0000000..770003e --- /dev/null +++ b/src/net/handler/ipv6.rs @@ -0,0 +1,960 @@ +use coarsetime::Instant; + +use crate::{ + net::{ + NeighborHandler, PmtuCache, + wire::{ + ethernet::EthernetFrame, + icmpv6::{Icmpv6Codes, Icmpv6Types}, + ip::{ + EXT_AH, EXT_DESTINATION, EXT_FRAGMENT, EXT_HOP_BY_HOP, EXT_ROUTING, + FRAGMENT_EXT_LEN, IPV6_HEADER_LEN, IPV6_MIN_FRAME_LEN, IpProtocols, Ipv6Header, + NO_NEXT_HEADER, + }, + }, + }, + xdp::frame::{Frame, FrameBuffer}, +}; + +use super::{icmpv6, tcp::TcpHandler, udp::UdpHandler}; + +/// Result of walking IPv6 extension headers. +pub(crate) enum NextHeaderResult { + /// Upper-layer protocol found. `protocol` is the protocol number, + /// `payload_offset` is the byte offset from the start of the frame + /// to the upper-layer payload, and `next_header_offset` is the byte + /// offset of the Next Header field that contained `protocol` (used + /// for ICMPv6 Parameter Problem pointer). + Protocol { + protocol: u8, + payload_offset: usize, + next_header_offset: usize, + }, + /// A Fragment extension header was found, indicating this packet + /// is fragmented. `offset` is the byte offset of the fragment + /// extension header from the start of the frame. + Fragment { offset: usize }, + /// The extension header chain is malformed (frame too short, etc.). + Malformed, + /// Reached `No Next Header` (protocol 59). The packet has no + /// upper-layer payload. + NoPayload, +} + +/// Walks the IPv6 extension header chain starting from `next_header` +/// at byte offset `offset` (from the start of the frame). +/// +/// Returns the final upper-layer protocol and its payload offset, or +/// an indication that the packet is fragmented or malformed. +/// +/// Supported extension headers (skipped transparently): +/// - Hop-by-Hop Options (0) +/// - Routing (43) +/// - Destination Options (60) +/// - Authentication Header (51) +/// +/// Terminal conditions: +/// - Fragment (44) -> [`NextHeaderResult::Fragment`] +/// - ESP (50) -> returned as a protocol (cannot see past encryption) +/// - No Next Header (59) -> [`NextHeaderResult::NoPayload`] +/// - Any other value -> returned as an upper-layer protocol +pub(crate) fn walk_extension_headers( + frame: &[u8], + mut next_header: u8, + mut offset: usize, + initial_nh_offset: usize, +) -> NextHeaderResult { + const MAX_EXT_HEADERS: usize = 16; + let mut nh_offset = initial_nh_offset; + + for _ in 0..MAX_EXT_HEADERS { + match next_header { + EXT_HOP_BY_HOP | EXT_ROUTING | EXT_DESTINATION => { + if offset + 2 > frame.len() { + return NextHeaderResult::Malformed; + } + nh_offset = offset; + let nh = frame[offset]; + let hdr_ext_len = frame[offset + 1] as usize; + let ext_len = (hdr_ext_len + 1) * 8; + if offset + ext_len > frame.len() { + return NextHeaderResult::Malformed; + } + next_header = nh; + offset += ext_len; + } + EXT_AH => { + if offset + 2 > frame.len() { + return NextHeaderResult::Malformed; + } + nh_offset = offset; + let nh = frame[offset]; + let payload_len = frame[offset + 1] as usize; + let ext_len = (payload_len + 2) * 4; + if offset + ext_len > frame.len() { + return NextHeaderResult::Malformed; + } + next_header = nh; + offset += ext_len; + } + EXT_FRAGMENT => { + if offset + FRAGMENT_EXT_LEN > frame.len() { + return NextHeaderResult::Malformed; + } + return NextHeaderResult::Fragment { offset }; + } + NO_NEXT_HEADER => { + return NextHeaderResult::NoPayload; + } + protocol => { + return NextHeaderResult::Protocol { + protocol, + payload_offset: offset, + next_header_offset: nh_offset, + }; + } + } + } + + NextHeaderResult::Malformed +} + +/// Layer-3 handler for incoming IPv6 frames. +/// +/// Validates the IPv6 header (version, length), walks extension headers, +/// and dispatches to protocol-specific handlers. +/// +/// The frame is always consumed and pushed to exactly one buffer: +/// +/// * `rx_return` -- validation failures, fragmented packets, or +/// received data for upper layers. +/// * `tx_return` -- responses generated by protocol handlers (e.g. +/// ICMPv6 echo reply, Destination Unreachable). +pub struct Ipv6Handler { + rx_offload: bool, + tx_offload: bool, + pub ipv6_tcp_fragments_dropped: u64, +} + +impl Ipv6Handler { + pub fn new(rx_offload: bool, tx_offload: bool) -> Self { + Self { + rx_offload, + tx_offload, + ipv6_tcp_fragments_dropped: 0, + } + } + + /// Processes an incoming IPv6 frame. + /// + /// Validates the header, walks extension headers, and dispatches to + /// the appropriate protocol handler. The frame is always consumed. + /// + /// NDP messages (ICMPv6 types 133-137) are dispatched to + /// `neighbor_handler` instead of the generic ICMPv6 handler. + pub fn handle<'umem>( + &mut self, + frame: Frame<'umem>, + neighbor_handler: &NeighborHandler, + udp_handler: &mut UdpHandler<'umem>, + tcp_handler: &mut TcpHandler, + pmtu: &PmtuCache, + now: Instant, + free_frames: &mut impl FrameBuffer<'umem>, + rx_return: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) { + if frame.len() < IPV6_MIN_FRAME_LEN { + rx_return.push(frame); + return; + } + + let ip = Ipv6Header::from_bytes(&frame); + + if ip.version() != 6 { + rx_return.push(frame); + return; + } + + let payload_length = ip.payload_length() as usize; + let eth_len = size_of::(); + + if frame.len() < eth_len + IPV6_HEADER_LEN + payload_length { + rx_return.push(frame); + return; + } + + let first_next_header = ip.next_header; + let ext_start = eth_len + IPV6_HEADER_LEN; + let ip_end = ext_start + payload_length; + let initial_nh_offset = eth_len + 6; // IPv6 Next Header field + + match walk_extension_headers( + &frame[..ip_end], + first_next_header, + ext_start, + initial_nh_offset, + ) { + NextHeaderResult::Protocol { + protocol, + payload_offset, + next_header_offset, + } => match protocol { + IpProtocols::IcmpV6 => { + let icmpv6_len = ip_end - payload_offset; + icmpv6::handle_icmpv6( + frame, + payload_offset, + icmpv6_len, + neighbor_handler, + pmtu, + now, + self.rx_offload, + self.tx_offload, + rx_return, + tx_return, + ); + } + IpProtocols::Udp => { + udp_handler.process_ipv6(frame, None, payload_offset, rx_return) + } + IpProtocols::Tcp => { + tcp_handler.process_ipv6( + frame, payload_offset, now, neighbor_handler, free_frames, rx_return, tx_return, + ); + } + _ => { + // RFC 4443 §3.4: send Parameter Problem (code 1) with + // pointer to the unrecognized Next Header field. + let pointer = (next_header_offset - eth_len) as u32; + icmpv6::send_icmpv6_error( + frame, + Icmpv6Types::ParameterProblem, + Icmpv6Codes::UnrecognizedNextHeader, + pointer.to_be_bytes(), + protocol, + payload_offset, + self.tx_offload, + rx_return, + tx_return, + ); + } + }, + NextHeaderResult::Fragment { offset } => { + // Read the fragment extension header's next_header field + // to determine the upper-layer protocol. Only dispatch + // UDP fragments to the UDP handler; return others. + let frag_next_header = frame[offset]; + if frag_next_header == IpProtocols::Udp { + udp_handler.process_ipv6(frame, Some(offset), 0, rx_return); + } else if frag_next_header == IpProtocols::Tcp { + // TCP does not yet support fragment reassembly; drop to rx_return. + self.ipv6_tcp_fragments_dropped += 1; + rx_return.push(frame); + } else { + rx_return.push(frame); + } + } + NextHeaderResult::Malformed | NextHeaderResult::NoPayload => { + rx_return.push(frame); + } + } + } +} + +#[cfg(test)] +mod tests { + use coarsetime::Duration; + + use crate::{ + net::checksum::{compute_icmpv6_checksum, compute_udp_checksum_v6}, + net::wire::{ + ip::{IpAddress, Ipv6Address}, + udp::UDP_HEADER_LEN, + }, + xdp::frame::BasicFrameBuffer, + }; + + use super::*; + + const REMOTE_IP: Ipv6Address = + Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]); + const LOCAL_IP: Ipv6Address = + Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + + fn new_handler() -> Ipv6Handler { + Ipv6Handler::new(false, false) + } + + fn new_neighbor_handler() -> NeighborHandler { + NeighborHandler::new("test0", Duration::from_secs(60)).unwrap() + } + + fn new_tcp_handler() -> TcpHandler { + TcpHandler::new(false, false) + } + + fn new_udp_handler<'umem>() -> UdpHandler<'umem> { + UdpHandler::new(256, false) + } + + /// Builds a minimal valid UDP segment (header only, no payload) with a + /// correct IPv6 checksum. Ports are zero, length is 8. + fn build_udp_bytes_v6(src: &Ipv6Address, dst: &Ipv6Address) -> Vec { + let mut buf = vec![0u8; UDP_HEADER_LEN]; // ports=0, len=0, cksum=0 + let udp_len = UDP_HEADER_LEN as u16; + buf[4..6].copy_from_slice(&udp_len.to_be_bytes()); + let cksum = compute_udp_checksum_v6(src, dst, &buf); + buf[6] = cksum[0]; + buf[7] = cksum[1]; + buf + } + + fn new_udp_handler_with_socket<'umem>( + port: u16, + ) -> ( + UdpHandler<'umem>, + crate::net::socket::LocalQueue>, + ) { + let mut udp = UdpHandler::new(256, false); + let rx_queue = udp.bind(IpAddress::V6(LOCAL_IP), port, 256).unwrap(); + (udp, rx_queue) + } + + /// Builds a valid Ethernet + IPv6 frame with no extension headers. + fn build_ipv6_frame( + src: Ipv6Address, + dst: Ipv6Address, + next_header: u8, + hop_limit: u8, + payload: &[u8], + ) -> Vec { + let eth_len = size_of::(); + let payload_len = payload.len() as u16; + let mut buf = vec![0u8; eth_len + IPV6_HEADER_LEN + payload.len()]; + + buf[12] = 0x86; + buf[13] = 0xDD; + + let ip = &mut buf[14..]; + ip[0] = 0x60; + ip[4..6].copy_from_slice(&payload_len.to_be_bytes()); + ip[6] = next_header; + ip[7] = hop_limit; + let src_bytes: [u8; 16] = src.into(); + ip[8..24].copy_from_slice(&src_bytes); + let dst_bytes: [u8; 16] = dst.into(); + ip[24..40].copy_from_slice(&dst_bytes); + + buf[eth_len + IPV6_HEADER_LEN..].copy_from_slice(payload); + buf + } + + /// Builds an IPv6 frame with a Hop-by-Hop extension header. + fn build_ipv6_with_ext_header( + src: Ipv6Address, + dst: Ipv6Address, + upper_protocol: u8, + hop_limit: u8, + payload: &[u8], + ) -> Vec { + let eth_len = size_of::(); + let ext_len = 8; + let payload_len = (ext_len + payload.len()) as u16; + let mut buf = vec![0u8; eth_len + IPV6_HEADER_LEN + ext_len + payload.len()]; + + buf[12] = 0x86; + buf[13] = 0xDD; + + let ip = &mut buf[14..]; + ip[0] = 0x60; + ip[4..6].copy_from_slice(&payload_len.to_be_bytes()); + ip[6] = EXT_HOP_BY_HOP; + ip[7] = hop_limit; + let src_bytes: [u8; 16] = src.into(); + ip[8..24].copy_from_slice(&src_bytes); + let dst_bytes: [u8; 16] = dst.into(); + ip[24..40].copy_from_slice(&dst_bytes); + + let ext_offset = eth_len + IPV6_HEADER_LEN; + buf[ext_offset] = upper_protocol; + buf[ext_offset + 1] = 0; + + buf[ext_offset + ext_len..].copy_from_slice(payload); + buf + } + + /// Builds an IPv6 frame with a Fragment extension header. + fn build_ipv6_with_fragment( + src: Ipv6Address, + dst: Ipv6Address, + upper_protocol: u8, + frag_offset: u16, + more_fragments: bool, + payload: &[u8], + ) -> Vec { + let eth_len = size_of::(); + let payload_len = (FRAGMENT_EXT_LEN + payload.len()) as u16; + let mut buf = vec![0u8; eth_len + IPV6_HEADER_LEN + FRAGMENT_EXT_LEN + payload.len()]; + + buf[12] = 0x86; + buf[13] = 0xDD; + + let ip = &mut buf[14..]; + ip[0] = 0x60; + ip[4..6].copy_from_slice(&payload_len.to_be_bytes()); + ip[6] = EXT_FRAGMENT; + ip[7] = 64; + let src_bytes: [u8; 16] = src.into(); + ip[8..24].copy_from_slice(&src_bytes); + let dst_bytes: [u8; 16] = dst.into(); + ip[24..40].copy_from_slice(&dst_bytes); + + let frag_offset_raw = eth_len + IPV6_HEADER_LEN; + buf[frag_offset_raw] = upper_protocol; + buf[frag_offset_raw + 1] = 0; + let fo_m = (frag_offset << 3) | (if more_fragments { 1 } else { 0 }); + buf[frag_offset_raw + 2..frag_offset_raw + 4].copy_from_slice(&fo_m.to_be_bytes()); + + buf[frag_offset_raw + FRAGMENT_EXT_LEN..].copy_from_slice(payload); + buf + } + + #[test] + fn walk_no_extensions() { + let buf = build_ipv6_frame(REMOTE_IP, LOCAL_IP, IpProtocols::Udp, 64, &[0; 8]); + let eth_len = size_of::(); + let start = eth_len + IPV6_HEADER_LEN; + let nh_offset = eth_len + 6; + + match walk_extension_headers(&buf, IpProtocols::Udp, start, nh_offset) { + NextHeaderResult::Protocol { + protocol, + payload_offset, + next_header_offset, + } => { + assert_eq!(protocol, IpProtocols::Udp); + assert_eq!(payload_offset, start); + assert_eq!(next_header_offset, nh_offset); + } + _ => panic!("expected Protocol"), + } + } + + #[test] + fn walk_hop_by_hop_extension() { + let buf = build_ipv6_with_ext_header(REMOTE_IP, LOCAL_IP, IpProtocols::Tcp, 64, &[0; 20]); + let eth_len = size_of::(); + let start = eth_len + IPV6_HEADER_LEN; + let nh_offset = eth_len + 6; + + match walk_extension_headers(&buf, EXT_HOP_BY_HOP, start, nh_offset) { + NextHeaderResult::Protocol { + protocol, + payload_offset, + next_header_offset, + } => { + assert_eq!(protocol, IpProtocols::Tcp); + assert_eq!(payload_offset, start + 8); + assert_eq!(next_header_offset, start); + } + _ => panic!("expected Protocol"), + } + } + + #[test] + fn walk_fragment_returns_fragment() { + let buf = build_ipv6_with_fragment(REMOTE_IP, LOCAL_IP, IpProtocols::Udp, 0, true, &[0; 8]); + let eth_len = size_of::(); + let start = eth_len + IPV6_HEADER_LEN; + + match walk_extension_headers(&buf, EXT_FRAGMENT, start, eth_len + 6) { + NextHeaderResult::Fragment { offset } => { + assert_eq!(offset, start); + } + _ => panic!("expected Fragment"), + } + } + + #[test] + fn walk_no_next_header() { + let buf = build_ipv6_frame(REMOTE_IP, LOCAL_IP, NO_NEXT_HEADER, 64, &[]); + let eth_len = size_of::(); + let start = eth_len + IPV6_HEADER_LEN; + + match walk_extension_headers(&buf, NO_NEXT_HEADER, start, eth_len + 6) { + NextHeaderResult::NoPayload => {} + _ => panic!("expected NoPayload"), + } + } + + #[test] + fn walk_truncated_extension_is_malformed() { + let eth_len = size_of::(); + let mut buf = vec![0u8; eth_len + IPV6_HEADER_LEN + 2]; + buf[12] = 0x86; + buf[13] = 0xDD; + buf[14] = 0x60; + buf[18..20].copy_from_slice(&2u16.to_be_bytes()); + buf[20] = EXT_HOP_BY_HOP; + buf[21] = 64; + + let ext_start = eth_len + IPV6_HEADER_LEN; + buf[ext_start] = IpProtocols::Udp; + buf[ext_start + 1] = 0; // Hdr Ext Len = 0 means 8 bytes, but only 2 available + + match walk_extension_headers(&buf, EXT_HOP_BY_HOP, ext_start, eth_len + 6) { + NextHeaderResult::Malformed => {} + _ => panic!("expected Malformed"), + } + } + + #[test] + fn walk_exceeds_max_extensions_is_malformed() { + // Build a chain of 17 Hop-by-Hop extension headers (exceeds MAX_EXT_HEADERS=16). + let eth_len = size_of::(); + let ext_count = 17; + let ext_size = 8; // each ext header is 8 bytes (Hdr Ext Len = 0) + let total_ext = ext_count * ext_size; + let mut buf = vec![0u8; eth_len + IPV6_HEADER_LEN + total_ext]; + + buf[12] = 0x86; + buf[13] = 0xDD; + buf[14] = 0x60; + buf[18..20].copy_from_slice(&(total_ext as u16).to_be_bytes()); + buf[20] = EXT_HOP_BY_HOP; + buf[21] = 64; + + let ext_start = eth_len + IPV6_HEADER_LEN; + for i in 0..ext_count { + let off = ext_start + i * ext_size; + // Each points to Hop-by-Hop (0) as next header, except the last + // which points to UDP (but we'll never reach it). + buf[off] = if i < ext_count - 1 { + EXT_HOP_BY_HOP + } else { + IpProtocols::Udp + }; + buf[off + 1] = 0; // Hdr Ext Len = 0 -> 8 bytes + } + + match walk_extension_headers(&buf, EXT_HOP_BY_HOP, ext_start, eth_len + 6) { + NextHeaderResult::Malformed => {} + _ => panic!("expected Malformed for excessive extension headers"), + } + } + + #[test] + fn frame_too_short_goes_to_rx() { + let mut data = [0u8; 50]; + + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut udp = new_udp_handler(); + let mut tcp = new_tcp_handler(); + let mut free = BasicFrameBuffer::new(4); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let frame = Frame::new(0, &mut data, 50, false); + + handler.handle( + frame, + &nh, + &mut udp, + &mut tcp, + &PmtuCache::new(), + Instant::now(), + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(rx.num_frames(), 1); + assert_eq!(tx.num_frames(), 0); + } + + #[test] + fn wrong_version_goes_to_rx() { + let mut data = build_ipv6_frame(REMOTE_IP, LOCAL_IP, IpProtocols::Udp, 64, &[0; 8]); + data[14] = (data[14] & 0x0F) | 0x40; // version 4 instead of 6 + + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut udp = new_udp_handler(); + let mut tcp = new_tcp_handler(); + let mut free = BasicFrameBuffer::new(4); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let len = data.len(); + let frame = Frame::new(0, &mut data, len, false); + + handler.handle( + frame, + &nh, + &mut udp, + &mut tcp, + &PmtuCache::new(), + Instant::now(), + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(rx.num_frames(), 1); + assert_eq!(tx.num_frames(), 0); + } + + #[test] + fn payload_shorter_than_claimed_goes_to_rx() { + // Build a frame with only 8 bytes payload but claim 100 in payload_length + let mut data = build_ipv6_frame(REMOTE_IP, LOCAL_IP, IpProtocols::Udp, 64, &[0; 8]); + data[18] = 0x00; + data[19] = 100; // claim 100 bytes but only 8 available + + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut udp = new_udp_handler(); + let mut tcp = new_tcp_handler(); + let mut free = BasicFrameBuffer::new(4); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let len = data.len(); + let frame = Frame::new(0, &mut data, len, false); + + handler.handle( + frame, + &nh, + &mut udp, + &mut tcp, + &PmtuCache::new(), + Instant::now(), + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(rx.num_frames(), 1); + assert_eq!(tx.num_frames(), 0); + } + + #[test] + fn fragment_goes_to_udp_handler() { + let mut data = + build_ipv6_with_fragment(REMOTE_IP, LOCAL_IP, IpProtocols::Udp, 0, true, &[0; 8]); + + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut udp = new_udp_handler(); + let mut tcp = new_tcp_handler(); + let mut free = BasicFrameBuffer::new(4); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let len = data.len(); + let frame = Frame::new(0, &mut data, len, false); + + handler.handle( + frame, + &nh, + &mut udp, + &mut tcp, + &PmtuCache::new(), + Instant::now(), + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(rx.num_frames(), 0); + assert_eq!(tx.num_frames(), 0); + assert_eq!(udp.pending_reassembly(), 1); + } + + #[test] + fn icmpv6_echo_request_generates_reply() { + let eth_len = size_of::(); + let icmpv6_len = 16; + let frame_len = eth_len + IPV6_HEADER_LEN + icmpv6_len; + let mut data = vec![0u8; 512]; + + data[12] = 0x86; + data[13] = 0xDD; + data[14] = 0x60; + data[18..20].copy_from_slice(&(icmpv6_len as u16).to_be_bytes()); + data[20] = IpProtocols::IcmpV6; + data[21] = 64; + let src_bytes: [u8; 16] = REMOTE_IP.into(); + data[22..38].copy_from_slice(&src_bytes); + let dst_bytes: [u8; 16] = LOCAL_IP.into(); + data[38..54].copy_from_slice(&dst_bytes); + + let icmp_off = eth_len + IPV6_HEADER_LEN; + data[icmp_off] = Icmpv6Types::EchoRequest; + let cksum = compute_icmpv6_checksum( + &REMOTE_IP, + &LOCAL_IP, + &data[icmp_off..icmp_off + icmpv6_len], + ); + data[icmp_off + 2] = cksum[0]; + data[icmp_off + 3] = cksum[1]; + + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut udp = new_udp_handler(); + let mut tcp = new_tcp_handler(); + let mut free = BasicFrameBuffer::new(4); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let frame = Frame::new(0, &mut data, frame_len, false); + handler.handle( + frame, + &nh, + &mut udp, + &mut tcp, + &PmtuCache::new(), + Instant::now(), + &mut free, + &mut rx, + &mut tx, + ); + + assert_eq!(rx.num_frames(), 0); + assert_eq!(tx.num_frames(), 1); + } + + #[test] + fn valid_tcp_accepted() { + let mut data = build_ipv6_frame(REMOTE_IP, LOCAL_IP, IpProtocols::Tcp, 64, &[0; 20]); + + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut udp = new_udp_handler(); + let mut tcp = new_tcp_handler(); + let mut free = BasicFrameBuffer::new(4); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let len = data.len(); + let frame = Frame::new(0, &mut data, len, false); + + handler.handle( + frame, + &nh, + &mut udp, + &mut tcp, + &PmtuCache::new(), + Instant::now(), + &mut free, + &mut rx, + &mut tx, + ); + // TCP payload is all zeros so data_offset=0 fails header validation, + // causing the frame to be dropped to rx_return. + assert_eq!(rx.num_frames(), 1); + assert_eq!(tx.num_frames(), 0); + } + + #[test] + fn valid_udp_routed_to_socket() { + let udp_bytes = build_udp_bytes_v6(&REMOTE_IP, &LOCAL_IP); + let mut data = build_ipv6_frame(REMOTE_IP, LOCAL_IP, IpProtocols::Udp, 64, &udp_bytes); + + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let (mut udp, rx_queue) = new_udp_handler_with_socket(0); + let mut tcp = new_tcp_handler(); + let mut free = BasicFrameBuffer::new(4); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let len = data.len(); + let frame = Frame::new(0, &mut data, len, false); + + handler.handle( + frame, + &nh, + &mut udp, + &mut tcp, + &PmtuCache::new(), + Instant::now(), + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(rx.num_frames(), 0); + assert_eq!(tx.num_frames(), 0); + assert_eq!(rx_queue.len(), 1); + } + + #[test] + fn unknown_protocol_sends_parameter_problem() { + let raw = build_ipv6_frame(REMOTE_IP, LOCAL_IP, 255, 64, &[0; 8]); + let mut data = vec![0u8; 2048]; + data[..raw.len()].copy_from_slice(&raw); + + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut udp = new_udp_handler(); + let mut tcp = new_tcp_handler(); + let mut free = BasicFrameBuffer::new(4); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let frame = Frame::new(0, &mut data, raw.len(), false); + + handler.handle( + frame, + &nh, + &mut udp, + &mut tcp, + &PmtuCache::new(), + Instant::now(), + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(rx.num_frames(), 0); + assert_eq!(tx.num_frames(), 1); + } + + #[test] + fn extension_header_then_udp_routed_to_socket() { + let udp_bytes = build_udp_bytes_v6(&REMOTE_IP, &LOCAL_IP); + let mut data = + build_ipv6_with_ext_header(REMOTE_IP, LOCAL_IP, IpProtocols::Udp, 64, &udp_bytes); + + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let (mut udp, rx_queue) = new_udp_handler_with_socket(0); + let mut tcp = new_tcp_handler(); + let mut free = BasicFrameBuffer::new(4); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let len = data.len(); + let frame = Frame::new(0, &mut data, len, false); + + handler.handle( + frame, + &nh, + &mut udp, + &mut tcp, + &PmtuCache::new(), + Instant::now(), + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(rx.num_frames(), 0); + assert_eq!(tx.num_frames(), 0); + assert_eq!(rx_queue.len(), 1); + } + + #[test] + fn no_next_header_goes_to_rx() { + let mut data = build_ipv6_frame(REMOTE_IP, LOCAL_IP, NO_NEXT_HEADER, 64, &[]); + + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut udp = new_udp_handler(); + let mut tcp = new_tcp_handler(); + let mut free = BasicFrameBuffer::new(4); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let len = data.len(); + let frame = Frame::new(0, &mut data, len, false); + + handler.handle( + frame, + &nh, + &mut udp, + &mut tcp, + &PmtuCache::new(), + Instant::now(), + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(rx.num_frames(), 1); + assert_eq!(tx.num_frames(), 0); + } + + #[test] + fn malformed_extension_goes_to_rx() { + // Build frame with Hop-by-Hop next header but only 2 bytes of ext header data + // (claims to need 8 bytes via Hdr Ext Len=0). + let eth_len = size_of::(); + let mut data = vec![0u8; eth_len + IPV6_HEADER_LEN + 2]; + data[12] = 0x86; + data[13] = 0xDD; + data[14] = 0x60; + data[18..20].copy_from_slice(&2u16.to_be_bytes()); + data[20] = EXT_HOP_BY_HOP; + data[21] = 64; + + let ext_start = eth_len + IPV6_HEADER_LEN; + data[ext_start] = IpProtocols::Udp; + data[ext_start + 1] = 0; + + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut udp = new_udp_handler(); + let mut tcp = new_tcp_handler(); + let mut free = BasicFrameBuffer::new(4); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let len = data.len(); + let frame = Frame::new(0, &mut data, len, false); + + handler.handle( + frame, + &nh, + &mut udp, + &mut tcp, + &PmtuCache::new(), + Instant::now(), + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(rx.num_frames(), 1); + assert_eq!(tx.num_frames(), 0); + } + + #[test] + fn fragmented_tcp_increments_drop_counter() { + let mut data = + build_ipv6_with_fragment(REMOTE_IP, LOCAL_IP, IpProtocols::Tcp, 0, true, &[0; 20]); + + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut udp = new_udp_handler(); + let mut tcp = new_tcp_handler(); + let mut free = BasicFrameBuffer::new(4); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + assert_eq!(handler.ipv6_tcp_fragments_dropped, 0); + + let len = data.len(); + let frame = Frame::new(0, &mut data, len, false); + + handler.handle( + frame, + &nh, + &mut udp, + &mut tcp, + &PmtuCache::new(), + Instant::now(), + &mut free, + &mut rx, + &mut tx, + ); + + assert_eq!(handler.ipv6_tcp_fragments_dropped, 1); + assert_eq!(rx.num_frames(), 1); + assert_eq!(tx.num_frames(), 0); + } +} diff --git a/src/net/handler/mod.rs b/src/net/handler/mod.rs new file mode 100644 index 0000000..a3543ab --- /dev/null +++ b/src/net/handler/mod.rs @@ -0,0 +1,7 @@ +pub mod ethernet; +pub mod icmpv4; +pub mod icmpv6; +pub mod ipv4; +pub mod ipv6; +pub mod tcp; +pub mod udp; diff --git a/src/net/handler/tcp/congestion.rs b/src/net/handler/tcp/congestion.rs new file mode 100644 index 0000000..64705d6 --- /dev/null +++ b/src/net/handler/tcp/congestion.rs @@ -0,0 +1,300 @@ +use coarsetime::Instant; + +/// CUBIC constants (RFC 9438 S5). +const CUBIC_C: f64 = 0.4; +const CUBIC_BETA: f64 = 0.7; + +pub struct CubicState { + pub cwnd: u32, + pub ssthresh: u32, + w_max: u32, + w_max_prev: u32, + epoch_start: Option, + k: f64, + origin_point: u32, + tcp_cwnd: u32, + ack_count: u32, + eff_mss: u16, + cwnd_before_rto: Option, + ssthresh_before_rto: Option, +} + +impl CubicState { + pub fn new(eff_mss: u16) -> Self { + Self { + cwnd: 10 * eff_mss as u32, + ssthresh: u32::MAX, + w_max: 0, + w_max_prev: 0, + epoch_start: None, + k: 0.0, + origin_point: 0, + tcp_cwnd: 0, + ack_count: 0, + eff_mss, + cwnd_before_rto: None, + ssthresh_before_rto: None, + } + } + + pub fn set_mss(&mut self, eff_mss: u16) { + self.eff_mss = eff_mss; + } + + /// Called on each new ACK (that advances snd_una). NOT called during recovery. + /// + /// `receiver_window` is the largest window advertised by the peer + /// (`max_snd_wnd`). `cwnd` is clamped to this value after growth to + /// prevent unbounded increase on lossless links. + pub fn on_ack(&mut self, bytes_acked: u32, now: Instant, rtt_ms: u64, receiver_window: u32) { + let mss = self.eff_mss as u32; + if self.cwnd < self.ssthresh { + // Slow start. + self.cwnd = self.cwnd.saturating_add(mss); + } else { + // Congestion avoidance — stub for now (Task 2 replaces this). + self.cubic_update(bytes_acked, now, rtt_ms); + } + // Cap cwnd at receiver window — growing beyond the peer's buffer is + // pointless since the effective send window is min(cwnd, snd_wnd). + if receiver_window > 0 { + self.cwnd = self.cwnd.min(receiver_window); + } + } + + /// Called on packet loss (3 dup ACKs / SACK recovery entry). + pub fn on_loss(&mut self) { + self.epoch_start = None; + let mss = self.eff_mss as u32; + + // Fast convergence (RFC 9438 S5.8). + if self.cwnd < self.w_max_prev { + self.w_max_prev = self.cwnd; + self.w_max = (self.cwnd as f64 * (1.0 + CUBIC_BETA) / 2.0) as u32; + } else { + self.w_max_prev = self.cwnd; + self.w_max = self.cwnd; + } + + self.ssthresh = (self.cwnd as f64 * CUBIC_BETA) as u32; + self.ssthresh = self.ssthresh.max(2 * mss); + self.cwnd = self.ssthresh; + } + + /// Called on ECN congestion signal. Same as loss per RFC 9438. + pub fn on_ecn(&mut self) { + self.on_loss(); + } + + /// Called on RTO expiry. + pub fn on_rto(&mut self) { + let mss = self.eff_mss as u32; + self.cwnd_before_rto = Some(self.cwnd); + self.ssthresh_before_rto = Some(self.ssthresh); + self.epoch_start = None; + + self.w_max_prev = self.w_max; + self.w_max = self.cwnd; + self.ssthresh = (self.cwnd as f64 * CUBIC_BETA) as u32; + self.ssthresh = self.ssthresh.max(2 * mss); + self.cwnd = mss; + } + + /// Restore cwnd/ssthresh after F-RTO determines RTO was spurious. + pub fn restore_after_spurious_rto(&mut self) { + if let (Some(cwnd), Some(ssthresh)) = (self.cwnd_before_rto, self.ssthresh_before_rto) { + self.cwnd = cwnd; + self.ssthresh = ssthresh; + self.cwnd_before_rto = None; + self.ssthresh_before_rto = None; + } + } + + /// CUBIC window function (RFC 9438 §5). + fn cubic_update(&mut self, _bytes_acked: u32, now: Instant, rtt_ms: u64) { + let mss = self.eff_mss as u32; + + // Initialize epoch on first ACK in congestion avoidance. + if self.epoch_start.is_none() { + self.epoch_start = Some(now); + if self.cwnd < self.w_max { + self.k = ((self.w_max - self.cwnd) as f64 / CUBIC_C).cbrt(); + self.origin_point = self.w_max; + } else { + self.k = 0.0; + self.origin_point = self.cwnd; + } + self.ack_count = 0; + self.tcp_cwnd = self.cwnd; + } + + let epoch_start = self.epoch_start.unwrap(); + let t = now.duration_since(epoch_start).as_millis() as f64 / 1000.0; + + // W_cubic(t) = C * (t - K)^3 + origin_point. + let t_minus_k = t - self.k; + let w_cubic = + (CUBIC_C * t_minus_k * t_minus_k * t_minus_k) as i64 + self.origin_point as i64; + let w_cubic = (w_cubic.max(mss as i64)) as u32; + + // TCP-friendly estimate. + if rtt_ms > 0 { + let rtt_sec = rtt_ms as f64 / 1000.0; + let acks_since_epoch = t / rtt_sec; + let reno_inc = + (3.0 * (1.0 - CUBIC_BETA) / (1.0 + CUBIC_BETA)) * acks_since_epoch; + self.tcp_cwnd = + ((self.origin_point as f64 * CUBIC_BETA) + reno_inc * mss as f64) as u32; + } + + let target = w_cubic.max(self.tcp_cwnd); + + if target > self.cwnd { + let delta = target - self.cwnd; + let inc = ((delta as u64 * mss as u64) / self.cwnd as u64) as u32; + self.cwnd = self.cwnd.saturating_add(inc.max(1)); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Large receiver window used in tests so the cap doesn't interfere + /// with the property being tested. + const TEST_RWND: u32 = u32::MAX; + + #[test] + fn slow_start_increases_cwnd_by_mss_per_ack() { + let mut cubic = CubicState::new(1460); + // IW = 10 * MSS = 14600, ssthresh = MAX => slow start. + let now = Instant::now(); + cubic.on_ack(1460, now, 100, TEST_RWND); + assert_eq!(cubic.cwnd, 14600 + 1460); + cubic.on_ack(1460, now, 100, TEST_RWND); + assert_eq!(cubic.cwnd, 14600 + 2 * 1460); + } + + #[test] + fn on_loss_sets_ssthresh_to_cwnd_times_beta() { + let mut cubic = CubicState::new(1460); + cubic.cwnd = 100_000; + cubic.on_loss(); + assert_eq!(cubic.ssthresh, 70_000); // 100_000 * 0.7 + assert_eq!(cubic.cwnd, 70_000); + assert_eq!(cubic.w_max, 100_000); + } + + #[test] + fn on_rto_resets_cwnd_to_one_mss() { + let mut cubic = CubicState::new(1460); + cubic.cwnd = 100_000; + cubic.ssthresh = 50_000; + cubic.on_rto(); + assert_eq!(cubic.cwnd, 1460); + assert_eq!(cubic.ssthresh, 70_000); // 100_000 * 0.7 + assert_eq!(cubic.cwnd_before_rto, Some(100_000)); + assert_eq!(cubic.ssthresh_before_rto, Some(50_000)); + } + + #[test] + fn beta_is_07_not_05() { + let mut cubic = CubicState::new(1460); + cubic.cwnd = 10_000; + cubic.on_loss(); + // Reno would be 5000, CUBIC should be 7000. + assert_eq!(cubic.ssthresh, 7000); + } + + #[test] + fn fast_convergence_reduces_w_max() { + let mut cubic = CubicState::new(1460); + // First loss at cwnd=100_000. + cubic.cwnd = 100_000; + cubic.on_loss(); + // w_max_prev = 100_000. + // Second loss at cwnd=80_000 (below w_max_prev). + cubic.cwnd = 80_000; + cubic.on_loss(); + // Fast convergence: w_max = 80_000 * (1 + 0.7) / 2 = 68_000. + assert_eq!(cubic.w_max, 68_000); + } + + #[test] + fn congestion_avoidance_grows_past_w_max() { + let mut cubic = CubicState::new(1460); + // Simulate loss at cwnd=100_000 to set w_max. + cubic.cwnd = 100_000; + cubic.on_loss(); + // Now cwnd = 70_000, ssthresh = 70_000, w_max = 100_000. + + let start = Instant::now(); + let rtt_ms = 50; + // Simulate ~400 ACKs over several seconds. + for i in 0..400 { + let elapsed_ms = (i as u64) * rtt_ms; + let now = start + coarsetime::Duration::from_millis(elapsed_ms); + cubic.on_ack(1460, now, rtt_ms, TEST_RWND); + } + // After enough time, cwnd should exceed w_max. + assert!( + cubic.cwnd > 100_000, + "cwnd {} should exceed w_max 100_000", + cubic.cwnd + ); + } + + #[test] + fn tcp_friendliness_cwnd_at_least_reno() { + let mut cubic = CubicState::new(1460); + // Loss at 50_000. + cubic.cwnd = 50_000; + cubic.on_loss(); + // cwnd = 35_000, w_max = 50_000. + + let start = Instant::now(); + let rtt_ms = 100; + let mut reno_cwnd = 35_000u32; + let mss = 1460u32; + for i in 0..200 { + let now = start + coarsetime::Duration::from_millis(i * rtt_ms); + cubic.on_ack(1460, now, rtt_ms, TEST_RWND); + // Reno: cwnd += MSS^2 / cwnd per ACK. + reno_cwnd += (mss * mss) / reno_cwnd; + } + assert!( + cubic.cwnd >= reno_cwnd - mss, + "CUBIC cwnd {} should be >= Reno cwnd {} (within 1 MSS)", + cubic.cwnd, + reno_cwnd + ); + } + + #[test] + fn cwnd_capped_at_receiver_window() { + let mut cubic = CubicState::new(1460); + // IW = 14600, ssthresh = MAX => slow start. + let now = Instant::now(); + let receiver_window = 20_000u32; + // Grow past receiver window. + for _ in 0..100 { + cubic.on_ack(1460, now, 100, receiver_window); + } + assert_eq!( + cubic.cwnd, receiver_window, + "cwnd should be capped at receiver window" + ); + } + + #[test] + fn cwnd_does_not_overflow_in_slow_start() { + let mut cubic = CubicState::new(1460); + // Manually set cwnd near u32::MAX to verify saturating_add. + cubic.cwnd = u32::MAX - 500; + let now = Instant::now(); + // Without saturating_add this would panic. + cubic.on_ack(1460, now, 100, u32::MAX); + assert_eq!(cubic.cwnd, u32::MAX); + } +} diff --git a/src/net/handler/tcp/connection.rs b/src/net/handler/tcp/connection.rs new file mode 100644 index 0000000..76a291b --- /dev/null +++ b/src/net/handler/tcp/connection.rs @@ -0,0 +1,204 @@ +use std::collections::BTreeMap; + +use coarsetime::Instant; + +use crate::net::handler::udp::BindError; +use crate::net::socket::LocalQueue; +use crate::net::wire::ethernet::MacAddress; +use crate::net::wire::ip::IpAddress; +use crate::xdp::frame::FrameBuffer; + +use super::TcpHandler; +use super::handler::INITIAL_RTO_MS; +use super::congestion::CubicState; +use super::recovery::{FRtoState, PrrState, SackRecovery}; +use super::ring_buffer::RingBuffer; +use super::segment::SegmentBuilder; +use super::state::TcpState; +use super::tcb::{ + ConnectionId, DEFAULT_DELAYED_ACK_MS, DEFAULT_RCV_MSS, DEFAULT_RCV_WND, DEFAULT_RCV_WSCALE, + Tcb, TcpConfig, TcpEvent, +}; + +impl TcpHandler { + // --- Active open --- + + /// Initiate an active open (connect). + pub fn connect<'umem>( + &mut self, + local_addr: IpAddress, + local_port: u16, + remote_addr: IpAddress, + remote_port: u16, + src_mac: MacAddress, + dst_mac: MacAddress, + now: Instant, + free_frames: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) -> Result, BindError> { + self.connect_with_config( + local_addr, + local_port, + remote_addr, + remote_port, + src_mac, + dst_mac, + now, + TcpConfig::default(), + free_frames, + tx_return, + ) + } + + /// Initiate an active open (connect) with custom buffer configuration. + pub fn connect_with_config<'umem>( + &mut self, + local_addr: IpAddress, + local_port: u16, + remote_addr: IpAddress, + remote_port: u16, + src_mac: MacAddress, + dst_mac: MacAddress, + now: Instant, + config: TcpConfig, + free_frames: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) -> Result, BindError> { + let id = ConnectionId { + local_addr, + local_port, + remote_addr, + remote_port, + }; + + // Check for existing connection with same 4-tuple. + if self.connections.iter().any(|c| c.id == id) { + return Err(BindError::AddressInUse); + } + + let iss = self.isn_generator.generate(&id); + let event_queue = LocalQueue::new(16); + + let tcb = Tcb { + id, + state: TcpState::SynSent, + from_passive_open: false, + iss, + snd_una: iss, + snd_nxt: iss.wrapping_add(1), + snd_wnd: 0, + snd_wl1: 0, + snd_wl2: 0, + irs: 0, + rcv_nxt: 0, + rcv_wnd: DEFAULT_RCV_WND as u32, + snd_mss: DEFAULT_RCV_MSS, + rcv_mss: DEFAULT_RCV_MSS, + eff_snd_mss: DEFAULT_RCV_MSS, + snd_wscale: 0, + rcv_wscale: DEFAULT_RCV_WSCALE, + wscale_enabled: false, + retransmit_deadline: Some( + now + coarsetime::Duration::from_millis(INITIAL_RTO_MS), + ), + rto_backoff: 0, + event_queue: event_queue.clone(), + send_buffer: RingBuffer::new(config.send_buffer_size), + recv_buffer: RingBuffer::new(config.recv_buffer_size), + ooo_ranges: BTreeMap::new(), + cubic: CubicState::new(DEFAULT_RCV_MSS), + recovery: SackRecovery::new(), + prr: PrrState::new(), + frto: FRtoState::new(), + srtt: None, + rttvar: 0, + rto: 1000, + last_send_time: None, + pending_fin: false, + fin_seq: None, + time_wait_deadline: None, + time_wait_duration: config.time_wait_duration_ms, + ack_pending: false, + delayed_ack_deadline: None, + ack_delay_count: 0, + delayed_ack_ms: DEFAULT_DELAYED_ACK_MS, + nagle_enabled: !config.tcp_no_delay, + keep_alive_enabled: config.keep_alive, + keep_alive_idle_ms: config.keep_alive_idle_ms, + keep_alive_interval_ms: config.keep_alive_interval_ms, + keep_alive_count: config.keep_alive_count, + last_activity: now, + keep_alive_probes_sent: 0, + linger: config.linger, + linger_deadline: None, + ts_enabled: config.timestamps, + ts_recent: 0, + ts_recent_age: now, + ts_offset: now, + sack_enabled: config.sack, + sack_scoreboard: BTreeMap::new(), + ecn_enabled: config.ecn, + ecn_ce_received: false, + ecn_cwr_sent: false, + persist_deadline: None, + persist_backoff: 0, + max_snd_wnd: 0, + last_advertised_right_edge: 0, + }; + + // Send SYN. + let ts_opt = if config.timestamps { + Some((0u32, 0u32)) + } else { + None + }; + SegmentBuilder::build_syn( + local_addr, + remote_addr, + local_port, + remote_port, + iss, + DEFAULT_RCV_WND, + DEFAULT_RCV_MSS, + DEFAULT_RCV_WSCALE, + ts_opt, + config.sack, + config.ecn, + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); + + self.connections.push(tcb); + Ok(event_queue) + } + + /// Initiate a graceful close for a connection. + pub fn initiate_close(&mut self, id: &ConnectionId) { + if let Some(tcb) = self.connections.iter_mut().find(|c| c.id == *id) { + if tcb.pending_fin + || (tcb.state != TcpState::Established && tcb.state != TcpState::CloseWait) + { + return; + } + + tcb.pending_fin = true; + match tcb.linger { + Some(0) => { + // Linger(0): set deadline to now — poll_send will send RST immediately. + tcb.linger_deadline = Some(Instant::recent()); + } + Some(ms) => { + // Linger(timeout): graceful close with deadline. + tcb.linger_deadline = + Some(Instant::now() + coarsetime::Duration::from_millis(ms)); + } + None => { + // Default: graceful close, no deadline. + } + } + } + } +} diff --git a/src/net/handler/tcp/handler.rs b/src/net/handler/tcp/handler.rs new file mode 100644 index 0000000..494bd3d --- /dev/null +++ b/src/net/handler/tcp/handler.rs @@ -0,0 +1,114 @@ +use crate::{ + net::wire::{ethernet::MacAddress, tcp::flags}, + xdp::frame::FrameBuffer, +}; + +use super::{ + isn::IsnGenerator, + listener, + segment::SegmentBuilder, + state::TcpState, + tcb::{ConnectionId, Tcb}, +}; + +/// Initial RTO for SYN retransmission (1 second in coarsetime ticks). +pub(super) const INITIAL_RTO_MS: u64 = 1000; + +/// R2 threshold for SYN retransmission (~3 minutes per MUST-23). +pub(super) const SYN_R2_THRESHOLD_MS: u64 = 180_000; + +/// TCP protocol handler. +/// +/// Manages the connection table, listener table, and dispatches +/// incoming TCP segments through the appropriate state machine. +pub struct TcpHandler { + pub(super) connections: Vec, + pub(super) listeners: Vec, + pub(super) isn_generator: IsnGenerator, + pub(super) rx_offload: bool, + pub(super) tx_offload: bool, +} + +impl TcpHandler { + pub fn new(rx_offload: bool, tx_offload: bool) -> Self { + Self { + connections: Vec::new(), + listeners: Vec::new(), + isn_generator: IsnGenerator::new(), + rx_offload, + tx_offload, + } + } + + /// Get a reference to the connection for a given ConnectionId. + pub fn get_connection(&self, id: &ConnectionId) -> Option<&Tcb> { + self.connections.iter().find(|c| c.id == *id) + } + + /// Get a mutable reference to the connection for a given ConnectionId. + pub fn get_connection_mut(&mut self, id: &ConnectionId) -> Option<&mut Tcb> { + self.connections.iter_mut().find(|c| c.id == *id) + } + + /// Find the index of a connection by ID. + #[inline] + pub fn find_connection_idx(&self, id: &ConnectionId) -> Option { + self.connections.iter().position(|c| c.id == *id) + } + + /// Get a mutable reference by cached index, validating the connection ID matches. + /// Returns the TCB and the (possibly updated) index, or None if not found. + /// Falls back to linear scan if the cached index is stale. + #[inline] + pub fn get_connection_by_idx_mut( + &mut self, + idx: usize, + id: &ConnectionId, + ) -> Option<(usize, &mut Tcb)> { + if let Some(tcb) = self.connections.get(idx) + && tcb.id == *id + { + // SAFETY: we just checked bounds above; re-borrow mutably. + return Some((idx, &mut self.connections[idx])); + } + // Index is stale — fall back to linear scan. + if let Some(new_idx) = self.connections.iter().position(|c| c.id == *id) { + Some((new_idx, &mut self.connections[new_idx])) + } else { + None + } + } + + /// Remove a connection by ConnectionId (used by TcpStream::close). + pub fn remove_connection<'umem>( + &mut self, + id: &ConnectionId, + src_mac: MacAddress, + dst_mac: MacAddress, + free_frames: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) { + if let Some(idx) = self.connections.iter().position(|c| c.id == *id) { + let tcb = &self.connections[idx]; + // Send RST for now (proper FIN sequence deferred). + if tcb.state.is_synchronized() || tcb.state == TcpState::SynReceived { + SegmentBuilder::build_rst( + id.local_addr, + id.remote_addr, + id.local_port, + id.remote_port, + 0, + 0, + flags::ACK, + 0, + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); + } + self.connections.remove(idx); + } + } +} diff --git a/src/net/handler/tcp/inbound.rs b/src/net/handler/tcp/inbound.rs new file mode 100644 index 0000000..1ccd08d --- /dev/null +++ b/src/net/handler/tcp/inbound.rs @@ -0,0 +1,2212 @@ +use coarsetime::Instant; + +use crate::{ + net::{ + NeighborHandler, + checksum::{verify_tcp_checksum, verify_tcp_checksum_v6}, + socket::LocalQueue, + wire::{ + ethernet::EthernetFrame, + ip::{IpAddress, Ipv4Header, Ipv6Header}, + tcp::{ + TCP_HEADER_LEN, TcpHeader, flags, parse_mss, parse_sack_permitted, parse_timestamp, + parse_window_scale, + }, + }, + }, + xdp::frame::{Frame, FrameBuffer}, +}; + +use std::collections::BTreeMap; + +use super::congestion::CubicState; +use super::handler::{INITIAL_RTO_MS, TcpHandler}; +use super::options::ParsedOptions; +use super::recovery::{FRtoAction, FRtoState, PrrState, SackRecovery}; +use super::ring_buffer::RingBuffer; +use super::segment::SegmentBuilder; +use super::state::TcpState; +use super::tcb::{ + ConnectionId, DEFAULT_RCV_MSS, DEFAULT_RCV_WND, DEFAULT_RCV_WSCALE, + TS_OPTION_LEN, Tcb, TcpEvent, +}; + +/// Check segment acceptability per RFC 9293 §3.10.7.4. +#[inline] +pub(super) fn is_segment_acceptable(seg_seq: u32, seg_len: u32, rcv_nxt: u32, rcv_wnd: u32) -> bool { + use crate::net::wire::tcp::{seq_le, seq_lt}; + + if seg_len == 0 { + if rcv_wnd == 0 { + seg_seq == rcv_nxt + } else { + seq_le(rcv_nxt, seg_seq) && seq_lt(seg_seq, rcv_nxt.wrapping_add(rcv_wnd)) + } + } else if rcv_wnd == 0 { + false + } else { + let seg_end = seg_seq.wrapping_add(seg_len - 1); + let wnd_end = rcv_nxt.wrapping_add(rcv_wnd); + (seq_le(rcv_nxt, seg_seq) && seq_lt(seg_seq, wnd_end)) + || (seq_le(rcv_nxt, seg_end) && seq_lt(seg_end, wnd_end)) + } +} + +impl TcpHandler { + // --- Segment processing --- + + /// Process an incoming IPv4 TCP segment. + pub fn process_ipv4<'umem>( + &mut self, + frame: Frame<'umem>, + now: Instant, + neighbor_handler: &NeighborHandler, + free_frames: &mut impl FrameBuffer<'umem>, + rx_return: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) { + let ip = Ipv4Header::from_bytes(&frame); + let src_addr = ip.src_addr; + let dst_addr = ip.dst_addr; + let tcp_offset = ip.payload_offset(); + + if frame.len() < tcp_offset + TCP_HEADER_LEN { + rx_return.push(frame); + return; + } + + let tcp = unsafe { TcpHeader::from_bytes_at(&frame, tcp_offset) }; + + // Validate data offset. + let data_offset = tcp.data_offset(); + if data_offset < 5 || frame.len() < tcp_offset + tcp.header_len() { + rx_return.push(frame); + return; + } + + // Checksum verification. + if !self.rx_offload { + let tcp_segment = &frame[tcp_offset..]; + if !verify_tcp_checksum(&src_addr, &dst_addr, tcp_segment) { + rx_return.push(frame); + return; + } + } + + let src_port = tcp.src_port(); + let dst_port = tcp.dst_port(); + let seg_seq = tcp.seq_num(); + let seg_ack = tcp.ack_num(); + let seg_flags = tcp.flags(); + let seg_wnd = tcp.window() as u32; + let header_len = tcp.header_len(); + + // Copy options to stack buffer before moving frame. + let mut opt_buf = [0u8; 40]; + let opt_len = if header_len > TCP_HEADER_LEN { + let len = header_len - TCP_HEADER_LEN; + opt_buf[..len] + .copy_from_slice(&frame[tcp_offset + TCP_HEADER_LEN..tcp_offset + header_len]); + len + } else { + 0 + }; + + let seg_data_len = frame.len() - tcp_offset - header_len; + let seg_len = Tcb::seg_len(seg_data_len, seg_flags); + + // IPv4 ToS byte is at ethernet header length + 1. + // ECN bits are the low 2 bits of the ToS byte. + let ecn_bits = frame[std::mem::size_of::() + 1] & 0x03; + + let incoming_src = IpAddress::V4(src_addr); + let incoming_dst = IpAddress::V4(dst_addr); + let src_mac = neighbor_handler.local_mac(); + // For responses, swap MACs from incoming frame. + let dst_mac = EthernetFrame::from_bytes(&frame).src_mac; + + self.process_segment( + frame, + now, + incoming_src, + incoming_dst, + src_port, + dst_port, + seg_seq, + seg_ack, + seg_flags, + seg_wnd, + seg_len, + &opt_buf[..opt_len], + tcp_offset, + header_len, + ecn_bits, + src_mac, + dst_mac, + free_frames, + rx_return, + tx_return, + ); + } + + /// Process an incoming IPv6 TCP segment. + pub fn process_ipv6<'umem>( + &mut self, + frame: Frame<'umem>, + tcp_offset: usize, + now: Instant, + neighbor_handler: &NeighborHandler, + free_frames: &mut impl FrameBuffer<'umem>, + rx_return: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) { + let ip = Ipv6Header::from_bytes(&frame); + let src_addr = ip.src_addr; + let dst_addr = ip.dst_addr; + + if frame.len() < tcp_offset + TCP_HEADER_LEN { + rx_return.push(frame); + return; + } + + let tcp = unsafe { TcpHeader::from_bytes_at(&frame, tcp_offset) }; + + let data_offset = tcp.data_offset(); + if data_offset < 5 || frame.len() < tcp_offset + tcp.header_len() { + rx_return.push(frame); + return; + } + + if !self.rx_offload { + let tcp_segment = &frame[tcp_offset..]; + if !verify_tcp_checksum_v6(&src_addr, &dst_addr, tcp_segment) { + rx_return.push(frame); + return; + } + } + + let src_port = tcp.src_port(); + let dst_port = tcp.dst_port(); + let seg_seq = tcp.seq_num(); + let seg_ack = tcp.ack_num(); + let seg_flags = tcp.flags(); + let seg_wnd = tcp.window() as u32; + let header_len = tcp.header_len(); + + // Copy options to stack buffer before moving frame. + let mut opt_buf = [0u8; 40]; + let opt_len = if header_len > TCP_HEADER_LEN { + let len = header_len - TCP_HEADER_LEN; + opt_buf[..len] + .copy_from_slice(&frame[tcp_offset + TCP_HEADER_LEN..tcp_offset + header_len]); + len + } else { + 0 + }; + + let seg_data_len = frame.len() - tcp_offset - header_len; + let seg_len = Tcb::seg_len(seg_data_len, seg_flags); + + // IPv6 TC ECN bits: (frame_data[ETH_LEN + 1] >> 4) & 0x03 + let ecn_bits = (frame[std::mem::size_of::() + 1] >> 4) & 0x03; + + let incoming_src = IpAddress::V6(src_addr); + let incoming_dst = IpAddress::V6(dst_addr); + let src_mac = neighbor_handler.local_mac(); + let dst_mac = EthernetFrame::from_bytes(&frame).src_mac; + + self.process_segment( + frame, + now, + incoming_src, + incoming_dst, + src_port, + dst_port, + seg_seq, + seg_ack, + seg_flags, + seg_wnd, + seg_len, + &opt_buf[..opt_len], + tcp_offset, + header_len, + ecn_bits, + src_mac, + dst_mac, + free_frames, + rx_return, + tx_return, + ); + } + + /// Unified segment processing for both IPv4 and IPv6. + #[inline] + fn process_segment<'umem>( + &mut self, + frame: Frame<'umem>, + now: Instant, + incoming_src: IpAddress, + incoming_dst: IpAddress, + src_port: u16, + dst_port: u16, + seg_seq: u32, + seg_ack: u32, + seg_flags: u8, + seg_wnd: u32, + seg_len: u32, + options: &[u8], + tcp_offset: usize, + tcp_header_len: usize, + ecn_bits: u8, + src_mac: crate::net::wire::ethernet::MacAddress, + dst_mac: crate::net::wire::ethernet::MacAddress, + free_frames: &mut impl FrameBuffer<'umem>, + rx_return: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) { + // Look up existing connection. + let conn_id = ConnectionId { + local_addr: incoming_dst, + local_port: dst_port, + remote_addr: incoming_src, + remote_port: src_port, + }; + + if let Some(idx) = self.connections.iter().position(|c| c.id == conn_id) { + let tsval = if self.connections[idx].ts_enabled { + now.duration_since(self.connections[idx].ts_offset).as_millis() as u32 + } else { + 0 + }; + let state = self.connections[idx].state; + match state { + TcpState::SynSent => { + self.process_syn_sent( + idx, + now, + tsval, + seg_seq, + seg_ack, + seg_flags, + seg_wnd, + options, + src_mac, + dst_mac, + free_frames, + tx_return, + ); + rx_return.push(frame); + } + TcpState::SynReceived => { + self.process_syn_received( + idx, + now, + tsval, + seg_seq, + seg_ack, + seg_flags, + seg_wnd, + seg_len, + src_mac, + dst_mac, + free_frames, + tx_return, + ); + rx_return.push(frame); + } + TcpState::Established => { + let payload_offset = tcp_offset + tcp_header_len; + let payload_len = frame.len().saturating_sub(payload_offset); + let opts = ParsedOptions::parse(options); + self.process_established( + idx, + frame, + now, + tsval, + seg_seq, + seg_ack, + seg_flags, + seg_wnd, + payload_offset, + payload_len, + &opts, + ecn_bits, + src_mac, + dst_mac, + free_frames, + rx_return, + tx_return, + ); + } + TcpState::FinWait1 + | TcpState::FinWait2 + | TcpState::CloseWait + | TcpState::Closing + | TcpState::LastAck + | TcpState::TimeWait => { + let payload_offset = tcp_offset + tcp_header_len; + let payload_len = frame.len().saturating_sub(payload_offset); + let opts = ParsedOptions::parse(options); + self.process_teardown( + idx, + frame, + now, + tsval, + seg_seq, + seg_ack, + seg_flags, + seg_wnd, + payload_offset, + payload_len, + &opts, + src_mac, + dst_mac, + free_frames, + rx_return, + tx_return, + ); + } + _ => { + rx_return.push(frame); + } + } + return; + } + + // No connection found — check listeners (LISTEN state, §16.2). + if let Some(listener_idx) = self.find_listener(incoming_dst, dst_port) { + self.process_listen( + listener_idx, + now, + incoming_src, + incoming_dst, + src_port, + dst_port, + seg_seq, + seg_ack, + seg_flags, + seg_wnd, + seg_len, + options, + src_mac, + dst_mac, + free_frames, + tx_return, + ); + rx_return.push(frame); + return; + } + + // CLOSED state — no connection, no listener. + // RST segments are silently dropped (no RST for RST). + if seg_flags & flags::RST != 0 { + rx_return.push(frame); + return; + } + + // Send RST per RFC §16.1. + SegmentBuilder::build_rst( + incoming_src, + incoming_dst, + src_port, + dst_port, + seg_seq, + seg_ack, + seg_flags, + seg_len, + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); + rx_return.push(frame); + } + + // --- LISTEN state processing (RFC §16.2) --- + + fn process_listen<'umem>( + &mut self, + listener_idx: usize, + now: Instant, + incoming_src: IpAddress, + incoming_dst: IpAddress, + src_port: u16, + dst_port: u16, + seg_seq: u32, + seg_ack: u32, + seg_flags: u8, + seg_wnd: u32, + seg_len: u32, + options: &[u8], + src_mac: crate::net::wire::ethernet::MacAddress, + dst_mac: crate::net::wire::ethernet::MacAddress, + free_frames: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) { + // Step 1: RST → ignore. + if seg_flags & flags::RST != 0 { + return; + } + + // Step 2: ACK (no SYN) → send RST. + if seg_flags & flags::ACK != 0 && seg_flags & flags::SYN == 0 { + SegmentBuilder::build_rst( + incoming_src, + incoming_dst, + src_port, + dst_port, + seg_seq, + seg_ack, + seg_flags, + seg_len, + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); + return; + } + + // Step 3: SYN → create new connection in SYN-RECEIVED. + if seg_flags & flags::SYN != 0 { + // Check backlog. + let listener = &self.listeners[listener_idx]; + if listener.syn_received_count >= listener.backlog { + return; // Drop excess SYNs. + } + + let id = ConnectionId { + local_addr: incoming_dst, + local_port: dst_port, + remote_addr: incoming_src, + remote_port: src_port, + }; + + let iss = self.isn_generator.generate(&id); + let peer_mss = parse_mss(options).unwrap_or(536); + let peer_wscale = parse_window_scale(options); + let peer_ts = parse_timestamp(options); + let peer_sack = parse_sack_permitted(options); + + let wscale_enabled = peer_wscale.is_some(); + let snd_wscale = peer_wscale.unwrap_or(0); + + // Negotiate timestamps, SACK, and ECN. + let ts_enabled = listener.timestamps && peer_ts.is_some(); + let sack_enabled = listener.sack && peer_sack; + let ecn_enabled = listener.ecn + && (seg_flags & (flags::ECE | flags::CWR) == (flags::ECE | flags::CWR)); + let peer_tsval = peer_ts.map(|(v, _)| v).unwrap_or(0); + + let send_buffer_size = listener.send_buffer_size; + let recv_buffer_size = listener.recv_buffer_size; + let time_wait_duration = listener.time_wait_duration; + + let event_queue = LocalQueue::new(16); + + let tcb = Tcb { + id, + state: TcpState::SynReceived, + from_passive_open: true, + iss, + snd_una: iss, + snd_nxt: iss.wrapping_add(1), + snd_wnd: seg_wnd, + snd_wl1: seg_seq, + snd_wl2: seg_ack, + irs: seg_seq, + rcv_nxt: seg_seq.wrapping_add(1), + rcv_wnd: DEFAULT_RCV_WND as u32, + snd_mss: peer_mss, + rcv_mss: DEFAULT_RCV_MSS, + eff_snd_mss: { + let base = peer_mss.min(DEFAULT_RCV_MSS); + if ts_enabled { base.saturating_sub(TS_OPTION_LEN) } else { base } + }, + snd_wscale, + rcv_wscale: if wscale_enabled { + DEFAULT_RCV_WSCALE + } else { + 0 + }, + wscale_enabled, + retransmit_deadline: Some(now + coarsetime::Duration::from_millis(INITIAL_RTO_MS)), + rto_backoff: 0, + event_queue, + send_buffer: RingBuffer::new(send_buffer_size), + recv_buffer: RingBuffer::new(recv_buffer_size), + ooo_ranges: BTreeMap::new(), + cubic: CubicState::new({ + let base = peer_mss.min(DEFAULT_RCV_MSS); + if ts_enabled { base.saturating_sub(TS_OPTION_LEN) } else { base } + }), + recovery: SackRecovery::new(), + prr: PrrState::new(), + frto: FRtoState::new(), + srtt: None, + rttvar: 0, + rto: 1000, + last_send_time: None, + pending_fin: false, + fin_seq: None, + time_wait_deadline: None, + time_wait_duration, + ack_pending: false, + delayed_ack_deadline: None, + ack_delay_count: 0, + delayed_ack_ms: listener.delayed_ack_ms, + nagle_enabled: !listener.tcp_no_delay, + keep_alive_enabled: listener.keep_alive, + keep_alive_idle_ms: listener.keep_alive_idle_ms, + keep_alive_interval_ms: listener.keep_alive_interval_ms, + keep_alive_count: listener.keep_alive_count, + last_activity: now, + keep_alive_probes_sent: 0, + linger: listener.linger, + linger_deadline: None, + ts_enabled, + ts_recent: if ts_enabled { peer_tsval } else { 0 }, + ts_recent_age: now, + ts_offset: now, + sack_enabled, + sack_scoreboard: BTreeMap::new(), + ecn_enabled, + ecn_ce_received: false, + ecn_cwr_sent: false, + persist_deadline: None, + persist_backoff: 0, + max_snd_wnd: 0, + last_advertised_right_edge: 0, + }; + + // Send SYN-ACK. + let wscale_opt = if wscale_enabled { + Some(DEFAULT_RCV_WSCALE) + } else { + None + }; + let ts_opt = if ts_enabled { + Some((0u32, peer_tsval)) + } else { + None + }; + SegmentBuilder::build_syn_ack( + incoming_dst, + incoming_src, + dst_port, + src_port, + iss, + seg_seq.wrapping_add(1), + DEFAULT_RCV_WND, + DEFAULT_RCV_MSS, + wscale_opt, + ts_opt, + sack_enabled, + ecn_enabled, + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); + + self.connections.push(tcb); + self.listeners[listener_idx].syn_received_count += 1; + } + + // Step 4: Other → drop (frame returned by caller). + } + + // --- SYN-RECEIVED state processing (RFC §16.4) --- + + fn process_syn_received<'umem>( + &mut self, + idx: usize, + _now: Instant, + tsval: u32, + seg_seq: u32, + seg_ack: u32, + seg_flags: u8, + seg_wnd: u32, + _seg_len: u32, + src_mac: crate::net::wire::ethernet::MacAddress, + dst_mac: crate::net::wire::ethernet::MacAddress, + free_frames: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) { + let tcb = &self.connections[idx]; + let id = tcb.id; + + // Step 1: Check sequence number acceptability. + // For SYN-RECEIVED with no data, we expect seg_seq == rcv_nxt. + // For simultaneous open, the peer's SYN-ACK has seg_seq == IRS (the + // SYN occupies that sequence number) while rcv_nxt == IRS+1, so we + // also accept seg_seq == rcv_nxt - 1 when the SYN flag is set. + let seq_ok = seg_seq == tcb.rcv_nxt + || (seg_flags & flags::SYN != 0 && seg_seq.wrapping_add(1) == tcb.rcv_nxt); + if !seq_ok { + // Out of window — if not RST, send challenge ACK. + if seg_flags & flags::RST == 0 { + let tcb = &self.connections[idx]; + let ts = tcb.ts_option(tsval); + SegmentBuilder::build_ack( + tcb.id.local_addr, + tcb.id.remote_addr, + tcb.id.local_port, + tcb.id.remote_port, + tcb.snd_nxt, + tcb.rcv_nxt, + DEFAULT_RCV_WND, + flags::ACK, + ts, + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); + } + return; + } + + // Step 2: Check RST. + if seg_flags & flags::RST != 0 { + let from_passive = tcb.from_passive_open; + if from_passive { + // Return to LISTEN — remove TCB. + self.decrement_syn_received(&id); + self.connections.remove(idx); + } else { + // Active open → signal refused. + self.connections[idx] + .event_queue + .push(TcpEvent::ConnectionRefused); + self.connections.remove(idx); + } + return; + } + + // Step 3: Check SYN (duplicate SYN in synchronized state). + // A bare SYN (without ACK) is a duplicate — send challenge ACK. + // A SYN-ACK is expected during simultaneous open — fall through to + // step 5 so the ACK is processed and we transition to ESTABLISHED. + if seg_flags & flags::SYN != 0 && seg_flags & flags::ACK == 0 { + // Send challenge ACK per RFC 5961. + let tcb = &self.connections[idx]; + let ts = tcb.ts_option(tsval); + SegmentBuilder::build_ack( + tcb.id.local_addr, + tcb.id.remote_addr, + tcb.id.local_port, + tcb.id.remote_port, + tcb.snd_nxt, + tcb.rcv_nxt, + DEFAULT_RCV_WND, + flags::ACK, + ts, + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); + return; + } + + // Step 5: Check ACK. + if seg_flags & flags::ACK != 0 { + let tcb = &self.connections[idx]; + let snd_una = tcb.snd_una; + let snd_nxt = tcb.snd_nxt; + + if crate::net::wire::tcp::seq_lt(snd_una, seg_ack) + && crate::net::wire::tcp::seq_le(seg_ack, snd_nxt) + { + // ACK is acceptable → transition to ESTABLISHED. + let tcb = &mut self.connections[idx]; + tcb.state = TcpState::Established; + tcb.snd_una = seg_ack; + tcb.snd_wnd = seg_wnd; + tcb.max_snd_wnd = tcb.max_snd_wnd.max(tcb.snd_wnd); + tcb.snd_wl1 = seg_seq; + tcb.snd_wl2 = seg_ack; + tcb.retransmit_deadline = None; + tcb.rto_backoff = 0; + + let from_passive = tcb.from_passive_open; + if from_passive { + // Push ConnectionId to listener's accept_queue. + self.push_to_accept_queue(&id); + self.decrement_syn_received(&id); + } else { + // Simultaneous open — notify the active opener. + self.connections[idx].event_queue.push(TcpEvent::Connected); + } + } else { + // Bad ACK → send RST. + SegmentBuilder::build_rst( + id.remote_addr, + id.local_addr, + id.remote_port, + id.local_port, + seg_seq, + seg_ack, + seg_flags, + 0, + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); + } + } + } + + // --- SYN-SENT state processing (RFC §16.3) --- + + fn process_syn_sent<'umem>( + &mut self, + idx: usize, + now: Instant, + tsval: u32, + seg_seq: u32, + seg_ack: u32, + seg_flags: u8, + seg_wnd: u32, + options: &[u8], + src_mac: crate::net::wire::ethernet::MacAddress, + dst_mac: crate::net::wire::ethernet::MacAddress, + free_frames: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) { + let tcb = &self.connections[idx]; + let iss = tcb.iss; + + // Step 1: Check ACK. + if seg_flags & flags::ACK != 0 + && (crate::net::wire::tcp::seq_le(seg_ack, iss) + || crate::net::wire::tcp::seq_lt(tcb.snd_nxt, seg_ack)) + { + // Unacceptable ACK. + if seg_flags & flags::RST == 0 { + // Send RST unless RST is set. + let id = tcb.id; + SegmentBuilder::build_rst( + id.remote_addr, + id.local_addr, + id.remote_port, + id.local_port, + seg_seq, + seg_ack, + seg_flags, + 0, + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); + } + return; + } + + // Step 2: Check RST. + if seg_flags & flags::RST != 0 { + if seg_flags & flags::ACK != 0 { + // ACK was acceptable (passed step 1) → connection refused. + self.connections[idx] + .event_queue + .push(TcpEvent::ConnectionRefused); + self.connections.remove(idx); + } + // RST without ACK → drop silently. + return; + } + + // Step 3: Check SYN. + if seg_flags & flags::SYN != 0 { + let tcb = &mut self.connections[idx]; + tcb.irs = seg_seq; + tcb.rcv_nxt = seg_seq.wrapping_add(1); + + // Parse peer options. + let peer_mss = parse_mss(options).unwrap_or(536); + let peer_wscale = parse_window_scale(options); + tcb.snd_mss = peer_mss; + + if let Some(ws) = peer_wscale { + tcb.snd_wscale = ws; + tcb.wscale_enabled = true; + } + + // Timestamp negotiation. + if tcb.ts_enabled { + if let Some((peer_tsval, _)) = parse_timestamp(options) { + tcb.ts_recent = peer_tsval; + tcb.ts_recent_age = now; + } else { + tcb.ts_enabled = false; // peer doesn't support + } + } + + // Set eff_snd_mss after timestamp negotiation so we know the option overhead. + let base_mss = peer_mss.min(tcb.rcv_mss); + tcb.eff_snd_mss = if tcb.ts_enabled { + base_mss.saturating_sub(TS_OPTION_LEN) + } else { + base_mss + }; + tcb.cubic.set_mss(tcb.eff_snd_mss); + // SACK negotiation. + if tcb.sack_enabled && !parse_sack_permitted(options) { + tcb.sack_enabled = false; + } + // ECN negotiation: confirm only if SYN-ACK has ECE set. + if tcb.ecn_enabled && seg_flags & flags::ECE == 0 { + tcb.ecn_enabled = false; + } + + if seg_flags & flags::ACK != 0 { + // Our SYN was ACKed. + tcb.snd_una = seg_ack; + } + + if crate::net::wire::tcp::seq_lt(tcb.iss, tcb.snd_una) { + // SND.UNA > ISS → ESTABLISHED. + let tcb = &mut self.connections[idx]; + tcb.state = TcpState::Established; + tcb.snd_wnd = seg_wnd; + tcb.max_snd_wnd = tcb.max_snd_wnd.max(tcb.snd_wnd); + tcb.snd_wl1 = seg_seq; + tcb.snd_wl2 = seg_ack; + tcb.retransmit_deadline = None; + tcb.rto_backoff = 0; + + // Send ACK. + let id = tcb.id; + let ts = tcb.ts_option(tsval); + SegmentBuilder::build_ack( + id.local_addr, + id.remote_addr, + id.local_port, + id.remote_port, + tcb.snd_nxt, + tcb.rcv_nxt, + DEFAULT_RCV_WND, + flags::ACK, + ts, + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); + + tcb.event_queue.push(TcpEvent::Connected); + } else { + // Simultaneous open → SYN-RECEIVED (MUST-10). + let tcb = &mut self.connections[idx]; + tcb.state = TcpState::SynReceived; + tcb.from_passive_open = false; + tcb.snd_wnd = seg_wnd; + tcb.max_snd_wnd = tcb.max_snd_wnd.max(tcb.snd_wnd); + tcb.snd_wl1 = seg_seq; + tcb.snd_wl2 = seg_ack; + + // Send SYN-ACK. + let id = tcb.id; + let wscale_opt = if tcb.wscale_enabled { + Some(tcb.rcv_wscale) + } else { + None + }; + let ts_opt = tcb.ts_option(tsval); + SegmentBuilder::build_syn_ack( + id.local_addr, + id.remote_addr, + id.local_port, + id.remote_port, + tcb.iss, + tcb.rcv_nxt, + DEFAULT_RCV_WND, + tcb.rcv_mss, + wscale_opt, + ts_opt, + tcb.sack_enabled, + tcb.ecn_enabled, + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); + + // Reset retransmit timer for SYN-ACK. + tcb.retransmit_deadline = + Some(now + coarsetime::Duration::from_millis(INITIAL_RTO_MS)); + tcb.rto_backoff = 0; + } + } + + // Step 4: Neither SYN nor RST → drop. + } + + // --- ESTABLISHED state processing: cold-path helpers --- + + /// Handle RST in established state (RFC 5961). + /// Returns `true` if the connection was removed (exact sequence match). + #[inline(never)] + fn handle_rst_established<'umem>( + &mut self, + idx: usize, + seg_seq: u32, + tsval: u32, + ack_flags: u8, + src_mac: crate::net::wire::ethernet::MacAddress, + dst_mac: crate::net::wire::ethernet::MacAddress, + free_frames: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) -> bool { + if seg_seq == self.connections[idx].rcv_nxt { + // Exact match: reset connection. + self.connections[idx].event_queue.push(TcpEvent::Reset); + let id = self.connections[idx].id; + self.decrement_syn_received(&id); + self.connections.remove(idx); + return true; + } + // In-window but not exact: send challenge ACK, drop segment. + let tcb = &self.connections[idx]; + let ts = tcb.ts_option(tsval); + SegmentBuilder::build_ack( + tcb.id.local_addr, + tcb.id.remote_addr, + tcb.id.local_port, + tcb.id.remote_port, + tcb.snd_nxt, + tcb.rcv_nxt, + tcb.advertised_window(), + ack_flags, + ts, + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); + false + } + + /// Handle SYN in established state — send challenge ACK (RFC 5961). + #[inline(never)] + fn handle_syn_established<'umem>( + &mut self, + idx: usize, + tsval: u32, + ack_flags: u8, + src_mac: crate::net::wire::ethernet::MacAddress, + dst_mac: crate::net::wire::ethernet::MacAddress, + free_frames: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) { + let tcb = &self.connections[idx]; + let ts = tcb.ts_option(tsval); + SegmentBuilder::build_ack( + tcb.id.local_addr, + tcb.id.remote_addr, + tcb.id.local_port, + tcb.id.remote_port, + tcb.snd_nxt, + tcb.rcv_nxt, + tcb.advertised_window(), + ack_flags, + ts, + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); + } + + /// Handle out-of-order data — store in OOO buffer and send duplicate ACK with SACK blocks. + #[inline(never)] + fn handle_ooo_data<'umem>( + &mut self, + idx: usize, + frame: &Frame<'umem>, + seg_seq: u32, + tsval: u32, + payload_offset: usize, + payload_len: usize, + ack_flags: u8, + src_mac: crate::net::wire::ethernet::MacAddress, + dst_mac: crate::net::wire::ethernet::MacAddress, + free_frames: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) { + let rcv_nxt = self.connections[idx].rcv_nxt; + let offset = seg_seq.wrapping_sub(rcv_nxt) as usize; + let payload = &frame[payload_offset..payload_offset + payload_len]; + let tcb = &mut self.connections[idx]; + tcb.recv_buffer.write_at(offset, payload); + tcb.ooo_ranges.insert(seg_seq, payload_len as u32); + + // Send duplicate ACK (with current rcv_nxt) and SACK blocks. + let ts = tcb.ts_option(tsval); + + let max_blocks = if tcb.ts_enabled { 3 } else { 4 }; + let mut sack_buf = [(0u32, 0u32); 4]; + let mut sack_count = 0usize; + if tcb.sack_enabled { + // Most recently received range first (per RFC 2018 §3). + sack_buf[0] = (seg_seq, seg_seq.wrapping_add(payload_len as u32)); + sack_count = 1; + for (&start, &len) in tcb.ooo_ranges.iter().rev() { + if sack_count >= max_blocks { + break; + } + let end = start.wrapping_add(len); + if start != seg_seq { + sack_buf[sack_count] = (start, end); + sack_count += 1; + } + } + } + let sack_blocks = &sack_buf[..sack_count]; + + SegmentBuilder::build_ack_with_sack( + tcb.id.local_addr, + tcb.id.remote_addr, + tcb.id.local_port, + tcb.id.remote_port, + tcb.snd_nxt, + tcb.rcv_nxt, + tcb.advertised_window(), + ack_flags, + ts, + sack_blocks, + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); + } + + // --- ESTABLISHED state processing --- + + fn process_established<'umem>( + &mut self, + idx: usize, + frame: Frame<'umem>, + now: Instant, + tsval: u32, + seg_seq: u32, + seg_ack: u32, + seg_flags: u8, + seg_wnd: u32, + payload_offset: usize, + payload_len: usize, + opts: &ParsedOptions, + ecn_bits: u8, + src_mac: crate::net::wire::ethernet::MacAddress, + dst_mac: crate::net::wire::ethernet::MacAddress, + free_frames: &mut impl FrameBuffer<'umem>, + rx_return: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) { + use crate::net::wire::tcp::{seq_le, seq_lt}; + + // ECN CE detection: if ECN is enabled and CE codepoint (0b11) is received, + // record it so we can signal ECE back to the sender. + if self.connections[idx].ecn_enabled && ecn_bits == 0x03 { + self.connections[idx].ecn_ce_received = true; + } + + // ECN CWR processing: peer acknowledges our ECE by sending CWR. + if self.connections[idx].ecn_enabled && seg_flags & flags::CWR != 0 { + self.connections[idx].ecn_ce_received = false; + } + + // Compute ACK flags: include ECE when we need to signal congestion back. + let ack_flags = if self.connections[idx].ecn_ce_received { + flags::ACK | flags::ECE + } else { + flags::ACK + }; + + // Fast-path: common case — in-order data + valid new ACK, no special flags. + { + let tcb = &self.connections[idx]; + let fast_path = (seg_flags & (flags::RST | flags::SYN | flags::FIN)) == 0 + && seg_flags & flags::ACK != 0 + && payload_len > 0 + && seg_seq == tcb.rcv_nxt + && seq_lt(tcb.snd_una, seg_ack) + && seq_le(seg_ack, tcb.snd_nxt) + && !tcb.recovery.in_recovery + && tcb.ooo_ranges.is_empty(); + + if fast_path { + // PAWS check — on failure, fall through to slow path. + if self.connections[idx].ts_enabled { + if let Some((ts_val, _)) = opts.timestamp { + let ts_diff = ts_val.wrapping_sub(self.connections[idx].ts_recent) as i32; + if ts_diff < 0 { + // Possible PAWS rejection — let slow path handle it. + } else { + // PAWS OK — proceed with fast path. + + // Segment acceptability (simplified: seg_seq == rcv_nxt, payload > 0). + if self.connections[idx].recv_buffer.free_space() == 0 { + // No window space — fall through to slow path. + } else { + // Update ts_recent. + let tcb = &mut self.connections[idx]; + tcb.ts_recent = ts_val; + tcb.ts_recent_age = now; + + // ACK advancement (we know snd_una < seg_ack <= snd_nxt). + let bytes_acked = seg_ack.wrapping_sub(tcb.snd_una) as usize; + tcb.snd_una = seg_ack; + tcb.send_buffer.advance(bytes_acked); + tcb.last_activity = now; + tcb.keep_alive_probes_sent = 0; + + // F-RTO check. + let mut frto_handled = false; + if tcb.frto.is_active() { + let action = tcb.frto.on_ack(seg_ack); + match action { + FRtoAction::SpuriousRto => { + tcb.cubic.restore_after_spurious_rto(); + frto_handled = true; + } + FRtoAction::SendNewData => { + frto_handled = true; + } + FRtoAction::GenuineLoss | FRtoAction::None => {} + } + } + + // Congestion control (fast path is never in recovery). + if !frto_handled { + let rtt_ms = tcb.srtt.unwrap_or(tcb.rto); + tcb.cubic + .on_ack(bytes_acked as u32, now, rtt_ms, tcb.max_snd_wnd); + } + tcb.recovery.dup_ack_count = 0; + + // RTT measurement via timestamps. + if let Some((_tsval, tsecr)) = opts.timestamp + && tsecr != 0 + { + let our_ts = tsval; + let rtt_ms = our_ts.wrapping_sub(tsecr) as u64; + match tcb.srtt { + None => { + tcb.srtt = Some(rtt_ms); + tcb.rttvar = rtt_ms / 2; + } + Some(srtt) => { + let diff = rtt_ms.abs_diff(srtt); + tcb.rttvar = (3 * tcb.rttvar + diff) / 4; + tcb.srtt = Some((7 * srtt + rtt_ms) / 8); + } + } + tcb.rto = + (tcb.srtt.unwrap() + 4 * tcb.rttvar).clamp(1000, 60_000); + } + + // Window update. + if seq_lt(tcb.snd_wl1, seg_seq) + || (tcb.snd_wl1 == seg_seq && seq_le(tcb.snd_wl2, seg_ack)) + { + tcb.snd_wnd = tcb.scale_incoming_window(seg_wnd); + tcb.max_snd_wnd = tcb.max_snd_wnd.max(tcb.snd_wnd); + tcb.snd_wl1 = seg_seq; + tcb.snd_wl2 = seg_ack; + } + + // Clear persist timer when window reopens. + if tcb.snd_wnd > 0 && tcb.persist_deadline.is_some() { + tcb.persist_deadline = None; + tcb.persist_backoff = 0; + } + + // SACK scoreboard. + if tcb.sack_enabled { + let (blocks, count) = opts.sack_blocks; + for (left, right) in blocks.iter().take(count).flatten() { + tcb.sack_scoreboard + .insert(*left, right.wrapping_sub(*left)); + } + let snd_una = tcb.snd_una; + tcb.sack_scoreboard.retain(|&start, _| { + !crate::net::wire::tcp::seq_lt(start, snd_una) + }); + } + + // ECN congestion response. + if tcb.ecn_enabled + && seg_flags & flags::ECE != 0 + && !tcb.ecn_cwr_sent + { + tcb.cubic.on_ecn(); + tcb.ecn_cwr_sent = true; + } + + // Data write (in-order, no OOO to drain). + let payload = + &frame[payload_offset..payload_offset + payload_len]; + let written = tcb.recv_buffer.write(payload); + tcb.rcv_nxt = tcb.rcv_nxt.wrapping_add(written as u32); + tcb.last_activity = now; + tcb.keep_alive_probes_sent = 0; + + // Delayed ACK — defer to poll_send for piggyback opportunity. + tcb.ack_delay_count += 1; + tcb.ack_pending = true; + if tcb.delayed_ack_deadline.is_none() { + tcb.delayed_ack_deadline = Some( + now + coarsetime::Duration::from_millis( + tcb.delayed_ack_ms, + ), + ); + } + + rx_return.push(frame); + return; + } + } + } + // ts_enabled but no timestamp in packet — fall through to slow path. + } else { + // Timestamps not enabled — fast path without PAWS. + + // Segment acceptability (simplified: seg_seq == rcv_nxt, payload > 0). + if self.connections[idx].recv_buffer.free_space() > 0 { + let tcb = &mut self.connections[idx]; + + // ACK advancement. + let bytes_acked = seg_ack.wrapping_sub(tcb.snd_una) as usize; + tcb.snd_una = seg_ack; + tcb.send_buffer.advance(bytes_acked); + tcb.last_activity = now; + tcb.keep_alive_probes_sent = 0; + + // F-RTO check. + let mut frto_handled = false; + if tcb.frto.is_active() { + let action = tcb.frto.on_ack(seg_ack); + match action { + FRtoAction::SpuriousRto => { + tcb.cubic.restore_after_spurious_rto(); + frto_handled = true; + } + FRtoAction::SendNewData => { + frto_handled = true; + } + FRtoAction::GenuineLoss | FRtoAction::None => {} + } + } + + // Congestion control. + if !frto_handled { + let rtt_ms = tcb.srtt.unwrap_or(tcb.rto); + tcb.cubic + .on_ack(bytes_acked as u32, now, rtt_ms, tcb.max_snd_wnd); + } + tcb.recovery.dup_ack_count = 0; + + // RTT measurement via last_send_time (fallback). + if let Some(send_time) = tcb.last_send_time { + let rtt_ms = now.duration_since(send_time).as_millis(); + match tcb.srtt { + None => { + tcb.srtt = Some(rtt_ms); + tcb.rttvar = rtt_ms / 2; + } + Some(srtt) => { + let diff = rtt_ms.abs_diff(srtt); + tcb.rttvar = (3 * tcb.rttvar + diff) / 4; + tcb.srtt = Some((7 * srtt + rtt_ms) / 8); + } + } + let srtt = tcb.srtt.unwrap(); + tcb.rto = (srtt + 4 * tcb.rttvar).clamp(1000, 60_000); + tcb.last_send_time = None; + } + + // Window update. + if seq_lt(tcb.snd_wl1, seg_seq) + || (tcb.snd_wl1 == seg_seq && seq_le(tcb.snd_wl2, seg_ack)) + { + tcb.snd_wnd = tcb.scale_incoming_window(seg_wnd); + tcb.max_snd_wnd = tcb.max_snd_wnd.max(tcb.snd_wnd); + tcb.snd_wl1 = seg_seq; + tcb.snd_wl2 = seg_ack; + } + + // Clear persist timer when window reopens. + if tcb.snd_wnd > 0 && tcb.persist_deadline.is_some() { + tcb.persist_deadline = None; + tcb.persist_backoff = 0; + } + + // SACK scoreboard. + if tcb.sack_enabled { + let (blocks, count) = opts.sack_blocks; + for (left, right) in blocks.iter().take(count).flatten() { + tcb.sack_scoreboard + .insert(*left, right.wrapping_sub(*left)); + } + let snd_una = tcb.snd_una; + tcb.sack_scoreboard.retain(|&start, _| { + !crate::net::wire::tcp::seq_lt(start, snd_una) + }); + } + + // ECN congestion response. + if tcb.ecn_enabled + && seg_flags & flags::ECE != 0 + && !tcb.ecn_cwr_sent + { + tcb.cubic.on_ecn(); + tcb.ecn_cwr_sent = true; + } + + // Data write (in-order, no OOO to drain). + let payload = &frame[payload_offset..payload_offset + payload_len]; + let written = tcb.recv_buffer.write(payload); + tcb.rcv_nxt = tcb.rcv_nxt.wrapping_add(written as u32); + tcb.last_activity = now; + tcb.keep_alive_probes_sent = 0; + + // Delayed ACK — defer to poll_send for piggyback opportunity. + tcb.ack_delay_count += 1; + tcb.ack_pending = true; + if tcb.delayed_ack_deadline.is_none() { + tcb.delayed_ack_deadline = Some( + now + coarsetime::Duration::from_millis(tcb.delayed_ack_ms), + ); + } + + rx_return.push(frame); + return; + } + // No window space — fall through to slow path. + } + } + } + // Fall through to existing slow path. + + // PAWS check (RFC 7323 §5). + if self.connections[idx].ts_enabled + && let Some((tsval, _)) = opts.timestamp + { + let tcb = &self.connections[idx]; + // Check if TSval is older than ts_recent. + // Use signed comparison for wraparound. + let ts_diff = tsval.wrapping_sub(tcb.ts_recent) as i32; + if ts_diff < 0 { + // Check staleness: if ts_recent is older than 24 days, accept anyway. + let staleness = now.duration_since(tcb.ts_recent_age).as_millis(); + if staleness < 24 * 24 * 60 * 60 * 1000 { + // Reject: send ACK and drop (unless RST, which is silently dropped). + if seg_flags & flags::RST != 0 { + rx_return.push(frame); + return; + } + let tcb = &self.connections[idx]; + let ts = tcb.ts_option(tsval); + SegmentBuilder::build_ack( + tcb.id.local_addr, + tcb.id.remote_addr, + tcb.id.local_port, + tcb.id.remote_port, + tcb.snd_nxt, + tcb.rcv_nxt, + tcb.advertised_window(), + ack_flags, + ts, + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); + rx_return.push(frame); + return; + } + } + } + + // Segment acceptability check (RFC 9293 §3.10.7.4). + { + let tcb = &self.connections[idx]; + let seg_len = Tcb::seg_len(payload_len, seg_flags); + let rcv_wnd = tcb.recv_buffer.free_space() as u32; + if !is_segment_acceptable(seg_seq, seg_len, tcb.rcv_nxt, rcv_wnd) { + // Out-of-window: send ACK (unless RST, which is silently dropped). + if seg_flags & flags::RST != 0 { + rx_return.push(frame); + return; + } + let ts = tcb.ts_option(tsval); + SegmentBuilder::build_ack( + tcb.id.local_addr, + tcb.id.remote_addr, + tcb.id.local_port, + tcb.id.remote_port, + tcb.snd_nxt, + tcb.rcv_nxt, + tcb.advertised_window(), + ack_flags, + ts, + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); + rx_return.push(frame); + return; + } + } + + // Step 2: RST check (RFC 5961). + if seg_flags & flags::RST != 0 { + self.handle_rst_established( + idx, seg_seq, tsval, ack_flags, src_mac, dst_mac, free_frames, tx_return, + ); + rx_return.push(frame); + return; + } + + // Step 4: SYN check (RFC 5961 — challenge ACK for SYN in synchronized state). + if seg_flags & flags::SYN != 0 { + self.handle_syn_established( + idx, tsval, ack_flags, src_mac, dst_mac, free_frames, tx_return, + ); + rx_return.push(frame); + return; + } + + // Step 5 preamble: if ACK bit is off, drop segment and return. + if seg_flags & flags::ACK == 0 { + rx_return.push(frame); + return; + } + + // Update ts_recent from incoming segment. + { + let tcb = &mut self.connections[idx]; + if tcb.ts_enabled + && let Some((tsval, _)) = opts.timestamp + { + tcb.ts_recent = tsval; + tcb.ts_recent_age = now; + } + } + + // Step 2: ACK processing. + if seg_flags & flags::ACK != 0 { + let tcb = &self.connections[idx]; + let snd_una = tcb.snd_una; + let snd_nxt = tcb.snd_nxt; + + if seq_lt(snd_una, seg_ack) && seq_le(seg_ack, snd_nxt) { + // Valid new ACK — advance snd_una and send buffer. + let bytes_acked = seg_ack.wrapping_sub(snd_una) as usize; + let tcb = &mut self.connections[idx]; + tcb.snd_una = seg_ack; + tcb.send_buffer.advance(bytes_acked); + + // Reset keep-alive timer on activity. + tcb.last_activity = now; + tcb.keep_alive_probes_sent = 0; + + // F-RTO check — must come before recovery/congestion control. + let mut frto_handled = false; + if tcb.frto.is_active() { + let action = tcb.frto.on_ack(seg_ack); + match action { + FRtoAction::SpuriousRto => { + tcb.cubic.restore_after_spurious_rto(); + frto_handled = true; + } + FRtoAction::GenuineLoss => { + // Keep reduced cwnd — proceed normally. + } + FRtoAction::SendNewData => { + // F-RTO Step1→Step2: prefer sending new data in poll_send. + // No cwnd changes here. + frto_handled = true; + } + FRtoAction::None => {} + } + } + + // Recovery exit check — must come before congestion control. + if tcb.recovery.in_recovery && tcb.recovery.on_ack(seg_ack) { + // Exited recovery — full ACK covers recovery_point. + tcb.prr.exit(); + + // Note: if still in recovery (partial ACK), we skip cubic.on_ack below + // because it's already gated by !tcb.recovery.in_recovery + } + + // Congestion control — only update outside recovery and F-RTO. + if !tcb.recovery.in_recovery && !frto_handled { + let rtt_ms = tcb.srtt.unwrap_or(tcb.rto); + tcb.cubic.on_ack(bytes_acked as u32, now, rtt_ms, tcb.max_snd_wnd); + } + + tcb.recovery.dup_ack_count = 0; + + // RTT measurement. + if tcb.ts_enabled { + // RTTM via timestamps (RFC 7323). + if let Some((_tsval, tsecr)) = opts.timestamp + && tsecr != 0 + { + let our_ts = tsval; + let rtt_ms = our_ts.wrapping_sub(tsecr) as u64; + match tcb.srtt { + None => { + tcb.srtt = Some(rtt_ms); + tcb.rttvar = rtt_ms / 2; + } + Some(srtt) => { + let diff = rtt_ms.abs_diff(srtt); + tcb.rttvar = (3 * tcb.rttvar + diff) / 4; + tcb.srtt = Some((7 * srtt + rtt_ms) / 8); + } + } + tcb.rto = (tcb.srtt.unwrap() + 4 * tcb.rttvar).clamp(1000, 60_000); + } + } else if let Some(send_time) = tcb.last_send_time { + // Fallback: RTT from last_send_time (RFC 6298). + let rtt_ms = now.duration_since(send_time).as_millis(); + + match tcb.srtt { + None => { + tcb.srtt = Some(rtt_ms); + tcb.rttvar = rtt_ms / 2; + } + Some(srtt) => { + let diff = rtt_ms.abs_diff(srtt); + tcb.rttvar = (3 * tcb.rttvar + diff) / 4; + tcb.srtt = Some((7 * srtt + rtt_ms) / 8); + } + } + let srtt = tcb.srtt.unwrap(); + tcb.rto = (srtt + 4 * tcb.rttvar).clamp(1000, 60_000); + tcb.last_send_time = None; // consumed + } + + // Update send window (RFC 9293 §3.10.7.4 Step 5). + if seq_lt(tcb.snd_wl1, seg_seq) + || (tcb.snd_wl1 == seg_seq && seq_le(tcb.snd_wl2, seg_ack)) + { + tcb.snd_wnd = tcb.scale_incoming_window(seg_wnd); + tcb.max_snd_wnd = tcb.max_snd_wnd.max(tcb.snd_wnd); + tcb.snd_wl1 = seg_seq; + tcb.snd_wl2 = seg_ack; + } + + // C. Clear persist timer when window reopens. + if tcb.snd_wnd > 0 && tcb.persist_deadline.is_some() { + tcb.persist_deadline = None; + tcb.persist_backoff = 0; + } + + // Parse and merge SACK blocks into scoreboard. + if tcb.sack_enabled { + let (blocks, count) = opts.sack_blocks; + for (left, right) in blocks.iter().take(count).flatten() { + tcb.sack_scoreboard.insert(*left, right.wrapping_sub(*left)); + } + // Prune scoreboard entries below snd_una (already ACKed cumulatively). + let snd_una = tcb.snd_una; + tcb.sack_scoreboard + .retain(|&start, _| !crate::net::wire::tcp::seq_lt(start, snd_una)); + } + + // ECN congestion response: if peer signals ECE, halve cwnd and + // schedule CWR on the next data segment. + if tcb.ecn_enabled && seg_flags & flags::ECE != 0 && !tcb.ecn_cwr_sent { + tcb.cubic.on_ecn(); + tcb.ecn_cwr_sent = true; + } + } else if seq_lt(snd_nxt, seg_ack) { + // ACK for unsent data — send ACK and drop (RFC 9293 §3.10.7.4 Step 5). + let tcb = &self.connections[idx]; + let ts = tcb.ts_option(tsval); + SegmentBuilder::build_ack( + tcb.id.local_addr, + tcb.id.remote_addr, + tcb.id.local_port, + tcb.id.remote_port, + tcb.snd_nxt, + tcb.rcv_nxt, + tcb.advertised_window(), + ack_flags, + ts, + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); + rx_return.push(frame); + return; + } else if seg_ack == snd_una && payload_len == 0 { + // Duplicate ACK. + let tcb = &mut self.connections[idx]; + tcb.recovery.dup_ack_count += 1; + // Keep-alive probe responses arrive as duplicate ACKs — reset timer. + if tcb.keep_alive_enabled && tcb.keep_alive_probes_sent > 0 { + tcb.last_activity = now; + tcb.keep_alive_probes_sent = 0; + } + + // Window update may arrive as a duplicate ACK (RFC 9293 §3.10.7.4 Step 5). + let new_wnd = tcb.scale_incoming_window(seg_wnd); + if seq_lt(tcb.snd_wl1, seg_seq) + || (tcb.snd_wl1 == seg_seq && seq_le(tcb.snd_wl2, seg_ack)) + { + tcb.snd_wnd = new_wnd; + tcb.max_snd_wnd = tcb.max_snd_wnd.max(tcb.snd_wnd); + tcb.snd_wl1 = seg_seq; + tcb.snd_wl2 = seg_ack; + } + + // Clear persist timer when window reopens via duplicate ACK. + if tcb.snd_wnd > 0 && tcb.persist_deadline.is_some() { + tcb.persist_deadline = None; + tcb.persist_backoff = 0; + } + + // Parse SACK blocks on duplicate ACKs too. + if tcb.sack_enabled { + let (blocks, count) = opts.sack_blocks; + for (left, right) in blocks.iter().take(count).flatten() { + tcb.sack_scoreboard.insert(*left, right.wrapping_sub(*left)); + } + } + + // Enter SACK recovery on the 3rd duplicate ACK. + if tcb.recovery.dup_ack_count == 3 && !tcb.recovery.in_recovery { + let bytes_in_flight = tcb.snd_nxt.wrapping_sub(tcb.snd_una); + tcb.recovery.enter(tcb.snd_nxt); + tcb.prr.enter(bytes_in_flight); + tcb.cubic.on_loss(); + } + } + } + + // Step 3: Data processing. + if payload_len > 0 { + let tcb = &self.connections[idx]; + let rcv_nxt = tcb.rcv_nxt; + + if seg_seq == rcv_nxt { + // In-order data. + let payload = &frame[payload_offset..payload_offset + payload_len]; + let tcb = &mut self.connections[idx]; + let written = tcb.recv_buffer.write(payload); + tcb.rcv_nxt = rcv_nxt.wrapping_add(written as u32); + + // Reset keep-alive timer on received data. + tcb.last_activity = now; + tcb.keep_alive_probes_sent = 0; + + // Drain contiguous OOO ranges. + loop { + let current_nxt = self.connections[idx].rcv_nxt; + if let Some(&ooo_len) = self.connections[idx].ooo_ranges.get(¤t_nxt) { + self.connections[idx].ooo_ranges.remove(¤t_nxt); + self.connections[idx].recv_buffer.commit(ooo_len as usize); + self.connections[idx].rcv_nxt = current_nxt.wrapping_add(ooo_len); + } else { + break; + } + } + + // Defer ACK (delayed ACK) — defer to poll_send for piggyback opportunity. + let tcb = &mut self.connections[idx]; + tcb.ack_delay_count += 1; + tcb.ack_pending = true; + if tcb.delayed_ack_deadline.is_none() { + tcb.delayed_ack_deadline = + Some(now + coarsetime::Duration::from_millis(tcb.delayed_ack_ms)); + } + } else if seq_lt(rcv_nxt, seg_seq) { + // Out-of-order data. + self.handle_ooo_data( + idx, &frame, seg_seq, tsval, payload_offset, payload_len, ack_flags, src_mac, + dst_mac, free_frames, tx_return, + ); + } else { + // Duplicate data (seg_seq < rcv_nxt) — just ACK. + let tcb = &self.connections[idx]; + let ts = tcb.ts_option(tsval); + SegmentBuilder::build_ack( + tcb.id.local_addr, + tcb.id.remote_addr, + tcb.id.local_port, + tcb.id.remote_port, + tcb.snd_nxt, + tcb.rcv_nxt, + tcb.advertised_window(), + ack_flags, + ts, + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); + } + } + + // Step 4: Process FIN flag. + // Only process FIN when all preceding data has been received, i.e. the + // segment's data ends exactly at rcv_nxt (RFC 9293 §3.10.7.4 Step 8). + if seg_flags & flags::FIN != 0 + && seg_seq.wrapping_add(payload_len as u32) == self.connections[idx].rcv_nxt + { + let tcb = &mut self.connections[idx]; + tcb.rcv_nxt = tcb.rcv_nxt.wrapping_add(1); // FIN consumes one sequence number + tcb.state = TcpState::CloseWait; + tcb.event_queue.push(TcpEvent::RemoteClose); + + // Send ACK for FIN. + let id = tcb.id; + let snd_nxt = tcb.snd_nxt; + let new_rcv_nxt = tcb.rcv_nxt; + let window = tcb.advertised_window(); + let ts = tcb.ts_option(tsval); + SegmentBuilder::build_ack( + id.local_addr, + id.remote_addr, + id.local_port, + id.remote_port, + snd_nxt, + new_rcv_nxt, + window, + ack_flags, + ts, + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); + } + + // Always return incoming frame to rx_return. + rx_return.push(frame); + } + + // --- Connection teardown --- + + // --- Teardown state processing (FinWait1, FinWait2, CloseWait, Closing, LastAck, TimeWait) --- + + fn process_teardown<'umem>( + &mut self, + idx: usize, + frame: Frame<'umem>, + now: Instant, + tsval: u32, + seg_seq: u32, + seg_ack: u32, + seg_flags: u8, + seg_wnd: u32, + payload_offset: usize, + payload_len: usize, + opts: &ParsedOptions, + src_mac: crate::net::wire::ethernet::MacAddress, + dst_mac: crate::net::wire::ethernet::MacAddress, + free_frames: &mut impl FrameBuffer<'umem>, + rx_return: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) { + let state = self.connections[idx].state; + + // PAWS check (RFC 7323 §5). + if self.connections[idx].ts_enabled + && let Some((tsval, _)) = opts.timestamp + { + let tcb = &self.connections[idx]; + let ts_diff = tsval.wrapping_sub(tcb.ts_recent) as i32; + if ts_diff < 0 { + let staleness = now.duration_since(tcb.ts_recent_age).as_millis(); + if staleness < 24 * 24 * 60 * 60 * 1000 { + // Reject: silently drop RST, send ACK for others. + if seg_flags & flags::RST != 0 { + rx_return.push(frame); + return; + } + let tcb = &self.connections[idx]; + let ts = tcb.ts_option(tsval); + SegmentBuilder::build_ack( + tcb.id.local_addr, + tcb.id.remote_addr, + tcb.id.local_port, + tcb.id.remote_port, + tcb.snd_nxt, + tcb.rcv_nxt, + tcb.advertised_window(), + flags::ACK, + ts, + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); + rx_return.push(frame); + return; + } + } + } + + // Segment acceptability check (RFC 9293 §3.10.7.4). + { + let tcb = &self.connections[idx]; + let seg_len = Tcb::seg_len(payload_len, seg_flags); + let rcv_wnd = tcb.recv_buffer.free_space() as u32; + if !is_segment_acceptable(seg_seq, seg_len, tcb.rcv_nxt, rcv_wnd) { + // Out-of-window: silently drop RST, send ACK for others. + if seg_flags & flags::RST != 0 { + rx_return.push(frame); + return; + } + let ts = tcb.ts_option(tsval); + SegmentBuilder::build_ack( + tcb.id.local_addr, + tcb.id.remote_addr, + tcb.id.local_port, + tcb.id.remote_port, + tcb.snd_nxt, + tcb.rcv_nxt, + tcb.advertised_window(), + flags::ACK, + ts, + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); + rx_return.push(frame); + return; + } + } + + // RST check (RFC 5961) — after sequence validation. + if seg_flags & flags::RST != 0 { + if state == TcpState::TimeWait { + // Ignore RST in TIME-WAIT (prevents RST attacks). + rx_return.push(frame); + return; + } + if seg_seq == self.connections[idx].rcv_nxt { + // Exact match: reset connection. + self.connections[idx].event_queue.push(TcpEvent::Reset); + self.connections.remove(idx); + rx_return.push(frame); + return; + } + // In-window but not exact: send challenge ACK, drop segment. + let tcb = &self.connections[idx]; + let ts = tcb.ts_option(tsval); + SegmentBuilder::build_ack( + tcb.id.local_addr, + tcb.id.remote_addr, + tcb.id.local_port, + tcb.id.remote_port, + tcb.snd_nxt, + tcb.rcv_nxt, + tcb.advertised_window(), + flags::ACK, + ts, + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); + rx_return.push(frame); + return; + } + + // Step 4: SYN check (RFC 5961 — challenge ACK for SYN in synchronized state). + if seg_flags & flags::SYN != 0 { + let tcb = &self.connections[idx]; + let ts = tcb.ts_option(tsval); + SegmentBuilder::build_ack( + tcb.id.local_addr, + tcb.id.remote_addr, + tcb.id.local_port, + tcb.id.remote_port, + tcb.snd_nxt, + tcb.rcv_nxt, + tcb.advertised_window(), + flags::ACK, + ts, + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); + rx_return.push(frame); + return; + } + + // Step 5 preamble: if ACK bit is off, drop segment and return. + if seg_flags & flags::ACK == 0 { + rx_return.push(frame); + return; + } + + match state { + TcpState::FinWait1 => { + let tcb = &mut self.connections[idx]; + let fin_acked = if seg_flags & flags::ACK != 0 { + if let Some(fin_seq) = tcb.fin_seq { + crate::net::wire::tcp::seq_lt(fin_seq, seg_ack) + } else { + false + } + } else { + false + }; + + // Process ACK (advance snd_una if valid). + if seg_flags & flags::ACK != 0 { + let snd_una = tcb.snd_una; + let snd_nxt = tcb.snd_nxt; + if crate::net::wire::tcp::seq_lt(snd_una, seg_ack) + && crate::net::wire::tcp::seq_le(seg_ack, snd_nxt) + { + let bytes_acked = seg_ack.wrapping_sub(snd_una) as usize; + tcb.snd_una = seg_ack; + // Cap advance to actual buffer content (FIN consumes a sequence + // number but has no corresponding data in the send buffer). + let buf_advance = bytes_acked.min(tcb.send_buffer.available()); + tcb.send_buffer.advance(buf_advance); + tcb.snd_wnd = tcb.scale_incoming_window(seg_wnd); + tcb.max_snd_wnd = tcb.max_snd_wnd.max(tcb.snd_wnd); + } + } + + // Process data if present (remote may still be sending). + if payload_len > 0 && seg_seq == tcb.rcv_nxt { + let payload = &frame[payload_offset..payload_offset + payload_len]; + let written = tcb.recv_buffer.write(payload); + tcb.rcv_nxt = tcb.rcv_nxt.wrapping_add(written as u32); + } + + // Check for FIN from remote. + let remote_fin = seg_flags & flags::FIN != 0; + if remote_fin { + tcb.rcv_nxt = tcb.rcv_nxt.wrapping_add(1); + } + + // Determine new state. + let tcb = &mut self.connections[idx]; + if fin_acked && remote_fin { + // Both sides FINed and our FIN is ACKed → TimeWait. + tcb.state = TcpState::TimeWait; + tcb.time_wait_deadline = + Some(now + coarsetime::Duration::from_millis(tcb.time_wait_duration)); + tcb.retransmit_deadline = None; + } else if fin_acked { + // Our FIN ACKed but no remote FIN yet → FinWait2. + tcb.state = TcpState::FinWait2; + tcb.retransmit_deadline = None; + } else if remote_fin { + // Remote FINed but our FIN not yet ACKed → Closing. + tcb.state = TcpState::Closing; + } + + // Send ACK if FIN or data received. + if remote_fin || payload_len > 0 { + let tcb = &self.connections[idx]; + let id = tcb.id; + let snd_nxt = tcb.snd_nxt; + let rcv_nxt = tcb.rcv_nxt; + let ts = tcb.ts_option(tsval); + SegmentBuilder::build_ack( + id.local_addr, + id.remote_addr, + id.local_port, + id.remote_port, + snd_nxt, + rcv_nxt, + self.connections[idx].advertised_window(), + flags::ACK, + ts, + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); + } + + rx_return.push(frame); + } + + TcpState::FinWait2 => { + let tcb = &mut self.connections[idx]; + + // Process data if present (remote still sending). + if payload_len > 0 && seg_seq == tcb.rcv_nxt { + let payload = &frame[payload_offset..payload_offset + payload_len]; + let written = tcb.recv_buffer.write(payload); + tcb.rcv_nxt = tcb.rcv_nxt.wrapping_add(written as u32); + } + + // Check for FIN. + if seg_flags & flags::FIN != 0 { + tcb.rcv_nxt = tcb.rcv_nxt.wrapping_add(1); + tcb.state = TcpState::TimeWait; + tcb.time_wait_deadline = + Some(now + coarsetime::Duration::from_millis(tcb.time_wait_duration)); + } + + // Send ACK if FIN or data. + if seg_flags & flags::FIN != 0 || payload_len > 0 { + let id = tcb.id; + let snd_nxt = tcb.snd_nxt; + let rcv_nxt = tcb.rcv_nxt; + let ts = tcb.ts_option(tsval); + SegmentBuilder::build_ack( + id.local_addr, + id.remote_addr, + id.local_port, + id.remote_port, + snd_nxt, + rcv_nxt, + tcb.advertised_window(), + flags::ACK, + ts, + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); + } + + rx_return.push(frame); + } + + TcpState::Closing => { + let tcb = &mut self.connections[idx]; + // Waiting for ACK of our FIN. + if seg_flags & flags::ACK != 0 + && let Some(fin_seq) = tcb.fin_seq + && crate::net::wire::tcp::seq_lt(fin_seq, seg_ack) + { + tcb.snd_una = seg_ack; + tcb.state = TcpState::TimeWait; + tcb.time_wait_deadline = + Some(now + coarsetime::Duration::from_millis(tcb.time_wait_duration)); + tcb.retransmit_deadline = None; + } + rx_return.push(frame); + } + + TcpState::LastAck => { + // Waiting for ACK of our FIN. + if seg_flags & flags::ACK != 0 { + let tcb = &self.connections[idx]; + if let Some(fin_seq) = tcb.fin_seq + && crate::net::wire::tcp::seq_lt(fin_seq, seg_ack) + { + self.connections.remove(idx); + rx_return.push(frame); + return; + } + } + rx_return.push(frame); + } + + TcpState::TimeWait => { + let tcb = &mut self.connections[idx]; + // FIN retransmit → re-ACK and restart timer. + if seg_flags & flags::FIN != 0 { + let id = tcb.id; + let snd_nxt = tcb.snd_nxt; + let rcv_nxt = tcb.rcv_nxt; + let ts = tcb.ts_option(tsval); + SegmentBuilder::build_ack( + id.local_addr, + id.remote_addr, + id.local_port, + id.remote_port, + snd_nxt, + rcv_nxt, + tcb.advertised_window(), + flags::ACK, + ts, + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); + tcb.time_wait_deadline = + Some(now + coarsetime::Duration::from_millis(tcb.time_wait_duration)); + } + // Everything else (including RST) is ignored — RST handled above. + rx_return.push(frame); + } + + TcpState::CloseWait => { + // Process ACKs — local side can still send data. + if seg_flags & flags::ACK != 0 { + let tcb = &mut self.connections[idx]; + let snd_una = tcb.snd_una; + let snd_nxt = tcb.snd_nxt; + + if crate::net::wire::tcp::seq_lt(snd_una, seg_ack) + && crate::net::wire::tcp::seq_le(seg_ack, snd_nxt) + { + let bytes_acked = seg_ack.wrapping_sub(snd_una) as usize; + tcb.snd_una = seg_ack; + let buf_advance = bytes_acked.min(tcb.send_buffer.available()); + tcb.send_buffer.advance(buf_advance); + + // Window update with WL1/WL2 guard. + if crate::net::wire::tcp::seq_lt(tcb.snd_wl1, seg_seq) + || (tcb.snd_wl1 == seg_seq + && crate::net::wire::tcp::seq_le(tcb.snd_wl2, seg_ack)) + { + tcb.snd_wnd = tcb.scale_incoming_window(seg_wnd); + tcb.max_snd_wnd = tcb.max_snd_wnd.max(tcb.snd_wnd); + tcb.snd_wl1 = seg_seq; + tcb.snd_wl2 = seg_ack; + } + } + } + rx_return.push(frame); + } + + _ => { + rx_return.push(frame); + } + } + } +} diff --git a/src/net/handler/tcp/isn.rs b/src/net/handler/tcp/isn.rs new file mode 100644 index 0000000..e8314f5 --- /dev/null +++ b/src/net/handler/tcp/isn.rs @@ -0,0 +1,95 @@ +use std::hash::{DefaultHasher, Hash, Hasher}; + +use coarsetime::Instant; + +use super::tcb::ConnectionId; + +/// ISN (Initial Sequence Number) generator per RFC 9293. +/// +/// Combines a clock component (~4µs ticks) with a keyed hash +/// of the connection 4-tuple and a random secret. This makes ISNs +/// unpredictable to off-path attackers while still monotonically +/// increasing per-connection to avoid overlap with old segments. +pub struct IsnGenerator { + secret: [u64; 2], + epoch: Instant, +} + +fn random_u64() -> u64 { + let mut buf = [0u8; 8]; + unsafe { + libc::getrandom(buf.as_mut_ptr().cast(), 8, 0); + } + u64::from_ne_bytes(buf) +} + +impl IsnGenerator { + /// Creates a new ISN generator with a random secret. + pub fn new() -> Self { + Self { + secret: [random_u64(), random_u64()], + epoch: Instant::now(), + } + } + + /// Generates an ISN for the given connection 4-tuple. + /// + /// The result is `Hash(4-tuple, secret) + clock_ticks` where + /// clock_ticks increments at approximately 250,000 per second (~4µs). + #[inline] + pub fn generate(&self, id: &ConnectionId) -> u32 { + let mut hasher = DefaultHasher::new(); + self.secret[0].hash(&mut hasher); + self.secret[1].hash(&mut hasher); + id.local_addr.hash(&mut hasher); + id.local_port.hash(&mut hasher); + id.remote_addr.hash(&mut hasher); + id.remote_port.hash(&mut hasher); + let hash = hasher.finish() as u32; + + // Clock component: ~4µs ticks = 250,000 ticks/sec. + let elapsed_us = self.epoch.elapsed().as_micros(); + let clock_ticks = (elapsed_us / 4) as u32; + + hash.wrapping_add(clock_ticks) + } +} + +#[cfg(test)] +mod tests { + use crate::net::wire::ip::{IpAddress, Ipv4Address}; + + use super::*; + + #[test] + fn different_connections_get_different_isns() { + let generator = IsnGenerator::new(); + let id1 = ConnectionId { + local_addr: IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])), + local_port: 1234, + remote_addr: IpAddress::V4(Ipv4Address::new([10, 0, 0, 2])), + remote_port: 80, + }; + let id2 = ConnectionId { + local_addr: IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])), + local_port: 1235, + remote_addr: IpAddress::V4(Ipv4Address::new([10, 0, 0, 2])), + remote_port: 80, + }; + assert_ne!(generator.generate(&id1), generator.generate(&id2)); + } + + #[test] + fn same_connection_produces_consistent_isn() { + let generator = IsnGenerator::new(); + let id = ConnectionId { + local_addr: IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])), + local_port: 1234, + remote_addr: IpAddress::V4(Ipv4Address::new([10, 0, 0, 2])), + remote_port: 80, + }; + let isn1 = generator.generate(&id); + let isn2 = generator.generate(&id); + assert!(isn1.wrapping_sub(isn2) < 1000 || isn2.wrapping_sub(isn1) < 1000); + } +} diff --git a/src/net/handler/tcp/listener.rs b/src/net/handler/tcp/listener.rs new file mode 100644 index 0000000..e1017d1 --- /dev/null +++ b/src/net/handler/tcp/listener.rs @@ -0,0 +1,126 @@ +use crate::net::handler::udp::BindError; +use crate::net::socket::LocalQueue; +use crate::net::wire::ip::IpAddress; + +use super::TcpHandler; +use super::state::TcpState; +use super::tcb::{ConnectionId, TcpConfig}; + +/// Entry for a listening socket. +pub(crate) struct ListenEntry { + pub addr: IpAddress, + pub port: u16, + pub backlog: usize, + pub accept_queue: LocalQueue, + pub syn_received_count: usize, + pub send_buffer_size: usize, + pub recv_buffer_size: usize, + pub time_wait_duration: u64, + pub tcp_no_delay: bool, + pub delayed_ack_ms: u64, + pub keep_alive: bool, + pub keep_alive_idle_ms: u64, + pub keep_alive_interval_ms: u64, + pub keep_alive_count: u8, + pub linger: Option, + pub timestamps: bool, + pub sack: bool, + pub ecn: bool, +} + +impl TcpHandler { + /// Register a listening socket on (addr, port). + pub fn listen( + &mut self, + addr: IpAddress, + port: u16, + backlog: usize, + ) -> Result, BindError> { + let config = TcpConfig { + backlog, + ..TcpConfig::default() + }; + self.listen_with_config(addr, port, config) + } + + /// Register a listening socket with custom buffer configuration. + pub fn listen_with_config( + &mut self, + addr: IpAddress, + port: u16, + config: TcpConfig, + ) -> Result, BindError> { + // Check for duplicate listeners. + if self.listeners.iter().any(|l| { + l.port == port && (l.addr == addr || l.addr.is_unspecified() || addr.is_unspecified()) + }) { + return Err(BindError::AddressInUse); + } + let accept_queue = LocalQueue::new(config.backlog); + self.listeners.push(ListenEntry { + addr, + port, + backlog: config.backlog, + accept_queue: accept_queue.clone(), + syn_received_count: 0, + send_buffer_size: config.send_buffer_size, + recv_buffer_size: config.recv_buffer_size, + time_wait_duration: config.time_wait_duration_ms, + tcp_no_delay: config.tcp_no_delay, + delayed_ack_ms: config.delayed_ack_ms, + keep_alive: config.keep_alive, + keep_alive_idle_ms: config.keep_alive_idle_ms, + keep_alive_interval_ms: config.keep_alive_interval_ms, + keep_alive_count: config.keep_alive_count, + linger: config.linger, + timestamps: config.timestamps, + sack: config.sack, + ecn: config.ecn, + }); + Ok(accept_queue) + } + + /// Remove a listener on (addr, port) and clean up associated SYN-RECEIVED connections. + pub fn unlisten(&mut self, addr: IpAddress, port: u16) { + self.listeners + .retain(|l| !(l.port == port && l.addr == addr)); + // Remove any SYN-RECEIVED connections associated with this listener. + self.connections.retain(|c| { + !(c.state == TcpState::SynReceived + && c.from_passive_open + && c.id.local_port == port + && (addr.is_unspecified() || c.id.local_addr == addr)) + }); + } + + /// Find a matching listener for the given address and port. + pub(super) fn find_listener(&self, addr: IpAddress, port: u16) -> Option { + self.listeners + .iter() + .position(|l| l.port == port && (l.addr.is_unspecified() || l.addr == addr)) + } + + /// Push a ConnectionId to the matching listener's accept queue. + pub(super) fn push_to_accept_queue(&self, id: &ConnectionId) { + for listener in &self.listeners { + if listener.port == id.local_port + && (listener.addr.is_unspecified() || listener.addr == id.local_addr) + { + listener.accept_queue.push(*id); + return; + } + } + } + + /// Decrement syn_received_count on the matching listener. + pub(super) fn decrement_syn_received(&mut self, id: &ConnectionId) { + for listener in &mut self.listeners { + if listener.port == id.local_port + && (listener.addr.is_unspecified() || listener.addr == id.local_addr) + { + listener.syn_received_count = listener.syn_received_count.saturating_sub(1); + return; + } + } + } +} diff --git a/src/net/handler/tcp/mod.rs b/src/net/handler/tcp/mod.rs new file mode 100644 index 0000000..5cbb3dd --- /dev/null +++ b/src/net/handler/tcp/mod.rs @@ -0,0 +1,19 @@ +pub(crate) mod congestion; +mod connection; +mod handler; +mod inbound; +mod isn; +pub(crate) mod listener; +pub(crate) mod options; +pub(crate) mod recovery; +pub(crate) mod ring_buffer; +pub(crate) mod segment; +pub(crate) mod state; +pub(crate) mod tcb; +mod timers; +mod transmit; + +pub use handler::TcpHandler; + +#[cfg(test)] +mod tests; diff --git a/src/net/handler/tcp/options.rs b/src/net/handler/tcp/options.rs new file mode 100644 index 0000000..16a1724 --- /dev/null +++ b/src/net/handler/tcp/options.rs @@ -0,0 +1,19 @@ +use crate::net::wire::tcp::{parse_sack_blocks, parse_timestamp}; + +/// Pre-parsed TCP options. Computed once per segment, passed by reference. +#[derive(Debug, Clone, Copy)] +pub struct ParsedOptions { + pub timestamp: Option<(u32, u32)>, + pub sack_blocks: ([Option<(u32, u32)>; 4], usize), +} + +impl ParsedOptions { + /// Parse TCP options relevant to established-state processing. + #[inline] + pub fn parse(options: &[u8]) -> Self { + Self { + timestamp: parse_timestamp(options), + sack_blocks: parse_sack_blocks(options), + } + } +} diff --git a/src/net/handler/tcp/recovery.rs b/src/net/handler/tcp/recovery.rs new file mode 100644 index 0000000..d569e70 --- /dev/null +++ b/src/net/handler/tcp/recovery.rs @@ -0,0 +1,432 @@ +use std::collections::BTreeMap; + +use crate::net::wire::tcp::{seq_le, seq_lt}; + +const DUP_THRESH: u32 = 3; + +pub struct SackRecovery { + pub in_recovery: bool, + pub recovery_point: u32, + pub pipe: u32, + pub dup_ack_count: u8, +} + +impl SackRecovery { + pub fn new() -> Self { + Self { + in_recovery: false, + recovery_point: 0, + pipe: 0, + dup_ack_count: 0, + } + } + + /// Enter SACK recovery. `snd_nxt` is the recovery point. + pub fn enter(&mut self, snd_nxt: u32) { + self.in_recovery = true; + self.recovery_point = snd_nxt; + // dup_ack_count intentionally NOT reset here — it stays at 3+ + // during recovery and is reset on exit(). + } + + /// Exit recovery, reset state. + pub fn exit(&mut self) { + self.in_recovery = false; + self.recovery_point = 0; + self.pipe = 0; + self.dup_ack_count = 0; + } + + /// Process a new ACK during recovery. Returns true if recovery is complete + /// (seg_ack >= recovery_point). + pub fn on_ack(&mut self, seg_ack: u32) -> bool { + if seq_le(self.recovery_point, seg_ack) { + self.exit(); + true + } else { + false + } + } + + /// RFC 6675 S4: IsLost predicate. A segment starting at `seq` is lost if + /// DupThresh (3) segments above it have been SACKed, or the byte distance + /// exceeds DupThresh * MSS. + pub fn is_lost(&self, seq: u32, scoreboard: &BTreeMap, eff_mss: u16) -> bool { + let mut sacked_segments_above = 0u32; + let mut highest_sacked_end: Option = None; + + for (&start, &len) in scoreboard { + let end = start.wrapping_add(len); + // Only consider SACK blocks that are above seq. + if seq_lt(seq, start) { + // Count MSS-sized segments within this SACK block. + let block_segments = len.div_ceil(u32::from(eff_mss)); + sacked_segments_above += block_segments; + + // Track the highest SACKed byte. + match highest_sacked_end { + Some(prev) if seq_lt(prev, end) => highest_sacked_end = Some(end), + None => highest_sacked_end = Some(end), + _ => {} + } + } + } + + if sacked_segments_above >= DUP_THRESH { + return true; + } + + // Check byte distance from seq to highest SACKed end. + if let Some(high) = highest_sacked_end { + let byte_distance = high.wrapping_sub(seq); + if byte_distance > DUP_THRESH * u32::from(eff_mss) { + return true; + } + } + + false + } + + /// RFC 6675 S4.1: SetPipe -- estimate bytes in the network. + /// Iterates MSS-sized blocks from snd_una to snd_nxt. + pub fn set_pipe( + &mut self, + snd_una: u32, + snd_nxt: u32, + scoreboard: &BTreeMap, + eff_mss: u16, + ) { + let mss = u32::from(eff_mss); + let mut pipe: u32 = 0; + let mut seq = snd_una; + + while seq_lt(seq, snd_nxt) { + if !self.is_sacked(seq, scoreboard) && !self.is_lost(seq, scoreboard, eff_mss) { + pipe += mss; + } + seq = seq.wrapping_add(mss); + } + + self.pipe = pipe; + } + + /// Check if a sequence number falls within a SACKed range. + fn is_sacked(&self, seq: u32, scoreboard: &BTreeMap) -> bool { + for (&start, &len) in scoreboard { + let end = start.wrapping_add(len); + if seq_le(start, seq) && seq_lt(seq, end) { + return true; + } + } + false + } + + /// Find the lowest lost sequence number >= `from` for retransmission. + pub fn next_lost_segment( + &self, + from: u32, + snd_nxt: u32, + scoreboard: &BTreeMap, + eff_mss: u16, + ) -> Option { + let mss = u32::from(eff_mss); + let mut seq = from; + + while seq_lt(seq, snd_nxt) { + if !self.is_sacked(seq, scoreboard) && self.is_lost(seq, scoreboard, eff_mss) { + return Some(seq); + } + seq = seq.wrapping_add(mss); + } + + None + } +} + +pub struct PrrState { + pub prr_delivered: u32, + pub prr_out: u32, + pub recover_fs: u32, +} + +impl PrrState { + pub fn new() -> Self { + Self { + prr_delivered: 0, + prr_out: 0, + recover_fs: 0, + } + } + + /// Enter recovery. `bytes_in_flight` = snd_nxt - snd_una at entry. + pub fn enter(&mut self, bytes_in_flight: u32) { + self.recover_fs = bytes_in_flight; + self.prr_delivered = 0; + self.prr_out = 0; + } + + /// Reset on recovery exit. + pub fn exit(&mut self) { + self.prr_delivered = 0; + self.prr_out = 0; + self.recover_fs = 0; + } + + /// Called on each ACK during recovery. Returns snd_cnt (bytes allowed to send). + /// `bytes_newly_delivered` = bytes_acked + bytes_newly_sacked. + /// `pipe` = current pipe estimate. `ssthresh` = target cwnd. + pub fn on_ack( + &mut self, + bytes_newly_delivered: u32, + pipe: u32, + ssthresh: u32, + eff_mss: u16, + ) -> u32 { + self.prr_delivered += bytes_newly_delivered; + + if self.recover_fs == 0 { + return eff_mss as u32; + } + + if pipe > ssthresh { + // Proportional: snd_cnt = ceil(prr_delivered * ssthresh / recover_fs) - prr_out. + let numer = self.prr_delivered as u64 * ssthresh as u64; + let target = numer.div_ceil(self.recover_fs as u64) as u32; + target.saturating_sub(self.prr_out) + } else { + // Slow start reduction bound. + let limit = self.prr_delivered.saturating_sub(self.prr_out) + eff_mss as u32; + ssthresh.saturating_sub(pipe).min(limit) + } + } + + /// Called after sending bytes during recovery. + pub fn on_sent(&mut self, bytes_sent: u32) { + self.prr_out += bytes_sent; + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FRtoPhase { + Disabled, + Step1, + Step2, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FRtoAction { + /// No F-RTO active — proceed normally. + None, + /// Send new data (not retransmissions) to probe. + SendNewData, + /// Spurious RTO detected — restore cwnd. + SpuriousRto, + /// Genuine loss confirmed — keep reduced cwnd. + GenuineLoss, +} + +pub struct FRtoState { + pub phase: FRtoPhase, + snd_una_at_rto: u32, + snd_una_last: u32, +} + +impl FRtoState { + pub fn new() -> Self { + Self { + phase: FRtoPhase::Disabled, + snd_una_at_rto: 0, + snd_una_last: 0, + } + } + + /// Arm F-RTO on RTO retransmit. + pub fn enter(&mut self, snd_una: u32) { + self.phase = FRtoPhase::Step1; + self.snd_una_at_rto = snd_una; + self.snd_una_last = snd_una; + } + + /// Process ACK during F-RTO. `new_snd_una` is the updated snd_una after this ACK. + pub fn on_ack(&mut self, new_snd_una: u32) -> FRtoAction { + match self.phase { + FRtoPhase::Disabled => FRtoAction::None, + FRtoPhase::Step1 => { + if seq_lt(self.snd_una_last, new_snd_una) { + self.snd_una_last = new_snd_una; + self.phase = FRtoPhase::Step2; + FRtoAction::SendNewData + } else { + self.phase = FRtoPhase::Disabled; + FRtoAction::GenuineLoss + } + } + FRtoPhase::Step2 => { + if seq_lt(self.snd_una_last, new_snd_una) { + self.phase = FRtoPhase::Disabled; + FRtoAction::SpuriousRto + } else { + self.phase = FRtoPhase::Disabled; + FRtoAction::GenuineLoss + } + } + } + } + + /// Check if F-RTO is active. + pub fn is_active(&self) -> bool { + self.phase != FRtoPhase::Disabled + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn is_lost_with_3_sacked_above() { + let recovery = SackRecovery::new(); + let mut scoreboard = BTreeMap::new(); + // SACKed: [2000..3000], [3000..4000], [4000..5000] -- 3 blocks above seq 1000. + scoreboard.insert(2000u32, 1000u32); + scoreboard.insert(3000u32, 1000u32); + scoreboard.insert(4000u32, 1000u32); + assert!(recovery.is_lost(1000, &scoreboard, 1000)); + } + + #[test] + fn is_lost_with_large_gap() { + let recovery = SackRecovery::new(); + let mut scoreboard = BTreeMap::new(); + // Single SACK at [5000..6000] -- 4000 bytes above seq 1000, > 3*MSS. + scoreboard.insert(5000u32, 1000u32); + assert!(recovery.is_lost(1000, &scoreboard, 1000)); + } + + #[test] + fn not_lost_with_insufficient_sacks() { + let recovery = SackRecovery::new(); + let mut scoreboard = BTreeMap::new(); + // Only 2 blocks above. + scoreboard.insert(2000u32, 1000u32); + scoreboard.insert(3000u32, 1000u32); + assert!(!recovery.is_lost(1000, &scoreboard, 1000)); + } + + #[test] + fn set_pipe_counts_in_flight() { + let mut recovery = SackRecovery::new(); + let mut scoreboard = BTreeMap::new(); + // snd_una=1000, snd_nxt=5000 => 4 MSS-sized blocks. + // All 3 blocks [2000..5000] SACKed. Block [1000..2000] is lost (3 SACKed above). + scoreboard.insert(2000u32, 1000u32); + scoreboard.insert(3000u32, 1000u32); + scoreboard.insert(4000u32, 1000u32); + recovery.set_pipe(1000, 5000, &scoreboard, 1000); + assert_eq!(recovery.pipe, 0); // all either SACKed or lost + } + + #[test] + fn set_pipe_counts_unacked_unsacked_as_in_flight() { + let mut recovery = SackRecovery::new(); + let mut scoreboard = BTreeMap::new(); + // snd_una=1000, snd_nxt=6000 => 5 MSS blocks. + // Only [2000..3000] SACKed. + scoreboard.insert(2000u32, 1000u32); + // [1000..2000]: not SACKed, only 1 block above -- not lost => in flight. + // [2000..3000]: SACKed. + // [3000..4000]: not SACKed, 0 blocks above => in flight. + // [4000..5000]: not SACKed, 0 blocks above => in flight. + // [5000..6000]: not SACKed, 0 blocks above => in flight. + recovery.set_pipe(1000, 6000, &scoreboard, 1000); + assert_eq!(recovery.pipe, 4000); // 4 blocks in flight + } + + #[test] + fn enter_and_exit_recovery() { + let mut recovery = SackRecovery::new(); + recovery.enter(5000); + assert!(recovery.in_recovery); + assert_eq!(recovery.recovery_point, 5000); + + // Partial ACK -- doesn't exit. + assert!(!recovery.on_ack(3000)); + assert!(recovery.in_recovery); + + // Full ACK -- exits. + assert!(recovery.on_ack(5000)); + assert!(!recovery.in_recovery); + } + + #[test] + fn prr_proportional_when_pipe_above_ssthresh() { + let mut prr = PrrState::new(); + prr.enter(10_000); // recover_fs = 10_000. + let ssthresh = 7_000u32; + let mss = 1460u16; + + // Simulate ACK delivering 1460 bytes. + let snd_cnt = prr.on_ack(1460, 8_000, ssthresh, mss); + // pipe=8000 > ssthresh=7000 => proportional. + // snd_cnt = ceil(1460 * 7000 / 10000) - 0 = ceil(1022) = 1022. + assert_eq!(snd_cnt, 1022); + + // After "sending" 1022 bytes. + prr.on_sent(1022); + // Second ACK (pipe still above ssthresh). + let snd_cnt = prr.on_ack(1460, 7_500, ssthresh, mss); + // snd_cnt = ceil(2920 * 7000 / 10000) - 1022 = ceil(2044) - 1022 = 1022. + assert_eq!(snd_cnt, 1022); + } + + #[test] + fn prr_slow_start_reduction_when_pipe_below_ssthresh() { + let mut prr = PrrState::new(); + prr.enter(10_000); + let ssthresh = 7_000u32; + let mss = 1460u16; + + // pipe=5000 < ssthresh. + let snd_cnt = prr.on_ack(1460, 5_000, ssthresh, mss); + // snd_cnt = min(7000 - 5000, 1460 - 0 + 1460) = min(2000, 2920) = 2000. + assert_eq!(snd_cnt, 2000); + } + + #[test] + fn frto_spurious_rto_detected() { + let mut frto = FRtoState::new(); + frto.enter(1000); + + // First ACK advances snd_una. + let result = frto.on_ack(2000); + assert_eq!(result, FRtoAction::SendNewData); + + // Second ACK advances snd_una again => spurious. + let result = frto.on_ack(3000); + assert_eq!(result, FRtoAction::SpuriousRto); + } + + #[test] + fn frto_genuine_loss_on_first_dup_ack() { + let mut frto = FRtoState::new(); + frto.enter(1000); + + // First ACK is dup (snd_una doesn't advance). + let result = frto.on_ack(1000); + assert_eq!(result, FRtoAction::GenuineLoss); + } + + #[test] + fn frto_genuine_loss_on_second_dup_ack() { + let mut frto = FRtoState::new(); + frto.enter(1000); + + // First ACK advances. + let result = frto.on_ack(2000); + assert_eq!(result, FRtoAction::SendNewData); + + // Second ACK is dup. + let result = frto.on_ack(2000); + assert_eq!(result, FRtoAction::GenuineLoss); + } +} diff --git a/src/net/handler/tcp/ring_buffer.rs b/src/net/handler/tcp/ring_buffer.rs new file mode 100644 index 0000000..b7bb949 --- /dev/null +++ b/src/net/handler/tcp/ring_buffer.rs @@ -0,0 +1,409 @@ +/// Fixed-capacity ring buffer for TCP send/receive data. +/// +/// Capacity must be a power of two. Uses bitwise AND for index wrapping +/// instead of modulo for performance. +pub struct RingBuffer { + buf: Vec, + head: usize, + tail: usize, + len: usize, + mask: usize, +} + +impl RingBuffer { + /// Create a new ring buffer. `capacity` must be a power of two. + pub fn new(capacity: usize) -> Self { + assert!( + capacity.is_power_of_two(), + "RingBuffer capacity must be a power of two" + ); + Self { + buf: vec![0u8; capacity], + head: 0, + tail: 0, + len: 0, + mask: capacity - 1, + } + } + + /// Number of bytes available to read. + #[inline] + pub fn available(&self) -> usize { + self.len + } + + /// Number of bytes available to write. + #[inline] + pub fn free_space(&self) -> usize { + self.buf.len() - self.len + } + + /// Total capacity. + #[inline] + pub fn capacity(&self) -> usize { + self.buf.len() + } + + /// Write bytes into the buffer at `tail`. Returns the number of bytes written. + /// Writes as many bytes as fit; returns less than `data.len()` if buffer is nearly full. + #[inline] + pub fn write(&mut self, data: &[u8]) -> usize { + let to_write = data.len().min(self.free_space()); + if to_write == 0 { + return 0; + } + let tail = self.tail & self.mask; + let first = to_write.min(self.buf.len() - tail); + self.buf[tail..tail + first].copy_from_slice(&data[..first]); + if first < to_write { + self.buf[..to_write - first].copy_from_slice(&data[first..to_write]); + } + self.tail = self.tail.wrapping_add(to_write); + self.len += to_write; + to_write + } + + /// Write bytes at an arbitrary offset from `head`. Does NOT advance `tail` or `len`. + /// Used by the receive side for out-of-order segments — the caller is responsible + /// for tracking which ranges are filled and advancing via `commit()` when contiguous. + #[inline] + pub fn write_at(&mut self, offset: usize, data: &[u8]) { + let pos = (self.head.wrapping_add(offset)) & self.mask; + let first = data.len().min(self.buf.len() - pos); + self.buf[pos..pos + first].copy_from_slice(&data[..first]); + if first < data.len() { + self.buf[..data.len() - first].copy_from_slice(&data[first..]); + } + } + + /// Read bytes at an arbitrary offset from `head` without advancing `head`. + /// Used by the send side for retransmission. + #[inline] + pub fn peek_at(&self, offset: usize, buf: &mut [u8]) { + let len = buf.len(); + let pos = (self.head.wrapping_add(offset)) & self.mask; + let first = len.min(self.buf.len() - pos); + buf[..first].copy_from_slice(&self.buf[pos..pos + first]); + if first < len { + buf[first..].copy_from_slice(&self.buf[..len - first]); + } + } + + /// Return two slices covering `len` bytes starting at `offset` from `head`, + /// without advancing `head`. The first slice covers data up to the end of + /// the backing buffer; the second covers the wrap-around portion (empty if + /// no wrap occurs). + #[inline] + pub fn peek_slices(&self, offset: usize, len: usize) -> (&[u8], &[u8]) { + debug_assert!(offset + len <= self.len); + let pos = (self.head.wrapping_add(offset)) & self.mask; + let first = len.min(self.buf.len() - pos); + if first >= len { + (&self.buf[pos..pos + len], &[]) + } else { + (&self.buf[pos..], &self.buf[..len - first]) + } + } + + /// Advance `head` by `n` bytes, freeing space. Used when bytes are ACKed (send) + /// or consumed by `TcpStream::read()` (receive). + #[inline] + pub fn advance(&mut self, n: usize) { + debug_assert!(n <= self.len); + self.head = self.head.wrapping_add(n); + self.len -= n; + } + + /// Advance `tail` and `len` to mark bytes as available for reading. + /// Used by the receive side when contiguous data is confirmed. + #[inline] + pub fn commit(&mut self, n: usize) { + debug_assert!(n <= self.free_space()); + self.tail = self.tail.wrapping_add(n); + self.len += n; + } + + /// Transfer up to `max_len` bytes from self (as source/read-side) to `dst` + /// (as destination/write-side). Reads from self's head, writes to dst's tail. + /// Returns the number of bytes transferred. + /// Equivalent to read() + write() but avoids the intermediate buffer. + #[inline] + pub fn transfer(&mut self, dst: &mut RingBuffer, max_len: usize) -> usize { + let available = self.available().min(max_len); + let to_transfer = available.min(dst.free_space()); + if to_transfer == 0 { + return 0; + } + // Get source slices (may wrap around). + let (s1, s2) = self.peek_slices(0, to_transfer); + // Write each slice to destination. + let wrote1 = dst.write(s1); + let wrote2 = if !s2.is_empty() { dst.write(s2) } else { 0 }; + let total = wrote1 + wrote2; + // Advance source head. + self.advance(total); + total + } + + /// Read bytes from the buffer starting at `head`. Returns the number of bytes read. + /// Advances `head` by the number of bytes read. + #[inline] + pub fn read(&mut self, buf: &mut [u8]) -> usize { + let to_read = buf.len().min(self.available()); + if to_read == 0 { + return 0; + } + let head = self.head & self.mask; + let first = to_read.min(self.buf.len() - head); + buf[..first].copy_from_slice(&self.buf[head..head + first]); + if first < to_read { + buf[first..to_read].copy_from_slice(&self.buf[..to_read - first]); + } + self.head = self.head.wrapping_add(to_read); + self.len -= to_read; + to_read + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn new_ring_buffer() { + let rb = RingBuffer::new(1024); + assert_eq!(rb.capacity(), 1024); + assert_eq!(rb.available(), 0); + assert_eq!(rb.free_space(), 1024); + } + + #[test] + #[should_panic(expected = "power of two")] + fn non_power_of_two_panics() { + RingBuffer::new(1000); + } + + #[test] + fn write_and_read() { + let mut rb = RingBuffer::new(64); + let data = b"hello world"; + let written = rb.write(data); + assert_eq!(written, data.len()); + assert_eq!(rb.available(), data.len()); + assert_eq!(rb.free_space(), 64 - data.len()); + + let mut buf = [0u8; 64]; + let read = rb.read(&mut buf); + assert_eq!(read, data.len()); + assert_eq!(&buf[..read], data); + assert_eq!(rb.available(), 0); + assert_eq!(rb.free_space(), 64); + } + + #[test] + fn write_wraps_around() { + let mut rb = RingBuffer::new(16); + rb.write(&[0xAA; 12]); + let mut discard = [0u8; 12]; + rb.read(&mut discard); + // head=12, tail=12. Write 8 bytes: 4 at end, 4 wrap to start. + let written = rb.write(&[0xBB; 8]); + assert_eq!(written, 8); + assert_eq!(rb.available(), 8); + + let mut buf = [0u8; 8]; + let read = rb.read(&mut buf); + assert_eq!(read, 8); + assert_eq!(buf, [0xBB; 8]); + } + + #[test] + fn write_when_full_returns_zero() { + let mut rb = RingBuffer::new(16); + let written = rb.write(&[0xFF; 16]); + assert_eq!(written, 16); + let written = rb.write(&[0xAA; 1]); + assert_eq!(written, 0); + } + + #[test] + fn partial_write_when_nearly_full() { + let mut rb = RingBuffer::new(16); + rb.write(&[0xFF; 12]); + let written = rb.write(&[0xAA; 8]); + assert_eq!(written, 4); + assert_eq!(rb.available(), 16); + } + + #[test] + fn write_at_and_peek() { + let mut rb = RingBuffer::new(64); + // Write at offset 0. + rb.write_at(0, b"AAAA"); + // Write at offset 8 (gap at 4..8). + rb.write_at(8, b"CCCC"); + // Fill the gap. + rb.write_at(4, b"BBBB"); + // write_at doesn't advance len/tail. + assert_eq!(rb.available(), 0); + // Commit all 12 bytes. + rb.commit(12); + assert_eq!(rb.available(), 12); + // Read and verify. + let mut buf = [0u8; 12]; + let read = rb.read(&mut buf); + assert_eq!(read, 12); + assert_eq!(&buf, b"AAAABBBBCCCC"); + } + + #[test] + fn write_at_wraps() { + let mut rb = RingBuffer::new(16); + // Move head to position 12. + rb.write(&[0xAA; 12]); + let mut discard = [0u8; 12]; + rb.read(&mut discard); + // head=12. write_at offset 2 from head = position 14. Writing 4 bytes wraps. + rb.write_at(2, &[0xBB; 4]); + // Verify by peeking. + let mut buf = [0u8; 4]; + rb.peek_at(2, &mut buf); + assert_eq!(buf, [0xBB; 4]); + } + + #[test] + fn peek_at_does_not_advance() { + let mut rb = RingBuffer::new(64); + rb.write(b"hello"); + let mut buf = [0u8; 5]; + rb.peek_at(0, &mut buf); + assert_eq!(&buf, b"hello"); + assert_eq!(rb.available(), 5); // unchanged + // Peek again at offset 2. + let mut buf2 = [0u8; 3]; + rb.peek_at(2, &mut buf2); + assert_eq!(&buf2, b"llo"); + } + + #[test] + fn advance_frees_space() { + let mut rb = RingBuffer::new(64); + rb.write(b"hello world"); + rb.advance(5); + assert_eq!(rb.available(), 6); + assert_eq!(rb.free_space(), 64 - 6); + let mut buf = [0u8; 6]; + rb.read(&mut buf); + assert_eq!(&buf, b" world"); + } + + #[test] + fn commit_makes_data_readable() { + let mut rb = RingBuffer::new(64); + // write_at without commit = no data available. + rb.write_at(0, b"test"); + assert_eq!(rb.available(), 0); + // After commit, data is readable. + rb.commit(4); + assert_eq!(rb.available(), 4); + let mut buf = [0u8; 4]; + rb.read(&mut buf); + assert_eq!(&buf, b"test"); + } + + #[test] + fn peek_slices_no_wrap() { + let mut rb = RingBuffer::new(64); + rb.write(b"hello world"); + let (a, b) = rb.peek_slices(0, 5); + assert_eq!(a, b"hello"); + assert!(b.is_empty()); + assert_eq!(rb.available(), 11); // unchanged + } + + #[test] + fn peek_slices_with_wrap() { + let mut rb = RingBuffer::new(16); + rb.write(&[0xAA; 12]); + let mut discard = [0u8; 12]; + rb.read(&mut discard); + // head=12. Write 8 bytes: 4 at end [12..16], 4 wrap [0..4]. + rb.write(&[0xBB; 8]); + let (a, b) = rb.peek_slices(0, 8); + assert_eq!(a, &[0xBB; 4]); + assert_eq!(b, &[0xBB; 4]); + } + + #[test] + fn peek_slices_with_offset() { + let mut rb = RingBuffer::new(64); + rb.write(b"hello world"); + let (a, b) = rb.peek_slices(6, 5); + assert_eq!(a, b"world"); + assert!(b.is_empty()); + } + + #[test] + fn transfer_basic() { + let mut src = RingBuffer::new(64); + let mut dst = RingBuffer::new(64); + src.write(b"hello"); + let n = src.transfer(&mut dst, 5); + assert_eq!(n, 5); + assert_eq!(src.available(), 0); + assert_eq!(dst.available(), 5); + let mut buf = [0u8; 5]; + dst.read(&mut buf); + assert_eq!(&buf, b"hello"); + } + + #[test] + fn transfer_with_source_wrap() { + let mut src = RingBuffer::new(16); + let mut dst = RingBuffer::new(64); + // Move head to position 12. + src.write(&[0xAA; 12]); + let mut discard = [0u8; 12]; + src.read(&mut discard); + // head=12. Write 8 bytes: 4 at end [12..16], 4 wrap [0..4]. + src.write(&[0xBB; 8]); + let n = src.transfer(&mut dst, 8); + assert_eq!(n, 8); + assert_eq!(src.available(), 0); + assert_eq!(dst.available(), 8); + let mut buf = [0u8; 8]; + dst.read(&mut buf); + assert_eq!(buf, [0xBB; 8]); + } + + #[test] + fn transfer_partial_when_dst_nearly_full() { + let mut src = RingBuffer::new(64); + let mut dst = RingBuffer::new(16); + // Fill dst with 12 bytes, leaving 4 free. + dst.write(&[0xCC; 12]); + src.write(b"hello world!"); + let n = src.transfer(&mut dst, 12); + assert_eq!(n, 4); + assert_eq!(src.available(), 8); + assert_eq!(dst.available(), 16); + } + + #[test] + fn transfer_empty_source() { + let mut src = RingBuffer::new(64); + let mut dst = RingBuffer::new(64); + let n = src.transfer(&mut dst, 100); + assert_eq!(n, 0); + } + + #[test] + fn transfer_full_dst() { + let mut src = RingBuffer::new(64); + let mut dst = RingBuffer::new(16); + dst.write(&[0xFF; 16]); + src.write(b"hello"); + let n = src.transfer(&mut dst, 5); + assert_eq!(n, 0); + } +} diff --git a/src/net/handler/tcp/segment.rs b/src/net/handler/tcp/segment.rs new file mode 100644 index 0000000..0fd4a11 --- /dev/null +++ b/src/net/handler/tcp/segment.rs @@ -0,0 +1,1581 @@ +use crate::net::{ + checksum::{compute_tcp_checksum_from_parts, compute_tcp_checksum_v6_from_parts}, + wire::{ + ethernet::{EtherTypes, EthernetFrame, MacAddress, write_ethernet_header}, + ip::{ + IPV4_MIN_HEADER_LEN, IPV6_HEADER_LEN, IpAddress, IpProtocols, Ipv4Address, Ipv6Address, + }, + tcp::{ + TCP_HEADER_LEN, TcpHeader, flags, options, write_mss_option, + write_sack_option, write_sack_permitted_option, write_timestamp_option, + write_window_scale_option, + }, + }, +}; +use crate::xdp::frame::{Frame, FrameBuffer}; + +const ETH_LEN: usize = size_of::(); + +/// Stateless builder for outbound TCP segments. +/// +/// All methods pop a frame from `free_frames`, write Ethernet + IP + TCP +/// headers, compute checksums (unless `tx_offload`), and push the result +/// to `tx_return`. The original received frame is the caller's +/// responsibility. +pub struct SegmentBuilder; + +impl SegmentBuilder { + /// Build a RST segment for the CLOSED state (no matching connection). + /// + /// Per RFC 9293 §3.10.7.1: + /// - If ACK bit off: `` + /// - If ACK bit on: `` + #[inline] + pub fn build_rst<'umem>( + incoming_src_addr: IpAddress, + incoming_dst_addr: IpAddress, + incoming_src_port: u16, + incoming_dst_port: u16, + incoming_seq: u32, + incoming_ack: u32, + incoming_flags: u8, + incoming_seg_len: u32, + src_mac: MacAddress, + dst_mac: MacAddress, + tx_offload: bool, + free_frames: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) { + let (seq, ack, rst_flags) = if incoming_flags & flags::ACK == 0 { + // ACK off: + ( + 0u32, + incoming_seq.wrapping_add(incoming_seg_len), + flags::RST | flags::ACK, + ) + } else { + // ACK on: + (incoming_ack, 0u32, flags::RST) + }; + + match (incoming_dst_addr, incoming_src_addr) { + (IpAddress::V4(local_ip), IpAddress::V4(remote_ip)) => { + Self::build_ipv4_segment( + local_ip, + remote_ip, + incoming_dst_port, + incoming_src_port, + seq, + ack, + rst_flags, + 0, + &[], // no options + src_mac, + dst_mac, + tx_offload, + free_frames, + tx_return, + ); + } + (IpAddress::V6(local_ip), IpAddress::V6(remote_ip)) => { + Self::build_ipv6_segment( + local_ip, + remote_ip, + incoming_dst_port, + incoming_src_port, + seq, + ack, + rst_flags, + 0, + &[], + src_mac, + dst_mac, + tx_offload, + free_frames, + tx_return, + ); + } + _ => {} // mixed v4/v6 — should not happen + } + } + + /// Build a SYN segment (active open). + #[inline] + pub fn build_syn<'umem>( + local_addr: IpAddress, + remote_addr: IpAddress, + local_port: u16, + remote_port: u16, + iss: u32, + window: u16, + mss: u16, + wscale: u8, + timestamp: Option<(u32, u32)>, + sack_permitted: bool, + ecn: bool, + src_mac: MacAddress, + dst_mac: MacAddress, + tx_offload: bool, + free_frames: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) { + // Max options: MSS(4) + NOP(1) + WSCALE(3) + NOP(1) + NOP(1) + TS(10) + SACK_PERM(2) = 22, pad to 24 + let mut opt_buf = [0u8; 24]; + let mut opt_len = write_mss_option(&mut opt_buf, mss); + opt_buf[opt_len] = options::NOP; + opt_len += 1; + opt_len += write_window_scale_option(&mut opt_buf[opt_len..], wscale); + if let Some((tsval, tsecr)) = timestamp { + opt_buf[opt_len] = options::NOP; + opt_len += 1; + opt_buf[opt_len] = options::NOP; + opt_len += 1; + opt_len += write_timestamp_option(&mut opt_buf[opt_len..], tsval, tsecr); + } + if sack_permitted { + opt_len += write_sack_permitted_option(&mut opt_buf[opt_len..]); + } + + let syn_flags = if ecn { flags::SYN | flags::ECE | flags::CWR } else { flags::SYN }; + + match (local_addr, remote_addr) { + (IpAddress::V4(local_ip), IpAddress::V4(remote_ip)) => { + Self::build_ipv4_segment( + local_ip, + remote_ip, + local_port, + remote_port, + iss, + 0, + syn_flags, + window, + &opt_buf[..opt_len], + src_mac, + dst_mac, + tx_offload, + free_frames, + tx_return, + ); + } + (IpAddress::V6(local_ip), IpAddress::V6(remote_ip)) => { + Self::build_ipv6_segment( + local_ip, + remote_ip, + local_port, + remote_port, + iss, + 0, + syn_flags, + window, + &opt_buf[..opt_len], + src_mac, + dst_mac, + tx_offload, + free_frames, + tx_return, + ); + } + _ => {} + } + } + + /// Build a SYN-ACK segment (passive open response). + #[inline] + pub fn build_syn_ack<'umem>( + local_addr: IpAddress, + remote_addr: IpAddress, + local_port: u16, + remote_port: u16, + iss: u32, + ack: u32, + window: u16, + mss: u16, + wscale: Option, + timestamp: Option<(u32, u32)>, + sack_permitted: bool, + ecn: bool, + src_mac: MacAddress, + dst_mac: MacAddress, + tx_offload: bool, + free_frames: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) { + let mut opt_buf = [0u8; 24]; + let mut opt_len = write_mss_option(&mut opt_buf, mss); + if let Some(shift) = wscale { + opt_buf[opt_len] = options::NOP; + opt_len += 1; + opt_len += write_window_scale_option(&mut opt_buf[opt_len..], shift); + } + if let Some((tsval, tsecr)) = timestamp { + opt_buf[opt_len] = options::NOP; + opt_len += 1; + opt_buf[opt_len] = options::NOP; + opt_len += 1; + opt_len += write_timestamp_option(&mut opt_buf[opt_len..], tsval, tsecr); + } + if sack_permitted { + opt_len += write_sack_permitted_option(&mut opt_buf[opt_len..]); + } + + let syn_ack_flags = if ecn { flags::SYN | flags::ACK | flags::ECE } else { flags::SYN | flags::ACK }; + + match (local_addr, remote_addr) { + (IpAddress::V4(local_ip), IpAddress::V4(remote_ip)) => { + Self::build_ipv4_segment( + local_ip, + remote_ip, + local_port, + remote_port, + iss, + ack, + syn_ack_flags, + window, + &opt_buf[..opt_len], + src_mac, + dst_mac, + tx_offload, + free_frames, + tx_return, + ); + } + (IpAddress::V6(local_ip), IpAddress::V6(remote_ip)) => { + Self::build_ipv6_segment( + local_ip, + remote_ip, + local_port, + remote_port, + iss, + ack, + syn_ack_flags, + window, + &opt_buf[..opt_len], + src_mac, + dst_mac, + tx_offload, + free_frames, + tx_return, + ); + } + _ => {} + } + } + + /// Build a pure ACK segment (no data, no SYN/FIN). + /// + /// The `tcp_flags` parameter allows the caller to set additional flags + /// (e.g. ECE) alongside the base ACK. Callers typically pass `flags::ACK` + /// or `flags::ACK | flags::ECE`. + #[inline] + pub fn build_ack<'umem>( + local_addr: IpAddress, + remote_addr: IpAddress, + local_port: u16, + remote_port: u16, + seq: u32, + ack: u32, + window: u16, + tcp_flags: u8, + timestamp: Option<(u32, u32)>, + src_mac: MacAddress, + dst_mac: MacAddress, + tx_offload: bool, + free_frames: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) { + let mut ts_buf = [0u8; 12]; + let tcp_options: &[u8] = if let Some((tsval, tsecr)) = timestamp { + ts_buf[0] = options::NOP; + ts_buf[1] = options::NOP; + write_timestamp_option(&mut ts_buf[2..], tsval, tsecr); + &ts_buf + } else { + &[] + }; + + match (local_addr, remote_addr) { + (IpAddress::V4(local_ip), IpAddress::V4(remote_ip)) => { + Self::build_ipv4_segment( + local_ip, + remote_ip, + local_port, + remote_port, + seq, + ack, + tcp_flags, + window, + tcp_options, + src_mac, + dst_mac, + tx_offload, + free_frames, + tx_return, + ); + } + (IpAddress::V6(local_ip), IpAddress::V6(remote_ip)) => { + Self::build_ipv6_segment( + local_ip, + remote_ip, + local_port, + remote_port, + seq, + ack, + tcp_flags, + window, + tcp_options, + src_mac, + dst_mac, + tx_offload, + free_frames, + tx_return, + ); + } + _ => {} + } + } + + /// Build a pure ACK segment with optional SACK blocks. + /// + /// When `sack_blocks` is empty this behaves identically to `build_ack`. + /// SACK blocks are encoded per RFC 2018 using `write_sack_option`. + #[inline] + pub fn build_ack_with_sack<'umem>( + local_addr: IpAddress, + remote_addr: IpAddress, + local_port: u16, + remote_port: u16, + seq: u32, + ack: u32, + window: u16, + tcp_flags: u8, + timestamp: Option<(u32, u32)>, + sack_blocks: &[(u32, u32)], + src_mac: MacAddress, + dst_mac: MacAddress, + tx_offload: bool, + free_frames: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) { + // Max options: NOP(1) + NOP(1) + TS(10) + SACK(2 + 4*8) = 44, but TCP + // options are capped at 40 bytes. With timestamps (12 bytes) we have 28 + // bytes left = room for 3 SACK blocks (2 + 3*8 = 26). Without timestamps + // we can fit 4 blocks (2 + 4*8 = 34). + let mut opt_buf = [0u8; 40]; + let mut opt_len = 0usize; + + if let Some((tsval, tsecr)) = timestamp { + opt_buf[0] = options::NOP; + opt_buf[1] = options::NOP; + opt_len = 2 + write_timestamp_option(&mut opt_buf[2..], tsval, tsecr); + } + + if !sack_blocks.is_empty() { + opt_len += write_sack_option(&mut opt_buf[opt_len..], sack_blocks); + } + + let tcp_options = &opt_buf[..opt_len]; + + match (local_addr, remote_addr) { + (IpAddress::V4(local_ip), IpAddress::V4(remote_ip)) => { + Self::build_ipv4_segment( + local_ip, + remote_ip, + local_port, + remote_port, + seq, + ack, + tcp_flags, + window, + tcp_options, + src_mac, + dst_mac, + tx_offload, + free_frames, + tx_return, + ); + } + (IpAddress::V6(local_ip), IpAddress::V6(remote_ip)) => { + Self::build_ipv6_segment( + local_ip, + remote_ip, + local_port, + remote_port, + seq, + ack, + tcp_flags, + window, + tcp_options, + src_mac, + dst_mac, + tx_offload, + free_frames, + tx_return, + ); + } + _ => {} + } + } + + /// Build a FIN-ACK segment (no data). Used for graceful connection close. + #[inline] + pub fn build_fin_ack<'umem>( + local_addr: IpAddress, + remote_addr: IpAddress, + local_port: u16, + remote_port: u16, + seq: u32, + ack: u32, + window: u16, + timestamp: Option<(u32, u32)>, + src_mac: MacAddress, + dst_mac: MacAddress, + tx_offload: bool, + free_frames: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) { + let mut ts_buf = [0u8; 12]; + let tcp_options: &[u8] = if let Some((tsval, tsecr)) = timestamp { + ts_buf[0] = options::NOP; + ts_buf[1] = options::NOP; + write_timestamp_option(&mut ts_buf[2..], tsval, tsecr); + &ts_buf + } else { + &[] + }; + + match (local_addr, remote_addr) { + (IpAddress::V4(local_ip), IpAddress::V4(remote_ip)) => { + Self::build_ipv4_segment( + local_ip, + remote_ip, + local_port, + remote_port, + seq, + ack, + flags::ACK | flags::FIN, + window, + tcp_options, + src_mac, + dst_mac, + tx_offload, + free_frames, + tx_return, + ); + } + (IpAddress::V6(local_ip), IpAddress::V6(remote_ip)) => { + Self::build_ipv6_segment( + local_ip, + remote_ip, + local_port, + remote_port, + seq, + ack, + flags::ACK | flags::FIN, + window, + tcp_options, + src_mac, + dst_mac, + tx_offload, + free_frames, + tx_return, + ); + } + _ => {} + } + } + + /// Build a data segment with payload (ACK flag set). + #[inline] + pub fn build_data<'umem>( + local_addr: IpAddress, + remote_addr: IpAddress, + local_port: u16, + remote_port: u16, + seq: u32, + ack: u32, + window: u16, + payload: &[u8], + timestamp: Option<(u32, u32)>, + src_mac: MacAddress, + dst_mac: MacAddress, + tx_offload: bool, + free_frames: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) { + let mut ts_buf = [0u8; 12]; + let tcp_options: &[u8] = if let Some((tsval, tsecr)) = timestamp { + ts_buf[0] = options::NOP; + ts_buf[1] = options::NOP; + write_timestamp_option(&mut ts_buf[2..], tsval, tsecr); + &ts_buf + } else { + &[] + }; + + match (local_addr, remote_addr) { + (IpAddress::V4(local_ip), IpAddress::V4(remote_ip)) => { + Self::build_ipv4_data_segment( + local_ip, + remote_ip, + local_port, + remote_port, + seq, + ack, + flags::ACK, + window, + payload, + tcp_options, + src_mac, + dst_mac, + tx_offload, + free_frames, + tx_return, + ); + } + (IpAddress::V6(local_ip), IpAddress::V6(remote_ip)) => { + Self::build_ipv6_data_segment( + local_ip, + remote_ip, + local_port, + remote_port, + seq, + ack, + flags::ACK, + window, + payload, + tcp_options, + src_mac, + dst_mac, + tx_offload, + free_frames, + tx_return, + ); + } + _ => {} + } + } + + /// Build a data segment from two contiguous slices (zero-copy from ring buffer). + #[inline] + pub fn build_data_from_slices<'umem>( + local_addr: IpAddress, + remote_addr: IpAddress, + local_port: u16, + remote_port: u16, + seq: u32, + ack: u32, + window: u16, + payload: (&[u8], &[u8]), + tcp_flags: u8, + ecn_ect: bool, + timestamp: Option<(u32, u32)>, + src_mac: MacAddress, + dst_mac: MacAddress, + tx_offload: bool, + free_frames: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) { + // Same options construction as build_data + let mut ts_buf = [0u8; 12]; + let tcp_options: &[u8] = if let Some((tsval, tsecr)) = timestamp { + ts_buf[0] = options::NOP; + ts_buf[1] = options::NOP; + write_timestamp_option(&mut ts_buf[2..], tsval, tsecr); + &ts_buf + } else { + &[] + }; + + match (local_addr, remote_addr) { + (IpAddress::V4(local_ip), IpAddress::V4(remote_ip)) => { + Self::build_ipv4_data_segment_slices( + local_ip, + remote_ip, + local_port, + remote_port, + seq, + ack, + tcp_flags, + window, + payload, + tcp_options, + ecn_ect, + src_mac, + dst_mac, + tx_offload, + free_frames, + tx_return, + ); + } + (IpAddress::V6(local_ip), IpAddress::V6(remote_ip)) => { + Self::build_ipv6_data_segment_slices( + local_ip, + remote_ip, + local_port, + remote_port, + seq, + ack, + tcp_flags, + window, + payload, + tcp_options, + ecn_ect, + src_mac, + dst_mac, + tx_offload, + free_frames, + tx_return, + ); + } + _ => {} + } + } + + // --- Internal helpers --- + + #[inline] + fn build_ipv4_segment<'umem>( + src_ip: Ipv4Address, + dst_ip: Ipv4Address, + src_port: u16, + dst_port: u16, + seq: u32, + ack: u32, + tcp_flags: u8, + window: u16, + tcp_options: &[u8], + src_mac: MacAddress, + dst_mac: MacAddress, + tx_offload: bool, + free_frames: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) { + let Some(mut frame) = free_frames.pop() else { + return; + }; + + // Pad options to 4-byte boundary. + let opt_padded_len = (tcp_options.len() + 3) & !3; + let tcp_header_len = TCP_HEADER_LEN + opt_padded_len; + let data_offset = (tcp_header_len / 4) as u8; + let total_ip_len = (IPV4_MIN_HEADER_LEN + tcp_header_len) as u16; + let frame_len = ETH_LEN + IPV4_MIN_HEADER_LEN + tcp_header_len; + + if frame.capacity() < frame_len { + free_frames.push(frame); + return; + } + + unsafe { frame.set_len(frame_len) }; + + // Ethernet header. + write_ethernet_header(&mut frame, dst_mac, src_mac, EtherTypes::IPv4); + + // IPv4 header. + { + let ip = &mut frame[ETH_LEN..ETH_LEN + IPV4_MIN_HEADER_LEN]; + ip.fill(0); + ip[0] = 0x45; // version=4, ihl=5 + ip[2..4].copy_from_slice(&total_ip_len.to_be_bytes()); + ip[6] = 0x40; // Don't Fragment + ip[8] = 64; // TTL + ip[9] = IpProtocols::Tcp; + let src_bytes: [u8; 4] = src_ip.into(); + ip[12..16].copy_from_slice(&src_bytes); + let dst_bytes: [u8; 4] = dst_ip.into(); + ip[16..20].copy_from_slice(&dst_bytes); + } + // Compute IPv4 header checksum. + { + let ip = crate::net::wire::ip::Ipv4Header::from_bytes_mut(&mut frame); + ip.fill_checksum(); + } + + // TCP header. + let tcp_offset = ETH_LEN + IPV4_MIN_HEADER_LEN; + Self::write_tcp_header( + &mut frame, + tcp_offset, + src_port, + dst_port, + seq, + ack, + data_offset, + tcp_flags, + window, + tcp_options, + opt_padded_len, + ); + + // TCP checksum. + if !tx_offload { + let tcp_bytes = &frame[tcp_offset..frame_len]; + let checksum = compute_tcp_checksum_from_parts(&src_ip, &dst_ip, tcp_bytes, &[]); + frame[tcp_offset + 16] = checksum[0]; + frame[tcp_offset + 17] = checksum[1]; + } + + tx_return.push(frame); + } + + #[inline] + fn build_ipv6_segment<'umem>( + src_ip: Ipv6Address, + dst_ip: Ipv6Address, + src_port: u16, + dst_port: u16, + seq: u32, + ack: u32, + tcp_flags: u8, + window: u16, + tcp_options: &[u8], + src_mac: MacAddress, + dst_mac: MacAddress, + tx_offload: bool, + free_frames: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) { + let Some(mut frame) = free_frames.pop() else { + return; + }; + + let opt_padded_len = (tcp_options.len() + 3) & !3; + let tcp_header_len = TCP_HEADER_LEN + opt_padded_len; + let data_offset = (tcp_header_len / 4) as u8; + let payload_len = tcp_header_len as u16; + let frame_len = ETH_LEN + IPV6_HEADER_LEN + tcp_header_len; + + if frame.capacity() < frame_len { + free_frames.push(frame); + return; + } + + unsafe { frame.set_len(frame_len) }; + + // Ethernet header. + write_ethernet_header(&mut frame, dst_mac, src_mac, EtherTypes::IPv6); + + // IPv6 header. + { + let ip = &mut frame[ETH_LEN..ETH_LEN + IPV6_HEADER_LEN]; + ip.fill(0); + ip[0] = 0x60; // version=6 + ip[4..6].copy_from_slice(&payload_len.to_be_bytes()); + ip[6] = IpProtocols::Tcp; // Next Header + ip[7] = 64; // Hop Limit + let src_bytes: [u8; 16] = src_ip.into(); + ip[8..24].copy_from_slice(&src_bytes); + let dst_bytes: [u8; 16] = dst_ip.into(); + ip[24..40].copy_from_slice(&dst_bytes); + } + + // TCP header. + let tcp_offset = ETH_LEN + IPV6_HEADER_LEN; + Self::write_tcp_header( + &mut frame, + tcp_offset, + src_port, + dst_port, + seq, + ack, + data_offset, + tcp_flags, + window, + tcp_options, + opt_padded_len, + ); + + // TCP checksum. + if !tx_offload { + let tcp_bytes = &frame[tcp_offset..frame_len]; + let checksum = compute_tcp_checksum_v6_from_parts(&src_ip, &dst_ip, tcp_bytes, &[]); + frame[tcp_offset + 16] = checksum[0]; + frame[tcp_offset + 17] = checksum[1]; + } + + tx_return.push(frame); + } + + #[inline] + fn build_ipv4_data_segment<'umem>( + src_ip: Ipv4Address, + dst_ip: Ipv4Address, + src_port: u16, + dst_port: u16, + seq: u32, + ack: u32, + tcp_flags: u8, + window: u16, + payload: &[u8], + tcp_options: &[u8], + src_mac: MacAddress, + dst_mac: MacAddress, + tx_offload: bool, + free_frames: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) { + let Some(mut frame) = free_frames.pop() else { + return; + }; + + let opt_padded_len = (tcp_options.len() + 3) & !3; + let tcp_header_len = TCP_HEADER_LEN + opt_padded_len; + let data_offset = (tcp_header_len / 4) as u8; + let total_ip_len = (IPV4_MIN_HEADER_LEN + tcp_header_len + payload.len()) as u16; + let frame_len = ETH_LEN + IPV4_MIN_HEADER_LEN + tcp_header_len + payload.len(); + + if frame.capacity() < frame_len { + free_frames.push(frame); + return; + } + + unsafe { frame.set_len(frame_len) }; + + // Ethernet header. + write_ethernet_header(&mut frame, dst_mac, src_mac, EtherTypes::IPv4); + + // IPv4 header. + { + let ip = &mut frame[ETH_LEN..ETH_LEN + IPV4_MIN_HEADER_LEN]; + ip.fill(0); + ip[0] = 0x45; // version=4, ihl=5 + ip[2..4].copy_from_slice(&total_ip_len.to_be_bytes()); + ip[6] = 0x40; // Don't Fragment + ip[8] = 64; // TTL + ip[9] = IpProtocols::Tcp; + let src_bytes: [u8; 4] = src_ip.into(); + ip[12..16].copy_from_slice(&src_bytes); + let dst_bytes: [u8; 4] = dst_ip.into(); + ip[16..20].copy_from_slice(&dst_bytes); + } + // Compute IPv4 header checksum. + { + let ip = crate::net::wire::ip::Ipv4Header::from_bytes_mut(&mut frame); + ip.fill_checksum(); + } + + // TCP header. + let tcp_offset = ETH_LEN + IPV4_MIN_HEADER_LEN; + Self::write_tcp_header( + &mut frame, + tcp_offset, + src_port, + dst_port, + seq, + ack, + data_offset, + tcp_flags, + window, + tcp_options, + opt_padded_len, + ); + + // Copy payload. + let payload_offset = tcp_offset + tcp_header_len; + frame[payload_offset..payload_offset + payload.len()].copy_from_slice(payload); + + // TCP checksum — must cover header (including options) + payload. + if !tx_offload { + let checksum = compute_tcp_checksum_from_parts( + &src_ip, + &dst_ip, + &frame[tcp_offset..tcp_offset + tcp_header_len], + payload, + ); + frame[tcp_offset + 16] = checksum[0]; + frame[tcp_offset + 17] = checksum[1]; + } + + tx_return.push(frame); + } + + #[inline] + fn build_ipv6_data_segment<'umem>( + src_ip: Ipv6Address, + dst_ip: Ipv6Address, + src_port: u16, + dst_port: u16, + seq: u32, + ack: u32, + tcp_flags: u8, + window: u16, + payload: &[u8], + tcp_options: &[u8], + src_mac: MacAddress, + dst_mac: MacAddress, + tx_offload: bool, + free_frames: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) { + let Some(mut frame) = free_frames.pop() else { + return; + }; + + let opt_padded_len = (tcp_options.len() + 3) & !3; + let tcp_header_len = TCP_HEADER_LEN + opt_padded_len; + let data_offset = (tcp_header_len / 4) as u8; + let ipv6_payload_len = (tcp_header_len + payload.len()) as u16; + let frame_len = ETH_LEN + IPV6_HEADER_LEN + tcp_header_len + payload.len(); + + if frame.capacity() < frame_len { + free_frames.push(frame); + return; + } + + unsafe { frame.set_len(frame_len) }; + + // Ethernet header. + write_ethernet_header(&mut frame, dst_mac, src_mac, EtherTypes::IPv6); + + // IPv6 header. + { + let ip = &mut frame[ETH_LEN..ETH_LEN + IPV6_HEADER_LEN]; + ip.fill(0); + ip[0] = 0x60; // version=6 + ip[4..6].copy_from_slice(&ipv6_payload_len.to_be_bytes()); + ip[6] = IpProtocols::Tcp; // Next Header + ip[7] = 64; // Hop Limit + let src_bytes: [u8; 16] = src_ip.into(); + ip[8..24].copy_from_slice(&src_bytes); + let dst_bytes: [u8; 16] = dst_ip.into(); + ip[24..40].copy_from_slice(&dst_bytes); + } + + // TCP header. + let tcp_offset = ETH_LEN + IPV6_HEADER_LEN; + Self::write_tcp_header( + &mut frame, + tcp_offset, + src_port, + dst_port, + seq, + ack, + data_offset, + tcp_flags, + window, + tcp_options, + opt_padded_len, + ); + + // Copy payload. + let payload_offset = tcp_offset + tcp_header_len; + frame[payload_offset..payload_offset + payload.len()].copy_from_slice(payload); + + // TCP checksum. + if !tx_offload { + let checksum = compute_tcp_checksum_v6_from_parts( + &src_ip, + &dst_ip, + &frame[tcp_offset..tcp_offset + tcp_header_len], + payload, + ); + frame[tcp_offset + 16] = checksum[0]; + frame[tcp_offset + 17] = checksum[1]; + } + + tx_return.push(frame); + } + + #[inline] + fn build_ipv4_data_segment_slices<'umem>( + src_ip: Ipv4Address, + dst_ip: Ipv4Address, + src_port: u16, + dst_port: u16, + seq: u32, + ack: u32, + tcp_flags: u8, + window: u16, + payload: (&[u8], &[u8]), + tcp_options: &[u8], + ecn_ect: bool, + src_mac: MacAddress, + dst_mac: MacAddress, + tx_offload: bool, + free_frames: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) { + let Some(mut frame) = free_frames.pop() else { + return; + }; + + let payload_len = payload.0.len() + payload.1.len(); + let opt_padded_len = (tcp_options.len() + 3) & !3; + let tcp_header_len = TCP_HEADER_LEN + opt_padded_len; + let data_offset = (tcp_header_len / 4) as u8; + let total_ip_len = (IPV4_MIN_HEADER_LEN + tcp_header_len + payload_len) as u16; + let frame_len = ETH_LEN + IPV4_MIN_HEADER_LEN + tcp_header_len + payload_len; + + if frame.capacity() < frame_len { + free_frames.push(frame); + return; + } + + unsafe { frame.set_len(frame_len) }; + + // Ethernet header. + write_ethernet_header(&mut frame, dst_mac, src_mac, EtherTypes::IPv4); + + // IPv4 header. + { + let ip = &mut frame[ETH_LEN..ETH_LEN + IPV4_MIN_HEADER_LEN]; + ip.fill(0); + ip[0] = 0x45; // version=4, ihl=5 + if ecn_ect { + ip[1] = 0x02; // ECT(0) in ToS byte + } + ip[2..4].copy_from_slice(&total_ip_len.to_be_bytes()); + ip[6] = 0x40; // Don't Fragment + ip[8] = 64; // TTL + ip[9] = IpProtocols::Tcp; + let src_bytes: [u8; 4] = src_ip.into(); + ip[12..16].copy_from_slice(&src_bytes); + let dst_bytes: [u8; 4] = dst_ip.into(); + ip[16..20].copy_from_slice(&dst_bytes); + } + // Compute IPv4 header checksum. + { + let ip = crate::net::wire::ip::Ipv4Header::from_bytes_mut(&mut frame); + ip.fill_checksum(); + } + + // TCP header. + let tcp_offset = ETH_LEN + IPV4_MIN_HEADER_LEN; + Self::write_tcp_header( + &mut frame, + tcp_offset, + src_port, + dst_port, + seq, + ack, + data_offset, + tcp_flags, + window, + tcp_options, + opt_padded_len, + ); + + // Copy payload (two slices). + let payload_start = tcp_offset + tcp_header_len; + frame[payload_start..payload_start + payload.0.len()].copy_from_slice(payload.0); + if !payload.1.is_empty() { + let p2_start = payload_start + payload.0.len(); + frame[p2_start..p2_start + payload.1.len()].copy_from_slice(payload.1); + } + + // TCP checksum — computed from the frame after both slices are written. + if !tx_offload { + let checksum = compute_tcp_checksum_from_parts( + &src_ip, + &dst_ip, + &frame[tcp_offset..tcp_offset + tcp_header_len], + &frame[payload_start..frame_len], + ); + frame[tcp_offset + 16] = checksum[0]; + frame[tcp_offset + 17] = checksum[1]; + } + + tx_return.push(frame); + } + + #[inline] + fn build_ipv6_data_segment_slices<'umem>( + src_ip: Ipv6Address, + dst_ip: Ipv6Address, + src_port: u16, + dst_port: u16, + seq: u32, + ack: u32, + tcp_flags: u8, + window: u16, + payload: (&[u8], &[u8]), + tcp_options: &[u8], + ecn_ect: bool, + src_mac: MacAddress, + dst_mac: MacAddress, + tx_offload: bool, + free_frames: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) { + let Some(mut frame) = free_frames.pop() else { + return; + }; + + let payload_len = payload.0.len() + payload.1.len(); + let opt_padded_len = (tcp_options.len() + 3) & !3; + let tcp_header_len = TCP_HEADER_LEN + opt_padded_len; + let data_offset = (tcp_header_len / 4) as u8; + let ipv6_payload_len = (tcp_header_len + payload_len) as u16; + let frame_len = ETH_LEN + IPV6_HEADER_LEN + tcp_header_len + payload_len; + + if frame.capacity() < frame_len { + free_frames.push(frame); + return; + } + + unsafe { frame.set_len(frame_len) }; + + // Ethernet header. + write_ethernet_header(&mut frame, dst_mac, src_mac, EtherTypes::IPv6); + + // IPv6 header. + { + let ip = &mut frame[ETH_LEN..ETH_LEN + IPV6_HEADER_LEN]; + ip.fill(0); + ip[0] = 0x60; // version=6 + if ecn_ect { + ip[1] |= 0x20; // ECT(0) in Traffic Class low bits + } + ip[4..6].copy_from_slice(&ipv6_payload_len.to_be_bytes()); + ip[6] = IpProtocols::Tcp; // Next Header + ip[7] = 64; // Hop Limit + let src_bytes: [u8; 16] = src_ip.into(); + ip[8..24].copy_from_slice(&src_bytes); + let dst_bytes: [u8; 16] = dst_ip.into(); + ip[24..40].copy_from_slice(&dst_bytes); + } + + // TCP header. + let tcp_offset = ETH_LEN + IPV6_HEADER_LEN; + Self::write_tcp_header( + &mut frame, + tcp_offset, + src_port, + dst_port, + seq, + ack, + data_offset, + tcp_flags, + window, + tcp_options, + opt_padded_len, + ); + + // Copy payload (two slices). + let payload_start = tcp_offset + tcp_header_len; + frame[payload_start..payload_start + payload.0.len()].copy_from_slice(payload.0); + if !payload.1.is_empty() { + let p2_start = payload_start + payload.0.len(); + frame[p2_start..p2_start + payload.1.len()].copy_from_slice(payload.1); + } + + // TCP checksum. + if !tx_offload { + let checksum = compute_tcp_checksum_v6_from_parts( + &src_ip, + &dst_ip, + &frame[tcp_offset..tcp_offset + tcp_header_len], + &frame[payload_start..frame_len], + ); + frame[tcp_offset + 16] = checksum[0]; + frame[tcp_offset + 17] = checksum[1]; + } + + tx_return.push(frame); + } + + #[inline(always)] + fn write_tcp_header( + frame: &mut Frame<'_>, + offset: usize, + src_port: u16, + dst_port: u16, + seq: u32, + ack: u32, + data_offset: u8, + tcp_flags: u8, + window: u16, + tcp_options: &[u8], + opt_padded_len: usize, + ) { + let hdr = TcpHeader::new( + src_port, + dst_port, + seq, + ack, + data_offset, + tcp_flags, + window, + [0, 0], // checksum placeholder + 0, // urgent pointer + ); + // Write the 20-byte base header. + let hdr_bytes = unsafe { + std::slice::from_raw_parts(&hdr as *const TcpHeader as *const u8, TCP_HEADER_LEN) + }; + frame[offset..offset + TCP_HEADER_LEN].copy_from_slice(hdr_bytes); + + // Write options + padding. + if !tcp_options.is_empty() { + let opt_start = offset + TCP_HEADER_LEN; + frame[opt_start..opt_start + tcp_options.len()].copy_from_slice(tcp_options); + // Zero-pad remaining bytes to 4-byte boundary. + for i in tcp_options.len()..opt_padded_len { + frame[opt_start + i] = 0; + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::net::checksum::verify_tcp_checksum; + use crate::net::wire::ip::{IPV4_MIN_HEADER_LEN, IpAddress, Ipv4Address, Ipv4Header}; + use crate::xdp::frame::{BasicFrameBuffer, Frame}; + + const ETH_HEADER_LEN: usize = 14; + + fn alloc_free_frame(addr: u64) -> Frame<'static> { + let buf = Box::leak(vec![0u8; 2048].into_boxed_slice()); + Frame::new(addr, buf, 2048, false) + } + + #[test] + fn build_data_ipv4() { + let mut free = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + free.push(alloc_free_frame(100)); + + let payload = b"Hello, TCP!"; + let src_mac = MacAddress::new([0xAA; 6]); + let dst_mac = MacAddress::new([0xBB; 6]); + + SegmentBuilder::build_data( + IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])), + IpAddress::V4(Ipv4Address::new([10, 0, 0, 2])), + 8080, + 80, + 1000, + 500, + 65535, + payload, + None, + src_mac, + dst_mac, + false, + &mut free, + &mut tx, + ); + + assert_eq!(tx.num_frames(), 1, "data segment built"); + assert_eq!(free.num_frames(), 0, "free frame consumed"); + + let frame = tx.pop().unwrap(); + // ETH(14) + IPv4(20) + TCP(20) + payload(11) = 65 + assert_eq!(frame.len(), 65); + + // Verify payload is at the end. + let payload_start = ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN + TCP_HEADER_LEN; + assert_eq!(&frame[payload_start..frame.len()], payload); + + // Verify TCP checksum. + let tcp_offset = ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN; + let ip = Ipv4Header::from_bytes(&frame); + assert!(verify_tcp_checksum( + &ip.src_addr, + &ip.dst_addr, + &frame[tcp_offset..] + )); + } + + #[test] + fn build_fin_ack_ipv4() { + let mut free = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + free.push(alloc_free_frame(100)); + + SegmentBuilder::build_fin_ack( + IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])), + IpAddress::V4(Ipv4Address::new([10, 0, 0, 2])), + 80, + 12345, + 5000, + 3000, + 65535, + None, + MacAddress::new([0xAA; 6]), + MacAddress::new([0xBB; 6]), + false, + &mut free, + &mut tx, + ); + + assert_eq!(tx.num_frames(), 1, "FIN-ACK segment built"); + assert_eq!(free.num_frames(), 0, "free frame consumed"); + + let frame = tx.pop().unwrap(); + let tcp_offset = ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN; + let tcp = unsafe { TcpHeader::from_bytes_at(&frame, tcp_offset) }; + assert_eq!(tcp.flags(), flags::ACK | flags::FIN); + assert_eq!(tcp.seq_num(), 5000); + assert_eq!(tcp.ack_num(), 3000); + } + + #[test] + fn build_data_frame_too_small() { + // Frame capacity too small for the segment. + let buf = Box::leak(vec![0u8; 40].into_boxed_slice()); + let frame = Frame::new(0, buf, 40, false); + let mut free = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + free.push(frame); + + SegmentBuilder::build_data( + IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])), + IpAddress::V4(Ipv4Address::new([10, 0, 0, 2])), + 8080, + 80, + 1000, + 500, + 65535, + b"payload", + None, + MacAddress::new([0xAA; 6]), + MacAddress::new([0xBB; 6]), + false, + &mut free, + &mut tx, + ); + + assert_eq!(tx.num_frames(), 0, "no segment built"); + assert_eq!(free.num_frames(), 1, "frame returned to free"); + } + + #[test] + fn build_data_no_free_frames() { + let mut free = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + SegmentBuilder::build_data( + IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])), + IpAddress::V4(Ipv4Address::new([10, 0, 0, 2])), + 8080, + 80, + 1000, + 500, + 65535, + b"payload", + None, + MacAddress::new([0xAA; 6]), + MacAddress::new([0xBB; 6]), + false, + &mut free, + &mut tx, + ); + + assert_eq!(tx.num_frames(), 0, "no segment built"); + } + + #[test] + fn build_ack_with_sack_blocks_ipv4() { + let mut free = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + free.push(alloc_free_frame(100)); + + let sack_blocks = [(2000u32, 2500u32), (3000u32, 3500u32)]; + let src_mac = MacAddress::new([0xAA; 6]); + let dst_mac = MacAddress::new([0xBB; 6]); + + SegmentBuilder::build_ack_with_sack( + IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])), + IpAddress::V4(Ipv4Address::new([10, 0, 0, 2])), + 8080, + 80, + 1000, + 500, + 65535, + flags::ACK, + None, + &sack_blocks, + src_mac, + dst_mac, + false, + &mut free, + &mut tx, + ); + + assert_eq!(tx.num_frames(), 1, "ACK+SACK segment built"); + assert_eq!(free.num_frames(), 0, "free frame consumed"); + + let frame = tx.pop().unwrap(); + let tcp_offset = ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN; + let tcp = unsafe { TcpHeader::from_bytes_at(&frame, tcp_offset) }; + assert_eq!(tcp.flags(), flags::ACK); + assert_eq!(tcp.seq_num(), 1000); + assert_eq!(tcp.ack_num(), 500); + + // data_offset should indicate options are present. + // SACK option: 2 + 2*8 = 18 bytes, padded to 20. + // TCP header = 20 + 20 = 40, data_offset = 10. + assert_eq!(tcp.data_offset(), 10); + + // Verify SACK option bytes: kind=5, len=18, then two (left,right) pairs. + let opt_start = tcp_offset + TCP_HEADER_LEN; + assert_eq!(frame[opt_start], 5); // SACK kind + assert_eq!(frame[opt_start + 1], 18); // SACK length: 2 + 2*8 + // First block: left=2000, right=2500. + assert_eq!( + u32::from_be_bytes([ + frame[opt_start + 2], + frame[opt_start + 3], + frame[opt_start + 4], + frame[opt_start + 5] + ]), + 2000 + ); + assert_eq!( + u32::from_be_bytes([ + frame[opt_start + 6], + frame[opt_start + 7], + frame[opt_start + 8], + frame[opt_start + 9] + ]), + 2500 + ); + + // Verify TCP checksum. + let ip = Ipv4Header::from_bytes(&frame); + assert!(verify_tcp_checksum( + &ip.src_addr, + &ip.dst_addr, + &frame[tcp_offset..] + )); + } + + #[test] + fn build_ack_with_sack_and_timestamp_ipv4() { + let mut free = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + free.push(alloc_free_frame(100)); + + let sack_blocks = [(5000u32, 5100u32)]; + + SegmentBuilder::build_ack_with_sack( + IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])), + IpAddress::V4(Ipv4Address::new([10, 0, 0, 2])), + 8080, + 80, + 1000, + 500, + 65535, + flags::ACK, + Some((100, 200)), + &sack_blocks, + MacAddress::new([0xAA; 6]), + MacAddress::new([0xBB; 6]), + false, + &mut free, + &mut tx, + ); + + assert_eq!(tx.num_frames(), 1, "ACK+TS+SACK segment built"); + + let frame = tx.pop().unwrap(); + let tcp_offset = ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN; + let tcp = unsafe { TcpHeader::from_bytes_at(&frame, tcp_offset) }; + assert_eq!(tcp.flags(), flags::ACK); + + // Options: NOP(1)+NOP(1)+TS(10) + SACK(2+1*8=10) = 22, padded to 24. + // data_offset = (20 + 24) / 4 = 11. + assert_eq!(tcp.data_offset(), 11); + + // Verify timestamp: NOP, NOP, kind=8, len=10. + let opt_start = tcp_offset + TCP_HEADER_LEN; + assert_eq!(frame[opt_start], 1); // NOP + assert_eq!(frame[opt_start + 1], 1); // NOP + assert_eq!(frame[opt_start + 2], 8); // TS kind + assert_eq!(frame[opt_start + 3], 10); // TS len + + // SACK follows at offset 12. + assert_eq!(frame[opt_start + 12], 5); // SACK kind + assert_eq!(frame[opt_start + 13], 10); // SACK len: 2 + 1*8 + + // Verify TCP checksum. + let ip = Ipv4Header::from_bytes(&frame); + assert!(verify_tcp_checksum( + &ip.src_addr, + &ip.dst_addr, + &frame[tcp_offset..] + )); + } + + #[test] + fn build_data_from_slices_produces_frame() { + let mut free = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + free.push(alloc_free_frame(100)); + + SegmentBuilder::build_data_from_slices( + IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])), + IpAddress::V4(Ipv4Address::new([10, 0, 0, 2])), + 1234, + 80, + 100, + 200, + 65535, + (b"hel", b"lo"), + flags::ACK, + false, + None, + MacAddress::broadcast(), + MacAddress::broadcast(), + false, + &mut free, + &mut tx, + ); + + assert_eq!(tx.num_frames(), 1); + + let frame = tx.pop().unwrap(); + // ETH(14) + IPv4(20) + TCP(20) + payload(5) = 59 + assert_eq!(frame.len(), 59); + + // Verify payload is correctly assembled. + let payload_start = ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN + TCP_HEADER_LEN; + assert_eq!(&frame[payload_start..frame.len()], b"hello"); + + // Verify TCP checksum. + let tcp_offset = ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN; + let ip = Ipv4Header::from_bytes(&frame); + assert!(verify_tcp_checksum( + &ip.src_addr, + &ip.dst_addr, + &frame[tcp_offset..] + )); + } + + #[test] + fn build_ack_with_empty_sack_blocks() { + let mut free = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + free.push(alloc_free_frame(100)); + + SegmentBuilder::build_ack_with_sack( + IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])), + IpAddress::V4(Ipv4Address::new([10, 0, 0, 2])), + 8080, + 80, + 1000, + 500, + 65535, + flags::ACK, + None, + &[], + MacAddress::new([0xAA; 6]), + MacAddress::new([0xBB; 6]), + false, + &mut free, + &mut tx, + ); + + assert_eq!(tx.num_frames(), 1, "plain ACK segment built"); + + let frame = tx.pop().unwrap(); + // No options: ETH(14) + IPv4(20) + TCP(20) = 54. + assert_eq!(frame.len(), 54); + let tcp_offset = ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN; + let tcp = unsafe { TcpHeader::from_bytes_at(&frame, tcp_offset) }; + assert_eq!(tcp.data_offset(), 5); // No options. + } +} diff --git a/src/net/handler/tcp/state.rs b/src/net/handler/tcp/state.rs new file mode 100644 index 0000000..bb69375 --- /dev/null +++ b/src/net/handler/tcp/state.rs @@ -0,0 +1,46 @@ +/// TCP connection state per RFC 9293. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TcpState { + Closed, + Listen, + SynSent, + SynReceived, + Established, + FinWait1, + FinWait2, + CloseWait, + Closing, + LastAck, + TimeWait, +} + +impl TcpState { + /// Returns `true` for states where the connection is synchronized + /// (Established through TimeWait). + #[inline] + pub fn is_synchronized(self) -> bool { + matches!( + self, + TcpState::Established + | TcpState::FinWait1 + | TcpState::FinWait2 + | TcpState::CloseWait + | TcpState::Closing + | TcpState::LastAck + | TcpState::TimeWait + ) + } + + /// Returns `true` when the remote side has sent a FIN. + #[inline] + pub fn is_remote_closed(self) -> bool { + matches!( + self, + TcpState::CloseWait + | TcpState::LastAck + | TcpState::TimeWait + | TcpState::Closing + | TcpState::Closed + ) + } +} diff --git a/src/net/handler/tcp/tcb.rs b/src/net/handler/tcp/tcb.rs new file mode 100644 index 0000000..ba29c60 --- /dev/null +++ b/src/net/handler/tcp/tcb.rs @@ -0,0 +1,518 @@ +use std::collections::BTreeMap; +use std::fmt; + +use coarsetime::Instant; + +use crate::net::{ + socket::LocalQueue, + wire::ip::IpAddress, +}; + +use super::congestion::CubicState; +use super::recovery::{SackRecovery, PrrState, FRtoState}; +use super::ring_buffer::RingBuffer; +use super::state::TcpState; + +/// Identifies a TCP connection by its 4-tuple. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ConnectionId { + pub local_addr: IpAddress, + pub local_port: u16, + pub remote_addr: IpAddress, + pub remote_port: u16, +} + +impl fmt::Display for ConnectionId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}:{} -> {}:{}", + self.local_addr, self.local_port, self.remote_addr, self.remote_port + ) + } +} + +/// Events delivered to user-facing TCP sockets. +#[derive(Debug, PartialEq, Eq)] +pub enum TcpEvent { + Connected, + ConnectionRefused, + Reset, + Timeout, + RemoteClose, +} + +/// Errors returned by TCP operations. +#[derive(Debug, Clone, PartialEq, Eq)] +#[allow(dead_code)] +pub enum TcpError { + ConnectionRefused, + Timeout, + Reset, + NotConnected, +} + +impl fmt::Display for TcpError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TcpError::ConnectionRefused => write!(f, "connection refused"), + TcpError::Timeout => write!(f, "connection timed out"), + TcpError::Reset => write!(f, "connection reset"), + TcpError::NotConnected => write!(f, "not connected"), + } + } +} + +/// Default receive MSS advertised by this implementation. +pub const DEFAULT_RCV_MSS: u16 = 1460; + +/// TCP timestamp option overhead: NOP + NOP + Timestamp (10 bytes) = 12 bytes. +/// When timestamps are negotiated, eff_snd_mss must be reduced by this amount +/// to avoid exceeding MTU (RFC 7323 §5.1). +pub const TS_OPTION_LEN: u16 = 12; + +/// Default receive window size. +pub const DEFAULT_RCV_WND: u16 = 65535; + +/// Default receive window scale shift count. +pub const DEFAULT_RCV_WSCALE: u8 = 7; + +/// Default delayed ACK timeout in milliseconds (RFC 9293 §4.2: < 500ms). +pub const DEFAULT_DELAYED_ACK_MS: u64 = 40; + +/// Maximum consecutive unACKed segments before flushing (RFC 5681 §4.2). +pub const MAX_DELAYED_ACK_COUNT: u8 = 2; + +/// Configuration for TCP connections. +pub struct TcpConfig { + /// Send buffer size in bytes. Must be a power of two. Default: 256KB. + pub send_buffer_size: usize, + /// Receive buffer size in bytes. Must be a power of two. Default: 256KB. + pub recv_buffer_size: usize, + /// Listener backlog. Default: 128. + pub backlog: usize, + /// Duration to remain in TIME-WAIT state in milliseconds. Default: 60000 (60s). + pub time_wait_duration_ms: u64, + /// If true, disable Nagle algorithm (send small segments immediately). Default: false. + pub tcp_no_delay: bool, + /// Maximum delay for ACKs in milliseconds. Default: 40. + pub delayed_ack_ms: u64, + /// Enable TCP keep-alive probes. Default: false. + pub keep_alive: bool, + /// Idle time before first keep-alive probe in milliseconds. Default: 7200000 (2 hours). + pub keep_alive_idle_ms: u64, + /// Interval between keep-alive probes in milliseconds. Default: 75000 (75 seconds). + pub keep_alive_interval_ms: u64, + /// Max probes before aborting connection. Default: 9. + pub keep_alive_count: u8, + /// SO_LINGER setting. None = off (default), Some(0) = RST, Some(ms) = timeout. + pub linger: Option, + /// Enable TCP timestamps (RFC 7323). Default: true. + pub timestamps: bool, + /// Enable SACK (RFC 2018). Default: true. + pub sack: bool, + /// Enable ECN (RFC 3168). Default: true. + pub ecn: bool, +} + +impl Default for TcpConfig { + fn default() -> Self { + Self { + send_buffer_size: 256 * 1024, + recv_buffer_size: 256 * 1024, + backlog: 128, + time_wait_duration_ms: 60_000, + tcp_no_delay: false, + delayed_ack_ms: DEFAULT_DELAYED_ACK_MS, + keep_alive: false, + keep_alive_idle_ms: 7_200_000, + keep_alive_interval_ms: 75_000, + keep_alive_count: 9, + linger: None, + timestamps: true, + sack: true, + ecn: true, + } + } +} + +/// Transmission Control Block — per-connection state. +/// +/// Fields are ordered by access frequency to maximize L1d cache locality. +/// Hot fields (accessed every packet) are packed into the first 1-2 cache lines. +pub struct Tcb { + // === HOT: accessed every packet === + + /// Next sequence number expected on incoming segments. + pub rcv_nxt: u32, + /// Next sequence number to send. + pub snd_nxt: u32, + /// Oldest unacknowledged sequence number. + pub snd_una: u32, + /// Send window. + pub snd_wnd: u32, + pub state: TcpState, + /// True when an ACK is owed but deferred. + pub ack_pending: bool, + /// Count of consecutive unACKed segments (flush at MAX_DELAYED_ACK_COUNT). + pub ack_delay_count: u8, + /// Whether timestamps were negotiated. + pub ts_enabled: bool, + /// Whether SACK was negotiated. + pub sack_enabled: bool, + /// Whether ECN was negotiated for this connection. + pub ecn_enabled: bool, + /// True when a CE-marked segment has been received and needs to be echoed. + pub ecn_ce_received: bool, + /// Whether window scaling is enabled for this connection. + pub wscale_enabled: bool, + /// Effective send MSS (min of snd_mss and path MTU constraints). + pub eff_snd_mss: u16, + /// Send window scale factor (shift count from remote). + pub snd_wscale: u8, + /// Receive window scale factor (our shift count). + pub rcv_wscale: u8, + /// MSS we advertise to the remote peer. + pub rcv_mss: u16, + /// When true, the Nagle algorithm gates small sends. Disabled by TCP_NODELAY. + pub nagle_enabled: bool, + /// True if this connection originated from a passive open (LISTEN). + pub from_passive_open: bool, + + // === WARM: accessed most packets (data transfer) === + + pub id: ConnectionId, + /// Send ring buffer — user data is copied in, segments built from here. + pub send_buffer: RingBuffer, + /// Receive ring buffer — incoming payload copied here, user reads from here. + pub recv_buffer: RingBuffer, + /// Most recent TSval received from peer. + pub ts_recent: u32, + /// Deadline for sending the deferred ACK. + pub delayed_ack_deadline: Option, + /// Delayed ACK timeout in milliseconds. + pub delayed_ack_ms: u64, + + // === ACK processing === + + /// Segment sequence number used for last window update. + pub snd_wl1: u32, + /// Segment acknowledgment number used for last window update. + pub snd_wl2: u32, + /// Largest send window ever advertised by peer (for sender SWS). + pub max_snd_wnd: u32, + /// Right edge of last advertised receive window (rcv_nxt + wnd at ACK time). + pub last_advertised_right_edge: u32, + /// Initial send sequence number. + pub iss: u32, + /// Initial receive sequence number. + pub irs: u32, + /// Receive window. + pub rcv_wnd: u32, + /// MSS advertised by the remote peer. + pub snd_mss: u16, + + // === Congestion/recovery === + + pub cubic: CubicState, + pub recovery: SackRecovery, + pub prr: PrrState, + pub frto: FRtoState, + + // === RTT estimation (RFC 6298) === + + /// Smoothed RTT in milliseconds. + pub srtt: Option, + /// RTT variance in milliseconds. + pub rttvar: u64, + /// Retransmission timeout in milliseconds (computed from srtt/rttvar). + pub rto: u64, + /// Timestamp of last data segment sent (for RTT measurement). + pub last_send_time: Option, + /// When ts_recent was last updated. + pub ts_recent_age: Instant, + /// Base instant for deriving our monotonic timestamp clock. + pub ts_offset: Instant, + + // === COLD: rarely accessed === + + /// Deadline for retransmitting unacknowledged SYN or SYN-ACK. + pub retransmit_deadline: Option, + /// Exponential backoff counter for retransmissions. + pub rto_backoff: u8, + /// Queue for delivering events to user-facing socket. + pub event_queue: LocalQueue, + /// Out-of-order receive ranges: seq -> byte_length (metadata only). + pub ooo_ranges: BTreeMap, + /// Scoreboard: byte ranges the peer has confirmed receiving (left_edge -> right_edge). + pub sack_scoreboard: BTreeMap, + /// True when CWR has been sent and is awaiting acknowledgment. + pub ecn_cwr_sent: bool, + /// True when a FIN needs to be sent. + pub pending_fin: bool, + /// Sequence number of our FIN (set when FIN is sent). + pub fin_seq: Option, + /// Deadline for exiting TIME-WAIT state. + pub time_wait_deadline: Option, + /// Duration to remain in TIME-WAIT state in milliseconds. + pub time_wait_duration: u64, + /// Deadline for next zero-window probe. + pub persist_deadline: Option, + /// Exponential backoff counter for persist probes (cap at 6). + pub persist_backoff: u8, + pub keep_alive_enabled: bool, + pub keep_alive_idle_ms: u64, + pub keep_alive_interval_ms: u64, + pub keep_alive_count: u8, + pub last_activity: Instant, + pub keep_alive_probes_sent: u8, + pub linger: Option, + pub linger_deadline: Option, +} + +impl Tcb { + /// Compute SEG.LEN for a segment: data octets + SYN/FIN contribution. + #[inline] + pub fn seg_len(data_len: usize, flags: u8) -> u32 { + let mut len = data_len as u32; + if flags & crate::net::wire::tcp::flags::SYN != 0 { + len += 1; + } + if flags & crate::net::wire::tcp::flags::FIN != 0 { + len += 1; + } + len + } + + /// Compute the window value to advertise in outgoing segments. + /// Downscales by `rcv_wscale` if window scaling is enabled. + /// Applies receiver SWS avoidance (MUST-39): don't open the window + /// until we can advertise at least min(MSS, buffer/2) of new space. + #[inline] + pub fn advertised_window(&self) -> u16 { + let free = self.recv_buffer.free_space(); + let threshold = (self.rcv_mss as usize).min(self.recv_buffer.capacity() / 2); + + // SWS avoidance (MUST-39): don't open the window until we can + // advertise at least min(MSS, buffer/2) of new space. + let right_edge = self.rcv_nxt.wrapping_add(free as u32); + let prev_right_edge = self.last_advertised_right_edge; + + let effective_free = if prev_right_edge != 0 { + let new_space = right_edge.wrapping_sub(prev_right_edge) as i32; + if new_space > 0 && (new_space as usize) < threshold { + // Not enough new space — clamp to previous right edge. + let clamped = prev_right_edge.wrapping_sub(self.rcv_nxt); + (clamped as usize).min(free) + } else { + free + } + } else { + free + }; + + if self.wscale_enabled { + (effective_free >> self.rcv_wscale as usize).min(u16::MAX as usize) as u16 + } else { + effective_free.min(u16::MAX as usize) as u16 + } + } + + /// Update the last advertised right edge after sending an ACK. + /// Call this after any path that sends an ACK with our advertised window. + #[inline] + pub fn update_advertised_edge(&mut self) { + self.last_advertised_right_edge = + self.rcv_nxt.wrapping_add(self.recv_buffer.free_space() as u32); + } + + /// Build the timestamp option tuple using a pre-computed tsval. + /// `ts_recent` is read at call time since it may change during segment processing. + #[inline(always)] + pub(super) fn ts_option(&self, tsval: u32) -> Option<(u32, u32)> { + if self.ts_enabled { + Some((tsval, self.ts_recent)) + } else { + None + } + } + + /// Scale an incoming window value by `snd_wscale`. + /// Only call after SYN exchange (Established onward). + #[inline] + pub fn scale_incoming_window(&self, raw_wnd: u32) -> u32 { + if self.wscale_enabled { + raw_wnd << self.snd_wscale as u32 + } else { + raw_wnd + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use super::super::ring_buffer::RingBuffer; + + fn make_tcb(wscale_enabled: bool, snd_wscale: u8, rcv_wscale: u8, recv_buf_size: usize) -> Tcb { + Tcb { + id: ConnectionId { + local_addr: IpAddress::V4(crate::net::wire::ip::Ipv4Address { octets: [127, 0, 0, 1] }), + local_port: 1234, + remote_addr: IpAddress::V4(crate::net::wire::ip::Ipv4Address { octets: [127, 0, 0, 1] }), + remote_port: 5678, + }, + state: TcpState::Established, + from_passive_open: false, + iss: 0, + snd_una: 0, + snd_nxt: 0, + snd_wnd: 0, + snd_wl1: 0, + snd_wl2: 0, + irs: 0, + rcv_nxt: 0, + rcv_wnd: 0, + snd_mss: DEFAULT_RCV_MSS, + rcv_mss: DEFAULT_RCV_MSS, + eff_snd_mss: DEFAULT_RCV_MSS, + snd_wscale, + rcv_wscale, + wscale_enabled, + retransmit_deadline: None, + rto_backoff: 0, + event_queue: LocalQueue::new(16), + send_buffer: RingBuffer::new(1024), + recv_buffer: RingBuffer::new(recv_buf_size), + ooo_ranges: BTreeMap::new(), + cubic: CubicState::new(DEFAULT_RCV_MSS), + recovery: SackRecovery::new(), + prr: PrrState::new(), + frto: FRtoState::new(), + srtt: None, + rttvar: 0, + rto: 1000, + last_send_time: None, + pending_fin: false, + fin_seq: None, + time_wait_deadline: None, + time_wait_duration: 60_000, + ack_pending: false, + delayed_ack_deadline: None, + ack_delay_count: 0, + delayed_ack_ms: DEFAULT_DELAYED_ACK_MS, + nagle_enabled: true, + keep_alive_enabled: false, + keep_alive_idle_ms: 7_200_000, + keep_alive_interval_ms: 75_000, + keep_alive_count: 9, + last_activity: Instant::now(), + keep_alive_probes_sent: 0, + linger: None, + linger_deadline: None, + ts_enabled: false, + ts_recent: 0, + ts_recent_age: Instant::now(), + ts_offset: Instant::now(), + sack_enabled: false, + sack_scoreboard: BTreeMap::new(), + ecn_enabled: false, + ecn_ce_received: false, + ecn_cwr_sent: false, + persist_deadline: None, + persist_backoff: 0, + max_snd_wnd: 0, + last_advertised_right_edge: 0, + } + } + + #[test] + fn advertised_window_no_scaling() { + // recv_buffer capacity 1024, all free → capped at 1024 (fits in u16) + let tcb = make_tcb(false, 0, 0, 1024); + assert_eq!(tcb.advertised_window(), 1024); + } + + #[test] + fn advertised_window_with_scaling() { + // recv_buffer capacity 1024, rcv_wscale = 2 → 1024 >> 2 = 256 + let tcb = make_tcb(true, 0, 2, 1024); + assert_eq!(tcb.advertised_window(), 256); + } + + #[test] + fn scale_incoming_window_no_scaling() { + let tcb = make_tcb(false, 0, 0, 1024); + assert_eq!(tcb.scale_incoming_window(500), 500); + } + + #[test] + fn scale_incoming_window_with_scaling() { + // snd_wscale = 3 → 500 << 3 = 4000 + let tcb = make_tcb(true, 3, 0, 1024); + assert_eq!(tcb.scale_incoming_window(500), 4000); + } + + #[test] + fn receiver_sws_avoidance_holds_small_window_opens() { + // Buffer capacity = 64, rcv_mss = 1460. + // threshold = min(1460, 64/2) = 32. + let mut tcb = make_tcb(false, 0, 0, 64); + tcb.rcv_nxt = 1000; + + // Initially, no previous right edge — full window advertised. + assert_eq!(tcb.advertised_window(), 64); + + // Simulate: buffer was full, we advertised right edge at rcv_nxt + 64. + tcb.last_advertised_right_edge = 1000u32.wrapping_add(64); + + // Fill 48 bytes of the buffer, leaving 16 bytes free. + tcb.recv_buffer.write(&[0u8; 48]); + + // Free space = 16, new_space = (1000+16) - (1000+64) = -48 (negative, no new space). + // Since new_space is not positive, effective_free = free = 16. + assert_eq!(tcb.advertised_window(), 16); + + // Now simulate the app reads 48 bytes → buffer is fully free (64 bytes). + let mut drain = [0u8; 48]; + tcb.recv_buffer.read(&mut drain); + + // Free space = 64, right_edge = 1000+64, prev = 1064. + // new_space = 1064 - 1064 = 0, not > 0 → effective_free = 64. + assert_eq!(tcb.advertised_window(), 64); + + // Now advance rcv_nxt by 48 (data was received and consumed). + tcb.rcv_nxt = 1048; + // Fill 54 bytes leaving 10 free. + tcb.recv_buffer.write(&[0u8; 54]); + // Free space = 10, right_edge = 1048+10 = 1058, prev = 1064. + // new_space = 1058 - 1064 = -6 (negative) → effective_free = 10. + assert_eq!(tcb.advertised_window(), 10); + + // App reads 10 bytes → 20 free. + let mut drain2 = [0u8; 10]; + tcb.recv_buffer.read(&mut drain2); + // Free = 20, right_edge = 1048+20 = 1068, prev = 1064. + // new_space = 1068 - 1064 = 4 < threshold(32) → clamped. + // clamped = 1064 - 1048 = 16. min(16, 20) = 16. + assert_eq!(tcb.advertised_window(), 16, "SWS holds: only 4 bytes of new space < threshold 32"); + + // App reads 34 more bytes → 54 free. + let mut drain3 = [0u8; 34]; + tcb.recv_buffer.read(&mut drain3); + // Free = 54, right_edge = 1048+54 = 1102, prev = 1064. + // new_space = 1102 - 1064 = 38 >= threshold(32) → opens. + assert_eq!(tcb.advertised_window(), 54, "SWS opens: 38 bytes of new space >= threshold 32"); + } + + #[test] + fn update_advertised_edge_sets_right_edge() { + let mut tcb = make_tcb(false, 0, 0, 128); + tcb.rcv_nxt = 5000; + assert_eq!(tcb.last_advertised_right_edge, 0); + + tcb.update_advertised_edge(); + // free_space = 128 (empty buffer). + assert_eq!(tcb.last_advertised_right_edge, 5000u32.wrapping_add(128)); + } +} diff --git a/src/net/handler/tcp/tests/congestion_tests.rs b/src/net/handler/tcp/tests/congestion_tests.rs new file mode 100644 index 0000000..34e3304 --- /dev/null +++ b/src/net/handler/tcp/tests/congestion_tests.rs @@ -0,0 +1,249 @@ +use super::*; + +#[test] +fn cubic_slow_start_on_new_ack() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + + let cwnd_before = handler.connections[0].cubic.cwnd; + let mss = handler.connections[0].eff_snd_mss; + + // Send data and get it ACKed. + handler.connections[0].send_buffer.write(&[0xAA; 1460]); + handler.connections[0].snd_wnd = 65535; + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + while tx.pop().is_some() {} + + // ACK the data. + let snd_nxt = handler.connections[0].snd_nxt; + let ack = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + snd_nxt, + flags::ACK, + 65535, + &[], + ); + let ack_len = ack.len(); + handler.process_ipv4( + Frame::new(2, leak(ack), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // In slow start: cwnd should increase by MSS (CUBIC slow start same as Reno). + let cwnd_after = handler.connections[0].cubic.cwnd; + assert_eq!( + cwnd_after, + cwnd_before + mss as u32, + "slow start: cwnd += MSS" + ); +} + +#[test] +fn frto_restores_cwnd_on_spurious_rto() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(64); + let mut rx = BasicFrameBuffer::new(64); + let mut tx = BasicFrameBuffer::new(64); + for i in 0..32 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + + // Send 2 MSS of data (poll_send sends 1 MSS per call). + let mss = handler.connections[0].eff_snd_mss as usize; + handler.connections[0] + .send_buffer + .write(&vec![0xAA; mss * 2]); + handler.connections[0].snd_wnd = 65535; + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + while tx.pop().is_some() {} + + let cwnd_before_rto = handler.connections[0].cubic.cwnd; + + // Trigger RTO by setting deadline in the past. + handler.connections[0].retransmit_deadline = Some(now); + let rto_time = now + coarsetime::Duration::from_millis(1100); + handler.poll_timers(rto_time, nh.local_mac(), &nh, &mut free, &mut tx); + while tx.pop().is_some() {} + + // F-RTO should be active. + assert!( + handler.connections[0].frto.is_active(), + "F-RTO should be in Step1" + ); + assert_eq!( + handler.connections[0].cubic.cwnd, mss as u32, + "cwnd should be 1 MSS after RTO" + ); + + // First ACK advances snd_una. + let snd_una = handler.connections[0].snd_una; + let ack1_seq = snd_una.wrapping_add(mss as u32); + let ack1 = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + ack1_seq, + flags::ACK, + 65535, + &[], + ); + let ack1_len = ack1.len(); + handler.process_ipv4( + Frame::new(10, leak(ack1), ack1_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + assert!( + handler.connections[0].frto.is_active(), + "F-RTO should be in Step2" + ); + + // Second ACK advances snd_una again => spurious RTO. + let snd_una = handler.connections[0].snd_una; + let ack2_seq = snd_una.wrapping_add(mss as u32); + let ack2 = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + ack2_seq, + flags::ACK, + 65535, + &[], + ); + let ack2_len = ack2.len(); + handler.process_ipv4( + Frame::new(11, leak(ack2), ack2_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // cwnd should be restored. + assert!( + !handler.connections[0].frto.is_active(), + "F-RTO should be disabled" + ); + assert_eq!( + handler.connections[0].cubic.cwnd, cwnd_before_rto, + "cwnd should be restored after spurious RTO" + ); +} + diff --git a/src/net/handler/tcp/tests/data_transfer.rs b/src/net/handler/tcp/tests/data_transfer.rs new file mode 100644 index 0000000..e74d452 --- /dev/null +++ b/src/net/handler/tcp/tests/data_transfer.rs @@ -0,0 +1,668 @@ +use super::*; + +#[test] +fn established_receives_in_order_data() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(16); + let mut rx = BasicFrameBuffer::new(16); + let mut tx = BasicFrameBuffer::new(16); + + for i in 0..8 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete the handshake. + let accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(handler.connections[0].state, TcpState::Established); + + // Clear tx from handshake. + while tx.pop().is_some() {} + + // Send first data segment (deferred by delayed ACK). + let payload = b"Hello, TCP!"; + let data = build_tcp_frame_with_payload( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + payload, + ); + let data_len = data.len(); + handler.process_ipv4( + Frame::new(2, leak(data), data_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // First segment deferred — no immediate ACK. + assert_eq!( + tx.num_frames(), + 0, + "ACK deferred for first in-order segment" + ); + assert!( + handler.connections[0].ack_pending, + "ack_pending should be true" + ); + + // Send second data segment to flush delayed ACK. + let payload2 = b"World!"; + let data2 = build_tcp_frame_with_payload( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001 + payload.len() as u32, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + payload2, + ); + let data2_len = data2.len(); + handler.process_ipv4( + Frame::new(3, leak(data2), data2_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // ACK is now deferred to poll_send for piggyback opportunity. + assert_eq!(tx.num_frames(), 0, "ACK deferred until poll_send"); + assert!(handler.connections[0].ack_pending, "ack_pending should be true"); + + // poll_send generates pure ACK since no data to piggyback. + handler.poll_send(coarsetime::Instant::now(), nh.local_mac(), &nh, &mut free, &mut tx); + assert_eq!(tx.num_frames(), 1, "ACK flushed by poll_send"); + + // Verify: data is in the receive ring buffer. + let tcb = &handler.connections[0]; + assert_eq!(tcb.recv_buffer.available(), payload.len() + payload2.len()); + assert_eq!( + tcb.rcv_nxt, + 1001 + payload.len() as u32 + payload2.len() as u32 + ); + + drop(accept_queue); +} + +#[test] +fn established_out_of_order_reassembly() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + + // Send segment 2 first (out of order): seq=1006, 5 bytes "world". + let seg2 = build_tcp_frame_with_payload( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1006, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + b"world", + ); + let seg2_len = seg2.len(); + handler.process_ipv4( + Frame::new(2, leak(seg2), seg2_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!( + handler.connections[0].rcv_nxt, 1001, + "rcv_nxt not advanced for OOO" + ); + assert_eq!(handler.connections[0].ooo_ranges.len(), 1); + + // Now send segment 1 (fills the gap): seq=1001, 5 bytes "hello". + let seg1 = build_tcp_frame_with_payload( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + b"hello", + ); + let seg1_len = seg1.len(); + handler.process_ipv4( + Frame::new(3, leak(seg1), seg1_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // Both segments should now be contiguous. + assert_eq!( + handler.connections[0].rcv_nxt, 1011, + "rcv_nxt advanced past both segments" + ); + assert_eq!( + handler.connections[0].ooo_ranges.len(), + 0, + "OOO ranges drained" + ); + assert_eq!(handler.connections[0].recv_buffer.available(), 10); + + // Read from recv buffer and verify contents. + let mut buf = [0u8; 10]; + handler.connections[0].recv_buffer.read(&mut buf); + assert_eq!(&buf, b"helloworld"); +} + +#[test] +fn poll_send_builds_data_segment() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + + // Write data into the connection's send buffer. + let payload = b"Hello from server!"; + handler.connections[0].send_buffer.write(payload); + + // Set snd_wnd so the window allows sending. + handler.connections[0].snd_wnd = 65535; + + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + + assert_eq!(tx.num_frames(), 1, "data segment built"); + let tcb = &handler.connections[0]; + assert_eq!( + tcb.snd_nxt, + server_iss + .wrapping_add(1) + .wrapping_add(payload.len() as u32) + ); + assert_eq!(tcb.send_buffer.available(), payload.len()); // still in buffer until ACKed +} + +#[test] +fn frame_accounting_through_data_transfer() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(64); + let mut rx = BasicFrameBuffer::new(64); + let mut tx = BasicFrameBuffer::new(64); + + for i in 0..32 { + free.push(alloc_free_frame(100 + i)); + } + + let initial_total = free.num_frames(); + + // Handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn.len(); + handler.process_ipv4( + Frame::new(0, leak(syn), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack.len(); + handler.process_ipv4( + Frame::new(1, leak(ack), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // Data segment 1 (ACK deferred). + let data = build_tcp_frame_with_payload( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + b"test data", + ); + let data_len = data.len(); + handler.process_ipv4( + Frame::new(2, leak(data), data_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // Data segment 2 (flushes delayed ACK). + let data2 = build_tcp_frame_with_payload( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001 + 9, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + b"more data", + ); + let data2_len = data2.len(); + handler.process_ipv4( + Frame::new(3, leak(data2), data2_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // All frames accounted for: free + rx + tx = initial + incoming frames. + let total = free.num_frames() + rx.num_frames() + tx.num_frames(); + // We started with initial_total free frames and injected 4 incoming frames. + assert_eq!(total, initial_total + 4, "all frames accounted for"); +} + +#[test] +fn poll_send_sets_psh_on_last_segment() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + let _server_iss = establish_connection(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + // Write small data (< MSS) into send buffer. + handler.connections[0].send_buffer.write(b"Hello"); + + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + + assert_eq!(tx.num_frames(), 1, "expected one data segment"); + let frame = tx.pop().unwrap(); + + // TCP flags byte is at offset ETH(14) + IPv4(20) + 13 = 47. + let tcp_flags_offset = ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN + 13; + let tcp_flags_byte = frame[tcp_flags_offset]; + assert!( + tcp_flags_byte & flags::PSH != 0, + "PSH flag should be set on last (only) data segment, got flags: {:#04x}", + tcp_flags_byte + ); + assert!( + tcp_flags_byte & flags::ACK != 0, + "ACK flag should also be set, got flags: {:#04x}", + tcp_flags_byte + ); +} + +#[test] +fn poll_send_no_psh_on_first_segment_when_more_data() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + // Allocate frames large enough for MSS-sized segments (ETH+IP+TCP+1460). + for i in 0..16 { + let buf = leak(vec![0u8; 2048]); + free.push(Frame::new(2000 + i, buf, 2048, false)); + } + + let _server_iss = establish_connection(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + // Write more than 1 MSS of data. + let mss = handler.connections[0].eff_snd_mss as usize; + let big_data = vec![0x41u8; mss + 100]; + handler.connections[0].send_buffer.write(&big_data); + + // Ensure cwnd is large enough to allow sending. + handler.connections[0].cubic.cwnd = (mss as u32) * 10; + // Disable Nagle so the second (sub-MSS) segment can be sent. + handler.connections[0].nagle_enabled = false; + + let now = coarsetime::Instant::now(); + // poll_send now sends all segments in one call (fills the window). + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + + assert_eq!( + tx.num_frames(), + 2, + "expected two data segments from poll_send" + ); + + let first_frame = tx.pop().unwrap(); + let tcp_flags_offset = ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN + 13; + let first_flags = first_frame[tcp_flags_offset]; + assert!( + first_flags & flags::PSH == 0, + "PSH should NOT be set on first segment when more data remains, got flags: {:#04x}", + first_flags + ); + assert!( + first_flags & flags::ACK != 0, + "ACK flag should be set, got flags: {:#04x}", + first_flags + ); + + // Second frame: remaining 100 bytes, no more data -> PSH set. + let second_frame = tx.pop().unwrap(); + let second_flags = second_frame[tcp_flags_offset]; + assert!( + second_flags & flags::PSH != 0, + "PSH should be set on last segment, got flags: {:#04x}", + second_flags + ); +} + +#[test] +fn rcv_nxt_advances_only_by_bytes_written_to_recv_buffer() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(16); + let mut rx = BasicFrameBuffer::new(16); + let mut tx = BasicFrameBuffer::new(16); + + for i in 0..8 { + free.push(alloc_free_frame(100 + i)); + } + + // Listen with a tiny recv buffer (32 bytes, must be power of two). + let config = TcpConfig { + recv_buffer_size: 32, + backlog: 128, + timestamps: false, + sack: false, + ecn: false, + ..TcpConfig::default() + }; + let _accept_queue = handler + .listen_with_config(IpAddress::V4(LOCAL_IP), 80, config) + .unwrap(); + + // SYN from remote. + let syn = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn.len(); + handler.process_ipv4( + Frame::new(0, leak(syn), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + + // ACK to complete handshake. + let ack = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack.len(); + handler.process_ipv4( + Frame::new(1, leak(ack), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(handler.connections[0].state, TcpState::Established); + while tx.pop().is_some() {} + + // Fill 20 of 32 bytes in the recv buffer directly, leaving 12 free. + let filler = [0xAA_u8; 20]; + handler.connections[0].recv_buffer.write(&filler); + // Advance rcv_nxt to account for the filler (as if received normally). + handler.connections[0].rcv_nxt = handler.connections[0].rcv_nxt.wrapping_add(20); + let rcv_nxt_before = handler.connections[0].rcv_nxt; + + // Send a 20-byte payload — only 12 should fit. + let payload = [0xBB_u8; 20]; + let data = build_tcp_frame_with_payload( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + rcv_nxt_before, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + &payload, + ); + let data_len = data.len(); + handler.process_ipv4( + Frame::new(2, leak(data), data_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // rcv_nxt must advance by only 12 (the bytes actually written), not 20. + let rcv_nxt_after = handler.connections[0].rcv_nxt; + let advanced = rcv_nxt_after.wrapping_sub(rcv_nxt_before); + assert_eq!( + advanced, 12, + "rcv_nxt should advance by bytes written (12), not payload len (20); got {}", + advanced + ); + assert_eq!( + handler.connections[0].recv_buffer.available(), + 32, + "recv buffer should be completely full (20 filler + 12 new)" + ); +} + diff --git a/src/net/handler/tcp/tests/delayed_ack.rs b/src/net/handler/tcp/tests/delayed_ack.rs new file mode 100644 index 0000000..6016a82 --- /dev/null +++ b/src/net/handler/tcp/tests/delayed_ack.rs @@ -0,0 +1,646 @@ +use super::*; + +#[test] +fn delayed_ack_defers_ack_for_in_order_data() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(16); + let mut rx = BasicFrameBuffer::new(16); + let mut tx = BasicFrameBuffer::new(16); + + for i in 0..8 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + + // Send one in-order data segment. + let payload = b"hello"; + let data = build_tcp_frame_with_payload( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + payload, + ); + let data_len = data.len(); + handler.process_ipv4( + Frame::new(2, leak(data), data_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // No immediate ACK — deferred. + assert_eq!(tx.num_frames(), 0, "ACK should be deferred"); + let tcb = &handler.connections[0]; + assert!(tcb.ack_pending, "ack_pending should be true"); + assert!( + tcb.delayed_ack_deadline.is_some(), + "delayed_ack_deadline should be set" + ); + assert_eq!(tcb.ack_delay_count, 1, "ack_delay_count should be 1"); +} + +#[test] +fn delayed_ack_flushes_on_second_segment() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(16); + let mut rx = BasicFrameBuffer::new(16); + let mut tx = BasicFrameBuffer::new(16); + + for i in 0..8 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + + // First in-order segment — deferred. + let seg1 = build_tcp_frame_with_payload( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + b"aaaaa", + ); + let seg1_len = seg1.len(); + handler.process_ipv4( + Frame::new(2, leak(seg1), seg1_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(tx.num_frames(), 0, "first segment deferred"); + + // Second in-order segment — flushes ACK. + let seg2 = build_tcp_frame_with_payload( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1006, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + b"bbbbb", + ); + let seg2_len = seg2.len(); + handler.process_ipv4( + Frame::new(3, leak(seg2), seg2_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + // ACK is now deferred to poll_send for piggyback opportunity. + assert_eq!(tx.num_frames(), 0, "ACK deferred until poll_send"); + assert!(handler.connections[0].ack_pending, "ack_pending should be true after second segment"); + + // poll_send generates pure ACK since no data to piggyback. + handler.poll_send(coarsetime::Instant::now(), nh.local_mac(), &nh, &mut free, &mut tx); + assert_eq!(tx.num_frames(), 1, "poll_send flushes ACK"); + + let tcb = &handler.connections[0]; + assert!(!tcb.ack_pending, "ack_pending should be false after flush"); + assert_eq!( + tcb.ack_delay_count, 0, + "ack_delay_count should be 0 after flush" + ); +} + +#[test] +fn out_of_order_data_sends_immediate_ack() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(16); + let mut rx = BasicFrameBuffer::new(16); + let mut tx = BasicFrameBuffer::new(16); + + for i in 0..8 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + + // Send out-of-order data (skip sequence numbers). + let ooo_data = build_tcp_frame_with_payload( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1011, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + b"ooo", + ); + let ooo_len = ooo_data.len(); + handler.process_ipv4( + Frame::new(2, leak(ooo_data), ooo_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + assert_eq!( + tx.num_frames(), + 1, + "out-of-order data triggers immediate ACK" + ); +} + +#[test] +fn fin_sends_immediate_ack() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(16); + let mut rx = BasicFrameBuffer::new(16); + let mut tx = BasicFrameBuffer::new(16); + + for i in 0..8 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + + // Send FIN. + let fin_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK | flags::FIN, + 65535, + &[], + ); + let fin_len = fin_data.len(); + handler.process_ipv4( + Frame::new(2, leak(fin_data), fin_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + assert_eq!(tx.num_frames(), 1, "FIN triggers immediate ACK"); + assert_eq!(handler.connections[0].state, TcpState::CloseWait); +} + +#[test] +fn new_connection_has_delayed_ack_fields() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(8); + let mut rx = BasicFrameBuffer::new(8); + let mut tx = BasicFrameBuffer::new(8); + + for i in 0..4 { + free.push(alloc_free_frame(100 + i)); + } + + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + + // Step 1: SYN. + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + let syn_frame = Frame::new(0, leak(syn_data), syn_len, false); + handler.process_ipv4(syn_frame, coarsetime::Instant::now(), &nh, &mut free, &mut rx, &mut tx); + assert_eq!(handler.connections[0].state, TcpState::SynReceived); + + let server_iss = handler.connections[0].iss; + + // Step 2: ACK completing handshake. + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + let ack_frame = Frame::new(1, leak(ack_data), ack_len, false); + handler.process_ipv4(ack_frame, coarsetime::Instant::now(), &nh, &mut free, &mut rx, &mut tx); + + assert_eq!(handler.connections[0].state, TcpState::Established); + + // Verify delayed ACK and Nagle defaults. + let tcb = &handler.connections[0]; + assert!(!tcb.ack_pending, "ack_pending should be false"); + assert!( + tcb.delayed_ack_deadline.is_none(), + "delayed_ack_deadline should be None" + ); + assert_eq!(tcb.ack_delay_count, 0, "ack_delay_count should be 0"); + assert_eq!( + tcb.delayed_ack_ms, + tcb::DEFAULT_DELAYED_ACK_MS, + "delayed_ack_ms should match default" + ); + assert!(tcb.nagle_enabled, "nagle should be enabled by default"); +} + +#[test] +fn delayed_ack_timer_flushes_pending_ack() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + let now = coarsetime::Instant::now(); + + // Complete handshake. + let _accept = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + + // Manually set delayed ACK state on the TCB. + let id = ConnectionId { + local_addr: IpAddress::V4(LOCAL_IP), + local_port: 80, + remote_addr: IpAddress::V4(REMOTE_IP), + remote_port: 12345, + }; + { + let tcb = handler.get_connection_mut(&id).unwrap(); + tcb.ack_pending = true; + tcb.ack_delay_count = 1; + tcb.delayed_ack_deadline = Some(now + coarsetime::Duration::from_millis(40)); + } + + // Before deadline — should NOT flush. + handler.poll_timers(now, nh.local_mac(), &nh, &mut free, &mut tx); + assert_eq!(tx.num_frames(), 0, "should not flush before deadline"); + + // After deadline — should flush. + let later = now + coarsetime::Duration::from_millis(50); + handler.poll_timers(later, nh.local_mac(), &nh, &mut free, &mut tx); + assert_eq!(tx.num_frames(), 1, "should flush after deadline"); + + let tcb = handler.get_connection(&id).unwrap(); + assert!(!tcb.ack_pending); + assert_eq!(tcb.ack_delay_count, 0); + assert!(tcb.delayed_ack_deadline.is_none()); +} + +#[test] +fn data_send_clears_delayed_ack() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // 1. Complete handshake via passive open (listener). + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + + // 2. Receive in-order data — ack_pending becomes true. + let data_seg = build_tcp_frame_with_payload( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + b"incoming data", + ); + let data_len = data_seg.len(); + handler.process_ipv4( + Frame::new(2, leak(data_seg), data_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} // consume any immediate ACK frames + + assert!( + handler.connections[0].ack_pending, + "ack_pending should be true after receiving data" + ); + + // 3. Write data to send buffer. + handler.connections[0].send_buffer.write(b"reply data"); + handler.connections[0].snd_wnd = 65535; + + // 4. poll_send — sends data (piggybacks ACK). + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + assert!(tx.num_frames() >= 1, "data segment should be sent"); + + // 5. Verify delayed ACK state is cleared. + let tcb = &handler.connections[0]; + assert!( + !tcb.ack_pending, + "ack_pending should be cleared after data send" + ); + assert_eq!(tcb.ack_delay_count, 0, "ack_delay_count should be cleared"); + assert!( + tcb.delayed_ack_deadline.is_none(), + "delayed_ack_deadline should be cleared" + ); +} + diff --git a/src/net/handler/tcp/tests/ecn.rs b/src/net/handler/tcp/tests/ecn.rs new file mode 100644 index 0000000..f026669 --- /dev/null +++ b/src/net/handler/tcp/tests/ecn.rs @@ -0,0 +1,908 @@ +use super::*; + +#[test] +fn ecn_negotiated_when_both_sides_support() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + let tcp_flags_offset = ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN + 13; + + // Active open with ECN enabled (default config has ecn=true). + let config = TcpConfig::default(); + assert!(config.ecn, "default config should have ecn=true"); + let _events = handler + .connect_with_config( + IpAddress::V4(LOCAL_IP), + 5000, + IpAddress::V4(REMOTE_IP), + 80, + nh.local_mac(), + crate::net::wire::ethernet::MacAddress::broadcast(), + coarsetime::Instant::now(), + config, + &mut free, + &mut tx, + ) + .unwrap(); + + // Verify SYN has ECE+CWR flags. + assert_eq!(tx.num_frames(), 1, "SYN should be sent"); + let syn_frame = tx.pop().unwrap(); + let syn_flags = syn_frame[tcp_flags_offset]; + assert!( + syn_flags & flags::SYN != 0, + "SYN flag should be set, got {:#04x}", + syn_flags + ); + assert!( + syn_flags & flags::ECE != 0, + "ECE flag should be set on SYN when ECN enabled, got {:#04x}", + syn_flags + ); + assert!( + syn_flags & flags::CWR != 0, + "CWR flag should be set on SYN when ECN enabled, got {:#04x}", + syn_flags + ); + + // ecn_enabled should be provisionally true. + assert!(handler.connections[0].ecn_enabled); + + // Send SYN-ACK with ECE (peer supports ECN). + let server_iss = 2000u32; + let client_iss = handler.connections[0].iss; + let syn_ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 80, + 5000, + server_iss, + client_iss.wrapping_add(1), + flags::SYN | flags::ACK | flags::ECE, + 65535, + &[], + ); + let syn_ack_len = syn_ack_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_ack_data), syn_ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // ECN should remain enabled. + assert!( + handler.connections[0].ecn_enabled, + "ecn_enabled should be true after SYN-ACK with ECE" + ); + assert_eq!( + handler.connections[0].state, + TcpState::Established, + "connection should be established" + ); +} + +#[test] +fn ecn_disabled_when_peer_doesnt_support() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Active open with ECN enabled. + let _events = handler + .connect_with_config( + IpAddress::V4(LOCAL_IP), + 5001, + IpAddress::V4(REMOTE_IP), + 80, + nh.local_mac(), + crate::net::wire::ethernet::MacAddress::broadcast(), + coarsetime::Instant::now(), + TcpConfig::default(), + &mut free, + &mut tx, + ) + .unwrap(); + + // Drain the SYN. + while tx.pop().is_some() {} + + // ecn_enabled should be provisionally true. + assert!(handler.connections[0].ecn_enabled); + + // Send SYN-ACK WITHOUT ECE (peer doesn't support ECN). + let server_iss = 3000u32; + let client_iss = handler.connections[0].iss; + let syn_ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 80, + 5001, + server_iss, + client_iss.wrapping_add(1), + flags::SYN | flags::ACK, + 65535, + &[], + ); + let syn_ack_len = syn_ack_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_ack_data), syn_ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // ECN should be disabled. + assert!( + !handler.connections[0].ecn_enabled, + "ecn_enabled should be false after SYN-ACK without ECE" + ); + assert_eq!( + handler.connections[0].state, + TcpState::Established, + "connection should be established" + ); +} + +#[test] +fn ecn_negotiated_on_passive_open() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + let tcp_flags_offset = ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN + 13; + + // Listen with default config (ecn=true). + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + + // Send SYN with ECE+CWR (client supports ECN). + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN | flags::ECE | flags::CWR, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // Verify SYN-ACK has ECE flag. + assert_eq!(tx.num_frames(), 1, "SYN-ACK should be sent"); + let syn_ack_frame = tx.pop().unwrap(); + let syn_ack_flags = syn_ack_frame[tcp_flags_offset]; + assert!( + syn_ack_flags & flags::SYN != 0, + "SYN flag should be set on SYN-ACK, got {:#04x}", + syn_ack_flags + ); + assert!( + syn_ack_flags & flags::ACK != 0, + "ACK flag should be set on SYN-ACK, got {:#04x}", + syn_ack_flags + ); + assert!( + syn_ack_flags & flags::ECE != 0, + "ECE flag should be set on SYN-ACK when ECN negotiated, got {:#04x}", + syn_ack_flags + ); + + // TCB should have ecn_enabled = true. + assert!( + handler.connections[0].ecn_enabled, + "ecn_enabled should be true on passive side" + ); + + // Complete handshake with final ACK. + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // Connection should be established with ECN still enabled. + assert_eq!(handler.connections[0].state, TcpState::Established); + assert!( + handler.connections[0].ecn_enabled, + "ecn_enabled should remain true after handshake" + ); +} + +#[test] +fn ecn_ect_set_on_outgoing_data() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake via listen path. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + + // Enable ECN on the connection. + handler.connections[0].ecn_enabled = true; + handler.connections[0].snd_wnd = 65535; + + // Write data into the send buffer. + let payload = b"Hello ECN!"; + handler.connections[0].send_buffer.write(payload); + + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + + assert_eq!(tx.num_frames(), 1, "should have one data segment"); + let frame = tx.pop().unwrap(); + // IPv4 ToS byte is at ETH_HEADER_LEN + 1. + let tos_byte = frame[ETH_HEADER_LEN + 1]; + assert_eq!(tos_byte, 0x02, "ECT(0) should be set in ToS byte"); +} + +#[test] +fn ecn_ect_not_set_on_retransmit() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + + // Enable ECN and send data. + handler.connections[0].ecn_enabled = true; + handler.connections[0].snd_wnd = 65535; + handler.connections[0].send_buffer.write(b"RTO test data"); + + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + // Drain the initial data segment. + while tx.pop().is_some() {} + + // Expire the retransmit timer to trigger RTO retransmit. + handler.connections[0].retransmit_deadline = Some(now); + handler.poll_timers(now, nh.local_mac(), &nh, &mut free, &mut tx); + + assert_eq!(tx.num_frames(), 1, "should have one retransmit segment"); + let frame = tx.pop().unwrap(); + let tos_byte = frame[ETH_HEADER_LEN + 1]; + assert_eq!(tos_byte, 0x00, "ECT should NOT be set on RTO retransmit"); +} + +#[test] +fn ecn_ce_detected_on_incoming() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + + // Enable ECN on the connection. + handler.connections[0].ecn_enabled = true; + assert!( + !handler.connections[0].ecn_ce_received, + "CE should not be set yet" + ); + + // Build a data segment with CE mark (ToS = 0x03) in the IP header. + let mut data_frame = build_tcp_frame_with_payload( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + b"hello", + ); + // Set CE codepoint in IPv4 ToS byte: ECN bits = 0b11 = 0x03. + data_frame[ETH_HEADER_LEN + 1] = 0x03; + // Recompute IPv4 header checksum after modifying ToS. + let ip_cksum = compute_ipv4_checksum(&data_frame[ETH_HEADER_LEN..ETH_HEADER_LEN + 20]); + data_frame[ETH_HEADER_LEN + 10] = ip_cksum[0]; + data_frame[ETH_HEADER_LEN + 11] = ip_cksum[1]; + + let data_len = data_frame.len(); + handler.process_ipv4( + Frame::new(2, leak(data_frame), data_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + assert!( + handler.connections[0].ecn_ce_received, + "ecn_ce_received should be true after receiving CE-marked segment" + ); +} + +#[test] +fn ecn_ece_sent_when_ce_received() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + + // Enable ECN and set ecn_ce_received. + handler.connections[0].ecn_enabled = true; + handler.connections[0].ecn_ce_received = true; + + // Send data to trigger an ACK with ECE. + let data_frame = build_tcp_frame_with_payload( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + b"hello", + ); + let data_len = data_frame.len(); + handler.process_ipv4( + Frame::new(2, leak(data_frame), data_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // The handler should have sent an ACK (delayed or immediate). + // Force delayed ACK flush if needed. + if tx.num_frames() == 0 { + let now = coarsetime::Instant::now(); + handler.connections[0].delayed_ack_deadline = Some(now); + handler.poll_timers(now, nh.local_mac(), &nh, &mut free, &mut tx); + } + + assert!( + tx.num_frames() >= 1, + "should have at least one outgoing ACK" + ); + let frame = tx.pop().unwrap(); + let tcp_flags_byte = frame[ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN + 13]; + assert!( + tcp_flags_byte & flags::ECE != 0, + "outgoing ACK should have ECE flag set, got flags={:#04x}", + tcp_flags_byte, + ); +} + +#[test] +fn ecn_cwnd_halved_on_ece() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + + // Enable ECN and set cwnd to a known value. + handler.connections[0].ecn_enabled = true; + handler.connections[0].snd_wnd = 65535; + let mss = handler.connections[0].eff_snd_mss as u32; + handler.connections[0].cubic.cwnd = 10 * mss; + handler.connections[0].cubic.ssthresh = 20 * mss; + + // Send some data so snd_nxt advances. + handler.connections[0] + .send_buffer + .write(b"test data for ecn"); + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + while tx.pop().is_some() {} + + let snd_nxt = handler.connections[0].snd_nxt; + + // Record cwnd before receiving ECE. + let cwnd_before = handler.connections[0].cubic.cwnd; + + // Receive ACK with ECE flag — simulating peer's congestion signal. + let ece_ack = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + snd_nxt, + flags::ACK | flags::ECE, + 65535, + &[], + ); + let ece_len = ece_ack.len(); + handler.process_ipv4( + Frame::new(3, leak(ece_ack), ece_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // Verify cwnd was reduced (halved after congestion avoidance increment). + let cwnd_after = handler.connections[0].cubic.cwnd; + assert!( + cwnd_after < cwnd_before, + "cwnd should be reduced: before={}, after={}", + cwnd_before, + cwnd_after, + ); + assert_eq!( + handler.connections[0].cubic.cwnd, handler.connections[0].cubic.ssthresh, + "cwnd should equal ssthresh after ECN response" + ); + assert!( + handler.connections[0].cubic.ssthresh >= 2 * mss, + "ssthresh should be at least 2*MSS" + ); + assert!( + handler.connections[0].ecn_cwr_sent, + "ecn_cwr_sent should be true" + ); +} + +#[test] +fn ecn_cwr_sent_on_next_data() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + + // Enable ECN and set ecn_cwr_sent to true (simulating ECE reception). + handler.connections[0].ecn_enabled = true; + handler.connections[0].ecn_cwr_sent = true; + handler.connections[0].snd_wnd = 65535; + + // Write data and poll_send. + handler.connections[0].send_buffer.write(b"cwr test data"); + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + + assert_eq!(tx.num_frames(), 1, "should have one data segment"); + let frame = tx.pop().unwrap(); + let tcp_flags_byte = frame[ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN + 13]; + assert!( + tcp_flags_byte & flags::CWR != 0, + "outgoing data segment should have CWR flag set, got flags={:#04x}", + tcp_flags_byte, + ); + + // Verify ecn_cwr_sent is cleared after sending. + assert!( + !handler.connections[0].ecn_cwr_sent, + "ecn_cwr_sent should be cleared after sending CWR" + ); +} + +#[test] +fn ecn_ce_received_cleared_on_cwr() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + + // Enable ECN and set ecn_ce_received. + handler.connections[0].ecn_enabled = true; + handler.connections[0].ecn_ce_received = true; + + // Receive a segment with CWR flag from peer (acknowledging our ECE). + let rcv_nxt = handler.connections[0].rcv_nxt; + let cwr_frame = build_tcp_frame_with_payload( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + rcv_nxt, + server_iss.wrapping_add(1), + flags::ACK | flags::CWR, + 65535, + &[], + b"data", + ); + let cwr_len = cwr_frame.len(); + handler.process_ipv4( + Frame::new(2, leak(cwr_frame), cwr_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + assert!( + !handler.connections[0].ecn_ce_received, + "ecn_ce_received should be cleared after receiving CWR" + ); +} + diff --git a/src/net/handler/tcp/tests/edge_cases.rs b/src/net/handler/tcp/tests/edge_cases.rs new file mode 100644 index 0000000..2ef1379 --- /dev/null +++ b/src/net/handler/tcp/tests/edge_cases.rs @@ -0,0 +1,674 @@ +use super::*; + +#[test] +fn unmatched_syn_generates_rst() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(4); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + free.push(alloc_free_frame(100)); + + let data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let len = data.len(); + let frame = Frame::new(0, leak(data), len, false); + + handler.process_ipv4(frame, coarsetime::Instant::now(), &nh, &mut free, &mut rx, &mut tx); + + assert_eq!(rx.num_frames(), 1, "original frame returned to rx"); + assert_eq!(tx.num_frames(), 1, "RST generated on tx"); + assert_eq!(free.num_frames(), 0, "free frame consumed"); +} + +#[test] +fn rst_to_unbound_port_silently_dropped() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(4); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let data = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1000, 0, flags::RST, 0, &[]); + let len = data.len(); + let frame = Frame::new(0, leak(data), len, false); + + handler.process_ipv4(frame, coarsetime::Instant::now(), &nh, &mut free, &mut rx, &mut tx); + + assert_eq!(rx.num_frames(), 1, "frame returned to rx"); + assert_eq!(tx.num_frames(), 0, "no RST for RST"); +} + +#[test] +fn invalid_checksum_dropped() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(4); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + // Corrupt checksum. + let tcp_off = ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN; + let leaked = leak(data); + leaked[tcp_off + 16] ^= 0xFF; + let len = leaked.len(); + let frame = Frame::new(0, leaked, len, false); + + handler.process_ipv4(frame, coarsetime::Instant::now(), &nh, &mut free, &mut rx, &mut tx); + + assert_eq!(rx.num_frames(), 1, "frame returned to rx"); + assert_eq!(tx.num_frames(), 0, "no RST for bad checksum"); +} + +#[test] +fn truncated_tcp_header_dropped() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(4); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + // Build a frame that's too short for a TCP header. + let mut data = vec![0u8; ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN + 10]; // only 10 bytes of TCP + data[12] = 0x08; + data[13] = 0x00; + data[ETH_HEADER_LEN] = 0x45; + let total = (IPV4_MIN_HEADER_LEN + 10) as u16; + data[ETH_HEADER_LEN + 2..ETH_HEADER_LEN + 4].copy_from_slice(&total.to_be_bytes()); + data[ETH_HEADER_LEN + 8] = 64; + data[ETH_HEADER_LEN + 9] = IpProtocols::Tcp; + let src_bytes: [u8; 4] = REMOTE_IP.into(); + data[ETH_HEADER_LEN + 12..ETH_HEADER_LEN + 16].copy_from_slice(&src_bytes); + let dst_bytes: [u8; 4] = LOCAL_IP.into(); + data[ETH_HEADER_LEN + 16..ETH_HEADER_LEN + 20].copy_from_slice(&dst_bytes); + let cksum = compute_ipv4_checksum(&data[ETH_HEADER_LEN..ETH_HEADER_LEN + 20]); + data[ETH_HEADER_LEN + 10] = cksum[0]; + data[ETH_HEADER_LEN + 11] = cksum[1]; + + let len = data.len(); + let frame = Frame::new(0, leak(data), len, false); + + handler.process_ipv4(frame, coarsetime::Instant::now(), &nh, &mut free, &mut rx, &mut tx); + + assert_eq!(rx.num_frames(), 1, "truncated frame returned to rx"); + assert_eq!(tx.num_frames(), 0, "no response"); +} + +#[test] +fn frame_accounting_after_rst() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(4); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + free.push(alloc_free_frame(100)); + + let data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let len = data.len(); + let frame = Frame::new(0, leak(data), len, false); + + handler.process_ipv4(frame, coarsetime::Instant::now(), &nh, &mut free, &mut rx, &mut tx); + + let total = free.num_frames() + rx.num_frames() + tx.num_frames(); + assert_eq!(total, 2, "all frames accounted for (1 rx + 1 tx)"); +} + +#[test] +fn rst_outside_window_is_dropped() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + let server_iss = establish_connection(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + // rcv_nxt is 1001, window is 65535 → valid range [1001, 66536). + // Send RST with seq far outside the window. + let rst_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 999_999, // way outside window + server_iss.wrapping_add(1), + flags::RST | flags::ACK, + 0, + &[], + ); + let rst_len = rst_data.len(); + handler.process_ipv4( + Frame::new(50, leak(rst_data), rst_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // Connection must survive — out-of-window RST should be silently dropped. + assert_eq!( + handler.connections.len(), + 1, + "out-of-window RST must not reset the connection" + ); + assert_eq!(handler.connections[0].state, TcpState::Established); +} + +#[test] +fn rst_in_window_but_not_exact_sends_challenge_ack() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + let server_iss = establish_connection(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + // rcv_nxt is 1001. Send RST with seq = 1005 (in-window but not exact). + let rst_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1005, // in-window but != rcv_nxt (1001) + server_iss.wrapping_add(1), + flags::RST | flags::ACK, + 0, + &[], + ); + let rst_len = rst_data.len(); + handler.process_ipv4( + Frame::new(50, leak(rst_data), rst_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // Connection must survive — in-window non-exact RST triggers challenge ACK. + assert_eq!( + handler.connections.len(), + 1, + "in-window non-exact RST must not reset the connection" + ); + assert_eq!(handler.connections[0].state, TcpState::Established); + + // A challenge ACK should have been sent. + assert!( + tx.num_frames() > 0, + "challenge ACK should be sent for in-window non-exact RST" + ); +} + +#[test] +fn syn_in_established_sends_challenge_ack() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + let server_iss = establish_connection(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + // Send a SYN segment to the established connection. + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, // seq = rcv_nxt (in-window) + server_iss.wrapping_add(1), + flags::SYN | flags::ACK, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(50, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // (1) Connection must NOT be reset — it should still be Established. + assert_eq!( + handler.connections.len(), + 1, + "SYN in Established must not destroy the connection" + ); + assert_eq!(handler.connections[0].state, TcpState::Established); + + // (2) A challenge ACK should have been sent. + assert!( + tx.num_frames() > 0, + "challenge ACK should be sent for SYN in Established (RFC 5961)" + ); + + // (3) No data should have been processed (rcv_nxt unchanged). + assert_eq!( + handler.connections[0].rcv_nxt, 1001, + "rcv_nxt must not advance when SYN is received in Established" + ); +} + +#[test] +fn segment_without_ack_is_dropped() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + let server_iss = establish_connection(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + let rcv_nxt_before = handler.connections[0].rcv_nxt; + + // Send a data segment with no flags set (ACK bit off). + let no_ack_data = build_tcp_frame_with_payload( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, // seq = rcv_nxt + server_iss.wrapping_add(1), + 0, // no flags at all + 65535, + &[], + b"hello", + ); + let no_ack_len = no_ack_data.len(); + handler.process_ipv4( + Frame::new(50, leak(no_ack_data), no_ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // (1) No data should be written to recv_buffer (rcv_nxt unchanged). + assert_eq!( + handler.connections[0].rcv_nxt, rcv_nxt_before, + "rcv_nxt must not advance for segment without ACK" + ); + + // (2) No response should be sent (tx empty). + assert_eq!( + tx.num_frames(), + 0, + "no response should be sent for segment without ACK" + ); +} + +#[test] +fn ack_beyond_snd_nxt_sends_ack_and_drops() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + let server_iss = establish_connection(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + let snd_una_before = handler.connections[0].snd_una; + let snd_nxt_before = handler.connections[0].snd_nxt; + let rcv_nxt_before = handler.connections[0].rcv_nxt; + + // Send a segment with seg_ack far beyond snd_nxt. + let bad_ack = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, // seq = rcv_nxt (in-window) + server_iss.wrapping_add(1).wrapping_add(999999), // ACK for unsent data + flags::ACK, + 65535, + &[], + ); + let bad_ack_len = bad_ack.len(); + handler.process_ipv4( + Frame::new(50, leak(bad_ack), bad_ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // (1) Connection must survive. + assert_eq!( + handler.connections.len(), + 1, + "connection must not be destroyed by future ACK" + ); + assert_eq!(handler.connections[0].state, TcpState::Established); + + // (2) An ACK must be sent in response. + assert!( + tx.num_frames() > 0, + "ACK must be sent when seg_ack > snd_nxt (RFC 9293 §3.10.7.4)" + ); + + // Verify the response has ACK flags. + let resp = tx.pop().unwrap(); + let tcp_off = ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN; + let tcp_flags = resp[tcp_off + 13]; + assert_eq!( + tcp_flags & flags::ACK, + flags::ACK, + "response must be an ACK" + ); + + // (3) No state changes: snd_una must be unchanged. + assert_eq!( + handler.connections[0].snd_una, snd_una_before, + "snd_una must not change on future ACK" + ); + assert_eq!( + handler.connections[0].snd_nxt, snd_nxt_before, + "snd_nxt must not change on future ACK" + ); + assert_eq!( + handler.connections[0].rcv_nxt, rcv_nxt_before, + "rcv_nxt must not change on future ACK" + ); +} + +#[test] +fn stale_segment_does_not_regress_window() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(8); + let mut rx = BasicFrameBuffer::new(8); + let mut tx = BasicFrameBuffer::new(8); + for i in 0..6 { + free.push(alloc_free_frame(100 + i)); + } + + let server_iss = establish_connection(&mut handler, &nh, &mut free, &mut rx, &mut tx); + while rx.pop().is_some() {} + + // Segment B arrives first: higher seg_seq (1002), window = 8000. + let seg_b = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1002, + server_iss.wrapping_add(1), + flags::ACK, + 8000, + &[], + ); + let seg_b_len = seg_b.len(); + handler.process_ipv4( + Frame::new(10, leak(seg_b), seg_b_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while rx.pop().is_some() {} + while tx.pop().is_some() {} + + assert_eq!( + handler.connections[0].snd_wnd, 8000, + "window set to 8000 from seg B" + ); + assert_eq!( + handler.connections[0].snd_wl1, 1002, + "snd_wl1 set from seg B" + ); + + // Segment A arrives late: lower seg_seq (1001), window = 4000. + // This is stale — its window must NOT regress. + let seg_a = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 4000, + &[], + ); + let seg_a_len = seg_a.len(); + handler.process_ipv4( + Frame::new(11, leak(seg_a), seg_a_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while rx.pop().is_some() {} + while tx.pop().is_some() {} + + assert_eq!( + handler.connections[0].snd_wnd, 8000, + "stale segment must not regress snd_wnd" + ); + assert_eq!( + handler.connections[0].snd_wl1, 1002, + "stale segment must not regress snd_wl1" + ); +} + +#[test] +fn sender_sws_avoidance_holds_small_sends() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + + let mss = handler.connections[0].eff_snd_mss; + + // Set max_snd_wnd high (simulates the peer previously advertised a large window). + handler.connections[0].max_snd_wnd = 65535; + // Set current snd_wnd to 10 bytes — much less than MSS and max_snd_wnd/2. + handler.connections[0].snd_wnd = 10; + + // Write more data than can_send (10 bytes) so data_available > can_send. + handler.connections[0].send_buffer.write(&[0x41u8; 100]); + + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + + // SWS check: can_send=10, eff_snd_mss=1460, max_snd_wnd/2=32767, data_available=100. + // 10 < 1460 (not full MSS), 10 < 32767 (not half max window), 100 > 10 (not all data fits). + // sws_ok = false → no send. + assert_eq!( + tx.num_frames(), + 0, + "sender SWS should hold: window too small" + ); + + // Now set snd_wnd to eff_snd_mss — should send. + handler.connections[0].snd_wnd = mss as u32; + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + assert_eq!( + tx.num_frames(), + 1, + "should send when can_send >= eff_snd_mss" + ); +} + +#[test] +fn sender_sws_allows_send_when_all_data_fits() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + + // Set max_snd_wnd high, snd_wnd to 5 bytes (small window). + handler.connections[0].max_snd_wnd = 65535; + handler.connections[0].snd_wnd = 5; + + // Write only 3 bytes — all data fits in the window. + handler.connections[0].send_buffer.write(b"abc"); + + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + + // SWS: can_send=5, data_available=3, 3 <= 5 → all data fits → sws_ok = true. + assert_eq!( + tx.num_frames(), + 1, + "should send when all data fits in window" + ); +} + diff --git a/src/net/handler/tcp/tests/handshake.rs b/src/net/handler/tcp/tests/handshake.rs new file mode 100644 index 0000000..9be2b5e --- /dev/null +++ b/src/net/handler/tcp/tests/handshake.rs @@ -0,0 +1,406 @@ +use super::*; + +#[test] +fn syn_to_listener_generates_syn_ack() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(4); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + free.push(alloc_free_frame(100)); + + let accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + + // MSS option in SYN. + let mss_opt = [0x02, 0x04, 0x05, 0xB4]; // MSS=1460 + let data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &mss_opt, + ); + let len = data.len(); + let frame = Frame::new(0, leak(data), len, false); + + handler.process_ipv4(frame, coarsetime::Instant::now(), &nh, &mut free, &mut rx, &mut tx); + + assert_eq!(rx.num_frames(), 1, "original frame to rx"); + assert_eq!(tx.num_frames(), 1, "SYN-ACK generated"); + assert_eq!(handler.connections.len(), 1, "connection created"); + assert_eq!(handler.connections[0].state, TcpState::SynReceived); + assert_eq!(handler.connections[0].snd_mss, 1460); + assert!(accept_queue.is_empty(), "not yet in accept queue"); +} + +#[test] +fn handshake_completes_on_ack() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(8); + let mut rx = BasicFrameBuffer::new(8); + let mut tx = BasicFrameBuffer::new(8); + + for i in 0..4 { + free.push(alloc_free_frame(100 + i)); + } + + let accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + + // Step 1: SYN. + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + let syn_frame = Frame::new(0, leak(syn_data), syn_len, false); + handler.process_ipv4(syn_frame, coarsetime::Instant::now(), &nh, &mut free, &mut rx, &mut tx); + assert_eq!(handler.connections[0].state, TcpState::SynReceived); + + // Get ISS from the TCB. + let server_iss = handler.connections[0].iss; + + // Step 2: ACK completing handshake. + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + let ack_frame = Frame::new(1, leak(ack_data), ack_len, false); + handler.process_ipv4(ack_frame, coarsetime::Instant::now(), &nh, &mut free, &mut rx, &mut tx); + + assert_eq!(handler.connections[0].state, TcpState::Established); + assert_eq!(accept_queue.len(), 1, "connection in accept queue"); +} + +#[test] +fn rst_in_syn_received_removes_connection() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(8); + let mut rx = BasicFrameBuffer::new(8); + let mut tx = BasicFrameBuffer::new(8); + + for i in 0..4 { + free.push(alloc_free_frame(100 + i)); + } + + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + + // SYN. + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + let syn_frame = Frame::new(0, leak(syn_data), syn_len, false); + handler.process_ipv4(syn_frame, coarsetime::Instant::now(), &nh, &mut free, &mut rx, &mut tx); + assert_eq!(handler.connections.len(), 1); + + // RST. + let rcv_nxt = handler.connections[0].rcv_nxt; + let rst_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + rcv_nxt, + 0, + flags::RST, + 0, + &[], + ); + let rst_len = rst_data.len(); + let rst_frame = Frame::new(2, leak(rst_data), rst_len, false); + handler.process_ipv4(rst_frame, coarsetime::Instant::now(), &nh, &mut free, &mut rx, &mut tx); + + assert_eq!(handler.connections.len(), 0, "connection removed"); +} + +#[test] +fn backlog_limits_syn_received() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(16); + let mut rx = BasicFrameBuffer::new(16); + let mut tx = BasicFrameBuffer::new(16); + + for i in 0..8 { + free.push(alloc_free_frame(100 + i)); + } + + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 2).unwrap(); + + // Send 3 SYNs — only 2 should be accepted (backlog=2). + for i in 0..3u16 { + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 10000 + i, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + let syn_frame = Frame::new(i as u64, leak(syn_data), syn_len, false); + handler.process_ipv4(syn_frame, coarsetime::Instant::now(), &nh, &mut free, &mut rx, &mut tx); + } + + assert_eq!(handler.connections.len(), 2, "backlog limits connections"); +} + +#[test] +fn window_scale_negotiation() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(8); + let mut rx = BasicFrameBuffer::new(8); + let mut tx = BasicFrameBuffer::new(8); + + for i in 0..4 { + free.push(alloc_free_frame(100 + i)); + } + + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + + // SYN with MSS + Window Scale options. + let ws_opts = [ + 0x02, 0x04, 0x05, 0xB4, // MSS=1460 + 0x01, // NOP + 0x03, 0x03, 0x07, // Window Scale=7 + ]; + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &ws_opts, + ); + let syn_len = syn_data.len(); + let syn_frame = Frame::new(0, leak(syn_data), syn_len, false); + handler.process_ipv4(syn_frame, coarsetime::Instant::now(), &nh, &mut free, &mut rx, &mut tx); + + assert!(handler.connections[0].wscale_enabled); + assert_eq!(handler.connections[0].snd_wscale, 7); + assert_eq!(handler.connections[0].rcv_wscale, DEFAULT_RCV_WSCALE); +} + +#[test] +fn simultaneous_open_both_reach_established() { + // Two handlers representing side A and side B. + let mut handler_a = new_handler(); + let mut handler_b = new_handler(); + let nh = new_neighbor_handler(); + + let ip_a = Ipv4Address::new([10, 0, 0, 1]); + let ip_b = Ipv4Address::new([10, 0, 0, 2]); + let port_a: u16 = 5000; + let port_b: u16 = 6000; + let mac_a = MacAddress::new([0xAA, 0x00, 0x00, 0x00, 0x00, 0x01]); + let mac_b = MacAddress::new([0xBB, 0x00, 0x00, 0x00, 0x00, 0x02]); + + let mut free_a = BasicFrameBuffer::new(16); + let mut free_b = BasicFrameBuffer::new(16); + let mut rx = BasicFrameBuffer::new(16); + let mut tx = BasicFrameBuffer::new(16); + + for i in 0..8 { + free_a.push(alloc_free_frame(200 + i)); + free_b.push(alloc_free_frame(300 + i)); + } + + // Both sides initiate active open (connect). + let events_a = handler_a + .connect( + IpAddress::V4(ip_a), + port_a, + IpAddress::V4(ip_b), + port_b, + mac_a, + mac_b, + coarsetime::Instant::now(), + &mut free_a, + &mut tx, + ) + .unwrap(); + // Discard the SYN frame emitted by connect — we'll build frames manually. + while tx.pop().is_some() {} + + let events_b = handler_b + .connect( + IpAddress::V4(ip_b), + port_b, + IpAddress::V4(ip_a), + port_a, + mac_b, + mac_a, + coarsetime::Instant::now(), + &mut free_b, + &mut tx, + ) + .unwrap(); + while tx.pop().is_some() {} + + assert_eq!(handler_a.connections[0].state, TcpState::SynSent); + assert_eq!(handler_b.connections[0].state, TcpState::SynSent); + + let iss_a = handler_a.connections[0].iss; + let iss_b = handler_b.connections[0].iss; + + // Step 1: Feed side B's SYN to handler A. + // B sent SYN with seq=ISS_B, no ACK. A should transition to SynReceived. + let syn_b = build_tcp_frame( + ip_b, + ip_a, + port_b, + port_a, + iss_b, + 0, + flags::SYN, + 65535, + &[0x02, 0x04, 0x05, 0xB4], // MSS=1460 + ); + let syn_b_len = syn_b.len(); + handler_a.process_ipv4( + Frame::new(10, leak(syn_b), syn_b_len, false), + coarsetime::Instant::now(), + &nh, + &mut free_a, + &mut rx, + &mut tx, + ); + assert_eq!( + handler_a.connections[0].state, + TcpState::SynReceived, + "A should transition to SynReceived on receiving B's SYN" + ); + // A emits a SYN-ACK on tx. + assert!(tx.num_frames() >= 1, "A should emit SYN-ACK"); + while tx.pop().is_some() {} + while rx.pop().is_some() {} + + // Step 2: Feed side A's SYN to handler B. + let syn_a = build_tcp_frame( + ip_a, + ip_b, + port_a, + port_b, + iss_a, + 0, + flags::SYN, + 65535, + &[0x02, 0x04, 0x05, 0xB4], + ); + let syn_a_len = syn_a.len(); + handler_b.process_ipv4( + Frame::new(11, leak(syn_a), syn_a_len, false), + coarsetime::Instant::now(), + &nh, + &mut free_b, + &mut rx, + &mut tx, + ); + assert_eq!( + handler_b.connections[0].state, + TcpState::SynReceived, + "B should transition to SynReceived on receiving A's SYN" + ); + assert!(tx.num_frames() >= 1, "B should emit SYN-ACK"); + while tx.pop().is_some() {} + while rx.pop().is_some() {} + + // Step 3: Feed B's SYN-ACK to handler A. + // B's SYN-ACK: seq=ISS_B, ack=ISS_A+1, flags=SYN|ACK. + let syn_ack_b = build_tcp_frame( + ip_b, + ip_a, + port_b, + port_a, + iss_b, + iss_a.wrapping_add(1), + flags::SYN | flags::ACK, + 65535, + &[0x02, 0x04, 0x05, 0xB4], + ); + let syn_ack_b_len = syn_ack_b.len(); + handler_a.process_ipv4( + Frame::new(12, leak(syn_ack_b), syn_ack_b_len, false), + coarsetime::Instant::now(), + &nh, + &mut free_a, + &mut rx, + &mut tx, + ); + assert_eq!( + handler_a.connections[0].state, + TcpState::Established, + "A should transition to Established on receiving B's SYN-ACK" + ); + while tx.pop().is_some() {} + while rx.pop().is_some() {} + + // Step 4: Feed A's SYN-ACK to handler B. + let syn_ack_a = build_tcp_frame( + ip_a, + ip_b, + port_a, + port_b, + iss_a, + iss_b.wrapping_add(1), + flags::SYN | flags::ACK, + 65535, + &[0x02, 0x04, 0x05, 0xB4], + ); + let syn_ack_a_len = syn_ack_a.len(); + handler_b.process_ipv4( + Frame::new(13, leak(syn_ack_a), syn_ack_a_len, false), + coarsetime::Instant::now(), + &nh, + &mut free_b, + &mut rx, + &mut tx, + ); + assert_eq!( + handler_b.connections[0].state, + TcpState::Established, + "B should transition to Established on receiving A's SYN-ACK" + ); + + // Verify both sides emitted Connected events. + assert!(events_a.pop().is_some(), "A should have a Connected event"); + assert!(events_b.pop().is_some(), "B should have a Connected event"); +} + diff --git a/src/net/handler/tcp/tests/keepalive.rs b/src/net/handler/tcp/tests/keepalive.rs new file mode 100644 index 0000000..5de4da4 --- /dev/null +++ b/src/net/handler/tcp/tests/keepalive.rs @@ -0,0 +1,956 @@ +use super::*; + +#[test] +fn keep_alive_activity_resets_probe_timer() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(16); + let mut rx = BasicFrameBuffer::new(16); + let mut tx = BasicFrameBuffer::new(16); + + for i in 0..8 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake to reach Established. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(handler.connections[0].state, TcpState::Established); + + // Simulate stale keep-alive state: set probes sent to 5. + let old_activity = handler.connections[0].last_activity; + handler.connections[0].keep_alive_probes_sent = 5; + + // Clear tx from handshake. + while tx.pop().is_some() {} + + // Send a data segment to the established connection. + let payload = b"keepalive-reset"; + let data = build_tcp_frame_with_payload( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + payload, + ); + let data_len = data.len(); + handler.process_ipv4( + Frame::new(2, leak(data), data_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // Verify keep-alive probes were reset. + let tcb = &handler.connections[0]; + assert_eq!( + tcb.keep_alive_probes_sent, 0, + "keep_alive_probes_sent should be reset to 0 on data receipt" + ); + assert!( + tcb.last_activity >= old_activity, + "last_activity should be updated on data receipt" + ); +} + +#[test] +fn keep_alive_probe_sent_after_idle_timeout() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + assert_eq!(handler.connections[0].state, TcpState::Established); + + // Configure keep-alive with short timeouts. + { + let tcb = &mut handler.connections[0]; + tcb.keep_alive_enabled = true; + tcb.keep_alive_idle_ms = 100; + tcb.keep_alive_interval_ms = 50; + tcb.keep_alive_count = 3; + tcb.ack_pending = false; + } + + // Sleep long enough for the idle timeout to expire. + std::thread::sleep(std::time::Duration::from_millis(150)); + let now = coarsetime::Instant::now(); + + handler.poll_timers(now, nh.local_mac(), &nh, &mut free, &mut tx); + + assert_eq!( + handler.connections[0].keep_alive_probes_sent, 1, + "one keep-alive probe should have been sent" + ); + assert!( + tx.num_frames() >= 1, + "a probe segment should have been emitted" + ); +} + +#[test] +fn keep_alive_no_probe_when_disabled() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + assert_eq!(handler.connections[0].state, TcpState::Established); + + // Keep-alive is disabled by default; set ack_pending false to avoid delayed ACK output. + handler.connections[0].ack_pending = false; + + // Sleep long enough that it would have triggered if enabled. + std::thread::sleep(std::time::Duration::from_millis(150)); + let now = coarsetime::Instant::now(); + + handler.poll_timers(now, nh.local_mac(), &nh, &mut free, &mut tx); + + assert_eq!( + handler.connections[0].keep_alive_probes_sent, 0, + "no probes should be sent when keep-alive is disabled" + ); + assert_eq!(tx.num_frames(), 0, "no segments should be emitted"); +} + +#[test] +fn keep_alive_connection_aborted_after_max_probes() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + assert_eq!(handler.connections[0].state, TcpState::Established); + + // Capture event queue before connection is removed. + let event_queue = handler.connections[0].event_queue.clone(); + + // Configure keep-alive: already sent max probes. + { + let tcb = &mut handler.connections[0]; + tcb.keep_alive_enabled = true; + tcb.keep_alive_idle_ms = 50; + tcb.keep_alive_interval_ms = 25; + tcb.keep_alive_count = 2; + tcb.keep_alive_probes_sent = 2; // already at max + tcb.ack_pending = false; + } + + // Sleep past the probe threshold. + std::thread::sleep(std::time::Duration::from_millis(150)); + let now = coarsetime::Instant::now(); + + handler.poll_timers(now, nh.local_mac(), &nh, &mut free, &mut tx); + + // Connection should be removed. + assert!( + handler.connections.is_empty(), + "connection should be removed after max probes exceeded" + ); + + // Timeout event should have been pushed. + let event = event_queue.pop(); + assert_eq!( + event, + Some(TcpEvent::Timeout), + "TcpEvent::Timeout should be emitted" + ); +} + +#[test] +fn linger_zero_sends_rst_on_poll_send() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(16); + let mut rx = BasicFrameBuffer::new(16); + let mut tx = BasicFrameBuffer::new(16); + + for i in 0..8 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake to reach Established. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(handler.connections[0].state, TcpState::Established); + + // Set linger to 0 and capture event queue. + handler.connections[0].linger = Some(0); + let event_queue = handler.connections[0].event_queue.clone(); + let conn_id = handler.connections[0].id; + + // Clear tx from handshake. + while tx.pop().is_some() {} + + // Call initiate_close — should set pending_fin and linger_deadline to Instant::recent(). + handler.initiate_close(&conn_id); + assert!( + handler.connections[0].pending_fin, + "pending_fin should be set" + ); + assert!( + handler.connections[0].linger_deadline.is_some(), + "linger_deadline should be set" + ); + + // Call poll_send — linger deadline is already expired, should send RST. + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + + // Connection should be removed. + assert!( + handler.get_connection(&conn_id).is_none(), + "connection should be removed after linger(0) RST" + ); + + // A RST segment should have been emitted. + assert!(tx.num_frames() > 0, "RST segment should be emitted"); + + // Reset event should have been pushed. + let event = event_queue.pop(); + assert_eq!( + event, + Some(TcpEvent::Reset), + "TcpEvent::Reset should be emitted" + ); +} + +#[test] +fn linger_timeout_sets_deadline() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(16); + let mut rx = BasicFrameBuffer::new(16); + let mut tx = BasicFrameBuffer::new(16); + + for i in 0..8 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake to reach Established. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(handler.connections[0].state, TcpState::Established); + + // Set linger to 5000ms. + handler.connections[0].linger = Some(5000); + let conn_id = handler.connections[0].id; + + // Clear tx from handshake. + while tx.pop().is_some() {} + + // Call initiate_close. + handler.initiate_close(&conn_id); + assert!( + handler.connections[0].pending_fin, + "pending_fin should be set" + ); + assert!( + handler.connections[0].linger_deadline.is_some(), + "linger_deadline should be set" + ); + + // Call poll_send immediately — deadline is 5s in the future, should NOT abort. + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + + // Connection should still exist. + assert!( + handler.get_connection(&conn_id).is_some(), + "connection should still exist before deadline" + ); +} + +#[test] +fn linger_none_normal_close() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(16); + let mut rx = BasicFrameBuffer::new(16); + let mut tx = BasicFrameBuffer::new(16); + + for i in 0..8 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake to reach Established. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(handler.connections[0].state, TcpState::Established); + + // Ensure linger is None (default). + assert!( + handler.connections[0].linger.is_none(), + "linger should be None by default" + ); + let conn_id = handler.connections[0].id; + + // Call initiate_close. + handler.initiate_close(&conn_id); + + // Verify pending_fin is true and linger_deadline is None. + assert!( + handler.connections[0].pending_fin, + "pending_fin should be set" + ); + assert!( + handler.connections[0].linger_deadline.is_none(), + "linger_deadline should be None for default close" + ); +} + +#[test] +fn keep_alive_probe_and_recovery() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + assert_eq!(handler.connections[0].state, TcpState::Established); + + // Configure keep-alive. + { + let tcb = &mut handler.connections[0]; + tcb.keep_alive_enabled = true; + tcb.keep_alive_idle_ms = 100; + tcb.keep_alive_interval_ms = 50; + tcb.keep_alive_count = 3; + tcb.ack_pending = false; + tcb.delayed_ack_deadline = None; + } + + // Wait past the idle threshold. + std::thread::sleep(std::time::Duration::from_millis(150)); + coarsetime::Instant::update(); + let now = coarsetime::Instant::now(); + + let tx_before = tx.num_frames(); + handler.poll_timers(now, nh.local_mac(), &nh, &mut free, &mut tx); + + // Verify probe was sent. + assert!( + tx.num_frames() > tx_before, + "keep-alive probe should be sent" + ); + assert_eq!( + handler.connections[0].keep_alive_probes_sent, 1, + "probes_sent should be 1" + ); + + // Record last_activity before recovery. + let activity_before = handler.connections[0].last_activity; + + // Simulate receiving an ACK from the remote (recovery). + let rcv_nxt = handler.connections[0].rcv_nxt; + let snd_una = handler.connections[0].snd_una; + let ack_frame = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + rcv_nxt, + snd_una, + flags::ACK, + 65535, + &[], + ); + let ack_frame_len = ack_frame.len(); + + // Use process_ipv4 with a known timestamp so last_activity is deterministic. + coarsetime::Instant::update(); + let recv_now = coarsetime::Instant::now(); + handler.process_ipv4( + Frame::new(10, leak(ack_frame), ack_frame_len, false), + recv_now, + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // The ACK is a duplicate ACK (seg_ack == snd_una, no data). + // Keep-alive probe responses are duplicate ACKs — the fix in process_established + // resets keep_alive_probes_sent when a dup ACK arrives and probes are outstanding. + let tcb = &handler.connections[0]; + assert_eq!( + tcb.keep_alive_probes_sent, 0, + "probes_sent should be reset by dup ACK probe response" + ); + assert!( + tcb.last_activity >= activity_before, + "last_activity should be updated" + ); +} + +#[test] +fn keep_alive_exhaustion_removes_connection() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + assert_eq!(handler.connections[0].state, TcpState::Established); + + // Capture event queue before connection is removed. + let event_queue = handler.connections[0].event_queue.clone(); + + // Configure keep-alive with count=1. + { + let tcb = &mut handler.connections[0]; + tcb.keep_alive_enabled = true; + tcb.keep_alive_count = 1; + tcb.keep_alive_idle_ms = 50; + tcb.keep_alive_interval_ms = 50; + tcb.ack_pending = false; + tcb.delayed_ack_deadline = None; + } + + // Wait past the idle threshold and send first probe. + std::thread::sleep(std::time::Duration::from_millis(100)); + coarsetime::Instant::update(); + let now1 = coarsetime::Instant::now(); + handler.poll_timers(now1, nh.local_mac(), &nh, &mut free, &mut tx); + + // First probe should have been sent. + assert_eq!( + handler.connections[0].keep_alive_probes_sent, 1, + "first probe sent" + ); + assert_eq!( + handler.connections.len(), + 1, + "connection still alive after first probe" + ); + + // Wait again past the interval — max probes exceeded. + std::thread::sleep(std::time::Duration::from_millis(100)); + coarsetime::Instant::update(); + let now2 = coarsetime::Instant::now(); + handler.poll_timers(now2, nh.local_mac(), &nh, &mut free, &mut tx); + + // Connection should be removed. + assert!( + handler.connections.is_empty(), + "connection should be removed after max probes exceeded" + ); + + // Timeout event should have been pushed. + let event = event_queue.pop(); + assert_eq!( + event, + Some(TcpEvent::Timeout), + "TcpEvent::Timeout should be emitted" + ); +} + +#[test] +fn linger_zero_immediate_rst() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + assert_eq!(handler.connections[0].state, TcpState::Established); + + // Write some data to send buffer. + handler.connections[0].send_buffer.write(b"unsent data"); + + // Set linger to 0 and capture event queue. + handler.connections[0].linger = Some(0); + let event_queue = handler.connections[0].event_queue.clone(); + let conn_id = handler.connections[0].id; + + // Initiate close — linger(0) sets immediate deadline. + handler.initiate_close(&conn_id); + assert!( + handler.connections[0].pending_fin, + "pending_fin should be set" + ); + assert!( + handler.connections[0].linger_deadline.is_some(), + "linger_deadline should be set for linger(0)" + ); + + // Call poll_send — linger deadline is already expired, should send RST. + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + + // Connection should be removed. + assert!( + handler.get_connection(&conn_id).is_none(), + "connection should be removed after linger(0) RST" + ); + + // RST segment should have been emitted. + assert!(tx.num_frames() > 0, "RST segment should be emitted on tx"); + + // Reset event should have been pushed. + let event = event_queue.pop(); + assert_eq!( + event, + Some(TcpEvent::Reset), + "TcpEvent::Reset should be emitted for linger(0) abort" + ); +} + diff --git a/src/net/handler/tcp/tests/mod.rs b/src/net/handler/tcp/tests/mod.rs new file mode 100644 index 0000000..8955f8f --- /dev/null +++ b/src/net/handler/tcp/tests/mod.rs @@ -0,0 +1,483 @@ +use crate::{ + net::{ + NeighborHandler, + checksum::{compute_ipv4_checksum, compute_tcp_checksum}, + wire::{ + ethernet::MacAddress, + ip::{IPV4_MIN_HEADER_LEN, IpAddress, IpProtocols, Ipv4Address}, + tcp::{TCP_HEADER_LEN, TcpHeader, flags}, + }, + }, + xdp::frame::{BasicFrameBuffer, Frame}, +}; + +use super::*; +use super::inbound::is_segment_acceptable; +use super::state::TcpState; +use super::tcb::{ConnectionId, DEFAULT_RCV_WSCALE, TcpConfig, TcpEvent}; + +use crate::xdp::frame::FrameBuffer; + +pub(super) const LOCAL_IP: Ipv4Address = Ipv4Address::new([10, 0, 0, 1]); +pub(super) const REMOTE_IP: Ipv4Address = Ipv4Address::new([10, 0, 0, 2]); +pub(super) const ETH_HEADER_LEN: usize = 14; + +pub(super) fn new_handler() -> TcpHandler { + TcpHandler::new(false, false) +} + +pub(super) fn new_neighbor_handler() -> NeighborHandler { + NeighborHandler::new("test0", coarsetime::Duration::from_secs(60)).unwrap() +} + +/// Build a valid Ethernet + IPv4 + TCP frame. +pub(super) fn build_tcp_frame( + src_ip: Ipv4Address, + dst_ip: Ipv4Address, + src_port: u16, + dst_port: u16, + seq: u32, + ack: u32, + tcp_flags: u8, + window: u16, + tcp_options: &[u8], +) -> Vec { + let opt_padded_len = (tcp_options.len() + 3) & !3; + let tcp_header_len = TCP_HEADER_LEN + opt_padded_len; + let data_offset = (tcp_header_len / 4) as u8; + let total_ip_len = (IPV4_MIN_HEADER_LEN + tcp_header_len) as u16; + let mut buf = vec![0u8; ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN + tcp_header_len]; + + // Ethernet header. + buf[0..6].copy_from_slice(&[0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]); // dst mac + buf[6..12].copy_from_slice(&[0x11, 0x22, 0x33, 0x44, 0x55, 0x66]); // src mac + buf[12] = 0x08; + buf[13] = 0x00; + + // IPv4 header. + let ip = &mut buf[ETH_HEADER_LEN..]; + ip[0] = 0x45; + ip[2..4].copy_from_slice(&total_ip_len.to_be_bytes()); + ip[6] = 0x40; + ip[8] = 64; + ip[9] = IpProtocols::Tcp; + let src_bytes: [u8; 4] = src_ip.into(); + ip[12..16].copy_from_slice(&src_bytes); + let dst_bytes: [u8; 4] = dst_ip.into(); + ip[16..20].copy_from_slice(&dst_bytes); + let cksum = compute_ipv4_checksum(&ip[..20]); + ip[10] = cksum[0]; + ip[11] = cksum[1]; + + // TCP header. + let tcp_off = ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN; + let hdr = TcpHeader::new( + src_port, + dst_port, + seq, + ack, + data_offset, + tcp_flags, + window, + [0, 0], + 0, + ); + let hdr_bytes = unsafe { + std::slice::from_raw_parts(&hdr as *const TcpHeader as *const u8, TCP_HEADER_LEN) + }; + buf[tcp_off..tcp_off + TCP_HEADER_LEN].copy_from_slice(hdr_bytes); + + // Options. + if !tcp_options.is_empty() { + buf[tcp_off + TCP_HEADER_LEN..tcp_off + TCP_HEADER_LEN + tcp_options.len()] + .copy_from_slice(tcp_options); + } + + // TCP checksum. + let tcp_segment = &mut buf[tcp_off..]; + let cksum = compute_tcp_checksum(&src_ip, &dst_ip, tcp_segment); + buf[tcp_off + 16] = cksum[0]; + buf[tcp_off + 17] = cksum[1]; + + buf +} + +/// Build a valid Ethernet + IPv4 + TCP frame with payload data. +pub(super) fn build_tcp_frame_with_payload( + src_ip: Ipv4Address, + dst_ip: Ipv4Address, + src_port: u16, + dst_port: u16, + seq: u32, + ack: u32, + tcp_flags: u8, + window: u16, + tcp_options: &[u8], + payload: &[u8], +) -> Vec { + let opt_padded_len = (tcp_options.len() + 3) & !3; + let tcp_header_len = TCP_HEADER_LEN + opt_padded_len; + let data_offset = (tcp_header_len / 4) as u8; + let total_ip_len = (IPV4_MIN_HEADER_LEN + tcp_header_len + payload.len()) as u16; + let mut buf = + vec![0u8; ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN + tcp_header_len + payload.len()]; + + // Ethernet header. + buf[0..6].copy_from_slice(&[0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]); // dst mac + buf[6..12].copy_from_slice(&[0x11, 0x22, 0x33, 0x44, 0x55, 0x66]); // src mac + buf[12] = 0x08; + buf[13] = 0x00; + + // IPv4 header. + let ip = &mut buf[ETH_HEADER_LEN..]; + ip[0] = 0x45; + ip[2..4].copy_from_slice(&total_ip_len.to_be_bytes()); + ip[6] = 0x40; + ip[8] = 64; + ip[9] = IpProtocols::Tcp; + let src_bytes: [u8; 4] = src_ip.into(); + ip[12..16].copy_from_slice(&src_bytes); + let dst_bytes: [u8; 4] = dst_ip.into(); + ip[16..20].copy_from_slice(&dst_bytes); + let cksum = compute_ipv4_checksum(&ip[..20]); + ip[10] = cksum[0]; + ip[11] = cksum[1]; + + // TCP header. + let tcp_off = ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN; + let hdr = TcpHeader::new( + src_port, + dst_port, + seq, + ack, + data_offset, + tcp_flags, + window, + [0, 0], + 0, + ); + let hdr_bytes = unsafe { + std::slice::from_raw_parts(&hdr as *const TcpHeader as *const u8, TCP_HEADER_LEN) + }; + buf[tcp_off..tcp_off + TCP_HEADER_LEN].copy_from_slice(hdr_bytes); + + // Options. + if !tcp_options.is_empty() { + buf[tcp_off + TCP_HEADER_LEN..tcp_off + TCP_HEADER_LEN + tcp_options.len()] + .copy_from_slice(tcp_options); + } + + // Payload. + if !payload.is_empty() { + buf[tcp_off + tcp_header_len..tcp_off + tcp_header_len + payload.len()] + .copy_from_slice(payload); + } + + // TCP checksum (covers header + payload). + let tcp_segment = &mut buf[tcp_off..]; + let cksum = compute_tcp_checksum(&src_ip, &dst_ip, tcp_segment); + buf[tcp_off + 16] = cksum[0]; + buf[tcp_off + 17] = cksum[1]; + + buf +} + +/// Leak data for test frames — avoids lifetime issues with frame buffers. +pub(super) fn leak(data: Vec) -> &'static mut [u8] { + Box::leak(data.into_boxed_slice()) +} + +pub(super) fn alloc_free_frame(addr: u64) -> Frame<'static> { + Frame::new(addr, leak(vec![0u8; 256]), 256, false) +} + +/// Build a 12-byte TCP timestamp option (NOP NOP TSopt) for use in test frames. +pub(super) fn build_ts_option(tsval: u32, tsecr: u32) -> Vec { + let mut opt = vec![1u8, 1]; // NOP, NOP (alignment) + opt.push(8); // kind = TIMESTAMP + opt.push(10); // length = 10 + opt.extend_from_slice(&tsval.to_be_bytes()); + opt.extend_from_slice(&tsecr.to_be_bytes()); + opt +} + +/// Helper: complete a 3-way handshake and return server_iss. +/// Drains tx after handshake so callers start with an empty tx buffer. +pub(super) fn establish_connection( + handler: &mut TcpHandler, + nh: &NeighborHandler, + free: &mut BasicFrameBuffer<'static>, + rx: &mut BasicFrameBuffer<'static>, + tx: &mut BasicFrameBuffer<'static>, +) -> u32 { + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + nh, + free, + rx, + tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + nh, + free, + rx, + tx, + ); + while tx.pop().is_some() {} + server_iss +} + +/// Helper: establish a connection with SACK enabled, returning server_iss. +pub(super) fn establish_connection_with_sack( + handler: &mut TcpHandler, + nh: &NeighborHandler, + free: &mut BasicFrameBuffer<'static>, + rx: &mut BasicFrameBuffer<'static>, + tx: &mut BasicFrameBuffer<'static>, +) -> u32 { + use crate::net::wire::tcp::options as tcp_options; + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let sack_perm_opts = [tcp_options::SACK_PERMITTED, 2]; + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &sack_perm_opts, + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + nh, + free, + rx, + tx, + ); + assert!(handler.connections[0].sack_enabled); + let server_iss = handler.connections[0].iss; + while tx.pop().is_some() {} + + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + nh, + free, + rx, + tx, + ); + while tx.pop().is_some() {} + assert_eq!(handler.connections[0].state, TcpState::Established); + server_iss +} + +/// Helper: perform active open handshake via connect + SYN-ACK processing. +/// Returns the client ISS (so the caller knows snd_una/snd_nxt base). +pub(super) fn active_open_handshake( + handler: &mut TcpHandler, + nh: &NeighborHandler, + free: &mut BasicFrameBuffer<'static>, + rx: &mut BasicFrameBuffer<'static>, + tx: &mut BasicFrameBuffer<'static>, +) -> u32 { + let src_mac = + crate::net::wire::ethernet::MacAddress::from([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]); + let dst_mac = + crate::net::wire::ethernet::MacAddress::from([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]); + + // connect sends SYN. + let _event_queue = handler + .connect( + IpAddress::V4(LOCAL_IP), + 5000, + IpAddress::V4(REMOTE_IP), + 80, + src_mac, + dst_mac, + coarsetime::Instant::now(), + free, + tx, + ) + .unwrap(); + while tx.pop().is_some() {} // consume SYN frame + + let client_iss = handler.connections[0].iss; + + // Feed SYN-ACK from the remote. + let syn_ack = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 80, + 5000, + 2000, + client_iss.wrapping_add(1), + flags::SYN | flags::ACK, + 65535, + &[], + ); + let syn_ack_len = syn_ack.len(); + handler.process_ipv4( + Frame::new(50, leak(syn_ack), syn_ack_len, false), + coarsetime::Instant::now(), + nh, + free, + rx, + tx, + ); + while tx.pop().is_some() {} // consume ACK frame + + assert_eq!(handler.connections[0].state, TcpState::Established); + handler.connections[0].snd_wnd = 65535; + + client_iss +} + +/// Helper: perform active open handshake with custom TcpConfig. +pub(super) fn active_open_handshake_with_config( + handler: &mut TcpHandler, + nh: &NeighborHandler, + config: TcpConfig, + free: &mut BasicFrameBuffer<'static>, + rx: &mut BasicFrameBuffer<'static>, + tx: &mut BasicFrameBuffer<'static>, +) -> u32 { + let src_mac = + crate::net::wire::ethernet::MacAddress::from([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]); + let dst_mac = + crate::net::wire::ethernet::MacAddress::from([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]); + + let _event_queue = handler + .connect_with_config( + IpAddress::V4(LOCAL_IP), + 5000, + IpAddress::V4(REMOTE_IP), + 80, + src_mac, + dst_mac, + coarsetime::Instant::now(), + config, + free, + tx, + ) + .unwrap(); + while tx.pop().is_some() {} + + let client_iss = handler.connections[0].iss; + + let syn_ack = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 80, + 5000, + 2000, + client_iss.wrapping_add(1), + flags::SYN | flags::ACK, + 65535, + &[], + ); + let syn_ack_len = syn_ack.len(); + handler.process_ipv4( + Frame::new(50, leak(syn_ack), syn_ack_len, false), + coarsetime::Instant::now(), + nh, + free, + rx, + tx, + ); + while tx.pop().is_some() {} + + assert_eq!(handler.connections[0].state, TcpState::Established); + handler.connections[0].snd_wnd = 65535; + + client_iss +} + +#[test] +fn segment_acceptability_zero_len_zero_wnd() { + assert!(is_segment_acceptable(100, 0, 100, 0)); + assert!(!is_segment_acceptable(101, 0, 100, 0)); +} + +#[test] +fn segment_acceptability_zero_len_nonzero_wnd() { + assert!(is_segment_acceptable(100, 0, 100, 1000)); + assert!(is_segment_acceptable(1099, 0, 100, 1000)); + assert!(!is_segment_acceptable(1100, 0, 100, 1000)); + assert!(!is_segment_acceptable(99, 0, 100, 1000)); +} + +#[test] +fn segment_acceptability_nonzero_len_zero_wnd() { + assert!(!is_segment_acceptable(100, 10, 100, 0)); +} + +#[test] +fn segment_acceptability_nonzero_len_nonzero_wnd() { + // Start in window + assert!(is_segment_acceptable(100, 10, 100, 1000)); + // End in window (start slightly before) + assert!(is_segment_acceptable(95, 10, 100, 1000)); + // Completely outside + assert!(!is_segment_acceptable(1200, 10, 100, 1000)); + // Completely before + assert!(!is_segment_acceptable(80, 10, 100, 1000)); +} + +mod congestion_tests; +mod data_transfer; +mod delayed_ack; +mod ecn; +mod edge_cases; +mod handshake; +mod keepalive; +mod nagle; +mod persist; +mod retransmission; +mod sack; +mod teardown; +mod timestamps; diff --git a/src/net/handler/tcp/tests/nagle.rs b/src/net/handler/tcp/tests/nagle.rs new file mode 100644 index 0000000..7cd6815 --- /dev/null +++ b/src/net/handler/tcp/tests/nagle.rs @@ -0,0 +1,127 @@ +use super::*; + +#[test] +fn nagle_holds_small_data_when_bytes_in_flight() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // 1. Complete handshake via active open. + let _iss = active_open_handshake(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + // 2. Write small data. + handler.connections[0].send_buffer.write(b"hello"); + + // 3. poll_send — first send goes (nothing in flight). + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + assert_eq!(tx.num_frames(), 1, "first small segment should send"); + + // 4. Pop tx frame. + while tx.pop().is_some() {} + + // 5. Write more small data — bytes still in flight (unACKed). + handler.connections[0].send_buffer.write(b"world"); + + // 6. poll_send — Nagle holds it. + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + assert_eq!( + tx.num_frames(), + 0, + "Nagle should hold small data when bytes in flight" + ); +} + +#[test] +fn nagle_allows_full_mss_even_with_bytes_in_flight() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + // Allocate frames large enough for MSS-sized segments (ETH+IP+TCP+536 = 590). + for i in 0..16 { + free.push(Frame::new(100 + i, leak(vec![0u8; 1024]), 1024, false)); + } + + // 1. Complete handshake via active open. + let _iss = active_open_handshake(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + // 2. Write small data, send it (creates bytes_in_flight), pop tx. + handler.connections[0].send_buffer.write(b"hi"); + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + while tx.pop().is_some() {} + + // 3. Write MSS-worth of data. + let mss = handler.connections[0].eff_snd_mss as usize; + let mss_data = vec![0xAA; mss]; + handler.connections[0].send_buffer.write(&mss_data); + + // 4. poll_send — full MSS always sends even with bytes in flight. + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + assert_eq!( + tx.num_frames(), + 1, + "full MSS segment should send even with bytes in flight" + ); +} + +#[test] +fn tcp_no_delay_sends_small_data_immediately() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // 1. Complete handshake with tcp_no_delay. + let config = TcpConfig { + tcp_no_delay: true, + ..Default::default() + }; + let _iss = active_open_handshake_with_config( + &mut handler, + &nh, + config, + &mut free, + &mut rx, + &mut tx, + ); + + // Verify nagle is disabled. + assert!( + !handler.connections[0].nagle_enabled, + "nagle should be disabled with tcp_no_delay" + ); + + // 2. Write small data, poll_send (first send), pop tx. + handler.connections[0].send_buffer.write(b"hello"); + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + assert_eq!(tx.num_frames(), 1); + while tx.pop().is_some() {} + + // 3. Write more small data while first is in flight. + handler.connections[0].send_buffer.write(b"world"); + + // 4. poll_send — TCP_NODELAY bypasses Nagle. + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + assert_eq!( + tx.num_frames(), + 1, + "TCP_NODELAY should bypass Nagle and send immediately" + ); +} + diff --git a/src/net/handler/tcp/tests/persist.rs b/src/net/handler/tcp/tests/persist.rs new file mode 100644 index 0000000..827b39e --- /dev/null +++ b/src/net/handler/tcp/tests/persist.rs @@ -0,0 +1,129 @@ +use super::*; + +#[test] +fn persist_timer_activates_on_zero_window() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + let _server_iss = establish_connection(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + // Write data into send buffer, but set window to 0. + handler.connections[0].send_buffer.write(b"Hello"); + handler.connections[0].snd_wnd = 0; + + assert!(handler.connections[0].persist_deadline.is_none()); + + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + + // Persist timer should now be armed. + assert!( + handler.connections[0].persist_deadline.is_some(), + "persist_deadline should be set when window=0 and data available" + ); + // No data segment should have been sent (deadline not yet reached). + assert_eq!(tx.num_frames(), 0, "no segment sent before deadline"); +} + +#[test] +fn persist_probe_sent_when_deadline_expires() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + let server_iss = establish_connection(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + // Write data, set window to 0. + handler.connections[0].send_buffer.write(b"Hello"); + handler.connections[0].snd_wnd = 0; + + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + assert!(handler.connections[0].persist_deadline.is_some()); + assert_eq!(handler.connections[0].persist_backoff, 0); + + // Simulate time passing beyond the deadline by setting it to the past. + handler.connections[0].persist_deadline = Some(now); + + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + + // A 1-byte probe should have been sent. + assert_eq!(tx.num_frames(), 1, "probe segment should be sent"); + // snd_nxt should advance by 1. + assert_eq!( + handler.connections[0].snd_nxt, + server_iss.wrapping_add(1).wrapping_add(1), + "snd_nxt advanced by 1 for probe" + ); + // persist_backoff should have incremented. + assert_eq!(handler.connections[0].persist_backoff, 1); + // persist_deadline should be rescheduled (not None). + assert!(handler.connections[0].persist_deadline.is_some()); +} + +#[test] +fn persist_timer_clears_when_window_reopens() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + let server_iss = establish_connection(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + // Write data, set window to 0, arm persist timer. + handler.connections[0].send_buffer.write(b"Hello"); + handler.connections[0].snd_wnd = 0; + + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + assert!(handler.connections[0].persist_deadline.is_some()); + handler.connections[0].persist_backoff = 3; // simulate some backoff + + // Peer sends ACK with non-zero window, reopening it. + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 32000, // non-zero window + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(2, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // Persist timer should be cleared. + assert!( + handler.connections[0].persist_deadline.is_none(), + "persist_deadline should be cleared when window reopens" + ); + assert_eq!( + handler.connections[0].persist_backoff, 0, + "persist_backoff should be reset" + ); +} + diff --git a/src/net/handler/tcp/tests/retransmission.rs b/src/net/handler/tcp/tests/retransmission.rs new file mode 100644 index 0000000..eb4ac41 --- /dev/null +++ b/src/net/handler/tcp/tests/retransmission.rs @@ -0,0 +1,412 @@ +use super::*; + +#[test] +fn fast_retransmit_on_three_dup_acks() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + + // Put data in send buffer and send it. + handler.connections[0].send_buffer.write(b"AAAA"); + handler.connections[0].snd_wnd = 65535; + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + while tx.pop().is_some() {} // consume sent segment + + let cwnd_before = handler.connections[0].cubic.cwnd; + + // Send 3 duplicate ACKs (ACKing the old snd_una, not the new data). + let dup_ack_seq = server_iss.wrapping_add(1); // original snd_una + for i in 0..3u64 { + let dup = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + dup_ack_seq, + flags::ACK, + 65535, + &[], + ); + let dup_len = dup.len(); + handler.process_ipv4( + Frame::new(10 + i, leak(dup), dup_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + } + + // Recovery entry now happens at dup ACK processing time (not poll_timers). + let tcb = &handler.connections[0]; + assert_eq!(tcb.recovery.dup_ack_count, 3); + assert!(tcb.recovery.in_recovery, "should be in SACK recovery"); + // CUBIC beta=0.7: cwnd = ssthresh = cwnd_before * 0.7. + assert!( + tcb.cubic.cwnd < cwnd_before, + "cwnd should be reduced after recovery entry" + ); + assert_eq!( + tcb.cubic.cwnd, tcb.cubic.ssthresh, + "cwnd should equal ssthresh after CUBIC on_loss" + ); +} + +#[test] +fn rto_retransmit_on_timer_expiry() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + + // Put data in send buffer and send it. + handler.connections[0].send_buffer.write(b"BBBB"); + handler.connections[0].snd_wnd = 65535; + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + while tx.pop().is_some() {} + + let cwnd_before = handler.connections[0].cubic.cwnd; + + // Simulate timer expiry by setting a deadline in the past. + handler.connections[0].retransmit_deadline = + Some(now - coarsetime::Duration::from_millis(1)); + handler.connections[0].rto_backoff = 0; + + // poll_timers should trigger RTO retransmit. + handler.poll_timers(now, nh.local_mac(), &nh, &mut free, &mut tx); + assert!(tx.num_frames() >= 1, "retransmitted segment expected"); + + let tcb = &handler.connections[0]; + // cwnd should be reset to 1 MSS (slow start). + assert_eq!( + tcb.cubic.cwnd, tcb.eff_snd_mss as u32, + "cwnd should be 1 MSS after RTO" + ); + assert!( + tcb.cubic.ssthresh < cwnd_before, + "ssthresh should be reduced" + ); + assert_eq!(tcb.rto_backoff, 1, "rto_backoff should be incremented"); +} + +#[test] +fn rtt_estimation_updates_rto() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + + // Send data. + handler.connections[0].send_buffer.write(b"test data"); + handler.connections[0].snd_wnd = 65535; + let send_time = coarsetime::Instant::now(); + handler.poll_send(send_time, nh.local_mac(), &nh, &mut free, &mut tx); + while tx.pop().is_some() {} + + // Verify last_send_time is set. + assert!( + handler.connections[0].last_send_time.is_some(), + "last_send_time should be set after poll_send" + ); + + // ACK the data. + let new_ack = server_iss.wrapping_add(1).wrapping_add(9); // ISS+1 + 9 bytes + let ack = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + new_ack, + flags::ACK, + 65535, + &[], + ); + let ack_len = ack.len(); + let recv_time = coarsetime::Instant::now(); + handler.process_ipv4( + Frame::new(5, leak(ack), ack_len, false), + recv_time, + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // Verify RTT was measured. + let tcb = &handler.connections[0]; + assert!( + tcb.srtt.is_some(), + "srtt should be set after first RTT measurement" + ); + assert!( + tcb.last_send_time.is_none(), + "last_send_time should be consumed" + ); + // RTO should be at least 1000ms (the minimum clamp). + assert!(tcb.rto >= 1000, "rto should be at least 1000ms"); + assert!(tcb.rto <= 60_000, "rto should be at most 60000ms"); +} + +#[test] +fn limited_transmit_sends_on_first_dup_ack() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(64); + let mut rx = BasicFrameBuffer::new(64); + let mut tx = BasicFrameBuffer::new(64); + for i in 0..32 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + + let mss = handler.connections[0].eff_snd_mss as usize; + // Fill send buffer with 6 MSS of data, set cwnd to 3*MSS. + handler.connections[0] + .send_buffer + .write(&vec![0xAA; mss * 6]); + handler.connections[0].snd_wnd = 65535; + handler.connections[0].cubic.cwnd = (mss * 3) as u32; + + // Send 3 segments (fills cwnd). + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + while tx.pop().is_some() {} + + let snd_nxt_before = handler.connections[0].snd_nxt; + let snd_una = handler.connections[0].snd_una; + + // First dup ACK. + let dup = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + snd_una, + flags::ACK, + 65535, + &[], + ); + let dup_len = dup.len(); + handler.process_ipv4( + Frame::new(10, leak(dup), dup_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + assert_eq!(handler.connections[0].recovery.dup_ack_count, 1); + + // poll_send should allow 1 MSS of new data (limited transmit). + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + + let snd_nxt_after = handler.connections[0].snd_nxt; + assert_eq!( + snd_nxt_after.wrapping_sub(snd_nxt_before) as usize, + mss, + "limited transmit: 1 MSS sent on first dup ACK" + ); +} + diff --git a/src/net/handler/tcp/tests/sack.rs b/src/net/handler/tcp/tests/sack.rs new file mode 100644 index 0000000..5c1cba1 --- /dev/null +++ b/src/net/handler/tcp/tests/sack.rs @@ -0,0 +1,738 @@ +use super::*; + +#[test] +fn ooo_data_sends_sack_blocks_in_dup_ack() { + use crate::net::wire::tcp::{options as tcp_options, parse_sack_blocks}; + + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake with SACK_PERMITTED in SYN options. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let sack_perm_opts = [tcp_options::SACK_PERMITTED, 2]; + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &sack_perm_opts, + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + assert!( + handler.connections[0].sack_enabled, + "SACK should be negotiated" + ); + let server_iss = handler.connections[0].iss; + // Drain SYN-ACK. + while tx.pop().is_some() {} + + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + + assert_eq!(handler.connections[0].state, TcpState::Established); + + // Send out-of-order segment: seq=1011, 5 bytes (gap from 1001..1011). + let ooo_seg = build_tcp_frame_with_payload( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1011, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + b"world", + ); + let ooo_len = ooo_seg.len(); + handler.process_ipv4( + Frame::new(2, leak(ooo_seg), ooo_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // Should have emitted a dup ACK with SACK blocks. + assert_eq!(tx.num_frames(), 1, "OOO data triggers dup ACK"); + let ack_frame = tx.pop().unwrap(); + + let tcp_offset = ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN; + let tcp = unsafe { TcpHeader::from_bytes_at(&ack_frame, tcp_offset) }; + assert_eq!(tcp.flags(), flags::ACK); + assert_eq!(tcp.ack_num(), 1001, "dup ACK has original rcv_nxt"); + + // Parse SACK blocks from the TCP options. + let data_off_bytes = (tcp.data_offset() as usize) * 4; + let opt_len = data_off_bytes - TCP_HEADER_LEN; + assert!(opt_len > 0, "options should be present"); + let opt_start = tcp_offset + TCP_HEADER_LEN; + let tcp_opts = &ack_frame[opt_start..opt_start + opt_len]; + let (blocks, count) = parse_sack_blocks(tcp_opts); + assert_eq!(count, 1, "one SACK block expected"); + // Block should cover the OOO range: [1011, 1016). + assert_eq!(blocks[0], Some((1011, 1016))); +} + +#[test] +fn sack_blocks_update_scoreboard_on_ack() { + use crate::net::wire::tcp::write_sack_option; + + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + let _server_iss = + establish_connection_with_sack(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + // Put data in the send buffer and advance snd_nxt to simulate sent data. + let tcb = &mut handler.connections[0]; + tcb.send_buffer.write(&[0u8; 100]); + tcb.snd_nxt = tcb.snd_una.wrapping_add(100); + + // Build an ACK that advances snd_una by 10, with SACK blocks for [30..50) and [70..90). + let snd_una = tcb.snd_una; + let new_ack = snd_una.wrapping_add(10); + let sack_left1 = snd_una.wrapping_add(30); + let sack_right1 = snd_una.wrapping_add(50); + let sack_left2 = snd_una.wrapping_add(70); + let sack_right2 = snd_una.wrapping_add(90); + + let mut opts = [0u8; 20]; + let written = write_sack_option( + &mut opts, + &[(sack_left1, sack_right1), (sack_left2, sack_right2)], + ); + + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + new_ack, + flags::ACK, + 65535, + &opts[..written], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(2, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + let tcb = &handler.connections[0]; + assert_eq!(tcb.snd_una, new_ack, "snd_una should advance"); + assert_eq!( + tcb.sack_scoreboard.len(), + 2, + "two SACK blocks in scoreboard" + ); + assert_eq!(tcb.sack_scoreboard.get(&sack_left1), Some(&20)); + assert_eq!(tcb.sack_scoreboard.get(&sack_left2), Some(&20)); +} + +#[test] +fn sack_scoreboard_pruned_on_cumulative_ack_advance() { + use crate::net::wire::tcp::write_sack_option; + + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + let _server_iss = + establish_connection_with_sack(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + let tcb = &mut handler.connections[0]; + tcb.send_buffer.write(&[0u8; 200]); + tcb.snd_nxt = tcb.snd_una.wrapping_add(200); + let snd_una = tcb.snd_una; + + // First ACK: advance by 10, SACK blocks at [30..50) and [100..120). + let ack1 = snd_una.wrapping_add(10); + let sack_left1 = snd_una.wrapping_add(30); + let sack_right1 = snd_una.wrapping_add(50); + let sack_left2 = snd_una.wrapping_add(100); + let sack_right2 = snd_una.wrapping_add(120); + + let mut opts = [0u8; 20]; + let written = write_sack_option( + &mut opts, + &[(sack_left1, sack_right1), (sack_left2, sack_right2)], + ); + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + ack1, + flags::ACK, + 65535, + &opts[..written], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(2, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(handler.connections[0].sack_scoreboard.len(), 2); + + // Second ACK: advance cumulative ACK past the first SACK block (to 50). + // Include the second block again. + let ack2 = snd_una.wrapping_add(50); + let mut opts2 = [0u8; 12]; + let written2 = write_sack_option(&mut opts2, &[(sack_left2, sack_right2)]); + let ack_data2 = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + ack2, + flags::ACK, + 65535, + &opts2[..written2], + ); + let ack_len2 = ack_data2.len(); + handler.process_ipv4( + Frame::new(3, leak(ack_data2), ack_len2, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + let tcb = &handler.connections[0]; + assert_eq!(tcb.snd_una, ack2); + // The first block (start=snd_una+30) should be pruned since 30 < 50. + assert!( + !tcb.sack_scoreboard.contains_key(&sack_left1), + "old block should be pruned" + ); + // The second block should remain. + assert_eq!(tcb.sack_scoreboard.len(), 1, "only second block remains"); + assert!(tcb.sack_scoreboard.contains_key(&sack_left2)); +} + +#[test] +fn sack_blocks_updated_on_dup_ack() { + use crate::net::wire::tcp::write_sack_option; + + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + let _server_iss = + establish_connection_with_sack(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + let tcb = &mut handler.connections[0]; + tcb.send_buffer.write(&[0u8; 100]); + tcb.snd_nxt = tcb.snd_una.wrapping_add(100); + let snd_una = tcb.snd_una; + + // Send a duplicate ACK (same ack number, no payload) with SACK block. + let sack_left = snd_una.wrapping_add(20); + let sack_right = snd_una.wrapping_add(40); + let mut opts = [0u8; 12]; + let written = write_sack_option(&mut opts, &[(sack_left, sack_right)]); + + let dup_ack = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + snd_una, + flags::ACK, + 65535, + &opts[..written], + ); + let dup_len = dup_ack.len(); + handler.process_ipv4( + Frame::new(2, leak(dup_ack), dup_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + let tcb = &handler.connections[0]; + assert_eq!(tcb.recovery.dup_ack_count, 1); + assert_eq!(tcb.sack_scoreboard.len(), 1); + assert_eq!(tcb.sack_scoreboard.get(&sack_left), Some(&20)); +} + +#[test] +fn sack_scoreboard_cleared_on_rto() { + use crate::net::wire::tcp::write_sack_option; + + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + let _server_iss = + establish_connection_with_sack(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + let tcb = &mut handler.connections[0]; + tcb.send_buffer.write(&[0u8; 100]); + tcb.snd_nxt = tcb.snd_una.wrapping_add(100); + let snd_una = tcb.snd_una; + + // Send a dup ACK with SACK blocks to populate the scoreboard. + let sack_left = snd_una.wrapping_add(20); + let sack_right = snd_una.wrapping_add(40); + let mut opts = [0u8; 12]; + let written = write_sack_option(&mut opts, &[(sack_left, sack_right)]); + + let dup_ack = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + snd_una, + flags::ACK, + 65535, + &opts[..written], + ); + let dup_len = dup_ack.len(); + handler.process_ipv4( + Frame::new(2, leak(dup_ack), dup_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(handler.connections[0].sack_scoreboard.len(), 1); + + // Set up RTO: arm the retransmit deadline in the past. + let now = coarsetime::Instant::now(); + handler.connections[0].retransmit_deadline = Some(now); + handler.connections[0].rto_backoff = 0; + + // Trigger poll_timers, which should fire the RTO and clear the scoreboard. + handler.poll_timers(now, nh.local_mac(), &nh, &mut free, &mut tx); + + assert!( + handler.connections[0].sack_scoreboard.is_empty(), + "scoreboard should be cleared on RTO" + ); +} + +#[test] +fn fast_retransmit_uses_sack_gap() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + let _server_iss = + establish_connection_with_sack(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + let tcb = &mut handler.connections[0]; + // Use a small effective MSS so the retransmit fits in a 256-byte frame. + tcb.eff_snd_mss = 100; + let mss = tcb.eff_snd_mss as usize; + + // Fill send buffer with 5 MSS worth of data. + let mut data = vec![0u8; 5 * mss]; + for (i, byte) in data.iter_mut().enumerate() { + *byte = (i / mss) as u8; + } + tcb.send_buffer.write(&data); + tcb.snd_nxt = tcb.snd_una.wrapping_add(5 * mss as u32); + let snd_una = tcb.snd_una; + + // SACK blocks for MSS 2, 3, 4 (gap is the 1st MSS). + // This gives 3 SACKed segments above snd_una, satisfying DupThresh. + tcb.sack_scoreboard + .insert(snd_una.wrapping_add(mss as u32), mss as u32); + tcb.sack_scoreboard + .insert(snd_una.wrapping_add(2 * mss as u32), mss as u32); + tcb.sack_scoreboard + .insert(snd_una.wrapping_add(3 * mss as u32), mss as u32); + + // Enter SACK recovery (simulating what happens on 3 dup ACKs). + let cwnd_before = tcb.cubic.cwnd; + tcb.recovery.dup_ack_count = 3; + tcb.recovery.enter(tcb.snd_nxt); + tcb.prr.enter(tcb.snd_nxt.wrapping_sub(tcb.snd_una)); + tcb.cubic.on_loss(); + + let now = coarsetime::Instant::now(); + handler.poll_timers(now, nh.local_mac(), &nh, &mut free, &mut tx); + + // Verify a segment was emitted (the 1st MSS gap should be retransmitted). + assert!(tx.pop().is_some(), "expected a retransmitted segment"); + + let tcb = &handler.connections[0]; + assert!(tcb.recovery.in_recovery, "should still be in recovery"); + // cwnd should be reduced by CUBIC on_loss (beta=0.7). + assert!( + tcb.cubic.cwnd < cwnd_before, + "cwnd should have been reduced" + ); + assert_eq!( + tcb.cubic.cwnd, tcb.cubic.ssthresh, + "cwnd should equal ssthresh after CUBIC on_loss" + ); +} + +#[test] +fn fast_retransmit_fallback_when_scoreboard_empty() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + let _server_iss = + establish_connection_with_sack(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + let tcb = &mut handler.connections[0]; + // Use a small effective MSS so the retransmit fits in a 256-byte frame. + tcb.eff_snd_mss = 100; + let mss = tcb.eff_snd_mss as usize; + + // Fill send buffer. + tcb.send_buffer.write(&vec![0xABu8; 3 * mss]); + tcb.snd_nxt = tcb.snd_una.wrapping_add(3 * mss as u32); + + // Record cwnd before. + let cwnd_before = tcb.cubic.cwnd; + + // Scoreboard is empty; enter recovery manually. + assert!(tcb.sack_scoreboard.is_empty()); + tcb.recovery.dup_ack_count = 3; + tcb.recovery.enter(tcb.snd_nxt); + tcb.prr.enter(tcb.snd_nxt.wrapping_sub(tcb.snd_una)); + tcb.cubic.on_loss(); + + let now = coarsetime::Instant::now(); + handler.poll_timers(now, nh.local_mac(), &nh, &mut free, &mut tx); + + // With empty scoreboard, next_lost_segment returns None (nothing marked lost + // by RFC 6675 criteria), so no retransmit is emitted from the recovery loop. + // This is correct: without SACK blocks, nothing can be determined as lost. + let tcb = &handler.connections[0]; + assert!(tcb.recovery.in_recovery, "should still be in recovery"); + assert!( + tcb.cubic.cwnd < cwnd_before, + "cwnd should have been reduced" + ); + assert_eq!( + tcb.cubic.cwnd, tcb.cubic.ssthresh, + "cwnd should equal ssthresh after CUBIC on_loss" + ); +} + +#[test] +fn sack_recovery_enters_on_3_dup_acks() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(64); + let mut rx = BasicFrameBuffer::new(64); + let mut tx = BasicFrameBuffer::new(64); + for i in 0..32 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + + // Use small MSS so segments fit in 256-byte test frames. + let mss = 100u16; + handler.connections[0].eff_snd_mss = mss; + + // Write 4 MSS of data and simulate 4 segments sent by advancing snd_nxt. + let data_len = 4 * mss as usize; + handler.connections[0] + .send_buffer + .write(&vec![0xAA; data_len]); + handler.connections[0].snd_wnd = 65535; + handler.connections[0].snd_nxt = + handler.connections[0].snd_una.wrapping_add(data_len as u32); + + let snd_una = handler.connections[0].snd_una; + let cwnd_before = handler.connections[0].cubic.cwnd; + + // Send 3 dup ACKs. + for i in 0..3 { + let dup = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + snd_una, + flags::ACK, + 65535, + &[], + ); + let dup_len = dup.len(); + handler.process_ipv4( + Frame::new(10 + i, leak(dup), dup_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + } + + let tcb = &handler.connections[0]; + assert!(tcb.recovery.in_recovery, "should be in SACK recovery"); + assert_eq!(tcb.recovery.dup_ack_count, 3); + // CUBIC beta=0.7: cwnd should be reduced by on_loss. + assert!( + tcb.cubic.cwnd < cwnd_before, + "cwnd should be reduced by CUBIC on_loss" + ); +} + +#[test] +fn sack_recovery_partial_ack_stays_in_recovery() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(64); + let mut rx = BasicFrameBuffer::new(64); + let mut tx = BasicFrameBuffer::new(64); + for i in 0..32 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + + // Use small MSS so segments fit in 256-byte test frames. + let mss = 100u16; + handler.connections[0].eff_snd_mss = mss; + + // Write 4 MSS of data and simulate 4 segments sent by advancing snd_nxt. + let data_len = 4 * mss as usize; + handler.connections[0] + .send_buffer + .write(&vec![0xAA; data_len]); + handler.connections[0].snd_wnd = 65535; + handler.connections[0].snd_nxt = + handler.connections[0].snd_una.wrapping_add(data_len as u32); + + let snd_una = handler.connections[0].snd_una; + + // 3 dup ACKs -> enter recovery. + for i in 0..3 { + let dup = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + snd_una, + flags::ACK, + 65535, + &[], + ); + let dup_len = dup.len(); + handler.process_ipv4( + Frame::new(10 + i, leak(dup), dup_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + } + assert!(handler.connections[0].recovery.in_recovery); + + // Partial ACK — advances snd_una by 1 MSS but doesn't reach recovery_point. + let partial_ack_seq = snd_una.wrapping_add(mss as u32); + let partial = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + partial_ack_seq, + flags::ACK, + 65535, + &[], + ); + let partial_len = partial.len(); + handler.process_ipv4( + Frame::new(20, leak(partial), partial_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + assert!( + handler.connections[0].recovery.in_recovery, + "should still be in recovery after partial ACK" + ); + assert_eq!(handler.connections[0].snd_una, partial_ack_seq); +} + diff --git a/src/net/handler/tcp/tests/teardown.rs b/src/net/handler/tcp/tests/teardown.rs new file mode 100644 index 0000000..6fadc8a --- /dev/null +++ b/src/net/handler/tcp/tests/teardown.rs @@ -0,0 +1,1741 @@ +use super::*; + +#[test] +fn established_receives_fin_transitions_to_close_wait() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + + // Remote sends FIN. + let fin_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK | flags::FIN, + 65535, + &[], + ); + let fin_len = fin_data.len(); + handler.process_ipv4( + Frame::new(2, leak(fin_data), fin_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + assert_eq!(handler.connections[0].state, TcpState::CloseWait); + assert_eq!(handler.connections[0].rcv_nxt, 1002); // 1001 + FIN=1 + assert_eq!(tx.num_frames(), 1, "ACK for FIN sent"); +} + +#[test] +fn established_receives_fin_with_data() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + + // Remote sends data + FIN piggybacked. + let payload = b"goodbye"; + let fin_data = build_tcp_frame_with_payload( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK | flags::FIN, + 65535, + &[], + payload, + ); + let fin_len = fin_data.len(); + handler.process_ipv4( + Frame::new(2, leak(fin_data), fin_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + assert_eq!(handler.connections[0].state, TcpState::CloseWait); + assert_eq!( + handler.connections[0].recv_buffer.available(), + payload.len() + ); + // rcv_nxt = 1001 + 7 bytes data + 1 FIN = 1009 + assert_eq!( + handler.connections[0].rcv_nxt, + 1001 + payload.len() as u32 + 1 + ); +} + +#[test] +fn poll_send_sends_fin_when_pending() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + + // Set pending_fin. + handler.connections[0].pending_fin = true; + + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + + // FIN should have been sent. + assert_eq!(tx.num_frames(), 1, "FIN segment sent"); + let tcb = &handler.connections[0]; + assert_eq!(tcb.state, TcpState::FinWait1); + assert!(!tcb.pending_fin, "pending_fin consumed"); + assert!(tcb.fin_seq.is_some(), "fin_seq recorded"); +} + +#[test] +fn poll_send_drains_data_before_fin() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + + // Write data AND set pending_fin. + handler.connections[0].send_buffer.write(b"final data"); + handler.connections[0].snd_wnd = 65535; + handler.connections[0].pending_fin = true; + + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + + // Should send data first, NOT FIN yet (data still in flight). + assert_eq!(tx.num_frames(), 1, "data segment sent"); + assert_eq!( + handler.connections[0].state, + TcpState::Established, + "still Established until data ACKed" + ); + assert!(handler.connections[0].pending_fin, "pending_fin still set"); +} + +#[test] +fn active_close_fin_wait1_to_fin_wait2() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + + // Active close: set pending_fin, poll_send sends FIN → FinWait1. + handler.connections[0].pending_fin = true; + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + while tx.pop().is_some() {} + assert_eq!(handler.connections[0].state, TcpState::FinWait1); + let fin_seq = handler.connections[0].fin_seq.unwrap(); + + // Remote ACKs our FIN → FinWait2. + let ack = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + fin_seq.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack.len(); + handler.process_ipv4( + Frame::new(3, leak(ack), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + assert_eq!(handler.connections[0].state, TcpState::FinWait2); +} + +#[test] +fn fin_wait2_receives_fin_to_time_wait() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + + // Active close → FinWait1 → FinWait2. + handler.connections[0].pending_fin = true; + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + while tx.pop().is_some() {} + let fin_seq = handler.connections[0].fin_seq.unwrap(); + let ack = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + fin_seq.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack.len(); + handler.process_ipv4( + Frame::new(3, leak(ack), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(handler.connections[0].state, TcpState::FinWait2); + while tx.pop().is_some() {} + + // Remote sends FIN → TimeWait. + let fin = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + fin_seq.wrapping_add(1), + flags::ACK | flags::FIN, + 65535, + &[], + ); + let fin_len = fin.len(); + handler.process_ipv4( + Frame::new(4, leak(fin), fin_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + assert_eq!(handler.connections[0].state, TcpState::TimeWait); + assert!(handler.connections[0].time_wait_deadline.is_some()); + assert_eq!(tx.num_frames(), 1, "ACK for remote FIN"); +} + +#[test] +fn simultaneous_close_closing_to_time_wait() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + + // Active close → FinWait1. + handler.connections[0].pending_fin = true; + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + while tx.pop().is_some() {} + assert_eq!(handler.connections[0].state, TcpState::FinWait1); + + // Simultaneous close: remote sends FIN without ACKing ours → Closing. + let fin = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK | flags::FIN, + 65535, + &[], + ); + let fin_len = fin.len(); + handler.process_ipv4( + Frame::new(3, leak(fin), fin_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(handler.connections[0].state, TcpState::Closing); + while tx.pop().is_some() {} + + // Remote ACKs our FIN → TimeWait. + let fin_seq = handler.connections[0].fin_seq.unwrap(); + let ack = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1002, + fin_seq.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack.len(); + handler.process_ipv4( + Frame::new(4, leak(ack), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(handler.connections[0].state, TcpState::TimeWait); +} + +#[test] +fn passive_close_last_ack_removes_connection() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + + // Remote sends FIN → CloseWait. + let fin = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK | flags::FIN, + 65535, + &[], + ); + let fin_len = fin.len(); + handler.process_ipv4( + Frame::new(2, leak(fin), fin_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(handler.connections[0].state, TcpState::CloseWait); + while tx.pop().is_some() {} + + // We close → pending_fin, poll_send sends FIN → LastAck. + handler.connections[0].pending_fin = true; + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + while tx.pop().is_some() {} + assert_eq!(handler.connections[0].state, TcpState::LastAck); + let fin_seq = handler.connections[0].fin_seq.unwrap(); + + // Remote ACKs our FIN → connection removed. + let ack = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1002, + fin_seq.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack.len(); + handler.process_ipv4( + Frame::new(4, leak(ack), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!( + handler.connections.len(), + 0, + "connection removed after LastAck" + ); +} + +#[test] +fn time_wait_ignores_rst() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + + // Full active close → TimeWait. + handler.connections[0].pending_fin = true; + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + while tx.pop().is_some() {} + let fin_seq = handler.connections[0].fin_seq.unwrap(); + let ack = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + fin_seq.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack.len(); + handler.process_ipv4( + Frame::new(3, leak(ack), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let fin = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + fin_seq.wrapping_add(1), + flags::ACK | flags::FIN, + 65535, + &[], + ); + let fin_len = fin.len(); + handler.process_ipv4( + Frame::new(4, leak(fin), fin_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(handler.connections[0].state, TcpState::TimeWait); + while tx.pop().is_some() {} + + // RST in TIME-WAIT should be ignored. + let rst = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, 1002, 0, flags::RST, 0, &[]); + let rst_len = rst.len(); + handler.process_ipv4( + Frame::new(5, leak(rst), rst_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!( + handler.connections.len(), + 1, + "connection NOT removed by RST in TIME-WAIT" + ); + assert_eq!(handler.connections[0].state, TcpState::TimeWait); +} + +#[test] +fn time_wait_evicted_after_deadline() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + + // Force into TimeWait state with expired deadline. + handler.connections[0].state = TcpState::TimeWait; + handler.connections[0].time_wait_deadline = Some(coarsetime::Instant::now()); + + // Evict with a time in the future. + let future = coarsetime::Instant::now() + coarsetime::Duration::from_secs(120); + handler.evict_stale(future, &mut rx); + assert_eq!(handler.connections.len(), 0, "TIME-WAIT connection evicted"); +} + +#[test] +fn full_active_close_lifecycle() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + let initial_free = free.num_frames(); + + // 1. Handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(handler.connections[0].state, TcpState::Established); + + // 2. Data exchange. + let payload = b"hello"; + let data = build_tcp_frame_with_payload( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + payload, + ); + let data_len = data.len(); + handler.process_ipv4( + Frame::new(2, leak(data), data_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // 3. Active close. + handler.connections[0].pending_fin = true; + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + assert_eq!(handler.connections[0].state, TcpState::FinWait1); + while tx.pop().is_some() {} + + // 4. Remote ACKs our FIN -> FinWait2. + let fin_seq = handler.connections[0].fin_seq.unwrap(); + let ack = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1006, + fin_seq.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack.len(); + handler.process_ipv4( + Frame::new(3, leak(ack), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(handler.connections[0].state, TcpState::FinWait2); + + // 5. Remote sends FIN -> TimeWait. + let fin = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1006, + fin_seq.wrapping_add(1), + flags::ACK | flags::FIN, + 65535, + &[], + ); + let fin_len = fin.len(); + handler.process_ipv4( + Frame::new(4, leak(fin), fin_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(handler.connections[0].state, TcpState::TimeWait); + while tx.pop().is_some() {} + + // 6. TIME-WAIT expires -> connection removed. + let future = coarsetime::Instant::now() + coarsetime::Duration::from_secs(120); + handler.evict_stale(future, &mut rx); + assert_eq!( + handler.connections.len(), + 0, + "connection removed after TIME-WAIT" + ); + + // 7. Frame accounting: all frames accounted for. + // We injected 5 incoming frames (SYN, ACK, data, FIN-ACK, FIN) which end up in rx. + // The handler consumed free frames for outgoing segments (SYN-ACK, data ACK, FIN, + // ACK-for-FIN) which we drained from tx. So the total in free+rx+tx equals + // initial_free + incoming - outgoing_drained. + let total = free.num_frames() + rx.num_frames() + tx.num_frames(); + let outgoing_drained = initial_free + 5 - total; + assert!( + outgoing_drained > 0 && total > 0, + "no frames leaked: free={} rx={} tx={} outgoing_drained={}", + free.num_frames(), + rx.num_frames(), + tx.num_frames(), + outgoing_drained, + ); + assert_eq!( + total + outgoing_drained, + initial_free + 5, + "all frames accounted for (free={} rx={} tx={} outgoing_drained={})", + free.num_frames(), + rx.num_frames(), + tx.num_frames(), + outgoing_drained, + ); +} + +#[test] +fn full_passive_close_lifecycle() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // 1. Handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + + // 2. Remote sends FIN -> CloseWait. + let fin = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK | flags::FIN, + 65535, + &[], + ); + let fin_len = fin.len(); + handler.process_ipv4( + Frame::new(2, leak(fin), fin_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(handler.connections[0].state, TcpState::CloseWait); + while tx.pop().is_some() {} + + // 3. We close -> LastAck. + handler.connections[0].pending_fin = true; + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + assert_eq!(handler.connections[0].state, TcpState::LastAck); + while tx.pop().is_some() {} + let fin_seq = handler.connections[0].fin_seq.unwrap(); + + // 4. Remote ACKs our FIN -> connection removed. + let ack = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1002, + fin_seq.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack.len(); + handler.process_ipv4( + Frame::new(3, leak(ack), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!( + handler.connections.len(), + 0, + "connection removed after LastAck" + ); +} + +#[test] +fn shutdown_sets_pending_fin() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(16); + let mut rx = BasicFrameBuffer::new(16); + let mut tx = BasicFrameBuffer::new(16); + + for i in 0..8 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake to reach Established. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(handler.connections[0].state, TcpState::Established); + + // Verify pending_fin is initially false. + assert!( + !handler.connections[0].pending_fin, + "pending_fin should start false" + ); + + // Call initiate_close (the handler method that shutdown() delegates to). + let conn_id = handler.connections[0].id; + handler.initiate_close(&conn_id); + + // Verify pending_fin is now true. + assert!( + handler.connections[0].pending_fin, + "pending_fin should be true after initiate_close" + ); + + // Verify connection is still Established (FIN not yet sent). + assert_eq!( + handler.connections[0].state, + TcpState::Established, + "state should remain Established until poll_send" + ); +} + +#[test] +fn half_close_writes_blocked_reads_continue() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + assert_eq!(handler.connections[0].state, TcpState::Established); + + // Initiate half-close (shutdown write side). + let conn_id = handler.connections[0].id; + handler.initiate_close(&conn_id); + + // Verify pending_fin is set but state is still Established (FIN not sent yet). + assert!( + handler.connections[0].pending_fin, + "pending_fin should be true after initiate_close" + ); + assert_eq!( + handler.connections[0].state, + TcpState::Established, + "state should still be Established before poll_send" + ); + + // Write data directly into recv_buffer (simulating received data). + let incoming = b"data after half-close"; + handler.connections[0].recv_buffer.write(incoming); + + // Verify reads still work after half-close. + assert_eq!( + handler.connections[0].recv_buffer.available(), + incoming.len(), + "recv_buffer should still be readable after half-close" + ); + let mut buf = [0u8; 32]; + let read_len = handler.connections[0].recv_buffer.read(&mut buf); + assert_eq!(read_len, incoming.len(), "should read all data"); + assert_eq!( + &buf[..read_len], + incoming, + "read data should match written data" + ); +} + +#[test] +fn close_wait_processes_ack_for_sent_data() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + + // Write data into the send buffer. + let payload = b"half-close data!"; + handler.connections[0].send_buffer.write(payload); + handler.connections[0].snd_wnd = 65535; + + // poll_send to transmit data (advances snd_nxt). + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut tx); + while tx.pop().is_some() {} + + let snd_nxt_after_send = handler.connections[0].snd_nxt; + assert_eq!( + snd_nxt_after_send, + server_iss + .wrapping_add(1) + .wrapping_add(payload.len() as u32) + ); + // Data still in send buffer until ACKed. + assert_eq!( + handler.connections[0].send_buffer.available(), + payload.len() + ); + + // Transition to CloseWait by receiving FIN+ACK from remote. + let fin_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK | flags::FIN, + 65535, + &[], + ); + let fin_len = fin_data.len(); + handler.process_ipv4( + Frame::new(2, leak(fin_data), fin_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + assert_eq!(handler.connections[0].state, TcpState::CloseWait); + + // Now send an ACK for the data we sent (seg_ack = snd_nxt). + let data_ack = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1002, // 1001 + FIN consumes 1 + snd_nxt_after_send, + flags::ACK, + 32000, + &[], + ); + let data_ack_len = data_ack.len(); + handler.process_ipv4( + Frame::new(3, leak(data_ack), data_ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // snd_una should advance to snd_nxt (all data ACKed). + assert_eq!( + handler.connections[0].snd_una, snd_nxt_after_send, + "snd_una must advance to cover ACKed data" + ); + // Send buffer should be drained. + assert_eq!( + handler.connections[0].send_buffer.available(), + 0, + "send buffer must be drained after ACK" + ); + // Window should be updated. + assert_eq!( + handler.connections[0].snd_wnd, 32000, + "send window must be updated from ACK" + ); +} + +#[test] +fn fin_retransmitted_in_fin_wait1() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + for i in 0..16 { + free.push(alloc_free_frame(200 + i)); + } + + // Establish connection via passive open. + let _server_iss = establish_connection(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + let id = ConnectionId { + local_addr: IpAddress::V4(LOCAL_IP), + local_port: 80, + remote_addr: IpAddress::V4(REMOTE_IP), + remote_port: 12345, + }; + + // Manually transition TCB to FinWait1 with an expired retransmit deadline. + let now = coarsetime::Instant::now(); + { + let tcb = handler.get_connection_mut(&id).unwrap(); + let fin_seq = tcb.snd_nxt; + tcb.state = TcpState::FinWait1; + tcb.fin_seq = Some(fin_seq); + tcb.retransmit_deadline = Some(coarsetime::Instant::recent()); // already expired + tcb.rto_backoff = 0; + } + + // poll_timers should retransmit the FIN-ACK. + handler.poll_timers(now, nh.local_mac(), &nh, &mut free, &mut tx); + + // (1) A FIN-ACK segment must be emitted. + assert!( + tx.num_frames() >= 1, + "expected FIN-ACK retransmission, got {} frames", + tx.num_frames() + ); + + // Verify the emitted frame has FIN|ACK flags. + let frame = tx.pop().unwrap(); + let tcp_off = ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN; + // TCP flags byte is at offset 13 in the TCP header. + let tcp_flags = frame[tcp_off + 13]; + assert_eq!( + tcp_flags & (flags::FIN | flags::ACK), + flags::FIN | flags::ACK, + "retransmitted segment must have FIN|ACK flags" + ); + + // (2) retransmit_deadline must be rescheduled with backoff. + let tcb = handler.get_connection(&id).unwrap(); + assert!( + tcb.retransmit_deadline.is_some(), + "retransmit_deadline must be rescheduled" + ); + assert_eq!(tcb.rto_backoff, 1, "rto_backoff must be incremented"); +} + +#[test] +fn out_of_order_fin_does_not_transition_to_close_wait() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(32); + let mut rx = BasicFrameBuffer::new(32); + let mut tx = BasicFrameBuffer::new(32); + + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete handshake: remote ISS=1000, so after SYN rcv_nxt=1001. + let server_iss = establish_connection(&mut handler, &nh, &mut free, &mut rx, &mut tx); + assert_eq!(handler.connections[0].state, TcpState::Established); + assert_eq!(handler.connections[0].rcv_nxt, 1001); + + // Send a data+FIN segment that is out-of-order: there is a gap. + // rcv_nxt is 1001, but we send seq=1006 with 5 bytes of data + FIN. + // This means bytes 1001..1005 are missing. + let fin_data = build_tcp_frame_with_payload( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1006, + server_iss.wrapping_add(1), + flags::ACK | flags::FIN, + 65535, + &[], + b"world", + ); + let fin_len = fin_data.len(); + handler.process_ipv4( + Frame::new(2, leak(fin_data), fin_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // Connection must remain Established because preceding data is missing. + assert_eq!( + handler.connections[0].state, + TcpState::Established, + "FIN must not be processed when there is a gap before it" + ); + assert_eq!( + handler.connections[0].rcv_nxt, 1001, + "rcv_nxt must not advance past the gap" + ); + + // Drain any ACKs from tx. + while tx.pop().is_some() {} + + // Now fill the gap: send the missing 5 bytes at seq=1001. + let fill_data = build_tcp_frame_with_payload( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + b"hello", + ); + let fill_len = fill_data.len(); + handler.process_ipv4( + Frame::new(3, leak(fill_data), fill_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // After gap fill, rcv_nxt should advance through the reassembled data. + // But FIN was out-of-order and needs to be re-sent by the peer. + // rcv_nxt should be at 1011 (1001 + 5 gap-fill + 5 OOO data). + assert_eq!(handler.connections[0].rcv_nxt, 1011); + + // Drain tx again. + while tx.pop().is_some() {} + + // Re-send the FIN now that all data is received (seq=1011, no payload, FIN). + let fin_retry = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1011, + server_iss.wrapping_add(1), + flags::ACK | flags::FIN, + 65535, + &[], + ); + let fin_retry_len = fin_retry.len(); + handler.process_ipv4( + Frame::new(4, leak(fin_retry), fin_retry_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // Now the FIN should be processed and we transition to CloseWait. + assert_eq!( + handler.connections[0].state, + TcpState::CloseWait, + "FIN should be processed once all preceding data is received" + ); + assert_eq!( + handler.connections[0].rcv_nxt, 1012, + "rcv_nxt should advance by 1 for the FIN" + ); +} + diff --git a/src/net/handler/tcp/tests/timestamps.rs b/src/net/handler/tcp/tests/timestamps.rs new file mode 100644 index 0000000..2643242 --- /dev/null +++ b/src/net/handler/tcp/tests/timestamps.rs @@ -0,0 +1,315 @@ +use super::*; + +#[test] +fn paws_rejects_old_timestamp() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(16); + let mut rx = BasicFrameBuffer::new(16); + let mut tx = BasicFrameBuffer::new(16); + + for i in 0..8 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete the handshake with timestamp options. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let ts_opt = build_ts_option(500, 0); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &ts_opt, + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ts_opt2 = build_ts_option(600, 0); + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &ts_opt2, + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(handler.connections[0].state, TcpState::Established); + assert!(handler.connections[0].ts_enabled); + + // Set ts_recent to 1000 to make the test deterministic. + handler.connections[0].ts_recent = 1000; + handler.connections[0].ts_recent_age = coarsetime::Instant::now(); + + // Clear tx from handshake. + while tx.pop().is_some() {} + + // Send a segment with TSval=999 (older than ts_recent=1000). + let old_ts_opt = build_ts_option(999, 0); + let data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &old_ts_opt, + ); + let data_len = data.len(); + handler.process_ipv4( + Frame::new(2, leak(data), data_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // Segment should be dropped and an ACK sent back. + assert_eq!( + handler.connections.len(), + 1, + "connection should still exist" + ); + assert!( + tx.pop().is_some(), + "ACK should be sent in response to PAWS rejection" + ); +} + +#[test] +fn paws_drops_rst_with_old_timestamp() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(16); + let mut rx = BasicFrameBuffer::new(16); + let mut tx = BasicFrameBuffer::new(16); + + for i in 0..8 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete the handshake with timestamp options. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let ts_opt = build_ts_option(500, 0); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &ts_opt, + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ts_opt2 = build_ts_option(600, 0); + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &ts_opt2, + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(handler.connections[0].state, TcpState::Established); + assert!(handler.connections[0].ts_enabled); + + // Set ts_recent to 1000. + handler.connections[0].ts_recent = 1000; + handler.connections[0].ts_recent_age = coarsetime::Instant::now(); + + while tx.pop().is_some() {} + + // Send RST with old TSval=999 — RST should be silently dropped by PAWS + // (RFC 5961: RST is no longer exempt from PAWS to prevent replayed RST attacks). + let old_ts_opt = build_ts_option(999, 0); + let rst_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::RST, + 65535, + &old_ts_opt, + ); + let rst_len = rst_data.len(); + handler.process_ipv4( + Frame::new(2, leak(rst_data), rst_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // RST with old timestamp should be silently dropped — connection survives. + assert_eq!( + handler.connections.len(), + 1, + "RST with old timestamp should be dropped by PAWS" + ); + assert_eq!(handler.connections[0].state, TcpState::Established); +} + +#[test] +fn paws_accepts_stale_ts_recent() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(16); + let mut rx = BasicFrameBuffer::new(16); + let mut tx = BasicFrameBuffer::new(16); + + for i in 0..8 { + free.push(alloc_free_frame(100 + i)); + } + + // Complete the handshake with timestamp options. + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let ts_opt = build_ts_option(500, 0); + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &ts_opt, + ); + let syn_len = syn_data.len(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + let server_iss = handler.connections[0].iss; + let ts_opt2 = build_ts_option(600, 0); + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &ts_opt2, + ); + let ack_len = ack_data.len(); + handler.process_ipv4( + Frame::new(1, leak(ack_data), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(handler.connections[0].state, TcpState::Established); + assert!(handler.connections[0].ts_enabled); + + // Set ts_recent to 1000 and ts_recent_age to > 24 days ago. + handler.connections[0].ts_recent = 1000; + // Set ts_recent_age far in the past using a fixed tick value. + handler.connections[0].ts_recent_age = coarsetime::Instant::from_ticks(0); + + while tx.pop().is_some() {} + + // Construct a `now` that is guaranteed to be 25 days after ts_recent_age(0), + // regardless of system uptime. This ensures the PAWS staleness check sees + // > 24 days elapsed and accepts the segment despite old TSval. + let twenty_five_days = coarsetime::Duration::from_secs(25 * 24 * 60 * 60); + let now = coarsetime::Instant::from_ticks(0) + twenty_five_days; + + // Send a segment with old TSval=999, but ts_recent_age is stale (> 24 days). + // The PAWS check should accept the segment despite old timestamp. + let old_ts_opt = build_ts_option(999, 0); + let data = build_tcp_frame_with_payload( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &old_ts_opt, + b"Hello", + ); + let data_len = data.len(); + handler.process_ipv4( + Frame::new(2, leak(data), data_len, false), + now, + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // Segment should be accepted — connection still exists and data received. + assert_eq!( + handler.connections.len(), + 1, + "connection should still exist" + ); + let tcb = &handler.connections[0]; + assert_eq!( + tcb.recv_buffer.available(), + 5, + "data should be accepted when ts_recent is stale" + ); +} + diff --git a/src/net/handler/tcp/timers.rs b/src/net/handler/tcp/timers.rs new file mode 100644 index 0000000..425d73a --- /dev/null +++ b/src/net/handler/tcp/timers.rs @@ -0,0 +1,414 @@ +use coarsetime::Instant; + +use crate::{ + net::{ + NeighborHandler, + wire::tcp::flags, + }, + xdp::frame::FrameBuffer, +}; + +use super::{ + segment::SegmentBuilder, + state::TcpState, + tcb::{DEFAULT_RCV_MSS, DEFAULT_RCV_WND, DEFAULT_RCV_WSCALE, TcpEvent}, + TcpHandler, handler::{INITIAL_RTO_MS, SYN_R2_THRESHOLD_MS}, +}; + +impl TcpHandler { + // --- Timer polling --- + + /// Poll retransmission timers for SYN/SYN-ACK retransmission. + pub fn poll_timers<'umem>( + &mut self, + now: Instant, + src_mac: crate::net::wire::ethernet::MacAddress, + neighbor_handler: &NeighborHandler, + free_frames: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) { + // Delayed ACK pass — flush pending ACKs whose deadline has expired. + for tcb in &mut self.connections { + if !tcb.ack_pending { + continue; + } + let tsval = if tcb.ts_enabled { + now.duration_since(tcb.ts_offset).as_millis() as u32 + } else { + 0 + }; + if let Some(deadline) = tcb.delayed_ack_deadline + && now >= deadline + { + let id = tcb.id; + let dst_mac = neighbor_handler + .lookup(now, &id.remote_addr) + .unwrap_or(crate::net::wire::ethernet::MacAddress::broadcast()); + let ts = tcb.ts_option(tsval); + let ack_flags = if tcb.ecn_ce_received { + flags::ACK | flags::ECE + } else { + flags::ACK + }; + SegmentBuilder::build_ack( + id.local_addr, + id.remote_addr, + id.local_port, + id.remote_port, + tcb.snd_nxt, + tcb.rcv_nxt, + tcb.advertised_window(), + ack_flags, + ts, + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); + tcb.update_advertised_edge(); + tcb.ack_pending = false; + tcb.ack_delay_count = 0; + tcb.delayed_ack_deadline = None; + } + } + + // Keep-alive probe pass — send probes for idle established connections. + let mut keep_alive_removals = [0usize; 64]; + let mut keep_alive_removal_count = 0usize; + for (i, tcb) in self.connections.iter_mut().enumerate() { + if tcb.state != TcpState::Established || !tcb.keep_alive_enabled { + continue; + } + + let tsval = if tcb.ts_enabled { + now.duration_since(tcb.ts_offset).as_millis() as u32 + } else { + 0 + }; + + let idle_ms = now.duration_since(tcb.last_activity).as_millis(); + + let probe_threshold = if tcb.keep_alive_probes_sent == 0 { + tcb.keep_alive_idle_ms + } else { + tcb.keep_alive_idle_ms + + tcb.keep_alive_interval_ms * tcb.keep_alive_probes_sent as u64 + }; + + if idle_ms >= probe_threshold { + if tcb.keep_alive_probes_sent >= tcb.keep_alive_count { + // Max probes exceeded — abort connection. + tcb.event_queue.push(TcpEvent::Timeout); + if keep_alive_removal_count < 64 { + keep_alive_removals[keep_alive_removal_count] = i; + keep_alive_removal_count += 1; + } + continue; + } + + // Send keep-alive probe: seq = snd_una - 1, no data, ACK. + let id = tcb.id; + let dst_mac = neighbor_handler + .lookup(now, &id.remote_addr) + .unwrap_or(crate::net::wire::ethernet::MacAddress::broadcast()); + let ts = tcb.ts_option(tsval); + let ack_flags = if tcb.ecn_ce_received { + flags::ACK | flags::ECE + } else { + flags::ACK + }; + SegmentBuilder::build_ack( + id.local_addr, + id.remote_addr, + id.local_port, + id.remote_port, + tcb.snd_una.wrapping_sub(1), + tcb.rcv_nxt, + tcb.advertised_window(), + ack_flags, + ts, + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); + + tcb.keep_alive_probes_sent += 1; + } + } + + // Remove connections that exceeded keep-alive probes (reverse order). + for &idx in keep_alive_removals[..keep_alive_removal_count].iter().rev() { + let id = self.connections[idx].id; + self.decrement_syn_received(&id); + self.connections.remove(idx); + } + + // SACK recovery pass — RFC 6675 recovery loop. + for tcb in &mut self.connections { + if tcb.state != TcpState::Established || !tcb.recovery.in_recovery { + continue; + } + + let tsval = if tcb.ts_enabled { + now.duration_since(tcb.ts_offset).as_millis() as u32 + } else { + 0 + }; + + // Recompute pipe estimate. + tcb.recovery.set_pipe( + tcb.snd_una, + tcb.snd_nxt, + &tcb.sack_scoreboard, + tcb.eff_snd_mss, + ); + + // Send while pipe < cwnd. + while tcb.recovery.pipe < tcb.cubic.cwnd { + // Try lost segment first. + if let Some(lost_seq) = tcb.recovery.next_lost_segment( + tcb.snd_una, + tcb.snd_nxt, + &tcb.sack_scoreboard, + tcb.eff_snd_mss, + ) { + let offset = lost_seq.wrapping_sub(tcb.snd_una) as usize; + let retransmit_len = tcb + .send_buffer + .available() + .saturating_sub(offset) + .min(tcb.eff_snd_mss as usize); + if retransmit_len == 0 { + break; + } + + let id = tcb.id; + let dst_mac = neighbor_handler + .lookup(now, &id.remote_addr) + .unwrap_or(crate::net::wire::ethernet::MacAddress::broadcast()); + + let payload = tcb.send_buffer.peek_slices(offset, retransmit_len); + let ts = tcb.ts_option(tsval); + SegmentBuilder::build_data_from_slices( + id.local_addr, + id.remote_addr, + id.local_port, + id.remote_port, + lost_seq, + tcb.rcv_nxt, + tcb.advertised_window(), + payload, + flags::ACK, + false, + ts, + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); + + tcb.recovery.pipe += tcb.eff_snd_mss as u32; + tcb.prr.on_sent(retransmit_len as u32); + } else { + // No more lost segments — could send new data, but that's + // handled by poll_send. Break here. + break; + } + } + } + + // RTO retransmit pass — timer-based. + let mut to_remove = [0usize; 64]; + let mut to_remove_count = 0usize; + + for (idx, tcb) in self.connections.iter_mut().enumerate() { + let Some(deadline) = tcb.retransmit_deadline else { + continue; + }; + + if now < deadline { + continue; + } + + let tsval = if tcb.ts_enabled { + now.duration_since(tcb.ts_offset).as_millis() as u32 + } else { + 0 + }; + + // Check R2 threshold. + let total_elapsed_ms = { + let base_rto = INITIAL_RTO_MS; + let mut total: u64 = 0; + for i in 0..=tcb.rto_backoff { + total += base_rto << i; + } + total + }; + + if total_elapsed_ms >= SYN_R2_THRESHOLD_MS { + // Timeout — signal and mark for removal. + tcb.event_queue.push(TcpEvent::Timeout); + if to_remove_count < 64 { + to_remove[to_remove_count] = idx; + to_remove_count += 1; + } + continue; + } + + // Retransmit. + let id = tcb.id; + let dst_mac = neighbor_handler + .lookup(now, &id.remote_addr) + .unwrap_or(crate::net::wire::ethernet::MacAddress::broadcast()); + + match tcb.state { + TcpState::SynSent => { + let ts_opt = if tcb.ts_enabled { + Some((tsval, 0u32)) + } else { + None + }; + SegmentBuilder::build_syn( + id.local_addr, + id.remote_addr, + id.local_port, + id.remote_port, + tcb.iss, + DEFAULT_RCV_WND, + DEFAULT_RCV_MSS, + DEFAULT_RCV_WSCALE, + ts_opt, + tcb.sack_enabled, + tcb.ecn_enabled, + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); + } + TcpState::SynReceived => { + let wscale_opt = if tcb.wscale_enabled { + Some(tcb.rcv_wscale) + } else { + None + }; + let ts_opt = tcb.ts_option(tsval); + SegmentBuilder::build_syn_ack( + id.local_addr, + id.remote_addr, + id.local_port, + id.remote_port, + tcb.iss, + tcb.rcv_nxt, + DEFAULT_RCV_WND, + tcb.rcv_mss, + wscale_opt, + ts_opt, + tcb.sack_enabled, + tcb.ecn_enabled, + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); + } + TcpState::Established => { + let retransmit_len = tcb.send_buffer.available().min(tcb.eff_snd_mss as usize); + if retransmit_len > 0 { + let payload = tcb.send_buffer.peek_slices(0, retransmit_len); + let ts = tcb.ts_option(tsval); + SegmentBuilder::build_data_from_slices( + id.local_addr, + id.remote_addr, + id.local_port, + id.remote_port, + tcb.snd_una, + tcb.rcv_nxt, + tcb.advertised_window(), + payload, + flags::ACK, + false, // retransmits don't get ECT per RFC 3168 §6.1.5 + ts, + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); + } + // F-RTO + CUBIC RTO response. + tcb.frto.enter(tcb.snd_una); + tcb.cubic.on_rto(); + tcb.recovery.exit(); + tcb.sack_scoreboard.clear(); + tcb.rto_backoff += 1; + tcb.retransmit_deadline = + Some(now + coarsetime::Duration::from_millis(tcb.rto << tcb.rto_backoff)); + } + TcpState::FinWait1 | TcpState::Closing | TcpState::LastAck => { + // Retransmit FIN-ACK. + let ts = tcb.ts_option(tsval); + if let Some(fin_seq) = tcb.fin_seq { + SegmentBuilder::build_fin_ack( + id.local_addr, + id.remote_addr, + id.local_port, + id.remote_port, + fin_seq, + tcb.rcv_nxt, + tcb.advertised_window(), + ts, + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); + } + // Exponential backoff (same as Established). + tcb.rto_backoff += 1; + tcb.retransmit_deadline = + Some(now + coarsetime::Duration::from_millis(tcb.rto << tcb.rto_backoff)); + } + _ => continue, + } + + // Exponential backoff for SYN/SYN-ACK states. + match tcb.state { + TcpState::SynSent | TcpState::SynReceived => { + tcb.rto_backoff += 1; + let rto = INITIAL_RTO_MS << tcb.rto_backoff; + tcb.retransmit_deadline = Some(now + coarsetime::Duration::from_millis(rto)); + } + _ => {} // Established handles its own backoff above. + } + } + + // Remove timed-out connections (in reverse order to preserve indices). + for &idx in to_remove[..to_remove_count].iter().rev() { + let id = self.connections[idx].id; + self.decrement_syn_received(&id); + self.connections.remove(idx); + } + } + + /// Evict stale connections whose TIME-WAIT deadline has passed. + pub fn evict_stale<'umem>(&mut self, now: Instant, _rx_return: &mut impl FrameBuffer<'umem>) { + self.connections.retain(|tcb| { + if tcb.state == TcpState::TimeWait + && let Some(deadline) = tcb.time_wait_deadline + && now >= deadline + { + return false; // remove + } + true // keep + }); + } +} diff --git a/src/net/handler/tcp/transmit.rs b/src/net/handler/tcp/transmit.rs new file mode 100644 index 0000000..469da11 --- /dev/null +++ b/src/net/handler/tcp/transmit.rs @@ -0,0 +1,333 @@ +use coarsetime::Instant; + +use crate::{ + net::{NeighborHandler, wire::tcp::flags}, + xdp::frame::FrameBuffer, +}; + +use super::{ + TcpHandler, segment::SegmentBuilder, state::TcpState, + tcb::{MAX_DELAYED_ACK_COUNT, TcpEvent}, +}; + +impl TcpHandler { + // --- Data transmission --- + + /// Poll established connections for outbound data segments. + /// Called each tick from the runtime loop after receive processing. + pub fn poll_send<'umem>( + &mut self, + now: Instant, + src_mac: crate::net::wire::ethernet::MacAddress, + neighbor_handler: &NeighborHandler, + free_frames: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) { + for tcb in &mut self.connections { + if tcb.state != TcpState::Established && tcb.state != TcpState::CloseWait { + continue; + } + + let tsval = if tcb.ts_enabled { + now.duration_since(tcb.ts_offset).as_millis() as u32 + } else { + 0 + }; + + // Send as many segments as the window allows. + loop { + let bytes_in_flight = tcb.snd_nxt.wrapping_sub(tcb.snd_una) as usize; + let data_available = + tcb.send_buffer.available().saturating_sub(bytes_in_flight); + + let can_send = if tcb.recovery.in_recovery { + tcb.recovery.set_pipe( + tcb.snd_una, + tcb.snd_nxt, + &tcb.sack_scoreboard, + tcb.eff_snd_mss, + ); + let pipe = tcb.recovery.pipe as usize; + let cwnd = tcb.cubic.cwnd as usize; + if pipe < cwnd { + (cwnd - pipe).min(tcb.eff_snd_mss as usize) + } else { + 0 + } + } else { + // Limited Transmit (RFC 3042): inflate effective cwnd by + // dup_ack_count * MSS on the 1st/2nd dup ACK. + let effective_cwnd = if tcb.recovery.dup_ack_count > 0 + && tcb.recovery.dup_ack_count <= 2 + && !tcb.recovery.in_recovery + { + tcb.cubic.cwnd as usize + + tcb.recovery.dup_ack_count as usize + * tcb.eff_snd_mss as usize + } else { + tcb.cubic.cwnd as usize + }; + let send_window = (tcb.snd_wnd as usize).min(effective_cwnd); + send_window.saturating_sub(bytes_in_flight) + }; + + if can_send == 0 || data_available == 0 { + break; + } + + // Sender SWS avoidance (MUST-38): don't send sub-MSS data unless + // the usable window is large enough or all remaining data fits. + let sws_ok = can_send >= tcb.eff_snd_mss as usize + || can_send >= (tcb.max_snd_wnd as usize / 2).max(1) + || data_available <= can_send; + if !sws_ok { + break; + } + + let to_send = can_send.min(data_available).min(tcb.eff_snd_mss as usize); + + // Nagle algorithm: hold small segments when data is in flight. + if tcb.nagle_enabled + && bytes_in_flight > 0 + && to_send < tcb.eff_snd_mss as usize + { + break; + } + + // Peek the data from the send buffer (don't advance — held until ACKed). + let payload = tcb.send_buffer.peek_slices(bytes_in_flight, to_send); + + let dst_mac = neighbor_handler + .lookup(now, &tcb.id.remote_addr) + .unwrap_or(crate::net::wire::ethernet::MacAddress::broadcast()); + + let ts = tcb.ts_option(tsval); + let remaining_after_send = data_available.saturating_sub(to_send); + let mut data_flags = if remaining_after_send == 0 || to_send >= can_send { + flags::ACK | flags::PSH + } else { + flags::ACK + }; + if tcb.ecn_ce_received { + data_flags |= flags::ECE; + } + if tcb.ecn_cwr_sent { + data_flags |= flags::CWR; + } + SegmentBuilder::build_data_from_slices( + tcb.id.local_addr, + tcb.id.remote_addr, + tcb.id.local_port, + tcb.id.remote_port, + tcb.snd_nxt, + tcb.rcv_nxt, + tcb.advertised_window(), + payload, + data_flags, + tcb.ecn_enabled, + ts, + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); + + tcb.snd_nxt = tcb.snd_nxt.wrapping_add(to_send as u32); + tcb.last_send_time = Some(now); + + // Reset keep-alive timer on sent data. + tcb.last_activity = now; + tcb.keep_alive_probes_sent = 0; + + // Set retransmit timer if not already running. + if tcb.retransmit_deadline.is_none() { + tcb.retransmit_deadline = + Some(now + coarsetime::Duration::from_millis(tcb.rto)); + } + + // Clear CWR after sending (only needs to be on one segment). + if tcb.ecn_cwr_sent { + tcb.ecn_cwr_sent = false; + } + + // Piggyback: data segment carries ACK, so clear delayed ACK state. + tcb.update_advertised_edge(); + tcb.ack_pending = false; + tcb.ack_delay_count = 0; + tcb.delayed_ack_deadline = None; + } + + // Delayed ACK fallback: if the data send loop didn't piggyback an ACK + // and enough segments have arrived to require an immediate ACK, send a + // pure ACK now. This defers ACK generation from inbound processing to + // give data segments a chance to piggyback the ACK first. + if tcb.ack_pending && tcb.ack_delay_count >= MAX_DELAYED_ACK_COUNT { + let dst_mac = neighbor_handler + .lookup(now, &tcb.id.remote_addr) + .unwrap_or(crate::net::wire::ethernet::MacAddress::broadcast()); + + let ts = tcb.ts_option(tsval); + let mut ack_flags = flags::ACK; + if tcb.ecn_ce_received { + ack_flags |= flags::ECE; + } + SegmentBuilder::build_ack( + tcb.id.local_addr, + tcb.id.remote_addr, + tcb.id.local_port, + tcb.id.remote_port, + tcb.snd_nxt, + tcb.rcv_nxt, + tcb.advertised_window(), + ack_flags, + ts, + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); + tcb.update_advertised_edge(); + tcb.ack_pending = false; + tcb.ack_delay_count = 0; + tcb.delayed_ack_deadline = None; + } + + // --- Zero-window probing (persist timer) --- + // Recompute state after the send loop for persist timer and FIN checks. + let bytes_in_flight = tcb.snd_nxt.wrapping_sub(tcb.snd_una) as usize; + let data_available = tcb.send_buffer.available().saturating_sub(bytes_in_flight); + let send_window = (tcb.snd_wnd as usize).min(tcb.cubic.cwnd as usize); + + // A. Arm persist timer when peer advertises window=0 and we have data to send. + if send_window == 0 && data_available > 0 && tcb.persist_deadline.is_none() { + tcb.persist_deadline = Some(now + coarsetime::Duration::from_millis(tcb.rto)); + } + + // B. Send 1-byte probe when persist deadline expires. + if let Some(deadline) = tcb.persist_deadline + && now >= deadline + && send_window == 0 + && data_available > 0 + { + let mut probe = [0u8; 1]; + tcb.send_buffer.peek_at(bytes_in_flight, &mut probe); + + let dst_mac = neighbor_handler + .lookup(now, &tcb.id.remote_addr) + .unwrap_or(crate::net::wire::ethernet::MacAddress::broadcast()); + + let ts = tcb.ts_option(tsval); + SegmentBuilder::build_data( + tcb.id.local_addr, + tcb.id.remote_addr, + tcb.id.local_port, + tcb.id.remote_port, + tcb.snd_nxt, + tcb.rcv_nxt, + tcb.advertised_window(), + &probe, + ts, + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); + + tcb.snd_nxt = tcb.snd_nxt.wrapping_add(1); + + // Schedule next probe with exponential backoff, capped at 60s. + let backoff_ms = (tcb.rto << tcb.persist_backoff).min(60_000); + tcb.persist_deadline = Some(now + coarsetime::Duration::from_millis(backoff_ms)); + tcb.persist_backoff = tcb.persist_backoff.saturating_add(1).min(6); + } + + // Check linger deadline — if expired, abort with RST. + if tcb.pending_fin + && let Some(deadline) = tcb.linger_deadline + && now >= deadline + { + // Send RST to peer. + let id = tcb.id; + let dst_mac = neighbor_handler + .lookup(now, &id.remote_addr) + .unwrap_or(crate::net::wire::ethernet::MacAddress::broadcast()); + + // Use build_rst by simulating an "incoming ACK" segment. + // This produces: = + SegmentBuilder::build_rst( + id.remote_addr, + id.local_addr, // swapped: "incoming" from remote + id.remote_port, + id.local_port, // swapped + 0, + tcb.snd_nxt, // incoming_seq=0, incoming_ack=snd_nxt + flags::ACK, // pretend incoming has ACK set + 0, // seg_len doesn't matter + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); + + tcb.event_queue.push(TcpEvent::Reset); + tcb.state = TcpState::Closed; + tcb.pending_fin = false; + continue; + } + + // After data sending: check if we should send FIN. + if tcb.pending_fin { + let bytes_in_flight = tcb.snd_nxt.wrapping_sub(tcb.snd_una) as usize; + let data_available = tcb.send_buffer.available().saturating_sub(bytes_in_flight); + + // Only send FIN when all data has been sent and ACKed. + if data_available == 0 && bytes_in_flight == 0 { + let id = tcb.id; + let dst_mac = neighbor_handler + .lookup(now, &id.remote_addr) + .unwrap_or(crate::net::wire::ethernet::MacAddress::broadcast()); + let ts = tcb.ts_option(tsval); + SegmentBuilder::build_fin_ack( + id.local_addr, + id.remote_addr, + id.local_port, + id.remote_port, + tcb.snd_nxt, + tcb.rcv_nxt, + tcb.advertised_window(), + ts, + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); + + tcb.fin_seq = Some(tcb.snd_nxt); + tcb.snd_nxt = tcb.snd_nxt.wrapping_add(1); // FIN consumes one sequence number + + tcb.pending_fin = false; + + match tcb.state { + TcpState::Established => tcb.state = TcpState::FinWait1, + TcpState::CloseWait => tcb.state = TcpState::LastAck, + _ => {} + } + + // Set retransmit timer for FIN. + if tcb.retransmit_deadline.is_none() { + tcb.retransmit_deadline = + Some(now + coarsetime::Duration::from_millis(tcb.rto)); + } + } + } + } + + // Remove connections aborted by linger deadline. + self.connections.retain(|tcb| tcb.state != TcpState::Closed); + } +} diff --git a/src/net/handler/udp.rs b/src/net/handler/udp.rs new file mode 100644 index 0000000..f7d3d87 --- /dev/null +++ b/src/net/handler/udp.rs @@ -0,0 +1,1133 @@ +use std::fmt; + +use coarsetime::{Duration, Instant}; + +use crate::{ + net::{ + checksum::{ + fold_and_verify, pseudo_header_sum_v4, pseudo_header_sum_v6, sum_words_carry, + verify_udp_checksum, verify_udp_checksum_v6, + }, + fragment::{FragmentReader, Packet}, + socket::LocalQueue, + wire::{ + ethernet::EthernetFrame, + ip::{ + EXT_FRAGMENT, FRAGMENT_EXT_LEN, IPV6_HEADER_LEN, IpAddress, Ipv4Header, Ipv6Header, + }, + udp::{UDP_HEADER_LEN, UdpHeader}, + }, + }, + xdp::frame::{Frame, FrameBuffer}, +}; + +/// A completed received UDP packet, ready for delivery to user space. +#[derive(Debug)] +pub struct ReceivedUdpPacket<'umem> { + pub src_addr: IpAddress, + pub dst_addr: IpAddress, + pub src_port: u16, + pub dst_port: u16, + pub packet: Packet<'umem>, +} + +impl<'umem> ReceivedUdpPacket<'umem> { + /// Swap source and destination addresses in-place (Ethernet MACs, IP + /// addresses, UDP ports) so the packet can be echoed back to the sender. + /// + /// This performs zero-copy in-place mutations on the frame bytes. Checksums + /// do not need recalculation because swapping src↔dst produces the same + /// one's-complement sum (addition is commutative). + pub fn swap_addresses(&mut self) { + let is_ipv4 = matches!(self.src_addr, IpAddress::V4(_)); + let mut first = true; + + for frame in self.packet.frames_mut() { + // Swap Ethernet MACs. + { + let eth = EthernetFrame::from_bytes_mut(frame); + std::mem::swap(&mut eth.src_mac, &mut eth.dst_mac); + } + + // Swap IP addresses. + if is_ipv4 { + let ip = Ipv4Header::from_bytes_mut(frame); + std::mem::swap(&mut ip.src_addr, &mut ip.dst_addr); + } else { + let ip = Ipv6Header::from_bytes_mut(frame); + std::mem::swap(&mut ip.src_addr, &mut ip.dst_addr); + } + + // Swap UDP ports on the first frame only. + if first { + first = false; + let udp_offset = if is_ipv4 { + Ipv4Header::from_bytes(frame).payload_offset() + } else { + let next_header = Ipv6Header::from_bytes(frame).next_header; + let base = size_of::() + IPV6_HEADER_LEN; + if next_header == EXT_FRAGMENT { + base + FRAGMENT_EXT_LEN + } else { + base + } + }; + let udp = unsafe { UdpHeader::from_bytes_at_mut(frame, udp_offset) }; + std::mem::swap(&mut udp.src_port, &mut udp.dst_port); + } + } + + // Swap metadata fields. + std::mem::swap(&mut self.src_addr, &mut self.dst_addr); + std::mem::swap(&mut self.src_port, &mut self.dst_port); + } +} + +/// Error returned when a `bind` call fails. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum BindError { + AddressInUse, +} + +impl fmt::Display for BindError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + BindError::AddressInUse => write!(f, "address already in use"), + } + } +} + +/// Internal binding record kept by `UdpHandler`. +pub(crate) struct UdpBinding<'umem> { + rx_queue: LocalQueue>, +} + +/// Groups bindings for a single port: explicit IP bindings and an optional wildcard. +struct PortBindings<'umem> { + explicit: Vec<(IpAddress, UdpBinding<'umem>)>, + wildcard: Option>, +} + +/// Manages bound UDP sockets and routes reassembled packets to them. +/// +/// Wraps [`FragmentReader`] for fragment reassembly and dispatches completed +/// packets to the matching socket's receive queue. Called from IPv4/IPv6 +/// handlers instead of `FragmentReader` directly. +pub struct UdpHandler<'umem> { + fragment_reader: FragmentReader<'umem>, + bindings: Vec<(u16, PortBindings<'umem>)>, + rx_offload: bool, +} + +impl<'umem> UdpHandler<'umem> { + pub fn new(max_reassembly_entries: usize, rx_offload: bool) -> Self { + Self { + fragment_reader: FragmentReader::new(max_reassembly_entries), + bindings: Vec::new(), + rx_offload, + } + } + + /// Bind a new socket to (addr, port). + /// + /// If `addr` is the unspecified address (`0.0.0.0` / `::`), the binding + /// acts as a wildcard: packets that match the port but have no explicit IP + /// binding will be delivered here. Only one wildcard per port is allowed. + /// + /// Returns the shared receive queue so the caller can build a + /// user-facing `UdpSocket` that shares the same queue. + pub fn bind( + &mut self, + addr: IpAddress, + port: u16, + rx_capacity: usize, + ) -> Result>, BindError> { + let rx_queue = LocalQueue::new(rx_capacity); + let binding = UdpBinding { + rx_queue: rx_queue.clone(), + }; + + if let Some((_, pb)) = self.bindings.iter_mut().find(|(p, _)| *p == port) { + if addr.is_unspecified() { + if pb.wildcard.is_some() { + return Err(BindError::AddressInUse); + } + pb.wildcard = Some(binding); + } else { + if pb.explicit.iter().any(|(a, _)| *a == addr) { + return Err(BindError::AddressInUse); + } + pb.explicit.push((addr, binding)); + } + } else { + let mut pb = PortBindings { + explicit: Vec::new(), + wildcard: None, + }; + if addr.is_unspecified() { + pb.wildcard = Some(binding); + } else { + pb.explicit.push((addr, binding)); + } + self.bindings.push((port, pb)); + } + + Ok(rx_queue) + } + + /// Unbind a previously bound socket from (addr, port). + /// + /// If `addr` is the unspecified address, removes the wildcard binding for that port. + /// Otherwise removes the matching explicit IP binding. Cleans up the port entry + /// entirely if no bindings remain. + pub fn unbind(&mut self, addr: IpAddress, port: u16) { + if let Some((_, pb)) = self.bindings.iter_mut().find(|(p, _)| *p == port) { + if addr.is_unspecified() { + pb.wildcard = None; + } else { + pb.explicit.retain(|(a, _)| *a != addr); + } + } + self.bindings + .retain(|(_, pb)| !pb.explicit.is_empty() || pb.wildcard.is_some()); + } + + /// Called by `Ipv4Handler` for UDP frames/fragments. + pub fn process_ipv4(&mut self, frame: Frame<'umem>, rx_return: &mut impl FrameBuffer<'umem>) { + if let Some(reassembled) = self.fragment_reader.process_ipv4(frame, rx_return) { + let (src_addr, dst_addr, src_port, dst_port, valid) = { + let first = match &reassembled.packet { + Packet::Single(f) => f, + Packet::Multi(fs) => &fs[0], + Packet::Empty => return, + }; + let ip = Ipv4Header::from_bytes(first); + let udp_offset = ip.payload_offset(); + if first.len() < udp_offset + UDP_HEADER_LEN { + reassembled.packet.drain_to(rx_return); + return; + } + let udp = unsafe { UdpHeader::from_bytes_at(first, udp_offset) }; + let udp_len = udp.length() as usize; + if udp_len < UDP_HEADER_LEN { + reassembled.packet.drain_to(rx_return); + return; + } + + let valid = if self.rx_offload { + true + } else { + match &reassembled.packet { + Packet::Single(f) => { + let available = f.len() - udp_offset; + if available < udp_len { + false + } else { + verify_udp_checksum( + &ip.src_addr, + &ip.dst_addr, + &f[udp_offset..udp_offset + udp_len], + ) + } + } + Packet::Multi(fs) => { + // Zero checksum means "no checksum" for IPv4. + if fs[0][udp_offset + 6] == 0 && fs[0][udp_offset + 7] == 0 { + true + } else { + let mut sum: u64 = 0; + let mut pending: Option = None; + let mut total: usize = 0; + + let slice = &fs[0][udp_offset..]; + total += slice.len(); + (sum, pending) = sum_words_carry(slice, sum, pending); + + for f in &fs[1..] { + let fip = Ipv4Header::from_bytes(f); + let slice = &f[fip.payload_offset()..]; + total += slice.len(); + (sum, pending) = sum_words_carry(slice, sum, pending); + } + + if let Some(hi) = pending { + sum += (hi as u64) << 8; + } + + if total < udp_len { + false + } else { + sum += pseudo_header_sum_v4( + &ip.src_addr, + &ip.dst_addr, + 17, // UDP + total as u16, + ); + fold_and_verify(sum, 0x0000) + } + } + } + Packet::Empty => return, + } + }; + + ( + IpAddress::V4(ip.src_addr), + IpAddress::V4(ip.dst_addr), + udp.src_port(), + udp.dst_port(), + valid, + ) + }; + if !valid { + reassembled.packet.drain_to(rx_return); + return; + } + self.route( + ReceivedUdpPacket { + src_addr, + dst_addr, + src_port, + dst_port, + packet: reassembled.packet, + }, + rx_return, + ); + } + } + + /// Called by `Ipv6Handler` for UDP frames/fragments. + pub fn process_ipv6( + &mut self, + frame: Frame<'umem>, + frag_ext_offset: Option, + udp_offset: usize, + rx_return: &mut impl FrameBuffer<'umem>, + ) { + match frag_ext_offset { + None => { + // Non-fragmented UDP — hot path. + let ip = Ipv6Header::from_bytes(&frame); + let src_addr = ip.src_addr; + let dst_addr = ip.dst_addr; + + if frame.len() < udp_offset + UDP_HEADER_LEN { + rx_return.push(frame); + return; + } + let udp = unsafe { UdpHeader::from_bytes_at(&frame, udp_offset) }; + let udp_len = udp.length() as usize; + if udp_len < UDP_HEADER_LEN || frame.len() < udp_offset + udp_len { + rx_return.push(frame); + return; + } + if !self.rx_offload + && !verify_udp_checksum_v6( + &src_addr, + &dst_addr, + &frame[udp_offset..udp_offset + udp_len], + ) + { + rx_return.push(frame); + return; + } + let src_port = udp.src_port(); + let dst_port = udp.dst_port(); + self.route( + ReceivedUdpPacket { + src_addr: IpAddress::V6(src_addr), + dst_addr: IpAddress::V6(dst_addr), + src_port, + dst_port, + packet: Packet::Single(frame), + }, + rx_return, + ); + } + Some(frag_off) => { + if frame.len() < frag_off + FRAGMENT_EXT_LEN { + rx_return.push(frame); + return; + } + + if let Some(reassembled) = self + .fragment_reader + .process_ipv6(frame, frag_off, rx_return) + { + let (src_addr, dst_addr, src_port, dst_port, valid) = { + let first = match &reassembled.packet { + Packet::Single(f) => f, + Packet::Multi(fs) => &fs[0], + Packet::Empty => return, + }; + let ip = Ipv6Header::from_bytes(first); + let udp_start = frag_off + FRAGMENT_EXT_LEN; + if first.len() < udp_start + UDP_HEADER_LEN { + reassembled.packet.drain_to(rx_return); + return; + } + let udp = unsafe { UdpHeader::from_bytes_at(first, udp_start) }; + let udp_len = udp.length() as usize; + if udp_len < UDP_HEADER_LEN { + reassembled.packet.drain_to(rx_return); + return; + } + + let valid = if self.rx_offload { + true + } else { + match &reassembled.packet { + Packet::Single(f) => { + let available = f.len() - udp_start; + if available < udp_len { + false + } else { + verify_udp_checksum_v6( + &ip.src_addr, + &ip.dst_addr, + &f[udp_start..udp_start + udp_len], + ) + } + } + Packet::Multi(fs) => { + let data_start = frag_off + FRAGMENT_EXT_LEN; + // IPv6 does not allow zero checksum. + if fs[0][data_start + 6] == 0 && fs[0][data_start + 7] == 0 { + false + } else { + let mut sum: u64 = 0; + let mut pending: Option = None; + let mut total: usize = 0; + + let slice = &fs[0][data_start..]; + total += slice.len(); + (sum, pending) = sum_words_carry(slice, sum, pending); + + for f in &fs[1..] { + let slice = &f[data_start..]; + total += slice.len(); + (sum, pending) = sum_words_carry(slice, sum, pending); + } + + if let Some(hi) = pending { + sum += (hi as u64) << 8; + } + + if total < udp_len { + false + } else { + sum += pseudo_header_sum_v6( + &ip.src_addr, + &ip.dst_addr, + 17, // UDP + total as u32, + ); + fold_and_verify(sum, 0x0000) + } + } + } + Packet::Empty => return, + } + }; + + ( + IpAddress::V6(ip.src_addr), + IpAddress::V6(ip.dst_addr), + udp.src_port(), + udp.dst_port(), + valid, + ) + }; + if !valid { + reassembled.packet.drain_to(rx_return); + return; + } + self.route( + ReceivedUdpPacket { + src_addr, + dst_addr, + src_port, + dst_port, + packet: reassembled.packet, + }, + rx_return, + ); + } + } + } + } + + /// Evict stale reassembly entries (delegates to `FragmentReader`). + pub fn evict_stale( + &mut self, + now: Instant, + timeout: Duration, + rx_return: &mut impl FrameBuffer<'umem>, + ) { + self.fragment_reader.evict_stale(now, timeout, rx_return); + } + + /// Number of in-progress reassembly entries. + pub fn pending_reassembly(&self) -> usize { + self.fragment_reader.pending_entries() + } + + fn route( + &mut self, + received: ReceivedUdpPacket<'umem>, + rx_return: &mut impl FrameBuffer<'umem>, + ) { + let port = received.dst_port; + let addr = received.dst_addr; + + if let Some((_, pb)) = self.bindings.iter().find(|(p, _)| *p == port) { + // Try explicit match first. + if let Some((_, binding)) = pb.explicit.iter().find(|(a, _)| *a == addr) { + if let Some(evicted) = binding.rx_queue.push(received) { + evicted.packet.drain_to(rx_return); + } + return; + } + // Fall back to wildcard. + if let Some(binding) = &pb.wildcard { + if let Some(evicted) = binding.rx_queue.push(received) { + evicted.packet.drain_to(rx_return); + } + return; + } + } + // No match — return frames to kernel. + received.packet.drain_to(rx_return); + } +} + +#[cfg(test)] +mod tests { + use crate::{ + net::{ + checksum::{compute_ipv4_checksum, compute_udp_checksum, compute_udp_checksum_v6}, + wire::{ + ip::{ + IPV4_MIN_HEADER_LEN, IPV6_HEADER_LEN, IpAddress, IpProtocols, Ipv4Address, + Ipv6Address, + }, + udp::UDP_HEADER_LEN, + }, + }, + xdp::frame::{BasicFrameBuffer, Frame}, + }; + + use super::*; + + const LOCAL_IPV4: Ipv4Address = Ipv4Address::new([192, 168, 1, 1]); + const REMOTE_IPV4: Ipv4Address = Ipv4Address::new([10, 0, 0, 2]); + const LOCAL_IPV6: Ipv6Address = + Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + const REMOTE_IPV6: Ipv6Address = + Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]); + + const ETH_HEADER_LEN: usize = 14; + + /// Builds a minimal Ethernet + IPv4 + UDP frame for testing. + fn build_ipv4_udp_frame( + src_ip: Ipv4Address, + dst_ip: Ipv4Address, + src_port: u16, + dst_port: u16, + payload: &[u8], + ) -> Vec { + use crate::net::wire::ip::IpProtocols; + let total_ip_len = (IPV4_MIN_HEADER_LEN + UDP_HEADER_LEN + payload.len()) as u16; + let mut buf = + vec![0u8; ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN + UDP_HEADER_LEN + payload.len()]; + + buf[12] = 0x08; + buf[13] = 0x00; + + let ip = &mut buf[14..]; + ip[0] = 0x45; + ip[2..4].copy_from_slice(&total_ip_len.to_be_bytes()); + ip[6] = 0x40; + ip[8] = 64; + ip[9] = IpProtocols::Udp; + let src_bytes: [u8; 4] = src_ip.into(); + ip[12..16].copy_from_slice(&src_bytes); + let dst_bytes: [u8; 4] = dst_ip.into(); + ip[16..20].copy_from_slice(&dst_bytes); + let cksum = compute_ipv4_checksum(&ip[..20]); + ip[10] = cksum[0]; + ip[11] = cksum[1]; + + let udp_off = ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN; + let udp_len = (UDP_HEADER_LEN + payload.len()) as u16; + buf[udp_off..udp_off + 2].copy_from_slice(&src_port.to_be_bytes()); + buf[udp_off + 2..udp_off + 4].copy_from_slice(&dst_port.to_be_bytes()); + buf[udp_off + 4..udp_off + 6].copy_from_slice(&udp_len.to_be_bytes()); + // checksum bytes [6..8] are zero; fill payload first, then compute + buf[udp_off + UDP_HEADER_LEN..].copy_from_slice(payload); + + let cksum = compute_udp_checksum(&src_ip, &dst_ip, &buf[udp_off..]); + buf[udp_off + 6] = cksum[0]; + buf[udp_off + 7] = cksum[1]; + + buf + } + + /// Builds a minimal Ethernet + IPv6 + UDP frame for testing. + fn build_ipv6_udp_frame( + src_ip: Ipv6Address, + dst_ip: Ipv6Address, + src_port: u16, + dst_port: u16, + payload: &[u8], + ) -> Vec { + let payload_len = (UDP_HEADER_LEN + payload.len()) as u16; + let mut buf = vec![0u8; ETH_HEADER_LEN + IPV6_HEADER_LEN + UDP_HEADER_LEN + payload.len()]; + + buf[12] = 0x86; + buf[13] = 0xDD; + + let ip = &mut buf[14..]; + ip[0] = 0x60; + ip[4..6].copy_from_slice(&payload_len.to_be_bytes()); + ip[6] = IpProtocols::Udp; + ip[7] = 64; + let src_bytes: [u8; 16] = src_ip.into(); + ip[8..24].copy_from_slice(&src_bytes); + let dst_bytes: [u8; 16] = dst_ip.into(); + ip[24..40].copy_from_slice(&dst_bytes); + + let udp_off = ETH_HEADER_LEN + IPV6_HEADER_LEN; + let udp_len = (UDP_HEADER_LEN + payload.len()) as u16; + buf[udp_off..udp_off + 2].copy_from_slice(&src_port.to_be_bytes()); + buf[udp_off + 2..udp_off + 4].copy_from_slice(&dst_port.to_be_bytes()); + buf[udp_off + 4..udp_off + 6].copy_from_slice(&udp_len.to_be_bytes()); + // checksum bytes [6..8] are zero; fill payload first, then compute + buf[udp_off + UDP_HEADER_LEN..].copy_from_slice(payload); + + let cksum = compute_udp_checksum_v6(&src_ip, &dst_ip, &buf[udp_off..]); + buf[udp_off + 6] = cksum[0]; + buf[udp_off + 7] = cksum[1]; + + buf + } + + #[test] + fn bind_returns_shared_queues() { + let mut handler = UdpHandler::new(256, false); + let rx_queue = handler.bind(IpAddress::V4(LOCAL_IPV4), 5000, 128).unwrap(); + assert!(rx_queue.is_empty()); + } + + #[test] + fn duplicate_bind_returns_address_in_use() { + let mut handler = UdpHandler::new(256, false); + handler.bind(IpAddress::V4(LOCAL_IPV4), 5000, 128).unwrap(); + let err = handler + .bind(IpAddress::V4(LOCAL_IPV4), 5000, 128) + .unwrap_err(); + assert_eq!(err, BindError::AddressInUse); + } + + #[test] + fn process_ipv4_routes_to_bound_socket() { + let mut buf = build_ipv4_udp_frame(REMOTE_IPV4, LOCAL_IPV4, 12345, 53, b"hello"); + + let mut handler = UdpHandler::new(256, false); + let rx_queue = handler.bind(IpAddress::V4(LOCAL_IPV4), 53, 128).unwrap(); + + let len = buf.len(); + let frame = Frame::new(0, &mut buf, len, false); + let mut rx = BasicFrameBuffer::new(4); + + handler.process_ipv4(frame, &mut rx); + + assert_eq!(rx.num_frames(), 0); + assert_eq!(rx_queue.len(), 1); + } + + #[test] + fn process_ipv4_no_bound_socket_returns_frames() { + let mut buf = build_ipv4_udp_frame(REMOTE_IPV4, LOCAL_IPV4, 12345, 53, b"hello"); + + let mut handler = UdpHandler::new(256, false); + + let len = buf.len(); + let frame = Frame::new(0, &mut buf, len, false); + let mut rx = BasicFrameBuffer::new(4); + + handler.process_ipv4(frame, &mut rx); + assert_eq!(rx.num_frames(), 1); + } + + #[test] + fn process_ipv4_full_rx_queue_drops_and_returns_frames() { + let mut buf1 = build_ipv4_udp_frame(REMOTE_IPV4, LOCAL_IPV4, 12345, 53, b"first"); + let mut buf2 = build_ipv4_udp_frame(REMOTE_IPV4, LOCAL_IPV4, 12345, 53, b"second"); + + let mut handler = UdpHandler::new(256, false); + let rx_queue = handler.bind(IpAddress::V4(LOCAL_IPV4), 53, 1).unwrap(); + + let len1 = buf1.len(); + let frame1 = Frame::new(0, &mut buf1, len1, false); + let mut rx = BasicFrameBuffer::new(4); + handler.process_ipv4(frame1, &mut rx); + assert_eq!(rx_queue.len(), 1); + assert_eq!(rx.num_frames(), 0); + + let len2 = buf2.len(); + let frame2 = Frame::new(1, &mut buf2, len2, false); + handler.process_ipv4(frame2, &mut rx); + + assert_eq!(rx_queue.len(), 1); + assert_eq!(rx.num_frames(), 1); + } + + #[test] + fn process_ipv6_routes_to_bound_socket() { + let mut buf = build_ipv6_udp_frame(REMOTE_IPV6, LOCAL_IPV6, 12345, 53, b"hello"); + + let mut handler = UdpHandler::new(256, false); + let rx_queue = handler.bind(IpAddress::V6(LOCAL_IPV6), 53, 128).unwrap(); + + let len = buf.len(); + let frame = Frame::new(0, &mut buf, len, false); + let mut rx = BasicFrameBuffer::new(4); + + let udp_offset = ETH_HEADER_LEN + IPV6_HEADER_LEN; + handler.process_ipv6(frame, None, udp_offset, &mut rx); + + assert_eq!(rx.num_frames(), 0); + assert_eq!(rx_queue.len(), 1); + } + + #[test] + fn process_ipv6_no_bound_socket_returns_frames() { + let mut buf = build_ipv6_udp_frame(REMOTE_IPV6, LOCAL_IPV6, 12345, 53, b"hello"); + + let mut handler = UdpHandler::new(256, false); + + let len = buf.len(); + let frame = Frame::new(0, &mut buf, len, false); + let mut rx = BasicFrameBuffer::new(4); + + let udp_offset = ETH_HEADER_LEN + IPV6_HEADER_LEN; + handler.process_ipv6(frame, None, udp_offset, &mut rx); + + assert_eq!(rx.num_frames(), 1); + } + + #[test] + fn rx_queue_receives_routed_packet() { + let mut buf = build_ipv4_udp_frame(REMOTE_IPV4, LOCAL_IPV4, 12345, 53, b"hello"); + + let mut handler = UdpHandler::new(256, false); + let rx_queue = handler.bind(IpAddress::V4(LOCAL_IPV4), 53, 128).unwrap(); + + let len = buf.len(); + let frame = Frame::new(0, &mut buf, len, false); + let mut rx = BasicFrameBuffer::new(4); + handler.process_ipv4(frame, &mut rx); + + assert_eq!(rx_queue.len(), 1); + let pkt = rx_queue.pop().unwrap(); + assert_eq!(pkt.src_addr, IpAddress::V4(REMOTE_IPV4)); + assert_eq!(pkt.dst_addr, IpAddress::V4(LOCAL_IPV4)); + assert_eq!(pkt.src_port, 12345); + assert_eq!(pkt.dst_port, 53); + assert!(rx_queue.is_empty()); + assert!(rx_queue.pop().is_none()); + } + + #[test] + fn evict_stale_delegates_correctly() { + let mut handler = UdpHandler::new(256, false); + let mut rx = BasicFrameBuffer::new(4); + + handler.evict_stale(Instant::now(), Duration::from_secs(30), &mut rx); + assert_eq!(handler.pending_reassembly(), 0); + } + + #[test] + fn wildcard_bind_receives_packets() { + let mut buf = build_ipv4_udp_frame(REMOTE_IPV4, LOCAL_IPV4, 12345, 53, b"hello"); + + let mut handler = UdpHandler::new(256, false); + let rx_queue = handler + .bind(IpAddress::V4(Ipv4Address::unspecified()), 53, 128) + .unwrap(); + + let len = buf.len(); + let frame = Frame::new(0, &mut buf, len, false); + let mut rx = BasicFrameBuffer::new(4); + + handler.process_ipv4(frame, &mut rx); + + assert_eq!(rx.num_frames(), 0); + assert_eq!(rx_queue.len(), 1); + } + + #[test] + fn explicit_bind_takes_priority_over_wildcard() { + let mut buf = build_ipv4_udp_frame(REMOTE_IPV4, LOCAL_IPV4, 12345, 53, b"hello"); + + let mut handler = UdpHandler::new(256, false); + let wildcard_q = handler + .bind(IpAddress::V4(Ipv4Address::unspecified()), 53, 128) + .unwrap(); + let explicit_q = handler.bind(IpAddress::V4(LOCAL_IPV4), 53, 128).unwrap(); + + let len = buf.len(); + let frame = Frame::new(0, &mut buf, len, false); + let mut rx = BasicFrameBuffer::new(4); + + handler.process_ipv4(frame, &mut rx); + + assert_eq!(explicit_q.len(), 1); + assert_eq!(wildcard_q.len(), 0); + } + + #[test] + fn wildcard_receives_when_no_explicit_match() { + // Packet destined for LOCAL_IPV4, but only a different explicit IP is bound. + let other_ip = Ipv4Address::new([10, 0, 0, 99]); + let mut buf = build_ipv4_udp_frame(REMOTE_IPV4, LOCAL_IPV4, 12345, 53, b"hello"); + + let mut handler = UdpHandler::new(256, false); + let wildcard_q = handler + .bind(IpAddress::V4(Ipv4Address::unspecified()), 53, 128) + .unwrap(); + let explicit_q = handler.bind(IpAddress::V4(other_ip), 53, 128).unwrap(); + + let len = buf.len(); + let frame = Frame::new(0, &mut buf, len, false); + let mut rx = BasicFrameBuffer::new(4); + + handler.process_ipv4(frame, &mut rx); + + assert_eq!(wildcard_q.len(), 1); + assert_eq!(explicit_q.len(), 0); + } + + #[test] + fn duplicate_wildcard_bind_returns_address_in_use() { + let mut handler = UdpHandler::new(256, false); + handler + .bind(IpAddress::V4(Ipv4Address::unspecified()), 53, 128) + .unwrap(); + let err = handler + .bind(IpAddress::V4(Ipv4Address::unspecified()), 53, 128) + .unwrap_err(); + assert_eq!(err, BindError::AddressInUse); + } + + // -- Checksum & length validation tests -- + + #[test] + fn process_ipv4_rejects_bad_udp_checksum() { + let mut buf = build_ipv4_udp_frame(REMOTE_IPV4, LOCAL_IPV4, 12345, 53, b"hello"); + // Corrupt the UDP checksum byte + let udp_off = ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN; + buf[udp_off + 6] ^= 0xFF; + + let mut handler = UdpHandler::new(256, false); + let rx_queue = handler.bind(IpAddress::V4(LOCAL_IPV4), 53, 128).unwrap(); + + let len = buf.len(); + let frame = Frame::new(0, &mut buf, len, false); + let mut rx = BasicFrameBuffer::new(4); + + handler.process_ipv4(frame, &mut rx); + + assert_eq!(rx_queue.len(), 0, "bad checksum should not be delivered"); + assert_eq!(rx.num_frames(), 1, "frame should be returned"); + } + + #[test] + fn process_ipv4_accepts_zero_checksum() { + let mut buf = build_ipv4_udp_frame(REMOTE_IPV4, LOCAL_IPV4, 12345, 53, b"hello"); + // Set checksum to zero (valid for IPv4 — means "no checksum") + let udp_off = ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN; + buf[udp_off + 6] = 0; + buf[udp_off + 7] = 0; + + let mut handler = UdpHandler::new(256, false); + let rx_queue = handler.bind(IpAddress::V4(LOCAL_IPV4), 53, 128).unwrap(); + + let len = buf.len(); + let frame = Frame::new(0, &mut buf, len, false); + let mut rx = BasicFrameBuffer::new(4); + + handler.process_ipv4(frame, &mut rx); + + assert_eq!( + rx_queue.len(), + 1, + "zero checksum should be accepted for IPv4" + ); + assert_eq!(rx.num_frames(), 0); + } + + #[test] + fn process_ipv6_rejects_zero_checksum() { + let mut buf = build_ipv6_udp_frame(REMOTE_IPV6, LOCAL_IPV6, 12345, 53, b"hello"); + // Set checksum to zero (INVALID for IPv6) + let udp_off = ETH_HEADER_LEN + IPV6_HEADER_LEN; + buf[udp_off + 6] = 0; + buf[udp_off + 7] = 0; + + let mut handler = UdpHandler::new(256, false); + let rx_queue = handler.bind(IpAddress::V6(LOCAL_IPV6), 53, 128).unwrap(); + + let len = buf.len(); + let frame = Frame::new(0, &mut buf, len, false); + let mut rx = BasicFrameBuffer::new(4); + + let udp_offset = ETH_HEADER_LEN + IPV6_HEADER_LEN; + handler.process_ipv6(frame, None, udp_offset, &mut rx); + + assert_eq!( + rx_queue.len(), + 0, + "zero checksum should be rejected for IPv6" + ); + assert_eq!(rx.num_frames(), 1, "frame should be returned"); + } + + #[test] + fn process_ipv6_rejects_bad_udp_checksum() { + let mut buf = build_ipv6_udp_frame(REMOTE_IPV6, LOCAL_IPV6, 12345, 53, b"hello"); + // Corrupt the UDP checksum byte + let udp_off = ETH_HEADER_LEN + IPV6_HEADER_LEN; + buf[udp_off + 6] ^= 0xFF; + + let mut handler = UdpHandler::new(256, false); + let rx_queue = handler.bind(IpAddress::V6(LOCAL_IPV6), 53, 128).unwrap(); + + let len = buf.len(); + let frame = Frame::new(0, &mut buf, len, false); + let mut rx = BasicFrameBuffer::new(4); + + let udp_offset = ETH_HEADER_LEN + IPV6_HEADER_LEN; + handler.process_ipv6(frame, None, udp_offset, &mut rx); + + assert_eq!(rx_queue.len(), 0, "bad checksum should not be delivered"); + assert_eq!(rx.num_frames(), 1, "frame should be returned"); + } + + #[test] + fn process_ipv4_rejects_udp_length_too_small() { + let mut buf = build_ipv4_udp_frame(REMOTE_IPV4, LOCAL_IPV4, 12345, 53, b"hello"); + // Set UDP length to 4 (< 8 minimum) + let udp_off = ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN; + buf[udp_off + 4] = 0; + buf[udp_off + 5] = 4; + + let mut handler = UdpHandler::new(256, false); + let rx_queue = handler.bind(IpAddress::V4(LOCAL_IPV4), 53, 128).unwrap(); + + let len = buf.len(); + let frame = Frame::new(0, &mut buf, len, false); + let mut rx = BasicFrameBuffer::new(4); + + handler.process_ipv4(frame, &mut rx); + + assert_eq!(rx_queue.len(), 0, "short UDP length should be rejected"); + assert_eq!(rx.num_frames(), 1, "frame should be returned"); + } + + #[test] + fn process_ipv4_rejects_udp_length_exceeds_frame() { + let mut buf = build_ipv4_udp_frame(REMOTE_IPV4, LOCAL_IPV4, 12345, 53, b"hi"); + // Set UDP length to 100 (much larger than actual data) + let udp_off = ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN; + buf[udp_off + 4] = 0; + buf[udp_off + 5] = 100; + + let mut handler = UdpHandler::new(256, false); + let rx_queue = handler.bind(IpAddress::V4(LOCAL_IPV4), 53, 128).unwrap(); + + let len = buf.len(); + let frame = Frame::new(0, &mut buf, len, false); + let mut rx = BasicFrameBuffer::new(4); + + handler.process_ipv4(frame, &mut rx); + + assert_eq!(rx_queue.len(), 0, "oversized UDP length should be rejected"); + assert_eq!(rx.num_frames(), 1, "frame should be returned"); + } + + // -- swap_addresses tests -- + + #[test] + fn swap_addresses_ipv4() { + let src_mac = [0x11, 0x22, 0x33, 0x44, 0x55, 0x66]; + let dst_mac = [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]; + + let mut buf = build_ipv4_udp_frame(REMOTE_IPV4, LOCAL_IPV4, 12345, 53, b"hello"); + // Set MAC addresses in the Ethernet header. + buf[0..6].copy_from_slice(&dst_mac); + buf[6..12].copy_from_slice(&src_mac); + + let len = buf.len(); + let frame = Frame::new(0, &mut buf, len, false); + + let mut pkt = ReceivedUdpPacket { + src_addr: IpAddress::V4(REMOTE_IPV4), + dst_addr: IpAddress::V4(LOCAL_IPV4), + src_port: 12345, + dst_port: 53, + packet: Packet::Single(frame), + }; + + pkt.swap_addresses(); + + // Metadata should be swapped. + assert_eq!(pkt.src_addr, IpAddress::V4(LOCAL_IPV4)); + assert_eq!(pkt.dst_addr, IpAddress::V4(REMOTE_IPV4)); + assert_eq!(pkt.src_port, 53); + assert_eq!(pkt.dst_port, 12345); + + // Verify wire bytes. + let frame = match &pkt.packet { + Packet::Single(f) => f, + _ => panic!("expected Single"), + }; + + // Ethernet MACs swapped. + assert_eq!(&frame[0..6], &src_mac); + assert_eq!(&frame[6..12], &dst_mac); + + // IPv4 addresses swapped. + let ip = Ipv4Header::from_bytes(frame); + assert_eq!(ip.src_addr, LOCAL_IPV4); + assert_eq!(ip.dst_addr, REMOTE_IPV4); + + // UDP ports swapped. + let udp_off = ip.payload_offset(); + let udp = unsafe { UdpHeader::from_bytes_at(frame, udp_off) }; + assert_eq!(udp.src_port(), 53); + assert_eq!(udp.dst_port(), 12345); + } + + #[test] + fn swap_addresses_ipv6() { + let src_mac = [0x11, 0x22, 0x33, 0x44, 0x55, 0x66]; + let dst_mac = [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]; + + let mut buf = build_ipv6_udp_frame(REMOTE_IPV6, LOCAL_IPV6, 12345, 53, b"hello"); + // Set MAC addresses in the Ethernet header. + buf[0..6].copy_from_slice(&dst_mac); + buf[6..12].copy_from_slice(&src_mac); + + let len = buf.len(); + let frame = Frame::new(0, &mut buf, len, false); + + let mut pkt = ReceivedUdpPacket { + src_addr: IpAddress::V6(REMOTE_IPV6), + dst_addr: IpAddress::V6(LOCAL_IPV6), + src_port: 12345, + dst_port: 53, + packet: Packet::Single(frame), + }; + + pkt.swap_addresses(); + + // Metadata should be swapped. + assert_eq!(pkt.src_addr, IpAddress::V6(LOCAL_IPV6)); + assert_eq!(pkt.dst_addr, IpAddress::V6(REMOTE_IPV6)); + assert_eq!(pkt.src_port, 53); + assert_eq!(pkt.dst_port, 12345); + + // Verify wire bytes. + let frame = match &pkt.packet { + Packet::Single(f) => f, + _ => panic!("expected Single"), + }; + + // Ethernet MACs swapped. + assert_eq!(&frame[0..6], &src_mac); + assert_eq!(&frame[6..12], &dst_mac); + + // IPv6 addresses swapped. + let ip = Ipv6Header::from_bytes(frame); + assert_eq!(ip.src_addr, LOCAL_IPV6); + assert_eq!(ip.dst_addr, REMOTE_IPV6); + + // UDP ports swapped. + let udp_off = ETH_HEADER_LEN + IPV6_HEADER_LEN; + let udp = unsafe { UdpHeader::from_bytes_at(frame, udp_off) }; + assert_eq!(udp.src_port(), 53); + assert_eq!(udp.dst_port(), 12345); + } + + // -- unbind tests -- + + #[test] + fn unbind_explicit_removes_binding() { + let mut handler = UdpHandler::new(256, false); + handler.bind(IpAddress::V4(LOCAL_IPV4), 5000, 128).unwrap(); + + handler.unbind(IpAddress::V4(LOCAL_IPV4), 5000); + + // Should be able to re-bind the same address. + handler.bind(IpAddress::V4(LOCAL_IPV4), 5000, 128).unwrap(); + } + + #[test] + fn unbind_wildcard_removes_binding() { + let mut handler = UdpHandler::new(256, false); + handler + .bind(IpAddress::V4(Ipv4Address::unspecified()), 5000, 128) + .unwrap(); + + handler.unbind(IpAddress::V4(Ipv4Address::unspecified()), 5000); + + // Should be able to re-bind wildcard. + handler + .bind(IpAddress::V4(Ipv4Address::unspecified()), 5000, 128) + .unwrap(); + } + + #[test] + fn unbind_cleans_up_empty_port_entry() { + let mut handler = UdpHandler::new(256, false); + handler.bind(IpAddress::V4(LOCAL_IPV4), 5000, 128).unwrap(); + + handler.unbind(IpAddress::V4(LOCAL_IPV4), 5000); + + // Port entry should be removed — no routing should happen. + let mut buf = build_ipv4_udp_frame(REMOTE_IPV4, LOCAL_IPV4, 12345, 5000, b"hello"); + let len = buf.len(); + let frame = Frame::new(0, &mut buf, len, false); + let mut rx = BasicFrameBuffer::new(4); + handler.process_ipv4(frame, &mut rx); + assert_eq!(rx.num_frames(), 1, "packet should be returned, not routed"); + } + + #[test] + fn unbind_preserves_other_bindings_on_same_port() { + let other_ip = Ipv4Address::new([10, 0, 0, 99]); + let mut handler = UdpHandler::new(256, false); + handler.bind(IpAddress::V4(LOCAL_IPV4), 5000, 128).unwrap(); + let other_q = handler.bind(IpAddress::V4(other_ip), 5000, 128).unwrap(); + + // Unbind only LOCAL_IPV4. + handler.unbind(IpAddress::V4(LOCAL_IPV4), 5000); + + // other_ip binding should still work. + let mut buf = build_ipv4_udp_frame(REMOTE_IPV4, other_ip, 12345, 5000, b"hello"); + let len = buf.len(); + let frame = Frame::new(0, &mut buf, len, false); + let mut rx = BasicFrameBuffer::new(4); + handler.process_ipv4(frame, &mut rx); + assert_eq!(other_q.len(), 1); + } + + #[test] + fn unbind_nonexistent_is_noop() { + let mut handler = UdpHandler::new(256, false); + // Should not panic. + handler.unbind(IpAddress::V4(LOCAL_IPV4), 5000); + } +} diff --git a/src/net/mod.rs b/src/net/mod.rs new file mode 100644 index 0000000..819f4a2 --- /dev/null +++ b/src/net/mod.rs @@ -0,0 +1,14 @@ +pub(crate) mod checksum; +mod fragment; +mod neighbor; +mod pmtu; + +pub mod handler; +pub mod socket; +pub mod wire; + +pub use fragment::{FragmentReader, FragmentWriter, Packet, ReassembledPacket, TransportHeader}; +pub use handler::tcp::tcb::{ConnectionId, TcpError, TcpEvent}; +pub use handler::udp::{BindError, ReceivedUdpPacket}; +pub use neighbor::NeighborHandler; +pub use pmtu::PmtuCache; diff --git a/src/net/neighbor/arp.rs b/src/net/neighbor/arp.rs new file mode 100644 index 0000000..7a3ec2d --- /dev/null +++ b/src/net/neighbor/arp.rs @@ -0,0 +1,509 @@ +use coarsetime::{Duration, Instant}; +use dashmap::DashMap; + +use crate::{ + net::wire::{ + arp::{ARP_FRAME_LEN, ArpFrame, ArpHardwareTypes, ArpOperations}, + ethernet::{EtherTypes, MacAddress}, + ip::{IpAddress, Ipv4Address}, + }, + xdp::frame::{Frame, FrameBuffer}, +}; + +use super::NeighborEntry; + +#[inline(always)] +pub(super) fn resolve_v4<'umem>( + local_mac: MacAddress, + source_ip: Ipv4Address, + target_ip: Ipv4Address, + mut frame: Frame<'umem>, + rx_return: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, +) { + // This is essentially a no-op since frames really can't be this small, but + // JUST in case lets check and bail out. + if frame.capacity() < ARP_FRAME_LEN { + rx_return.push(frame); + return; + } + + // First set the frame length to the full ARP frame length. + // + // SAFETY: we know for a fact we can store this much data so we are good to go. + unsafe { + frame.set_len(ARP_FRAME_LEN); + } + + // Create our ARP request frame: + // - Broadcast ethernet frame, set to ARP protocol. + // - ARP request packet, for Ethernet/IPv4, sent to the target IP. + let arp = ArpFrame::from_bytes_mut(&mut frame); + + // First setup ethernet headers. + arp.ethernet.dst_mac = MacAddress::broadcast(); + arp.ethernet.src_mac = local_mac; + arp.ethernet.ether_type = EtherTypes::Arp; + + // Fill out all arp fields. + arp.arp.htype = ArpHardwareTypes::Ethernet; + arp.arp.ptype = EtherTypes::IPv4; + arp.arp.hlen = 6; + arp.arp.plen = 4; + arp.arp.oper = ArpOperations::Request; + arp.arp.sha = local_mac; + arp.arp.spa = source_ip; + arp.arp.tha = MacAddress::zero(); + arp.arp.tpa = target_ip; + + // Push the frame to the TX buffer for transmission. + tx_return.push(frame); +} + +pub(super) fn handle_arp<'umem>( + now: Instant, + table: &DashMap, + local_mac: MacAddress, + local_ipv4: &[Ipv4Address], + ttl: Duration, + mut frame: Frame<'umem>, + rx_return: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, +) { + // Check frame length here make sure we at least have enough data to parse the ARP packet. + if frame.len() < ARP_FRAME_LEN { + rx_return.push(frame); + return; + } + + // Parse the ARP packet from the frame. + let ArpFrame { ethernet, arp } = ArpFrame::from_bytes_mut(&mut frame); + + // Verify that we are dealing with: + // - Ethernet hardware type. + // - IPv4 protocol type. + // - 6-byte hardware address length. + // - 4-byte protocol address length. + // + // If not, return the frame to the RX buffer. + if arp.htype != ArpHardwareTypes::Ethernet + || arp.ptype != EtherTypes::IPv4 + || arp.hlen != 6 + || arp.plen != 4 + { + rx_return.push(frame); + return; + } + + // Extract the sender and target addresses from the ARP packet. + let sha = arp.sha; + let spa = arp.spa; + + // Insert the sender into the neighbor table. + table.insert(IpAddress::V4(spa), NeighborEntry::new(sha, now + ttl)); + + // If we are not dealing with an ARP request or the target IP is not one of our local IPv4 addresses, + // return the frame to the RX buffer. + if arp.oper != ArpOperations::Request || !local_ipv4.contains(&arp.tpa) { + rx_return.push(frame); + return; + } + + // Swap the ethernet frame's source and destination MAC addresses. + ethernet.dst_mac = sha; + ethernet.src_mac = local_mac; + + // Swap the ARP packet's sender and target addresses, and set the operation to reply. + arp.oper = ArpOperations::Reply; + arp.sha = local_mac; + arp.spa = arp.tpa; + arp.tha = sha; + arp.tpa = spa; + + // Push the frame to the TX buffer. + tx_return.push(frame); +} + +#[cfg(test)] +mod tests { + use crate::{ + net::{ + neighbor::NeighborHandler, + wire::{ + arp::{ARP_FRAME_LEN, ArpFrame, ArpHardwareTypes, ArpOperations, ArpPacket}, + ethernet::{EtherTypes, EthernetFrame, MacAddress}, + ip::Ipv4Address, + }, + }, + xdp::frame::{BasicFrameBuffer, Frame, FrameBuffer}, + }; + + use super::*; + + const TEST_LOCAL_MAC: MacAddress = MacAddress::new([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]); + const TEST_LOCAL_IP: Ipv4Address = Ipv4Address::new([192, 168, 1, 1]); + const TEST_REMOTE_MAC: MacAddress = MacAddress::new([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]); + const TEST_REMOTE_IP: Ipv4Address = Ipv4Address::new([192, 168, 1, 100]); + const TEST_TTL: Duration = Duration::from_secs(60); + + fn new_handler() -> NeighborHandler { + let mut nh = NeighborHandler::new("test0", TEST_TTL).unwrap(); + nh.add_local_ipv4(TEST_LOCAL_IP); + nh.set_local_mac(TEST_LOCAL_MAC); + nh + } + + fn build_arp_request_bytes(target_ip: Ipv4Address) -> [u8; ARP_FRAME_LEN] { + let f = ArpFrame { + ethernet: EthernetFrame { + dst_mac: MacAddress::broadcast(), + src_mac: TEST_REMOTE_MAC, + ether_type: EtherTypes::Arp, + }, + arp: ArpPacket { + htype: ArpHardwareTypes::Ethernet, + ptype: EtherTypes::IPv4, + hlen: 6, + plen: 4, + oper: ArpOperations::Request, + sha: TEST_REMOTE_MAC, + spa: TEST_REMOTE_IP, + tha: MacAddress::zero(), + tpa: target_ip, + }, + }; + let mut bytes = [0u8; ARP_FRAME_LEN]; + bytes.copy_from_slice(f.as_bytes()); + bytes + } + + fn build_arp_reply_bytes( + sender_mac: MacAddress, + sender_ip: Ipv4Address, + ) -> [u8; ARP_FRAME_LEN] { + let f = ArpFrame { + ethernet: EthernetFrame { + dst_mac: TEST_LOCAL_MAC, + src_mac: sender_mac, + ether_type: EtherTypes::Arp, + }, + arp: ArpPacket { + htype: ArpHardwareTypes::Ethernet, + ptype: EtherTypes::IPv4, + hlen: 6, + plen: 4, + oper: ArpOperations::Reply, + sha: sender_mac, + spa: sender_ip, + tha: TEST_LOCAL_MAC, + tpa: TEST_LOCAL_IP, + }, + }; + let mut bytes = [0u8; ARP_FRAME_LEN]; + bytes.copy_from_slice(f.as_bytes()); + bytes + } + + #[test] + fn valid_request_produces_reply() { + let now = Instant::now(); + let handler = new_handler(); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let mut data = build_arp_request_bytes(TEST_LOCAL_IP); + let frame = Frame::new(0, &mut data, ARP_FRAME_LEN, false); + + handler.handle_arp(now, frame, &mut rx, &mut tx); + + assert_eq!(rx.num_frames(), 0); + assert_eq!(tx.num_frames(), 1); + + let reply = tx.pop().unwrap(); + let eth = EthernetFrame::from_bytes(&reply); + let arp = ArpPacket::from_bytes(&reply); + + assert_eq!(eth.dst_mac, TEST_REMOTE_MAC); + assert_eq!(eth.src_mac, TEST_LOCAL_MAC); + assert_eq!(arp.oper, ArpOperations::Reply); + assert_eq!(arp.sha, TEST_LOCAL_MAC); + assert_eq!(arp.spa, TEST_LOCAL_IP); + assert_eq!(arp.tha, TEST_REMOTE_MAC); + assert_eq!(arp.tpa, TEST_REMOTE_IP); + } + + #[test] + fn request_for_wrong_ip_goes_to_rx() { + let now = Instant::now(); + let handler = new_handler(); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let wrong = Ipv4Address::new([10, 0, 0, 1]); + let mut data = build_arp_request_bytes(wrong); + let frame = Frame::new(0, &mut data, ARP_FRAME_LEN, false); + + handler.handle_arp(now, frame, &mut rx, &mut tx); + + assert_eq!(rx.num_frames(), 1); + assert_eq!(tx.num_frames(), 0); + } + + #[test] + fn reply_goes_to_rx() { + let now = Instant::now(); + let handler = new_handler(); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let mut data = build_arp_reply_bytes(TEST_REMOTE_MAC, TEST_REMOTE_IP); + let frame = Frame::new(0, &mut data, ARP_FRAME_LEN, false); + + handler.handle_arp(now, frame, &mut rx, &mut tx); + + assert_eq!(rx.num_frames(), 1); + assert_eq!(tx.num_frames(), 0); + } + + #[test] + fn frame_too_short_goes_to_rx() { + let now = Instant::now(); + let handler = new_handler(); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let mut data = [0u8; 30]; + let frame = Frame::new(0, &mut data, 30, false); + + handler.handle_arp(now, frame, &mut rx, &mut tx); + + assert_eq!(rx.num_frames(), 1); + assert_eq!(tx.num_frames(), 0); + } + + #[test] + fn invalid_htype_goes_to_rx() { + let now = Instant::now(); + let handler = new_handler(); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let mut data = build_arp_request_bytes(TEST_LOCAL_IP); + data[14] = 0xFF; + data[15] = 0xFF; + let frame = Frame::new(0, &mut data, ARP_FRAME_LEN, false); + + handler.handle_arp(now, frame, &mut rx, &mut tx); + + assert_eq!(rx.num_frames(), 1); + assert_eq!(tx.num_frames(), 0); + } + + #[test] + fn invalid_ptype_goes_to_rx() { + let now = Instant::now(); + let handler = new_handler(); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let mut data = build_arp_request_bytes(TEST_LOCAL_IP); + data[16] = 0x86; + data[17] = 0xDD; + let frame = Frame::new(0, &mut data, ARP_FRAME_LEN, false); + + handler.handle_arp(now, frame, &mut rx, &mut tx); + + assert_eq!(rx.num_frames(), 1); + assert_eq!(tx.num_frames(), 0); + } + + #[test] + fn invalid_address_lengths_goes_to_rx() { + let now = Instant::now(); + let handler = new_handler(); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let mut data = build_arp_request_bytes(TEST_LOCAL_IP); + data[18] = 8; + let frame = Frame::new(0, &mut data, ARP_FRAME_LEN, false); + + handler.handle_arp(now, frame, &mut rx, &mut tx); + + assert_eq!(rx.num_frames(), 1); + assert_eq!(tx.num_frames(), 0); + } + + #[test] + fn request_caches_sender() { + let now = Instant::now(); + let handler = new_handler(); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + assert!(handler.lookup_v4(now, &TEST_REMOTE_IP).is_none()); + + let mut data = build_arp_request_bytes(TEST_LOCAL_IP); + let frame = Frame::new(0, &mut data, ARP_FRAME_LEN, false); + handler.handle_arp(now, frame, &mut rx, &mut tx); + + assert_eq!( + handler.lookup_v4(now, &TEST_REMOTE_IP), + Some(TEST_REMOTE_MAC) + ); + } + + #[test] + fn reply_caches_sender() { + let now = Instant::now(); + let handler = new_handler(); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + assert!(handler.lookup_v4(now, &TEST_REMOTE_IP).is_none()); + + let mut data = build_arp_reply_bytes(TEST_REMOTE_MAC, TEST_REMOTE_IP); + let frame = Frame::new(0, &mut data, ARP_FRAME_LEN, false); + handler.handle_arp(now, frame, &mut rx, &mut tx); + + assert_eq!( + handler.lookup_v4(now, &TEST_REMOTE_IP), + Some(TEST_REMOTE_MAC) + ); + } + + #[test] + fn wrong_target_still_caches_sender() { + let now = Instant::now(); + let handler = new_handler(); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let wrong = Ipv4Address::new([10, 0, 0, 1]); + let mut data = build_arp_request_bytes(wrong); + let frame = Frame::new(0, &mut data, ARP_FRAME_LEN, false); + handler.handle_arp(now, frame, &mut rx, &mut tx); + + assert_eq!( + handler.lookup_v4(now, &TEST_REMOTE_IP), + Some(TEST_REMOTE_MAC) + ); + } + + #[test] + fn cache_updates_on_new_mac() { + let now = Instant::now(); + let handler = new_handler(); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let mut data = build_arp_reply_bytes(TEST_REMOTE_MAC, TEST_REMOTE_IP); + let frame = Frame::new(0, &mut data, ARP_FRAME_LEN, false); + handler.handle_arp(now, frame, &mut rx, &mut tx); + assert_eq!( + handler.lookup_v4(now, &TEST_REMOTE_IP), + Some(TEST_REMOTE_MAC) + ); + + let new_mac = MacAddress::new([0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x01]); + let mut data2 = build_arp_reply_bytes(new_mac, TEST_REMOTE_IP); + let frame2 = Frame::new(0, &mut data2, ARP_FRAME_LEN, false); + handler.handle_arp(now, frame2, &mut rx, &mut tx); + assert_eq!(handler.lookup_v4(now, &TEST_REMOTE_IP), Some(new_mac)); + } + + #[test] + fn expired_entry_returns_none() { + let now = Instant::now(); + let mut handler = NeighborHandler::new("test0", Duration::from_ticks(0)).unwrap(); + handler.add_local_ipv4(TEST_LOCAL_IP); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let mut data = build_arp_reply_bytes(TEST_REMOTE_MAC, TEST_REMOTE_IP); + let frame = Frame::new(0, &mut data, ARP_FRAME_LEN, false); + handler.handle_arp(now, frame, &mut rx, &mut tx); + + assert!(handler.lookup_v4(now, &TEST_REMOTE_IP).is_none()); + } + + #[test] + fn lookup_unknown_returns_none() { + let now = Instant::now(); + let handler = new_handler(); + let unknown = Ipv4Address::new([10, 0, 0, 1]); + assert!(handler.lookup_v4(now, &unknown).is_none()); + } + + #[test] + fn invalid_packet_does_not_cache() { + let now = Instant::now(); + let handler = new_handler(); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let mut data = [0u8; 30]; + let frame = Frame::new(0, &mut data, 30, false); + handler.handle_arp(now, frame, &mut rx, &mut tx); + + assert!(handler.lookup_v4(now, &TEST_REMOTE_IP).is_none()); + } + + #[test] + fn resolve_produces_valid_request() { + let handler = new_handler(); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let target = Ipv4Address::new([192, 168, 1, 200]); + + let mut data = [0u8; 64]; + let frame = Frame::new(0, &mut data, 1, false); + + handler.resolve_v4(TEST_LOCAL_IP, target, frame, &mut rx, &mut tx); + + assert_eq!(rx.num_frames(), 0); + assert_eq!(tx.num_frames(), 1); + + let req = tx.pop().unwrap(); + assert_eq!(req.len(), ARP_FRAME_LEN); + + let eth = EthernetFrame::from_bytes(&req); + let arp = ArpPacket::from_bytes(&req); + + assert_eq!(eth.dst_mac, MacAddress::broadcast()); + assert_eq!(eth.src_mac, TEST_LOCAL_MAC); + assert_eq!(eth.ether_type, EtherTypes::Arp); + + assert_eq!(arp.htype, ArpHardwareTypes::Ethernet); + assert_eq!(arp.ptype, EtherTypes::IPv4); + assert_eq!(arp.hlen, 6); + assert_eq!(arp.plen, 4); + assert_eq!(arp.oper, ArpOperations::Request); + assert_eq!(arp.sha, TEST_LOCAL_MAC); + assert_eq!(arp.spa, TEST_LOCAL_IP); + assert_eq!(arp.tha, MacAddress::zero()); + assert_eq!(arp.tpa, target); + } + + #[test] + fn resolve_frame_too_small_goes_to_rx() { + let handler = new_handler(); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let mut data = [0u8; 20]; + let frame = Frame::new(0, &mut data, 1, false); + + handler.resolve_v4( + TEST_LOCAL_IP, + Ipv4Address::new([10, 0, 0, 1]), + frame, + &mut rx, + &mut tx, + ); + + assert_eq!(rx.num_frames(), 1); + assert_eq!(tx.num_frames(), 0); + } +} diff --git a/src/net/neighbor/entry.rs b/src/net/neighbor/entry.rs new file mode 100644 index 0000000..49f919f --- /dev/null +++ b/src/net/neighbor/entry.rs @@ -0,0 +1,27 @@ +use coarsetime::Instant; + +use crate::net::wire::ethernet::MacAddress; + +/// A neighbor entry representation. +#[derive(Debug)] +pub(super) struct NeighborEntry { + mac: MacAddress, + expires_at: Instant, +} + +impl NeighborEntry { + /// Creates a new neighbor entry. + pub const fn new(mac: MacAddress, expires_at: Instant) -> Self { + Self { mac, expires_at } + } + + /// Returns true if the entry is expired. + pub fn is_expired(&self, now: Instant) -> bool { + now >= self.expires_at + } + + /// Returns the MAC address. + pub const fn mac(&self) -> MacAddress { + self.mac + } +} diff --git a/src/net/neighbor/handler.rs b/src/net/neighbor/handler.rs new file mode 100644 index 0000000..1ef17fa --- /dev/null +++ b/src/net/neighbor/handler.rs @@ -0,0 +1,497 @@ +use std::net::IpAddr; + +use coarsetime::{Duration, Instant}; +use dashmap::DashMap; +use getifaddrs::InterfaceFilter; + +use crate::{ + net::wire::{ + ethernet::MacAddress, + ip::{IpAddress, Ipv4Address, Ipv6Address}, + }, + xdp::{ + error::Result, + frame::{Frame, FrameBuffer}, + }, +}; + +use super::{ + NeighborEntry, + arp::{handle_arp, resolve_v4}, + ndp::{handle_ndp, resolve_v6}, +}; + +/// Unified neighbor-resolution handler for ARP (IPv4) and NDP (IPv6). +/// +/// Maintains an internal neighbor cache that maps protocol addresses to +/// hardware (MAC) addresses with a configurable TTL. Incoming ARP traffic +/// (both requests and replies) automatically populates the cache. +#[derive(Debug)] +pub struct NeighborHandler { + local_mac: MacAddress, + local_ipv4: Vec, + local_ipv6: Vec, + table: DashMap, + ttl: Duration, + rx_offload: bool, + tx_offload: bool, +} + +impl NeighborHandler { + /// Creates a new handler bound to the given MAC and IPv4 address. + pub fn new(if_name: &str, ttl: Duration) -> Result { + let mut local_ipv4 = Vec::new(); + let mut local_ipv6 = Vec::new(); + let mut local_mac = MacAddress::zero(); + + let addresses = InterfaceFilter::new().name(if_name).get()?; + for addr in addresses { + if let Some(mac) = addr.address.mac_addr() { + local_mac = MacAddress::from(mac); + continue; + } + let Some(ip_addr) = addr.address.ip_addr() else { + continue; + }; + match ip_addr { + IpAddr::V4(ip_addr) => { + local_ipv4.push(ip_addr.into()); + } + IpAddr::V6(ip_addr) => { + local_ipv6.push(ip_addr.into()); + } + } + } + Ok(Self { + local_mac, + local_ipv4, + local_ipv6, + table: DashMap::new(), + ttl, + rx_offload: false, + tx_offload: false, + }) + } + + /// Sets the checksum offload flags. + pub fn set_offload(&mut self, rx_offload: bool, tx_offload: bool) { + self.rx_offload = rx_offload; + self.tx_offload = tx_offload; + } + + /// Sets the local MAC address. + pub fn set_local_mac(&mut self, mac: MacAddress) { + self.local_mac = mac; + } + + /// Registers a local IPv6 address for NDP response. + pub fn add_local_ipv6(&mut self, addr: Ipv6Address) { + if !self.local_ipv6.contains(&addr) { + self.local_ipv6.push(addr); + } + } + + /// Registers a local IPv4 address for ARP response. + pub fn add_local_ipv4(&mut self, addr: Ipv4Address) { + if !self.local_ipv4.contains(&addr) { + self.local_ipv4.push(addr); + } + } + + /// Registers a local IP address for NDP or ARP response. + pub fn add_local_ip(&mut self, addr: IpAddress) { + match addr { + IpAddress::V4(addr) => self.add_local_ipv4(addr), + IpAddress::V6(addr) => self.add_local_ipv6(addr), + } + } + + /// Removes all entries whose TTL has expired. + pub fn evict_stale(&self, now: Instant) { + self.table.retain(|_, entry| !entry.is_expired(now)); + } + + /// Looks up a cached MAC for the given IP address (v4 or v6). + /// + /// Returns `None` if the entry is missing or expired. + pub fn lookup(&self, now: Instant, ip: &IpAddress) -> Option { + self.table.get(ip).and_then(|e| { + if !e.is_expired(now) { + Some(e.mac()) + } else { + None + } + }) + } + + /// Convenience wrapper that looks up an IPv4 address in the neighbor cache. + pub fn lookup_v4(&self, now: Instant, ip: &Ipv4Address) -> Option { + self.lookup(now, &IpAddress::V4(*ip)) + } + + /// Convenience wrapper that looks up an IPv6 address in the neighbor cache. + pub fn lookup_v6(&self, now: Instant, ip: &Ipv6Address) -> Option { + self.lookup(now, &IpAddress::V6(*ip)) + } + + /// Returns the local MAC address. + pub fn local_mac(&self) -> MacAddress { + self.local_mac + } + + /// Constructs a broadcast ARP request for `target_ip` and enqueues it on + /// `tx_return`. + /// + /// If the frame capacity is too small the error is logged and the frame + /// is pushed to `rx_return` instead. + pub fn resolve_v4<'umem>( + &self, + source_ip: Ipv4Address, + target_ip: Ipv4Address, + frame: Frame<'umem>, + rx_return: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) { + resolve_v4( + self.local_mac, + source_ip, + target_ip, + frame, + rx_return, + tx_return, + ); + } + + /// Constructs a Neighbor Solicitation for `target_ip` and enqueues it + /// on `tx_return`. + /// + /// The solicitation is sent to the solicited-node multicast address + /// derived from `target_ip` and includes a Source Link-Layer Address + /// option containing our MAC. + pub fn resolve_v6<'umem>( + &self, + source_ip: Ipv6Address, + target_ip: Ipv6Address, + frame: Frame<'umem>, + rx_return: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) { + resolve_v6( + self.local_mac, + source_ip, + target_ip, + self.tx_offload, + frame, + rx_return, + tx_return, + ); + } + + /// Processes an incoming ARP frame. + /// + /// The frame is always consumed and pushed to exactly one buffer: + /// + /// * `rx_return` -- validation errors, non-Ethernet/IPv4 ARP, replies, + /// or requests not targeting our IP. + /// * `tx_return` -- valid ARP requests targeting our IPv4 address, after + /// the frame has been modified in-place to carry the reply. + /// + /// Both requests and replies from structurally valid Ethernet/IPv4 ARP + /// packets update the neighbor cache. + pub fn handle_arp<'umem>( + &self, + now: Instant, + frame: Frame<'umem>, + rx_return: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) { + handle_arp( + now, + &self.table, + self.local_mac, + &self.local_ipv4, + self.ttl, + frame, + rx_return, + tx_return, + ); + } + + /// Handles an incoming NDP (ICMPv6 Neighbor Discovery) frame. + /// + /// Dispatches on ICMPv6 type: + /// * Neighbor Solicitation (135) -- reply with NA if targeting our address + /// * Neighbor Advertisement (136) -- cache the advertised MAC + /// * Router Advertisement (134) -- cache the router's MAC + /// * Router Solicitation (133) and Redirect (137) -- pass to `rx_return` + pub fn handle_ndp<'umem>( + &self, + now: Instant, + frame: Frame<'umem>, + icmpv6_offset: usize, + icmpv6_len: usize, + rx_return: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) { + handle_ndp( + now, + self.ttl, + &self.table, + &self.local_ipv6, + self.local_mac, + self.rx_offload, + self.tx_offload, + frame, + icmpv6_offset, + icmpv6_len, + rx_return, + tx_return, + ); + } +} + +#[cfg(test)] +mod tests { + use crate::{ + net::wire::{ + arp::{ARP_FRAME_LEN, ArpFrame, ArpHardwareTypes, ArpOperations, ArpPacket}, + ethernet::{EtherTypes, EthernetFrame, MacAddress}, + }, + xdp::frame::BasicFrameBuffer, + }; + + use super::*; + + const TEST_LOCAL_MAC: MacAddress = MacAddress::new([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]); + const TEST_LOCAL_IP: Ipv4Address = Ipv4Address::new([192, 168, 1, 1]); + const TEST_REMOTE_MAC: MacAddress = MacAddress::new([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]); + const TEST_REMOTE_IP: Ipv4Address = Ipv4Address::new([192, 168, 1, 100]); + const TEST_TTL: Duration = Duration::from_secs(60); + const TEST_LOCAL_IPV6: Ipv6Address = + Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + + fn new_handler() -> NeighborHandler { + let mut nh = NeighborHandler::new("test0", TEST_TTL).unwrap(); + nh.set_local_mac(TEST_LOCAL_MAC); + nh.add_local_ipv4(TEST_LOCAL_IP); + nh.add_local_ipv6(TEST_LOCAL_IPV6); + nh + } + + fn build_arp_reply(sender_mac: MacAddress, sender_ip: Ipv4Address) -> [u8; ARP_FRAME_LEN] { + let f = ArpFrame { + ethernet: EthernetFrame { + dst_mac: TEST_LOCAL_MAC, + src_mac: sender_mac, + ether_type: EtherTypes::Arp, + }, + arp: ArpPacket { + htype: ArpHardwareTypes::Ethernet, + ptype: EtherTypes::IPv4, + hlen: 6, + plen: 4, + oper: ArpOperations::Reply, + sha: sender_mac, + spa: sender_ip, + tha: TEST_LOCAL_MAC, + tpa: TEST_LOCAL_IP, + }, + }; + let mut bytes = [0u8; ARP_FRAME_LEN]; + bytes.copy_from_slice(f.as_bytes()); + bytes + } + + fn build_arp_request(target_ip: Ipv4Address) -> [u8; ARP_FRAME_LEN] { + let f = ArpFrame { + ethernet: EthernetFrame { + dst_mac: MacAddress::broadcast(), + src_mac: TEST_REMOTE_MAC, + ether_type: EtherTypes::Arp, + }, + arp: ArpPacket { + htype: ArpHardwareTypes::Ethernet, + ptype: EtherTypes::IPv4, + hlen: 6, + plen: 4, + oper: ArpOperations::Request, + sha: TEST_REMOTE_MAC, + spa: TEST_REMOTE_IP, + tha: MacAddress::zero(), + tpa: target_ip, + }, + }; + let mut bytes = [0u8; ARP_FRAME_LEN]; + bytes.copy_from_slice(f.as_bytes()); + bytes + } + + #[test] + fn local_mac_returns_configured_value() { + let handler = new_handler(); + assert_eq!(handler.local_mac(), TEST_LOCAL_MAC); + } + + #[test] + fn lookup_unknown_v4_returns_none() { + let now = Instant::now(); + let handler = new_handler(); + let unknown = IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])); + assert!(handler.lookup(now, &unknown).is_none()); + } + + #[test] + fn lookup_unknown_v6_returns_none() { + let now = Instant::now(); + let handler = new_handler(); + let unknown = IpAddress::V6(Ipv6Address::new([ + 0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 99, + ])); + assert!(handler.lookup(now, &unknown).is_none()); + } + + #[test] + fn lookup_returns_cached_entry() { + let now = Instant::now(); + let handler = new_handler(); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let mut data = build_arp_reply(TEST_REMOTE_MAC, TEST_REMOTE_IP); + let frame = Frame::new(0, &mut data, ARP_FRAME_LEN, false); + handler.handle_arp(now, frame, &mut rx, &mut tx); + + assert_eq!( + handler.lookup(now, &IpAddress::V4(TEST_REMOTE_IP)), + Some(TEST_REMOTE_MAC) + ); + } + + #[test] + fn lookup_expired_entry_returns_none() { + let now = Instant::now(); + let handler = new_handler(); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let mut data = build_arp_reply(TEST_REMOTE_MAC, TEST_REMOTE_IP); + let frame = Frame::new(0, &mut data, ARP_FRAME_LEN, false); + handler.handle_arp(now, frame, &mut rx, &mut tx); + + let future = now + TEST_TTL + Duration::from_secs(1); + assert!( + handler + .lookup(future, &IpAddress::V4(TEST_REMOTE_IP)) + .is_none() + ); + } + + #[test] + fn evict_stale_removes_expired_entries() { + let now = Instant::now(); + let handler = new_handler(); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let mut data = build_arp_reply(TEST_REMOTE_MAC, TEST_REMOTE_IP); + let frame = Frame::new(0, &mut data, ARP_FRAME_LEN, false); + handler.handle_arp(now, frame, &mut rx, &mut tx); + + assert!(handler.lookup_v4(now, &TEST_REMOTE_IP).is_some()); + + let future = now + TEST_TTL + Duration::from_secs(1); + handler.evict_stale(future); + + assert!(handler.lookup_v4(future, &TEST_REMOTE_IP).is_none()); + } + + #[test] + fn evict_stale_keeps_fresh_entries() { + let now = Instant::now(); + let handler = new_handler(); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let mut data = build_arp_reply(TEST_REMOTE_MAC, TEST_REMOTE_IP); + let frame = Frame::new(0, &mut data, ARP_FRAME_LEN, false); + handler.handle_arp(now, frame, &mut rx, &mut tx); + + let still_valid = now + Duration::from_secs(30); + handler.evict_stale(still_valid); + + assert_eq!( + handler.lookup_v4(still_valid, &TEST_REMOTE_IP), + Some(TEST_REMOTE_MAC) + ); + } + + #[test] + fn evict_stale_selectively_removes_only_expired() { + let now = Instant::now(); + let short_ttl = Duration::from_secs(10); + let mut handler = NeighborHandler::new("test0", short_ttl).unwrap(); + handler.add_local_ipv4(TEST_LOCAL_IP); + + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + // Insert entry A at t=0 (expires at t=10) + let ip_a = Ipv4Address::new([192, 168, 1, 100]); + let mac_a = MacAddress::new([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]); + let mut data_a = build_arp_reply(mac_a, ip_a); + let frame_a = Frame::new(0, &mut data_a, ARP_FRAME_LEN, false); + handler.handle_arp(now, frame_a, &mut rx, &mut tx); + + // Insert entry B at t=5s (expires at t=15s) + let t5 = now + Duration::from_secs(5); + let ip_b = Ipv4Address::new([192, 168, 1, 101]); + let mac_b = MacAddress::new([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x01]); + let mut data_b = build_arp_reply(mac_b, ip_b); + let frame_b = Frame::new(0, &mut data_b, ARP_FRAME_LEN, false); + handler.handle_arp(t5, frame_b, &mut rx, &mut tx); + + // At t=11, A is expired but B is still valid + let t11 = now + Duration::from_secs(11); + handler.evict_stale(t11); + + assert!(handler.lookup_v4(t11, &ip_a).is_none()); + assert_eq!(handler.lookup_v4(t11, &ip_b), Some(mac_b)); + } + + #[test] + fn add_local_ip_v4_enables_arp_response() { + let now = Instant::now(); + let mut handler = NeighborHandler::new("test0", TEST_TTL).unwrap(); + + let new_ip = Ipv4Address::new([10, 0, 0, 1]); + handler.add_local_ip(IpAddress::V4(new_ip)); + + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let mut data = build_arp_request(new_ip); + let frame = Frame::new(0, &mut data, ARP_FRAME_LEN, false); + handler.handle_arp(now, frame, &mut rx, &mut tx); + + assert_eq!(tx.num_frames(), 1); + assert_eq!(rx.num_frames(), 0); + } + + #[test] + fn add_local_ipv4_dedup_does_not_break_responses() { + let now = Instant::now(); + let mut handler = NeighborHandler::new("test0", TEST_TTL).unwrap(); + handler.add_local_ipv4(TEST_LOCAL_IP); + handler.add_local_ipv4(TEST_LOCAL_IP); // duplicate + + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let mut data = build_arp_request(TEST_LOCAL_IP); + let frame = Frame::new(0, &mut data, ARP_FRAME_LEN, false); + handler.handle_arp(now, frame, &mut rx, &mut tx); + + assert_eq!(tx.num_frames(), 1); + assert_eq!(rx.num_frames(), 0); + } +} diff --git a/src/net/neighbor/mod.rs b/src/net/neighbor/mod.rs new file mode 100644 index 0000000..63b931b --- /dev/null +++ b/src/net/neighbor/mod.rs @@ -0,0 +1,7 @@ +mod arp; +mod entry; +mod handler; +mod ndp; + +use entry::NeighborEntry; +pub use handler::NeighborHandler; diff --git a/src/net/neighbor/ndp.rs b/src/net/neighbor/ndp.rs new file mode 100644 index 0000000..2cc13b5 --- /dev/null +++ b/src/net/neighbor/ndp.rs @@ -0,0 +1,955 @@ +use coarsetime::{Duration, Instant}; +use dashmap::DashMap; + +use crate::{ + net::checksum::compute_icmpv6_checksum, + net::wire::{ + ethernet::{EtherTypes, EthernetFrame, MacAddress}, + icmpv6::Icmpv6Types, + ip::{IpAddress, IpProtocols, Ipv6Address, Ipv6Header}, + ndp::{ + ALL_NODES_MULTICAST, NDP_MIN_NS_NA_LEN, NDP_MIN_RA_LEN, NDP_NA_FRAME_LEN, + NDP_NS_FRAME_LEN, NdpNaFrame, NdpNaMessage, NdpNsFrame, NdpNsMessage, + }, + }, + xdp::frame::{Frame, FrameBuffer}, +}; + +use super::NeighborEntry; + +pub(super) fn resolve_v6<'umem>( + local_mac: MacAddress, + source_ip: Ipv6Address, + target_ip: Ipv6Address, + tx_offload: bool, + mut frame: Frame<'umem>, + rx_return: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, +) { + // This is essentially a no-op since frames really can't be this small, but + // JUST in case lets check and bail out. + if frame.capacity() < NDP_NS_FRAME_LEN { + rx_return.push(frame); + return; + } + + let sol_mcast = target_ip.solicited_node_multicast(); + let dst_mac = sol_mcast.multicast_mac(); + + // First set the frame length to the full NDP NS frame length. + // + // SAFETY: we know for a fact we can store this much data so we are good to go. + unsafe { + frame.set_len(NDP_NS_FRAME_LEN); + } + + // Create our NDP NS frame. + let pkt = NdpNsFrame::from_bytes_mut(&mut frame); + + // Setup ethernet headers. + pkt.ethernet.dst_mac = dst_mac; + pkt.ethernet.src_mac = local_mac; + pkt.ethernet.ether_type = EtherTypes::IPv6; + + // Setup IPv6 header. + pkt.ipv6.version_tc_fl = [0x60, 0x00, 0x00, 0x00]; + pkt.ipv6.payload_length = (size_of::() as u16).to_be_bytes(); + pkt.ipv6.next_header = IpProtocols::IcmpV6; + pkt.ipv6.hop_limit = 255; // per RFC 4861 + pkt.ipv6.src_addr = source_ip; + pkt.ipv6.dst_addr = sol_mcast; + + // Setup NDP NS message. + pkt.ns.icmp_type = Icmpv6Types::NeighborSolicitation; + pkt.ns.code = 0; + pkt.ns.checksum = [0, 0]; + pkt.ns.reserved = [0; 4]; + pkt.ns.target = target_ip; + pkt.ns.opt_type = 1; // Source Link-Layer Address + pkt.ns.opt_len = 1; // 1 unit of 8 bytes + pkt.ns.opt_mac = local_mac; + + // Compute checksum. + if !tx_offload { + pkt.ns.checksum = compute_icmpv6_checksum(&source_ip, &sol_mcast, pkt.ns.as_bytes()); + } + + // Push the frame to the TX buffer for transmission. + tx_return.push(frame); +} + +pub(super) fn handle_ndp<'umem>( + now: Instant, + ttl: Duration, + table: &DashMap, + local_ipv6: &[Ipv6Address], + local_mac: MacAddress, + rx_offload: bool, + tx_offload: bool, + frame: Frame<'umem>, + icmpv6_offset: usize, + icmpv6_len: usize, + rx_return: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, +) { + if icmpv6_len < 8 { + rx_return.push(frame); + return; + } + + let icmpv6_end = icmpv6_offset + icmpv6_len; + + // Validate checksum. + if !rx_offload { + let ip = Ipv6Header::from_bytes(&frame); + let src_addr = ip.src_addr; + let dst_addr = ip.dst_addr; + if compute_icmpv6_checksum(&src_addr, &dst_addr, &frame[icmpv6_offset..icmpv6_end]) + != [0x00, 0x00] + { + rx_return.push(frame); + return; + } + } + + let icmpv6_type = frame[icmpv6_offset]; + + match icmpv6_type { + Icmpv6Types::NeighborSolicitation => { + handle_neighbor_solicitation( + now, + ttl, + table, + local_ipv6, + local_mac, + tx_offload, + frame, + icmpv6_offset, + icmpv6_len, + rx_return, + tx_return, + ); + } + Icmpv6Types::NeighborAdvertisement => { + handle_neighbor_advertisement( + now, + ttl, + table, + frame, + icmpv6_offset, + icmpv6_len, + rx_return, + ); + } + Icmpv6Types::RouterAdvertisement => { + handle_router_advertisement( + now, + ttl, + table, + frame, + icmpv6_offset, + icmpv6_len, + rx_return, + ); + } + _ => { + // RS (133), Redirect (137), or unknown NDP type. + rx_return.push(frame); + } + } +} + +fn handle_neighbor_solicitation<'umem>( + now: Instant, + ttl: Duration, + table: &DashMap, + local_ipv6: &[Ipv6Address], + local_mac: MacAddress, + tx_offload: bool, + mut frame: Frame<'umem>, + icmpv6_offset: usize, + icmpv6_len: usize, + rx_return: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, +) { + if icmpv6_len < NDP_MIN_NS_NA_LEN { + rx_return.push(frame); + return; + } + + let icmpv6_end = icmpv6_offset + icmpv6_len; + + // Extract target address (bytes 8..24 relative to ICMPv6 header). + let mut target_bytes = [0u8; 16]; + target_bytes.copy_from_slice(&frame[icmpv6_offset + 8..icmpv6_offset + 24]); + let target_addr = Ipv6Address::from(target_bytes); + + // Extract IPv6 source address. + let ip = Ipv6Header::from_bytes(&frame); + let src_addr = ip.src_addr; + + // Parse Source Link-Layer Address option (type=1) to get sender MAC. + let options_start = icmpv6_offset + 24; + let sender_mac = parse_ndp_link_layer_option(&frame, options_start, icmpv6_end, 1); + + // Cache sender's MAC if source is not unspecified (DAD uses ::). + if !src_addr.is_unspecified() + && let Some(mac) = sender_mac + { + table.insert(IpAddress::V6(src_addr), NeighborEntry::new(mac, now + ttl)); + } + + // Check if the target is one of our addresses. + if !local_ipv6.contains(&target_addr) { + rx_return.push(frame); + return; + } + + // Build NA reply. + let (reply_dst_addr, reply_dst_mac, na_flags) = if src_addr.is_unspecified() { + ( + ALL_NODES_MULTICAST, + ALL_NODES_MULTICAST.multicast_mac(), + [0x20, 0x00, 0x00, 0x00], + ) + } else { + ( + src_addr, + sender_mac.unwrap_or_else(|| { + // Fallback: use the Ethernet source MAC from the frame. + let eth = EthernetFrame::from_bytes(&frame); + eth.src_mac + }), + [0x60, 0x00, 0x00, 0x00], + ) + }; + + // First set the frame length to the full NDP NA frame length. + // + // SAFETY: we know for a fact we can store this much data so we are good to go. + unsafe { + frame.set_len(NDP_NA_FRAME_LEN); + } + + // Create our NDP NA frame. + let pkt = NdpNaFrame::from_bytes_mut(&mut frame); + + // Setup ethernet headers. + pkt.ethernet.dst_mac = reply_dst_mac; + pkt.ethernet.src_mac = local_mac; + pkt.ethernet.ether_type = EtherTypes::IPv6; + + // Setup IPv6 header. + pkt.ipv6.version_tc_fl = [0x60, 0x00, 0x00, 0x00]; + pkt.ipv6.payload_length = (size_of::() as u16).to_be_bytes(); + pkt.ipv6.next_header = IpProtocols::IcmpV6; + pkt.ipv6.hop_limit = 255; // per RFC 4861 + pkt.ipv6.src_addr = target_addr; + pkt.ipv6.dst_addr = reply_dst_addr; + + // Setup NDP NA message. + pkt.na.icmp_type = Icmpv6Types::NeighborAdvertisement; + pkt.na.code = 0; + pkt.na.checksum = [0, 0]; + pkt.na.flags = na_flags; + pkt.na.target = target_addr; + pkt.na.opt_type = 2; // Target Link-Layer Address + pkt.na.opt_len = 1; // 1 unit of 8 bytes + pkt.na.opt_mac = local_mac; + if !tx_offload { + pkt.na.checksum = compute_icmpv6_checksum(&target_addr, &reply_dst_addr, pkt.na.as_bytes()); + } + + tx_return.push(frame); +} + +fn handle_neighbor_advertisement<'umem>( + now: Instant, + ttl: Duration, + table: &DashMap, + frame: Frame<'umem>, + icmpv6_offset: usize, + icmpv6_len: usize, + rx_return: &mut impl FrameBuffer<'umem>, +) { + if icmpv6_len < NDP_MIN_NS_NA_LEN { + rx_return.push(frame); + return; + } + + let icmpv6_end = icmpv6_offset + icmpv6_len; + + // Extract target address. + let mut target_bytes = [0u8; 16]; + target_bytes.copy_from_slice(&frame[icmpv6_offset + 8..icmpv6_offset + 24]); + let target_addr = Ipv6Address::from(target_bytes); + + // Parse Target Link-Layer Address option (type=2). + let options_start = icmpv6_offset + 24; + if let Some(mac) = parse_ndp_link_layer_option(&frame, options_start, icmpv6_end, 2) { + table.insert( + IpAddress::V6(target_addr), + NeighborEntry::new(mac, now + ttl), + ); + } + + rx_return.push(frame); +} + +fn handle_router_advertisement<'umem>( + now: Instant, + ttl: Duration, + table: &DashMap, + frame: Frame<'umem>, + icmpv6_offset: usize, + icmpv6_len: usize, + rx_return: &mut impl FrameBuffer<'umem>, +) { + if icmpv6_len < NDP_MIN_RA_LEN { + rx_return.push(frame); + return; + } + + let icmpv6_end = icmpv6_offset + icmpv6_len; + + // Extract IPv6 source address (router). + let ip = Ipv6Header::from_bytes(&frame); + let src_addr = ip.src_addr; + + // Parse Source Link-Layer Address option (type=1). + let options_start = icmpv6_offset + 16; // RA header is 16 bytes + if let Some(mac) = parse_ndp_link_layer_option(&frame, options_start, icmpv6_end, 1) { + table.insert(IpAddress::V6(src_addr), NeighborEntry::new(mac, now + ttl)); + } + + rx_return.push(frame); +} + +/// Walks NDP options looking for a Link-Layer Address option of the +/// specified `option_type` (1 = Source, 2 = Target). +/// +/// Returns the 6-byte MAC address if found, `None` otherwise. +fn parse_ndp_link_layer_option( + frame: &[u8], + mut offset: usize, + end: usize, + option_type: u8, +) -> Option { + while offset + 2 <= end { + let opt_type = frame[offset]; + let opt_len = frame[offset + 1] as usize; + + // Option length is in units of 8 bytes; 0 is invalid. + if opt_len == 0 { + return None; + } + + let opt_byte_len = opt_len * 8; + if offset + opt_byte_len > end { + return None; + } + + if opt_type == option_type && opt_byte_len >= 8 { + let mac = MacAddress::new([ + frame[offset + 2], + frame[offset + 3], + frame[offset + 4], + frame[offset + 5], + frame[offset + 6], + frame[offset + 7], + ]); + return Some(mac); + } + + offset += opt_byte_len; + } + None +} + +#[cfg(test)] +mod tests { + use crate::{ + net::{ + neighbor::NeighborHandler, + wire::ip::{IPV6_HEADER_LEN, Ipv4Address}, + }, + xdp::frame::BasicFrameBuffer, + }; + + use super::*; + + const TEST_LOCAL_MAC: MacAddress = MacAddress::new([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]); + const TEST_LOCAL_IP: Ipv4Address = Ipv4Address::new([192, 168, 1, 1]); + const TEST_REMOTE_MAC: MacAddress = MacAddress::new([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]); + const TEST_TTL: Duration = Duration::from_secs(60); + + const TEST_LOCAL_IPV6: Ipv6Address = + Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + const TEST_REMOTE_IPV6: Ipv6Address = + Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]); + + fn new_handler() -> NeighborHandler { + let mut nh = NeighborHandler::new("test0", TEST_TTL).unwrap(); + nh.set_local_mac(TEST_LOCAL_MAC); + nh.add_local_ipv4(TEST_LOCAL_IP); + nh.add_local_ipv6(TEST_LOCAL_IPV6); + nh + } + + fn build_ndp_frame( + src_mac: [u8; 6], + dst_mac: [u8; 6], + src_ip: Ipv6Address, + dst_ip: Ipv6Address, + icmpv6_payload: &[u8], + ) -> Vec { + use std::mem::size_of; + + let eth_len = size_of::(); + let frame_len = eth_len + IPV6_HEADER_LEN + icmpv6_payload.len(); + let mut buf = vec![0u8; frame_len]; + + buf[0..6].copy_from_slice(&dst_mac); + buf[6..12].copy_from_slice(&src_mac); + buf[12] = 0x86; + buf[13] = 0xDD; + + buf[14] = 0x60; + let payload_len = (icmpv6_payload.len() as u16).to_be_bytes(); + buf[18..20].copy_from_slice(&payload_len); + buf[20] = IpProtocols::IcmpV6; + buf[21] = 255; + let src_bytes: [u8; 16] = src_ip.into(); + buf[22..38].copy_from_slice(&src_bytes); + let dst_bytes: [u8; 16] = dst_ip.into(); + buf[38..54].copy_from_slice(&dst_bytes); + + buf[eth_len + IPV6_HEADER_LEN..].copy_from_slice(icmpv6_payload); + + let icmp_off = eth_len + IPV6_HEADER_LEN; + buf[icmp_off + 2] = 0; + buf[icmp_off + 3] = 0; + let cksum = compute_icmpv6_checksum(&src_ip, &dst_ip, &buf[icmp_off..]); + buf[icmp_off + 2] = cksum[0]; + buf[icmp_off + 3] = cksum[1]; + + buf + } + + fn build_ns_payload(target: Ipv6Address, source_mac: Option<[u8; 6]>) -> Vec { + let mut payload = vec![0u8; 24]; + payload[0] = 135; + let target_bytes: [u8; 16] = target.into(); + payload[8..24].copy_from_slice(&target_bytes); + + if let Some(mac) = source_mac { + payload.push(1); + payload.push(1); + payload.extend_from_slice(&mac); + } + + payload + } + + fn build_na_payload(target: Ipv6Address, flags: u8, target_mac: Option<[u8; 6]>) -> Vec { + let mut payload = vec![0u8; 24]; + payload[0] = 136; + payload[4] = flags; + let target_bytes: [u8; 16] = target.into(); + payload[8..24].copy_from_slice(&target_bytes); + + if let Some(mac) = target_mac { + payload.push(2); + payload.push(1); + payload.extend_from_slice(&mac); + } + + payload + } + + fn build_ra_payload(source_mac: Option<[u8; 6]>) -> Vec { + let mut payload = vec![0u8; 16]; + payload[0] = 134; + + if let Some(mac) = source_mac { + payload.push(1); + payload.push(1); + payload.extend_from_slice(&mac); + } + + payload + } + + fn icmpv6_offset() -> usize { + std::mem::size_of::() + IPV6_HEADER_LEN + } + + #[test] + fn ns_targeting_our_ip_produces_na() { + let now = Instant::now(); + let handler = new_handler(); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let remote_mac: [u8; 6] = TEST_REMOTE_MAC.into(); + let local_mac: [u8; 6] = TEST_LOCAL_MAC.into(); + let ns = build_ns_payload(TEST_LOCAL_IPV6, Some(remote_mac)); + let sol_mcast = TEST_LOCAL_IPV6.solicited_node_multicast(); + let frame_data = build_ndp_frame( + remote_mac, + sol_mcast.multicast_mac().into(), + TEST_REMOTE_IPV6, + sol_mcast, + &ns, + ); + + let mut buf = vec![0u8; 512]; + buf[..frame_data.len()].copy_from_slice(&frame_data); + let frame = Frame::new(0, &mut buf, frame_data.len(), false); + + let off = icmpv6_offset(); + let icmp_len = frame_data.len() - off; + handler.handle_ndp(now, frame, off, icmp_len, &mut rx, &mut tx); + + assert_eq!(rx.num_frames(), 0); + assert_eq!(tx.num_frames(), 1); + + let reply = tx.pop().unwrap(); + + assert_eq!(reply[off], 136); + assert_eq!(reply[off + 4], 0x60); + + let mut target = [0u8; 16]; + target.copy_from_slice(&reply[off + 8..off + 24]); + assert_eq!(Ipv6Address::from(target), TEST_LOCAL_IPV6); + + assert_eq!(reply[off + 24], 2); + assert_eq!(reply[off + 25], 1); + assert_eq!(&reply[off + 26..off + 32], &local_mac); + + let ip = Ipv6Header::from_bytes(&reply); + assert_eq!(ip.hop_limit, 255); + + let cksum = compute_icmpv6_checksum(&ip.src_addr, &ip.dst_addr, &reply[off..off + 32]); + assert_eq!(cksum, [0x00, 0x00]); + } + + #[test] + fn ns_targeting_unknown_ip_goes_to_rx() { + let now = Instant::now(); + let handler = new_handler(); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let unknown = Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 99]); + let remote_mac: [u8; 6] = TEST_REMOTE_MAC.into(); + let ns = build_ns_payload(unknown, Some(remote_mac)); + let frame_data = build_ndp_frame( + remote_mac, + [0x33, 0x33, 0x00, 0x00, 0x00, 0x63], + TEST_REMOTE_IPV6, + unknown.solicited_node_multicast(), + &ns, + ); + + let mut buf = vec![0u8; 512]; + buf[..frame_data.len()].copy_from_slice(&frame_data); + let frame = Frame::new(0, &mut buf, frame_data.len(), false); + + let off = icmpv6_offset(); + let icmp_len = frame_data.len() - off; + handler.handle_ndp(now, frame, off, icmp_len, &mut rx, &mut tx); + + assert_eq!(rx.num_frames(), 1); + assert_eq!(tx.num_frames(), 0); + } + + #[test] + fn ns_caches_sender_mac() { + let now = Instant::now(); + let handler = new_handler(); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + assert!(handler.lookup_v6(now, &TEST_REMOTE_IPV6).is_none()); + + let remote_mac: [u8; 6] = TEST_REMOTE_MAC.into(); + let ns = build_ns_payload(TEST_LOCAL_IPV6, Some(remote_mac)); + let sol_mcast = TEST_LOCAL_IPV6.solicited_node_multicast(); + let frame_data = build_ndp_frame( + remote_mac, + sol_mcast.multicast_mac().into(), + TEST_REMOTE_IPV6, + sol_mcast, + &ns, + ); + + let mut buf = vec![0u8; 512]; + buf[..frame_data.len()].copy_from_slice(&frame_data); + let frame = Frame::new(0, &mut buf, frame_data.len(), false); + + let off = icmpv6_offset(); + let icmp_len = frame_data.len() - off; + handler.handle_ndp(now, frame, off, icmp_len, &mut rx, &mut tx); + + assert_eq!( + handler.lookup_v6(now, &TEST_REMOTE_IPV6), + Some(TEST_REMOTE_MAC) + ); + } + + #[test] + fn ns_dad_produces_na_with_correct_flags() { + let now = Instant::now(); + let handler = new_handler(); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let ns = build_ns_payload(TEST_LOCAL_IPV6, None); + let sol_mcast = TEST_LOCAL_IPV6.solicited_node_multicast(); + let all_nodes_mac: [u8; 6] = + Ipv6Address::new([0xFF, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]) + .multicast_mac() + .into(); + let frame_data = build_ndp_frame( + [0x00; 6], + sol_mcast.multicast_mac().into(), + Ipv6Address::unspecified(), + sol_mcast, + &ns, + ); + + let mut buf = vec![0u8; 512]; + buf[..frame_data.len()].copy_from_slice(&frame_data); + let frame = Frame::new(0, &mut buf, frame_data.len(), false); + + let off = icmpv6_offset(); + let icmp_len = frame_data.len() - off; + handler.handle_ndp(now, frame, off, icmp_len, &mut rx, &mut tx); + + assert_eq!(rx.num_frames(), 0); + assert_eq!(tx.num_frames(), 1); + + let reply = tx.pop().unwrap(); + + assert_eq!(reply[off + 4], 0x20); + + let eth = EthernetFrame::from_bytes(&reply); + assert_eq!(<[u8; 6]>::from(eth.dst_mac), all_nodes_mac); + + let ip = Ipv6Header::from_bytes(&reply); + assert_eq!( + ip.dst_addr, + Ipv6Address::new([0xFF, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]) + ); + } + + #[test] + fn ns_too_short_goes_to_rx() { + let now = Instant::now(); + let handler = new_handler(); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let short_ns = vec![135u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + let frame_data = build_ndp_frame( + [0x11; 6], + [0x33, 0x33, 0x00, 0x00, 0x00, 0x01], + TEST_REMOTE_IPV6, + TEST_LOCAL_IPV6.solicited_node_multicast(), + &short_ns, + ); + + let mut buf = vec![0u8; 512]; + buf[..frame_data.len()].copy_from_slice(&frame_data); + let frame = Frame::new(0, &mut buf, frame_data.len(), false); + + let off = icmpv6_offset(); + let icmp_len = frame_data.len() - off; + handler.handle_ndp(now, frame, off, icmp_len, &mut rx, &mut tx); + + assert_eq!(rx.num_frames(), 1); + assert_eq!(tx.num_frames(), 0); + } + + #[test] + fn ns_bad_checksum_goes_to_rx() { + let now = Instant::now(); + let handler = new_handler(); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let remote_mac: [u8; 6] = TEST_REMOTE_MAC.into(); + let ns = build_ns_payload(TEST_LOCAL_IPV6, Some(remote_mac)); + let sol_mcast = TEST_LOCAL_IPV6.solicited_node_multicast(); + let mut frame_data = build_ndp_frame( + remote_mac, + sol_mcast.multicast_mac().into(), + TEST_REMOTE_IPV6, + sol_mcast, + &ns, + ); + + let off = icmpv6_offset(); + frame_data[off + 2] ^= 0xFF; + + let mut buf = vec![0u8; 512]; + buf[..frame_data.len()].copy_from_slice(&frame_data); + let frame = Frame::new(0, &mut buf, frame_data.len(), false); + + let icmp_len = frame_data.len() - off; + handler.handle_ndp(now, frame, off, icmp_len, &mut rx, &mut tx); + + assert_eq!(rx.num_frames(), 1); + assert_eq!(tx.num_frames(), 0); + } + + #[test] + fn na_caches_target_mac() { + let now = Instant::now(); + let handler = new_handler(); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + assert!(handler.lookup_v6(now, &TEST_REMOTE_IPV6).is_none()); + + let remote_mac: [u8; 6] = TEST_REMOTE_MAC.into(); + let na = build_na_payload(TEST_REMOTE_IPV6, 0x60, Some(remote_mac)); + let frame_data = build_ndp_frame( + remote_mac, + TEST_LOCAL_MAC.into(), + TEST_REMOTE_IPV6, + TEST_LOCAL_IPV6, + &na, + ); + + let mut buf = vec![0u8; 512]; + buf[..frame_data.len()].copy_from_slice(&frame_data); + let frame = Frame::new(0, &mut buf, frame_data.len(), false); + + let off = icmpv6_offset(); + let icmp_len = frame_data.len() - off; + handler.handle_ndp(now, frame, off, icmp_len, &mut rx, &mut tx); + + assert_eq!(rx.num_frames(), 1); + assert_eq!(tx.num_frames(), 0); + assert_eq!( + handler.lookup_v6(now, &TEST_REMOTE_IPV6), + Some(TEST_REMOTE_MAC) + ); + } + + #[test] + fn na_too_short_goes_to_rx() { + let now = Instant::now(); + let handler = new_handler(); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let short_na = vec![136u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + let frame_data = build_ndp_frame( + [0x11; 6], + TEST_LOCAL_MAC.into(), + TEST_REMOTE_IPV6, + TEST_LOCAL_IPV6, + &short_na, + ); + + let mut buf = vec![0u8; 512]; + buf[..frame_data.len()].copy_from_slice(&frame_data); + let frame = Frame::new(0, &mut buf, frame_data.len(), false); + + let off = icmpv6_offset(); + let icmp_len = frame_data.len() - off; + handler.handle_ndp(now, frame, off, icmp_len, &mut rx, &mut tx); + + assert_eq!(rx.num_frames(), 1); + assert_eq!(tx.num_frames(), 0); + } + + #[test] + fn ra_caches_router_mac() { + let now = Instant::now(); + let handler = new_handler(); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let router_ip = Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFE]); + let router_mac: [u8; 6] = [0xAA, 0xBB, 0xCC, 0x00, 0x00, 0x01]; + + assert!(handler.lookup_v6(now, &router_ip).is_none()); + + let ra = build_ra_payload(Some(router_mac)); + let frame_data = build_ndp_frame( + router_mac, + [0x33, 0x33, 0x00, 0x00, 0x00, 0x01], + router_ip, + Ipv6Address::new([0xFF, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]), + &ra, + ); + + let mut buf = vec![0u8; 512]; + buf[..frame_data.len()].copy_from_slice(&frame_data); + let frame = Frame::new(0, &mut buf, frame_data.len(), false); + + let off = icmpv6_offset(); + let icmp_len = frame_data.len() - off; + handler.handle_ndp(now, frame, off, icmp_len, &mut rx, &mut tx); + + assert_eq!(rx.num_frames(), 1); + assert_eq!(tx.num_frames(), 0); + assert_eq!( + handler.lookup_v6(now, &router_ip), + Some(MacAddress::from(router_mac)) + ); + } + + #[test] + fn ra_without_source_lla_does_not_crash() { + let now = Instant::now(); + let handler = new_handler(); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let router_ip = Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFE]); + let ra = build_ra_payload(None); + let frame_data = build_ndp_frame( + [0xAA; 6], + [0x33, 0x33, 0x00, 0x00, 0x00, 0x01], + router_ip, + Ipv6Address::new([0xFF, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]), + &ra, + ); + + let mut buf = vec![0u8; 512]; + buf[..frame_data.len()].copy_from_slice(&frame_data); + let frame = Frame::new(0, &mut buf, frame_data.len(), false); + + let off = icmpv6_offset(); + let icmp_len = frame_data.len() - off; + handler.handle_ndp(now, frame, off, icmp_len, &mut rx, &mut tx); + + assert_eq!(rx.num_frames(), 1); + assert_eq!(tx.num_frames(), 0); + assert!(handler.lookup_v6(now, &router_ip).is_none()); + } + + #[test] + fn rs_goes_to_rx() { + let now = Instant::now(); + let handler = new_handler(); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let mut rs_payload = vec![0u8; 8]; + rs_payload[0] = 133; + let frame_data = build_ndp_frame( + [0x11; 6], + [0x33, 0x33, 0x00, 0x00, 0x00, 0x02], + TEST_REMOTE_IPV6, + Ipv6Address::new([0xFF, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]), + &rs_payload, + ); + + let mut buf = vec![0u8; 512]; + buf[..frame_data.len()].copy_from_slice(&frame_data); + let frame = Frame::new(0, &mut buf, frame_data.len(), false); + + let off = icmpv6_offset(); + let icmp_len = frame_data.len() - off; + handler.handle_ndp(now, frame, off, icmp_len, &mut rx, &mut tx); + + assert_eq!(rx.num_frames(), 1); + assert_eq!(tx.num_frames(), 0); + } + + #[test] + fn redirect_goes_to_rx() { + let now = Instant::now(); + let handler = new_handler(); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let mut redirect_payload = vec![0u8; 40]; + redirect_payload[0] = 137; + let frame_data = build_ndp_frame( + [0x11; 6], + TEST_LOCAL_MAC.into(), + TEST_REMOTE_IPV6, + TEST_LOCAL_IPV6, + &redirect_payload, + ); + + let mut buf = vec![0u8; 512]; + buf[..frame_data.len()].copy_from_slice(&frame_data); + let frame = Frame::new(0, &mut buf, frame_data.len(), false); + + let off = icmpv6_offset(); + let icmp_len = frame_data.len() - off; + handler.handle_ndp(now, frame, off, icmp_len, &mut rx, &mut tx); + + assert_eq!(rx.num_frames(), 1); + assert_eq!(tx.num_frames(), 0); + } + + #[test] + fn resolve_v6_produces_valid_ns() { + let handler = new_handler(); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let target = Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x42]); + + let mut data = [0u8; 128]; + let frame = Frame::new(0, &mut data, 1, false); + + handler.resolve_v6(TEST_LOCAL_IPV6, target, frame, &mut rx, &mut tx); + + assert_eq!(rx.num_frames(), 0); + assert_eq!(tx.num_frames(), 1); + + let req = tx.pop().unwrap(); + assert_eq!(req.len(), NDP_NS_FRAME_LEN); + + let sol_mcast = target.solicited_node_multicast(); + let expected_mac = sol_mcast.multicast_mac(); + let eth = EthernetFrame::from_bytes(&req); + assert_eq!(eth.dst_mac, expected_mac); + assert_eq!(eth.src_mac, TEST_LOCAL_MAC); + + let ip = Ipv6Header::from_bytes(&req); + assert_eq!(ip.dst_addr, sol_mcast); + assert_eq!(ip.src_addr, TEST_LOCAL_IPV6); + assert_eq!(ip.hop_limit, 255); + + let off = icmpv6_offset(); + assert_eq!(req[off], 135); + + let mut target_in_pkt = [0u8; 16]; + target_in_pkt.copy_from_slice(&req[off + 8..off + 24]); + assert_eq!(Ipv6Address::from(target_in_pkt), target); + + assert_eq!(req[off + 24], 1); + assert_eq!(req[off + 25], 1); + let local_mac_bytes: [u8; 6] = TEST_LOCAL_MAC.into(); + assert_eq!(&req[off + 26..off + 32], &local_mac_bytes); + + let cksum = compute_icmpv6_checksum(&ip.src_addr, &ip.dst_addr, &req[off..off + 32]); + assert_eq!(cksum, [0x00, 0x00]); + } + + #[test] + fn resolve_v6_frame_too_small_goes_to_rx() { + let handler = new_handler(); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let mut data = [0u8; 20]; + let frame = Frame::new(0, &mut data, 1, false); + + handler.resolve_v6(TEST_LOCAL_IPV6, TEST_REMOTE_IPV6, frame, &mut rx, &mut tx); + + assert_eq!(rx.num_frames(), 1); + assert_eq!(tx.num_frames(), 0); + } +} diff --git a/src/net/pmtu.rs b/src/net/pmtu.rs new file mode 100644 index 0000000..3338766 --- /dev/null +++ b/src/net/pmtu.rs @@ -0,0 +1,201 @@ +use coarsetime::{Duration, Instant}; +use dashmap::DashMap; + +use super::wire::ip::IpAddress; + +/// Minimum MTU for IPv4 per RFC 791. +pub const IPV4_MIN_MTU: u32 = 68; + +/// Minimum MTU for IPv6 per RFC 2460. +pub const IPV6_MIN_MTU: u32 = 1280; + +/// Default TTL for PMTU cache entries (10 minutes per RFC 1191). +const DEFAULT_PMTU_TTL: Duration = Duration::from_secs(600); + +/// A cache of discovered Path MTU values keyed by destination IP address. +/// +/// When the network stack receives an ICMP "Fragmentation Needed" (IPv4) +/// or "Packet Too Big" (IPv6) message, the reported next-hop MTU is +/// stored here so that upper layers can size outgoing packets accordingly. +/// +/// Entries expire after a configurable TTL (default 10 minutes per +/// RFC 1191). Expired entries are treated as absent (the default MTU is +/// returned) and can be removed in bulk via [`evict_stale`](Self::evict_stale). +#[derive(Debug)] +pub struct PmtuCache { + /// PMTU table mappings for destination IP addresses. + table: DashMap, + /// The default MTU to use when no PMTU is found for a destination. + default_mtu: u32, + /// The TTL for PMTU entries. + /// Expired entries are treated as absent (the default MTU is + /// returned) and can be removed in bulk via [`evict_stale`](Self::evict_stale). + ttl: Duration, +} + +impl PmtuCache { + /// Creates a new PMTU cache with the default MTU and TTL. + pub fn new() -> Self { + Self { + table: DashMap::new(), + default_mtu: 1500, + ttl: DEFAULT_PMTU_TTL, + } + } + + /// Creates a new PMTU cache with the given MTU and default TTL. + pub fn with_mtu(mtu: u32) -> Self { + Self { + table: DashMap::new(), + default_mtu: mtu, + ttl: DEFAULT_PMTU_TTL, + } + } + + /// Creates a new PMTU cache with the given MTU and TTL. + pub fn with_mtu_and_ttl(mtu: u32, ttl: Duration) -> Self { + Self { + table: DashMap::new(), + default_mtu: mtu, + ttl, + } + } + + /// Records a discovered path MTU for `addr`. + /// + /// The value is clamped to the protocol minimum (68 for IPv4, + /// 1280 for IPv6) before storing. + pub fn update(&self, now: Instant, addr: IpAddress, mtu: u32) { + let min = match addr { + IpAddress::V4(_) => IPV4_MIN_MTU, + IpAddress::V6(_) => IPV6_MIN_MTU, + }; + self.table.insert(addr, (mtu.max(min), now)); + } + + /// Returns the cached path MTU for `addr`, or the default MTU if + /// the entry is absent or expired. + pub fn get(&self, now: Instant, addr: &IpAddress) -> u32 { + self.table + .get(addr) + .and_then(|entry| { + let (mtu, inserted_at) = *entry.value(); + if now.duration_since(inserted_at) <= self.ttl { + Some(mtu) + } else { + None + } + }) + .unwrap_or(self.default_mtu) + } + + /// Removes all entries older than the configured TTL. + pub fn evict_stale(&self, now: Instant) { + self.table + .retain(|_, (_, inserted_at)| now.duration_since(*inserted_at) <= self.ttl); + } +} + +impl Default for PmtuCache { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use std::ops::Add; + + use crate::net::wire::ip::{Ipv4Address, Ipv6Address}; + + use super::*; + + #[test] + fn insert_and_get_ipv4() { + let now = Instant::now(); + let cache = PmtuCache::new(); + let addr = IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])); + cache.update(now, addr, 1500); + assert_eq!(cache.get(now, &addr), 1500); + } + + #[test] + fn insert_and_get_ipv6() { + let now = Instant::now(); + let cache = PmtuCache::new(); + let addr = IpAddress::V6(Ipv6Address::new([ + 0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + ])); + cache.update(now, addr, 1400); + assert_eq!(cache.get(now, &addr), 1400); + } + + #[test] + fn clamp_ipv4_to_minimum() { + let now = Instant::now(); + let cache = PmtuCache::new(); + let addr = IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])); + cache.update(now, addr, 20); + assert_eq!(cache.get(now, &addr), IPV4_MIN_MTU); + } + + #[test] + fn clamp_ipv6_to_minimum() { + let now = Instant::now(); + let cache = PmtuCache::new(); + let addr = IpAddress::V6(Ipv6Address::new([ + 0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + ])); + cache.update(now, addr, 500); + assert_eq!(cache.get(now, &addr), IPV6_MIN_MTU); + } + + #[test] + fn overwrite_with_smaller_mtu() { + let now = Instant::now(); + let cache = PmtuCache::new(); + let addr = IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])); + cache.update(now, addr, 1500); + assert_eq!(cache.get(now, &addr), 1500); + cache.update(now, addr, 1200); + assert_eq!(cache.get(now, &addr), 1200); + } + + #[test] + fn get_unknown_returns_default() { + let now = Instant::now(); + let cache = PmtuCache::new(); + let addr = IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])); + assert_eq!(cache.get(now, &addr), 1500); + } + + #[test] + fn expired_entry_returns_default() { + let now = Instant::now(); + let cache = PmtuCache::with_mtu_and_ttl(1500, Duration::from_ticks(0)); + let addr = IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])); + cache.update(now, addr, 1200); + assert_eq!(cache.get(now.add(Duration::from_millis(5)), &addr), 1500); + } + + #[test] + fn evict_stale_removes_expired() { + let now = Instant::now(); + let cache = PmtuCache::with_mtu_and_ttl(1500, Duration::from_ticks(0)); + let addr = IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])); + cache.update(now, addr, 1200); + cache.evict_stale(now.add(Duration::from_millis(5))); + assert_eq!(cache.table.len(), 0); + } + + #[test] + fn evict_stale_keeps_fresh() { + let now = Instant::now(); + let cache = PmtuCache::with_mtu_and_ttl(1500, Duration::from_secs(3600)); + let addr = IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])); + cache.update(now, addr, 1200); + cache.evict_stale(now.add(Duration::from_secs(3600))); + assert_eq!(cache.table.len(), 1); + assert_eq!(cache.get(now, &addr), 1200); + } +} diff --git a/src/net/socket/mod.rs b/src/net/socket/mod.rs new file mode 100644 index 0000000..6e6936c --- /dev/null +++ b/src/net/socket/mod.rs @@ -0,0 +1,7 @@ +mod queue; +mod tcp; +mod udp; + +pub use queue::{LocalQueue, SharedQueue}; +pub use tcp::{Accept, Connect, TcpConfig, TcpListener, TcpRead, TcpSplice, TcpStream, TcpWrite}; +pub use udp::{Echo, RecvFrom, RecvHalf, RecvStream, SendHalf, SendTo, UdpSocket}; diff --git a/src/net/socket/queue.rs b/src/net/socket/queue.rs new file mode 100644 index 0000000..de161b6 --- /dev/null +++ b/src/net/socket/queue.rs @@ -0,0 +1,312 @@ +use std::{ + cell::UnsafeCell, + collections::{VecDeque, vec_deque::Drain}, + fmt, + ops::RangeBounds, + rc::Rc, + sync::Arc, +}; + +use crossbeam_queue::ArrayQueue; + +/// A fixed-capacity, lock-free shared queue. +/// +/// Cloning shares the same underlying buffer (via `Arc`), allowing +/// a producer and consumer to communicate without mutexes or per-item +/// allocations. When the buffer is full, [`push`](Self::push) evicts +/// the oldest entry and returns it to the caller. +pub struct SharedQueue { + inner: Arc>, +} + +impl SharedQueue { + /// Create a new shared queue with the given capacity. + pub fn new(capacity: usize) -> Self { + Self { + inner: Arc::new(ArrayQueue::new(capacity)), + } + } + + /// Push an item. Returns the evicted oldest item if the queue was full. + pub fn push(&self, item: T) -> Option { + self.inner.force_push(item) + } + + /// Pop an item. Returns the oldest item if the queue was not empty. + pub fn pop(&self) -> Option { + self.inner.pop() + } + + /// Returns the number of items in the queue. + pub fn len(&self) -> usize { + self.inner.len() + } + + /// Returns true if the queue is empty. + pub fn is_empty(&self) -> bool { + self.inner.is_empty() + } + + /// Returns the capacity of the queue. + pub fn capacity(&self) -> usize { + self.inner.capacity() + } +} + +impl Clone for SharedQueue { + fn clone(&self) -> Self { + Self { + inner: Arc::clone(&self.inner), + } + } +} + +impl fmt::Debug for SharedQueue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("SharedQueue") + .field("len", &self.inner.len()) + .field("capacity", &self.inner.capacity()) + .finish() + } +} + +/// A fixed-capacity, single-threaded shared queue. +/// +/// Cloning shares the same underlying buffer (via `Rc`), allowing +/// a producer and consumer to communicate without atomic operations. +/// When the buffer is full, [`push`](Self::push) evicts the oldest +/// entry and returns it to the caller — matching `SharedQueue` semantics. +pub struct LocalQueue { + inner: Rc>>, + capacity: usize, +} + +impl LocalQueue { + /// Create a new local queue with the given capacity. + pub fn new(capacity: usize) -> Self { + Self { + inner: Rc::new(UnsafeCell::new(VecDeque::with_capacity(capacity))), + capacity, + } + } + + /// Push an item. Returns the evicted oldest item if the queue was full. + #[inline(always)] + pub fn push(&self, item: T) -> Option { + let q = unsafe { &mut *self.inner.get() }; + let evicted = if q.len() >= self.capacity { + q.pop_front() + } else { + None + }; + q.push_back(item); + evicted + } + + /// Pop an item. Returns the oldest item if the queue was not empty. + #[inline(always)] + pub fn pop(&self) -> Option { + unsafe { &mut *self.inner.get() }.pop_front() + } + + /// Drain a range of items. Returns an iterator over the drained items. + #[inline(always)] + pub fn drain>(&self, range: R) -> Drain<'_, T> { + unsafe { &mut *self.inner.get() }.drain(range) + } + + /// Returns the number of items in the queue. + #[inline(always)] + pub fn len(&self) -> usize { + unsafe { &*self.inner.get() }.len() + } + + /// Returns true if the queue is empty. + #[inline(always)] + pub fn is_empty(&self) -> bool { + unsafe { &*self.inner.get() }.is_empty() + } + + /// Returns the capacity of the queue. + #[inline(always)] + pub fn capacity(&self) -> usize { + self.capacity + } +} + +impl Clone for LocalQueue { + fn clone(&self) -> Self { + Self { + inner: Rc::clone(&self.inner), + capacity: self.capacity, + } + } +} + +impl fmt::Debug for LocalQueue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("LocalQueue") + .field("len", &self.len()) + .field("capacity", &self.capacity) + .finish() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn push_and_pop() { + let q = SharedQueue::new(8); + assert!(q.is_empty()); + assert_eq!(q.len(), 0); + assert_eq!(q.capacity(), 8); + + assert!(q.push(1).is_none()); + assert!(q.push(2).is_none()); + assert_eq!(q.len(), 2); + assert!(!q.is_empty()); + + assert_eq!(q.pop(), Some(1)); + assert_eq!(q.pop(), Some(2)); + assert_eq!(q.pop(), None); + assert!(q.is_empty()); + } + + #[test] + fn clone_shares_state() { + let q1 = SharedQueue::new(8); + let q2 = q1.clone(); + + q1.push(42); + assert_eq!(q2.pop(), Some(42)); + + q2.push(99); + assert_eq!(q1.pop(), Some(99)); + } + + #[test] + fn overwrites_oldest_when_full() { + let q = SharedQueue::new(3); + + assert!(q.push(1).is_none()); + assert!(q.push(2).is_none()); + assert!(q.push(3).is_none()); + assert_eq!(q.len(), 3); + + // Queue is full — pushing evicts oldest (1). + assert_eq!(q.push(4), Some(1)); + assert_eq!(q.len(), 3); + + // Evicts 2. + assert_eq!(q.push(5), Some(2)); + assert_eq!(q.len(), 3); + + // Drain: should get 3, 4, 5. + assert_eq!(q.pop(), Some(3)); + assert_eq!(q.pop(), Some(4)); + assert_eq!(q.pop(), Some(5)); + assert_eq!(q.pop(), None); + } + + #[test] + fn wraparound_correctness() { + let q = SharedQueue::new(3); + + // Fill and drain to advance head. + q.push(1); + q.push(2); + assert_eq!(q.pop(), Some(1)); + assert_eq!(q.pop(), Some(2)); + + // Now push 3 items to fill, then overwrite. + q.push(10); + q.push(11); + q.push(12); + assert_eq!(q.len(), 3); + + assert_eq!(q.push(13), Some(10)); + assert_eq!(q.pop(), Some(11)); + assert_eq!(q.pop(), Some(12)); + assert_eq!(q.pop(), Some(13)); + assert!(q.is_empty()); + } + + #[test] + fn local_push_and_pop() { + let q = LocalQueue::new(8); + assert!(q.is_empty()); + assert_eq!(q.len(), 0); + assert_eq!(q.capacity(), 8); + + assert!(q.push(1).is_none()); + assert!(q.push(2).is_none()); + assert_eq!(q.len(), 2); + assert!(!q.is_empty()); + + assert_eq!(q.pop(), Some(1)); + assert_eq!(q.pop(), Some(2)); + assert_eq!(q.pop(), None); + assert!(q.is_empty()); + } + + #[test] + fn local_clone_shares_state() { + let q1 = LocalQueue::new(8); + let q2 = q1.clone(); + + q1.push(42); + assert_eq!(q2.pop(), Some(42)); + + q2.push(99); + assert_eq!(q1.pop(), Some(99)); + } + + #[test] + fn local_overwrites_oldest_when_full() { + let q = LocalQueue::new(3); + + assert!(q.push(1).is_none()); + assert!(q.push(2).is_none()); + assert!(q.push(3).is_none()); + assert_eq!(q.len(), 3); + + // Queue is full — pushing evicts oldest (1). + assert_eq!(q.push(4), Some(1)); + assert_eq!(q.len(), 3); + + // Evicts 2. + assert_eq!(q.push(5), Some(2)); + assert_eq!(q.len(), 3); + + // Drain: should get 3, 4, 5. + assert_eq!(q.pop(), Some(3)); + assert_eq!(q.pop(), Some(4)); + assert_eq!(q.pop(), Some(5)); + assert_eq!(q.pop(), None); + } + + #[test] + fn local_wraparound_correctness() { + let q = LocalQueue::new(3); + + // Fill and drain to advance head. + q.push(1); + q.push(2); + assert_eq!(q.pop(), Some(1)); + assert_eq!(q.pop(), Some(2)); + + // Now push 3 items to fill, then overwrite. + q.push(10); + q.push(11); + q.push(12); + assert_eq!(q.len(), 3); + + assert_eq!(q.push(13), Some(10)); + assert_eq!(q.pop(), Some(11)); + assert_eq!(q.pop(), Some(12)); + assert_eq!(q.pop(), Some(13)); + assert!(q.is_empty()); + } +} diff --git a/src/net/socket/tcp.rs b/src/net/socket/tcp.rs new file mode 100644 index 0000000..013b6d2 --- /dev/null +++ b/src/net/socket/tcp.rs @@ -0,0 +1,622 @@ +use std::{ + cell::{Cell, UnsafeCell}, + pin::Pin, + rc::Rc, + task::{Context, Poll}, +}; + +use coarsetime::Instant; + +use crate::{ + net::{ + handler::{ + tcp::TcpHandler, + udp::BindError, + }, + wire::{ + ethernet::MacAddress, + ip::IpAddress, + }, + }, + rt::context::with_runtime_context, +}; + +use super::LocalQueue; + +pub use crate::net::handler::tcp::tcb::TcpConfig; +use crate::net::handler::tcp::tcb::{ConnectionId, TcpError, TcpEvent}; + +const DEFAULT_BACKLOG: usize = 128; + +/// A listening TCP socket that accepts incoming connections. +/// +/// Created via `TcpListener::listen()` inside a `LocalRuntime::run()` closure. +/// Use `accept()` to wait for incoming connections, which returns a `TcpStream`. +pub struct TcpListener { + local_addr: IpAddress, + local_port: u16, + accept_queue: LocalQueue, + handler: Rc>, +} + +impl TcpListener { + /// Create a new listening TCP socket bound to the given address and port. + /// + /// Must be called inside a `LocalRuntime::run()` closure. Panics otherwise. + pub fn listen(addr: IpAddress, port: u16) -> Result { + Self::listen_with_backlog(addr, port, DEFAULT_BACKLOG) + } + + /// Create a new listening TCP socket with a custom backlog size. + pub fn listen_with_backlog( + addr: IpAddress, + port: u16, + backlog: usize, + ) -> Result { + with_runtime_context(|ctx| { + let handler = unsafe { &mut *ctx.tcp_handler.get() }; + let accept_queue = handler.listen(addr, port, backlog)?; + Ok(Self { + local_addr: addr, + local_port: port, + accept_queue, + handler: ctx.tcp_handler.clone(), + }) + }) + } + + /// Create a new listening TCP socket with custom configuration. + /// + /// The config controls backlog size and per-connection buffer sizes. + pub fn listen_with_config( + addr: IpAddress, + port: u16, + config: TcpConfig, + ) -> Result { + with_runtime_context(|ctx| { + let handler = unsafe { &mut *ctx.tcp_handler.get() }; + let accept_queue = handler.listen_with_config(addr, port, config)?; + Ok(Self { + local_addr: addr, + local_port: port, + accept_queue, + handler: ctx.tcp_handler.clone(), + }) + }) + } + + /// Returns a future that resolves when a new connection is available. + #[inline(always)] + pub fn accept(&self) -> Accept<'_> { + Accept { + accept_queue: &self.accept_queue, + handler: &self.handler, + } + } + + /// Returns the local address this listener is bound to. + pub fn local_addr(&self) -> IpAddress { + self.local_addr + } + + /// Returns the local port this listener is bound to. + pub fn local_port(&self) -> u16 { + self.local_port + } + + /// Close this listener, removing it from the handler. + pub fn close(&mut self) { + let handler = unsafe { &mut *self.handler.get() }; + handler.unlisten(self.local_addr, self.local_port); + } +} + +impl Drop for TcpListener { + fn drop(&mut self) { + self.close(); + } +} + +/// Future returned by [`TcpListener::accept()`]. +/// +/// Resolves to a [`TcpStream`] when a new connection completes the 3-way handshake. +pub struct Accept<'listener> { + accept_queue: &'listener LocalQueue, + handler: &'listener Rc>, +} + +impl<'listener> Future for Accept<'listener> { + type Output = TcpStream; + + fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll { + let this = self.get_mut(); + match this.accept_queue.pop() { + Some(conn_id) => { + let handler = unsafe { &*this.handler.get() }; + let event_queue = handler + .get_connection(&conn_id) + .map(|tcb| tcb.event_queue.clone()); + + if let Some(event_queue) = event_queue { + Poll::Ready(TcpStream::from_accepted( + conn_id, + event_queue, + this.handler.clone(), + )) + } else { + // Connection was removed (e.g. by RST) before we accepted it. + // Keep polling for the next one. + Poll::Pending + } + } + None => Poll::Pending, + } + } +} + +/// A connected TCP stream. +/// +/// Created either by [`TcpListener::accept()`] (passive open) or +/// [`TcpStream::connect()`] (active open). +pub struct TcpStream { + conn_id: ConnectionId, + #[allow(dead_code)] // used in future data transfer phases + event_queue: LocalQueue, + handler: Rc>, + cached_idx: Cell, + closed: bool, + write_closed: bool, +} + +impl TcpStream { + /// Initiate an active open (connect) to a remote address. + /// + /// Returns a `Connect` future that resolves when the 3-way handshake + /// completes or fails. + /// + /// Must be called inside a `LocalRuntime::run()` closure. Panics otherwise. + pub fn connect( + local_addr: IpAddress, + local_port: u16, + remote_addr: IpAddress, + remote_port: u16, + ) -> Result { + with_runtime_context(|ctx| { + let handler = unsafe { &mut *ctx.tcp_handler.get() }; + let neighbor_handler = &*ctx.neighbor_handler; + let now = Instant::now(); + + // Resolve MACs for the outbound SYN. + let src_mac = neighbor_handler.local_mac(); + let dst_mac = neighbor_handler + .lookup(now, &remote_addr) + .unwrap_or(MacAddress::broadcast()); + + let mut free_frames = ctx.free_frames.clone(); + let mut tx_return = ctx.tx_return.clone(); + + let event_queue = handler + .connect( + local_addr, + local_port, + remote_addr, + remote_port, + src_mac, + dst_mac, + now, + &mut free_frames, + &mut tx_return, + ) + .map_err(|_| TcpError::NotConnected)?; + + let conn_id = ConnectionId { + local_addr, + local_port, + remote_addr, + remote_port, + }; + + Ok(Connect { + conn_id, + event_queue, + handler: ctx.tcp_handler.clone(), + }) + }) + } + + /// Initiate an active open (connect) with custom buffer configuration. + /// + /// Returns a `Connect` future that resolves when the 3-way handshake + /// completes or fails. + /// + /// Must be called inside a `LocalRuntime::run()` closure. Panics otherwise. + pub fn connect_with_config( + local_addr: IpAddress, + local_port: u16, + remote_addr: IpAddress, + remote_port: u16, + config: TcpConfig, + ) -> Result { + with_runtime_context(|ctx| { + let handler = unsafe { &mut *ctx.tcp_handler.get() }; + let neighbor_handler = &*ctx.neighbor_handler; + let now = Instant::now(); + + // Resolve MACs for the outbound SYN. + let src_mac = neighbor_handler.local_mac(); + let dst_mac = neighbor_handler + .lookup(now, &remote_addr) + .unwrap_or(MacAddress::broadcast()); + + let mut free_frames = ctx.free_frames.clone(); + let mut tx_return = ctx.tx_return.clone(); + + let event_queue = handler + .connect_with_config( + local_addr, + local_port, + remote_addr, + remote_port, + src_mac, + dst_mac, + now, + config, + &mut free_frames, + &mut tx_return, + ) + .map_err(|_| TcpError::NotConnected)?; + + let conn_id = ConnectionId { + local_addr, + local_port, + remote_addr, + remote_port, + }; + + Ok(Connect { + conn_id, + event_queue, + handler: ctx.tcp_handler.clone(), + }) + }) + } + + /// Internal constructor for connections created via accept. + fn from_accepted( + conn_id: ConnectionId, + event_queue: LocalQueue, + handler: Rc>, + ) -> Self { + let h = unsafe { &*handler.get() }; + let idx = h.find_connection_idx(&conn_id).unwrap_or(0); + Self { + conn_id, + event_queue, + handler, + cached_idx: Cell::new(idx), + closed: false, + write_closed: false, + } + } + + /// Returns the connection's 4-tuple identifier. + pub fn conn_id(&self) -> &ConnectionId { + &self.conn_id + } + + /// Returns the local address of this connection. + pub fn local_addr(&self) -> IpAddress { + self.conn_id.local_addr + } + + /// Returns the local port of this connection. + pub fn local_port(&self) -> u16 { + self.conn_id.local_port + } + + /// Returns the remote address of this connection. + pub fn remote_addr(&self) -> IpAddress { + self.conn_id.remote_addr + } + + /// Returns the remote port of this connection. + pub fn remote_port(&self) -> u16 { + self.conn_id.remote_port + } + + /// Write data to this connection. Returns a future that resolves when + /// all bytes are copied into the send buffer. + pub fn write<'a>(&'a self, data: &'a [u8]) -> TcpWrite<'a> { + TcpWrite { + handler: &self.handler, + conn_id: self.conn_id, + event_queue: &self.event_queue, + cached_idx: &self.cached_idx, + data, + written: 0, + write_closed: self.write_closed || self.closed, + } + } + + /// Transfer data from recv_buffer directly to send_buffer, avoiding the + /// intermediate user buffer copy. Returns a future that resolves when at + /// least 1 byte has been transferred, or 0 on EOF. + pub fn splice(&self, max_len: usize) -> TcpSplice<'_> { + TcpSplice { + handler: &self.handler, + conn_id: self.conn_id, + event_queue: &self.event_queue, + cached_idx: &self.cached_idx, + max_len, + } + } + + /// Read data from this connection. Returns a future that resolves when + /// data is available in the receive buffer. + pub fn read<'a>(&'a self, buf: &'a mut [u8]) -> TcpRead<'a> { + TcpRead { + handler: &self.handler, + conn_id: self.conn_id, + event_queue: &self.event_queue, + cached_idx: &self.cached_idx, + buf, + } + } + + /// Initiate graceful close of this connection. + /// + /// Sets a flag on the TCB; the runtime's `poll_send` will drain + /// any remaining send buffer data and then send FIN on the next tick. + pub fn close(&mut self) { + if self.closed { + return; + } + self.closed = true; + let handler = unsafe { &mut *self.handler.get() }; + handler.initiate_close(&self.conn_id); + } + + /// Shut down the write side of this connection (half-close). + /// + /// Sends FIN to the remote peer but keeps the read side open. + /// Subsequent writes will return 0. Reads continue until remote FIN. + pub fn shutdown(&mut self) { + if self.write_closed || self.closed { + return; + } + self.write_closed = true; + let handler = unsafe { &mut *self.handler.get() }; + handler.initiate_close(&self.conn_id); + } + + /// Enable or disable the Nagle algorithm (TCP_NODELAY). + /// + /// When `nodelay` is `true`, small segments are sent immediately + /// without waiting for outstanding ACKs. Default is `false` (Nagle enabled). + pub fn set_nodelay(&self, nodelay: bool) { + let handler = unsafe { &mut *self.handler.get() }; + if let Some(tcb) = handler.get_connection_mut(&self.conn_id) { + tcb.nagle_enabled = !nodelay; + } + } + + /// Returns whether TCP_NODELAY is set (Nagle disabled). + pub fn nodelay(&self) -> bool { + let handler = unsafe { &*self.handler.get() }; + handler + .get_connection(&self.conn_id) + .map(|tcb| !tcb.nagle_enabled) + .unwrap_or(false) + } + + /// Enable or disable TCP keep-alive probes. + pub fn set_keepalive(&self, enabled: bool) { + let handler = unsafe { &mut *self.handler.get() }; + if let Some(tcb) = handler.get_connection_mut(&self.conn_id) { + tcb.keep_alive_enabled = enabled; + } + } + + /// Returns whether TCP keep-alive is enabled. + pub fn keepalive(&self) -> bool { + let handler = unsafe { &*self.handler.get() }; + handler + .get_connection(&self.conn_id) + .map(|tcb| tcb.keep_alive_enabled) + .unwrap_or(false) + } + + /// Set the SO_LINGER option. + /// + /// - `None`: default graceful close + /// - `Some(0)`: hard RST on close + /// - `Some(ms)`: graceful close with timeout in milliseconds + pub fn set_linger(&self, linger: Option) { + let handler = unsafe { &mut *self.handler.get() }; + if let Some(tcb) = handler.get_connection_mut(&self.conn_id) { + tcb.linger = linger; + } + } + + /// Returns the current SO_LINGER setting. + pub fn linger(&self) -> Option { + let handler = unsafe { &*self.handler.get() }; + handler + .get_connection(&self.conn_id) + .and_then(|tcb| tcb.linger) + } +} + +impl Drop for TcpStream { + fn drop(&mut self) { + self.close(); + } +} + +/// Future returned by [`TcpStream::connect()`]. +/// +/// Resolves to a [`TcpStream`] when the handshake completes, or returns +/// an error if the connection is refused or times out. +pub struct Connect { + conn_id: ConnectionId, + event_queue: LocalQueue, + handler: Rc>, +} + +impl Future for Connect { + type Output = Result; + + fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll { + let this = self.get_mut(); + match this.event_queue.pop() { + Some(TcpEvent::Connected) => { + let handler = unsafe { &*this.handler.get() }; + let idx = handler.find_connection_idx(&this.conn_id).unwrap_or(0); + Poll::Ready(Ok(TcpStream { + conn_id: this.conn_id, + event_queue: this.event_queue.clone(), + handler: this.handler.clone(), + cached_idx: Cell::new(idx), + closed: false, + write_closed: false, + })) + } + Some(TcpEvent::ConnectionRefused) => Poll::Ready(Err(TcpError::ConnectionRefused)), + Some(TcpEvent::Timeout) => Poll::Ready(Err(TcpError::Timeout)), + Some(TcpEvent::Reset) => Poll::Ready(Err(TcpError::Reset)), + Some(TcpEvent::RemoteClose) => Poll::Ready(Err(TcpError::Reset)), + None => Poll::Pending, + } + } +} + +/// Future returned by [`TcpStream::write()`]. +pub struct TcpWrite<'stream> { + handler: &'stream Rc>, + conn_id: ConnectionId, + event_queue: &'stream LocalQueue, + cached_idx: &'stream Cell, + data: &'stream [u8], + written: usize, + write_closed: bool, +} + +impl<'stream> Future for TcpWrite<'stream> { + type Output = Result; + + fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll { + let this = self.get_mut(); + + // Check event queue for errors + while let Some(event) = this.event_queue.pop() { + match event { + TcpEvent::Reset => return Poll::Ready(Err(TcpError::Reset)), + TcpEvent::Timeout => return Poll::Ready(Err(TcpError::Timeout)), + _ => {} + } + } + + if this.write_closed { + return Poll::Ready(Err(TcpError::NotConnected)); + } + let handler = unsafe { &mut *this.handler.get() }; + let idx = this.cached_idx.get(); + if let Some((new_idx, tcb)) = handler.get_connection_by_idx_mut(idx, &this.conn_id) { + this.cached_idx.set(new_idx); + let remaining = &this.data[this.written..]; + let n = tcb.send_buffer.write(remaining); + this.written += n; + if this.written == this.data.len() { + Poll::Ready(Ok(this.written)) + } else { + Poll::Pending + } + } else { + Poll::Ready(Err(TcpError::NotConnected)) + } + } +} + +/// Future returned by [`TcpStream::read()`]. +pub struct TcpRead<'stream> { + handler: &'stream Rc>, + conn_id: ConnectionId, + event_queue: &'stream LocalQueue, + cached_idx: &'stream Cell, + buf: &'stream mut [u8], +} + +impl<'stream> Future for TcpRead<'stream> { + type Output = Result; + + fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll { + let this = self.get_mut(); + + // Check event queue for errors + while let Some(event) = this.event_queue.pop() { + match event { + TcpEvent::Reset => return Poll::Ready(Err(TcpError::Reset)), + TcpEvent::Timeout => return Poll::Ready(Err(TcpError::Timeout)), + _ => {} + } + } + + let handler = unsafe { &mut *this.handler.get() }; + let idx = this.cached_idx.get(); + if let Some((new_idx, tcb)) = handler.get_connection_by_idx_mut(idx, &this.conn_id) { + this.cached_idx.set(new_idx); + let n = tcb.recv_buffer.read(this.buf); + if n > 0 { + Poll::Ready(Ok(n)) + } else if tcb.state.is_remote_closed() { + Poll::Ready(Ok(0)) // EOF — remote has sent FIN and buffer is drained + } else { + Poll::Pending + } + } else { + Poll::Ready(Err(TcpError::NotConnected)) + } + } +} + +/// Future returned by [`TcpStream::splice()`]. +pub struct TcpSplice<'stream> { + handler: &'stream Rc>, + conn_id: ConnectionId, + event_queue: &'stream LocalQueue, + cached_idx: &'stream Cell, + max_len: usize, +} + +impl<'stream> Future for TcpSplice<'stream> { + type Output = Result; + + fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll { + let this = self.get_mut(); + + // Check event queue for errors. + while let Some(event) = this.event_queue.pop() { + match event { + TcpEvent::Reset => return Poll::Ready(Err(TcpError::Reset)), + TcpEvent::Timeout => return Poll::Ready(Err(TcpError::Timeout)), + _ => {} + } + } + + let handler = unsafe { &mut *this.handler.get() }; + let idx = this.cached_idx.get(); + if let Some((new_idx, tcb)) = handler.get_connection_by_idx_mut(idx, &this.conn_id) { + this.cached_idx.set(new_idx); + let n = tcb.recv_buffer.transfer(&mut tcb.send_buffer, this.max_len); + if n > 0 { + Poll::Ready(Ok(n)) + } else if tcb.state.is_remote_closed() { + Poll::Ready(Ok(0)) // EOF + } else { + Poll::Pending + } + } else { + Poll::Ready(Err(TcpError::NotConnected)) + } + } +} diff --git a/src/net/socket/udp.rs b/src/net/socket/udp.rs new file mode 100644 index 0000000..fbe134d --- /dev/null +++ b/src/net/socket/udp.rs @@ -0,0 +1,562 @@ +use std::{ + cell::UnsafeCell, + pin::Pin, + rc::Rc, + task::{Context, Poll}, +}; + +use coarsetime::Instant; + +use crate::{ + net::{ + NeighborHandler, PmtuCache, + checksum::{compute_udp_checksum_from_parts, compute_udp_checksum_v6_from_parts}, + fragment::{FragmentWriter, Packet}, + handler::udp::{BindError, ReceivedUdpPacket, UdpHandler}, + wire::{ + ethernet::MacAddress, + ip::{IpAddress, Ipv4Address, Ipv6Address}, + udp::{UDP_HEADER_LEN, UdpHeader}, + }, + }, + rt::context::with_runtime_context, + xdp::{ + error::WouldBlock, + frame::{FrameBuffer, SharedFrameBuffer}, + }, +}; + +use super::LocalQueue; + +const DEFAULT_MTU: u32 = 1500; +const DEFAULT_SOCKET_RX_CAPACITY: usize = 256; + +/// A user-facing UDP socket handle. +/// +/// Created via `UdpSocket::new()` inside a `LocalRuntime::run()` closure, +/// then bound to an address and port via `bind()`. +#[derive(Debug)] +pub struct UdpSocket<'umem> { + local_addr: IpAddress, + local_port: u16, + rx_queue: LocalQueue>, + free_frames: SharedFrameBuffer<'umem>, + tx_return: SharedFrameBuffer<'umem>, + rx_return: SharedFrameBuffer<'umem>, + pmtu: Rc, + neighbor_handler: Rc, + handler: Rc>>, + tx_offload: bool, +} + +impl<'umem> UdpSocket<'umem> { + /// Create a new bound UDP socket. + /// + /// Must be called inside a `LocalRuntime::run()` closure. Panics otherwise. + pub fn new(addr: IpAddress, port: u16) -> Result { + with_runtime_context(|ctx| { + let handler = unsafe { &mut *ctx.udp_handler.get() }; + let rx_queue = handler.bind(addr, port, DEFAULT_SOCKET_RX_CAPACITY)?; + Ok(Self { + local_addr: addr, + local_port: port, + rx_queue, + free_frames: ctx.free_frames.clone(), + tx_return: ctx.tx_return.clone(), + rx_return: ctx.rx_return.clone(), + pmtu: ctx.pmtu.clone(), + neighbor_handler: ctx.neighbor_handler.clone(), + handler: ctx.udp_handler.clone(), + tx_offload: ctx.tx_offload, + }) + }) + } + + /// Close this socket, unbinding from the address/port and draining queued packets. + pub fn close(&mut self) { + // SAFETY: single-threaded, no reentrant handler calls. + let handler = unsafe { &mut *self.handler.get() }; + handler.unbind(self.local_addr, self.local_port); + // Drain any queued packets back to rx_return. + while let Some(pkt) = self.rx_queue.pop() { + pkt.packet.drain_to(&mut self.rx_return); + } + } + + /// Returns the local address of this socket. + pub fn local_addr(&self) -> IpAddress { + self.local_addr + } + + /// Returns the local port of this socket. + pub fn local_port(&self) -> u16 { + self.local_port + } + + /// Receive a packet. Returns a future that resolves when a packet is available. + #[inline(always)] + pub fn recv_from(&self) -> RecvFrom<'_, 'umem> { + RecvFrom { + rx_queue: &self.rx_queue, + } + } + + /// Send a payload to a destination address and port. + #[inline(always)] + pub fn send_to<'buf>( + &mut self, + dst_addr: IpAddress, + dst_port: u16, + payload: &'buf [u8], + ) -> SendTo<'_, 'buf, 'umem> { + SendTo { + free_frames: &mut self.free_frames, + rx_return: &mut self.rx_return, + tx_return: &mut self.tx_return, + pmtu: &self.pmtu, + neighbor_handler: &self.neighbor_handler, + pkt: Packet::Empty, + src_addr: self.local_addr, + src_port: self.local_port, + dst_addr, + dst_port, + payload, + tx_offload: self.tx_offload, + } + } + + /// Echo a received packet back with backpressure (async). + /// + /// The caller should have already called `swap_addresses()` on the packet. + #[inline(always)] + pub fn echo(&mut self, packet: ReceivedUdpPacket<'umem>) -> Echo<'_, 'umem> { + let payload_len = packet.packet.len() as u32; + Echo { + tx_return: &mut self.tx_return, + pkt: packet.packet, + payload_len, + } + } + + /// Echo a received packet back immediately without backpressure (sync). + /// + /// The caller should have already called `swap_addresses()` on the packet. + #[inline(always)] + pub fn echo_immediate(&mut self, packet: ReceivedUdpPacket<'umem>) -> u32 { + let payload_len = packet.packet.len() as u32; + packet.packet.drain_to(&mut self.tx_return); + payload_len + } + + /// Discard a received packet, returning its frames to the kernel. + #[inline(always)] + pub fn discard(&mut self, packet: ReceivedUdpPacket<'umem>) { + packet.packet.drain_to(&mut self.rx_return); + } + + /// Split into separate receive and send halves for concurrent use. + pub fn split(&mut self) -> (RecvHalf<'_, 'umem>, SendHalf<'_, 'umem>) { + let rx_queue = &self.rx_queue; + let recv = RecvHalf { rx_queue }; + let send = SendHalf { + free_frames: &mut self.free_frames, + tx_return: &mut self.tx_return, + rx_return: &mut self.rx_return, + pmtu: &self.pmtu, + neighbor_handler: &self.neighbor_handler, + src_addr: self.local_addr, + src_port: self.local_port, + tx_offload: self.tx_offload, + }; + (recv, send) + } +} + +impl<'umem> Drop for UdpSocket<'umem> { + fn drop(&mut self) { + self.close(); + } +} + +/// Receive half of a split `UdpSocket`. +pub struct RecvHalf<'sock, 'umem> { + rx_queue: &'sock LocalQueue>, +} + +impl<'sock, 'umem> RecvHalf<'sock, 'umem> { + /// Receive a packet. Returns a future that resolves when a packet is available. + #[inline(always)] + pub fn recv_from(&self) -> RecvFrom<'_, 'umem> { + RecvFrom { + rx_queue: self.rx_queue, + } + } + + /// Receive a stream of packets. Returns a stream that yields packets as they are received. + #[inline(always)] + pub fn recv_stream(&self) -> RecvStream<'_, 'umem> { + RecvStream { + rx_queue: self.rx_queue, + } + } +} + +/// Send half of a split `UdpSocket`. +pub struct SendHalf<'sock, 'umem> { + free_frames: &'sock mut SharedFrameBuffer<'umem>, + tx_return: &'sock mut SharedFrameBuffer<'umem>, + rx_return: &'sock mut SharedFrameBuffer<'umem>, + pmtu: &'sock PmtuCache, + neighbor_handler: &'sock NeighborHandler, + src_addr: IpAddress, + src_port: u16, + tx_offload: bool, +} + +impl<'sock, 'umem> SendHalf<'sock, 'umem> { + /// Send a payload to a destination address and port. + #[inline(always)] + pub fn send_to<'buf>( + &mut self, + dst_addr: IpAddress, + dst_port: u16, + payload: &'buf [u8], + ) -> SendTo<'_, 'buf, 'umem> { + SendTo { + free_frames: self.free_frames, + rx_return: self.rx_return, + tx_return: self.tx_return, + pmtu: self.pmtu, + neighbor_handler: self.neighbor_handler, + pkt: Packet::Empty, + src_addr: self.src_addr, + src_port: self.src_port, + dst_addr, + dst_port, + payload, + tx_offload: self.tx_offload, + } + } + + /// Echo a received packet back with backpressure (async). + #[inline(always)] + pub fn echo(&mut self, packet: ReceivedUdpPacket<'umem>) -> Echo<'_, 'umem> { + let payload_len = packet.packet.len() as u32; + Echo { + tx_return: self.tx_return, + pkt: packet.packet, + payload_len, + } + } + + /// Echo a received packet back immediately without backpressure (sync). + #[inline(always)] + pub fn echo_immediate(&mut self, packet: ReceivedUdpPacket<'umem>) -> u32 { + let payload_len = packet.packet.len() as u32; + packet.packet.drain_to(&mut self.tx_return); + payload_len + } + + /// Discard a received packet, returning its frames to the kernel. + #[inline(always)] + pub fn discard(&mut self, packet: ReceivedUdpPacket<'umem>) { + packet.packet.drain_to(&mut self.rx_return); + } +} + +/// Future returned by `recv_from()`. +pub struct RecvFrom<'sock, 'umem> { + rx_queue: &'sock LocalQueue>, +} + +impl<'sock, 'umem> Future for RecvFrom<'sock, 'umem> { + type Output = ReceivedUdpPacket<'umem>; + + fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll { + match self.get_mut().rx_queue.pop() { + Some(packet) => Poll::Ready(packet), + None => Poll::Pending, + } + } +} + +/// Stream that yields received UDP packets. +pub struct RecvStream<'sock, 'umem> { + rx_queue: &'sock LocalQueue>, +} + +impl<'sock, 'umem> futures_core::Stream for RecvStream<'sock, 'umem> { + type Item = ReceivedUdpPacket<'umem>; + + fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + match self.get_mut().rx_queue.pop() { + Some(packet) => Poll::Ready(Some(packet)), + None => Poll::Pending, + } + } +} + +/// Future returned by `send_to()`. +pub struct SendTo<'sock, 'buf, 'umem> { + free_frames: &'sock mut SharedFrameBuffer<'umem>, + rx_return: &'sock mut SharedFrameBuffer<'umem>, + tx_return: &'sock mut SharedFrameBuffer<'umem>, + pmtu: &'sock PmtuCache, + neighbor_handler: &'sock NeighborHandler, + pkt: Packet<'umem>, + src_addr: IpAddress, + src_port: u16, + dst_addr: IpAddress, + dst_port: u16, + payload: &'buf [u8], + tx_offload: bool, +} + +impl<'sock, 'buf, 'umem> SendTo<'sock, 'buf, 'umem> { + fn prepare_udp_packet(&mut self) -> Result, WouldBlock> { + let now = Instant::now(); + let dst_mac = match self.neighbor_handler.lookup(now, &self.dst_addr) { + Some(mac) => mac, + None => { + let frame = self.free_frames.pop().ok_or(WouldBlock)?; + match (self.src_addr, self.dst_addr) { + (IpAddress::V4(src), IpAddress::V4(dst)) => { + self.neighbor_handler.resolve_v4( + src, + dst, + frame, + &mut self.rx_return, + &mut self.tx_return, + ); + } + (IpAddress::V6(src), IpAddress::V6(dst)) => { + self.neighbor_handler.resolve_v6( + src, + dst, + frame, + &mut self.rx_return, + &mut self.tx_return, + ); + } + _ => { + self.rx_return.push(frame); + } + } + return Err(WouldBlock); + } + }; + let src_mac = self.neighbor_handler.local_mac(); + + let pmtu = self.pmtu.get(now, &self.dst_addr).min(DEFAULT_MTU); + + match (self.src_addr, self.dst_addr) { + (IpAddress::V4(src_ip), IpAddress::V4(dst_ip)) => { + self.build_udp_v4(src_mac, dst_mac, src_ip, dst_ip, pmtu) + } + (IpAddress::V6(src_ip), IpAddress::V6(dst_ip)) => { + self.build_udp_v6(src_mac, dst_mac, src_ip, dst_ip, pmtu) + } + _ => Err(WouldBlock), + } + } + + fn build_udp_v4( + &mut self, + src_mac: MacAddress, + dst_mac: MacAddress, + src_ip: Ipv4Address, + dst_ip: Ipv4Address, + pmtu: u32, + ) -> Result, WouldBlock> { + let udp_len = (UDP_HEADER_LEN + self.payload.len()) as u16; + let checksum = if self.tx_offload { + [0, 0] + } else { + compute_udp_checksum_from_parts( + &src_ip, + &dst_ip, + self.src_port, + self.dst_port, + udp_len, + self.payload, + ) + }; + let transport = UdpHeader::new(self.src_port, self.dst_port, udp_len, checksum); + + FragmentWriter::fragment_ipv4( + src_mac, + dst_mac, + src_ip, + dst_ip, + 64, + &transport, + self.payload, + pmtu, + self.tx_offload, + &mut self.free_frames, + ) + } + + fn build_udp_v6( + &mut self, + src_mac: MacAddress, + dst_mac: MacAddress, + src_ip: Ipv6Address, + dst_ip: Ipv6Address, + pmtu: u32, + ) -> Result, WouldBlock> { + let udp_len = (UDP_HEADER_LEN + self.payload.len()) as u16; + let checksum = if self.tx_offload { + [0, 0] + } else { + compute_udp_checksum_v6_from_parts( + &src_ip, + &dst_ip, + self.src_port, + self.dst_port, + udp_len, + self.payload, + ) + }; + let transport = UdpHeader::new(self.src_port, self.dst_port, udp_len, checksum); + + FragmentWriter::fragment_ipv6( + src_mac, + dst_mac, + src_ip, + dst_ip, + 64, + &transport, + self.payload, + pmtu, + &mut self.free_frames, + ) + } +} + +impl<'sock, 'buf, 'umem> Future for SendTo<'sock, 'buf, 'umem> { + type Output = u32; + + fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll { + let this = self.get_mut(); + + let pkt = match std::mem::take(&mut this.pkt) { + Packet::Empty => match this.prepare_udp_packet() { + Ok(pkt) => pkt, + Err(_) => return Poll::Pending, + }, + pkt => pkt, + }; + + if this.tx_return.free_space() < pkt.num_frames() { + this.pkt = pkt; + return Poll::Pending; + } + + pkt.drain_to(&mut this.tx_return); + Poll::Ready(this.payload.len() as u32) + } +} + +/// Future returned by `echo()`. +pub struct Echo<'sock, 'umem> { + tx_return: &'sock mut SharedFrameBuffer<'umem>, + pkt: Packet<'umem>, + payload_len: u32, +} + +impl<'sock, 'umem> Future for Echo<'sock, 'umem> { + type Output = u32; + + fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll { + let this = self.get_mut(); + + if this.tx_return.free_space() < this.pkt.num_frames() { + return Poll::Pending; + } + + let len = this.payload_len; + let pkt = std::mem::take(&mut this.pkt); + pkt.drain_to(&mut this.tx_return); + Poll::Ready(len) + } +} + +#[cfg(test)] +mod tests { + use coarsetime::Duration; + + use crate::{ + rt::context::{ContextDropGuard, RuntimeContext}, + xdp::frame::BasicFrameBuffer, + }; + + use super::*; + + /// Set up a fake runtime context for testing. + fn with_test_context(f: F) { + let free_frames: SharedFrameBuffer = BasicFrameBuffer::new(128).into(); + let tx_return: SharedFrameBuffer = BasicFrameBuffer::new(128).into(); + let rx_return: SharedFrameBuffer = BasicFrameBuffer::new(128).into(); + let pmtu = Rc::new(PmtuCache::new()); + let neighbor_handler = + Rc::new(NeighborHandler::new("test0", Duration::from_secs(60)).unwrap()); + let udp_handler = Rc::new(UnsafeCell::new(UdpHandler::new(256, false))); + let tcp_handler = Rc::new(UnsafeCell::new(crate::net::handler::tcp::TcpHandler::new( + false, false, + ))); + + let ctx = RuntimeContext { + free_frames, + tx_return, + rx_return, + pmtu, + neighbor_handler, + udp_handler, + tcp_handler, + tx_offload: false, + }; + { + let _guard = ContextDropGuard::new(ctx); + f(); + } + } + + #[test] + fn new_and_accessors() { + with_test_context(|| { + let sock = UdpSocket::new(IpAddress::V4(Ipv4Address::unspecified()), 0).unwrap(); + assert_eq!(sock.local_addr(), IpAddress::V4(Ipv4Address::unspecified())); + assert_eq!(sock.local_port(), 0); + }); + } + + #[test] + fn bind_and_close_lifecycle() { + with_test_context(|| { + let addr = IpAddress::V4(Ipv4Address::new([192, 168, 1, 1])); + let mut sock = UdpSocket::new(addr, 5000).unwrap(); + + assert_eq!(sock.local_addr(), addr); + assert_eq!(sock.local_port(), 5000); + + sock.close(); + }); + } + + #[test] + fn double_bind_returns_address_in_use() { + with_test_context(|| { + let addr = IpAddress::V4(Ipv4Address::new([192, 168, 1, 1])); + let _sock = UdpSocket::new(addr, 5000).unwrap(); + let err = UdpSocket::new(addr, 5000).unwrap_err(); + assert_eq!(err, BindError::AddressInUse); + }); + } + + #[test] + #[should_panic(expected = "UdpSocket::new() called outside of LocalRuntime::run()")] + fn new_panics_outside_runtime() { + let _ = UdpSocket::new(IpAddress::V4(Ipv4Address::unspecified()), 0).unwrap(); + } +} diff --git a/src/net/wire/arp.rs b/src/net/wire/arp.rs new file mode 100644 index 0000000..007eb0a --- /dev/null +++ b/src/net/wire/arp.rs @@ -0,0 +1,142 @@ +use std::mem::size_of; + +use super::{ + ethernet::{EtherType, EthernetFrame, MacAddress}, + ip::Ipv4Address, +}; + +/// ARP hardware type representation. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[repr(C, packed)] +pub struct ArpHardwareType { + pub octets: [u8; 2], +} + +/// ARP hardware types. +#[allow(non_snake_case)] +#[allow(non_upper_case_globals)] +pub mod ArpHardwareTypes { + use super::ArpHardwareType; + + /// Ethernet hardware type. + pub const Ethernet: ArpHardwareType = ArpHardwareType { + octets: [0x00, 0x01], + }; +} + +/// ARP operation representation. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[repr(C, packed)] +pub struct ArpOperation { + pub octets: [u8; 2], +} + +/// ARP operations. +#[allow(non_snake_case)] +#[allow(non_upper_case_globals)] +pub mod ArpOperations { + use super::ArpOperation; + + /// ARP request operation. + pub const Request: ArpOperation = ArpOperation { + octets: [0x00, 0x01], + }; + + /// ARP reply operation. + pub const Reply: ArpOperation = ArpOperation { + octets: [0x00, 0x02], + }; +} + +/// An ARP packet for Ethernet / IPv4. +/// +/// This is the 28-byte ARP payload that immediately follows the 14-byte +/// Ethernet header. The struct is `repr(C, packed)` so it can be +/// zero-copy-cast directly from a received frame. +#[repr(C, packed)] +pub struct ArpPacket { + /// ARP hardware type. + pub htype: ArpHardwareType, + /// ARP protocol type. + pub ptype: EtherType, + /// ARP hardware address length. + pub hlen: u8, + /// ARP protocol address length. + pub plen: u8, + /// ARP operation. + pub oper: ArpOperation, + /// ARP sender hardware address. + pub sha: MacAddress, + /// ARP sender protocol address. + pub spa: Ipv4Address, + /// ARP target hardware address. + pub tha: MacAddress, + /// ARP target protocol address. + pub tpa: Ipv4Address, +} + +/// Complete ARP Ethernet frame (Ethernet header + ARP packet). +#[repr(C, packed)] +pub struct ArpFrame { + pub ethernet: EthernetFrame, + pub arp: ArpPacket, +} + +impl ArpFrame { + /// Returns the raw bytes of the ARP frame. + #[inline(always)] + pub fn as_bytes(&self) -> &[u8] { + unsafe { std::slice::from_raw_parts(self as *const Self as *const u8, size_of::()) } + } + + /// Mutable zero-copy borrow of the ARP frame from a received frame. + #[inline(always)] + pub fn from_bytes_mut(bytes: &mut [u8]) -> &mut Self { + debug_assert!(bytes.len() >= ARP_FRAME_LEN); + unsafe { &mut *(bytes.as_mut_ptr() as *mut Self) } + } +} + +/// Minimum frame length for an Ethernet + ARP packet. +pub const ARP_FRAME_LEN: usize = size_of::(); +const _: () = assert!(ARP_FRAME_LEN == 42); + +impl ArpPacket { + /// Zero-copy borrow of the ARP header from a received frame. + /// + /// # Safety + /// + /// The caller must ensure `frame.len() >= ARP_FRAME_LEN`. + pub fn from_bytes(bytes: &[u8]) -> &Self { + debug_assert!(bytes.len() >= ARP_FRAME_LEN); + unsafe { &*(bytes.as_ptr().add(size_of::()) as *const Self) } + } + + /// Mutable zero-copy borrow of the ARP header from a received frame. + /// + /// # Safety + /// + /// The caller must ensure `frame.len() >= ARP_FRAME_LEN`. + pub fn from_bytes_mut(bytes: &mut [u8]) -> &mut Self { + debug_assert!(bytes.len() >= ARP_FRAME_LEN); + unsafe { &mut *(bytes.as_mut_ptr().add(size_of::()) as *mut Self) } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn arp_constants() { + assert_eq!(ArpHardwareTypes::Ethernet.octets, [0x00, 0x01]); + assert_eq!(ArpOperations::Request.octets, [0x00, 0x01]); + assert_eq!(ArpOperations::Reply.octets, [0x00, 0x02]); + } + + #[test] + fn arp_packet_layout() { + assert_eq!(size_of::(), 28); + assert_eq!(ARP_FRAME_LEN, 42); + } +} diff --git a/src/net/wire/ethernet.rs b/src/net/wire/ethernet.rs new file mode 100644 index 0000000..259aa29 --- /dev/null +++ b/src/net/wire/ethernet.rs @@ -0,0 +1,183 @@ +use std::fmt::Display; + +/// A MAC address representation. +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] +#[repr(C, packed)] +pub struct MacAddress { + pub octets: [u8; 6], +} + +impl MacAddress { + /// Creates a new MAC address. + pub const fn new(octets: [u8; 6]) -> Self { + Self { octets } + } + + /// Creates a broadcast MAC address. + pub const fn broadcast() -> Self { + Self::new([0xFF; 6]) + } + + /// Creates a zero MAC address. + pub const fn zero() -> Self { + Self::new([0x00; 6]) + } +} + +impl From<[u8; 6]> for MacAddress { + fn from(octets: [u8; 6]) -> Self { + Self { octets } + } +} + +impl From for [u8; 6] { + fn from(mac: MacAddress) -> Self { + mac.octets + } +} + +impl Display for MacAddress { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}", + self.octets[0], + self.octets[1], + self.octets[2], + self.octets[3], + self.octets[4], + self.octets[5] + ) + } +} + +/// An EtherType representation. +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] +#[repr(C, packed)] +pub struct EtherType { + pub octets: [u8; 2], +} + +impl Display for EtherType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match *self { + EtherTypes::IPv4 => write!(f, "IPv4"), + EtherTypes::IPv6 => write!(f, "IPv6"), + EtherTypes::Arp => write!(f, "ARP"), + _ => write!(f, "Unknown"), + } + } +} + +/// EtherTypes. +#[allow(non_snake_case)] +#[allow(non_upper_case_globals)] +pub mod EtherTypes { + use super::EtherType; + + /// IPv4 EtherType. + pub const IPv4: EtherType = EtherType { + octets: [0x08, 0x00], + }; + + /// IPv6 EtherType. + pub const IPv6: EtherType = EtherType { + octets: [0x86, 0xDD], + }; + + /// ARP EtherType. + pub const Arp: EtherType = EtherType { + octets: [0x08, 0x06], + }; +} + +/// An Ethernet frame representation. +#[derive(Debug)] +#[repr(C, packed)] +pub struct EthernetFrame { + /// Destination MAC address. + pub dst_mac: MacAddress, + /// Source MAC address. + pub src_mac: MacAddress, + /// EtherType. + pub ether_type: EtherType, +} + +impl EthernetFrame { + /// Zero-copy borrow of the Ethernet header from a received frame. + /// + /// # Safety + /// + /// The caller must ensure `frame.len() >= size_of::()`. + pub fn from_bytes(frame: &[u8]) -> &Self { + debug_assert!(frame.len() >= size_of::()); + unsafe { &*(frame.as_ptr() as *const Self) } + } + + /// Mutable zero-copy borrow of the Ethernet header from a received frame. + /// + /// # Safety + /// + /// The caller must ensure `frame.len() >= size_of::()`. + pub fn from_bytes_mut(frame: &mut [u8]) -> &mut Self { + debug_assert!(frame.len() >= size_of::()); + unsafe { &mut *(frame.as_mut_ptr() as *mut Self) } + } +} + +impl Display for EthernetFrame { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "EthernetFrame {{ src_mac: {}, dst_mac: {}, ether_type: {} }}", + self.src_mac, self.dst_mac, self.ether_type + ) + } +} + +/// Writes the Ethernet header to a frame. +#[inline] +pub fn write_ethernet_header( + frame: &mut [u8], + dst_mac: MacAddress, + src_mac: MacAddress, + ether_type: EtherType, +) { + let eth = EthernetFrame::from_bytes_mut(frame); + eth.dst_mac = dst_mac; + eth.src_mac = src_mac; + eth.ether_type = ether_type; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn mac_address_constructors() { + let mac = MacAddress::new([0x01, 0x02, 0x03, 0x04, 0x05, 0x06]); + assert_eq!(mac.octets, [0x01, 0x02, 0x03, 0x04, 0x05, 0x06]); + assert_eq!(MacAddress::broadcast().octets, [0xFF; 6]); + assert_eq!(MacAddress::zero().octets, [0x00; 6]); + } + + #[test] + fn mac_address_from_conversions() { + let mac = MacAddress::from([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]); + assert_eq!(mac.octets, [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]); + let arr: [u8; 6] = mac.into(); + assert_eq!(arr, [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]); + } + + #[test] + fn ether_type_constants() { + assert_eq!(EtherTypes::IPv4.octets, [0x08, 0x00]); + assert_eq!(EtherTypes::IPv6.octets, [0x86, 0xDD]); + assert_eq!(EtherTypes::Arp.octets, [0x08, 0x06]); + } + + #[test] + fn ethernet_frame_layout() { + assert_eq!(size_of::(), 14); + } +} diff --git a/src/net/wire/icmpv4.rs b/src/net/wire/icmpv4.rs new file mode 100644 index 0000000..0176ba2 --- /dev/null +++ b/src/net/wire/icmpv4.rs @@ -0,0 +1,178 @@ +use super::{ethernet::EthernetFrame, ip::Ipv4Header}; + +/// ICMPv4 header length in bytes (type + code + checksum + rest-of-header). +pub const ICMPV4_HEADER_LEN: usize = 8; + +const _: () = assert!(size_of::() == ICMPV4_HEADER_LEN); + +/// ICMPv4 header wire format (8 bytes). +/// +/// The `rest_of_header` field is type-dependent: +/// * Echo Request/Reply: identifier (2 bytes) + sequence number (2 bytes) +/// * Destination Unreachable: unused (2 bytes) + next-hop MTU (2 bytes, code 4 only) +/// * Time Exceeded / Parameter Problem: unused (4 bytes) +#[repr(C, packed)] +pub struct Icmpv4Header { + /// ICMP type. + pub icmp_type: u8, + /// ICMP code. + pub code: u8, + /// ICMP checksum. + pub checksum: [u8; 2], + /// Rest of the header. + /// + /// For Echo Request/Reply: identifier (2 bytes) + sequence number (2 bytes) + /// For Destination Unreachable: unused (2 bytes) + next-hop MTU (2 bytes, code 4 only) + /// For Time Exceeded / Parameter Problem: unused (4 bytes) + pub rest_of_header: [u8; 4], +} + +impl Icmpv4Header { + /// Returns the raw bytes of this header. + #[inline(always)] + pub fn as_bytes(&self) -> &[u8] { + unsafe { std::slice::from_raw_parts(self as *const Self as *const u8, size_of::()) } + } + + /// Zero-copy borrow of the ICMPv4 header at the given byte offset in a frame. + /// + /// The caller must ensure `offset + ICMPV4_HEADER_LEN <= frame.len()`. + #[inline(always)] + pub fn from_bytes_at(bytes: &[u8], offset: usize) -> &Self { + debug_assert!(offset + ICMPV4_HEADER_LEN <= bytes.len()); + unsafe { &*(bytes.as_ptr().add(offset) as *const Self) } + } + + /// Mutable zero-copy borrow of the ICMPv4 header at the given byte offset. + /// + /// The caller must ensure `offset + ICMPV4_HEADER_LEN <= frame.len()`. + #[inline(always)] + pub fn from_bytes_at_mut(bytes: &mut [u8], offset: usize) -> &mut Self { + debug_assert!(offset + ICMPV4_HEADER_LEN <= bytes.len()); + unsafe { &mut *(bytes.as_mut_ptr().add(offset) as *mut Self) } + } + + /// Returns the next-hop MTU from a Destination Unreachable / Fragmentation + /// Needed message. Bytes 6-7 of the ICMP header (`rest_of_header[2..4]`) + /// contain the next-hop MTU in network byte order. + #[inline] + pub fn next_hop_mtu(&self) -> u16 { + u16::from_be_bytes([self.rest_of_header[2], self.rest_of_header[3]]) + } +} + +/// ICMPv4 types. +#[allow(non_snake_case)] +#[allow(non_upper_case_globals)] +pub mod Icmpv4Types { + /// Echo Reply. + pub const EchoReply: u8 = 0; + /// Destination Unreachable. + pub const DestinationUnreachable: u8 = 3; + /// Source Quench. + pub const SourceQuench: u8 = 4; + /// Redirect. + pub const Redirect: u8 = 5; + /// Echo Request. + pub const EchoRequest: u8 = 8; + /// Time Exceeded. + pub const TimeExceeded: u8 = 11; + /// Parameter Problem. + pub const ParameterProblem: u8 = 12; +} + +/// ICMPv4 codes. +#[allow(non_snake_case)] +#[allow(non_upper_case_globals)] +pub mod Icmpv4Codes { + /// Protocol Unreachable. + pub const ProtocolUnreachable: u8 = 2; + /// Port Unreachable. + pub const PortUnreachable: u8 = 3; + /// Fragmentation Needed. + pub const FragmentationNeeded: u8 = 4; +} + +/// Returns `true` if the given ICMPv4 type is an error message. +/// +/// Per RFC 1122, ICMP error messages MUST NOT be sent in response to +/// other ICMP error messages. +#[inline] +pub fn is_icmp_error(icmp_type: u8) -> bool { + matches!( + icmp_type, + Icmpv4Types::DestinationUnreachable + | Icmpv4Types::SourceQuench + | Icmpv4Types::Redirect + | Icmpv4Types::TimeExceeded + | Icmpv4Types::ParameterProblem + ) +} + +/// Ethernet + IPv4 (minimum header) + ICMPv4 header frame overlay. +/// +/// Used for building ICMP responses where the IPv4 header has no options +/// (IHL = 5). Any ICMP payload follows immediately after. +#[repr(C, packed)] +pub struct Icmpv4Frame { + pub ethernet: EthernetFrame, + pub ipv4: Ipv4Header, + pub icmpv4: Icmpv4Header, +} + +/// Minimum frame length for Ethernet + IPv4 + ICMPv4 header. +pub const ICMPV4_FRAME_LEN: usize = size_of::(); +const _: () = assert!(ICMPV4_FRAME_LEN == 42); + +impl Icmpv4Frame { + /// Mutable zero-copy borrow of the Ethernet + IPv4 + ICMPv4 headers. + /// + /// The caller must ensure `frame.len() >= ICMPV4_FRAME_LEN`. + #[inline(always)] + pub fn from_bytes_mut(bytes: &mut [u8]) -> &mut Self { + debug_assert!(bytes.len() >= ICMPV4_FRAME_LEN); + unsafe { &mut *(bytes.as_mut_ptr() as *mut Self) } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn is_icmp_error_classification() { + // All error types return true + assert!(is_icmp_error(Icmpv4Types::DestinationUnreachable)); + assert!(is_icmp_error(Icmpv4Types::SourceQuench)); + assert!(is_icmp_error(Icmpv4Types::Redirect)); + assert!(is_icmp_error(Icmpv4Types::TimeExceeded)); + assert!(is_icmp_error(Icmpv4Types::ParameterProblem)); + + // Non-error types return false + assert!(!is_icmp_error(Icmpv4Types::EchoReply)); + assert!(!is_icmp_error(Icmpv4Types::EchoRequest)); + assert!(!is_icmp_error(0xFF)); + } + + #[test] + fn icmpv4_header_layout() { + assert_eq!(ICMPV4_HEADER_LEN, 8); + assert_eq!(size_of::(), 8); + } + + #[test] + fn icmpv4_frame_layout() { + assert_eq!(ICMPV4_FRAME_LEN, 42); // 14 + 20 + 8 + } + + #[test] + fn next_hop_mtu_parsing() { + let hdr = Icmpv4Header { + icmp_type: Icmpv4Types::DestinationUnreachable, + code: Icmpv4Codes::FragmentationNeeded, + checksum: [0, 0], + rest_of_header: [0, 0, 0x05, 0x00], // MTU = 1280 + }; + assert_eq!(hdr.next_hop_mtu(), 1280); + } +} diff --git a/src/net/wire/icmpv6.rs b/src/net/wire/icmpv6.rs new file mode 100644 index 0000000..1fecc44 --- /dev/null +++ b/src/net/wire/icmpv6.rs @@ -0,0 +1,210 @@ +use super::{ + ethernet::EthernetFrame, + ip::{IPV6_HEADER_LEN, Ipv6Header}, +}; + +/// ICMPv6 header length in bytes (type + code + checksum + body). +pub const ICMPV6_HEADER_LEN: usize = 8; + +const _: () = assert!(size_of::() == ICMPV6_HEADER_LEN); + +/// Minimum IPv6 MTU per RFC 2460. +const IPV6_MIN_MTU: usize = 1280; + +/// Maximum bytes of the original packet that can be included in an +/// ICMPv6 error payload without exceeding the minimum IPv6 MTU. +/// 1280 (min MTU) - 40 (IPv6 header) - 8 (ICMPv6 header) = 1232. +pub const MAX_ERROR_PAYLOAD: usize = IPV6_MIN_MTU - IPV6_HEADER_LEN - ICMPV6_HEADER_LEN; + +/// ICMPv6 header wire format (8 bytes). +/// +/// The `body` field is type-dependent: +/// * Echo Request/Reply: identifier (2 bytes) + sequence number (2 bytes) +/// * Destination Unreachable: unused (4 bytes) +/// * Packet Too Big: MTU (4 bytes, network byte order) +/// * Time Exceeded: unused (4 bytes) +/// * Parameter Problem: pointer (4 bytes, network byte order) +#[repr(C, packed)] +pub struct Icmpv6Header { + /// ICMP type. + pub icmp_type: u8, + /// ICMP code. + pub code: u8, + /// ICMP checksum. + pub checksum: [u8; 2], + /// Body. + pub body: [u8; 4], +} + +impl Icmpv6Header { + /// Returns the raw bytes of this header. + #[inline(always)] + pub fn as_bytes(&self) -> &[u8] { + unsafe { std::slice::from_raw_parts(self as *const Self as *const u8, size_of::()) } + } + + /// Zero-copy borrow of the ICMPv6 header at the given byte offset in a frame. + /// + /// The caller must ensure `offset + ICMPV6_HEADER_LEN <= frame.len()`. + #[inline(always)] + pub fn from_bytes_at(bytes: &[u8], offset: usize) -> &Self { + debug_assert!(offset + ICMPV6_HEADER_LEN <= bytes.len()); + unsafe { &*(bytes.as_ptr().add(offset) as *const Self) } + } + + /// Mutable zero-copy borrow of the ICMPv6 header at the given byte offset. + /// + /// The caller must ensure `offset + ICMPV6_HEADER_LEN <= frame.len()`. + #[inline(always)] + pub fn from_bytes_at_mut(bytes: &mut [u8], offset: usize) -> &mut Self { + debug_assert!(offset + ICMPV6_HEADER_LEN <= bytes.len()); + unsafe { &mut *(bytes.as_mut_ptr().add(offset) as *mut Self) } + } + + /// Returns the body field as a big-endian `u32`. + /// + /// The interpretation depends on the ICMPv6 type: + /// * Packet Too Big: MTU + /// * Parameter Problem: pointer to the offending field + /// * Destination Unreachable / Time Exceeded: unused (zero) + #[inline] + pub fn body_as_u32(&self) -> u32 { + u32::from_be_bytes(self.body) + } +} + +/// ICMPv6 types. +#[allow(non_snake_case)] +#[allow(non_upper_case_globals)] +pub mod Icmpv6Types { + /// Destination Unreachable. + pub const DestinationUnreachable: u8 = 1; + /// Packet Too Big. + pub const PacketTooBig: u8 = 2; + /// Time Exceeded. + pub const TimeExceeded: u8 = 3; + /// Parameter Problem. + pub const ParameterProblem: u8 = 4; + /// Echo Request. + pub const EchoRequest: u8 = 128; + /// Echo Reply. + pub const EchoReply: u8 = 129; + /// Router Solicitation. + pub const RouterSolicitation: u8 = 133; + /// Router Advertisement. + pub const RouterAdvertisement: u8 = 134; + /// Neighbor Solicitation. + pub const NeighborSolicitation: u8 = 135; + /// Neighbor Advertisement. + pub const NeighborAdvertisement: u8 = 136; + /// Redirect. + pub const Redirect: u8 = 137; +} + +/// ICMPv6 codes. +#[allow(non_snake_case)] +#[allow(non_upper_case_globals)] +pub mod Icmpv6Codes { + /// No Route To Destination. + pub const NoRouteToDestination: u8 = 0; + /// Admin Prohibited. + pub const AdminProhibited: u8 = 1; + /// Beyond Scope. + pub const BeyondScope: u8 = 2; + /// Address Unreachable. + pub const AddressUnreachable: u8 = 3; + /// Port Unreachable. + pub const PortUnreachable: u8 = 4; + /// Erroneous Header Field. + pub const ErroneousHeaderField: u8 = 0; + /// Unrecognized Next Header. + pub const UnrecognizedNextHeader: u8 = 1; + /// Unrecognized Option. + pub const UnrecognizedOption: u8 = 2; +} + +/// Ethernet + IPv6 + ICMPv6 header frame overlay. +/// +/// Used for building ICMPv6 responses where no IPv6 extension headers +/// are present. Any ICMPv6 payload follows immediately after. +#[repr(C, packed)] +pub struct Icmpv6Frame { + pub ethernet: EthernetFrame, + pub ipv6: Ipv6Header, + pub icmpv6: Icmpv6Header, +} + +/// Minimum frame length for Ethernet + IPv6 + ICMPv6 header. +pub const ICMPV6_FRAME_LEN: usize = size_of::(); +const _: () = assert!(ICMPV6_FRAME_LEN == 62); // 14 + 40 + 8 + +impl Icmpv6Frame { + /// Mutable zero-copy borrow of the Ethernet + IPv6 + ICMPv6 headers. + /// + /// The caller must ensure `frame.len() >= ICMPV6_FRAME_LEN`. + #[inline(always)] + pub fn from_bytes_mut(bytes: &mut [u8]) -> &mut Self { + debug_assert!(bytes.len() >= ICMPV6_FRAME_LEN); + unsafe { &mut *(bytes.as_mut_ptr() as *mut Self) } + } +} + +/// Returns `true` if the ICMPv6 type is an error message. +/// +/// Per RFC 4443 §2.4, ICMPv6 error messages have types in the range +/// 0--127. Informational messages occupy 128--255. +#[inline] +pub fn is_icmpv6_error(icmpv6_type: u8) -> bool { + icmpv6_type < 128 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn is_icmpv6_error_boundary() { + // Error types (0-127) + assert!(is_icmpv6_error(0)); + assert!(is_icmpv6_error(127)); + assert!(is_icmpv6_error(Icmpv6Types::DestinationUnreachable)); + assert!(is_icmpv6_error(Icmpv6Types::PacketTooBig)); + assert!(is_icmpv6_error(Icmpv6Types::TimeExceeded)); + assert!(is_icmpv6_error(Icmpv6Types::ParameterProblem)); + + // Informational types (128-255) + assert!(!is_icmpv6_error(128)); + assert!(!is_icmpv6_error(255)); + assert!(!is_icmpv6_error(Icmpv6Types::EchoRequest)); + assert!(!is_icmpv6_error(Icmpv6Types::EchoReply)); + assert!(!is_icmpv6_error(Icmpv6Types::NeighborSolicitation)); + } + + #[test] + fn max_error_payload_value() { + // 1280 (min MTU) - 40 (IPv6 hdr) - 8 (ICMPv6 hdr) + assert_eq!(MAX_ERROR_PAYLOAD, 1232); + } + + #[test] + fn icmpv6_header_layout() { + assert_eq!(ICMPV6_HEADER_LEN, 8); + assert_eq!(size_of::(), 8); + } + + #[test] + fn icmpv6_frame_layout() { + assert_eq!(ICMPV6_FRAME_LEN, 62); // 14 + 40 + 8 + } + + #[test] + fn body_as_u32_parsing() { + let hdr = Icmpv6Header { + icmp_type: Icmpv6Types::PacketTooBig, + code: 0, + checksum: [0, 0], + body: 1280u32.to_be_bytes(), + }; + assert_eq!(hdr.body_as_u32(), 1280); + } +} diff --git a/src/net/wire/ip/addr.rs b/src/net/wire/ip/addr.rs new file mode 100644 index 0000000..54cc1f0 --- /dev/null +++ b/src/net/wire/ip/addr.rs @@ -0,0 +1,501 @@ +use std::{fmt::Display, str::FromStr}; + +/// An IPv4 address representation. +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] +#[repr(C, packed)] +pub struct Ipv4Address { + pub octets: [u8; 4], +} + +impl Ipv4Address { + /// Creates a new IPv4 address. + pub const fn new(octets: [u8; 4]) -> Self { + Self { octets } + } + + /// Creates a loopback IPv4 address (127.0.0.1). + pub const fn loopback() -> Self { + Self::new([127, 0, 0, 1]) + } + + /// Creates an unspecified IPv4 address (0.0.0.0). + pub const fn unspecified() -> Self { + Self::new([0; 4]) + } + + /// Creates a limited broadcast IPv4 address (255.255.255.255). + pub const fn broadcast() -> Self { + Self::new([255; 4]) + } + + /// Returns `true` if this is the unspecified address (0.0.0.0). + #[inline] + pub const fn is_unspecified(&self) -> bool { + self.octets[0] == 0 && self.octets[1] == 0 && self.octets[2] == 0 && self.octets[3] == 0 + } + + /// Returns `true` if this is the limited broadcast address (255.255.255.255). + #[inline] + pub const fn is_broadcast(&self) -> bool { + self.octets[0] == 255 + && self.octets[1] == 255 + && self.octets[2] == 255 + && self.octets[3] == 255 + } + + /// Returns `true` if this is a multicast address (224.0.0.0/4). + #[inline] + pub const fn is_multicast(&self) -> bool { + (self.octets[0] & 0xF0) == 0xE0 + } + + /// Returns `true` if this is a loopback address (127.0.0.0/8). + #[inline] + pub const fn is_loopback(&self) -> bool { + self.octets[0] == 127 + } +} + +impl From<[u8; 4]> for Ipv4Address { + fn from(octets: [u8; 4]) -> Self { + Self { octets } + } +} + +impl From for [u8; 4] { + fn from(addr: Ipv4Address) -> Self { + addr.octets + } +} + +impl From for Ipv4Address { + fn from(addr: std::net::Ipv4Addr) -> Self { + Self::new(addr.octets()) + } +} + +impl From for std::net::Ipv4Addr { + fn from(addr: Ipv4Address) -> Self { + std::net::Ipv4Addr::from(addr.octets) + } +} + +impl FromStr for Ipv4Address { + type Err = std::io::Error; + fn from_str(s: &str) -> Result { + let addr = std::net::Ipv4Addr::from_str(s) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?; + Ok(addr.into()) + } +} + +impl Display for Ipv4Address { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let v4: std::net::Ipv4Addr = (*self).into(); + write!(f, "{}", v4) + } +} + +/// An IPv6 address representation. +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] +#[repr(C, packed)] +pub struct Ipv6Address { + pub octets: [u8; 16], +} + +impl Ipv6Address { + /// Creates a new IPv6 address. + pub const fn new(octets: [u8; 16]) -> Self { + Self { octets } + } + + /// Creates a loopback IPv6 address (::1). + pub const fn loopback() -> Self { + Self::new([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]) + } + + /// Creates an unspecified IPv6 address (::). + pub const fn unspecified() -> Self { + Self::new([0; 16]) + } + + /// Returns `true` if this is the unspecified address (::). + #[inline] + pub const fn is_unspecified(&self) -> bool { + self.octets[0] == 0 + && self.octets[1] == 0 + && self.octets[2] == 0 + && self.octets[3] == 0 + && self.octets[4] == 0 + && self.octets[5] == 0 + && self.octets[6] == 0 + && self.octets[7] == 0 + && self.octets[8] == 0 + && self.octets[9] == 0 + && self.octets[10] == 0 + && self.octets[11] == 0 + && self.octets[12] == 0 + && self.octets[13] == 0 + && self.octets[14] == 0 + && self.octets[15] == 0 + } + + /// Returns `true` if this is a multicast address (ff00::/8). + #[inline] + pub const fn is_multicast(&self) -> bool { + self.octets[0] == 0xFF + } + + /// Returns `true` if this is the loopback address (::1). + #[inline] + pub const fn is_loopback(&self) -> bool { + self.octets[0] == 0 + && self.octets[1] == 0 + && self.octets[2] == 0 + && self.octets[3] == 0 + && self.octets[4] == 0 + && self.octets[5] == 0 + && self.octets[6] == 0 + && self.octets[7] == 0 + && self.octets[8] == 0 + && self.octets[9] == 0 + && self.octets[10] == 0 + && self.octets[11] == 0 + && self.octets[12] == 0 + && self.octets[13] == 0 + && self.octets[14] == 0 + && self.octets[15] == 1 + } + + /// Returns `true` if this is a link-local address (fe80::/10). + #[inline] + pub const fn is_link_local(&self) -> bool { + self.octets[0] == 0xFE && (self.octets[1] & 0xC0) == 0x80 + } + + /// Computes the solicited-node multicast address (`ff02::1:ffXX:XXXX`) + /// from the last 24 bits of this address. + pub const fn solicited_node_multicast(&self) -> Self { + Self::new([ + 0xFF, + 0x02, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0x01, + 0xFF, + self.octets[13], + self.octets[14], + self.octets[15], + ]) + } + + /// Computes the IPv6 multicast MAC address (`33:33:XX:XX:XX:XX`) + /// from the last 4 bytes of this address. + pub const fn multicast_mac(&self) -> super::ethernet::MacAddress { + super::ethernet::MacAddress::new([ + 0x33, + 0x33, + self.octets[12], + self.octets[13], + self.octets[14], + self.octets[15], + ]) + } +} + +impl From<[u8; 16]> for Ipv6Address { + fn from(octets: [u8; 16]) -> Self { + Self { octets } + } +} + +impl From for [u8; 16] { + fn from(addr: Ipv6Address) -> Self { + addr.octets + } +} + +impl From for Ipv6Address { + fn from(addr: std::net::Ipv6Addr) -> Self { + Self::new(addr.octets()) + } +} + +impl From for std::net::Ipv6Addr { + fn from(addr: Ipv6Address) -> Self { + std::net::Ipv6Addr::from(addr.octets) + } +} + +impl FromStr for Ipv6Address { + type Err = std::io::Error; + fn from_str(s: &str) -> Result { + let addr = std::net::Ipv6Addr::from_str(s) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?; + Ok(addr.into()) + } +} + +impl Display for Ipv6Address { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let v6: std::net::Ipv6Addr = (*self).into(); + write!(f, "{}", v6) + } +} + +/// Protocol-agnostic IP address used as the key in the neighbor cache. +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] +pub enum IpAddress { + /// IPv4 address. + V4(Ipv4Address), + /// IPv6 address. + V6(Ipv6Address), +} + +impl IpAddress { + /// Returns `true` if this is the unspecified address for its protocol. + #[inline] + pub fn is_unspecified(&self) -> bool { + match self { + IpAddress::V4(v4) => v4.is_unspecified(), + IpAddress::V6(v6) => v6.is_unspecified(), + } + } +} + +impl From for IpAddress { + fn from(addr: Ipv4Address) -> Self { + Self::V4(addr) + } +} + +impl From for IpAddress { + fn from(addr: Ipv6Address) -> Self { + Self::V6(addr) + } +} + +impl From for IpAddress { + fn from(addr: std::net::IpAddr) -> Self { + match addr { + std::net::IpAddr::V4(addr) => Self::V4(addr.into()), + std::net::IpAddr::V6(addr) => Self::V6(addr.into()), + } + } +} + +impl From for std::net::IpAddr { + fn from(addr: IpAddress) -> Self { + match addr { + IpAddress::V4(v4) => std::net::IpAddr::V4(v4.into()), + IpAddress::V6(v6) => std::net::IpAddr::V6(v6.into()), + } + } +} + +impl From for IpAddress { + fn from(addr: std::net::Ipv4Addr) -> Self { + Self::V4(addr.into()) + } +} + +impl From for IpAddress { + fn from(addr: std::net::Ipv6Addr) -> Self { + Self::V6(addr.into()) + } +} + +impl FromStr for IpAddress { + type Err = std::io::Error; + fn from_str(s: &str) -> Result { + let addr = std::net::IpAddr::from_str(s) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?; + Ok(Self::from(addr)) + } +} + +impl Display for IpAddress { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + IpAddress::V4(v4) => write!(f, "{}", v4), + IpAddress::V6(v6) => write!(f, "{}", v6), + } + } +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] +pub struct SocketAddr { + pub ip: IpAddress, + pub port: u16, +} + +impl SocketAddr { + pub fn new(ip: IpAddress, port: u16) -> Self { + Self { ip, port } + } +} + +impl From for SocketAddr { + fn from(addr: std::net::SocketAddr) -> Self { + Self { + ip: addr.ip().into(), + port: addr.port(), + } + } +} + +impl From for std::net::SocketAddr { + fn from(addr: SocketAddr) -> Self { + std::net::SocketAddr::new(addr.ip.into(), addr.port) + } +} + +impl FromStr for SocketAddr { + type Err = std::io::Error; + fn from_str(s: &str) -> Result { + let addr = std::net::SocketAddr::from_str(s) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?; + Ok(addr.into()) + } +} + +impl Display for SocketAddr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}:{}", self.ip, self.port) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn ipv4_special_addresses() { + assert_eq!(Ipv4Address::loopback().octets, [127, 0, 0, 1]); + assert_eq!(Ipv4Address::unspecified().octets, [0; 4]); + assert_eq!(Ipv4Address::broadcast().octets, [255; 4]); + } + + #[test] + fn ipv4_classifiers() { + assert!(Ipv4Address::loopback().is_loopback()); + assert!(Ipv4Address::new([127, 255, 0, 0]).is_loopback()); + assert!(!Ipv4Address::new([128, 0, 0, 1]).is_loopback()); + + assert!(Ipv4Address::unspecified().is_unspecified()); + assert!(!Ipv4Address::new([0, 0, 0, 1]).is_unspecified()); + + assert!(Ipv4Address::broadcast().is_broadcast()); + assert!(!Ipv4Address::new([255, 255, 255, 0]).is_broadcast()); + + // Multicast: 224.0.0.0/4 (first nibble 0xE) + assert!(Ipv4Address::new([224, 0, 0, 1]).is_multicast()); + assert!(Ipv4Address::new([239, 255, 255, 255]).is_multicast()); + assert!(!Ipv4Address::new([240, 0, 0, 1]).is_multicast()); + assert!(!Ipv4Address::new([223, 255, 255, 255]).is_multicast()); + } + + #[test] + fn ipv4_from_conversions() { + let addr = Ipv4Address::from([10, 0, 0, 1]); + assert_eq!(addr.octets, [10, 0, 0, 1]); + let arr: [u8; 4] = addr.into(); + assert_eq!(arr, [10, 0, 0, 1]); + } + + #[test] + fn ipv6_special_addresses() { + let loopback = Ipv6Address::loopback(); + assert_eq!(loopback.octets[15], 1); + assert!(loopback.octets[..15].iter().all(|&b| b == 0)); + + let unspec = Ipv6Address::unspecified(); + assert!(unspec.octets.iter().all(|&b| b == 0)); + } + + #[test] + fn ipv6_classifiers() { + assert!(Ipv6Address::loopback().is_loopback()); + assert!(!Ipv6Address::loopback().is_unspecified()); + assert!(Ipv6Address::unspecified().is_unspecified()); + assert!(!Ipv6Address::unspecified().is_loopback()); + + // Multicast ff00::/8 + assert!( + Ipv6Address::new([0xFF, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]).is_multicast() + ); + assert!( + !Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]) + .is_multicast() + ); + + // Link-local fe80::/10 + assert!( + Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]) + .is_link_local() + ); + assert!( + Ipv6Address::new([0xFE, 0xBF, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]) + .is_link_local() + ); + // fe_c0 is outside /10 + assert!( + !Ipv6Address::new([0xFE, 0xC0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]) + .is_link_local() + ); + assert!( + !Ipv6Address::new([0xFE, 0x00, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]) + .is_link_local() + ); + } + + #[test] + fn ipv6_from_conversions() { + let octets = [1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; + let addr = Ipv6Address::from(octets); + assert_eq!(addr.octets, octets); + let arr: [u8; 16] = addr.into(); + assert_eq!(arr, octets); + } + + #[test] + fn solicited_node_multicast_correctness() { + let addr = Ipv6Address::new([ + 0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x12, 0x34, 0x56, + ]); + let sol = addr.solicited_node_multicast(); + let expected = Ipv6Address::new([ + 0xFF, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x01, 0xFF, 0x12, 0x34, 0x56, + ]); + assert_eq!(sol, expected); + } + + #[test] + fn multicast_mac_correctness() { + let addr = Ipv6Address::new([ + 0xFF, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x01, 0xFF, 0x12, 0x34, 0x56, + ]); + let mac = addr.multicast_mac(); + let expected = + super::super::ethernet::MacAddress::new([0x33, 0x33, 0xFF, 0x12, 0x34, 0x56]); + assert_eq!(mac, expected); + } + + #[test] + fn ip_address_from_conversions() { + let v4 = Ipv4Address::loopback(); + let ip: IpAddress = v4.into(); + assert_eq!(ip, IpAddress::V4(Ipv4Address::loopback())); + + let v6 = Ipv6Address::loopback(); + let ip: IpAddress = v6.into(); + assert_eq!(ip, IpAddress::V6(Ipv6Address::loopback())); + } +} diff --git a/src/net/wire/ip/mod.rs b/src/net/wire/ip/mod.rs new file mode 100644 index 0000000..5c51705 --- /dev/null +++ b/src/net/wire/ip/mod.rs @@ -0,0 +1,14 @@ +mod addr; +mod proto; +mod v4; +mod v6; + +use super::ethernet; + +pub use addr::{IpAddress, Ipv4Address, Ipv6Address, SocketAddr}; +pub use proto::{IpProtocol, IpProtocols}; +pub use v4::{IPV4_MIN_FRAME_LEN, IPV4_MIN_HEADER_LEN, Ipv4Header}; +pub use v6::{ + EXT_AH, EXT_DESTINATION, EXT_FRAGMENT, EXT_HOP_BY_HOP, EXT_ROUTING, FRAGMENT_EXT_LEN, + IPV6_HEADER_LEN, IPV6_MIN_FRAME_LEN, Ipv6FragmentHeader, Ipv6Header, NO_NEXT_HEADER, +}; diff --git a/src/net/wire/ip/proto.rs b/src/net/wire/ip/proto.rs new file mode 100644 index 0000000..ff150f2 --- /dev/null +++ b/src/net/wire/ip/proto.rs @@ -0,0 +1,26 @@ +use std::fmt::Display; + +#[derive(Debug)] +#[repr(transparent)] +pub struct IpProtocol(pub u8); + +impl Display for IpProtocol { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.0 { + IpProtocols::Icmp => write!(f, "ICMP"), + IpProtocols::IcmpV6 => write!(f, "ICMPv6"), + IpProtocols::Udp => write!(f, "UDP"), + IpProtocols::Tcp => write!(f, "TCP"), + _ => write!(f, "Unknown"), + } + } +} + +#[allow(non_snake_case)] +#[allow(non_upper_case_globals)] +pub mod IpProtocols { + pub const Icmp: u8 = 1; + pub const IcmpV6: u8 = 58; + pub const Udp: u8 = 17; + pub const Tcp: u8 = 6; +} diff --git a/src/net/wire/ip/v4.rs b/src/net/wire/ip/v4.rs new file mode 100644 index 0000000..2744f13 --- /dev/null +++ b/src/net/wire/ip/v4.rs @@ -0,0 +1,312 @@ +use std::fmt::Display; + +use crate::net::checksum::compute_ipv4_checksum; + +use super::{IpProtocol, Ipv4Address, ethernet::EthernetFrame}; + +/// Minimum IPv4 header length in bytes (no options, IHL = 5). +pub const IPV4_MIN_HEADER_LEN: usize = 20; + +/// Compile-time guarantee that our struct matches the wire size. +const _: () = assert!(size_of::() == IPV4_MIN_HEADER_LEN); + +/// Minimum Ethernet + IPv4 frame length. +pub const IPV4_MIN_FRAME_LEN: usize = size_of::() + IPV4_MIN_HEADER_LEN; + +/// IPv4 header wire format (20 bytes, no options). +/// +/// `#[repr(C, packed)]` allows zero-copy casting from raw frame memory. +/// +/// Multi-byte fields are stored in network byte order as `[u8; 2]` to +/// avoid alignment issues on packed structs. Use the accessor methods +/// for host-order values. +#[derive(Debug)] +#[repr(C, packed)] +pub struct Ipv4Header { + /// Version (high 4 bits) + Internet Header Length (low 4 bits). + pub version_ihl: u8, + /// DSCP (high 6 bits) + ECN (low 2 bits). + pub dscp_ecn: u8, + /// Total length of the IP packet (header + payload), network byte order. + pub total_length: [u8; 2], + /// Identification field for fragment reassembly, network byte order. + pub identification: [u8; 2], + /// Flags (high 3 bits) + Fragment Offset (low 13 bits), network byte order. + pub flags_fragment_offset: [u8; 2], + /// Time to Live. + pub ttl: u8, + /// Upper-layer protocol number (e.g. TCP = 6, UDP = 17). + pub protocol: u8, + /// Header checksum, network byte order. + pub header_checksum: [u8; 2], + /// Source IPv4 address. + pub src_addr: Ipv4Address, + /// Destination IPv4 address. + pub dst_addr: Ipv4Address, +} + +impl Ipv4Header { + /// Returns the IP version (should be 4). + #[inline] + pub const fn version(&self) -> u8 { + (self.version_ihl >> 4) & 0x0F + } + + /// Returns the Internet Header Length in 32-bit words. + /// + /// A value of 5 means 20 bytes (no options). Values > 5 indicate + /// options are present. + #[inline] + pub const fn ihl(&self) -> u8 { + self.version_ihl & 0x0F + } + + /// Returns the header length in bytes (`ihl() * 4`). + #[inline] + pub const fn header_len(&self) -> usize { + self.ihl() as usize * 4 + } + + /// Returns the total length of the IPv4 packet (header + payload). + #[inline] + pub const fn total_length(&self) -> u16 { + u16::from_be_bytes(self.total_length) + } + + /// Returns the identification field. + #[inline] + pub const fn identification(&self) -> u16 { + u16::from_be_bytes(self.identification) + } + + /// Returns `true` if the Don't Fragment (DF) flag is set. + #[inline] + pub const fn dont_fragment(&self) -> bool { + self.flags_fragment_offset[0] & 0x40 != 0 + } + + /// Returns `true` if the More Fragments (MF) flag is set. + #[inline] + pub const fn more_fragments(&self) -> bool { + self.flags_fragment_offset[0] & 0x20 != 0 + } + + /// Returns the fragment offset in 8-byte units. + #[inline] + pub const fn fragment_offset(&self) -> u16 { + let hi = (self.flags_fragment_offset[0] & 0x1F) as u16; + let lo = self.flags_fragment_offset[1] as u16; + (hi << 8) | lo + } + + /// Returns `true` if this packet is an IP fragment. + /// + /// A packet is a fragment if MF is set or the fragment offset is non-zero. + /// This is the single-branch fast-path check used by the handler. + #[inline] + pub const fn is_fragment(&self) -> bool { + let combined = u16::from_be_bytes(self.flags_fragment_offset); + (combined & 0x3FFF) != 0 + } + + /// Byte offset from the start of the Ethernet frame to the IP payload. + /// + /// Equal to `sizeof(EthernetFrame) + header_len()`. When IHL == 5 + /// (no options) this compiles to a constant 34. + #[inline] + pub const fn payload_offset(&self) -> usize { + size_of::() + self.header_len() + } + + /// Returns the IP payload length (total_length minus header_len). + /// + /// Returns 0 if total_length < header_len (malformed). + #[inline] + pub const fn payload_len(&self) -> usize { + let total = self.total_length() as usize; + let hdr = self.header_len(); + total.saturating_sub(hdr) + } + + /// Fills `header_checksum` with the correct value. + /// + /// # Precondition + /// + /// When `IHL > 5` (options present), this reads `header_len()` bytes + /// starting from `self`. The caller must ensure the struct is backed + /// by at least `header_len()` bytes of accessible memory. This is + /// always true when obtained via [`from_frame_mut`](Self::from_frame_mut), + /// since the underlying frame memory extends beyond the 20-byte struct. + #[inline] + pub fn fill_checksum(&mut self) { + self.header_checksum = [0, 0]; + let bytes = unsafe { + std::slice::from_raw_parts(self as *const Self as *const u8, self.header_len()) + }; + self.header_checksum = compute_ipv4_checksum(bytes); + } + + /// Zero-copy borrow of the IPv4 header from a received frame. + /// + /// The header starts immediately after the Ethernet header. + /// + /// # Safety + /// + /// The caller must ensure `frame.len() >= IPV4_MIN_FRAME_LEN`. + #[inline] + pub fn from_bytes(frame: &[u8]) -> &Self { + debug_assert!(frame.len() >= IPV4_MIN_FRAME_LEN); + unsafe { &*(frame.as_ptr().add(size_of::()) as *const Self) } + } + + /// Mutable zero-copy borrow of the IPv4 header from a received frame. + /// + /// # Safety + /// + /// The caller must ensure `frame.len() >= IPV4_MIN_FRAME_LEN`. + #[inline] + pub fn from_bytes_mut(frame: &mut [u8]) -> &mut Self { + debug_assert!(frame.len() >= IPV4_MIN_FRAME_LEN); + unsafe { &mut *(frame.as_mut_ptr().add(size_of::()) as *mut Self) } + } +} + +impl Display for Ipv4Header { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Ipv4Header {{ version: {}, ihl: {}, dscp_ecn: {}, total_length: {}, identification: {}, dont_fragment: {}, more_fragments: {}, fragment_offset: {}, ttl: {}, protocol: {}, header_checksum: {:x?}, src_addr: {}, dst_addr: {} }}", + self.version(), + self.ihl(), + self.dscp_ecn, + self.total_length(), + self.identification(), + self.dont_fragment(), + self.more_fragments(), + self.fragment_offset(), + self.ttl, + IpProtocol(self.protocol), + self.header_checksum, + self.src_addr, + self.dst_addr + ) + } +} + +#[cfg(test)] +mod tests { + use crate::net::checksum::verify_ipv4_checksum; + + use super::*; + + fn sample_header() -> Ipv4Header { + Ipv4Header { + version_ihl: 0x45, // version 4, IHL 5 + dscp_ecn: 0x00, + total_length: [0x00, 0x28], // 40 bytes + identification: [0x00, 0x01], + flags_fragment_offset: [0x40, 0x00], // DF set + ttl: 64, + protocol: 17, // UDP + header_checksum: [0x00, 0x00], + src_addr: Ipv4Address::new([192, 168, 1, 1]), + dst_addr: Ipv4Address::new([10, 0, 0, 1]), + } + } + + #[test] + fn header_accessors() { + let hdr = sample_header(); + assert_eq!(hdr.version(), 4); + assert_eq!(hdr.ihl(), 5); + assert_eq!(hdr.header_len(), 20); + assert_eq!(hdr.total_length(), 40); + assert_eq!(hdr.identification(), 1); + assert_eq!(hdr.payload_len(), 20); + assert_eq!(hdr.payload_offset(), 34); // 14 (eth) + 20 (ip) + } + + #[test] + fn header_accessors_with_options() { + let mut hdr = sample_header(); + hdr.version_ihl = 0x46; // IHL 6 = 24 bytes (with options) + hdr.total_length = [0x00, 0x2C]; // 44 bytes + assert_eq!(hdr.ihl(), 6); + assert_eq!(hdr.header_len(), 24); + assert_eq!(hdr.payload_len(), 20); + assert_eq!(hdr.payload_offset(), 38); // 14 + 24 + } + + #[test] + fn fragment_flags_df_only() { + let hdr = sample_header(); // flags = [0x40, 0x00] (DF set) + assert!(hdr.dont_fragment()); + assert!(!hdr.more_fragments()); + assert_eq!(hdr.fragment_offset(), 0); + assert!(!hdr.is_fragment()); + } + + #[test] + fn fragment_flags_mf_set() { + let mut hdr = sample_header(); + hdr.flags_fragment_offset = [0x20, 0x00]; // MF set, offset 0 + assert!(!hdr.dont_fragment()); + assert!(hdr.more_fragments()); + assert_eq!(hdr.fragment_offset(), 0); + assert!(hdr.is_fragment()); + } + + #[test] + fn fragment_flags_offset_nonzero() { + let mut hdr = sample_header(); + hdr.flags_fragment_offset = [0x00, 0x01]; // offset = 1 + assert!(!hdr.dont_fragment()); + assert!(!hdr.more_fragments()); + assert_eq!(hdr.fragment_offset(), 1); + assert!(hdr.is_fragment()); + } + + #[test] + fn payload_len_malformed() { + let mut hdr = sample_header(); + hdr.total_length = [0x00, 0x0A]; // 10 < 20 header + assert_eq!(hdr.payload_len(), 0); + } + + #[test] + fn checksum_compute_and_verify() { + let bytes: [u8; 20] = [ + 0x45, 0x00, 0x00, 0x28, 0x00, 0x01, 0x40, 0x00, 0x40, 0x11, 0x00, 0x00, 0xC0, 0xA8, + 0x01, 0x01, 0x0A, 0x00, 0x00, 0x01, + ]; + let checksum = compute_ipv4_checksum(&bytes); + assert_eq!(checksum, [0x6F, 0x1A]); + + let mut with_checksum = bytes; + with_checksum[10] = checksum[0]; + with_checksum[11] = checksum[1]; + assert!(verify_ipv4_checksum(&with_checksum)); + } + + #[test] + fn fill_checksum_sets_correct_value() { + let mut hdr = sample_header(); + hdr.fill_checksum(); + assert_eq!(hdr.header_checksum, [0x6F, 0x1A]); + } + + #[test] + fn verify_checksum_rejects_bad_header() { + let bytes: [u8; 20] = [ + 0x45, 0x00, 0x00, 0x28, 0x00, 0x01, 0x40, 0x00, 0x40, 0x11, 0xFF, 0xFF, 0xC0, 0xA8, + 0x01, 0x01, 0x0A, 0x00, 0x00, 0x01, + ]; + assert!(!verify_ipv4_checksum(&bytes)); + } + + #[test] + fn header_layout() { + assert_eq!(IPV4_MIN_HEADER_LEN, 20); + assert_eq!(IPV4_MIN_FRAME_LEN, 34); + } +} diff --git a/src/net/wire/ip/v6.rs b/src/net/wire/ip/v6.rs new file mode 100644 index 0000000..9084aef --- /dev/null +++ b/src/net/wire/ip/v6.rs @@ -0,0 +1,346 @@ +use std::fmt::Display; + +use super::{IpProtocol, Ipv6Address, ethernet::EthernetFrame}; + +/// Fixed IPv6 header length in bytes (always 40, no variable-length header). +pub const IPV6_HEADER_LEN: usize = 40; + +/// Compile-time guarantee that our struct matches the wire size. +const _: () = assert!(size_of::() == IPV6_HEADER_LEN); + +/// Minimum Ethernet + IPv6 frame length. +pub const IPV6_MIN_FRAME_LEN: usize = size_of::() + IPV6_HEADER_LEN; + +/// Hop-by-Hop Options extension header. +pub const EXT_HOP_BY_HOP: u8 = 0; +/// Routing extension header. +pub const EXT_ROUTING: u8 = 43; +/// Fragment extension header. +pub const EXT_FRAGMENT: u8 = 44; +/// Authentication Header. +pub const EXT_AH: u8 = 51; +/// Destination Options extension header. +pub const EXT_DESTINATION: u8 = 60; +/// No Next Header. +pub const NO_NEXT_HEADER: u8 = 59; + +/// Fragment extension header length (always 8 bytes). +pub const FRAGMENT_EXT_LEN: usize = 8; + +/// Compile-time guarantee that our struct matches the wire size. +const _: () = assert!(size_of::() == FRAGMENT_EXT_LEN); + +/// IPv6 Fragment Extension Header wire format (8 bytes, RFC 8200 §4.5). +/// +/// `#[repr(C, packed)]` allows zero-copy casting from raw frame memory. +/// +/// Layout: +/// - `next_header` (1 byte): identifies the upper-layer protocol. +/// - `reserved` (1 byte): must be zero. +/// - `fragment_offset_mf` (2 bytes): 13-bit offset in 8-byte units, +/// 2 reserved bits, and 1 MF (More Fragments) bit. +/// - `identification` (4 bytes): unique datagram identifier. +#[repr(C, packed)] +pub struct Ipv6FragmentHeader { + /// Next Header type after reassembly (e.g. 17 for UDP, 6 for TCP). + pub next_header: u8, + /// Reserved field, must be zero on transmit. + pub reserved: u8, + /// Fragment Offset (13 bits) | Reserved (2 bits) | MF (1 bit), network byte order. + pub fragment_offset_mf: [u8; 2], + /// Identification field for reassembly, network byte order. + pub identification: [u8; 4], +} + +impl Ipv6FragmentHeader { + /// Returns the fragment offset in 8-byte units. + #[inline] + pub const fn fragment_offset(&self) -> u16 { + u16::from_be_bytes(self.fragment_offset_mf) >> 3 + } + + /// Returns `true` if the More Fragments (MF) bit is set. + #[inline] + pub const fn more_fragments(&self) -> bool { + self.fragment_offset_mf[1] & 0x01 != 0 + } + + /// Returns the 32-bit identification field. + #[inline] + pub const fn identification(&self) -> u32 { + u32::from_be_bytes(self.identification) + } + + /// Returns `true` if this header represents a fragment. + /// + /// A packet is a fragment if MF is set or the fragment offset is non-zero. + #[inline] + pub const fn is_fragment(&self) -> bool { + let combined = u16::from_be_bytes(self.fragment_offset_mf); + // offset bits (top 13) or MF bit (bottom 1) set + (combined & 0xFFF9) != 0 + } + + /// Sets the fragment_offset_mf field from an offset (in 8-byte units) and MF flag. + #[inline] + pub const fn set_fragment_offset_mf(&mut self, offset_units: u16, more_fragments: bool) { + let mf: u16 = if more_fragments { 1 } else { 0 }; + let value = (offset_units << 3) | mf; + self.fragment_offset_mf = value.to_be_bytes(); + } + + /// Zero-copy borrow from a byte slice at the given offset. + /// + /// # Safety + /// + /// The caller must ensure `bytes.len() >= offset + FRAGMENT_EXT_LEN`. + #[inline] + pub fn from_bytes_at(bytes: &[u8], offset: usize) -> &Self { + debug_assert!(bytes.len() >= offset + FRAGMENT_EXT_LEN); + unsafe { &*(bytes.as_ptr().add(offset) as *const Self) } + } + + /// Mutable zero-copy borrow from a byte slice at the given offset. + /// + /// # Safety + /// + /// The caller must ensure `bytes.len() >= offset + FRAGMENT_EXT_LEN`. + #[inline] + pub fn from_bytes_at_mut(bytes: &mut [u8], offset: usize) -> &mut Self { + debug_assert!(bytes.len() >= offset + FRAGMENT_EXT_LEN); + unsafe { &mut *(bytes.as_mut_ptr().add(offset) as *mut Self) } + } +} + +/// IPv6 fixed header wire format (40 bytes). +/// +/// `#[repr(C, packed)]` allows zero-copy casting from raw frame memory. +/// The first 4 bytes encode version (4 bits), traffic class (8 bits), +/// and flow label (20 bits) in network byte order. +#[repr(C, packed)] +pub struct Ipv6Header { + /// Version (4) + Traffic Class (8) + Flow Label (20), network byte order. + pub version_tc_fl: [u8; 4], + /// Payload length (excludes the 40-byte fixed header), network byte order. + pub payload_length: [u8; 2], + /// Next header protocol number (or extension header type). + pub next_header: u8, + /// Hop limit (analogous to IPv4 TTL). + pub hop_limit: u8, + /// Source IPv6 address. + pub src_addr: Ipv6Address, + /// Destination IPv6 address. + pub dst_addr: Ipv6Address, +} + +impl Ipv6Header { + /// Returns the IP version (should be 6). + #[inline] + pub fn version(&self) -> u8 { + (self.version_tc_fl[0] >> 4) & 0x0F + } + + /// Returns the 8-bit traffic class (DSCP + ECN). + #[inline] + pub fn traffic_class(&self) -> u8 { + ((self.version_tc_fl[0] & 0x0F) << 4) | ((self.version_tc_fl[1] >> 4) & 0x0F) + } + + /// Returns the 20-bit flow label. + #[inline] + pub fn flow_label(&self) -> u32 { + ((self.version_tc_fl[1] as u32 & 0x0F) << 16) + | ((self.version_tc_fl[2] as u32) << 8) + | (self.version_tc_fl[3] as u32) + } + + /// Returns the payload length (bytes after the 40-byte fixed header). + #[inline] + pub fn payload_length(&self) -> u16 { + u16::from_be_bytes(self.payload_length) + } + + /// Zero-copy borrow of the IPv6 header from a received frame. + /// + /// The header starts immediately after the Ethernet header. + /// + /// # Safety + /// + /// The caller must ensure `frame.len() >= IPV6_MIN_FRAME_LEN`. + #[inline] + pub fn from_bytes(bytes: &[u8]) -> &Self { + debug_assert!(bytes.len() >= IPV6_MIN_FRAME_LEN); + unsafe { &*(bytes.as_ptr().add(size_of::()) as *const Self) } + } + + /// Mutable zero-copy borrow of the IPv6 header from a received frame. + /// + /// # Safety + /// + /// The caller must ensure `frame.len() >= IPV6_MIN_FRAME_LEN`. + #[inline] + pub fn from_bytes_mut(bytes: &mut [u8]) -> &mut Self { + debug_assert!(bytes.len() >= IPV6_MIN_FRAME_LEN); + unsafe { &mut *(bytes.as_mut_ptr().add(size_of::()) as *mut Self) } + } +} + +impl Display for Ipv6Header { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Ipv6Header {{ version: {}, traffic_class: {}, flow_label: {}, payload_length: {}, next_header: {}, hop_limit: {}, src_addr: {}, dst_addr: {} }}", + self.version(), + self.traffic_class(), + self.flow_label(), + self.payload_length(), + IpProtocol(self.next_header), + self.hop_limit, + self.src_addr, + self.dst_addr + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn header_accessors_zeroed() { + let hdr = Ipv6Header { + version_tc_fl: [0x60, 0x00, 0x00, 0x00], + payload_length: [0x00, 0x00], + next_header: 59, // No Next Header + hop_limit: 255, + src_addr: Ipv6Address::unspecified(), + dst_addr: Ipv6Address::loopback(), + }; + assert_eq!(hdr.version(), 6); + assert_eq!(hdr.traffic_class(), 0); + assert_eq!(hdr.flow_label(), 0); + assert_eq!(hdr.payload_length(), 0); + } + + #[test] + fn header_accessors_with_tc_and_fl() { + // version=6, tc=0xAB, fl=0xCDEF0 + // byte[0] = 0110_1010 = 0x6A + // byte[1] = 1011_1100 = 0xBC + // byte[2] = 1101_1110 = 0xDE + // byte[3] = 1111_0000 = 0xF0 + let hdr = Ipv6Header { + version_tc_fl: [0x6A, 0xBC, 0xDE, 0xF0], + payload_length: [0x00, 0x20], // 32 + next_header: 17, + hop_limit: 64, + src_addr: Ipv6Address::unspecified(), + dst_addr: Ipv6Address::loopback(), + }; + assert_eq!(hdr.version(), 6); + assert_eq!(hdr.traffic_class(), 0xAB); + assert_eq!(hdr.flow_label(), 0xCDEF0); + assert_eq!(hdr.payload_length(), 32); + } + + #[test] + fn header_layout() { + assert_eq!(IPV6_HEADER_LEN, 40); + assert_eq!(IPV6_MIN_FRAME_LEN, 54); + } + + #[test] + fn fragment_header_layout() { + assert_eq!(FRAGMENT_EXT_LEN, 8); + assert_eq!(size_of::(), 8); + } + + #[test] + fn fragment_header_offset_zero_mf_set() { + let hdr = Ipv6FragmentHeader { + next_header: 17, + reserved: 0, + fragment_offset_mf: [0x00, 0x01], // offset=0, MF=1 + identification: [0x00, 0x00, 0x00, 0x42], + }; + assert_eq!(hdr.fragment_offset(), 0); + assert!(hdr.more_fragments()); + assert_eq!(hdr.identification(), 0x42); + assert!(hdr.is_fragment()); + } + + #[test] + fn fragment_header_offset_nonzero_mf_clear() { + let hdr = Ipv6FragmentHeader { + next_header: 17, + reserved: 0, + fragment_offset_mf: [0x00, 0x08], // offset=1 (1<<3=8), MF=0 + identification: [0x00, 0x00, 0x01, 0x00], + }; + assert_eq!(hdr.fragment_offset(), 1); + assert!(!hdr.more_fragments()); + assert_eq!(hdr.identification(), 256); + assert!(hdr.is_fragment()); + } + + #[test] + fn fragment_header_not_fragment() { + let hdr = Ipv6FragmentHeader { + next_header: 17, + reserved: 0, + fragment_offset_mf: [0x00, 0x00], // offset=0, MF=0 + identification: [0x00, 0x00, 0x00, 0x01], + }; + assert_eq!(hdr.fragment_offset(), 0); + assert!(!hdr.more_fragments()); + assert!(!hdr.is_fragment()); + } + + #[test] + fn fragment_header_set_offset_mf() { + let mut hdr = Ipv6FragmentHeader { + next_header: 17, + reserved: 0, + fragment_offset_mf: [0, 0], + identification: [0; 4], + }; + + hdr.set_fragment_offset_mf(185, true); // offset=185, MF=1 + assert_eq!(hdr.fragment_offset(), 185); + assert!(hdr.more_fragments()); + + hdr.set_fragment_offset_mf(370, false); // offset=370, MF=0 + assert_eq!(hdr.fragment_offset(), 370); + assert!(!hdr.more_fragments()); + } + + #[test] + fn fragment_header_from_bytes() { + let mut buf = [0u8; 16]; + buf[4] = 17; // next_header + buf[6] = 0x05; // fragment_offset_mf high + buf[7] = 0xC9; // fragment_offset_mf low: offset = 0x05C9>>3 = 185, MF=1 + buf[8..12].copy_from_slice(&42u32.to_be_bytes()); + + let hdr = Ipv6FragmentHeader::from_bytes_at(&buf, 4); + assert_eq!(hdr.next_header, 17); + assert_eq!(hdr.fragment_offset(), 185); + assert!(hdr.more_fragments()); + assert_eq!(hdr.identification(), 42); + } + + #[test] + fn fragment_header_from_bytes_mut() { + let mut buf = [0u8; 16]; + let hdr = Ipv6FragmentHeader::from_bytes_at_mut(&mut buf, 4); + hdr.next_header = 6; + hdr.set_fragment_offset_mf(100, false); + hdr.identification = 999u32.to_be_bytes(); + + let hdr = Ipv6FragmentHeader::from_bytes_at(&buf, 4); + assert_eq!(hdr.next_header, 6); + assert_eq!(hdr.fragment_offset(), 100); + assert!(!hdr.more_fragments()); + assert_eq!(hdr.identification(), 999); + } +} diff --git a/src/net/wire/mod.rs b/src/net/wire/mod.rs new file mode 100644 index 0000000..4c0768c --- /dev/null +++ b/src/net/wire/mod.rs @@ -0,0 +1,8 @@ +pub mod arp; +pub mod ethernet; +pub mod icmpv4; +pub mod icmpv6; +pub mod ip; +pub mod ndp; +pub mod tcp; +pub mod udp; diff --git a/src/net/wire/ndp.rs b/src/net/wire/ndp.rs new file mode 100644 index 0000000..7c15a34 --- /dev/null +++ b/src/net/wire/ndp.rs @@ -0,0 +1,152 @@ +use super::{ + ethernet::{EthernetFrame, MacAddress}, + ip::{Ipv6Address, Ipv6Header}, +}; + +/// NDP Neighbor Solicitation ICMPv6 message with Source Link-Layer Address option. +/// +/// Layout (32 bytes total): +/// - ICMPv6 type (1) + code (1) + checksum (2) + reserved (4) +/// - Target address (16) +/// - Source Link-Layer Address option: type (1) + length (1) + MAC (6) +#[repr(C, packed)] +pub struct NdpNsMessage { + pub icmp_type: u8, + pub code: u8, + pub checksum: [u8; 2], + pub reserved: [u8; 4], + pub target: Ipv6Address, + pub opt_type: u8, + pub opt_len: u8, + pub opt_mac: MacAddress, +} + +const _: () = assert!(size_of::() == 32); + +impl NdpNsMessage { + pub fn as_bytes(&self) -> &[u8] { + unsafe { std::slice::from_raw_parts(self as *const Self as *const u8, size_of::()) } + } +} + +/// Complete NDP Neighbor Solicitation frame (Ethernet + IPv6 + NS message). +#[repr(C, packed)] +pub struct NdpNsFrame { + pub ethernet: EthernetFrame, + pub ipv6: Ipv6Header, + pub ns: NdpNsMessage, +} + +impl NdpNsFrame { + #[inline(always)] + pub fn as_bytes(&self) -> &[u8] { + unsafe { std::slice::from_raw_parts(self as *const Self as *const u8, size_of::()) } + } + + #[inline(always)] + pub fn from_bytes_mut(bytes: &mut [u8]) -> &mut Self { + debug_assert!(bytes.len() >= NDP_NS_FRAME_LEN); + unsafe { &mut *(bytes.as_mut_ptr() as *mut Self) } + } +} + +/// Ethernet header (14) + IPv6 header (40) + ICMPv6 NS message (32) = 86 bytes. +pub const NDP_NS_FRAME_LEN: usize = size_of::(); +const _: () = assert!(NDP_NS_FRAME_LEN == 86); + +/// NDP Neighbor Advertisement ICMPv6 message with Target Link-Layer Address option. +/// +/// Layout (32 bytes total): +/// - ICMPv6 type (1) + code (1) + checksum (2) + flags (4) +/// - Target address (16) +/// - Target Link-Layer Address option: type (1) + length (1) + MAC (6) +#[repr(C, packed)] +pub struct NdpNaMessage { + pub icmp_type: u8, + pub code: u8, + pub checksum: [u8; 2], + pub flags: [u8; 4], + pub target: Ipv6Address, + pub opt_type: u8, + pub opt_len: u8, + pub opt_mac: MacAddress, +} + +const _: () = assert!(size_of::() == 32); + +impl NdpNaMessage { + pub fn as_bytes(&self) -> &[u8] { + unsafe { std::slice::from_raw_parts(self as *const Self as *const u8, size_of::()) } + } +} + +/// Complete NDP Neighbor Advertisement frame (Ethernet + IPv6 + NA message). +#[repr(C, packed)] +pub struct NdpNaFrame { + pub ethernet: EthernetFrame, + pub ipv6: Ipv6Header, + pub na: NdpNaMessage, +} + +impl NdpNaFrame { + #[inline(always)] + pub fn as_bytes(&self) -> &[u8] { + unsafe { std::slice::from_raw_parts(self as *const Self as *const u8, size_of::()) } + } + + #[inline(always)] + pub fn from_bytes_mut(bytes: &mut [u8]) -> &mut Self { + debug_assert!(bytes.len() >= NDP_NA_FRAME_LEN); + unsafe { &mut *(bytes.as_mut_ptr() as *mut Self) } + } +} + +/// Ethernet header (14) + IPv6 header (40) + ICMPv6 NA message (32) = 86 bytes. +pub const NDP_NA_FRAME_LEN: usize = size_of::(); +const _: () = assert!(NDP_NA_FRAME_LEN == 86); + +/// Minimum NDP message body length: 8 (ICMPv6 header) + 16 (target address). +pub const NDP_MIN_NS_NA_LEN: usize = 24; + +/// Minimum Router Advertisement body length: 8 (ICMPv6 header) + +/// 4 (cur hop limit + flags + router lifetime) + 4 (reachable time) + 4 (retrans timer). +pub const NDP_MIN_RA_LEN: usize = 16; + +/// IPv6 all-nodes link-local multicast address (ff02::1). +pub const ALL_NODES_MULTICAST: Ipv6Address = + Ipv6Address::new([0xFF, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + +#[cfg(test)] +mod tests { + use crate::net::wire::ip::IPV6_HEADER_LEN; + + use super::*; + + #[test] + fn ndp_ns_message_layout() { + assert_eq!(size_of::(), 32); + } + + #[test] + fn ndp_ns_frame_layout() { + assert_eq!(size_of::(), 86); + assert_eq!( + NDP_NS_FRAME_LEN, + size_of::() + IPV6_HEADER_LEN + 32 + ); + } + + #[test] + fn ndp_na_message_layout() { + assert_eq!(size_of::(), 32); + } + + #[test] + fn ndp_na_frame_layout() { + assert_eq!(size_of::(), 86); + assert_eq!( + NDP_NA_FRAME_LEN, + size_of::() + IPV6_HEADER_LEN + 32 + ); + } +} diff --git a/src/net/wire/tcp.rs b/src/net/wire/tcp.rs new file mode 100644 index 0000000..4f0b7fc --- /dev/null +++ b/src/net/wire/tcp.rs @@ -0,0 +1,740 @@ +use std::fmt::Display; + +/// TCP header length in bytes (minimum, without options). +pub const TCP_HEADER_LEN: usize = 20; + +/// Compile-time guarantee that our struct matches the wire size. +const _: () = assert!(size_of::() == TCP_HEADER_LEN); + +/// TCP flag constants. +pub mod flags { + pub const FIN: u8 = 0x01; + pub const SYN: u8 = 0x02; + pub const RST: u8 = 0x04; + pub const PSH: u8 = 0x08; + pub const ACK: u8 = 0x10; + pub const URG: u8 = 0x20; + pub const ECE: u8 = 0x40; + pub const CWR: u8 = 0x80; +} + +/// TCP option kind constants. +pub mod options { + pub const END: u8 = 0; + pub const NOP: u8 = 1; + pub const MSS: u8 = 2; + pub const WINDOW_SCALE: u8 = 3; + pub const SACK_PERMITTED: u8 = 4; + pub const SACK: u8 = 5; + pub const TIMESTAMP: u8 = 8; +} + +/// TCP header wire format (20 bytes, minimum). +/// +/// `#[repr(C, packed)]` allows zero-copy casting from raw frame memory. +/// +/// Multi-byte fields are stored in network byte order as `[u8; N]` to +/// avoid alignment issues on packed structs. Use the accessor methods +/// for host-order values. +#[derive(Debug)] +#[repr(C, packed)] +pub struct TcpHeader { + pub src_port: [u8; 2], + pub dst_port: [u8; 2], + pub seq_num: [u8; 4], + pub ack_num: [u8; 4], + pub data_offset_reserved: u8, + pub flags: u8, + pub window: [u8; 2], + pub checksum: [u8; 2], + pub urgent_ptr: [u8; 2], +} + +impl TcpHeader { + /// Create a TCP header from host-order values. + #[inline] + pub fn new( + src_port: u16, + dst_port: u16, + seq_num: u32, + ack_num: u32, + data_offset: u8, + flags: u8, + window: u16, + checksum: [u8; 2], + urgent_ptr: u16, + ) -> Self { + TcpHeader { + src_port: src_port.to_be_bytes(), + dst_port: dst_port.to_be_bytes(), + seq_num: seq_num.to_be_bytes(), + ack_num: ack_num.to_be_bytes(), + data_offset_reserved: data_offset << 4, + flags, + window: window.to_be_bytes(), + checksum, + urgent_ptr: urgent_ptr.to_be_bytes(), + } + } + + /// Returns the source port in host byte order. + #[inline] + pub fn src_port(&self) -> u16 { + u16::from_be_bytes(self.src_port) + } + + /// Returns the destination port in host byte order. + #[inline] + pub fn dst_port(&self) -> u16 { + u16::from_be_bytes(self.dst_port) + } + + /// Returns the sequence number in host byte order. + #[inline] + pub fn seq_num(&self) -> u32 { + u32::from_be_bytes(self.seq_num) + } + + /// Returns the acknowledgment number in host byte order. + #[inline] + pub fn ack_num(&self) -> u32 { + u32::from_be_bytes(self.ack_num) + } + + /// Returns the data offset (number of 32-bit words in the header). + #[inline] + pub fn data_offset(&self) -> u8 { + self.data_offset_reserved >> 4 + } + + /// Returns the header length in bytes (data_offset * 4). + #[inline] + pub fn header_len(&self) -> usize { + (self.data_offset() as usize) * 4 + } + + /// Returns the flags byte. + #[inline] + pub fn flags(&self) -> u8 { + self.flags + } + + /// Returns true if the given flag bit(s) are set. + #[inline] + pub fn has_flag(&self, flag: u8) -> bool { + self.flags & flag == flag + } + + /// Returns the window size in host byte order. + #[inline] + pub fn window(&self) -> u16 { + u16::from_be_bytes(self.window) + } + + /// Returns the urgent pointer in host byte order. + #[inline] + pub fn urgent_ptr(&self) -> u16 { + u16::from_be_bytes(self.urgent_ptr) + } + + /// Zero-copy reference to a TCP header at `offset` within a frame. + /// + /// # Safety + /// Caller must ensure `frame.len() >= offset + TCP_HEADER_LEN`. + #[inline] + pub unsafe fn from_bytes_at(bytes: &[u8], offset: usize) -> &Self { + debug_assert!(offset + TCP_HEADER_LEN <= bytes.len()); + unsafe { &*(bytes.as_ptr().add(offset) as *const Self) } + } + + /// Zero-copy mutable reference to a TCP header at `offset` within a frame. + /// + /// # Safety + /// Caller must ensure `frame.len() >= offset + TCP_HEADER_LEN`. + #[inline] + pub unsafe fn from_bytes_mut_at(bytes: &mut [u8], offset: usize) -> &mut Self { + debug_assert!(offset + TCP_HEADER_LEN <= bytes.len()); + unsafe { &mut *(bytes.as_mut_ptr().add(offset) as *mut Self) } + } +} + +impl Display for TcpHeader { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "TcpHeader {{ src_port: {}, dst_port: {}, seq_num: {}, ack_num: {}, data_offset: {}, flags: {}, window: {}, checksum: {}, urgent_ptr: {} }}", + self.src_port(), + self.dst_port(), + self.seq_num(), + self.ack_num(), + self.data_offset(), + self.flags(), + self.window(), + u16::from_be_bytes(self.checksum), + self.urgent_ptr(), + ) + } +} + +/// Parse MSS (Maximum Segment Size) from TCP options. +/// +/// Scans the options bytes for Kind=2, Len=4 and returns the MSS value. +/// Returns `None` if MSS option is not present or malformed. +pub fn parse_mss(options: &[u8]) -> Option { + let mut i = 0; + while i < options.len() { + match options[i] { + options::END => break, + options::NOP => { + i += 1; + } + options::MSS => { + if i + 4 > options.len() { + return None; + } + if options[i + 1] != 4 { + return None; + } + return Some(u16::from_be_bytes([options[i + 2], options[i + 3]])); + } + _ => { + // Unknown option — skip using length byte. + if i + 1 >= options.len() { + return None; + } + let len = options[i + 1] as usize; + if len < 2 || i + len > options.len() { + return None; + } + i += len; + } + } + } + None +} + +/// Write a 4-byte MSS option into `buf`. +/// +/// Returns the number of bytes written (4). +/// +/// # Panics +/// Panics if `buf.len() < 4`. +pub fn write_mss_option(buf: &mut [u8], mss: u16) -> usize { + buf[0] = options::MSS; + buf[1] = 4; + buf[2..4].copy_from_slice(&mss.to_be_bytes()); + 4 +} + +/// Parse Window Scale option from TCP options. +/// +/// Scans the options bytes for Kind=3, Len=3 and returns the shift count. +/// Returns `None` if the option is not present or malformed. +pub fn parse_window_scale(options: &[u8]) -> Option { + let mut i = 0; + while i < options.len() { + match options[i] { + options::END => break, + options::NOP => { + i += 1; + } + options::WINDOW_SCALE => { + if i + 3 > options.len() { + return None; + } + if options[i + 1] != 3 { + return None; + } + // RFC 7323: shift count capped at 14 + return Some(options[i + 2].min(14)); + } + _ => { + if i + 1 >= options.len() { + return None; + } + let len = options[i + 1] as usize; + if len < 2 || i + len > options.len() { + return None; + } + i += len; + } + } + } + None +} + +/// Write a 3-byte Window Scale option into `buf`. +/// +/// Returns the number of bytes written (3). +/// +/// # Panics +/// Panics if `buf.len() < 3`. +pub fn write_window_scale_option(buf: &mut [u8], shift: u8) -> usize { + buf[0] = options::WINDOW_SCALE; + buf[1] = 3; + buf[2] = shift; + 3 +} + +/// Parse Timestamp option (Kind=8, Len=10). +/// Returns (TSval, TSecr) or None if not present. +pub fn parse_timestamp(options: &[u8]) -> Option<(u32, u32)> { + let mut i = 0; + while i < options.len() { + match options[i] { + options::END => break, + options::NOP => { + i += 1; + } + options::TIMESTAMP => { + if i + 10 > options.len() { + return None; + } + if options[i + 1] != 10 { + return None; + } + let tsval = u32::from_be_bytes([ + options[i + 2], + options[i + 3], + options[i + 4], + options[i + 5], + ]); + let tsecr = u32::from_be_bytes([ + options[i + 6], + options[i + 7], + options[i + 8], + options[i + 9], + ]); + return Some((tsval, tsecr)); + } + _ => { + if i + 1 >= options.len() { + return None; + } + let len = options[i + 1] as usize; + if len < 2 || i + len > options.len() { + return None; + } + i += len; + } + } + } + None +} + +/// Write a 10-byte Timestamp option into `buf`. +/// +/// Returns the number of bytes written (10). +/// +/// # Panics +/// Panics if `buf.len() < 10`. +pub fn write_timestamp_option(buf: &mut [u8], tsval: u32, tsecr: u32) -> usize { + buf[0] = options::TIMESTAMP; + buf[1] = 10; + buf[2..6].copy_from_slice(&tsval.to_be_bytes()); + buf[6..10].copy_from_slice(&tsecr.to_be_bytes()); + 10 +} + +/// Check if SACK Permitted option (Kind=4, Len=2) is present. +pub fn parse_sack_permitted(options: &[u8]) -> bool { + let mut i = 0; + while i < options.len() { + match options[i] { + options::END => break, + options::NOP => { + i += 1; + } + options::SACK_PERMITTED => { + if i + 2 > options.len() { + return false; + } + if options[i + 1] != 2 { + return false; + } + return true; + } + _ => { + if i + 1 >= options.len() { + return false; + } + let len = options[i + 1] as usize; + if len < 2 || i + len > options.len() { + return false; + } + i += len; + } + } + } + false +} + +/// Write a 2-byte SACK Permitted option into `buf`. +/// +/// Returns the number of bytes written (2). +/// +/// # Panics +/// Panics if `buf.len() < 2`. +pub fn write_sack_permitted_option(buf: &mut [u8]) -> usize { + buf[0] = options::SACK_PERMITTED; + buf[1] = 2; + 2 +} + +/// Parse SACK blocks (Kind=5, variable length). +/// Returns up to 4 blocks as (left_edge, right_edge) pairs and the count. +pub fn parse_sack_blocks(options: &[u8]) -> ([Option<(u32, u32)>; 4], usize) { + let mut blocks = [None; 4]; + let mut i = 0; + while i < options.len() { + match options[i] { + options::END => break, + options::NOP => { + i += 1; + } + options::SACK => { + if i + 2 > options.len() { + return (blocks, 0); + } + let len = options[i + 1] as usize; + if len < 2 || i + len > options.len() { + return (blocks, 0); + } + let data_len = len - 2; + let num_blocks = data_len / 8; + let count = num_blocks.min(4); + for (b, block) in blocks.iter_mut().take(count).enumerate() { + let off = i + 2 + b * 8; + let left = u32::from_be_bytes([ + options[off], + options[off + 1], + options[off + 2], + options[off + 3], + ]); + let right = u32::from_be_bytes([ + options[off + 4], + options[off + 5], + options[off + 6], + options[off + 7], + ]); + *block = Some((left, right)); + } + return (blocks, count); + } + _ => { + if i + 1 >= options.len() { + return (blocks, 0); + } + let len = options[i + 1] as usize; + if len < 2 || i + len > options.len() { + return (blocks, 0); + } + i += len; + } + } + } + (blocks, 0) +} + +/// Write SACK blocks option into `buf`. Returns bytes written (2 + 8*N). +/// +/// # Panics +/// Panics if `buf` is too small for the blocks. +pub fn write_sack_option(buf: &mut [u8], blocks: &[(u32, u32)]) -> usize { + let count = blocks.len().min(4); + let len = 2 + count * 8; + buf[0] = options::SACK; + buf[1] = len as u8; + for (i, &(left, right)) in blocks.iter().take(count).enumerate() { + let off = 2 + i * 8; + buf[off..off + 4].copy_from_slice(&left.to_be_bytes()); + buf[off + 4..off + 8].copy_from_slice(&right.to_be_bytes()); + } + len +} + +/// Sequence number less-than comparison with wraparound. +#[inline] +pub fn seq_lt(a: u32, b: u32) -> bool { + (a.wrapping_sub(b) as i32) < 0 +} + +/// Sequence number less-than-or-equal comparison with wraparound. +#[inline] +pub fn seq_le(a: u32, b: u32) -> bool { + (a.wrapping_sub(b) as i32) <= 0 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn header_layout() { + assert_eq!(TCP_HEADER_LEN, 20); + assert_eq!(size_of::(), 20); + } + + #[test] + fn header_accessors() { + let hdr = TcpHeader::new( + 0x1234, // src_port + 0x0050, // dst_port (80) + 0xAABBCCDD, + 0x11223344, + 5, // data_offset (20 bytes) + flags::SYN | flags::ACK, + 0x7FFF, + [0x00, 0x00], + 0, + ); + assert_eq!(hdr.src_port(), 0x1234); + assert_eq!(hdr.dst_port(), 0x0050); + assert_eq!(hdr.seq_num(), 0xAABBCCDD); + assert_eq!(hdr.ack_num(), 0x11223344); + assert_eq!(hdr.data_offset(), 5); + assert_eq!(hdr.header_len(), 20); + assert!(hdr.has_flag(flags::SYN)); + assert!(hdr.has_flag(flags::ACK)); + assert!(!hdr.has_flag(flags::FIN)); + assert!(!hdr.has_flag(flags::RST)); + assert_eq!(hdr.window(), 0x7FFF); + assert_eq!(hdr.urgent_ptr(), 0); + } + + #[test] + fn data_offset_with_options() { + // data_offset = 8 means 32 bytes header (12 bytes of options) + let hdr = TcpHeader::new(0, 0, 0, 0, 8, 0, 0, [0; 2], 0); + assert_eq!(hdr.data_offset(), 8); + assert_eq!(hdr.header_len(), 32); + } + + #[test] + fn flag_helpers() { + let hdr = TcpHeader::new(0, 0, 0, 0, 5, flags::FIN | flags::PSH, 0, [0; 2], 0); + assert!(hdr.has_flag(flags::FIN)); + assert!(hdr.has_flag(flags::PSH)); + assert!(hdr.has_flag(flags::FIN | flags::PSH)); + assert!(!hdr.has_flag(flags::SYN)); + assert!(!hdr.has_flag(flags::ACK)); + assert!(!hdr.has_flag(flags::RST)); + } + + #[test] + fn parse_mss_valid() { + let opts = [options::MSS, 4, 0x05, 0xB4]; // MSS = 1460 + assert_eq!(parse_mss(&opts), Some(1460)); + } + + #[test] + fn parse_mss_absent() { + let opts = [options::NOP, options::NOP, options::END]; + assert_eq!(parse_mss(&opts), None); + } + + #[test] + fn parse_mss_empty() { + assert_eq!(parse_mss(&[]), None); + } + + #[test] + fn parse_mss_after_nop() { + let opts = [options::NOP, options::MSS, 4, 0x02, 0x04]; // MSS = 516 + assert_eq!(parse_mss(&opts), Some(516)); + } + + #[test] + fn parse_mss_malformed_length() { + // MSS option with wrong length byte + let opts = [options::MSS, 3, 0x05, 0xB4]; + assert_eq!(parse_mss(&opts), None); + } + + #[test] + fn parse_mss_truncated() { + let opts = [options::MSS, 4, 0x05]; // missing last byte + assert_eq!(parse_mss(&opts), None); + } + + #[test] + fn write_mss_option_test() { + let mut buf = [0u8; 4]; + let written = write_mss_option(&mut buf, 1460); + assert_eq!(written, 4); + assert_eq!(buf, [options::MSS, 4, 0x05, 0xB4]); + } + + #[test] + fn parse_window_scale_valid() { + let opts = [options::WINDOW_SCALE, 3, 7]; + assert_eq!(parse_window_scale(&opts), Some(7)); + } + + #[test] + fn parse_window_scale_capped_at_14() { + let opts = [options::WINDOW_SCALE, 3, 20]; + assert_eq!(parse_window_scale(&opts), Some(14)); + } + + #[test] + fn parse_window_scale_absent() { + let opts = [options::MSS, 4, 0x05, 0xB4]; + assert_eq!(parse_window_scale(&opts), None); + } + + #[test] + fn parse_window_scale_after_mss() { + let opts = [ + options::MSS, + 4, + 0x05, + 0xB4, + options::NOP, + options::WINDOW_SCALE, + 3, + 7, + ]; + assert_eq!(parse_window_scale(&opts), Some(7)); + } + + #[test] + fn write_window_scale_option_test() { + let mut buf = [0u8; 3]; + let written = write_window_scale_option(&mut buf, 7); + assert_eq!(written, 3); + assert_eq!(buf, [options::WINDOW_SCALE, 3, 7]); + } + + #[test] + fn seq_lt_basic() { + assert!(seq_lt(1, 2)); + assert!(!seq_lt(2, 1)); + assert!(!seq_lt(1, 1)); + } + + #[test] + fn seq_le_basic() { + assert!(seq_le(1, 2)); + assert!(!seq_le(2, 1)); + assert!(seq_le(1, 1)); + } + + #[test] + fn seq_lt_wraparound() { + // Near max wrapping to near zero. + assert!(seq_lt(u32::MAX - 1, u32::MAX)); + assert!(seq_lt(u32::MAX, 0)); + assert!(seq_lt(u32::MAX, 1)); + assert!(!seq_lt(1, u32::MAX)); + } + + #[test] + fn seq_le_wraparound() { + assert!(seq_le(u32::MAX, 0)); + assert!(seq_le(u32::MAX, u32::MAX)); + assert!(seq_le(0, 0)); + } + + #[test] + fn seq_lt_half_space() { + // Values in the lower half of the sequence space are "less than" + // values in the upper half when they differ by ~2^31. + let a = 0x80000000u32; + let b = 0x80000001u32; + assert!(seq_lt(a, b)); + assert!(!seq_lt(b, a)); + } + + #[test] + fn parse_timestamp_valid() { + let mut opts = [0u8; 10]; + opts[0] = options::TIMESTAMP; + opts[1] = 10; + opts[2..6].copy_from_slice(&12345u32.to_be_bytes()); + opts[6..10].copy_from_slice(&67890u32.to_be_bytes()); + assert_eq!(parse_timestamp(&opts), Some((12345, 67890))); + } + + #[test] + fn parse_timestamp_absent() { + let opts = [options::MSS, 4, 0x05, 0xB4]; + assert_eq!(parse_timestamp(&opts), None); + } + + #[test] + fn write_timestamp_roundtrip() { + let mut buf = [0u8; 10]; + let written = write_timestamp_option(&mut buf, 12345, 67890); + assert_eq!(written, 10); + assert_eq!(parse_timestamp(&buf), Some((12345, 67890))); + } + + #[test] + fn parse_sack_permitted_valid() { + let opts = [options::SACK_PERMITTED, 2]; + assert!(parse_sack_permitted(&opts)); + } + + #[test] + fn parse_sack_permitted_absent() { + let opts = [options::MSS, 4, 0x05, 0xB4]; + assert!(!parse_sack_permitted(&opts)); + } + + #[test] + fn write_sack_permitted_roundtrip() { + let mut buf = [0u8; 2]; + let written = write_sack_permitted_option(&mut buf); + assert_eq!(written, 2); + assert!(parse_sack_permitted(&buf)); + } + + #[test] + fn parse_sack_blocks_two_blocks() { + let mut opts = [0u8; 18]; + opts[0] = options::SACK; + opts[1] = 18; + opts[2..6].copy_from_slice(&100u32.to_be_bytes()); + opts[6..10].copy_from_slice(&200u32.to_be_bytes()); + opts[10..14].copy_from_slice(&300u32.to_be_bytes()); + opts[14..18].copy_from_slice(&400u32.to_be_bytes()); + let (blocks, count) = parse_sack_blocks(&opts); + assert_eq!(count, 2); + assert_eq!(blocks[0], Some((100, 200))); + assert_eq!(blocks[1], Some((300, 400))); + } + + #[test] + fn write_sack_blocks_roundtrip() { + let mut buf = [0u8; 34]; + let sack_blocks = [(100, 200), (300, 400)]; + let written = write_sack_option(&mut buf, &sack_blocks); + assert_eq!(written, 18); + let (blocks, count) = parse_sack_blocks(&buf[..written]); + assert_eq!(count, 2); + assert_eq!(blocks[0], Some((100, 200))); + assert_eq!(blocks[1], Some((300, 400))); + } + + #[test] + fn parse_mixed_options() { + let mut opts = [0u8; 24]; + let mut i = 0; + i += write_mss_option(&mut opts[i..], 1460); + opts[i] = options::NOP; + i += 1; + i += write_window_scale_option(&mut opts[i..], 7); + opts[i] = options::NOP; + i += 1; + opts[i] = options::NOP; + i += 1; + i += write_timestamp_option(&mut opts[i..], 1000, 2000); + i += write_sack_permitted_option(&mut opts[i..]); + + assert_eq!(parse_mss(&opts[..i]), Some(1460)); + assert_eq!(parse_window_scale(&opts[..i]), Some(7)); + assert_eq!(parse_timestamp(&opts[..i]), Some((1000, 2000))); + assert!(parse_sack_permitted(&opts[..i])); + } +} diff --git a/src/net/wire/udp.rs b/src/net/wire/udp.rs new file mode 100644 index 0000000..1b389cc --- /dev/null +++ b/src/net/wire/udp.rs @@ -0,0 +1,129 @@ +/// UDP header length in bytes. +pub const UDP_HEADER_LEN: usize = 8; + +/// Compile-time guarantee that our struct matches the wire size. +const _: () = assert!(size_of::() == UDP_HEADER_LEN); + +/// UDP header wire format (8 bytes). +/// +/// `#[repr(C, packed)]` allows zero-copy casting from raw frame memory. +/// +/// Multi-byte fields are stored in network byte order as `[u8; 2]` to +/// avoid alignment issues on packed structs. Use the accessor methods +/// for host-order values. +#[repr(C, packed)] +pub struct UdpHeader { + pub src_port: [u8; 2], + pub dst_port: [u8; 2], + pub length: [u8; 2], + pub checksum: [u8; 2], +} + +impl UdpHeader { + /// Create a UDP header from host-order values. + /// + /// Multi-byte fields are converted to network byte order internally. + #[inline] + pub fn new(src_port: u16, dst_port: u16, length: u16, checksum: [u8; 2]) -> Self { + UdpHeader { + src_port: src_port.to_be_bytes(), + dst_port: dst_port.to_be_bytes(), + length: length.to_be_bytes(), + checksum, + } + } + + /// Returns the source port in host byte order. + #[inline] + pub const fn src_port(&self) -> u16 { + u16::from_be_bytes(self.src_port) + } + + /// Returns the destination port in host byte order. + #[inline] + pub const fn dst_port(&self) -> u16 { + u16::from_be_bytes(self.dst_port) + } + + /// Returns the UDP length (header + payload) in host byte order. + #[inline] + pub const fn length(&self) -> u16 { + u16::from_be_bytes(self.length) + } + + /// Returns the payload length (total length minus header). + /// + /// Returns 0 if the length field is less than the header size (malformed). + #[inline] + pub const fn payload_len(&self) -> usize { + let total = self.length() as usize; + total.saturating_sub(UDP_HEADER_LEN) + } + + /// Zero-copy reference to a UDP header at `offset` within a frame. + /// + /// # Safety + /// Caller must ensure `frame.len() >= offset + UDP_HEADER_LEN`. + #[inline] + pub unsafe fn from_bytes_at(bytes: &[u8], offset: usize) -> &Self { + debug_assert!(offset + UDP_HEADER_LEN <= bytes.len()); + unsafe { &*(bytes.as_ptr().add(offset) as *const Self) } + } + + /// Mutable zero-copy reference to a UDP header at `offset` within a frame. + /// + /// # Safety + /// Caller must ensure `frame.len() >= offset + UDP_HEADER_LEN`. + #[inline] + pub unsafe fn from_bytes_at_mut(bytes: &mut [u8], offset: usize) -> &mut Self { + debug_assert!(offset + UDP_HEADER_LEN <= bytes.len()); + unsafe { &mut *(bytes.as_mut_ptr().add(offset) as *mut Self) } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn header_accessors() { + let hdr = UdpHeader { + src_port: [0x12, 0x34], + dst_port: [0x00, 0x35], + length: [0x00, 0x0C], // 12 + checksum: [0x00, 0x00], + }; + assert_eq!(hdr.src_port(), 0x1234); + assert_eq!(hdr.dst_port(), 53); + assert_eq!(hdr.length(), 12); + assert_eq!(hdr.payload_len(), 4); + } + + #[test] + fn payload_len_malformed() { + let hdr = UdpHeader { + src_port: [0; 2], + dst_port: [0; 2], + length: [0x00, 0x04], // 4 < 8 header + checksum: [0; 2], + }; + assert_eq!(hdr.payload_len(), 0); + } + + #[test] + fn payload_len_exact_header() { + let hdr = UdpHeader { + src_port: [0; 2], + dst_port: [0; 2], + length: [0x00, 0x08], // exactly header size + checksum: [0; 2], + }; + assert_eq!(hdr.payload_len(), 0); + } + + #[test] + fn udp_header_layout() { + assert_eq!(UDP_HEADER_LEN, 8); + assert_eq!(size_of::(), 8); + } +} diff --git a/src/rt/affinity.rs b/src/rt/affinity.rs new file mode 100644 index 0000000..a109364 --- /dev/null +++ b/src/rt/affinity.rs @@ -0,0 +1,6 @@ +pub fn pin_core(queue: u32) { + let core_ids = core_affinity::get_core_ids().unwrap(); + let core_id = *core_ids.get(queue as usize).unwrap(); + + core_affinity::set_for_current(core_id); +} diff --git a/src/rt/context.rs b/src/rt/context.rs new file mode 100644 index 0000000..c59c6a6 --- /dev/null +++ b/src/rt/context.rs @@ -0,0 +1,75 @@ +use std::{ + cell::{Cell, UnsafeCell}, + rc::Rc, +}; + +use crate::{ + net::{ + handler::{tcp::TcpHandler, udp::UdpHandler}, + {NeighborHandler, PmtuCache}, + }, + xdp::frame::SharedFrameBuffer, +}; + +pub struct ContextDropGuard; + +impl ContextDropGuard { + pub fn new<'umem>(ctx: RuntimeContext<'umem>) -> Self { + let ctx = Box::into_raw(Box::new(ctx)); + RT_CTX.with(|c| c.set(ctx as *const ())); + Self + } +} + +impl Drop for ContextDropGuard { + fn drop(&mut self) { + RT_CTX.with(|c| c.set(std::ptr::null())); + } +} + +thread_local! { + static RT_CTX: Cell<*const ()> = const { Cell::new(std::ptr::null()) }; +} + +/// Runtime context for the [`LocalRuntime`]. +pub(crate) struct RuntimeContext<'umem> { + /// Set of empty ready to go frame structs that can be used for building outbound packets. + pub free_frames: SharedFrameBuffer<'umem>, + /// Frames that are filled and ready to be sent to the network. + pub tx_return: SharedFrameBuffer<'umem>, + /// Frames that were read from the network and should be handed back to the kernel for re-use. + pub rx_return: SharedFrameBuffer<'umem>, + /// Path MTU cache for handling path MTU discovery. + pub pmtu: Rc, + /// ARP/NDP neighbor handling for IPv4 and IPv6. + pub neighbor_handler: Rc, + /// UDP handler is used to bind and send UDP packets, handling things like fragmentation and reassembly. + pub udp_handler: Rc>>, + /// TCP handler manages TCP connections and the TCP state machine. + pub tcp_handler: Rc>, + /// TX checksum offload. + pub tx_offload: bool, +} + +/// Called by `UdpSocket::new()` to access the current runtime context. +/// +/// # Safety +/// +/// Sound because: the context pointer is only set during `LocalRuntime::run()`, +/// and all sockets must live within that closure's scope. The `'umem` lifetime +/// is recovered from the calling context. +/// +/// # Panics +/// +/// Panics if called outside of `LocalRuntime::run()`. +pub(crate) fn with_runtime_context<'umem, R>(f: impl FnOnce(&RuntimeContext<'umem>) -> R) -> R { + RT_CTX.with(|c| { + let ptr = c.get(); + assert!( + !ptr.is_null(), + "UdpSocket::new() called outside of LocalRuntime::run()" + ); + let ctx = unsafe { &*(ptr as *const RuntimeContext<'umem>) }; + f(ctx) + }) +} diff --git a/src/rt/local.rs b/src/rt/local.rs new file mode 100644 index 0000000..e519a5c --- /dev/null +++ b/src/rt/local.rs @@ -0,0 +1,409 @@ +use std::{ + cell::UnsafeCell, + rc::Rc, + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, + task::Context, +}; + +use coarsetime::Duration; +use futures_util::pin_mut; + +use crate::{ + net::{ + NeighborHandler, PmtuCache, + handler::{ + ethernet::EthernetHandler, ipv4::Ipv4Handler, ipv6::Ipv6Handler, tcp::TcpHandler, + udp::UdpHandler, + }, + }, + xdp::{ + context::{XdpContext, XdpContextBuilder}, + error::Result, + frame::{BasicFrameBuffer, FrameBuffer, SharedFrameBuffer}, + program::AttachMode, + socket::{CopyMode, Socket, SocketBuilder}, + umem::{Umem, UmemBuilder}, + }, +}; + +use super::{ + context::{ContextDropGuard, RuntimeContext}, + waker, +}; + +const DEFAULT_ARP_TTL: Duration = Duration::from_secs(60); + +/// Builder for configuring and constructing a [`LocalRuntime`]. +/// +/// Wraps the underlying XDP context, UMEM, and socket builders with +/// sane defaults. All builder methods delegate to the appropriate +/// sub-builder so callers only interact with a single API. +pub struct LocalRuntimeBuilder<'name> { + if_name: &'name str, + queue: u32, + ctx: XdpContextBuilder<'name>, + umem: UmemBuilder, + socket: SocketBuilder<'name>, + arp_ttl: Duration, +} + +impl<'name> LocalRuntimeBuilder<'name> { + /// Creates a new [`LocalRuntimeBuilder`] with the given interface name and queue number. + pub fn new(if_name: &'name str, queue: u32) -> Self { + Self { + if_name, + queue, + ctx: XdpContextBuilder::new(if_name), + umem: UmemBuilder::new(), + socket: SocketBuilder::new(if_name, queue), + arp_ttl: DEFAULT_ARP_TTL, + } + } + + /// Sets the ARP TTL for the [`LocalRuntime`]. + pub fn arp_ttl(mut self, arp_ttl: Duration) -> Self { + self.arp_ttl = arp_ttl; + self + } + + /// Sets the attach mode for the [`LocalRuntime`]. + pub fn attach_mode(mut self, attach_mode: AttachMode) -> Self { + self.ctx = self.ctx.attach_mode(attach_mode); + self + } + + /// Enables fragmentation for the [`LocalRuntime`]. + pub fn enable_fragmentation(mut self, enable_fragmentation: bool) -> Self { + self.ctx = self.ctx.enable_fragmentation(enable_fragmentation); + self.socket = self.socket.enable_fragmentation(enable_fragmentation); + self + } + + /// Sets the completion ring size for the [`LocalRuntime`]. + pub fn completion_ring_size(mut self, completion_ring_size: u32) -> Self { + self.umem = self.umem.completion_ring_size(completion_ring_size); + self + } + + /// Sets the fill ring size for the [`LocalRuntime`]. + pub fn fill_ring_size(mut self, fill_ring_size: u32) -> Self { + self.umem = self.umem.fill_ring_size(fill_ring_size); + self + } + + /// Sets the frame size for the [`LocalRuntime`]. + pub fn frame_size(mut self, frame_size: usize) -> Self { + self.umem = self.umem.frame_size(frame_size); + self + } + + /// Enables busy polling for the [`LocalRuntime`]. + pub fn busy_poll(mut self, busy_poll: bool) -> Self { + self.umem = self.umem.busy_poll(busy_poll); + self.socket = self.socket.busy_poll(busy_poll); + self + } + + /// Sets the busy poll batch size for the [`LocalRuntime`]. + pub fn busy_poll_batch_size(mut self, busy_poll_batch_size: usize) -> Self { + self.socket = self.socket.busy_poll_batch_size(busy_poll_batch_size); + self + } + + /// Sets the busy poll timeout for the [`LocalRuntime`]. + pub fn busy_poll_timeout_us(mut self, busy_poll_timeout_us: i32) -> Self { + self.socket = self.socket.busy_poll_timeout_us(busy_poll_timeout_us); + self + } + + /// Enables huge tables for the [`LocalRuntime`]. + pub fn huge_tables(mut self, huge_tables: bool) -> Self { + self.umem = self.umem.huge_tables(huge_tables); + self + } + + /// Enables unaligned frames for the [`LocalRuntime`]. + pub fn unaligned(mut self, unaligned: bool) -> Self { + self.umem = self.umem.unaligned(unaligned); + self + } + + /// Sets the RX ring size for the [`LocalRuntime`]. + pub fn rx_ring_size(mut self, rx_ring_size: u32) -> Self { + self.socket = self.socket.rx_ring_size(rx_ring_size); + self + } + + /// Sets the TX ring size for the [`LocalRuntime`]. + pub fn tx_ring_size(mut self, tx_ring_size: u32) -> Self { + self.socket = self.socket.tx_ring_size(tx_ring_size); + self + } + + /// Sets the copy mode for the [`LocalRuntime`]. + pub fn copy_mode(mut self, copy_mode: CopyMode) -> Self { + self.socket = self.socket.copy_mode(copy_mode); + self + } + + /// Builds the [`LocalRuntime`]. + pub fn build<'umem>(self) -> Result> { + let mut ctx = self.ctx.build()?; + let umem = self.umem.build()?; + let socket = self.socket.build(&mut ctx, umem.owner().clone())?; + LocalRuntime::new(self.if_name, self.queue, ctx, umem, socket, self.arp_ttl) + } +} + +/// Single-threaded packet processing runtime with integrated protocol handlers. +/// +/// Owns the AF_XDP socket, UMEM, and all protocol handler state. The +/// `run` method drives the event loop: receive frames, dispatch through +/// the protocol stack, poll the user future, and transmit responses. +pub struct LocalRuntime<'umem> { + // Overall context for the XDP program, this is used to own the underlying XDP program and socket. + ctx: XdpContext, + // Queue number for the XDP program. + _queue: u32, + // Shared memory for reading and writing frames to the network. + umem: Umem<'umem>, + // Raw AF_XDP socket for reading and writing frames to the network. + socket: Socket<'umem>, + // ARP/NDP neighbor handling for IPv4 and IPv6. + neighbor_handler: Rc, + // Path MTU cache for handling path MTU discovery. + pmtu: Rc, + // Ethernet handler is used to handle Ethernet frames. + ethernet_handler: EthernetHandler, + // Main IPv4 protocol handler calls into udp_handler and tcp_handler. + ipv4_handler: Ipv4Handler, + // Main IPv6 protocol handler calls into neighbor_handler and udp_handler and tcp_handler. + ipv6_handler: Ipv6Handler, + // UDP handler is used to bind and send UDP packets, handling things like fragmentation and reassembly. + udp_handler: Rc>>, + // TCP handler manages TCP connections and the TCP state machine. + tcp_handler: Rc>, + // Set of empty ready to go frame structs that can be used for building outbound packets. + free_frames: SharedFrameBuffer<'umem>, + // Frames that are filled and ready to be sent to the network. + tx_return: SharedFrameBuffer<'umem>, + // Frames that were read from the network and should be handed back to the kernel for re-use. + rx_return: SharedFrameBuffer<'umem>, + // Counter for rate-limiting evict_stale() calls (~every 1024 iterations). + evict_counter: u32, +} + +impl<'umem> LocalRuntime<'umem> { + /// Builder for configuring and constructing a [`LocalRuntime`]. + /// + /// Wraps the underlying XDP context, UMEM, and socket builders with + /// sane defaults. All builder methods delegate to the appropriate + /// sub-builder so callers only interact with a single API. + pub fn builder<'name>(if_name: &'name str, queue: u32) -> LocalRuntimeBuilder<'name> { + LocalRuntimeBuilder::new(if_name, queue) + } + + fn new( + if_name: &str, + queue: u32, + ctx: XdpContext, + umem: Umem<'umem>, + socket: Socket<'umem>, + arp_ttl: Duration, + ) -> Result { + let info = ctx.info(); + println!("info: {:?}", info); + let mtu = info.mtu; + let rx_offload = info.rx_offload; + let tx_offload = info.tx_offload; + + let mut neighbor_handler = NeighborHandler::new(if_name, arp_ttl)?; + neighbor_handler.set_offload(rx_offload, tx_offload); + let neighbor_handler = Rc::new(neighbor_handler); + let pmtu = Rc::new(PmtuCache::with_mtu(mtu)); + + let tx_return = BasicFrameBuffer::new(umem.num_frames()).into(); + let rx_return = BasicFrameBuffer::new(umem.num_frames()).into(); + let free_frames = umem.init_buffer::().unwrap().into(); + + Ok(Self { + ctx, + _queue: queue, + umem, + socket, + neighbor_handler, + pmtu, + ethernet_handler: EthernetHandler, + ipv4_handler: Ipv4Handler::new(rx_offload, tx_offload), + ipv6_handler: Ipv6Handler::new(rx_offload, tx_offload), + udp_handler: Rc::new(UnsafeCell::new(UdpHandler::new(256, rx_offload))), + tcp_handler: Rc::new(UnsafeCell::new(TcpHandler::new(rx_offload, tx_offload))), + free_frames, + tx_return, + rx_return, + evict_counter: 0, + }) + } + + /// Runs the event loop until `exit` is set or `fut` completes. + /// + /// Each iteration: receive frames, dispatch through the protocol stack, + /// poll `fut`, drive TCP timers, evict stale state, and transmit. + pub fn run(&mut self, exit: Arc, fut: F) -> Result<()> + where + F: Future, + { + // Drop guard ensures the context is cleared even on early return/panic. + let _guard = ContextDropGuard::new(RuntimeContext { + free_frames: self.free_frames.clone(), + tx_return: self.tx_return.clone(), + rx_return: self.rx_return.clone(), + pmtu: self.pmtu.clone(), + neighbor_handler: self.neighbor_handler.clone(), + udp_handler: self.udp_handler.clone(), + tcp_handler: self.tcp_handler.clone(), + tx_offload: self.ctx.info().tx_offload, + }); + + // Before we can operate properly we need to seed the kernel with free frames to read into. + self.umem.maybe_wake_fill_queue(self.socket.fd())?; + self.umem + .process_fill_queue(&mut self.free_frames) + .expect("failed to process fill queue"); + + let expected_free_frames = self.free_frames.num_frames(); + + let waker = waker(); + let mut cx = Context::from_waker(&waker); + pin_mut!(fut); + + let mut buffer = BasicFrameBuffer::new(self.umem.num_frames()); + let mut now = coarsetime::Instant::now(); + while !exit.load(Ordering::Relaxed) { + let received = match self.socket.recv(&mut buffer) { + Err(_) => 0, + Ok(received) => { + // SAFETY: single-threaded, no reentrant handler calls. + let udp_handler = unsafe { &mut *self.udp_handler.get() }; + let tcp_handler = unsafe { &mut *self.tcp_handler.get() }; + let Self { + neighbor_handler, + pmtu, + ethernet_handler, + ipv4_handler, + ipv6_handler, + .. + } = self; + + for frame in buffer.take_frames() { + ethernet_handler.handle( + frame, + ipv4_handler, + ipv6_handler, + udp_handler, + tcp_handler, + neighbor_handler, + pmtu, + now, + &mut self.free_frames, + &mut self.rx_return, + &mut self.tx_return, + ); + } + received + } + }; + + if fut.as_mut().poll(&mut cx).is_ready() { + return Ok(()); + } + + // Drive TCP data segment transmission. + // + // SAFETY: single-threaded, no reentrant handler calls. + unsafe { &mut *self.tcp_handler.get() }.poll_send( + now, + self.neighbor_handler.local_mac(), + &self.neighbor_handler, + &mut self.free_frames, + &mut self.tx_return, + ); + + self.evict_counter = self.evict_counter.wrapping_add(1); + if self.evict_counter & 65535 == 0 { + // Update our current timestamp. + now = coarsetime::Instant::now(); + + // Evict stale UDP fragments. + // + // SAFETY: single-threaded, no reentrant handler calls. + unsafe { &mut *self.udp_handler.get() }.evict_stale( + now, + Duration::from_secs(30), + &mut self.rx_return, + ); + + // Evict stale TCP connections (e.g. TIME-WAIT), and poll timers with the new timestamp. + // + // SAFETY: single-threaded, no reentrant handler calls. + let tcp_handler = unsafe { &mut *self.tcp_handler.get() }; + tcp_handler.evict_stale(now, &mut self.rx_return); + tcp_handler.poll_timers( + now, + self.neighbor_handler.local_mac(), + &self.neighbor_handler, + &mut self.free_frames, + &mut self.tx_return, + ); + + // Evict stale neighbor entries. + self.neighbor_handler.evict_stale(now); + + // Evict stale PMTU entries. + self.pmtu.evict_stale(now); + } + + let expected_size = self.tx_return.num_frames() + self.rx_return.num_frames(); + + // We have frames to send, so send them. + while self.tx_return.num_frames() > 0 { + if self.socket.send(&mut self.tx_return).is_err() { + self.socket.maybe_wake()?; + let _ = self.umem.process_completion_queue(&mut self.rx_return); + } + } + + // Fully flush the writes from above. + while self.rx_return.num_frames() < expected_size { + if self + .umem + .process_completion_queue(&mut self.rx_return) + .is_err() + { + self.socket.maybe_wake()?; + } + } + + // Take care of refilling our free frames. + while self.rx_return.num_frames() > received as usize { + self.free_frames.push(self.rx_return.pop().unwrap()); + } + + // Anything left goes back to the fill queue. + while self.rx_return.num_frames() > 0 { + if self.umem.process_fill_queue(&mut self.rx_return).is_err() { + self.umem.maybe_wake_fill_queue(self.socket.fd())?; + } + } + + debug_assert_eq!(self.rx_return.num_frames(), 0); + debug_assert_eq!(self.tx_return.num_frames(), 0); + debug_assert_eq!(self.free_frames.num_frames(), expected_free_frames); + } + + Ok(()) + } +} diff --git a/src/rt/mod.rs b/src/rt/mod.rs new file mode 100644 index 0000000..860cccf --- /dev/null +++ b/src/rt/mod.rs @@ -0,0 +1,8 @@ +mod affinity; +pub(crate) mod context; +mod local; +pub(crate) mod waker; + +pub use affinity::*; +pub use local::*; +use waker::*; diff --git a/src/rt/waker.rs b/src/rt/waker.rs new file mode 100644 index 0000000..6f2b225 --- /dev/null +++ b/src/rt/waker.rs @@ -0,0 +1,24 @@ +use std::{ + ptr::null, + task::{RawWaker, RawWakerVTable, Waker}, +}; + +const fn raw_waker() -> RawWaker { + RawWaker::new(null(), &NOOP_WAKER_VTABLE) +} + +unsafe fn noop_clone(_data: *const ()) -> RawWaker { + raw_waker() +} + +unsafe fn noop(_data: *const ()) {} + +const NOOP_WAKER_VTABLE: RawWakerVTable = RawWakerVTable::new(noop_clone, noop, noop, noop); + +/// Creates a no-op waker for the local executor. +/// +/// The local runtime is driven by packet arrival, not waker +/// notifications, so wake calls are intentionally ignored. +pub(crate) const fn waker() -> Waker { + unsafe { Waker::from_raw(raw_waker()) } +} diff --git a/src/xdp/context/ctx.rs b/src/xdp/context/ctx.rs index 8a4675e..3726ff4 100644 --- a/src/xdp/context/ctx.rs +++ b/src/xdp/context/ctx.rs @@ -8,7 +8,7 @@ use crate::xdp::{ }; /// Embedded XDP BPF program for round-robin packet routing to AF_XDP sockets. -static XDP_PROG_DATA: &'static [u8] = include_bytes!("../../../bpf/xdp_kern.o"); +static XDP_PROG_DATA: &[u8] = include_bytes!("../../../bpf/xdp_kern.o"); /// Builder for creating an XdpContext. /// @@ -25,7 +25,15 @@ pub struct XdpContextBuilder<'name> { } impl<'name> XdpContextBuilder<'name> { - fn new(if_name: &'name str) -> Self { + /// Creates a new XdpContextBuilder. + /// + /// # Examples + /// + /// ```no_run + /// use libvoid::xdp::context::XdpContextBuilder; + /// let builder = XdpContextBuilder::new("eth0"); + /// ``` + pub fn new(if_name: &'name str) -> Self { Self { if_name, attach_mode: AttachMode::default(), @@ -47,7 +55,7 @@ impl<'name> XdpContextBuilder<'name> { /// Builds the XdpContext. pub fn build(self) -> Result { - XdpContext::new(&self.if_name, self.attach_mode, self.enable_fragmentation) + XdpContext::new(self.if_name, self.attach_mode, self.enable_fragmentation) } } @@ -161,9 +169,10 @@ impl XdpContext { #[cfg(test)] mod tests { - use super::*; use crate::xdp::test_utils::TestVethPair; + use super::*; + /// Tests that XdpContext can be created with default attach mode and fragmentation enabled. #[test] fn test_context_creation() { diff --git a/src/xdp/context/mod.rs b/src/xdp/context/mod.rs index 3fc3fe9..9d4e31a 100644 --- a/src/xdp/context/mod.rs +++ b/src/xdp/context/mod.rs @@ -50,10 +50,10 @@ //! .build()?; //! //! // 2. Create shared memory -//! let mut umem = Umem::builder(&mut ctx).num_frames(4096).build()?; +//! let mut umem = Umem::builder().num_frames(4096).build()?; //! //! // 3. Create an AF_XDP socket -//! let socket = Socket::builder(&mut ctx, "eth0", 0).build(umem.owner().clone())?; +//! let socket = Socket::builder("eth0", 0).build(&mut ctx, umem.owner().clone())?; //! //! // 4. Process packets via RX/TX rings... //! # Ok::<(), libvoid::xdp::error::Error>(()) @@ -71,4 +71,4 @@ mod ctx; -pub use ctx::XdpContext; +pub use ctx::{XdpContext, XdpContextBuilder}; diff --git a/src/xdp/error.rs b/src/xdp/error.rs index 72b5fb4..2c2b991 100644 --- a/src/xdp/error.rs +++ b/src/xdp/error.rs @@ -59,6 +59,8 @@ pub enum Error { QueryXdpFeatures(Errno), #[error("failed while getting MTU: {0}")] GetMtu(String), + #[error("failed to query checksum offload capabilities: {0}")] + GetChecksumOffload(String), #[error("fragmentation not supported by the network interface")] FragmentationNotSupported, #[error("zero copy not supported by the network interface")] @@ -71,6 +73,10 @@ pub enum Error { EpollWait(Errno), #[error("failed to register file descriptor with epoll instance: {0}")] EpollCtl(Errno), + #[error("{0}")] + Other(String), + #[error("Exiting runtime")] + ExitRuntime, } /// A simple ZST error variant for would block scenarios, this is explicitly a ZST to avoid the allocations and eventual drop calls of the error case. diff --git a/src/xdp/frame/basic.rs b/src/xdp/frame/basic.rs index 7fe4943..cbf760a 100644 --- a/src/xdp/frame/basic.rs +++ b/src/xdp/frame/basic.rs @@ -67,6 +67,13 @@ impl<'umem> FrameBuffer<'umem> for BasicFrameBuffer<'umem> { self.frames.push_back(frame); } + fn pop(&mut self) -> Option> { + self.frames.pop_front().inspect(|_| { + self.free_space += 1; + self.num_frames -= 1; + }) + } + fn take_frames(&mut self) -> Self::Drain<'_> { self.drain(..) } @@ -93,20 +100,6 @@ impl<'umem> FromIterator> for BasicFrameBuffer<'umem> { } } -impl<'umem> Extend> for BasicFrameBuffer<'umem> { - fn extend>>(&mut self, iter: T) { - let frames: VecDeque> = iter.into_iter().collect(); - debug_assert!( - self.free_space >= frames.len(), - "free space is less than the number of frames to extend" - ); - - self.free_space -= frames.len(); - self.num_frames += frames.len(); - self.frames.extend(frames); - } -} - #[cfg(test)] mod tests { use super::*; @@ -212,19 +205,4 @@ mod tests { assert_eq!(buffer.num_frames(), 2); assert_eq!(buffer.free_space(), 0); } - - #[test] - fn test_basic_frame_buffer_extend() { - let mut buffer = BasicFrameBuffer::new(10); - let mut data0 = [0u8; 10]; - let mut data1 = [0u8; 10]; - let frames = vec![ - Frame::new(1, &mut data0, 10, false), - Frame::new(2, &mut data1, 10, false), - ]; - - buffer.extend(frames); - assert_eq!(buffer.num_frames(), 2); - assert_eq!(buffer.free_space(), 8); - } } diff --git a/src/xdp/frame/buffer.rs b/src/xdp/frame/buffer.rs index 6c1c1ae..d00a0fa 100644 --- a/src/xdp/frame/buffer.rs +++ b/src/xdp/frame/buffer.rs @@ -30,6 +30,9 @@ pub trait FrameBuffer<'umem> { /// Pushes a frame into the buffer, note again that the XDP subsystem will use free_space above to determine how many frames to push this should be infalible in every way. fn push(&mut self, frame: Frame<'umem>); + /// Pops a frame from the buffer. + fn pop(&mut self) -> Option>; + /// Drain all frames from the buffer and pass off ownership to the caller, this is used to take the frames and pass them off to the kernel. fn take_frames(&mut self) -> Self::Drain<'_>; @@ -74,6 +77,11 @@ impl<'umem, B: FrameBuffer<'umem>> FrameBuffer<'umem> for &mut B { B::push(self, frame) } + #[inline(always)] + fn pop(&mut self) -> Option> { + B::pop(self) + } + #[inline(always)] fn take_frames(&mut self) -> Self::Drain<'_> { B::take_frames(self) @@ -111,32 +119,37 @@ impl<'umem, B: FrameBuffer<'umem>> FrameBuffer<'umem> for std::sync::MutexGuard< #[inline(always)] fn free_space(&self) -> usize { - B::free_space(&*self) + B::free_space(self) } #[inline(always)] fn num_frames(&self) -> usize { - B::num_frames(&*self) + B::num_frames(self) } #[inline(always)] fn push(&mut self, frame: Frame<'umem>) { - B::push(&mut *self, frame) + B::push(self, frame) + } + + #[inline(always)] + fn pop(&mut self) -> Option> { + B::pop(self) } #[inline(always)] fn take_frames(&mut self) -> Self::Drain<'_> { - B::take_frames(&mut *self) + B::take_frames(self) } #[inline(always)] fn iter_frames(&self) -> Self::Iter<'_> { - B::iter_frames(&*self) + B::iter_frames(self) } #[inline(always)] fn iter_frames_mut(&mut self) -> Self::IterMut<'_> { - B::iter_frames_mut(&mut *self) + B::iter_frames_mut(self) } } @@ -162,40 +175,48 @@ impl<'umem, B: FrameBuffer<'umem>> FrameBuffer<'umem> for futures_util::lock::Mu #[inline(always)] fn free_space(&self) -> usize { - B::free_space(&*self) + B::free_space(self) } #[inline(always)] fn num_frames(&self) -> usize { - B::num_frames(&*self) + B::num_frames(self) } #[inline(always)] fn push(&mut self, frame: Frame<'umem>) { - B::push(&mut *self, frame) + B::push(self, frame) + } + + #[inline(always)] + fn pop(&mut self) -> Option> { + B::pop(self) } #[inline(always)] fn take_frames(&mut self) -> Self::Drain<'_> { - B::take_frames(&mut *self) + B::take_frames(self) } #[inline(always)] fn iter_frames(&self) -> Self::Iter<'_> { - B::iter_frames(&*self) + B::iter_frames(self) } #[inline(always)] fn iter_frames_mut(&mut self) -> Self::IterMut<'_> { - B::iter_frames_mut(&mut *self) + B::iter_frames_mut(self) } } #[cfg(test)] mod tests { + use std::collections::{ + VecDeque, + vec_deque::{Drain, Iter, IterMut}, + }; + use super::*; - use std::collections::VecDeque; - use std::collections::vec_deque::{Drain, Iter, IterMut}; struct MockFrameBuffer<'umem> { frames: VecDeque>, @@ -240,6 +261,10 @@ mod tests { self.frames.push_back(frame); } + fn pop(&mut self) -> Option> { + self.frames.pop_front() + } + fn take_frames(&mut self) -> Self::Drain<'_> { self.frames.drain(..) } diff --git a/src/xdp/frame/frame.rs b/src/xdp/frame/frame.rs index 6059753..30f40eb 100644 --- a/src/xdp/frame/frame.rs +++ b/src/xdp/frame/frame.rs @@ -16,7 +16,7 @@ impl<'umem> Frame<'umem> { /// Create a new frame with the given address, data pointer, length, and capacity. pub(crate) fn new(addr: u64, data: &'umem mut [u8], len: usize, is_fragment: bool) -> Self { assert!( - data.len() > 0 && len <= data.len(), + !data.is_empty() && len <= data.len(), "len must be less than or equal to capacity, which must be greater than 0" ); @@ -42,6 +42,12 @@ impl<'umem> Frame<'umem> { self.len } + /// Returns true if the frame is empty. + #[inline] + pub fn is_empty(&self) -> bool { + self.len == 0 + } + /// Returns the capacity of the frame in bytes. /// /// This is the total number of bytes that the frame can hold. @@ -63,6 +69,10 @@ impl<'umem> Frame<'umem> { } /// Sets the length of the frame. + /// + /// # Safety + /// + /// This function does not check if the length is valid, it blindly sets the length of the frame. #[inline] pub unsafe fn set_len(&mut self, len: usize) { self.len = len; @@ -150,7 +160,7 @@ mod tests { let mut frame = Frame::new(0, &mut data, 0, false); let incoming = [1, 2, 3, 4, 5]; - frame.copy_from(&incoming); + frame.copy_from(incoming); assert_eq!(frame.len(), 5); assert_eq!(&frame[..], &incoming); @@ -163,7 +173,7 @@ mod tests { let mut frame = Frame::new(0, &mut data, 0, false); let incoming = [1, 2, 3, 4, 5, 6]; - frame.copy_from(&incoming); + frame.copy_from(incoming); } #[test] diff --git a/src/xdp/frame/mod.rs b/src/xdp/frame/mod.rs index 64a6d44..12b913c 100644 --- a/src/xdp/frame/mod.rs +++ b/src/xdp/frame/mod.rs @@ -6,7 +6,9 @@ mod basic; mod buffer; mod frame; +mod shared; pub use basic::BasicFrameBuffer; pub use buffer::FrameBuffer; pub use frame::Frame; +pub use shared::SharedFrameBuffer; diff --git a/src/xdp/frame/shared.rs b/src/xdp/frame/shared.rs new file mode 100644 index 0000000..0ea9a8a --- /dev/null +++ b/src/xdp/frame/shared.rs @@ -0,0 +1,117 @@ +use std::{ + cell::UnsafeCell, + collections::vec_deque::{Drain, Iter, IterMut}, + ops::RangeBounds, + rc::Rc, +}; + +use crate::xdp::frame::{BasicFrameBuffer, FrameBuffer}; + +use super::Frame; + +#[derive(Debug)] +pub struct SharedFrameBuffer<'umem> { + inner: Rc>>, +} + +impl<'umem> SharedFrameBuffer<'umem> { + #[inline(always)] + pub fn drain>(&self, range: R) -> Drain<'_, Frame<'umem>> { + unsafe { + let inner = &mut *self.inner.get(); + inner.drain(range) + } + } +} + +impl<'umem> From> for SharedFrameBuffer<'umem> { + fn from(inner: BasicFrameBuffer<'umem>) -> Self { + Self { + inner: Rc::new(UnsafeCell::new(inner)), + } + } +} + +impl<'umem> Clone for SharedFrameBuffer<'umem> { + fn clone(&self) -> Self { + Self { + inner: Rc::clone(&self.inner), + } + } +} + +impl<'umem> FrameBuffer<'umem> for SharedFrameBuffer<'umem> { + type Drain<'a> + = Drain<'a, Frame<'umem>> + where + Self: 'a, + 'umem: 'a; + + type IterMut<'a> + = IterMut<'a, Frame<'umem>> + where + Self: 'a, + 'umem: 'a; + + type Iter<'a> + = Iter<'a, Frame<'umem>> + where + Self: 'a, + 'umem: 'a; + + #[inline(always)] + fn free_space(&self) -> usize { + unsafe { + let inner = &mut *self.inner.get(); + inner.free_space() + } + } + + #[inline(always)] + fn num_frames(&self) -> usize { + unsafe { + let inner = &mut *self.inner.get(); + inner.num_frames() + } + } + + #[inline(always)] + fn push(&mut self, frame: Frame<'umem>) { + unsafe { + let inner = &mut *self.inner.get(); + inner.push(frame); + } + } + + #[inline(always)] + fn pop(&mut self) -> Option> { + unsafe { + let inner = &mut *self.inner.get(); + inner.pop() + } + } + + #[inline(always)] + fn take_frames(&mut self) -> as FrameBuffer<'umem>>::Drain<'_> { + unsafe { + let inner = &mut *self.inner.get(); + inner.take_frames() + } + } + + #[inline(always)] + fn iter_frames(&self) -> as FrameBuffer<'umem>>::Iter<'_> { + unsafe { + let inner = &*self.inner.get(); + inner.iter_frames() + } + } + + #[inline(always)] + fn iter_frames_mut(&mut self) -> as FrameBuffer<'umem>>::IterMut<'_> { + unsafe { + let inner = &mut *self.inner.get(); + inner.iter_frames_mut() + } + } +} diff --git a/src/xdp/futures/local/comp.rs b/src/xdp/futures/local/comp.rs index 60f31ea..6cf337e 100644 --- a/src/xdp/futures/local/comp.rs +++ b/src/xdp/futures/local/comp.rs @@ -61,7 +61,12 @@ impl<'que, 'umem, B: FrameBuffer<'umem>> Future for LocalCompFuture<'que, 'umem, // Those accesses are also guaranteed to not move self or the fields themselves. let this = unsafe { self.get_unchecked_mut() }; - if let Ok(_) = this.completion_queue.inner.process_queue(&mut this.batch) { + if this + .completion_queue + .inner + .process_queue(&mut this.batch) + .is_ok() + { return Poll::Ready(Ok(())); } diff --git a/src/xdp/futures/local/fill.rs b/src/xdp/futures/local/fill.rs index 332f359..744bc2b 100644 --- a/src/xdp/futures/local/fill.rs +++ b/src/xdp/futures/local/fill.rs @@ -69,11 +69,9 @@ impl<'que, 'umem, 'fd, B: FrameBuffer<'umem>> Future for LocalFillFuture<'que, ' } } - this.fill_queue.inner.process_queue(&mut this.batch); - if this.batch.num_frames() == 0 { - return Poll::Ready(Ok(())); + match this.fill_queue.inner.process_queue(&mut this.batch) { + Ok(_) => Poll::Ready(Ok(())), + Err(_) => Poll::Pending, } - - Poll::Pending } } diff --git a/src/xdp/futures/local/poller.rs b/src/xdp/futures/local/poller.rs index e50800e..6745dbb 100644 --- a/src/xdp/futures/local/poller.rs +++ b/src/xdp/futures/local/poller.rs @@ -69,7 +69,7 @@ impl Poller { let n = unsafe { epoll_wait( self.poll_fd, - self.events.as_mut_ptr() as *mut epoll_event, + self.events.as_mut_ptr(), MAX_EVENTS as i32, timeout_ms, ) diff --git a/src/xdp/futures/smol/comp.rs b/src/xdp/futures/smol/comp.rs index ace11cc..992e58f 100644 --- a/src/xdp/futures/smol/comp.rs +++ b/src/xdp/futures/smol/comp.rs @@ -70,7 +70,12 @@ impl<'que, 'umem, B: FrameBuffer<'umem>> Future for SmolCompFuture<'que, 'umem, // Those accesses are also guaranteed to not move self or the fields themselves. let this = unsafe { self.get_unchecked_mut() }; - if let Ok(_) = this.completion_queue.inner.process_queue(&mut this.batch) { + if this + .completion_queue + .inner + .process_queue(&mut this.batch) + .is_ok() + { return Poll::Ready(Ok(())); } @@ -78,7 +83,7 @@ impl<'que, 'umem, B: FrameBuffer<'umem>> Future for SmolCompFuture<'que, 'umem, loop { ready!(async_fd.poll_writable(cx))?; - if let Ok(_) = inner.process_queue(&mut this.batch) { + if inner.process_queue(&mut this.batch).is_ok() { return Poll::Ready(Ok(())); } } diff --git a/src/xdp/futures/smol/fill.rs b/src/xdp/futures/smol/fill.rs index 2f752df..fc4b660 100644 --- a/src/xdp/futures/smol/fill.rs +++ b/src/xdp/futures/smol/fill.rs @@ -77,8 +77,7 @@ impl<'que, 'umem, 'fd, B: FrameBuffer<'umem>> Future for SmolFillFuture<'que, 'u } } - this.fill_queue.inner.process_queue(&mut this.batch); - if this.batch.num_frames() == 0 { + if this.fill_queue.inner.process_queue(&mut this.batch).is_ok() { return Poll::Ready(Ok(())); } @@ -86,8 +85,7 @@ impl<'que, 'umem, 'fd, B: FrameBuffer<'umem>> Future for SmolFillFuture<'que, 'u loop { ready!(async_fd.poll_writable(cx))?; - inner.process_queue(&mut this.batch); - if this.batch.num_frames() == 0 { + if inner.process_queue(&mut this.batch).is_ok() { return Poll::Ready(Ok(())); } } diff --git a/src/xdp/futures/tokio/comp.rs b/src/xdp/futures/tokio/comp.rs index d9badd2..70375d9 100644 --- a/src/xdp/futures/tokio/comp.rs +++ b/src/xdp/futures/tokio/comp.rs @@ -71,7 +71,12 @@ impl<'que, 'umem, B: FrameBuffer<'umem>> Future for TokioCompFuture<'que, 'umem, // Those accesses are also guaranteed to not move self or the fields themselves. let this = unsafe { self.get_unchecked_mut() }; - if let Ok(_) = this.completion_queue.inner.process_queue(&mut this.batch) { + if this + .completion_queue + .inner + .process_queue(&mut this.batch) + .is_ok() + { return Poll::Ready(Ok(())); } @@ -79,7 +84,7 @@ impl<'que, 'umem, B: FrameBuffer<'umem>> Future for TokioCompFuture<'que, 'umem, loop { let mut guard = ready!(async_fd.poll_write_ready(cx))?; - if let Ok(_) = inner.process_queue(&mut this.batch) { + if inner.process_queue(&mut this.batch).is_ok() { return Poll::Ready(Ok(())); } diff --git a/src/xdp/futures/tokio/fill.rs b/src/xdp/futures/tokio/fill.rs index 54d32af..5d158be 100644 --- a/src/xdp/futures/tokio/fill.rs +++ b/src/xdp/futures/tokio/fill.rs @@ -77,8 +77,7 @@ impl<'que, 'umem, 'fd, B: FrameBuffer<'umem>> Future for TokioFillFuture<'que, ' } } - this.fill_queue.inner.process_queue(&mut this.batch); - if this.batch.num_frames() == 0 { + if this.fill_queue.inner.process_queue(&mut this.batch).is_ok() { return Poll::Ready(Ok(())); } @@ -86,8 +85,7 @@ impl<'que, 'umem, 'fd, B: FrameBuffer<'umem>> Future for TokioFillFuture<'que, ' loop { let mut guard = ready!(async_fd.poll_write_ready(cx))?; - inner.process_queue(&mut this.batch); - if this.batch.num_frames() == 0 { + if inner.process_queue(&mut this.batch).is_ok() { return Poll::Ready(Ok(())); } diff --git a/src/xdp/program/info.rs b/src/xdp/program/info.rs index d55efb6..e14a9bd 100644 --- a/src/xdp/program/info.rs +++ b/src/xdp/program/info.rs @@ -29,6 +29,10 @@ pub struct XdpInfo { /// /// Note this is an additional field that we use in this library, its not part of the original xdp_info struct in libxdp. pub mtu: u32, + /// Whether the XDP program supports RX checksum offloading. + pub rx_offload: bool, + /// Whether the XDP program supports TX checksum offloading. + pub tx_offload: bool, } impl XdpInfo { @@ -121,6 +125,8 @@ impl std::fmt::Debug for XdpInfo { .field("attach_mode", &self.attach_mode) .field("max_fragments", &self.xdp_zc_max_segs) .field("mtu", &self.mtu) + .field("rx_offload", &self.rx_offload) + .field("tx_offload", &self.tx_offload) .field("features", &features) .finish() } @@ -186,17 +192,21 @@ mod tests { #[test] fn test_xdp_info_max_fragments() { - let mut info = XdpInfo::default(); - info.xdp_zc_max_segs = 16; + let info = XdpInfo { + xdp_zc_max_segs: 16, + ..Default::default() + }; assert_eq!(info.max_fragments(), 16); } #[test] fn test_xdp_info_debug_display() { - let mut info = XdpInfo::default(); - info.prog_id = 42; - info.feature_flags = NETDEV_XDP_ACT_BASIC | NETDEV_XDP_ACT_REDIRECT; - info.mtu = 1500; + let info = XdpInfo { + prog_id: 42, + feature_flags: NETDEV_XDP_ACT_BASIC | NETDEV_XDP_ACT_REDIRECT, + mtu: 1500, + ..Default::default() + }; let debug_str = format!("{:?}", info); assert!(debug_str.contains("XdpInfo")); diff --git a/src/xdp/program/prog.rs b/src/xdp/program/prog.rs index 1654a54..09a99d3 100644 --- a/src/xdp/program/prog.rs +++ b/src/xdp/program/prog.rs @@ -1,4 +1,4 @@ -use std::{ffi::CString, os::raw::c_void, ptr::null}; +use std::{ffi::CString, io::Cursor, os::raw::c_void, ptr::null}; use errno::errno; use libc::if_nametoindex; @@ -9,14 +9,17 @@ use libxdp_sys::{ xdp_program__from_bpf_obj, xdp_program__set_xdp_frags_support, }; use neli::{ + FromBytesWithInput, Size, consts::{ nl::NlmF, rtnl::{Ifla, RtAddrFamily, Rtm}, socket::NlFamily, }, + genl::{AttrTypeBuilder, Genlmsghdr, GenlmsghdrBuilder, NlattrBuilder}, nl::NlPayload, router::synchronous::NlRouter, rtnl::{Ifinfomsg, IfinfomsgBuilder}, + types::{Buffer, GenlBuffer}, utils::Groups, }; @@ -24,6 +27,76 @@ use crate::xdp::error::{Error, Result, get_xdp_error_message}; use super::{AttachMode, Map, XdpInfo}; +/// Ethtool generic netlink constants defined in a submodule to avoid conflicts +/// with the crate-level `Result` type alias that the `neli_enum` proc macro +/// generated code would otherwise resolve to. +mod ethtool_nl { + use neli::{ + consts::genl::{Cmd, NlAttrType}, + neli_enum, + }; + + /// Ethtool generic netlink command IDs (from linux/ethtool_netlink.h). + #[neli_enum(serialized_type = "u8")] + pub enum EthtoolCmd { + FeaturesGet = 11, + } + impl Cmd for EthtoolCmd {} + + /// Ethtool FEATURES request/reply attributes. + #[neli_enum(serialized_type = "u16")] + pub enum EthtoolAttrFeatures { + Unspec = 0, + Header = 1, + Hw = 2, + Wanted = 3, + Active = 4, + Nochange = 5, + } + impl NlAttrType for EthtoolAttrFeatures {} + + /// Ethtool request header attributes. + #[neli_enum(serialized_type = "u16")] + pub enum EthtoolAttrHeader { + Unspec = 0, + DevIndex = 1, + DevName = 2, + Flags = 3, + } + impl NlAttrType for EthtoolAttrHeader {} + + /// Bitset container attributes. + #[neli_enum(serialized_type = "u16")] + pub enum EthtoolAttrBitset { + Unspec = 0, + Nomask = 1, + Size = 2, + Bits = 3, + Value = 4, + Mask = 5, + } + impl NlAttrType for EthtoolAttrBitset {} + + /// Bitset bits array attributes. + #[neli_enum(serialized_type = "u16")] + pub enum EthtoolAttrBitsetBits { + Unspec = 0, + Bit = 1, + } + impl NlAttrType for EthtoolAttrBitsetBits {} + + /// Individual bitset bit attributes. + #[neli_enum(serialized_type = "u16")] + pub enum EthtoolAttrBitsetBit { + Unspec = 0, + Index = 1, + Name = 2, + Value = 3, + } + impl NlAttrType for EthtoolAttrBitsetBit {} +} +use ethtool_nl::*; + /// A wrapper around a raw [xdp_program] object, this exposes a safe API for creating and attaching XDP programs. pub struct XdpProgram { program: *mut xdp_program, @@ -93,6 +166,10 @@ impl XdpProgram { info.mtu = get_mtu(if_index)?; + let (rx_offload, tx_offload) = get_checksum_offload(if_index)?; + info.rx_offload = rx_offload; + info.tx_offload = tx_offload; + Ok(Self { program, if_index, @@ -204,3 +281,113 @@ fn get_mtu(if_index: i32) -> Result { } Err(Error::GetMtu(format!("Interface {} not found", if_index))) } + +/// Queries the ethtool generic netlink interface to determine whether RX and TX +/// checksum offloading are enabled for the given network interface. +/// +/// Returns `(rx_offload, tx_offload)` where each is `true` if the corresponding +/// checksum offload is active on the hardware. +fn get_checksum_offload(if_index: i32) -> Result<(bool, bool)> { + let (router, _) = NlRouter::connect(NlFamily::Generic, None, Groups::empty()) + .map_err(|e| Error::GetChecksumOffload(e.to_string()))?; + + let family_id = router + .resolve_genl_family("ethtool") + .map_err(|e| Error::GetChecksumOffload(e.to_string()))?; + + // Build nested header with the device interface index. + let header_attrs: GenlBuffer = [NlattrBuilder::default() + .nla_type( + AttrTypeBuilder::default() + .nla_type(EthtoolAttrHeader::DevIndex) + .build() + .map_err(|e| Error::GetChecksumOffload(e.to_string()))?, + ) + .nla_payload(if_index as u32) + .build() + .map_err(|e| Error::GetChecksumOffload(e.to_string()))?] + .into_iter() + .collect(); + + // Build the FEATURES_GET request with the nested header. + let attrs: GenlBuffer = [NlattrBuilder::default() + .nla_type( + AttrTypeBuilder::default() + .nla_type(EthtoolAttrFeatures::Header) + .nla_nested(true) + .build() + .map_err(|e| Error::GetChecksumOffload(e.to_string()))?, + ) + .nla_payload(header_attrs) + .build() + .map_err(|e| Error::GetChecksumOffload(e.to_string()))?] + .into_iter() + .collect(); + + let msg = GenlmsghdrBuilder::default() + .cmd(EthtoolCmd::FeaturesGet) + .version(1) + .attrs(attrs) + .build() + .map_err(|e| Error::GetChecksumOffload(e.to_string()))?; + + let recv = router + .send::<_, _, u16, Genlmsghdr>( + family_id, + NlmF::REQUEST, + NlPayload::Payload(msg), + ) + .map_err(|e| Error::GetChecksumOffload(e.to_string()))?; + + let mut rx_offload = false; + let mut tx_offload = false; + + for response in recv { + let mut response = response.map_err(|e| Error::GetChecksumOffload(e.to_string()))?; + + if let Some(payload) = response.get_payload() { + let features_handle = payload.attrs().get_attr_handle(); + + // Parse the ACTIVE features bitset (verbose format with named bits). + let active_handle = features_handle + .get_nested_attributes::(EthtoolAttrFeatures::Active) + .map_err(|e| Error::GetChecksumOffload(e.to_string()))?; + + // Get the BITS array within the bitset. + let bits_handle = active_handle + .get_nested_attributes::(EthtoolAttrBitset::Bits) + .map_err(|e| Error::GetChecksumOffload(e.to_string()))?; + + // Iterate through each BIT entry looking for checksum features. + for bit_attr in bits_handle.iter() { + let payload = bit_attr.nla_payload(); + let nested = GenlBuffer::::from_bytes_with_input( + &mut Cursor::new(payload.as_ref()), + payload.unpadded_size(), + ) + .map_err(|e| Error::GetChecksumOffload(e.to_string()))?; + let bit_handle = nested.get_attr_handle(); + + if let Some(name_attr) = bit_handle.get_attribute(EthtoolAttrBitsetBit::Name) { + let name_bytes = name_attr.nla_payload().as_ref(); + let name = std::str::from_utf8( + name_bytes.split(|&b| b == 0).next().unwrap_or(name_bytes), + ) + .unwrap_or(""); + + match name { + "rx-checksum" => rx_offload = true, + "tx-checksum-ip-generic" => tx_offload = true, + _ => {} + } + } + } + } + + if let Some(err) = response.get_err() { + return Err(Error::GetChecksumOffload(err.to_string())); + } + } + + Ok((rx_offload, tx_offload)) +} diff --git a/src/xdp/ring/consumer.rs b/src/xdp/ring/consumer.rs index eb28d7a..6335ca4 100644 --- a/src/xdp/ring/consumer.rs +++ b/src/xdp/ring/consumer.rs @@ -67,6 +67,12 @@ impl Consumer { } } +impl Default for Consumer { + fn default() -> Self { + Self::new() + } +} + impl Consumer { /// Returns the size of the consumer ring. #[inline(always)] @@ -82,14 +88,14 @@ impl Consumer { /// ```c /// XDP_ALWAYS_INLINE __u32 xsk_cons_nb_avail(struct xsk_ring_cons *r, __u32 nb) /// { - /// __u32 entries = r->cached_prod - r->cached_cons; + /// __u32 entries = r->cached_prod - r->cached_cons; /// - /// if (entries == 0) { - /// r->cached_prod = __atomic_load_n(r->producer, __ATOMIC_ACQUIRE); - /// entries = r->cached_prod - r->cached_cons; - /// } + /// if (entries == 0) { + /// r->cached_prod = __atomic_load_n(r->producer, __ATOMIC_ACQUIRE); + /// entries = r->cached_prod - r->cached_cons; + /// } /// - /// return (entries > nb) ? nb : entries; + /// return (entries > nb) ? nb : entries; /// } /// ``` #[inline(always)] @@ -132,7 +138,7 @@ impl Consumer { /// return entries; /// } /// ``` - #[inline(always)] + // #[inline(always)] pub fn peek(&mut self, batch_size: u32) -> (u32, u32) { let mut idx: u32 = 0; let entries = self.nb_avail(batch_size); @@ -159,7 +165,7 @@ impl Consumer { /// return &descs[idx & rx->mask]; /// } /// ``` - #[inline(always)] + // #[inline(always)] pub fn rx_desc(&mut self, index: u32) -> &xdp_desc { unsafe { &*(self.ring.ring as *const xdp_desc).add((index & self.ring.mask) as usize) } } @@ -197,7 +203,7 @@ impl Consumer { /// __atomic_store_n(cons->consumer, *cons->consumer + nb, __ATOMIC_RELEASE); /// } /// ``` - #[inline(always)] + // #[inline(always)] pub fn release(&mut self, count: u32) { unsafe { AtomicU32::from_ptr(self.ring.consumer) diff --git a/src/xdp/ring/producer.rs b/src/xdp/ring/producer.rs index 684cdbc..13aa43e 100644 --- a/src/xdp/ring/producer.rs +++ b/src/xdp/ring/producer.rs @@ -67,6 +67,12 @@ impl Producer { } } +impl Default for Producer { + fn default() -> Self { + Self::new() + } +} + impl Producer { /// Returns the size of the producer ring. #[inline(always)] @@ -119,7 +125,7 @@ impl Producer { unsafe { AtomicU32::from_ptr(self.ring.consumer) }.load(Ordering::Acquire); self.ring.cached_cons += self.ring.size; - return self.ring.cached_cons - self.ring.cached_prod; + self.ring.cached_cons - self.ring.cached_prod } /// Reserves a batch of descriptors from the ring. @@ -139,7 +145,7 @@ impl Producer { /// return nb; /// } /// ``` - #[inline(always)] + // #[inline(always)] pub fn reserve(&mut self, batch_size: u32) -> (u32, u32) { if self.nb_free(batch_size) < batch_size { return (0, 0); @@ -158,9 +164,9 @@ impl Producer { /// ```c /// XDP_ALWAYS_INLINE struct xdp_desc *xsk_ring_prod__tx_desc(struct xsk_ring_prod *tx, __u32 idx) /// { - /// struct xdp_desc *descs = (struct xdp_desc *)tx->ring; + /// struct xdp_desc *descs = (struct xdp_desc *)tx->ring; /// - /// return &descs[idx & tx->mask]; + /// return &descs[idx & tx->mask]; /// } /// ``` #[inline(always)] @@ -176,12 +182,12 @@ impl Producer { /// ```c /// XDP_ALWAYS_INLINE __u64 *xsk_ring_prod__fill_addr(struct xsk_ring_prod *fill, __u32 idx) /// { - /// __u64 *addrs = (__u64 *)fill->ring; + /// __u64 *addrs = (__u64 *)fill->ring; /// - /// return &addrs[idx & fill->mask]; + /// return &addrs[idx & fill->mask]; /// } /// ``` - #[inline(always)] + // #[inline(always)] pub fn fill_addr(&mut self, index: u32) -> *mut u64 { unsafe { (self.ring.ring as *mut u64).add((index & self.ring.mask) as usize) } } @@ -194,10 +200,10 @@ impl Producer { /// ```c /// XDP_ALWAYS_INLINE void xsk_ring_prod__submit(struct xsk_ring_prod *prod, __u32 nb) /// { - /// /* Make sure everything has been written to the ring before indicating - /// * this to the kernel by writing the producer pointer. - /// */ - /// __atomic_store_n(prod->producer, *prod->producer + nb, __ATOMIC_RELEASE); + /// /* Make sure everything has been written to the ring before indicating + /// * this to the kernel by writing the producer pointer. + /// */ + /// __atomic_store_n(prod->producer, *prod->producer + nb, __ATOMIC_RELEASE); /// } /// ``` #[inline(always)] @@ -216,10 +222,10 @@ impl Producer { /// ```c /// XDP_ALWAYS_INLINE int xsk_ring_prod__needs_wakeup(const struct xsk_ring_prod *r) /// { - /// return *r->flags & XDP_RING_NEED_WAKEUP; + /// return *r->flags & XDP_RING_NEED_WAKEUP; /// } /// ``` - #[inline(always)] + // #[inline(always)] pub fn needs_wakeup(&self) -> bool { unsafe { *self.ring.flags & XDP_RING_NEED_WAKEUP != 0 } } diff --git a/src/xdp/socket/rx.rs b/src/xdp/socket/rx.rs index 69d8419..106e465 100644 --- a/src/xdp/socket/rx.rs +++ b/src/xdp/socket/rx.rs @@ -29,7 +29,7 @@ impl<'umem> SocketRx<'umem> { } /// Receives a batch of frames from the socket. - #[inline(always)] + // #[inline(always)] pub fn recv>(&mut self, mut batch: B) -> NonBlocking { // Take at least 1 frame up to the number of free slots in the batch. let (mut idx_rx, rcvd) = self.ring.peek(batch.free_space() as u32); diff --git a/src/xdp/socket/socket.rs b/src/xdp/socket/socket.rs index 89fe3f3..102bca7 100644 --- a/src/xdp/socket/socket.rs +++ b/src/xdp/socket/socket.rs @@ -166,7 +166,7 @@ impl<'if_name> SocketBuilder<'if_name> { ) -> Result> { let (owner, rx, tx) = self.build_internal(ctx, umem)?.split(); - Ok(LocalSocket::new(owner, rx, tx)?) + LocalSocket::new(owner, rx, tx) } /// Builds the socket as a [SmolSocket]. @@ -241,7 +241,7 @@ impl<'umem> Socket<'umem> { xsk_socket__create( xsk_ptr, if_name_c.as_ptr(), - queue as u32, + queue, umem.as_ptr(), rx.as_mut_ptr(), tx.as_mut_ptr(), @@ -362,11 +362,13 @@ fn setup_busy_poll(fd: c_int, busy_poll_timeout_us: i32, batch_size: usize) -> R mod tests { use std::time::{Duration, Instant}; - use crate::xdp::context::XdpContext; - use crate::xdp::frame::{BasicFrameBuffer, FrameBuffer}; - use crate::xdp::program::AttachMode; - use crate::xdp::test_utils::TestVethPair; - use crate::xdp::umem::Umem; + use crate::xdp::{ + context::XdpContext, + frame::{BasicFrameBuffer, FrameBuffer}, + program::AttachMode, + test_utils::TestVethPair, + umem::Umem, + }; use super::*; @@ -440,7 +442,9 @@ mod tests { for frame in rx_buffer.drain(..rx_prime_count) { prime_buffer.push(frame); } - fq_inner.process_queue(&mut prime_buffer); + fq_inner + .process_queue(&mut prime_buffer) + .expect("failed to process fill queue"); fq_inner .maybe_wake(socket_inner.fd()) .expect("failed to wake fill queue"); @@ -605,7 +609,8 @@ mod tests { for frame in buffer.drain(..8) { prime_buffer.push(frame); } - fq.process_queue(&mut prime_buffer); + fq.process_queue(&mut prime_buffer) + .expect("failed to process fill queue"); // Try to receive with no packets pending let mut recv_buffer = BasicFrameBuffer::new(8); @@ -650,7 +655,7 @@ mod tests { let mut send_buffer = BasicFrameBuffer::new(4); for frame in buffer.drain(..4) { let mut f = frame; - f.copy_from(&[0u8; 64]); + f.copy_from([0u8; 64]); send_buffer.push(f); } @@ -664,7 +669,7 @@ mod tests { let mut more_buffer = BasicFrameBuffer::new(4); for frame in buffer.drain(..) { let mut f = frame; - f.copy_from(&[0u8; 64]); + f.copy_from([0u8; 64]); more_buffer.push(f); } diff --git a/src/xdp/socket/tx.rs b/src/xdp/socket/tx.rs index 496ea98..6bc4aea 100644 --- a/src/xdp/socket/tx.rs +++ b/src/xdp/socket/tx.rs @@ -60,12 +60,14 @@ impl<'umem> SocketTx<'umem> { #[inline(always)] pub fn send>(&mut self, mut frames: B) -> NonBlocking { // Take exactly the number of frames we need to send. - let (mut idx_tx, ready) = self.ring.reserve(frames.num_frames() as u32); + let batch_size = frames.num_frames().min(self.ring.size() as usize); + let (mut idx_tx, ready) = self.ring.reserve(batch_size as u32); if ready == 0 { return Err(WouldBlock); } - for frame in frames.take_frames() { + for _ in 0..ready { + let frame = frames.pop().unwrap(); let desc = self.ring.tx_desc(idx_tx); unsafe { (*desc).addr = frame.addr(); diff --git a/src/xdp/test_utils.rs b/src/xdp/test_utils.rs index bf2b37d..e692c2a 100644 --- a/src/xdp/test_utils.rs +++ b/src/xdp/test_utils.rs @@ -349,14 +349,12 @@ impl TestVethPair { } // Read MAC addresses - let outer_mac = read_mac_address(&outer_name).map_err(|e| { + let outer_mac = read_mac_address(&outer_name).inspect_err(|_| { cleanup(&outer_name); - e })?; - let inner_mac = read_mac_address(&inner_name).map_err(|e| { + let inner_mac = read_mac_address(&inner_name).inspect_err(|_| { cleanup(&outer_name); - e })?; Ok(Self { diff --git a/src/xdp/umem/comp.rs b/src/xdp/umem/comp.rs index 05fdc37..8501108 100644 --- a/src/xdp/umem/comp.rs +++ b/src/xdp/umem/comp.rs @@ -36,7 +36,7 @@ impl<'umem> CompletionQueue<'umem> { idx += 1; } - self.ring.release(ready as u32); + self.ring.release(ready); Ok(ready) } } @@ -51,11 +51,9 @@ impl<'umem> Deref for CompletionQueue<'umem> { #[cfg(test)] mod tests { + use crate::xdp::{context::XdpContext, frame::BasicFrameBuffer, umem::Umem}; + use super::*; - use crate::xdp::context::XdpContext; - use crate::xdp::frame::{BasicFrameBuffer, FrameBuffer}; - use crate::xdp::umem::Umem; - use std::sync::Arc; struct TestContext { _ctx: XdpContext, diff --git a/src/xdp/umem/fill.rs b/src/xdp/umem/fill.rs index 113aa77..07ca07a 100644 --- a/src/xdp/umem/fill.rs +++ b/src/xdp/umem/fill.rs @@ -4,7 +4,7 @@ use errno::errno; use libc::{EAGAIN, EBUSY, ENETDOWN, ENOBUFS, MSG_DONTWAIT, c_int, recvfrom}; use crate::xdp::{ - error::{Error, Result}, + error::{Error, NonBlocking, Result, WouldBlock}, frame::FrameBuffer, ring::{Init, Producer}, }; @@ -37,7 +37,7 @@ impl<'umem> FillQueue<'umem> { /// Possibly wakes the fill queue, so the kernel continues to process incoming packets. /// /// This is done by first checking the needs wakeup flag, given its set we fire a empty recvfrom on the supplied fd. - #[inline(always)] + // #[inline(always)] pub fn maybe_wake(&self, fd: c_int) -> Result<()> { if self.busy_poll || self.ring.needs_wakeup() { let ret = unsafe { recvfrom(fd, null_mut(), 0, MSG_DONTWAIT, null_mut(), null_mut()) }; @@ -55,20 +55,23 @@ impl<'umem> FillQueue<'umem> { } /// Processes the fill queue, allocating new frames from the frame stack and submitting them to the fill ring up to the size of the fill ring. - #[inline(always)] - pub fn process_queue>(&mut self, mut batch: B) { - let (mut idx, ready) = self.ring.reserve(batch.num_frames() as u32); + // #[inline(always)] + pub fn process_queue>(&mut self, mut batch: B) -> NonBlocking { + let batch_size = batch.num_frames().min(self.ring.size() as usize); + let (mut idx, ready) = self.ring.reserve(batch_size as u32); if ready == 0 { - return; + return Err(WouldBlock); } - for frame in batch.take_frames() { + for _ in 0..ready { + let frame = batch.pop().unwrap(); let ptr = self.ring.fill_addr(idx); unsafe { *ptr = frame.addr() }; idx += 1; } self.ring.submit(ready); + Ok(ready) } } @@ -82,12 +85,11 @@ impl<'umem> Deref for FillQueue<'umem> { #[cfg(test)] mod tests { + use crate::xdp::{ + context::XdpContext, flags::AF_XDP_RESERVED, frame::BasicFrameBuffer, umem::Umem, + }; + use super::*; - use crate::xdp::context::XdpContext; - use crate::xdp::flags::AF_XDP_RESERVED; - use crate::xdp::frame::{BasicFrameBuffer, FrameBuffer}; - use crate::xdp::umem::Umem; - use std::sync::Arc; struct TestContext { _ctx: XdpContext, @@ -149,7 +151,9 @@ mod tests { ); // Sequential addresses // Process drains buffer completely - ctx.fq.process_queue(&mut ctx.buffer); + ctx.fq + .process_queue(&mut ctx.buffer) + .expect("failed to process fill queue"); assert_eq!(ctx.buffer.num_frames(), 0); assert_eq!(ctx.buffer.free_space(), 4); // Free space restored } @@ -163,7 +167,8 @@ mod tests { assert_eq!(ctx.buffer.num_frames(), 0); // Processing empty buffer is safe no-op - ctx.fq.process_queue(&mut ctx.buffer); + let err = ctx.fq.process_queue(&mut ctx.buffer); + assert_eq!(err, Err(WouldBlock)); assert_eq!(ctx.buffer.num_frames(), 0); } @@ -206,8 +211,12 @@ mod tests { ); // Process sequentially - ctx.fq.process_queue(&mut batch1); - ctx.fq.process_queue(&mut batch2); + ctx.fq + .process_queue(&mut batch1) + .expect("failed to process fill queue"); + ctx.fq + .process_queue(&mut batch2) + .expect("failed to process fill queue"); assert_eq!(batch1.num_frames(), 0); assert_eq!(batch2.num_frames(), 0); @@ -218,14 +227,17 @@ mod tests { let mut ctx = create_fq(4, false); // Fill the ring completely - ctx.fq.process_queue(&mut ctx.buffer); + ctx.fq + .process_queue(&mut ctx.buffer) + .expect("failed to process fill queue"); assert_eq!(ctx.buffer.num_frames(), 0); // Create new frames (simulating returned frames) let mut new_buffer = BasicFrameBuffer::new(4); // We can't easily create new frames without owner access in this test, // but we can verify empty buffer behavior - ctx.fq.process_queue(&mut new_buffer); + let err = ctx.fq.process_queue(&mut new_buffer); + assert_eq!(err, Err(WouldBlock)); assert_eq!(new_buffer.num_frames(), 0); } } diff --git a/src/xdp/umem/owner.rs b/src/xdp/umem/owner.rs index 4a54e96..a2b8ef7 100644 --- a/src/xdp/umem/owner.rs +++ b/src/xdp/umem/owner.rs @@ -120,6 +120,18 @@ impl<'umem> UmemOwner<'umem> { .collect(), ) } + + /// Returns the number of frames in the UMEM. + #[inline(always)] + pub fn num_frames(&self) -> usize { + self.num_frames + } + + /// Returns the size of the frames in the UMEM. + #[inline(always)] + pub fn frame_size(&self) -> usize { + self.frame_size + } } impl<'umem> Drop for UmemOwner<'umem> { @@ -133,12 +145,12 @@ impl<'umem> Drop for UmemOwner<'umem> { #[cfg(test)] mod tests { - use super::*; - use crate::xdp::context::XdpContext; - use crate::xdp::frame::BasicFrameBuffer; - use crate::xdp::umem::Umem; use std::thread; + use crate::xdp::{context::XdpContext, frame::BasicFrameBuffer, umem::Umem}; + + use super::*; + fn create_umem<'umem>( num_frames: usize, frame_size: usize, @@ -195,7 +207,7 @@ mod tests { // Write different data to each frame for (i, frame) in buffer.iter_frames_mut().enumerate() { - frame.copy_from(&[i as u8; 4]); + frame.copy_from([i as u8; 4]); } // Verify isolation: each frame retains its own data diff --git a/src/xdp/umem/umem.rs b/src/xdp/umem/umem.rs index b24cd5e..e69a844 100644 --- a/src/xdp/umem/umem.rs +++ b/src/xdp/umem/umem.rs @@ -149,7 +149,7 @@ impl UmemBuilder { pub fn build_local<'umem>(mut self) -> Result> { let (owner, fq, cq) = self.build_internal()?.split(); - Ok(LocalUmem::new(owner, fq, cq)?) + LocalUmem::new(owner, fq, cq) } #[cfg(feature = "smol")] @@ -190,7 +190,7 @@ impl<'umem> Umem<'umem> { let mut opts = xsk_umem_opts { sz: size_of::(), fd: 0, - size: size, + size, fill_size: fill_ring_size, comp_size: completion_ring_size, frame_size: frame_size as u32, @@ -211,7 +211,7 @@ impl<'umem> Umem<'umem> { if huge_tables { map_opts.huge(None); } - let mut mmap = map_opts.map_anon().map_err(|e| Error::MmapAllocate(e))?; + let mut mmap = map_opts.map_anon().map_err(Error::MmapAllocate)?; let umem = unsafe { xsk_umem__create_opts( @@ -258,6 +258,18 @@ impl<'umem> Umem<'umem> { &self.owner } + /// Returns the number of frames in the UMEM. + #[inline(always)] + pub fn num_frames(&self) -> usize { + self.owner.num_frames() + } + + /// Returns the size of the frames in the UMEM. + #[inline(always)] + pub fn frame_size(&self) -> usize { + self.owner.frame_size() + } + /// Initialize the frame buffer with the frames from the UMEM, its then up to the caller what to do with these frames, you can push them into the fill queue, use them /// for writing packets, or some combination of the two. This can only be called once on the [UmemOwner] instance, and will return None on every subsequent call. pub fn init_buffer + FromIterator>>(&self) -> Option { @@ -274,7 +286,7 @@ impl<'umem> Umem<'umem> { /// Processes the fill queue, allocating new frames from the frame stack and submitting them to the fill ring up to the size of the fill ring. #[inline(always)] - pub fn process_fill_queue>(&mut self, batch: B) { + pub fn process_fill_queue>(&mut self, batch: B) -> NonBlocking { self.fill_queue.process_queue(batch) } @@ -290,12 +302,13 @@ impl<'umem> Umem<'umem> { #[cfg(test)] mod tests { - use super::*; use crate::xdp::{ flags::AF_XDP_RESERVED, frame::{BasicFrameBuffer, FrameBuffer}, }; + use super::*; + #[test] fn test_builder_defaults() { let b = UmemBuilder::new(); diff --git a/tests/sync.rs b/tests/sync.rs index f38f5db..77973d1 100644 --- a/tests/sync.rs +++ b/tests/sync.rs @@ -54,7 +54,7 @@ fn test_sync() { // Wake up the fill queue and process the fill queue umem_inner.maybe_wake_fill_queue(socket_inner.fd()).unwrap(); - umem_inner.process_fill_queue(&mut rx_buffer); + umem_inner.process_fill_queue(&mut rx_buffer).unwrap(); let mut count = 0; while count < target_count { @@ -70,7 +70,7 @@ fn test_sync() { } umem_inner.maybe_wake_fill_queue(socket_inner.fd()).unwrap(); - umem_inner.process_fill_queue(&mut rx_buffer); + let _ = umem_inner.process_fill_queue(&mut rx_buffer); } assert!(count >= target_count, "did not receive enough packets"); }); @@ -117,7 +117,7 @@ fn test_sync() { } }; - while let Err(_) = umem_outer.process_completion_queue(&mut tx_buffer) { + while umem_outer.process_completion_queue(&mut tx_buffer).is_err() { socket_outer.maybe_wake().unwrap(); }