diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..119d7b8 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,19 @@ +{ + "enabledPlugins": { + "superpowers@claude-plugins-official": true + }, + "hooks": { + "PostToolUse": [ + { + "matcher": "Write|Edit|MultiEdit", + "hooks": [ + { + "type": "command", + "command": "bash -c 'cd \"$PWD\" && if [ -f Cargo.toml ]; then cargo fmt 2>/dev/null || echo \"Cargo fmt not available\"; fi'", + "timeout": 30 + } + ] + } + ] + } +} diff --git a/.gitignore b/.gitignore index 551c56c..704ed96 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,8 @@ .terraform/ .terraform.lock.hcl *.tfstate* +*.data +*.data.old +*.perf +*.pub +local.tfvars 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/Cargo.lock b/Cargo.lock index 1015847..ffdfd52 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" @@ -207,6 +213,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bindgen" version = "0.72.1" @@ -310,6 +322,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 +411,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 +437,42 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[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 +527,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" @@ -515,20 +594,6 @@ dependencies = [ "syn", ] -[[package]] -name = "dashmap" -version = "6.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" -dependencies = [ - "cfg-if", - "crossbeam-utils", - "hashbrown", - "lock_api", - "once_cell", - "parking_lot_core", -] - [[package]] name = "derive_builder" version = "0.20.2" @@ -578,6 +643,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 +698,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 +806,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]] @@ -759,6 +849,25 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "half" version = "2.7.1" @@ -772,9 +881,18 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.5" +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 = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heck" @@ -788,12 +906,130 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-layer", + "tower-service", + "tracing", + "windows-registry", +] + +[[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 = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + [[package]] name = "ipnetwork" version = "0.20.0" @@ -834,6 +1070,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 +1236,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" @@ -1072,6 +1324,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -1228,15 +1486,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 +1544,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 +1657,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" @@ -1548,6 +1791,27 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "thiserror" version = "2.0.17" @@ -1606,12 +1870,68 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "unicode-ident" 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" @@ -1622,22 +1942,34 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" name = "voidnet" version = "0.1.0" dependencies = [ - "async-io", + "bytes", "clap", + "coarsetime", + "core_affinity", "criterion", + "crossbeam-queue", + "crossbeam-utils", "ctrlc", - "dashmap", "errno", "futures", "futures-core", "futures-util", + "getifaddrs", + "http-body-util", + "hyper", + "hyper-util", "libc", "libxdp-sys", "macro_rules_attribute", + "memchr", "memmap2", "neli", + "num_cpus", "pnet", "rand", + "rustc-hash", + "slab", + "smallvec", "smol", "smol-macros", "thiserror", @@ -1654,6 +1986,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -1666,7 +2007,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 +2073,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 +2154,51 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[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 +2210,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 +2233,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 +2345,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..93a7988 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,40 +4,43 @@ version = "0.1.0" edition = "2024" [dependencies] -dashmap = { version = "6.1", default-features = false } +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 } 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.... +libxdp-sys = { version = "=0.2.2", default-features = false } # Latest version of this library has a broken build.... +memchr = { version = "2", default-features = false } memmap2 = { version = "0.9", default-features = false } +num_cpus = { version = "1.17", 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 } +slab = { version = "0.4", default-features = false } +smallvec = { version = "1.15", default-features = false } thiserror = { version = "2", default-features = false } -tokio = { version = "1", default-features = false, features = [ - "net", -], optional = true } [dev-dependencies] +bytes = { version = "1.11" } 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" } +http-body-util = "0.1" +hyper = { version = "1", features = ["full"] } +hyper-util = { version = "0.1", features = ["full"] } +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"] -local = ["async"] -smol = ["dep:async-io", "async"] -tokio = ["dep:tokio", "async"] [profile.profiling] inherits = "release" @@ -54,35 +57,10 @@ doctest = true name = "sync" path = "tests/sync.rs" -[[test]] -name = "local" -path = "tests/local.rs" -required-features = ["local"] - -[[test]] -name = "smol" -path = "tests/smol.rs" -required-features = ["smol"] - -[[test]] -name = "tokio" -path = "tests/tokio.rs" -required-features = ["tokio"] - [[example]] name = "info" path = "examples/info.rs" -[[example]] -name = "local-rx" -path = "examples/local-rx.rs" -required-features = ["local"] - -[[example]] -name = "smol-rx" -path = "examples/smol-rx.rs" -required-features = ["smol"] - [[example]] name = "sync-rx-ms" path = "examples/sync-rx-ms.rs" @@ -100,16 +78,25 @@ name = "sync-tx" path = "examples/sync-tx.rs" [[example]] -name = "tokio-rx-ms" -path = "examples/tokio-rx-ms.rs" -required-features = ["tokio"] +name = "std-tcp-echo-server" +path = "examples/std-tcp-echo-server.rs" [[example]] -name = "tokio-rx" -path = "examples/tokio-rx.rs" -required-features = ["tokio"] +name = "std-tcp-echo-client" +path = "examples/std-tcp-echo-client.rs" -[[example]] -name = "tokio-tx" -path = "examples/tokio-tx.rs" -required-features = ["tokio"] +[[bench]] +name = "checksum" +harness = false + +[[bench]] +name = "tcp_segment" +harness = false + +[[bench]] +name = "tcp_inbound" +harness = false + +[[bench]] +name = "http_parse" +harness = false diff --git a/benches/checksum.rs b/benches/checksum.rs new file mode 100644 index 0000000..2f94072 --- /dev/null +++ b/benches/checksum.rs @@ -0,0 +1,266 @@ +use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; +use libvoid::net::checksum::{ + compute_tcp_checksum_ip, compute_udp_checksum_ip, sum_words, sum_words_carry, + verify_tcp_checksum_ip, verify_udp_checksum_ip, +}; +use libvoid::net::wire::ip::{Ipv4, Ipv4Address, Ipv6, Ipv6Address}; +use std::hint::black_box; + +fn bench_sum_words(c: &mut Criterion) { + let mut group = c.benchmark_group("sum_words"); + for size in [64, 256, 1500, 9000] { + let data: Vec = (0..size).map(|i| (i & 0xFF) as u8).collect(); + group.bench_with_input(BenchmarkId::new("bytes", size), &data, |b, data| { + b.iter(|| black_box(sum_words(data))); + }); + } + group.finish(); +} + +fn bench_sum_words_carry_odd_pending(c: &mut Criterion) { + let mut group = c.benchmark_group("sum_words_carry_odd"); + let data: Vec = (0..1499).map(|i| (i & 0xFF) as u8).collect(); + group.bench_function("1499B_pending", |b| { + b.iter(|| black_box(sum_words_carry(&data, 0, Some(0xAB)))); + }); + group.finish(); +} + +// --- TCP checksum helpers --- + +/// Build a valid TCP segment with correct checksum for IPv4. +fn make_tcp_v4_segment(payload_size: usize) -> (Ipv4Address, Ipv4Address, Vec) { + let src = Ipv4Address::new([192, 168, 1, 1]); + let dst = Ipv4Address::new([10, 0, 0, 1]); + // 20-byte TCP header (data_offset=5, ACK flag, window=65535) + let mut segment = vec![ + 0xC0, 0x00, // src port 49152 + 0x00, 0x50, // dst port 80 + 0x00, 0x00, 0x01, 0x00, // seq + 0x00, 0x00, 0x02, 0x00, // ack + 0x50, 0x10, // data_offset=5, ACK + 0xFF, 0xFF, // window + 0x00, 0x00, // checksum (zeroed) + 0x00, 0x00, // urgent + ]; + // Append payload + for i in 0..payload_size { + segment.push((i & 0xFF) as u8); + } + // Compute and fill checksum + let cksum = compute_tcp_checksum_ip::(&src, &dst, &segment, &[]); + segment[16] = cksum[0]; + segment[17] = cksum[1]; + (src, dst, segment) +} + +/// Build a valid TCP segment with correct checksum for IPv6. +fn make_tcp_v6_segment(payload_size: usize) -> (Ipv6Address, Ipv6Address, Vec) { + 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 = vec![ + 0xC0, 0x00, 0x00, 0x50, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x50, 0x10, 0xFF, + 0xFF, 0x00, 0x00, 0x00, 0x00, + ]; + for i in 0..payload_size { + segment.push((i & 0xFF) as u8); + } + let cksum = compute_tcp_checksum_ip::(&src, &dst, &segment, &[]); + segment[16] = cksum[0]; + segment[17] = cksum[1]; + (src, dst, segment) +} + +// --- UDP checksum helpers --- + +/// Build a valid UDP segment with correct checksum for IPv4. +fn make_udp_v4_segment(payload_size: usize) -> (Ipv4Address, Ipv4Address, Vec) { + let src = Ipv4Address::new([192, 168, 1, 1]); + let dst = Ipv4Address::new([10, 0, 0, 1]); + let udp_len = (8 + payload_size) as u16; + let mut segment = vec![ + 0xC0, 0x00, // src port + 0x00, 0x35, // dst port (53) + ]; + segment.extend_from_slice(&udp_len.to_be_bytes()); + segment.extend_from_slice(&[0x00, 0x00]); // checksum zeroed + for i in 0..payload_size { + segment.push((i & 0xFF) as u8); + } + // Compute checksum via from_parts (header + payload separately) + let payload = &segment[8..].to_vec(); + let cksum = compute_udp_checksum_ip::(&src, &dst, 0xC000, 0x0035, udp_len, payload); + segment[6] = cksum[0]; + segment[7] = cksum[1]; + (src, dst, segment) +} + +/// Build a valid UDP segment with correct checksum for IPv6. +fn make_udp_v6_segment(payload_size: usize) -> (Ipv6Address, Ipv6Address, Vec) { + 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 udp_len = (8 + payload_size) as u16; + let mut segment = vec![0xC0, 0x00, 0x00, 0x35]; + segment.extend_from_slice(&udp_len.to_be_bytes()); + segment.extend_from_slice(&[0x00, 0x00]); + for i in 0..payload_size { + segment.push((i & 0xFF) as u8); + } + let payload = &segment[8..].to_vec(); + let cksum = compute_udp_checksum_ip::(&src, &dst, 0xC000, 0x0035, udp_len, payload); + segment[6] = cksum[0]; + segment[7] = cksum[1]; + (src, dst, segment) +} + +// --- Benchmarks --- + +fn bench_verify_tcp_checksum(c: &mut Criterion) { + let mut group = c.benchmark_group("verify_tcp_checksum"); + for payload_size in [44, 1460] { + let label = format!("{}B", 20 + payload_size); + + let (src4, dst4, seg4) = make_tcp_v4_segment(payload_size); + group.bench_with_input(BenchmarkId::new("v4", &label), &(), |b, _| { + b.iter(|| { + black_box(verify_tcp_checksum_ip::( + black_box(&src4), + black_box(&dst4), + black_box(&seg4), + )) + }); + }); + + let (src6, dst6, seg6) = make_tcp_v6_segment(payload_size); + group.bench_with_input(BenchmarkId::new("v6", &label), &(), |b, _| { + b.iter(|| { + black_box(verify_tcp_checksum_ip::( + black_box(&src6), + black_box(&dst6), + black_box(&seg6), + )) + }); + }); + } + group.finish(); +} + +fn bench_compute_tcp_checksum(c: &mut Criterion) { + let mut group = c.benchmark_group("compute_tcp_checksum"); + for payload_size in [44, 1460] { + let label = format!("{}B", 20 + payload_size); + + let (src4, dst4, seg4) = make_tcp_v4_segment(payload_size); + // Zero the checksum field for compute benchmarks + let mut seg4_zeroed = seg4.clone(); + seg4_zeroed[16] = 0; + seg4_zeroed[17] = 0; + group.bench_with_input(BenchmarkId::new("v4", &label), &(), |b, _| { + b.iter(|| { + black_box(compute_tcp_checksum_ip::( + black_box(&src4), + black_box(&dst4), + black_box(&seg4_zeroed), + black_box(&[]), + )) + }); + }); + + let (src6, dst6, seg6) = make_tcp_v6_segment(payload_size); + let mut seg6_zeroed = seg6.clone(); + seg6_zeroed[16] = 0; + seg6_zeroed[17] = 0; + group.bench_with_input(BenchmarkId::new("v6", &label), &(), |b, _| { + b.iter(|| { + black_box(compute_tcp_checksum_ip::( + black_box(&src6), + black_box(&dst6), + black_box(&seg6_zeroed), + black_box(&[]), + )) + }); + }); + } + group.finish(); +} + +fn bench_verify_udp_checksum(c: &mut Criterion) { + let mut group = c.benchmark_group("verify_udp_checksum"); + for payload_size in [56, 1492] { + let label = format!("{}B", 8 + payload_size); + + let (src4, dst4, seg4) = make_udp_v4_segment(payload_size); + group.bench_with_input(BenchmarkId::new("v4", &label), &(), |b, _| { + b.iter(|| { + black_box(verify_udp_checksum_ip::( + black_box(&src4), + black_box(&dst4), + black_box(&seg4), + )) + }); + }); + + let (src6, dst6, seg6) = make_udp_v6_segment(payload_size); + group.bench_with_input(BenchmarkId::new("v6", &label), &(), |b, _| { + b.iter(|| { + black_box(verify_udp_checksum_ip::( + black_box(&src6), + black_box(&dst6), + black_box(&seg6), + )) + }); + }); + } + group.finish(); +} + +fn bench_compute_udp_checksum(c: &mut Criterion) { + let mut group = c.benchmark_group("compute_udp_checksum"); + for payload_size in [56, 1492] { + let label = format!("{}B", 8 + payload_size); + let udp_len = (8 + payload_size) as u16; + let payload: Vec = (0..payload_size).map(|i| (i & 0xFF) as u8).collect(); + + let src4 = Ipv4Address::new([192, 168, 1, 1]); + let dst4 = Ipv4Address::new([10, 0, 0, 1]); + group.bench_with_input(BenchmarkId::new("v4", &label), &(), |b, _| { + b.iter(|| { + black_box(compute_udp_checksum_ip::( + black_box(&src4), + black_box(&dst4), + black_box(0xC000), + black_box(0x0035), + black_box(udp_len), + black_box(&payload), + )) + }); + }); + + let src6 = Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + let dst6 = Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]); + group.bench_with_input(BenchmarkId::new("v6", &label), &(), |b, _| { + b.iter(|| { + black_box(compute_udp_checksum_ip::( + black_box(&src6), + black_box(&dst6), + black_box(0xC000), + black_box(0x0035), + black_box(udp_len), + black_box(&payload), + )) + }); + }); + } + group.finish(); +} + +criterion_group!( + benches, + bench_sum_words, + bench_sum_words_carry_odd_pending, + bench_verify_tcp_checksum, + bench_compute_tcp_checksum, + bench_verify_udp_checksum, + bench_compute_udp_checksum, +); +criterion_main!(benches); diff --git a/benches/http_parse.rs b/benches/http_parse.rs new file mode 100644 index 0000000..a723b88 --- /dev/null +++ b/benches/http_parse.rs @@ -0,0 +1,66 @@ +use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; +use libvoid::net::http::codec::parse; + +fn make_request(header_count: usize) -> Vec { + let mut buf = b"GET /index.html HTTP/1.1\r\n".to_vec(); + for i in 0..header_count { + buf.extend_from_slice( + format!("X-Header-{i}: value-{i}-padding-to-make-it-longer\r\n").as_bytes(), + ); + } + buf.extend_from_slice(b"\r\n"); + buf +} + +fn bench_memchr_newline(c: &mut Criterion) { + let mut group = c.benchmark_group("memchr_newline"); + let short = b"GET /index.html HTTP/1.1\r\n"; + let long = b"GET /index.html/with/a/longer/path/to/make/it/more/realistic/for/benchmarking HTTP/1.1\r\n"; + group.bench_function("short_26B", |b| { + b.iter(|| std::hint::black_box(parse::memchr_newline(short))); + }); + group.bench_function("long_90B", |b| { + b.iter(|| std::hint::black_box(parse::memchr_newline(long))); + }); + group.finish(); +} + +fn bench_parse_request_line(c: &mut Criterion) { + let mut group = c.benchmark_group("parse_request_line"); + let simple = b"GET /index.html HTTP/1.1\r\n"; + let long_path = b"GET /api/v2/users/12345/documents/67890/metadata HTTP/1.1\r\n"; + group.bench_function("simple", |b| { + b.iter(|| std::hint::black_box(parse::parse_request_line(simple, 0))); + }); + group.bench_function("long_path", |b| { + b.iter(|| std::hint::black_box(parse::parse_request_line(long_path, 0))); + }); + group.finish(); +} + +fn bench_parse_headers(c: &mut Criterion) { + let mut group = c.benchmark_group("parse_headers"); + for count in [2, 8, 32, 64] { + let req = make_request(count); + // Skip past the request line to get just the header block. + let header_start = memchr::memchr(b'\n', &req).unwrap() + 1; + let header_buf = &req[header_start..]; + let label = format!("{count}_headers_{}B", header_buf.len()); + group.bench_with_input( + BenchmarkId::new("count", &label), + &header_buf.to_vec(), + |b, buf| { + b.iter(|| std::hint::black_box(parse::parse_headers(buf))); + }, + ); + } + group.finish(); +} + +criterion_group!( + benches, + bench_memchr_newline, + bench_parse_request_line, + bench_parse_headers +); +criterion_main!(benches); diff --git a/benches/tcp_inbound.rs b/benches/tcp_inbound.rs new file mode 100644 index 0000000..ee9bc3d --- /dev/null +++ b/benches/tcp_inbound.rs @@ -0,0 +1,316 @@ +#![allow(clippy::too_many_arguments)] + +use criterion::{Criterion, criterion_group, criterion_main}; +use libvoid::net::NeighborHandler; +use libvoid::net::checksum::{compute_ipv4_checksum, compute_tcp_checksum_ip}; +use libvoid::net::handler::tcp::TcpHandler; +use libvoid::net::wire::ethernet::MacAddress; +use libvoid::net::wire::ip::{IPV4_MIN_HEADER_LEN, IpAddress, IpProtocols, Ipv4, Ipv4Address}; +use libvoid::net::wire::tcp::{TCP_HEADER_LEN, flags}; +use libvoid::xdp::frame::{BasicFrameBuffer, Frame, FrameBuffer}; + +const ETH_LEN: usize = 14; +const LOCAL_IP: Ipv4Address = Ipv4Address::new([10, 0, 0, 1]); +const REMOTE_IP: Ipv4Address = Ipv4Address::new([10, 0, 0, 2]); +const SRC_MAC: MacAddress = MacAddress::new([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]); +const DST_MAC: MacAddress = MacAddress::new([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]); + +/// Pool of pre-allocated frame buffers to avoid leaking memory in benchmark loops. +struct FramePool { + bufs: Vec<*mut [u8]>, + idx: usize, +} + +impl FramePool { + fn new(count: usize) -> Self { + let bufs = (0..count) + .map(|_| Box::leak(vec![0u8; 2048].into_boxed_slice()) as *mut [u8]) + .collect(); + Self { bufs, idx: 0 } + } + + fn next(&mut self) -> &'static mut [u8] { + // Safety: we cycle through buffers. Each buffer is only used by one Frame at a time + // because the previous Frame was consumed/dropped before we reuse the buffer. + let buf = unsafe { &mut *self.bufs[self.idx] }; + self.idx = (self.idx + 1) % self.bufs.len(); + buf + } +} + +fn write_tcp_frame_into( + buf: &mut [u8], + src_ip: Ipv4Address, + dst_ip: Ipv4Address, + src_port: u16, + dst_port: u16, + seq: u32, + ack: u32, + tcp_flags: u8, + window: u16, +) -> usize { + let frame_len = ETH_LEN + IPV4_MIN_HEADER_LEN + TCP_HEADER_LEN; + let total_ip_len = (IPV4_MIN_HEADER_LEN + TCP_HEADER_LEN) as u16; + buf[..frame_len].fill(0); + + buf[0..6].copy_from_slice(&SRC_MAC.octets); + buf[6..12].copy_from_slice(&DST_MAC.octets); + buf[12] = 0x08; + buf[13] = 0x00; + + let ip = ETH_LEN; + buf[ip] = 0x45; + buf[ip + 2..ip + 4].copy_from_slice(&total_ip_len.to_be_bytes()); + buf[ip + 6] = 0x40; + buf[ip + 8] = 64; + buf[ip + 9] = IpProtocols::Tcp; + let src_b: [u8; 4] = src_ip.into(); + let dst_b: [u8; 4] = dst_ip.into(); + buf[ip + 12..ip + 16].copy_from_slice(&src_b); + buf[ip + 16..ip + 20].copy_from_slice(&dst_b); + let ck = compute_ipv4_checksum(&buf[ip..ip + IPV4_MIN_HEADER_LEN]); + buf[ip + 10] = ck[0]; + buf[ip + 11] = ck[1]; + + let tcp = ip + IPV4_MIN_HEADER_LEN; + buf[tcp..tcp + 2].copy_from_slice(&src_port.to_be_bytes()); + buf[tcp + 2..tcp + 4].copy_from_slice(&dst_port.to_be_bytes()); + buf[tcp + 4..tcp + 8].copy_from_slice(&seq.to_be_bytes()); + buf[tcp + 8..tcp + 12].copy_from_slice(&ack.to_be_bytes()); + buf[tcp + 12] = 5 << 4; + buf[tcp + 13] = tcp_flags; + buf[tcp + 14..tcp + 16].copy_from_slice(&window.to_be_bytes()); + let tc = compute_tcp_checksum_ip::(&src_ip, &dst_ip, &buf[tcp..frame_len], &[]); + buf[tcp + 16] = tc[0]; + buf[tcp + 17] = tc[1]; + + frame_len +} + +fn write_tcp_data_frame_into( + buf: &mut [u8], + src_ip: Ipv4Address, + dst_ip: Ipv4Address, + src_port: u16, + dst_port: u16, + seq: u32, + ack: u32, + tcp_flags: u8, + window: u16, + payload: &[u8], +) -> usize { + let frame_len = ETH_LEN + IPV4_MIN_HEADER_LEN + TCP_HEADER_LEN + payload.len(); + let total_ip_len = (frame_len - ETH_LEN) as u16; + buf[..frame_len].fill(0); + + buf[0..6].copy_from_slice(&SRC_MAC.octets); + buf[6..12].copy_from_slice(&DST_MAC.octets); + buf[12] = 0x08; + buf[13] = 0x00; + + let ip = ETH_LEN; + buf[ip] = 0x45; + buf[ip + 2..ip + 4].copy_from_slice(&total_ip_len.to_be_bytes()); + buf[ip + 6] = 0x40; + buf[ip + 8] = 64; + buf[ip + 9] = IpProtocols::Tcp; + let src_b: [u8; 4] = src_ip.into(); + let dst_b: [u8; 4] = dst_ip.into(); + buf[ip + 12..ip + 16].copy_from_slice(&src_b); + buf[ip + 16..ip + 20].copy_from_slice(&dst_b); + let ck = compute_ipv4_checksum(&buf[ip..ip + IPV4_MIN_HEADER_LEN]); + buf[ip + 10] = ck[0]; + buf[ip + 11] = ck[1]; + + let tcp = ip + IPV4_MIN_HEADER_LEN; + buf[tcp..tcp + 2].copy_from_slice(&src_port.to_be_bytes()); + buf[tcp + 2..tcp + 4].copy_from_slice(&dst_port.to_be_bytes()); + buf[tcp + 4..tcp + 8].copy_from_slice(&seq.to_be_bytes()); + buf[tcp + 8..tcp + 12].copy_from_slice(&ack.to_be_bytes()); + buf[tcp + 12] = 5 << 4; + buf[tcp + 13] = tcp_flags; + buf[tcp + 14..tcp + 16].copy_from_slice(&window.to_be_bytes()); + let ps = tcp + TCP_HEADER_LEN; + buf[ps..ps + payload.len()].copy_from_slice(payload); + let tc = + compute_tcp_checksum_ip::(&src_ip, &dst_ip, &buf[tcp..tcp + TCP_HEADER_LEN], payload); + buf[tcp + 16] = tc[0]; + buf[tcp + 17] = tc[1]; + + frame_len +} + +fn alloc_buf() -> &'static mut [u8] { + Box::leak(vec![0u8; 2048].into_boxed_slice()) +} + +fn setup_established( + handler: &mut TcpHandler, + nh: &NeighborHandler, + free: &mut BasicFrameBuffer<'static>, + rx: &mut BasicFrameBuffer<'static>, + tx: &mut BasicFrameBuffer<'static>, +) -> u32 { + let now = coarsetime::Instant::now(); + let _ = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128); + + let syn = alloc_buf(); + let syn_len = write_tcp_frame_into( + syn, + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + ); + handler.process_ipv4(Frame::new(0, syn, syn_len, false), now, nh, free, rx, tx); + + let syn_ack = tx.pop().unwrap(); + let t = ETH_LEN + IPV4_MIN_HEADER_LEN; + let iss = u32::from_be_bytes([ + syn_ack[t + 4], + syn_ack[t + 5], + syn_ack[t + 6], + syn_ack[t + 7], + ]); + free.push(syn_ack); + + let ack = alloc_buf(); + let ack_len = write_tcp_frame_into( + ack, + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + iss + 1, + flags::ACK, + 65535, + ); + handler.process_ipv4(Frame::new(0, ack, ack_len, false), now, nh, free, rx, tx); + while let Some(f) = tx.pop() { + free.push(f); + } + while let Some(f) = rx.pop() { + free.push(f); + } + + iss +} + +fn bench_process_data(c: &mut Criterion) { + c.bench_function("process_inbound/data_1400B", |b| { + b.iter_custom(|iters| { + let mut handler = TcpHandler::new(false, false); + let nh = NeighborHandler::new("lo", coarsetime::Duration::from_secs(60)).unwrap(); + nh.seed_cache( + coarsetime::Instant::now(), + IpAddress::V4(REMOTE_IP), + DST_MAC, + ); + + let mut free = BasicFrameBuffer::new(64); + let mut rx = BasicFrameBuffer::new(64); + let mut tx = BasicFrameBuffer::new(64); + for _ in 0..64 { + free.push(Frame::new(0, alloc_buf(), 2048, false)); + } + + let iss = setup_established(&mut handler, &nh, &mut free, &mut rx, &mut tx); + let payload = vec![0xABu8; 1400]; + let mut seq = 1001u32; + // Pool of 128 buffers cycled through — each buffer is reused after its Frame is consumed. + let mut pool = FramePool::new(128); + + let start = std::time::Instant::now(); + for _ in 0..iters { + let buf = pool.next(); + let len = write_tcp_data_frame_into( + buf, + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + seq, + iss + 1, + flags::ACK | flags::PSH, + 65535, + &payload, + ); + handler.process_ipv4( + Frame::new(0, buf, len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + seq = seq.wrapping_add(payload.len() as u32); + while let Some(f) = tx.pop() { + free.push(f); + } + while rx.pop().is_some() {} + } + start.elapsed() + }); + }); +} + +fn bench_process_ack(c: &mut Criterion) { + c.bench_function("process_inbound/ack", |b| { + b.iter_custom(|iters| { + let mut handler = TcpHandler::new(false, false); + let nh = NeighborHandler::new("lo", coarsetime::Duration::from_secs(60)).unwrap(); + nh.seed_cache( + coarsetime::Instant::now(), + IpAddress::V4(REMOTE_IP), + DST_MAC, + ); + + let mut free = BasicFrameBuffer::new(64); + let mut rx = BasicFrameBuffer::new(64); + let mut tx = BasicFrameBuffer::new(64); + for _ in 0..64 { + free.push(Frame::new(0, alloc_buf(), 2048, false)); + } + + let iss = setup_established(&mut handler, &nh, &mut free, &mut rx, &mut tx); + let mut pool = FramePool::new(128); + + let start = std::time::Instant::now(); + for _ in 0..iters { + let buf = pool.next(); + let len = write_tcp_frame_into( + buf, + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + iss + 1, + flags::ACK, + 65535, + ); + handler.process_ipv4( + Frame::new(0, buf, len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while let Some(f) = tx.pop() { + free.push(f); + } + while rx.pop().is_some() {} + } + start.elapsed() + }); + }); +} + +criterion_group!(benches, bench_process_data, bench_process_ack); +criterion_main!(benches); diff --git a/benches/tcp_segment.rs b/benches/tcp_segment.rs new file mode 100644 index 0000000..8bd017a --- /dev/null +++ b/benches/tcp_segment.rs @@ -0,0 +1,227 @@ +use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; +use libvoid::net::handler::tcp::SegmentBuilder; +use libvoid::net::wire::{ + ethernet::MacAddress, + ip::{IpAddress, Ipv4Address, Ipv6Address}, + tcp::flags, +}; +use libvoid::xdp::frame::{BasicFrameBuffer, Frame, FrameBuffer}; +use std::hint::black_box; + +/// Allocate a single reusable frame. Leaked intentionally — lives for the benchmark's lifetime. +fn alloc_frame() -> Frame<'static> { + let buf = Box::leak(vec![0u8; 2048].into_boxed_slice()); + Frame::new(0, buf, 2048, false) +} + +const SRC_MAC: MacAddress = MacAddress::new([0xAA; 6]); +const DST_MAC: MacAddress = MacAddress::new([0xBB; 6]); +const V4_LOCAL: Ipv4Address = Ipv4Address::new([10, 0, 0, 1]); +const V4_REMOTE: Ipv4Address = Ipv4Address::new([10, 0, 0, 2]); +const V6_LOCAL: Ipv6Address = + Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); +const V6_REMOTE: Ipv6Address = + Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]); + +fn bench_build_syn(c: &mut Criterion) { + let mut group = c.benchmark_group("build_syn"); + + group.bench_function("v4", |b| { + let mut free = BasicFrameBuffer::new(1); + let mut tx = BasicFrameBuffer::new(1); + free.push(alloc_frame()); + b.iter(|| { + SegmentBuilder::build_syn( + black_box(IpAddress::V4(V4_LOCAL)), + black_box(IpAddress::V4(V4_REMOTE)), + 8080, + 80, + 1000, + 65535, + 1460, + 7, + Some((100, 0)), + true, + false, + SRC_MAC, + DST_MAC, + false, + &mut free, + &mut tx, + ); + // Recycle frame back to free pool. + free.push(tx.pop().unwrap()); + }); + }); + + group.bench_function("v6", |b| { + let mut free = BasicFrameBuffer::new(1); + let mut tx = BasicFrameBuffer::new(1); + free.push(alloc_frame()); + b.iter(|| { + SegmentBuilder::build_syn( + black_box(IpAddress::V6(V6_LOCAL)), + black_box(IpAddress::V6(V6_REMOTE)), + 8080, + 80, + 1000, + 65535, + 1460, + 7, + Some((100, 0)), + true, + false, + SRC_MAC, + DST_MAC, + false, + &mut free, + &mut tx, + ); + free.push(tx.pop().unwrap()); + }); + }); + + group.finish(); +} + +fn bench_build_ack(c: &mut Criterion) { + let mut group = c.benchmark_group("build_ack"); + + group.bench_function("v4", |b| { + let mut free = BasicFrameBuffer::new(1); + let mut tx = BasicFrameBuffer::new(1); + free.push(alloc_frame()); + b.iter(|| { + SegmentBuilder::build_ack( + black_box(IpAddress::V4(V4_LOCAL)), + black_box(IpAddress::V4(V4_REMOTE)), + 8080, + 80, + 1000, + 500, + 65535, + flags::ACK, + Some((100, 50)), + SRC_MAC, + DST_MAC, + false, + &mut free, + &mut tx, + ); + free.push(tx.pop().unwrap()); + }); + }); + + group.bench_function("v6", |b| { + let mut free = BasicFrameBuffer::new(1); + let mut tx = BasicFrameBuffer::new(1); + free.push(alloc_frame()); + b.iter(|| { + SegmentBuilder::build_ack( + black_box(IpAddress::V6(V6_LOCAL)), + black_box(IpAddress::V6(V6_REMOTE)), + 8080, + 80, + 1000, + 500, + 65535, + flags::ACK, + Some((100, 50)), + SRC_MAC, + DST_MAC, + false, + &mut free, + &mut tx, + ); + free.push(tx.pop().unwrap()); + }); + }); + + group.finish(); +} + +fn bench_build_data(c: &mut Criterion) { + let mut group = c.benchmark_group("build_data"); + let payload = vec![0xABu8; 1400]; + + for (label, local, remote) in [ + ("v4", IpAddress::V4(V4_LOCAL), IpAddress::V4(V4_REMOTE)), + ("v6", IpAddress::V6(V6_LOCAL), IpAddress::V6(V6_REMOTE)), + ] { + group.bench_with_input(BenchmarkId::new(label, "1400B"), &(), |b, _| { + let mut free = BasicFrameBuffer::new(1); + let mut tx = BasicFrameBuffer::new(1); + free.push(alloc_frame()); + b.iter(|| { + SegmentBuilder::build_data( + black_box(local), + black_box(remote), + 8080, + 80, + 1000, + 500, + 65535, + black_box(&payload), + Some((100, 50)), + SRC_MAC, + DST_MAC, + false, + &mut free, + &mut tx, + ); + free.push(tx.pop().unwrap()); + }); + }); + } + + group.finish(); +} + +fn bench_build_data_from_slices(c: &mut Criterion) { + let mut group = c.benchmark_group("build_data_from_slices"); + let slice1 = vec![0xABu8; 700]; + let slice2 = vec![0xCDu8; 700]; + + for (label, local, remote) in [ + ("v4", IpAddress::V4(V4_LOCAL), IpAddress::V4(V4_REMOTE)), + ("v6", IpAddress::V6(V6_LOCAL), IpAddress::V6(V6_REMOTE)), + ] { + group.bench_with_input(BenchmarkId::new(label, "2x700B"), &(), |b, _| { + let mut free = BasicFrameBuffer::new(1); + let mut tx = BasicFrameBuffer::new(1); + free.push(alloc_frame()); + b.iter(|| { + SegmentBuilder::build_data_from_slices( + black_box(local), + black_box(remote), + 8080, + 80, + 1000, + 500, + 65535, + (black_box(&slice1[..]), black_box(&slice2[..])), + flags::ACK, + false, + Some((100, 50)), + SRC_MAC, + DST_MAC, + false, + &mut free, + &mut tx, + ); + free.push(tx.pop().unwrap()); + }); + }); + } + + group.finish(); +} + +criterion_group!( + benches, + bench_build_syn, + bench_build_ack, + bench_build_data, + bench_build_data_from_slices, +); +criterion_main!(benches); diff --git a/bpf/xdp_kern.c b/bpf/xdp_kern.c index bdcfccd..cbf6f05 100644 --- a/bpf/xdp_kern.c +++ b/bpf/xdp_kern.c @@ -3,55 +3,17 @@ char LICENSE[] SEC("license") = "GPL"; -// Our socket map for redirects, this is filled by the user space application as sockets are registered. +// Socket map for redirects, filled by userspace as sockets are registered. +// Indexed by queue ID (sparse — gaps are allowed for unbound queues). struct { __uint(type, BPF_MAP_TYPE_XSKMAP); - __uint(max_entries, 2048); // Having more then 2048 sockets is unlikely to be useful in practice.... + __uint(max_entries, 2048); __uint(key_size, sizeof(int)); __uint(value_size, sizeof(int)); } xsks_map SEC(".maps"); -// Per-CPU Array of current round robin indexes, all initialized to 0 and as a per-cpu array we need no synchronization. -struct { - __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY); - __uint(max_entries, 1); - __uint(key_size, sizeof(int)); - __uint(value_size, sizeof(unsigned int)); -} rr_map SEC(".maps"); - -// The total number of sockets registered, this is updated by the user space application as sockets are registered. -unsigned int num_socks = 0; - -// The key for the round robin map, this is used to index into the per-CPU array. -static unsigned int rr_key = 0; - +// Route packets from hardware queue N to socket N. +// If no socket is registered for this queue, XDP_PASS hands the packet to the kernel stack. SEC("xdp_sock") int xdp_sock_prog(struct xdp_md *ctx) { - // We don't have any user space sockets to hand off traffic to, so just pass it off to the kernel. - if (num_socks == 0) { - return XDP_PASS; - } - - // We have exactly one user space socket, so fast path directly into the redirect map. - if (num_socks == 1) { - return bpf_redirect_map(&xsks_map, 0, XDP_ABORTED); - } - - // Wrapping RR algo, this is inefficient but flexible and means we can have any number of sockets, - // a potentially "faster" approach would be to use bit masking and force power of 2 num sockets. - // - // Testing has proven this to be more than efficent enough for this use case as the I/O operations themselves - // dwarf these extra division operations. - - // First lookup the current round robin index for the current CPU. - unsigned int *rr_ptr = (unsigned int *)bpf_map_lookup_elem(&rr_map, &rr_key); - if (!rr_ptr) { - return XDP_ABORTED; - } - - // Increment the round robin index and wrap around if it exceeds the number of sockets. Again this could be faster with a bit mask - // approach but then we are forced into a power of 2 number of sockets. - *rr_ptr = (*rr_ptr + 1) % num_socks; - - // Return the redirect to the given rr index, if something goes wrong abort the packet. - return bpf_redirect_map(&xsks_map, *rr_ptr, XDP_ABORTED); + return bpf_redirect_map(&xsks_map, ctx->rx_queue_index, XDP_PASS); } diff --git a/dist/docs/rfc1191-pmtud_v4.txt b/dist/docs/rfc1191-pmtud_v4.txt new file mode 100644 index 0000000..1d6a42f --- /dev/null +++ b/dist/docs/rfc1191-pmtud_v4.txt @@ -0,0 +1,1083 @@ + + + + +Network Working Group J. Mogul +Request for Comments: 1191 DECWRL +Obsoletes: RFC 1063 S. Deering + Stanford University + November 1990 + + Path MTU Discovery + + +Status of this Memo + + This RFC specifies a protocol on the IAB Standards Track for the + Internet community, and requests discussion and suggestions for + improvements. Please refer to the current edition of the "IAB + Official Protocol Standards" for the standardization state and status + of this protocol. Distribution of this memo is unlimited. + + + Table of Contents + + Status of this Memo 1 + Abstract 2 + Acknowledgements 2 + 1. Introduction 2 + 2. Protocol overview 3 + 3. Host specification 4 + 3.1. TCP MSS Option 5 + 4. Router specification 6 + 5. Host processing of old-style messages 7 + 6. Host implementation 8 + 6.1. Layering 9 + 6.2. Storing PMTU information 10 + 6.3. Purging stale PMTU information 11 + 6.4. TCP layer actions 13 + 6.5. Issues for other transport protocols 14 + 6.6. Management interface 15 + 7. Likely values for Path MTUs 15 + 7.1. A better way to detect PMTU increases 16 + 8. Security considerations 18 + References 18 + Authors' Addresses 19 + + + List of Tables + + Table 7-1: Common MTUs in the Internet 17 + + + + + + +Mogul & Deering [page 1] + + +RFC 1191 Path MTU Discovery November 1990 + + + + +Abstract + + This memo describes a technique for dynamically discovering the + maximum transmission unit (MTU) of an arbitrary internet path. It + specifies a small change to the way routers generate one type of ICMP + message. For a path that passes through a router that has not been + so changed, this technique might not discover the correct Path MTU, + but it will always choose a Path MTU as accurate as, and in many + cases more accurate than, the Path MTU that would be chosen by + current practice. + + +Acknowledgements + + This proposal is a product of the IETF MTU Discovery Working Group. + + The mechanism proposed here was first suggested by Geof Cooper [2], + who in two short paragraphs set out all the basic ideas that took the + Working Group months to reinvent. + + +1. Introduction + + When one IP host has a large amount of data to send to another host, + the data is transmitted as a series of IP datagrams. It is usually + preferable that these datagrams be of the largest size that does not + require fragmentation anywhere along the path from the source to the + destination. (For the case against fragmentation, see [5].) This + datagram size is referred to as the Path MTU (PMTU), and it is equal + to the minimum of the MTUs of each hop in the path. A shortcoming of + the current Internet protocol suite is the lack of a standard + mechanism for a host to discover the PMTU of an arbitrary path. + + Note: The Path MTU is what in [1] is called the "Effective MTU + for sending" (EMTU_S). A PMTU is associated with a path, + which is a particular combination of IP source and destination + address and perhaps a Type-of-service (TOS). + + The current practice [1] is to use the lesser of 576 and the + first-hop MTU as the PMTU for any destination that is not connected + to the same network or subnet as the source. In many cases, this + results in the use of smaller datagrams than necessary, because many + paths have a PMTU greater than 576. A host sending datagrams much + smaller than the Path MTU allows is wasting Internet resources and + probably getting suboptimal throughput. Furthermore, current + practice does not prevent fragmentation in all cases, since there are + some paths whose PMTU is less than 576. + + +Mogul & Deering [page 2] + + +RFC 1191 Path MTU Discovery November 1990 + + + + + It is expected that future routing protocols will be able to provide + accurate PMTU information within a routing area, although perhaps not + across multi-level routing hierarchies. It is not clear how soon + that will be ubiquitously available, so for the next several years + the Internet needs a simple mechanism that discovers PMTUs without + wasting resources and that works before all hosts and routers are + modified. + + +2. Protocol overview + + In this memo, we describe a technique for using the Don't Fragment + (DF) bit in the IP header to dynamically discover the PMTU of a path. + The basic idea is that a source host initially assumes that the PMTU + of a path is the (known) MTU of its first hop, and sends all + datagrams on that path with the DF bit set. If any of the datagrams + are too large to be forwarded without fragmentation by some router + along the path, that router will discard them and return ICMP + Destination Unreachable messages with a code meaning "fragmentation + needed and DF set" [7]. Upon receipt of such a message (henceforth + called a "Datagram Too Big" message), the source host reduces its + assumed PMTU for the path. + + The PMTU discovery process ends when the host's estimate of the PMTU + is low enough that its datagrams can be delivered without + fragmentation. Or, the host may elect to end the discovery process + by ceasing to set the DF bit in the datagram headers; it may do so, + for example, because it is willing to have datagrams fragmented in + some circumstances. Normally, the host continues to set DF in all + datagrams, so that if the route changes and the new PMTU is lower, it + will be discovered. + + Unfortunately, the Datagram Too Big message, as currently specified, + does not report the MTU of the hop for which the rejected datagram + was too big, so the source host cannot tell exactly how much to + reduce its assumed PMTU. To remedy this, we propose that a currently + unused header field in the Datagram Too Big message be used to report + the MTU of the constricting hop. This is the only change specified + for routers in support of PMTU Discovery. + + The PMTU of a path may change over time, due to changes in the + routing topology. Reductions of the PMTU are detected by Datagram + Too Big messages, except on paths for which the host has stopped + setting the DF bit. To detect increases in a path's PMTU, a host + periodically increases its assumed PMTU (and if it had stopped, + resumes setting the DF bit). This will almost always result in + datagrams being discarded and Datagram Too Big messages being + + +Mogul & Deering [page 3] + + +RFC 1191 Path MTU Discovery November 1990 + + + + + generated, because in most cases the PMTU of the path will not have + changed, so it should be done infrequently. + + Since this mechanism essentially guarantees that host will not + receive any fragments from a peer doing PMTU Discovery, it may aid in + interoperating with certain hosts that (improperly) are unable to + reassemble fragmented datagrams. + + +3. Host specification + + When a host receives a Datagram Too Big message, it MUST reduce its + estimate of the PMTU for the relevant path, based on the value of the + Next-Hop MTU field in the message (see section 4). We do not specify + the precise behavior of a host in this circumstance, since different + applications may have different requirements, and since different + implementation architectures may favor different strategies. + + We do require that after receiving a Datagram Too Big message, a host + MUST attempt to avoid eliciting more such messages in the near + future. The host may either reduce the size of the datagrams it is + sending along the path, or cease setting the Don't Fragment bit in + the headers of those datagrams. Clearly, the former strategy may + continue to elicit Datagram Too Big messages for a while, but since + each of these messages (and the dropped datagrams they respond to) + consume Internet resources, the host MUST force the PMTU Discovery + process to converge. + + Hosts using PMTU Discovery MUST detect decreases in Path MTU as fast + as possible. Hosts MAY detect increases in Path MTU, but because + doing so requires sending datagrams larger than the current estimated + PMTU, and because the likelihood is that the PMTU will not have + increased, this MUST be done at infrequent intervals. An attempt to + detect an increase (by sending a datagram larger than the current + estimate) MUST NOT be done less than 5 minutes after a Datagram Too + Big message has been received for the given destination, or less than + 1 minute after a previous, successful attempted increase. We + recommend setting these timers at twice their minimum values (10 + minutes and 2 minutes, respectively). + + Hosts MUST be able to deal with Datagram Too Big messages that do not + include the next-hop MTU, since it is not feasible to upgrade all the + routers in the Internet in any finite time. A Datagram Too Big + message from an unmodified router can be recognized by the presence + of a zero in the (newly-defined) Next-Hop MTU field. (This is + required by the ICMP specification [7], which says that "unused" + fields must be zero.) In section 5, we discuss possible strategies + + +Mogul & Deering [page 4] + + +RFC 1191 Path MTU Discovery November 1990 + + + + + for a host to follow in response to an old-style Datagram Too Big + message (one sent by an unmodified router). + + A host MUST never reduce its estimate of the Path MTU below 68 + octets. + + A host MUST not increase its estimate of the Path MTU in response to + the contents of a Datagram Too Big message. A message purporting to + announce an increase in the Path MTU might be a stale datagram that + has been floating around in the Internet, a false packet injected as + part of a denial-of-service attack, or the result of having multiple + paths to the destination. + + +3.1. TCP MSS Option + + A host doing PMTU Discovery must obey the rule that it not send IP + datagrams larger than 576 octets unless it has permission from the + receiver. For TCP connections, this means that a host must not send + datagrams larger than 40 octets plus the Maximum Segment Size (MSS) + sent by its peer. + + Note: The TCP MSS is defined to be the relevant IP datagram + size minus 40 [9]. The default of 576 octets for the maximum + IP datagram size yields a default of 536 octets for the TCP + MSS. + + Section 4.2.2.6 of "Requirements for Internet Hosts -- Communication + Layers" [1] says: + + Some TCP implementations send an MSS option only if the + destination host is on a non-connected network. However, in + general the TCP layer may not have the appropriate information + to make this decision, so it is preferable to leave to the IP + layer the task of determining a suitable MTU for the Internet + path. + + Actually, many TCP implementations always send an MSS option, but set + the value to 536 if the destination is non-local. This behavior was + correct when the Internet was full of hosts that did not follow the + rule that datagrams larger than 576 octets should not be sent to + non-local destinations. Now that most hosts do follow this rule, it + is unnecessary to limit the value in the TCP MSS option to 536 for + non-local peers. + + Moreover, doing this prevents PMTU Discovery from discovering PMTUs + larger than 576, so hosts SHOULD no longer lower the value they send + + +Mogul & Deering [page 5] + + +RFC 1191 Path MTU Discovery November 1990 + + + + + in the MSS option. The MSS option should be 40 octets less than the + size of the largest datagram the host is able to reassemble (MMS_R, + as defined in [1]); in many cases, this will be the architectural + limit of 65495 (65535 - 40) octets. A host MAY send an MSS value + derived from the MTU of its connected network (the maximum MTU over + its connected networks, for a multi-homed host); this should not + cause problems for PMTU Discovery, and may dissuade a broken peer + from sending enormous datagrams. + + Note: At the moment, we see no reason to send an MSS greater + than the maximum MTU of the connected networks, and we + recommend that hosts do not use 65495. It is quite possible + that some IP implementations have sign-bit bugs that would be + tickled by unnecessary use of such a large MSS. + + +4. Router specification + + When a router is unable to forward a datagram because it exceeds the + MTU of the next-hop network and its Don't Fragment bit is set, the + router is required to return an ICMP Destination Unreachable message + to the source of the datagram, with the Code indicating + "fragmentation needed and DF set". To support the Path MTU Discovery + technique specified in this memo, the router MUST include the MTU of + that next-hop network in the low-order 16 bits of the ICMP header + field that is labelled "unused" in the ICMP specification [7]. The + high-order 16 bits remain unused, and MUST be set to zero. Thus, the + message has the following 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type = 3 | Code = 4 | Checksum | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | unused = 0 | Next-Hop MTU | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Internet Header + 64 bits of Original Datagram Data | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + + The value carried in the Next-Hop MTU field is: + + The size in octets of the largest datagram that could be + forwarded, along the path of the original datagram, without + being fragmented at this router. The size includes the IP + header and IP data, and does not include any lower-level + headers. + + +Mogul & Deering [page 6] + + +RFC 1191 Path MTU Discovery November 1990 + + + + + This field will never contain a value less than 68, since every + router "must be able to forward a datagram of 68 octets without + fragmentation" [8]. + + +5. Host processing of old-style messages + + In this section we outline several possible strategies for a host to + follow upon receiving a Datagram Too Big message from an unmodified + router (i.e., one where the Next-Hop MTU field is zero). This + section is not part of the protocol specification. + + The simplest thing for a host to do in response to such a message is + to assume that the PMTU is the minimum of its currently-assumed PMTU + and 576, and to stop setting the DF bit in datagrams sent on that + path. Thus, the host falls back to the same PMTU as it would choose + under current practice (see section 3.3.3 of "Requirements for + Internet Hosts -- Communication Layers" [1]). This strategy has the + advantage that it terminates quickly, and does no worse than existing + practice. It fails, however, to avoid fragmentation in some cases, + and to make the most efficient utilization of the internetwork in + other cases. + + More sophisticated strategies involve "searching" for an accurate + PMTU estimate, by continuing to send datagrams with the DF bit while + varying their sizes. A good search strategy is one that obtains an + accurate estimate of the Path MTU without causing many packets to be + lost in the process. + + Several possible strategies apply algorithmic functions to the + previous PMTU estimate to generate a new estimate. For example, one + could multiply the old estimate by a constant (say, 0.75). We do NOT + recommend this; it either converges far too slowly, or it + substantially underestimates the true PMTU. + + A more sophisticated approach is to do a binary search on the packet + size. This converges somewhat faster, although it still takes 4 or 5 + steps to converge from an FDDI MTU to an Ethernet MTU. A serious + disadvantage is that it requires a complex implementation in order to + recognize when a datagram has made it to the other end (indicating + that the current estimate is too low). We also do not recommend this + strategy. + + One strategy that appears to work quite well starts from the + observation that there are, in practice, relatively few MTU values in + use in the Internet. Thus, rather than blindly searching through + arbitrarily chosen values, we can search only the ones that are + + +Mogul & Deering [page 7] + + +RFC 1191 Path MTU Discovery November 1990 + + + + + likely to appear. Moreover, since designers tend to chose MTUs in + similar ways, it is possible to collect groups of similar MTU values + and use the lowest value in the group as our search "plateau". (It + is clearly better to underestimate an MTU by a few per cent than to + overestimate it by one octet.) + + In section 7, we describe how we arrived at a table of representative + MTU plateaus for use in PMTU estimation. With this table, + convergence is as good as binary search in the worst case, and is far + better in common cases (for example, it takes only two round-trip + times to go from an FDDI MTU to an Ethernet MTU). Since the plateaus + lie near powers of two, if an MTU is not represented in this table, + the algorithm will not underestimate it by more than a factor of 2. + + Any search strategy must have some "memory" of previous estimates in + order to chose the next one. One approach is to use the + currently-cached estimate of the Path MTU, but in fact there is + better information available in the Datagram Too Big message itself. + All ICMP Destination Unreachable messages, including this one, + contain the IP header of the original datagram, which contains the + Total Length of the datagram that was too big to be forwarded without + fragmentation. Since this Total Length may be less than the current + PMTU estimate, but is nonetheless larger than the actual PMTU, it may + be a good input to the method for choosing the next PMTU estimate. + + Note: routers based on implementations derived from 4.2BSD + Unix send an incorrect value for the Total Length of the + original IP datagram. The value sent by these routers is the + sum of the original Total Length and the original Header + Length (expressed in octets). Since it is impossible for the + host receiving such a Datagram Too Big message to know if it + sent by one of these routers, the host must be conservative + and assume that it is. If the Total Length field returned is + not less than the current PMTU estimate, it must be reduced by + 4 times the value of the returned Header Length field. + + The strategy we recommend, then, is to use as the next PMTU estimate + the greatest plateau value that is less than the returned Total + Length field (corrected, if necessary, according to the Note above). + + +6. Host implementation + + In this section we discuss how PMTU Discovery is implemented in host + software. This is not a specification, but rather a set of + suggestions. + + The issues include: + +Mogul & Deering [page 8] + + +RFC 1191 Path MTU Discovery November 1990 + + + + + - What layer or layers implement PMTU Discovery? + + - Where is the PMTU information cached? + + - How is stale PMTU information removed? + + - What must transport and higher layers do? + + +6.1. Layering + + In the IP architecture, the choice of what size datagram to send is + made by a protocol at a layer above IP. We refer to such a protocol + as a "packetization protocol". Packetization protocols are usually + transport protocols (for example, TCP) but can also be higher-layer + protocols (for example, protocols built on top of UDP). + + Implementing PMTU Discovery in the packetization layers simplifies + some of the inter-layer issues, but has several drawbacks: the + implementation may have to be redone for each packetization protocol, + it becomes hard to share PMTU information between different + packetization layers, and the connection-oriented state maintained by + some packetization layers may not easily extend to save PMTU + information for long periods. + + We therefore believe that the IP layer should store PMTU information + and that the ICMP layer should process received Datagram Too Big + messages. The packetization layers must still be able to respond to + changes in the Path MTU, by changing the size of the datagrams they + send, and must also be able to specify that datagrams are sent with + the DF bit set. We do not want the IP layer to simply set the DF bit + in every packet, since it is possible that a packetization layer, + perhaps a UDP application outside the kernel, is unable to change its + datagram size. Protocols involving intentional fragmentation, while + inelegant, are sometimes successful (NFS being the primary example), + and we do not want to break such protocols. + + To support this layering, packetization layers require an extension + of the IP service interface defined in [1]: + + A way to learn of changes in the value of MMS_S, the "maximum + send transport-message size", which is derived from the Path + MTU by subtracting the minimum IP header size. + + + + + + +Mogul & Deering [page 9] + + +RFC 1191 Path MTU Discovery November 1990 + + + + +6.2. Storing PMTU information + + In general, the IP layer should associate each PMTU value that it has + learned with a specific path. A path is identified by a source + address, a destination address and an IP type-of-service. (Some + implementations do not record the source address of paths; this is + acceptable for single-homed hosts, which have only one possible + source address.) + + Note: Some paths may be further distinguished by different + security classifications. The details of such classifications + are beyond the scope of this memo. + + The obvious place to store this association is as a field in the + routing table entries. A host will not have a route for every + possible destination, but it should be able to cache a per-host route + for every active destination. (This requirement is already imposed + by the need to process ICMP Redirect messages.) + + When the first packet is sent to a host for which no per-host route + exists, a route is chosen either from the set of per-network routes, + or from the set of default routes. The PMTU fields in these route + entries should be initialized to be the MTU of the associated + first-hop data link, and must never be changed by the PMTU Discovery + process. (PMTU Discovery only creates or changes entries for + per-host routes). Until a Datagram Too Big message is received, the + PMTU associated with the initially-chosen route is presumed to be + accurate. + + When a Datagram Too Big message is received, the ICMP layer + determines a new estimate for the Path MTU (either from a non-zero + Next-Hop MTU value in the packet, or using the method described in + section 5). If a per-host route for this path does not exist, then + one is created (almost as if a per-host ICMP Redirect is being + processed; the new route uses the same first-hop router as the + current route). If the PMTU estimate associated with the per-host + route is higher than the new estimate, then the value in the routing + entry is changed. + + The packetization layers must be notified about decreases in the + PMTU. Any packetization layer instance (for example, a TCP + connection) that is actively using the path must be notified if the + PMTU estimate is decreased. + + Note: even if the Datagram Too Big message contains an + Original Datagram Header that refers to a UDP packet, the TCP + layer must be notified if any of its connections use the given + + +Mogul & Deering [page 10] + + +RFC 1191 Path MTU Discovery November 1990 + + + + + path. + + Also, the instance that sent the datagram that elicited the Datagram + Too Big message should be notified that its datagram has been + dropped, even if the PMTU estimate has not changed, so that it may + retransmit the dropped datagram. + + Note: The notification mechanism can be analogous to the + mechanism used to provide notification of an ICMP Source + Quench message. In some implementations (such as + 4.2BSD-derived systems), the existing notification mechanism + is not able to identify the specific connection involved, and + so an additional mechanism is necessary. + + Alternatively, an implementation can avoid the use of an + asynchronous notification mechanism for PMTU decreases by + postponing notification until the next attempt to send a + datagram larger than the PMTU estimate. In this approach, + when an attempt is made to SEND a datagram with the DF bit + set, and the datagram is larger than the PMTU estimate, the + SEND function should fail and return a suitable error + indication. This approach may be more suitable to a + connectionless packetization layer (such as one using UDP), + which (in some implementations) may be hard to "notify" from + the ICMP layer. In this case, the normal timeout-based + retransmission mechanisms would be used to recover from the + dropped datagrams. + + It is important to understand that the notification of the + packetization layer instances using the path about the change in the + PMTU is distinct from the notification of a specific instance that a + packet has been dropped. The latter should be done as soon as + practical (i.e., asynchronously from the point of view of the + packetization layer instance), while the former may be delayed until + a packetization layer instance wants to create a packet. + Retransmission should be done for only for those packets that are + known to be dropped, as indicated by a Datagram Too Big message. + + +6.3. Purging stale PMTU information + + Internetwork topology is dynamic; routes change over time. The PMTU + discovered for a given destination may be wrong if a new route comes + into use. Thus, PMTU information cached by a host can become stale. + + Because a host using PMTU Discovery always sets the DF bit, if the + stale PMTU value is too large, this will be discovered almost + + +Mogul & Deering [page 11] + + +RFC 1191 Path MTU Discovery November 1990 + + + + + immediately once a datagram is sent to the given destination. No + such mechanism exists for realizing that a stale PMTU value is too + small, so an implementation should "age" cached values. When a PMTU + value has not been decreased for a while (on the order of 10 + minutes), the PMTU estimate should be set to the first-hop data-link + MTU, and the packetization layers should be notified of the change. + This will cause the complete PMTU Discovery process to take place + again. + + Note: an implementation should provide a means for changing + the timeout duration, including setting it to "infinity". For + example, hosts attached to an FDDI network which is then + attached to the rest of the Internet via a slow serial line + are never going to discover a new non-local PMTU, so they + should not have to put up with dropped datagrams every 10 + minutes. + + An upper layer MUST not retransmit datagrams in response to an + increase in the PMTU estimate, since this increase never comes in + response to an indication of a dropped datagram. + + One approach to implementing PMTU aging is to add a timestamp field + to the routing table entry. This field is initialized to a + "reserved" value, indicating that the PMTU has never been changed. + Whenever the PMTU is decreased in response to a Datagram Too Big + message, the timestamp is set to the current time. + + Once a minute, a timer-driven procedure runs through the routing + table, and for each entry whose timestamp is not "reserved" and is + older than the timeout interval: + + - The PMTU estimate is set to the MTU of the associated first + hop. + + - Packetization layers using this route are notified of the + increase. + + PMTU estimates may disappear from the routing table if the per-host + routes are removed; this can happen in response to an ICMP Redirect + message, or because certain routing-table daemons delete old routes + after several minutes. Also, on a multi-homed host a topology change + may result in the use of a different source interface. When this + happens, if the packetization layer is not notified then it may + continue to use a cached PMTU value that is now too small. One + solution is to notify the packetization layer of a possible PMTU + change whenever a Redirect message causes a route change, and + whenever a route is simply deleted from the routing table. + + +Mogul & Deering [page 12] + + +RFC 1191 Path MTU Discovery November 1990 + + + + + Note: a more sophisticated method for detecting PMTU increases + is described in section 7.1. + + +6.4. TCP layer actions + + The TCP layer must track the PMTU for the destination of a + connection; it should not send datagrams that would be larger than + this. A simple implementation could ask the IP layer for this value + (using the GET_MAXSIZES interface described in [1]) each time it + created a new segment, but this could be inefficient. Moreover, TCP + implementations that follow the "slow-start" congestion-avoidance + algorithm [4] typically calculate and cache several other values + derived from the PMTU. It may be simpler to receive asynchronous + notification when the PMTU changes, so that these variables may be + updated. + + A TCP implementation must also store the MSS value received from its + peer (which defaults to 536), and not send any segment larger than + this MSS, regardless of the PMTU. In 4.xBSD-derived implementations, + this requires adding an additional field to the TCP state record. + + Finally, when a Datagram Too Big message is received, it implies that + a datagram was dropped by the router that sent the ICMP message. It + is sufficient to treat this as any other dropped segment, and wait + until the retransmission timer expires to cause retransmission of the + segment. If the PMTU Discovery process requires several steps to + estimate the right PMTU, this could delay the connection by many + round-trip times. + + Alternatively, the retransmission could be done in immediate response + to a notification that the Path MTU has changed, but only for the + specific connection specified by the Datagram Too Big message. The + datagram size used in the retransmission should, of course, be no + larger than the new PMTU. + + Note: One MUST not retransmit in response to every Datagram + Too Big message, since a burst of several oversized segments + will give rise to several such messages and hence several + retransmissions of the same data. If the new estimated PMTU + is still wrong, the process repeats, and there is an + exponential growth in the number of superfluous segments sent! + + This means that the TCP layer must be able to recognize when a + Datagram Too Big notification actually decreases the PMTU that + it has already used to send a datagram on the given + connection, and should ignore any other notifications. + + +Mogul & Deering [page 13] + + +RFC 1191 Path MTU Discovery November 1990 + + + + + Modern TCP implementations incorporate "congestion advoidance" and + "slow-start" algorithms to improve performance [4]. Unlike a + retransmission caused by a TCP retransmission timeout, a + retransmission caused by a Datagram Too Big message should not change + the congestion window. It should, however, trigger the slow-start + mechanism (i.e., only one segment should be retransmitted until + acknowledgements begin to arrive again). + + TCP performance can be reduced if the sender's maximum window size is + not an exact multiple of the segment size in use (this is not the + congestion window size, which is always a multiple of the segment + size). In many system (such as those derived from 4.2BSD), the + segment size is often set to 1024 octets, and the maximum window size + (the "send space") is usually a multiple of 1024 octets, so the + proper relationship holds by default. If PMTU Discovery is used, + however, the segment size may not be a submultiple of the send space, + and it may change during a connection; this means that the TCP layer + may need to change the transmission window size when PMTU Discovery + changes the PMTU value. The maximum window size should be set to the + greatest multiple of the segment size (PMTU - 40) that is less than + or equal to the sender's buffer space size. + + PMTU Discovery does not affect the value sent in the TCP MSS option, + because that value is used by the other end of the connection, which + may be using an unrelated PMTU value. + + +6.5. Issues for other transport protocols + + Some transport protocols (such as ISO TP4 [3]) are not allowed to + repacketize when doing a retransmission. That is, once an attempt is + made to transmit a datagram of a certain size, its contents cannot be + split into smaller datagrams for retransmission. In such a case, the + original datagram should be retransmitted without the DF bit set, + allowing it to be fragmented as necessary to reach its destination. + Subsequent datagrams, when transmitted for the first time, should be + no larger than allowed by the Path MTU, and should have the DF bit + set. + + The Sun Network File System (NFS) uses a Remote Procedure Call (RPC) + protocol [11] that, in many cases, sends datagrams that must be + fragmented even for the first-hop link. This might improve + performance in certain cases, but it is known to cause reliability + and performance problems, especially when the client and server are + separated by routers. + + We recommend that NFS implementations use PMTU Discovery whenever + + +Mogul & Deering [page 14] + + +RFC 1191 Path MTU Discovery November 1990 + + + + + routers are involved. Most NFS implementations allow the RPC + datagram size to be changed at mount-time (indirectly, by changing + the effective file system block size), but might require some + modification to support changes later on. + + Also, since a single NFS operation cannot be split across several UDP + datagrams, certain operations (primarily, those operating on file + names and directories) require a minimum datagram size that may be + larger than the PMTU. NFS implementations should not reduce the + datagram size below this threshold, even if PMTU Discovery suggests a + lower value. (Of course, in this case datagrams should not be sent + with DF set.) + + +6.6. Management interface + + We suggest that an implementation provide a way for a system utility + program to: + + - Specify that PMTU Discovery not be done on a given route. + + - Change the PMTU value associated with a given route. + + The former can be accomplished by associating a flag with the routing + entry; when a packet is sent via a route with this flag set, the IP + layer leaves the DF bit clear no matter what the upper layer + requests. + + These features might be used to work around an anomalous situation, + or by a routing protocol implementation that is able to obtain Path + MTU values. + + The implementation should also provide a way to change the timeout + period for aging stale PMTU information. + + +7. Likely values for Path MTUs + + The algorithm recommended in section 5 for "searching" the space of + Path MTUs is based on a table of values that severely restricts the + search space. We describe here a table of MTU values that, as of + this writing, represents all major data-link technologies in use in + the Internet. + + In table 7-1, data links are listed in order of decreasing MTU, and + grouped so that each set of similar MTUs is associated with a + "plateau" equal to the lowest MTU in the group. (The table also + + +Mogul & Deering [page 15] + + +RFC 1191 Path MTU Discovery November 1990 + + + + + includes some entries not currently associated with a data link, and + gives references where available). Where a plateau represents more + than one MTU, the table shows the maximum inaccuracy associated with + the plateau, as a percentage. + + We do not expect that the values in the table, especially for higher + MTU levels, are going to be valid forever. The values given here are + an implementation suggestion, NOT a specification or requirement. + Implementors should use up-to-date references to pick a set of + plateaus; it is important that the table not contain too many entries + or the process of searching for a PMTU might waste Internet + resources. Implementors should also make it convenient for customers + without source code to update the table values in their systems (for + example, the table in a BSD-derived Unix kernel could be changed + using a new "ioctl" command). + + Note: It might be a good idea to add a few table entries for + values equal to small powers of 2 plus 40 (for the IP and TCP + headers), where no similar values exist, since this seems to + be a reasonably non-arbitrary way of choosing arbitrary + values. + + The table might also contain entries for values slightly less + than large powers of 2, in case MTUs are defined near those + values (it is better in this case for the table entries to be + low than to be high, or else the next lowest plateau may be + chosen instead). + + +7.1. A better way to detect PMTU increases + + Section 6.3 suggests detecting increases in the PMTU value by + periodically increasing the PTMU estimate to the first-hop MTU. + Since it is likely that this process will simply "rediscover" the + current PTMU estimate, at the cost of several dropped datagrams, it + should not be done often. + + A better approach is to periodically increase the PMTU estimate to + the next-highest value in the plateau table (or the first-hop MTU, if + that is smaller). If the increased estimate is wrong, at most one + round-trip time is wasted before the correct value is rediscovered. + If the increased estimate is still too low, a higher estimate will be + attempted somewhat later. + + Because it may take several such periods to discover a significant + increase in the PMTU, we recommend that a short timeout period should + be used after the estimate is increased, and a longer timeout be used + + +Mogul & Deering [page 16] + + +RFC 1191 Path MTU Discovery November 1990 + + + + + Plateau MTU Comments Reference + ------ --- -------- --------- + 65535 Official maximum MTU RFC 791 + 65535 Hyperchannel RFC 1044 + 65535 + 32000 Just in case + 17914 16Mb IBM Token Ring ref. [6] + 17914 + 8166 IEEE 802.4 RFC 1042 + 8166 + 4464 IEEE 802.5 (4Mb max) RFC 1042 + 4352 FDDI (Revised) RFC 1188 + 4352 (1%) + 2048 Wideband Network RFC 907 + 2002 IEEE 802.5 (4Mb recommended) RFC 1042 + 2002 (2%) + 1536 Exp. Ethernet Nets RFC 895 + 1500 Ethernet Networks RFC 894 + 1500 Point-to-Point (default) RFC 1134 + 1492 IEEE 802.3 RFC 1042 + 1492 (3%) + 1006 SLIP RFC 1055 + 1006 ARPANET BBN 1822 + 1006 + 576 X.25 Networks RFC 877 + 544 DEC IP Portal ref. [10] + 512 NETBIOS RFC 1088 + 508 IEEE 802/Source-Rt Bridge RFC 1042 + 508 ARCNET RFC 1051 + 508 (13%) + 296 Point-to-Point (low delay) RFC 1144 + 296 + 68 Official minimum MTU RFC 791 + + Table 7-1: Common MTUs in the Internet + + after the PTMU estimate is decreased because of a Datagram Too Big + message. For example, after the PTMU estimate is decreased, the + timeout should be set to 10 minutes; once this timer expires and a + larger MTU is attempted, the timeout can be set to a much smaller + value (say, 2 minutes). In no case should the timeout be shorter + than the estimated round-trip time, if this is known. + + + + + + + +Mogul & Deering [page 17] + + +RFC 1191 Path MTU Discovery November 1990 + + + + +8. Security considerations + + This Path MTU Discovery mechanism makes possible two denial-of- + service attacks, both based on a malicious party sending false + Datagram Too Big messages to an Internet host. + + In the first attack, the false message indicates a PMTU much smaller + than reality. This should not entirely stop data flow, since the + victim host should never set its PMTU estimate below the absolute + minimum, but at 8 octets of IP data per datagram, progress could be + slow. + + In the other attack, the false message indicates a PMTU greater than + reality. If believed, this could cause temporary blockage as the + victim sends datagrams that will be dropped by some router. Within + one round-trip time, the host would discover its mistake (receiving + Datagram Too Big messages from that router), but frequent repetition + of this attack could cause lots of datagrams to be dropped. A host, + however, should never raise its estimate of the PMTU based on a + Datagram Too Big message, so should not be vulnerable to this attack. + + A malicious party could also cause problems if it could stop a victim + from receiving legitimate Datagram Too Big messages, but in this case + there are simpler denial-of-service attacks available. + + +References + +[1] R. Braden, ed. Requirements for Internet Hosts -- Communication + Layers. RFC 1122, SRI Network Information Center, October, 1989. + +[2] Geof Cooper. IP Datagram Sizes. Electronic distribution of the + TCP-IP Discussion Group, Message-ID + <8705240517.AA01407@apolling.imagen.uucp>. + +[3] ISO. ISO Transport Protocol Specification: ISO DP 8073. RFC 905, + SRI Network Information Center, April, 1984. + +[4] Van Jacobson. Congestion Avoidance and Control. In Proc. SIGCOMM + '88 Symposium on Communications Architectures and Protocols, pages + 314-329. Stanford, CA, August, 1988. + +[5] C. Kent and J. Mogul. Fragmentation Considered Harmful. In Proc. + SIGCOMM '87 Workshop on Frontiers in Computer Communications + Technology. August, 1987. + +[6] Drew Daniel Perkins. Private Communication. + + +Mogul & Deering [page 18] + + +RFC 1191 Path MTU Discovery November 1990 + + + + +[7] J. Postel. Internet Control Message Protocol. RFC 792, SRI + Network Information Center, September, 1981. + +[8] J. Postel. Internet Protocol. RFC 791, SRI Network Information + Center, September, 1981. + +[9] J. Postel. The TCP Maximum Segment Size and Related Topics. RFC + 879, SRI Network Information Center, November, 1983. + +[10] Michael Reilly. Private Communication. + +[11] Sun Microsystems, Inc. RPC: Remote Procedure Call Protocol. RFC + 1057, SRI Network Information Center, June, 1988. + + + +Authors' Addresses + + Jeffrey Mogul + Digital Equipment Corporation Western Research Laboratory + 100 Hamilton Avenue + Palo Alto, CA 94301 + + Phone: (415) 853-6643 + EMail: mogul@decwrl.dec.com + + + Steve Deering + Xerox Palo Alto Research Center + 3333 Coyote Hill Road + Palo Alto, CA 94304 + + Phone: (415) 494-4839 + EMail: deering@xerox.com + + + + + + + + + + + + + + + +Mogul & Deering [page 19] diff --git a/dist/docs/rfc1945-http_1_0.txt b/dist/docs/rfc1945-http_1_0.txt new file mode 100644 index 0000000..7777117 --- /dev/null +++ b/dist/docs/rfc1945-http_1_0.txt @@ -0,0 +1,3362 @@ + + + + + + +Network Working Group T. Berners-Lee +Request for Comments: 1945 MIT/LCS +Category: Informational R. Fielding + UC Irvine + H. Frystyk + MIT/LCS + May 1996 + + + Hypertext Transfer Protocol -- HTTP/1.0 + +Status of This Memo + + This memo provides information for the Internet community. This memo + does not specify an Internet standard of any kind. Distribution of + this memo is unlimited. + +IESG Note: + + The IESG has concerns about this protocol, and expects this document + to be replaced relatively soon by a standards track document. + +Abstract + + The Hypertext Transfer Protocol (HTTP) is an application-level + protocol with the lightness and speed necessary for distributed, + collaborative, hypermedia information systems. It is a generic, + stateless, object-oriented protocol which can be used for many tasks, + such as name servers and distributed object management systems, + through extension of its request methods (commands). A feature of + HTTP is the typing of data representation, allowing systems to be + built independently of the data being transferred. + + HTTP has been in use by the World-Wide Web global information + initiative since 1990. This specification reflects common usage of + the protocol referred to as "HTTP/1.0". + +Table of Contents + + 1. Introduction .............................................. 4 + 1.1 Purpose .............................................. 4 + 1.2 Terminology .......................................... 4 + 1.3 Overall Operation .................................... 6 + 1.4 HTTP and MIME ........................................ 8 + 2. Notational Conventions and Generic Grammar ................ 8 + 2.1 Augmented BNF ........................................ 8 + 2.2 Basic Rules .......................................... 10 + 3. Protocol Parameters ....................................... 12 + + + +Berners-Lee, et al Informational [Page 1] + +RFC 1945 HTTP/1.0 May 1996 + + + 3.1 HTTP Version ......................................... 12 + 3.2 Uniform Resource Identifiers ......................... 14 + 3.2.1 General Syntax ................................ 14 + 3.2.2 http URL ...................................... 15 + 3.3 Date/Time Formats .................................... 15 + 3.4 Character Sets ....................................... 17 + 3.5 Content Codings ...................................... 18 + 3.6 Media Types .......................................... 19 + 3.6.1 Canonicalization and Text Defaults ............ 19 + 3.6.2 Multipart Types ............................... 20 + 3.7 Product Tokens ....................................... 20 + 4. HTTP Message .............................................. 21 + 4.1 Message Types ........................................ 21 + 4.2 Message Headers ...................................... 22 + 4.3 General Header Fields ................................ 23 + 5. Request ................................................... 23 + 5.1 Request-Line ......................................... 23 + 5.1.1 Method ........................................ 24 + 5.1.2 Request-URI ................................... 24 + 5.2 Request Header Fields ................................ 25 + 6. Response .................................................. 25 + 6.1 Status-Line .......................................... 26 + 6.1.1 Status Code and Reason Phrase ................. 26 + 6.2 Response Header Fields ............................... 28 + 7. Entity .................................................... 28 + 7.1 Entity Header Fields ................................. 29 + 7.2 Entity Body .......................................... 29 + 7.2.1 Type .......................................... 29 + 7.2.2 Length ........................................ 30 + 8. Method Definitions ........................................ 30 + 8.1 GET .................................................. 31 + 8.2 HEAD ................................................. 31 + 8.3 POST ................................................. 31 + 9. Status Code Definitions ................................... 32 + 9.1 Informational 1xx .................................... 32 + 9.2 Successful 2xx ....................................... 32 + 9.3 Redirection 3xx ...................................... 34 + 9.4 Client Error 4xx ..................................... 35 + 9.5 Server Error 5xx ..................................... 37 + 10. Header Field Definitions .................................. 37 + 10.1 Allow ............................................... 38 + 10.2 Authorization ....................................... 38 + 10.3 Content-Encoding .................................... 39 + 10.4 Content-Length ...................................... 39 + 10.5 Content-Type ........................................ 40 + 10.6 Date ................................................ 40 + 10.7 Expires ............................................. 41 + 10.8 From ................................................ 42 + + + +Berners-Lee, et al Informational [Page 2] + +RFC 1945 HTTP/1.0 May 1996 + + + 10.9 If-Modified-Since ................................... 42 + 10.10 Last-Modified ....................................... 43 + 10.11 Location ............................................ 44 + 10.12 Pragma .............................................. 44 + 10.13 Referer ............................................. 44 + 10.14 Server .............................................. 45 + 10.15 User-Agent .......................................... 46 + 10.16 WWW-Authenticate .................................... 46 + 11. Access Authentication ..................................... 47 + 11.1 Basic Authentication Scheme ......................... 48 + 12. Security Considerations ................................... 49 + 12.1 Authentication of Clients ........................... 49 + 12.2 Safe Methods ........................................ 49 + 12.3 Abuse of Server Log Information ..................... 50 + 12.4 Transfer of Sensitive Information ................... 50 + 12.5 Attacks Based On File and Path Names ................ 51 + 13. Acknowledgments ........................................... 51 + 14. References ................................................ 52 + 15. Authors' Addresses ........................................ 54 + Appendix A. Internet Media Type message/http ................ 55 + Appendix B. Tolerant Applications ........................... 55 + Appendix C. Relationship to MIME ............................ 56 + C.1 Conversion to Canonical Form ......................... 56 + C.2 Conversion of Date Formats ........................... 57 + C.3 Introduction of Content-Encoding ..................... 57 + C.4 No Content-Transfer-Encoding ......................... 57 + C.5 HTTP Header Fields in Multipart Body-Parts ........... 57 + Appendix D. Additional Features ............................. 57 + D.1 Additional Request Methods ........................... 58 + D.1.1 PUT ........................................... 58 + D.1.2 DELETE ........................................ 58 + D.1.3 LINK .......................................... 58 + D.1.4 UNLINK ........................................ 58 + D.2 Additional Header Field Definitions .................. 58 + D.2.1 Accept ........................................ 58 + D.2.2 Accept-Charset ................................ 59 + D.2.3 Accept-Encoding ............................... 59 + D.2.4 Accept-Language ............................... 59 + D.2.5 Content-Language .............................. 59 + D.2.6 Link .......................................... 59 + D.2.7 MIME-Version .................................. 59 + D.2.8 Retry-After ................................... 60 + D.2.9 Title ......................................... 60 + D.2.10 URI ........................................... 60 + + + + + + + +Berners-Lee, et al Informational [Page 3] + +RFC 1945 HTTP/1.0 May 1996 + + +1. Introduction + +1.1 Purpose + + The Hypertext Transfer Protocol (HTTP) is an application-level + protocol with the lightness and speed necessary for distributed, + collaborative, hypermedia information systems. HTTP has been in use + by the World-Wide Web global information initiative since 1990. This + specification reflects common usage of the protocol referred too as + "HTTP/1.0". This specification describes the features that seem to be + consistently implemented in most HTTP/1.0 clients and servers. The + specification is split into two sections. Those features of HTTP for + which implementations are usually consistent are described in the + main body of this document. Those features which have few or + inconsistent implementations are listed in Appendix D. + + Practical information systems require more functionality than simple + retrieval, including search, front-end update, and annotation. HTTP + allows an open-ended set of methods to be used to indicate the + purpose of a request. It builds on the discipline of reference + provided by the Uniform Resource Identifier (URI) [2], as a location + (URL) [4] or name (URN) [16], for indicating the resource on which a + method is to be applied. Messages are passed in a format similar to + that used by Internet Mail [7] and the Multipurpose Internet Mail + Extensions (MIME) [5]. + + HTTP is also used as a generic protocol for communication between + user agents and proxies/gateways to other Internet protocols, such as + SMTP [12], NNTP [11], FTP [14], Gopher [1], and WAIS [8], allowing + basic hypermedia access to resources available from diverse + applications and simplifying the implementation of user agents. + +1.2 Terminology + + This specification uses a number of terms to refer to the roles + played by participants in, and objects of, the HTTP communication. + + connection + + A transport layer virtual circuit established between two + application programs for the purpose of communication. + + message + + The basic unit of HTTP communication, consisting of a structured + sequence of octets matching the syntax defined in Section 4 and + transmitted via the connection. + + + + +Berners-Lee, et al Informational [Page 4] + +RFC 1945 HTTP/1.0 May 1996 + + + request + + An HTTP request message (as defined in Section 5). + + response + + An HTTP response message (as defined in Section 6). + + resource + + A network data object or service which can be identified by a + URI (Section 3.2). + + entity + + A particular representation or rendition of a data resource, or + reply from a service resource, that may be enclosed within a + request or response message. An entity consists of + metainformation in the form of entity headers and content in the + form of an entity body. + + client + + An application program that establishes connections for the + purpose of sending requests. + + user agent + + The client which initiates a request. These are often browsers, + editors, spiders (web-traversing robots), or other end user + tools. + + server + + An application program that accepts connections in order to + service requests by sending back responses. + + origin server + + The server on which a given resource resides or is to be created. + + proxy + + An intermediary program which acts as both a server and a client + for the purpose of making requests on behalf of other clients. + Requests are serviced internally or by passing them, with + possible translation, on to other servers. A proxy must + interpret and, if necessary, rewrite a request message before + + + +Berners-Lee, et al Informational [Page 5] + +RFC 1945 HTTP/1.0 May 1996 + + + forwarding it. Proxies are often used as client-side portals + through network firewalls and as helper applications for + handling requests via protocols not implemented by the user + agent. + + gateway + + A server which acts as an intermediary for some other server. + Unlike a proxy, a gateway receives requests as if it were the + origin server for the requested resource; the requesting client + may not be aware that it is communicating with a gateway. + Gateways are often used as server-side portals through network + firewalls and as protocol translators for access to resources + stored on non-HTTP systems. + + tunnel + + A tunnel is an intermediary program which is acting as a blind + relay between two connections. Once active, a tunnel is not + considered a party to the HTTP communication, though the tunnel + may have been initiated by an HTTP request. The tunnel ceases to + exist when both ends of the relayed connections are closed. + Tunnels are used when a portal is necessary and the intermediary + cannot, or should not, interpret the relayed communication. + + cache + + A program's local store of response messages and the subsystem + that controls its message storage, retrieval, and deletion. A + cache stores cachable responses in order to reduce the response + time and network bandwidth consumption on future, equivalent + requests. Any client or server may include a cache, though a + cache cannot be used by a server while it is acting as a tunnel. + + Any given program may be capable of being both a client and a server; + our use of these terms refers only to the role being performed by the + program for a particular connection, rather than to the program's + capabilities in general. Likewise, any server may act as an origin + server, proxy, gateway, or tunnel, switching behavior based on the + nature of each request. + +1.3 Overall Operation + + The HTTP protocol is based on a request/response paradigm. A client + establishes a connection with a server and sends a request to the + server in the form of a request method, URI, and protocol version, + followed by a MIME-like message containing request modifiers, client + information, and possible body content. The server responds with a + + + +Berners-Lee, et al Informational [Page 6] + +RFC 1945 HTTP/1.0 May 1996 + + + status line, including the message's protocol version and a success + or error code, followed by a MIME-like message containing server + information, entity metainformation, and possible body content. + + Most HTTP communication is initiated by a user agent and consists of + a request to be applied to a resource on some origin server. In the + simplest case, this may be accomplished via a single connection (v) + between the user agent (UA) and the origin server (O). + + request chain ------------------------> + UA -------------------v------------------- O + <----------------------- response chain + + A more complicated situation occurs when one or more intermediaries + are present in the request/response chain. There are three common + forms of intermediary: proxy, gateway, and tunnel. A proxy is a + forwarding agent, receiving requests for a URI in its absolute form, + rewriting all or parts of the message, and forwarding the reformatted + request toward the server identified by the URI. A gateway is a + receiving agent, acting as a layer above some other server(s) and, if + necessary, translating the requests to the underlying server's + protocol. A tunnel acts as a relay point between two connections + without changing the messages; tunnels are used when the + communication needs to pass through an intermediary (such as a + firewall) even when the intermediary cannot understand the contents + of the messages. + + request chain --------------------------------------> + UA -----v----- A -----v----- B -----v----- C -----v----- O + <------------------------------------- response chain + + The figure above shows three intermediaries (A, B, and C) between the + user agent and origin server. A request or response message that + travels the whole chain must pass through four separate connections. + This distinction is important because some HTTP communication options + may apply only to the connection with the nearest, non-tunnel + neighbor, only to the end-points of the chain, or to all connections + along the chain. Although the diagram is linear, each participant may + be engaged in multiple, simultaneous communications. For example, B + may be receiving requests from many clients other than A, and/or + forwarding requests to servers other than C, at the same time that it + is handling A's request. + + Any party to the communication which is not acting as a tunnel may + employ an internal cache for handling requests. The effect of a cache + is that the request/response chain is shortened if one of the + participants along the chain has a cached response applicable to that + request. The following illustrates the resulting chain if B has a + + + +Berners-Lee, et al Informational [Page 7] + +RFC 1945 HTTP/1.0 May 1996 + + + cached copy of an earlier response from O (via C) for a request which + has not been cached by UA or A. + + request chain ----------> + UA -----v----- A -----v----- B - - - - - - C - - - - - - O + <--------- response chain + + Not all responses are cachable, and some requests may contain + modifiers which place special requirements on cache behavior. Some + HTTP/1.0 applications use heuristics to describe what is or is not a + "cachable" response, but these rules are not standardized. + + On the Internet, HTTP communication generally takes place over TCP/IP + connections. The default port is TCP 80 [15], but other ports can be + used. This does not preclude HTTP from being implemented on top of + any other protocol on the Internet, or on other networks. HTTP only + presumes a reliable transport; any protocol that provides such + guarantees can be used, and the mapping of the HTTP/1.0 request and + response structures onto the transport data units of the protocol in + question is outside the scope of this specification. + + Except for experimental applications, current practice requires that + the connection be established by the client prior to each request and + closed by the server after sending the response. Both clients and + servers should be aware that either party may close the connection + prematurely, due to user action, automated time-out, or program + failure, and should handle such closing in a predictable fashion. In + any case, the closing of the connection by either or both parties + always terminates the current request, regardless of its status. + +1.4 HTTP and MIME + + HTTP/1.0 uses many of the constructs defined for MIME, as defined in + RFC 1521 [5]. Appendix C describes the ways in which the context of + HTTP allows for different use of Internet Media Types than is + typically found in Internet mail, and gives the rationale for those + differences. + +2. Notational Conventions and Generic Grammar + +2.1 Augmented BNF + + All of the mechanisms specified in this document are described in + both prose and an augmented Backus-Naur Form (BNF) similar to that + used by RFC 822 [7]. Implementors will need to be familiar with the + notation in order to understand this specification. The augmented BNF + includes the following constructs: + + + + +Berners-Lee, et al Informational [Page 8] + +RFC 1945 HTTP/1.0 May 1996 + + + name = definition + + The name of a rule is simply the name itself (without any + enclosing "<" and ">") and is separated from its definition by + the equal character "=". Whitespace is only significant in that + indentation of continuation lines is used to indicate a rule + definition that spans more than one line. Certain basic rules + are in uppercase, such as SP, LWS, HT, CRLF, DIGIT, ALPHA, etc. + Angle brackets are used within definitions whenever their + presence will facilitate discerning the use of rule names. + + "literal" + + Quotation marks surround literal text. Unless stated otherwise, + the text is case-insensitive. + + rule1 | rule2 + + Elements separated by a bar ("I") are alternatives, + e.g., "yes | no" will accept yes or no. + + (rule1 rule2) + + Elements enclosed in parentheses are treated as a single + element. Thus, "(elem (foo | bar) elem)" allows the token + sequences "elem foo elem" and "elem bar elem". + + *rule + + The character "*" preceding an element indicates repetition. The + full form is "*element" indicating at least and at + most occurrences of element. Default values are 0 and + infinity so that "*(element)" allows any number, including zero; + "1*element" requires at least one; and "1*2element" allows one + or two. + + [rule] + + Square brackets enclose optional elements; "[foo bar]" is + equivalent to "*1(foo bar)". + + N rule + + Specific repetition: "(element)" is equivalent to + "*(element)"; that is, exactly occurrences of + (element). Thus 2DIGIT is a 2-digit number, and 3ALPHA is a + string of three alphabetic characters. + + + + +Berners-Lee, et al Informational [Page 9] + +RFC 1945 HTTP/1.0 May 1996 + + + #rule + + A construct "#" is defined, similar to "*", for defining lists + of elements. The full form is "#element" indicating at + least and at most elements, each separated by one or + more commas (",") and optional linear whitespace (LWS). This + makes the usual form of lists very easy; a rule such as + "( *LWS element *( *LWS "," *LWS element ))" can be shown as + "1#element". Wherever this construct is used, null elements are + allowed, but do not contribute to the count of elements present. + That is, "(element), , (element)" is permitted, but counts as + only two elements. Therefore, where at least one element is + required, at least one non-null element must be present. Default + values are 0 and infinity so that "#(element)" allows any + number, including zero; "1#element" requires at least one; and + "1#2element" allows one or two. + + ; comment + + A semi-colon, set off some distance to the right of rule text, + starts a comment that continues to the end of line. This is a + simple way of including useful notes in parallel with the + specifications. + + implied *LWS + + The grammar described by this specification is word-based. + Except where noted otherwise, linear whitespace (LWS) can be + included between any two adjacent words (token or + quoted-string), and between adjacent tokens and delimiters + (tspecials), without changing the interpretation of a field. At + least one delimiter (tspecials) must exist between any two + tokens, since they would otherwise be interpreted as a single + token. However, applications should attempt to follow "common + form" when generating HTTP constructs, since there exist some + implementations that fail to accept anything beyond the common + forms. + +2.2 Basic Rules + + The following rules are used throughout this specification to + describe basic parsing constructs. The US-ASCII coded character set + is defined by [17]. + + OCTET = + CHAR = + UPALPHA = + LOALPHA = + + + +Berners-Lee, et al Informational [Page 10] + +RFC 1945 HTTP/1.0 May 1996 + + + ALPHA = UPALPHA | LOALPHA + DIGIT = + CTL = + CR = + LF = + SP = + HT = + <"> = + + HTTP/1.0 defines the octet sequence CR LF as the end-of-line marker + for all protocol elements except the Entity-Body (see Appendix B for + tolerant applications). The end-of-line marker within an Entity-Body + is defined by its associated media type, as described in Section 3.6. + + CRLF = CR LF + + HTTP/1.0 headers may be folded onto multiple lines if each + continuation line begins with a space or horizontal tab. All linear + whitespace, including folding, has the same semantics as SP. + + LWS = [CRLF] 1*( SP | HT ) + + However, folding of header lines is not expected by some + applications, and should not be generated by HTTP/1.0 applications. + + The TEXT rule is only used for descriptive field contents and values + that are not intended to be interpreted by the message parser. Words + of *TEXT may contain octets from character sets other than US-ASCII. + + TEXT = + + Recipients of header field TEXT containing octets outside the US- + ASCII character set may assume that they represent ISO-8859-1 + characters. + + Hexadecimal numeric characters are used in several protocol elements. + + HEX = "A" | "B" | "C" | "D" | "E" | "F" + | "a" | "b" | "c" | "d" | "e" | "f" | DIGIT + + Many HTTP/1.0 header field values consist of words separated by LWS + or special characters. These special characters must be in a quoted + string to be used within a parameter value. + + word = token | quoted-string + + + + +Berners-Lee, et al Informational [Page 11] + +RFC 1945 HTTP/1.0 May 1996 + + + token = 1* + + tspecials = "(" | ")" | "<" | ">" | "@" + | "," | ";" | ":" | "\" | <"> + | "/" | "[" | "]" | "?" | "=" + | "{" | "}" | SP | HT + + Comments may be included in some HTTP header fields by surrounding + the comment text with parentheses. Comments are only allowed in + fields containing "comment" as part of their field value definition. + In all other fields, parentheses are considered part of the field + value. + + comment = "(" *( ctext | comment ) ")" + ctext = + + A string of text is parsed as a single word if it is quoted using + double-quote marks. + + quoted-string = ( <"> *(qdtext) <"> ) + + qdtext = and CTLs, + but including LWS> + + Single-character quoting using the backslash ("\") character is not + permitted in HTTP/1.0. + +3. Protocol Parameters + +3.1 HTTP Version + + HTTP uses a "." numbering scheme to indicate versions + of the protocol. The protocol versioning policy is intended to allow + the sender to indicate the format of a message and its capacity for + understanding further HTTP communication, rather than the features + obtained via that communication. No change is made to the version + number for the addition of message components which do not affect + communication behavior or which only add to extensible field values. + The number is incremented when the changes made to the + protocol add features which do not change the general message parsing + algorithm, but which may add to the message semantics and imply + additional capabilities of the sender. The number is + incremented when the format of a message within the protocol is + changed. + + The version of an HTTP message is indicated by an HTTP-Version field + in the first line of the message. If the protocol version is not + specified, the recipient must assume that the message is in the + + + +Berners-Lee, et al Informational [Page 12] + +RFC 1945 HTTP/1.0 May 1996 + + + simple HTTP/0.9 format. + + HTTP-Version = "HTTP" "/" 1*DIGIT "." 1*DIGIT + + Note that the major and minor numbers should be treated as separate + integers and that each may be incremented higher than a single digit. + Thus, HTTP/2.4 is a lower version than HTTP/2.13, which in turn is + lower than HTTP/12.3. Leading zeros should be ignored by recipients + and never generated by senders. + + This document defines both the 0.9 and 1.0 versions of the HTTP + protocol. Applications sending Full-Request or Full-Response + messages, as defined by this specification, must include an HTTP- + Version of "HTTP/1.0". + + HTTP/1.0 servers must: + + o recognize the format of the Request-Line for HTTP/0.9 and + HTTP/1.0 requests; + + o understand any valid request in the format of HTTP/0.9 or + HTTP/1.0; + + o respond appropriately with a message in the same protocol + version used by the client. + + HTTP/1.0 clients must: + + o recognize the format of the Status-Line for HTTP/1.0 responses; + + o understand any valid response in the format of HTTP/0.9 or + HTTP/1.0. + + Proxy and gateway applications must be careful in forwarding requests + that are received in a format different than that of the + application's native HTTP version. Since the protocol version + indicates the protocol capability of the sender, a proxy/gateway must + never send a message with a version indicator which is greater than + its native version; if a higher version request is received, the + proxy/gateway must either downgrade the request version or respond + with an error. Requests with a version lower than that of the + application's native format may be upgraded before being forwarded; + the proxy/gateway's response to that request must follow the server + requirements listed above. + + + + + + + +Berners-Lee, et al Informational [Page 13] + +RFC 1945 HTTP/1.0 May 1996 + + +3.2 Uniform Resource Identifiers + + URIs have been known by many names: WWW addresses, Universal Document + Identifiers, Universal Resource Identifiers [2], and finally the + combination of Uniform Resource Locators (URL) [4] and Names (URN) + [16]. As far as HTTP is concerned, Uniform Resource Identifiers are + simply formatted strings which identify--via name, location, or any + other characteristic--a network resource. + +3.2.1 General Syntax + + URIs in HTTP can be represented in absolute form or relative to some + known base URI [9], depending upon the context of their use. The two + forms are differentiated by the fact that absolute URIs always begin + with a scheme name followed by a colon. + + URI = ( absoluteURI | relativeURI ) [ "#" fragment ] + + absoluteURI = scheme ":" *( uchar | reserved ) + + relativeURI = net_path | abs_path | rel_path + + net_path = "//" net_loc [ abs_path ] + abs_path = "/" rel_path + rel_path = [ path ] [ ";" params ] [ "?" query ] + + path = fsegment *( "/" segment ) + fsegment = 1*pchar + segment = *pchar + + params = param *( ";" param ) + param = *( pchar | "/" ) + + scheme = 1*( ALPHA | DIGIT | "+" | "-" | "." ) + net_loc = *( pchar | ";" | "?" ) + query = *( uchar | reserved ) + fragment = *( uchar | reserved ) + + pchar = uchar | ":" | "@" | "&" | "=" | "+" + uchar = unreserved | escape + unreserved = ALPHA | DIGIT | safe | extra | national + + escape = "%" HEX HEX + reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" + extra = "!" | "*" | "'" | "(" | ")" | "," + safe = "$" | "-" | "_" | "." + unsafe = CTL | SP | <"> | "#" | "%" | "<" | ">" + national = + + For definitive information on URL syntax and semantics, see RFC 1738 + [4] and RFC 1808 [9]. The BNF above includes national characters not + allowed in valid URLs as specified by RFC 1738, since HTTP servers + are not restricted in the set of unreserved characters allowed to + represent the rel_path part of addresses, and HTTP proxies may + receive requests for URIs not defined by RFC 1738. + +3.2.2 http URL + + The "http" scheme is used to locate network resources via the HTTP + protocol. This section defines the scheme-specific syntax and + semantics for http URLs. + + http_URL = "http:" "//" host [ ":" port ] [ abs_path ] + + host = + + port = *DIGIT + + If the port is empty or not given, port 80 is assumed. The semantics + are that the identified resource is located at the server listening + for TCP connections on that port of that host, and the Request-URI + for the resource is abs_path. If the abs_path is not present in the + URL, it must be given as "/" when used as a Request-URI (Section + 5.1.2). + + Note: Although the HTTP protocol is independent of the transport + layer protocol, the http URL only identifies resources by their + TCP location, and thus non-TCP resources must be identified by + some other URI scheme. + + The canonical form for "http" URLs is obtained by converting any + UPALPHA characters in host to their LOALPHA equivalent (hostnames are + case-insensitive), eliding the [ ":" port ] if the port is 80, and + replacing an empty abs_path with "/". + +3.3 Date/Time Formats + + HTTP/1.0 applications have historically allowed three different + formats for the representation of date/time stamps: + + Sun, 06 Nov 1994 08:49:37 GMT ; RFC 822, updated by RFC 1123 + Sunday, 06-Nov-94 08:49:37 GMT ; RFC 850, obsoleted by RFC 1036 + Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format + + + +Berners-Lee, et al Informational [Page 15] + +RFC 1945 HTTP/1.0 May 1996 + + + The first format is preferred as an Internet standard and represents + a fixed-length subset of that defined by RFC 1123 [6] (an update to + RFC 822 [7]). The second format is in common use, but is based on the + obsolete RFC 850 [10] date format and lacks a four-digit year. + HTTP/1.0 clients and servers that parse the date value should accept + all three formats, though they must never generate the third + (asctime) format. + + Note: Recipients of date values are encouraged to be robust in + accepting date values that may have been generated by non-HTTP + applications, as is sometimes the case when retrieving or posting + messages via proxies/gateways to SMTP or NNTP. + + All HTTP/1.0 date/time stamps must be represented in Universal Time + (UT), also known as Greenwich Mean Time (GMT), without exception. + This is indicated in the first two formats by the inclusion of "GMT" + as the three-letter abbreviation for time zone, and should be assumed + when reading the asctime format. + + HTTP-date = rfc1123-date | rfc850-date | asctime-date + + rfc1123-date = wkday "," SP date1 SP time SP "GMT" + rfc850-date = weekday "," SP date2 SP time SP "GMT" + asctime-date = wkday SP date3 SP time SP 4DIGIT + + date1 = 2DIGIT SP month SP 4DIGIT + ; day month year (e.g., 02 Jun 1982) + date2 = 2DIGIT "-" month "-" 2DIGIT + ; day-month-year (e.g., 02-Jun-82) + date3 = month SP ( 2DIGIT | ( SP 1DIGIT )) + ; month day (e.g., Jun 2) + + time = 2DIGIT ":" 2DIGIT ":" 2DIGIT + ; 00:00:00 - 23:59:59 + + wkday = "Mon" | "Tue" | "Wed" + | "Thu" | "Fri" | "Sat" | "Sun" + + weekday = "Monday" | "Tuesday" | "Wednesday" + | "Thursday" | "Friday" | "Saturday" | "Sunday" + + month = "Jan" | "Feb" | "Mar" | "Apr" + | "May" | "Jun" | "Jul" | "Aug" + | "Sep" | "Oct" | "Nov" | "Dec" + + Note: HTTP requirements for the date/time stamp format apply + only to their usage within the protocol stream. Clients and + servers are not required to use these formats for user + + + +Berners-Lee, et al Informational [Page 16] + +RFC 1945 HTTP/1.0 May 1996 + + + presentation, request logging, etc. + +3.4 Character Sets + + HTTP uses the same definition of the term "character set" as that + described for MIME: + + The term "character set" is used in this document to refer to a + method used with one or more tables to convert a sequence of + octets into a sequence of characters. Note that unconditional + conversion in the other direction is not required, in that not all + characters may be available in a given character set and a + character set may provide more than one sequence of octets to + represent a particular character. This definition is intended to + allow various kinds of character encodings, from simple single- + table mappings such as US-ASCII to complex table switching methods + such as those that use ISO 2022's techniques. However, the + definition associated with a MIME character set name must fully + specify the mapping to be performed from octets to characters. In + particular, use of external profiling information to determine the + exact mapping is not permitted. + + Note: This use of the term "character set" is more commonly + referred to as a "character encoding." However, since HTTP and + MIME share the same registry, it is important that the terminology + also be shared. + + HTTP character sets are identified by case-insensitive tokens. The + complete set of tokens are defined by the IANA Character Set registry + [15]. However, because that registry does not define a single, + consistent token for each character set, we define here the preferred + names for those character sets most likely to be used with HTTP + entities. These character sets include those registered by RFC 1521 + [5] -- the US-ASCII [17] and ISO-8859 [18] character sets -- and + other names specifically recommended for use within MIME charset + parameters. + + charset = "US-ASCII" + | "ISO-8859-1" | "ISO-8859-2" | "ISO-8859-3" + | "ISO-8859-4" | "ISO-8859-5" | "ISO-8859-6" + | "ISO-8859-7" | "ISO-8859-8" | "ISO-8859-9" + | "ISO-2022-JP" | "ISO-2022-JP-2" | "ISO-2022-KR" + | "UNICODE-1-1" | "UNICODE-1-1-UTF-7" | "UNICODE-1-1-UTF-8" + | token + + Although HTTP allows an arbitrary token to be used as a charset + value, any token that has a predefined value within the IANA + Character Set registry [15] must represent the character set defined + + + +Berners-Lee, et al Informational [Page 17] + +RFC 1945 HTTP/1.0 May 1996 + + + by that registry. Applications should limit their use of character + sets to those defined by the IANA registry. + + The character set of an entity body should be labelled as the lowest + common denominator of the character codes used within that body, with + the exception that no label is preferred over the labels US-ASCII or + ISO-8859-1. + +3.5 Content Codings + + Content coding values are used to indicate an encoding transformation + that has been applied to a resource. Content codings are primarily + used to allow a document to be compressed or encrypted without losing + the identity of its underlying media type. Typically, the resource is + stored in this encoding and only decoded before rendering or + analogous usage. + + content-coding = "x-gzip" | "x-compress" | token + + Note: For future compatibility, HTTP/1.0 applications should + consider "gzip" and "compress" to be equivalent to "x-gzip" + and "x-compress", respectively. + + All content-coding values are case-insensitive. HTTP/1.0 uses + content-coding values in the Content-Encoding (Section 10.3) header + field. Although the value describes the content-coding, what is more + important is that it indicates what decoding mechanism will be + required to remove the encoding. Note that a single program may be + capable of decoding multiple content-coding formats. Two values are + defined by this specification: + + x-gzip + An encoding format produced by the file compression program + "gzip" (GNU zip) developed by Jean-loup Gailly. This format is + typically a Lempel-Ziv coding (LZ77) with a 32 bit CRC. + + x-compress + The encoding format produced by the file compression program + "compress". This format is an adaptive Lempel-Ziv-Welch coding + (LZW). + + Note: Use of program names for the identification of + encoding formats is not desirable and should be discouraged + for future encodings. Their use here is representative of + historical practice, not good design. + + + + + + +Berners-Lee, et al Informational [Page 18] + +RFC 1945 HTTP/1.0 May 1996 + + +3.6 Media Types + + HTTP uses Internet Media Types [13] in the Content-Type header field + (Section 10.5) in order to provide open and extensible data typing. + + media-type = type "/" subtype *( ";" parameter ) + type = token + subtype = token + + Parameters may follow the type/subtype in the form of attribute/value + pairs. + + parameter = attribute "=" value + attribute = token + value = token | quoted-string + + The type, subtype, and parameter attribute names are case- + insensitive. Parameter values may or may not be case-sensitive, + depending on the semantics of the parameter name. LWS must not be + generated between the type and subtype, nor between an attribute and + its value. Upon receipt of a media type with an unrecognized + parameter, a user agent should treat the media type as if the + unrecognized parameter and its value were not present. + + Some older HTTP applications do not recognize media type parameters. + HTTP/1.0 applications should only use media type parameters when they + are necessary to define the content of a message. + + Media-type values are registered with the Internet Assigned Number + Authority (IANA [15]). The media type registration process is + outlined in RFC 1590 [13]. Use of non-registered media types is + discouraged. + +3.6.1 Canonicalization and Text Defaults + + Internet media types are registered with a canonical form. In + general, an Entity-Body transferred via HTTP must be represented in + the appropriate canonical form prior to its transmission. If the body + has been encoded with a Content-Encoding, the underlying data should + be in canonical form prior to being encoded. + + Media subtypes of the "text" type use CRLF as the text line break + when in canonical form. However, HTTP allows the transport of text + media with plain CR or LF alone representing a line break when used + consistently within the Entity-Body. HTTP applications must accept + CRLF, bare CR, and bare LF as being representative of a line break in + text media received via HTTP. + + + + +Berners-Lee, et al Informational [Page 19] + +RFC 1945 HTTP/1.0 May 1996 + + + In addition, if the text media is represented in a character set that + does not use octets 13 and 10 for CR and LF respectively, as is the + case for some multi-byte character sets, HTTP allows the use of + whatever octet sequences are defined by that character set to + represent the equivalent of CR and LF for line breaks. This + flexibility regarding line breaks applies only to text media in the + Entity-Body; a bare CR or LF should not be substituted for CRLF + within any of the HTTP control structures (such as header fields and + multipart boundaries). + + The "charset" parameter is used with some media types to define the + character set (Section 3.4) of the data. When no explicit charset + parameter is provided by the sender, media subtypes of the "text" + type are defined to have a default charset value of "ISO-8859-1" when + received via HTTP. Data in character sets other than "ISO-8859-1" or + its subsets must be labelled with an appropriate charset value in + order to be consistently interpreted by the recipient. + + Note: Many current HTTP servers provide data using charsets other + than "ISO-8859-1" without proper labelling. This situation reduces + interoperability and is not recommended. To compensate for this, + some HTTP user agents provide a configuration option to allow the + user to change the default interpretation of the media type + character set when no charset parameter is given. + +3.6.2 Multipart Types + + MIME provides for a number of "multipart" types -- encapsulations of + several entities within a single message's Entity-Body. The multipart + types registered by IANA [15] do not have any special meaning for + HTTP/1.0, though user agents may need to understand each type in + order to correctly interpret the purpose of each body-part. An HTTP + user agent should follow the same or similar behavior as a MIME user + agent does upon receipt of a multipart type. HTTP servers should not + assume that all HTTP clients are prepared to handle multipart types. + + All multipart types share a common syntax and must include a boundary + parameter as part of the media type value. The message body is itself + a protocol element and must therefore use only CRLF to represent line + breaks between body-parts. Multipart body-parts may contain HTTP + header fields which are significant to the meaning of that part. + +3.7 Product Tokens + + Product tokens are used to allow communicating applications to + identify themselves via a simple product token, with an optional + slash and version designator. Most fields using product tokens also + allow subproducts which form a significant part of the application to + + + +Berners-Lee, et al Informational [Page 20] + +RFC 1945 HTTP/1.0 May 1996 + + + be listed, separated by whitespace. By convention, the products are + listed in order of their significance for identifying the + application. + + product = token ["/" product-version] + product-version = token + + Examples: + + User-Agent: CERN-LineMode/2.15 libwww/2.17b3 + + Server: Apache/0.8.4 + + Product tokens should be short and to the point -- use of them for + advertizing or other non-essential information is explicitly + forbidden. Although any token character may appear in a product- + version, this token should only be used for a version identifier + (i.e., successive versions of the same product should only differ in + the product-version portion of the product value). + +4. HTTP Message + +4.1 Message Types + + HTTP messages consist of requests from client to server and responses + from server to client. + + HTTP-message = Simple-Request ; HTTP/0.9 messages + | Simple-Response + | Full-Request ; HTTP/1.0 messages + | Full-Response + + Full-Request and Full-Response use the generic message format of RFC + 822 [7] for transferring entities. Both messages may include optional + header fields (also known as "headers") and an entity body. The + entity body is separated from the headers by a null line (i.e., a + line with nothing preceding the CRLF). + + Full-Request = Request-Line ; Section 5.1 + *( General-Header ; Section 4.3 + | Request-Header ; Section 5.2 + | Entity-Header ) ; Section 7.1 + CRLF + [ Entity-Body ] ; Section 7.2 + + Full-Response = Status-Line ; Section 6.1 + *( General-Header ; Section 4.3 + | Response-Header ; Section 6.2 + + + +Berners-Lee, et al Informational [Page 21] + +RFC 1945 HTTP/1.0 May 1996 + + + | Entity-Header ) ; Section 7.1 + CRLF + [ Entity-Body ] ; Section 7.2 + + Simple-Request and Simple-Response do not allow the use of any header + information and are limited to a single request method (GET). + + Simple-Request = "GET" SP Request-URI CRLF + + Simple-Response = [ Entity-Body ] + + Use of the Simple-Request format is discouraged because it prevents + the server from identifying the media type of the returned entity. + +4.2 Message Headers + + HTTP header fields, which include General-Header (Section 4.3), + Request-Header (Section 5.2), Response-Header (Section 6.2), and + Entity-Header (Section 7.1) fields, follow the same generic format as + that given in Section 3.1 of RFC 822 [7]. Each header field consists + of a name followed immediately by a colon (":"), a single space (SP) + character, and the field value. Field names are case-insensitive. + Header fields can be extended over multiple lines by preceding each + extra line with at least one SP or HT, though this is not + recommended. + + HTTP-header = field-name ":" [ field-value ] CRLF + + field-name = token + field-value = *( field-content | LWS ) + + field-content = + + The order in which header fields are received is not significant. + However, it is "good practice" to send General-Header fields first, + followed by Request-Header or Response-Header fields prior to the + Entity-Header fields. + + Multiple HTTP-header fields with the same field-name may be present + in a message if and only if the entire field-value for that header + field is defined as a comma-separated list [i.e., #(values)]. It must + be possible to combine the multiple header fields into one "field- + name: field-value" pair, without changing the semantics of the + message, by appending each subsequent field-value to the first, each + separated by a comma. + + + + +Berners-Lee, et al Informational [Page 22] + +RFC 1945 HTTP/1.0 May 1996 + + +4.3 General Header Fields + + There are a few header fields which have general applicability for + both request and response messages, but which do not apply to the + entity being transferred. These headers apply only to the message + being transmitted. + + General-Header = Date ; Section 10.6 + | Pragma ; Section 10.12 + + General header field names can be extended reliably only in + combination with a change in the protocol version. However, new or + experimental header fields may be given the semantics of general + header fields if all parties in the communication recognize them to + be general header fields. Unrecognized header fields are treated as + Entity-Header fields. + +5. Request + + A request message from a client to a server includes, within the + first line of that message, the method to be applied to the resource, + the identifier of the resource, and the protocol version in use. For + backwards compatibility with the more limited HTTP/0.9 protocol, + there are two valid formats for an HTTP request: + + Request = Simple-Request | Full-Request + + Simple-Request = "GET" SP Request-URI CRLF + + Full-Request = Request-Line ; Section 5.1 + *( General-Header ; Section 4.3 + | Request-Header ; Section 5.2 + | Entity-Header ) ; Section 7.1 + CRLF + [ Entity-Body ] ; Section 7.2 + + If an HTTP/1.0 server receives a Simple-Request, it must respond with + an HTTP/0.9 Simple-Response. An HTTP/1.0 client capable of receiving + a Full-Response should never generate a Simple-Request. + +5.1 Request-Line + + The Request-Line begins with a method token, followed by the + Request-URI and the protocol version, and ending with CRLF. The + elements are separated by SP characters. No CR or LF are allowed + except in the final CRLF sequence. + + Request-Line = Method SP Request-URI SP HTTP-Version CRLF + + + +Berners-Lee, et al Informational [Page 23] + +RFC 1945 HTTP/1.0 May 1996 + + + Note that the difference between a Simple-Request and the Request- + Line of a Full-Request is the presence of the HTTP-Version field and + the availability of methods other than GET. + +5.1.1 Method + + The Method token indicates the method to be performed on the resource + identified by the Request-URI. The method is case-sensitive. + + Method = "GET" ; Section 8.1 + | "HEAD" ; Section 8.2 + | "POST" ; Section 8.3 + | extension-method + + extension-method = token + + The list of methods acceptable by a specific resource can change + dynamically; the client is notified through the return code of the + response if a method is not allowed on a resource. Servers should + return the status code 501 (not implemented) if the method is + unrecognized or not implemented. + + The methods commonly used by HTTP/1.0 applications are fully defined + in Section 8. + +5.1.2 Request-URI + + The Request-URI is a Uniform Resource Identifier (Section 3.2) and + identifies the resource upon which to apply the request. + + Request-URI = absoluteURI | abs_path + + The two options for Request-URI are dependent on the nature of the + request. + + The absoluteURI form is only allowed when the request is being made + to a proxy. The proxy is requested to forward the request and return + the response. If the request is GET or HEAD and a prior response is + cached, the proxy may use the cached message if it passes any + restrictions in the Expires header field. Note that the proxy may + forward the request on to another proxy or directly to the server + specified by the absoluteURI. In order to avoid request loops, a + proxy must be able to recognize all of its server names, including + any aliases, local variations, and the numeric IP address. An example + Request-Line would be: + + GET http://www.w3.org/pub/WWW/TheProject.html HTTP/1.0 + + + + +Berners-Lee, et al Informational [Page 24] + +RFC 1945 HTTP/1.0 May 1996 + + + The most common form of Request-URI is that used to identify a + resource on an origin server or gateway. In this case, only the + absolute path of the URI is transmitted (see Section 3.2.1, + abs_path). For example, a client wishing to retrieve the resource + above directly from the origin server would create a TCP connection + to port 80 of the host "www.w3.org" and send the line: + + GET /pub/WWW/TheProject.html HTTP/1.0 + + followed by the remainder of the Full-Request. Note that the absolute + path cannot be empty; if none is present in the original URI, it must + be given as "/" (the server root). + + The Request-URI is transmitted as an encoded string, where some + characters may be escaped using the "% HEX HEX" encoding defined by + RFC 1738 [4]. The origin server must decode the Request-URI in order + to properly interpret the request. + +5.2 Request Header Fields + + The request header fields allow the client to pass additional + information about the request, and about the client itself, to the + server. These fields act as request modifiers, with semantics + equivalent to the parameters on a programming language method + (procedure) invocation. + + Request-Header = Authorization ; Section 10.2 + | From ; Section 10.8 + | If-Modified-Since ; Section 10.9 + | Referer ; Section 10.13 + | User-Agent ; Section 10.15 + + Request-Header field names can be extended reliably only in + combination with a change in the protocol version. However, new or + experimental header fields may be given the semantics of request + header fields if all parties in the communication recognize them to + be request header fields. Unrecognized header fields are treated as + Entity-Header fields. + +6. Response + + After receiving and interpreting a request message, a server responds + in the form of an HTTP response message. + + Response = Simple-Response | Full-Response + + Simple-Response = [ Entity-Body ] + + + + +Berners-Lee, et al Informational [Page 25] + +RFC 1945 HTTP/1.0 May 1996 + + + Full-Response = Status-Line ; Section 6.1 + *( General-Header ; Section 4.3 + | Response-Header ; Section 6.2 + | Entity-Header ) ; Section 7.1 + CRLF + [ Entity-Body ] ; Section 7.2 + + A Simple-Response should only be sent in response to an HTTP/0.9 + Simple-Request or if the server only supports the more limited + HTTP/0.9 protocol. If a client sends an HTTP/1.0 Full-Request and + receives a response that does not begin with a Status-Line, it should + assume that the response is a Simple-Response and parse it + accordingly. Note that the Simple-Response consists only of the + entity body and is terminated by the server closing the connection. + +6.1 Status-Line + + The first line of a Full-Response message is the Status-Line, + consisting of the protocol version followed by a numeric status code + and its associated textual phrase, with each element separated by SP + characters. No CR or LF is allowed except in the final CRLF sequence. + + Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF + + Since a status line always begins with the protocol version and + status code + + "HTTP/" 1*DIGIT "." 1*DIGIT SP 3DIGIT SP + + (e.g., "HTTP/1.0 200 "), the presence of that expression is + sufficient to differentiate a Full-Response from a Simple-Response. + Although the Simple-Response format may allow such an expression to + occur at the beginning of an entity body, and thus cause a + misinterpretation of the message if it was given in response to a + Full-Request, most HTTP/0.9 servers are limited to responses of type + "text/html" and therefore would never generate such a response. + +6.1.1 Status Code and Reason Phrase + + The Status-Code element is a 3-digit integer result code of the + attempt to understand and satisfy the request. The Reason-Phrase is + intended to give a short textual description of the Status-Code. The + Status-Code is intended for use by automata and the Reason-Phrase is + intended for the human user. The client is not required to examine or + display the Reason-Phrase. + + + + + + +Berners-Lee, et al Informational [Page 26] + +RFC 1945 HTTP/1.0 May 1996 + + + The first digit of the Status-Code defines the class of response. The + last two digits do not have any categorization role. There are 5 + values for the first digit: + + o 1xx: Informational - Not used, but reserved for future use + + o 2xx: Success - The action was successfully received, + understood, and accepted. + + o 3xx: Redirection - Further action must be taken in order to + complete the request + + o 4xx: Client Error - The request contains bad syntax or cannot + be fulfilled + + o 5xx: Server Error - The server failed to fulfill an apparently + valid request + + The individual values of the numeric status codes defined for + HTTP/1.0, and an example set of corresponding Reason-Phrase's, are + presented below. The reason phrases listed here are only recommended + -- they may be replaced by local equivalents without affecting the + protocol. These codes are fully defined in Section 9. + + Status-Code = "200" ; OK + | "201" ; Created + | "202" ; Accepted + | "204" ; No Content + | "301" ; Moved Permanently + | "302" ; Moved Temporarily + | "304" ; Not Modified + | "400" ; Bad Request + | "401" ; Unauthorized + | "403" ; Forbidden + | "404" ; Not Found + | "500" ; Internal Server Error + | "501" ; Not Implemented + | "502" ; Bad Gateway + | "503" ; Service Unavailable + | extension-code + + extension-code = 3DIGIT + + Reason-Phrase = * + + HTTP status codes are extensible, but the above codes are the only + ones generally recognized in current practice. HTTP applications are + not required to understand the meaning of all registered status + + + +Berners-Lee, et al Informational [Page 27] + +RFC 1945 HTTP/1.0 May 1996 + + + codes, though such understanding is obviously desirable. However, + applications must understand the class of any status code, as + indicated by the first digit, and treat any unrecognized response as + being equivalent to the x00 status code of that class, with the + exception that an unrecognized response must not be cached. For + example, if an unrecognized status code of 431 is received by the + client, it can safely assume that there was something wrong with its + request and treat the response as if it had received a 400 status + code. In such cases, user agents should present to the user the + entity returned with the response, since that entity is likely to + include human-readable information which will explain the unusual + status. + +6.2 Response Header Fields + + The response header fields allow the server to pass additional + information about the response which cannot be placed in the Status- + Line. These header fields give information about the server and about + further access to the resource identified by the Request-URI. + + Response-Header = Location ; Section 10.11 + | Server ; Section 10.14 + | WWW-Authenticate ; Section 10.16 + + Response-Header field names can be extended reliably only in + combination with a change in the protocol version. However, new or + experimental header fields may be given the semantics of response + header fields if all parties in the communication recognize them to + be response header fields. Unrecognized header fields are treated as + Entity-Header fields. + +7. Entity + + Full-Request and Full-Response messages may transfer an entity within + some requests and responses. An entity consists of Entity-Header + fields and (usually) an Entity-Body. In this section, both sender and + recipient refer to either the client or the server, depending on who + sends and who receives the entity. + + + + + + + + + + + + + +Berners-Lee, et al Informational [Page 28] + +RFC 1945 HTTP/1.0 May 1996 + + +7.1 Entity Header Fields + + Entity-Header fields define optional metainformation about the + Entity-Body or, if no body is present, about the resource identified + by the request. + + Entity-Header = Allow ; Section 10.1 + | Content-Encoding ; Section 10.3 + | Content-Length ; Section 10.4 + | Content-Type ; Section 10.5 + | Expires ; Section 10.7 + | Last-Modified ; Section 10.10 + | extension-header + + extension-header = HTTP-header + + The extension-header mechanism allows additional Entity-Header fields + to be defined without changing the protocol, but these fields cannot + be assumed to be recognizable by the recipient. Unrecognized header + fields should be ignored by the recipient and forwarded by proxies. + +7.2 Entity Body + + The entity body (if any) sent with an HTTP request or response is in + a format and encoding defined by the Entity-Header fields. + + Entity-Body = *OCTET + + An entity body is included with a request message only when the + request method calls for one. The presence of an entity body in a + request is signaled by the inclusion of a Content-Length header field + in the request message headers. HTTP/1.0 requests containing an + entity body must include a valid Content-Length header field. + + For response messages, whether or not an entity body is included with + a message is dependent on both the request method and the response + code. All responses to the HEAD request method must not include a + body, even though the presence of entity header fields may lead one + to believe they do. All 1xx (informational), 204 (no content), and + 304 (not modified) responses must not include a body. All other + responses must include an entity body or a Content-Length header + field defined with a value of zero (0). + +7.2.1 Type + + When an Entity-Body is included with a message, the data type of that + body is determined via the header fields Content-Type and Content- + Encoding. These define a two-layer, ordered encoding model: + + + +Berners-Lee, et al Informational [Page 29] + +RFC 1945 HTTP/1.0 May 1996 + + + entity-body := Content-Encoding( Content-Type( data ) ) + + A Content-Type specifies the media type of the underlying data. A + Content-Encoding may be used to indicate any additional content + coding applied to the type, usually for the purpose of data + compression, that is a property of the resource requested. The + default for the content encoding is none (i.e., the identity + function). + + Any HTTP/1.0 message containing an entity body should include a + Content-Type header field defining the media type of that body. If + and only if the media type is not given by a Content-Type header, as + is the case for Simple-Response messages, the recipient may attempt + to guess the media type via inspection of its content and/or the name + extension(s) of the URL used to identify the resource. If the media + type remains unknown, the recipient should treat it as type + "application/octet-stream". + +7.2.2 Length + + When an Entity-Body is included with a message, the length of that + body may be determined in one of two ways. If a Content-Length header + field is present, its value in bytes represents the length of the + Entity-Body. Otherwise, the body length is determined by the closing + of the connection by the server. + + Closing the connection cannot be used to indicate the end of a + request body, since it leaves no possibility for the server to send + back a response. Therefore, HTTP/1.0 requests containing an entity + body must include a valid Content-Length header field. If a request + contains an entity body and Content-Length is not specified, and the + server does not recognize or cannot calculate the length from other + fields, then the server should send a 400 (bad request) response. + + Note: Some older servers supply an invalid Content-Length when + sending a document that contains server-side includes dynamically + inserted into the data stream. It must be emphasized that this + will not be tolerated by future versions of HTTP. Unless the + client knows that it is receiving a response from a compliant + server, it should not depend on the Content-Length value being + correct. + +8. Method Definitions + + The set of common methods for HTTP/1.0 is defined below. Although + this set can be expanded, additional methods cannot be assumed to + share the same semantics for separately extended clients and servers. + + + + +Berners-Lee, et al Informational [Page 30] + +RFC 1945 HTTP/1.0 May 1996 + + +8.1 GET + + The GET method means retrieve whatever information (in the form of an + entity) is identified by the Request-URI. If the Request-URI refers + to a data-producing process, it is the produced data which shall be + returned as the entity in the response and not the source text of the + process, unless that text happens to be the output of the process. + + The semantics of the GET method changes to a "conditional GET" if the + request message includes an If-Modified-Since header field. A + conditional GET method requests that the identified resource be + transferred only if it has been modified since the date given by the + If-Modified-Since header, as described in Section 10.9. The + conditional GET method is intended to reduce network usage by + allowing cached entities to be refreshed without requiring multiple + requests or transferring unnecessary data. + +8.2 HEAD + + The HEAD method is identical to GET except that the server must not + return any Entity-Body in the response. The metainformation contained + in the HTTP headers in response to a HEAD request should be identical + to the information sent in response to a GET request. This method can + be used for obtaining metainformation about the resource identified + by the Request-URI without transferring the Entity-Body itself. This + method is often used for testing hypertext links for validity, + accessibility, and recent modification. + + There is no "conditional HEAD" request analogous to the conditional + GET. If an If-Modified-Since header field is included with a HEAD + request, it should be ignored. + +8.3 POST + + The POST method is used to request that the destination server accept + the entity enclosed in the request as a new subordinate of the + resource identified by the Request-URI in the Request-Line. POST is + designed to allow a uniform method to cover the following functions: + + o Annotation of existing resources; + + o Posting a message to a bulletin board, newsgroup, mailing list, + or similar group of articles; + + o Providing a block of data, such as the result of submitting a + form [3], to a data-handling process; + + o Extending a database through an append operation. + + + +Berners-Lee, et al Informational [Page 31] + +RFC 1945 HTTP/1.0 May 1996 + + + The actual function performed by the POST method is determined by the + server and is usually dependent on the Request-URI. The posted entity + is subordinate to that URI in the same way that a file is subordinate + to a directory containing it, a news article is subordinate to a + newsgroup to which it is posted, or a record is subordinate to a + database. + + A successful POST does not require that the entity be created as a + resource on the origin server or made accessible for future + reference. That is, the action performed by the POST method might not + result in a resource that can be identified by a URI. In this case, + either 200 (ok) or 204 (no content) is the appropriate response + status, depending on whether or not the response includes an entity + that describes the result. + + If a resource has been created on the origin server, the response + should be 201 (created) and contain an entity (preferably of type + "text/html") which describes the status of the request and refers to + the new resource. + + A valid Content-Length is required on all HTTP/1.0 POST requests. An + HTTP/1.0 server should respond with a 400 (bad request) message if it + cannot determine the length of the request message's content. + + Applications must not cache responses to a POST request because the + application has no way of knowing that the server would return an + equivalent response on some future request. + +9. Status Code Definitions + + Each Status-Code is described below, including a description of which + method(s) it can follow and any metainformation required in the + response. + +9.1 Informational 1xx + + This class of status code indicates a provisional response, + consisting only of the Status-Line and optional headers, and is + terminated by an empty line. HTTP/1.0 does not define any 1xx status + codes and they are not a valid response to a HTTP/1.0 request. + However, they may be useful for experimental applications which are + outside the scope of this specification. + +9.2 Successful 2xx + + This class of status code indicates that the client's request was + successfully received, understood, and accepted. + + + + +Berners-Lee, et al Informational [Page 32] + +RFC 1945 HTTP/1.0 May 1996 + + + 200 OK + + The request has succeeded. The information returned with the + response is dependent on the method used in the request, as follows: + + GET an entity corresponding to the requested resource is sent + in the response; + + HEAD the response must only contain the header information and + no Entity-Body; + + POST an entity describing or containing the result of the action. + + 201 Created + + The request has been fulfilled and resulted in a new resource being + created. The newly created resource can be referenced by the URI(s) + returned in the entity of the response. The origin server should + create the resource before using this Status-Code. If the action + cannot be carried out immediately, the server must include in the + response body a description of when the resource will be available; + otherwise, the server should respond with 202 (accepted). + + Of the methods defined by this specification, only POST can create a + resource. + + 202 Accepted + + The request has been accepted for processing, but the processing + has not been completed. The request may or may not eventually be + acted upon, as it may be disallowed when processing actually takes + place. There is no facility for re-sending a status code from an + asynchronous operation such as this. + + The 202 response is intentionally non-committal. Its purpose is to + allow a server to accept a request for some other process (perhaps + a batch-oriented process that is only run once per day) without + requiring that the user agent's connection to the server persist + until the process is completed. The entity returned with this + response should include an indication of the request's current + status and either a pointer to a status monitor or some estimate of + when the user can expect the request to be fulfilled. + + 204 No Content + + The server has fulfilled the request but there is no new + information to send back. If the client is a user agent, it should + not change its document view from that which caused the request to + + + +Berners-Lee, et al Informational [Page 33] + +RFC 1945 HTTP/1.0 May 1996 + + + be generated. This response is primarily intended to allow input + for scripts or other actions to take place without causing a change + to the user agent's active document view. The response may include + new metainformation in the form of entity headers, which should + apply to the document currently in the user agent's active view. + +9.3 Redirection 3xx + + This class of status code indicates that further action needs to be + taken by the user agent in order to fulfill the request. The action + required may be carried out by the user agent without interaction + with the user if and only if the method used in the subsequent + request is GET or HEAD. A user agent should never automatically + redirect a request more than 5 times, since such redirections usually + indicate an infinite loop. + + 300 Multiple Choices + + This response code is not directly used by HTTP/1.0 applications, + but serves as the default for interpreting the 3xx class of + responses. + + The requested resource is available at one or more locations. + Unless it was a HEAD request, the response should include an entity + containing a list of resource characteristics and locations from + which the user or user agent can choose the one most appropriate. + If the server has a preferred choice, it should include the URL in + a Location field; user agents may use this field value for + automatic redirection. + + 301 Moved Permanently + + The requested resource has been assigned a new permanent URL and + any future references to this resource should be done using that + URL. Clients with link editing capabilities should automatically + relink references to the Request-URI to the new reference returned + by the server, where possible. + + The new URL must be given by the Location field in the response. + Unless it was a HEAD request, the Entity-Body of the response + should contain a short note with a hyperlink to the new URL. + + If the 301 status code is received in response to a request using + the POST method, the user agent must not automatically redirect the + request unless it can be confirmed by the user, since this might + change the conditions under which the request was issued. + + + + + +Berners-Lee, et al Informational [Page 34] + +RFC 1945 HTTP/1.0 May 1996 + + + Note: When automatically redirecting a POST request after + receiving a 301 status code, some existing user agents will + erroneously change it into a GET request. + + 302 Moved Temporarily + + The requested resource resides temporarily under a different URL. + Since the redirection may be altered on occasion, the client should + continue to use the Request-URI for future requests. + + The URL must be given by the Location field in the response. Unless + it was a HEAD request, the Entity-Body of the response should + contain a short note with a hyperlink to the new URI(s). + + If the 302 status code is received in response to a request using + the POST method, the user agent must not automatically redirect the + request unless it can be confirmed by the user, since this might + change the conditions under which the request was issued. + + Note: When automatically redirecting a POST request after + receiving a 302 status code, some existing user agents will + erroneously change it into a GET request. + + 304 Not Modified + + If the client has performed a conditional GET request and access is + allowed, but the document has not been modified since the date and + time specified in the If-Modified-Since field, the server must + respond with this status code and not send an Entity-Body to the + client. Header fields contained in the response should only include + information which is relevant to cache managers or which may have + changed independently of the entity's Last-Modified date. Examples + of relevant header fields include: Date, Server, and Expires. A + cache should update its cached entity to reflect any new field + values given in the 304 response. + +9.4 Client Error 4xx + + The 4xx class of status code is intended for cases in which the + client seems to have erred. If the client has not completed the + request when a 4xx code is received, it should immediately cease + sending data to the server. Except when responding to a HEAD request, + the server should include an entity containing an explanation of the + error situation, and whether it is a temporary or permanent + condition. These status codes are applicable to any request method. + + + + + + +Berners-Lee, et al Informational [Page 35] + +RFC 1945 HTTP/1.0 May 1996 + + + Note: If the client is sending data, server implementations on TCP + should be careful to ensure that the client acknowledges receipt + of the packet(s) containing the response prior to closing the + input connection. If the client continues sending data to the + server after the close, the server's controller will send a reset + packet to the client, which may erase the client's unacknowledged + input buffers before they can be read and interpreted by the HTTP + application. + + 400 Bad Request + + The request could not be understood by the server due to malformed + syntax. The client should not repeat the request without + modifications. + + 401 Unauthorized + + The request requires user authentication. The response must include + a WWW-Authenticate header field (Section 10.16) containing a + challenge applicable to the requested resource. The client may + repeat the request with a suitable Authorization header field + (Section 10.2). If the request already included Authorization + credentials, then the 401 response indicates that authorization has + been refused for those credentials. If the 401 response contains + the same challenge as the prior response, and the user agent has + already attempted authentication at least once, then the user + should be presented the entity that was given in the response, + since that entity may include relevant diagnostic information. HTTP + access authentication is explained in Section 11. + + 403 Forbidden + + The server understood the request, but is refusing to fulfill it. + Authorization will not help and the request should not be repeated. + If the request method was not HEAD and the server wishes to make + public why the request has not been fulfilled, it should describe + the reason for the refusal in the entity body. This status code is + commonly used when the server does not wish to reveal exactly why + the request has been refused, or when no other response is + applicable. + + 404 Not Found + + The server has not found anything matching the Request-URI. No + indication is given of whether the condition is temporary or + permanent. If the server does not wish to make this information + available to the client, the status code 403 (forbidden) can be + used instead. + + + +Berners-Lee, et al Informational [Page 36] + +RFC 1945 HTTP/1.0 May 1996 + + +9.5 Server Error 5xx + + Response status codes beginning with the digit "5" indicate cases in + which the server is aware that it has erred or is incapable of + performing the request. If the client has not completed the request + when a 5xx code is received, it should immediately cease sending data + to the server. Except when responding to a HEAD request, the server + should include an entity containing an explanation of the error + situation, and whether it is a temporary or permanent condition. + These response codes are applicable to any request method and there + are no required header fields. + + 500 Internal Server Error + + The server encountered an unexpected condition which prevented it + from fulfilling the request. + + 501 Not Implemented + + The server does not support the functionality required to fulfill + the request. This is the appropriate response when the server does + not recognize the request method and is not capable of supporting + it for any resource. + + 502 Bad Gateway + + The server, while acting as a gateway or proxy, received an invalid + response from the upstream server it accessed in attempting to + fulfill the request. + + 503 Service Unavailable + + The server is currently unable to handle the request due to a + temporary overloading or maintenance of the server. The implication + is that this is a temporary condition which will be alleviated + after some delay. + + Note: The existence of the 503 status code does not imply + that a server must use it when becoming overloaded. Some + servers may wish to simply refuse the connection. + +10. Header Field Definitions + + This section defines the syntax and semantics of all commonly used + HTTP/1.0 header fields. For general and entity header fields, both + sender and recipient refer to either the client or the server, + depending on who sends and who receives the message. + + + + +Berners-Lee, et al Informational [Page 37] + +RFC 1945 HTTP/1.0 May 1996 + + +10.1 Allow + + The Allow entity-header field lists the set of methods supported by + the resource identified by the Request-URI. The purpose of this field + is strictly to inform the recipient of valid methods associated with + the resource. The Allow header field is not permitted in a request + using the POST method, and thus should be ignored if it is received + as part of a POST entity. + + Allow = "Allow" ":" 1#method + + Example of use: + + Allow: GET, HEAD + + This field cannot prevent a client from trying other methods. + However, the indications given by the Allow header field value should + be followed. The actual set of allowed methods is defined by the + origin server at the time of each request. + + A proxy must not modify the Allow header field even if it does not + understand all the methods specified, since the user agent may have + other means of communicating with the origin server. + + The Allow header field does not indicate what methods are implemented + by the server. + +10.2 Authorization + + A user agent that wishes to authenticate itself with a server-- + usually, but not necessarily, after receiving a 401 response--may do + so by including an Authorization request-header field with the + request. The Authorization field value consists of credentials + containing the authentication information of the user agent for the + realm of the resource being requested. + + Authorization = "Authorization" ":" credentials + + HTTP access authentication is described in Section 11. If a request + is authenticated and a realm specified, the same credentials should + be valid for all other requests within this realm. + + Responses to requests containing an Authorization field are not + cachable. + + + + + + + +Berners-Lee, et al Informational [Page 38] + +RFC 1945 HTTP/1.0 May 1996 + + +10.3 Content-Encoding + + The Content-Encoding entity-header field is used as a modifier to the + media-type. When present, its value indicates what additional content + coding has been applied to the resource, and thus what decoding + mechanism must be applied in order to obtain the media-type + referenced by the Content-Type header field. The Content-Encoding is + primarily used to allow a document to be compressed without losing + the identity of its underlying media type. + + Content-Encoding = "Content-Encoding" ":" content-coding + + Content codings are defined in Section 3.5. An example of its use is + + Content-Encoding: x-gzip + + The Content-Encoding is a characteristic of the resource identified + by the Request-URI. Typically, the resource is stored with this + encoding and is only decoded before rendering or analogous usage. + +10.4 Content-Length + + The Content-Length entity-header field indicates the size of the + Entity-Body, in decimal number of octets, sent to the recipient or, + in the case of the HEAD method, the size of the Entity-Body that + would have been sent had the request been a GET. + + Content-Length = "Content-Length" ":" 1*DIGIT + + An example is + + Content-Length: 3495 + + Applications should use this field to indicate the size of the + Entity-Body to be transferred, regardless of the media type of the + entity. A valid Content-Length field value is required on all + HTTP/1.0 request messages containing an entity body. + + Any Content-Length greater than or equal to zero is a valid value. + Section 7.2.2 describes how to determine the length of a response + entity body if a Content-Length is not given. + + Note: The meaning of this field is significantly different from + the corresponding definition in MIME, where it is an optional + field used within the "message/external-body" content-type. In + HTTP, it should be used whenever the entity's length can be + determined prior to being transferred. + + + + +Berners-Lee, et al Informational [Page 39] + +RFC 1945 HTTP/1.0 May 1996 + + +10.5 Content-Type + + The Content-Type entity-header field indicates the media type of the + Entity-Body sent to the recipient or, in the case of the HEAD method, + the media type that would have been sent had the request been a GET. + + Content-Type = "Content-Type" ":" media-type + + Media types are defined in Section 3.6. An example of the field is + + Content-Type: text/html + + Further discussion of methods for identifying the media type of an + entity is provided in Section 7.2.1. + +10.6 Date + + The Date general-header field represents the date and time at which + the message was originated, having the same semantics as orig-date in + RFC 822. The field value is an HTTP-date, as described in Section + 3.3. + + Date = "Date" ":" HTTP-date + + An example is + + Date: Tue, 15 Nov 1994 08:12:31 GMT + + If a message is received via direct connection with the user agent + (in the case of requests) or the origin server (in the case of + responses), then the date can be assumed to be the current date at + the receiving end. However, since the date--as it is believed by the + origin--is important for evaluating cached responses, origin servers + should always include a Date header. Clients should only send a Date + header field in messages that include an entity body, as in the case + of the POST request, and even then it is optional. A received message + which does not have a Date header field should be assigned one by the + recipient if the message will be cached by that recipient or + gatewayed via a protocol which requires a Date. + + In theory, the date should represent the moment just before the + entity is generated. In practice, the date can be generated at any + time during the message origination without affecting its semantic + value. + + Note: An earlier version of this document incorrectly specified + that this field should contain the creation date of the enclosed + Entity-Body. This has been changed to reflect actual (and proper) + + + +Berners-Lee, et al Informational [Page 40] + +RFC 1945 HTTP/1.0 May 1996 + + + usage. + +10.7 Expires + + The Expires entity-header field gives the date/time after which the + entity should be considered stale. This allows information providers + to suggest the volatility of the resource, or a date after which the + information may no longer be valid. Applications must not cache this + entity beyond the date given. The presence of an Expires field does + not imply that the original resource will change or cease to exist + at, before, or after that time. However, information providers that + know or even suspect that a resource will change by a certain date + should include an Expires header with that date. The format is an + absolute date and time as defined by HTTP-date in Section 3.3. + + Expires = "Expires" ":" HTTP-date + + An example of its use is + + Expires: Thu, 01 Dec 1994 16:00:00 GMT + + If the date given is equal to or earlier than the value of the Date + header, the recipient must not cache the enclosed entity. If a + resource is dynamic by nature, as is the case with many data- + producing processes, entities from that resource should be given an + appropriate Expires value which reflects that dynamism. + + The Expires field cannot be used to force a user agent to refresh its + display or reload a resource; its semantics apply only to caching + mechanisms, and such mechanisms need only check a resource's + expiration status when a new request for that resource is initiated. + + User agents often have history mechanisms, such as "Back" buttons and + history lists, which can be used to redisplay an entity retrieved + earlier in a session. By default, the Expires field does not apply to + history mechanisms. If the entity is still in storage, a history + mechanism should display it even if the entity has expired, unless + the user has specifically configured the agent to refresh expired + history documents. + + Note: Applications are encouraged to be tolerant of bad or + misinformed implementations of the Expires header. A value of zero + (0) or an invalid date format should be considered equivalent to + an "expires immediately." Although these values are not legitimate + for HTTP/1.0, a robust implementation is always desirable. + + + + + + +Berners-Lee, et al Informational [Page 41] + +RFC 1945 HTTP/1.0 May 1996 + + +10.8 From + + The From request-header field, if given, should contain an Internet + e-mail address for the human user who controls the requesting user + agent. The address should be machine-usable, as defined by mailbox in + RFC 822 [7] (as updated by RFC 1123 [6]): + + From = "From" ":" mailbox + + An example is: + + From: webmaster@w3.org + + This header field may be used for logging purposes and as a means for + identifying the source of invalid or unwanted requests. It should not + be used as an insecure form of access protection. The interpretation + of this field is that the request is being performed on behalf of the + person given, who accepts responsibility for the method performed. In + particular, robot agents should include this header so that the + person responsible for running the robot can be contacted if problems + occur on the receiving end. + + The Internet e-mail address in this field may be separate from the + Internet host which issued the request. For example, when a request + is passed through a proxy, the original issuer's address should be + used. + + Note: The client should not send the From header field without the + user's approval, as it may conflict with the user's privacy + interests or their site's security policy. It is strongly + recommended that the user be able to disable, enable, and modify + the value of this field at any time prior to a request. + +10.9 If-Modified-Since + + The If-Modified-Since request-header field is used with the GET + method to make it conditional: if the requested resource has not been + modified since the time specified in this field, a copy of the + resource will not be returned from the server; instead, a 304 (not + modified) response will be returned without any Entity-Body. + + If-Modified-Since = "If-Modified-Since" ":" HTTP-date + + An example of the field is: + + If-Modified-Since: Sat, 29 Oct 1994 19:43:31 GMT + + + + + +Berners-Lee, et al Informational [Page 42] + +RFC 1945 HTTP/1.0 May 1996 + + + A conditional GET method requests that the identified resource be + transferred only if it has been modified since the date given by the + If-Modified-Since header. The algorithm for determining this includes + the following cases: + + a) If the request would normally result in anything other than + a 200 (ok) status, or if the passed If-Modified-Since date + is invalid, the response is exactly the same as for a + normal GET. A date which is later than the server's current + time is invalid. + + b) If the resource has been modified since the + If-Modified-Since date, the response is exactly the same as + for a normal GET. + + c) If the resource has not been modified since a valid + If-Modified-Since date, the server shall return a 304 (not + modified) response. + + The purpose of this feature is to allow efficient updates of cached + information with a minimum amount of transaction overhead. + +10.10 Last-Modified + + The Last-Modified entity-header field indicates the date and time at + which the sender believes the resource was last modified. The exact + semantics of this field are defined in terms of how the recipient + should interpret it: if the recipient has a copy of this resource + which is older than the date given by the Last-Modified field, that + copy should be considered stale. + + Last-Modified = "Last-Modified" ":" HTTP-date + + An example of its use is + + Last-Modified: Tue, 15 Nov 1994 12:45:26 GMT + + The exact meaning of this header field depends on the implementation + of the sender and the nature of the original resource. For files, it + may be just the file system last-modified time. For entities with + dynamically included parts, it may be the most recent of the set of + last-modify times for its component parts. For database gateways, it + may be the last-update timestamp of the record. For virtual objects, + it may be the last time the internal state changed. + + An origin server must not send a Last-Modified date which is later + than the server's time of message origination. In such cases, where + the resource's last modification would indicate some time in the + + + +Berners-Lee, et al Informational [Page 43] + +RFC 1945 HTTP/1.0 May 1996 + + + future, the server must replace that date with the message + origination date. + +10.11 Location + + The Location response-header field defines the exact location of the + resource that was identified by the Request-URI. For 3xx responses, + the location must indicate the server's preferred URL for automatic + redirection to the resource. Only one absolute URL is allowed. + + Location = "Location" ":" absoluteURI + + An example is + + Location: http://www.w3.org/hypertext/WWW/NewLocation.html + +10.12 Pragma + + The Pragma general-header field is used to include implementation- + specific directives that may apply to any recipient along the + request/response chain. All pragma directives specify optional + behavior from the viewpoint of the protocol; however, some systems + may require that behavior be consistent with the directives. + + Pragma = "Pragma" ":" 1#pragma-directive + + pragma-directive = "no-cache" | extension-pragma + extension-pragma = token [ "=" word ] + + When the "no-cache" directive is present in a request message, an + application should forward the request toward the origin server even + if it has a cached copy of what is being requested. This allows a + client to insist upon receiving an authoritative response to its + request. It also allows a client to refresh a cached copy which is + known to be corrupted or stale. + + Pragma directives must be passed through by a proxy or gateway + application, regardless of their significance to that application, + since the directives may be applicable to all recipients along the + request/response chain. It is not possible to specify a pragma for a + specific recipient; however, any pragma directive not relevant to a + recipient should be ignored by that recipient. + +10.13 Referer + + The Referer request-header field allows the client to specify, for + the server's benefit, the address (URI) of the resource from which + the Request-URI was obtained. This allows a server to generate lists + + + +Berners-Lee, et al Informational [Page 44] + +RFC 1945 HTTP/1.0 May 1996 + + + of back-links to resources for interest, logging, optimized caching, + etc. It also allows obsolete or mistyped links to be traced for + maintenance. The Referer field must not be sent if the Request-URI + was obtained from a source that does not have its own URI, such as + input from the user keyboard. + + Referer = "Referer" ":" ( absoluteURI | relativeURI ) + + Example: + + Referer: http://www.w3.org/hypertext/DataSources/Overview.html + + If a partial URI is given, it should be interpreted relative to the + Request-URI. The URI must not include a fragment. + + Note: Because the source of a link may be private information or + may reveal an otherwise private information source, it is strongly + recommended that the user be able to select whether or not the + Referer field is sent. For example, a browser client could have a + toggle switch for browsing openly/anonymously, which would + respectively enable/disable the sending of Referer and From + information. + +10.14 Server + + The Server response-header field contains information about the + software used by the origin server to handle the request. The field + can contain multiple product tokens (Section 3.7) and comments + identifying the server and any significant subproducts. By + convention, the product tokens are listed in order of their + significance for identifying the application. + + Server = "Server" ":" 1*( product | comment ) + + Example: + + Server: CERN/3.0 libwww/2.17 + + If the response is being forwarded through a proxy, the proxy + application must not add its data to the product list. + + Note: Revealing the specific software version of the server may + allow the server machine to become more vulnerable to attacks + against software that is known to contain security holes. Server + implementors are encouraged to make this field a configurable + option. + + + + + +Berners-Lee, et al Informational [Page 45] + +RFC 1945 HTTP/1.0 May 1996 + + + Note: Some existing servers fail to restrict themselves to the + product token syntax within the Server field. + +10.15 User-Agent + + The User-Agent request-header field contains information about the + user agent originating the request. This is for statistical purposes, + the tracing of protocol violations, and automated recognition of user + agents for the sake of tailoring responses to avoid particular user + agent limitations. Although it is not required, user agents should + include this field with requests. The field can contain multiple + product tokens (Section 3.7) and comments identifying the agent and + any subproducts which form a significant part of the user agent. By + convention, the product tokens are listed in order of their + significance for identifying the application. + + User-Agent = "User-Agent" ":" 1*( product | comment ) + + Example: + + User-Agent: CERN-LineMode/2.15 libwww/2.17b3 + + Note: Some current proxy applications append their product + information to the list in the User-Agent field. This is not + recommended, since it makes machine interpretation of these + fields ambiguous. + + Note: Some existing clients fail to restrict themselves to + the product token syntax within the User-Agent field. + +10.16 WWW-Authenticate + + The WWW-Authenticate response-header field must be included in 401 + (unauthorized) response messages. The field value consists of at + least one challenge that indicates the authentication scheme(s) and + parameters applicable to the Request-URI. + + WWW-Authenticate = "WWW-Authenticate" ":" 1#challenge + + The HTTP access authentication process is described in Section 11. + User agents must take special care in parsing the WWW-Authenticate + field value if it contains more than one challenge, or if more than + one WWW-Authenticate header field is provided, since the contents of + a challenge may itself contain a comma-separated list of + authentication parameters. + + + + + + +Berners-Lee, et al Informational [Page 46] + +RFC 1945 HTTP/1.0 May 1996 + + +11. Access Authentication + + HTTP provides a simple challenge-response authentication mechanism + which may be used by a server to challenge a client request and by a + client to provide authentication information. It uses an extensible, + case-insensitive token to identify the authentication scheme, + followed by a comma-separated list of attribute-value pairs which + carry the parameters necessary for achieving authentication via that + scheme. + + auth-scheme = token + + auth-param = token "=" quoted-string + + The 401 (unauthorized) response message is used by an origin server + to challenge the authorization of a user agent. This response must + include a WWW-Authenticate header field containing at least one + challenge applicable to the requested resource. + + challenge = auth-scheme 1*SP realm *( "," auth-param ) + + realm = "realm" "=" realm-value + realm-value = quoted-string + + The realm attribute (case-insensitive) is required for all + authentication schemes which issue a challenge. The realm value + (case-sensitive), in combination with the canonical root URL of the + server being accessed, defines the protection space. These realms + allow the protected resources on a server to be partitioned into a + set of protection spaces, each with its own authentication scheme + and/or authorization database. The realm value is a string, generally + assigned by the origin server, which may have additional semantics + specific to the authentication scheme. + + A user agent that wishes to authenticate itself with a server-- + usually, but not necessarily, after receiving a 401 response--may do + so by including an Authorization header field with the request. The + Authorization field value consists of credentials containing the + authentication information of the user agent for the realm of the + resource being requested. + + credentials = basic-credentials + | ( auth-scheme #auth-param ) + + The domain over which credentials can be automatically applied by a + user agent is determined by the protection space. If a prior request + has been authorized, the same credentials may be reused for all other + requests within that protection space for a period of time determined + + + +Berners-Lee, et al Informational [Page 47] + +RFC 1945 HTTP/1.0 May 1996 + + + by the authentication scheme, parameters, and/or user preference. + Unless otherwise defined by the authentication scheme, a single + protection space cannot extend outside the scope of its server. + + If the server does not wish to accept the credentials sent with a + request, it should return a 403 (forbidden) response. + + The HTTP protocol does not restrict applications to this simple + challenge-response mechanism for access authentication. Additional + mechanisms may be used, such as encryption at the transport level or + via message encapsulation, and with additional header fields + specifying authentication information. However, these additional + mechanisms are not defined by this specification. + + Proxies must be completely transparent regarding user agent + authentication. That is, they must forward the WWW-Authenticate and + Authorization headers untouched, and must not cache the response to a + request containing Authorization. HTTP/1.0 does not provide a means + for a client to be authenticated with a proxy. + +11.1 Basic Authentication Scheme + + The "basic" authentication scheme is based on the model that the user + agent must authenticate itself with a user-ID and a password for each + realm. The realm value should be considered an opaque string which + can only be compared for equality with other realms on that server. + The server will authorize the request only if it can validate the + user-ID and password for the protection space of the Request-URI. + There are no optional authentication parameters. + + Upon receipt of an unauthorized request for a URI within the + protection space, the server should respond with a challenge like the + following: + + WWW-Authenticate: Basic realm="WallyWorld" + + where "WallyWorld" is the string assigned by the server to identify + the protection space of the Request-URI. + + To receive authorization, the client sends the user-ID and password, + separated by a single colon (":") character, within a base64 [5] + encoded string in the credentials. + + basic-credentials = "Basic" SP basic-cookie + + basic-cookie = + + + + +Berners-Lee, et al Informational [Page 48] + +RFC 1945 HTTP/1.0 May 1996 + + + userid-password = [ token ] ":" *TEXT + + If the user agent wishes to send the user-ID "Aladdin" and password + "open sesame", it would use the following header field: + + Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== + + The basic authentication scheme is a non-secure method of filtering + unauthorized access to resources on an HTTP server. It is based on + the assumption that the connection between the client and the server + can be regarded as a trusted carrier. As this is not generally true + on an open network, the basic authentication scheme should be used + accordingly. In spite of this, clients should implement the scheme in + order to communicate with servers that use it. + +12. Security Considerations + + This section is meant to inform application developers, information + providers, and users of the security limitations in HTTP/1.0 as + described by this document. The discussion does not include + definitive solutions to the problems revealed, though it does make + some suggestions for reducing security risks. + +12.1 Authentication of Clients + + As mentioned in Section 11.1, the Basic authentication scheme is not + a secure method of user authentication, nor does it prevent the + Entity-Body from being transmitted in clear text across the physical + network used as the carrier. HTTP/1.0 does not prevent additional + authentication schemes and encryption mechanisms from being employed + to increase security. + +12.2 Safe Methods + + The writers of client software should be aware that the software + represents the user in their interactions over the Internet, and + should be careful to allow the user to be aware of any actions they + may take which may have an unexpected significance to themselves or + others. + + In particular, the convention has been established that the GET and + HEAD methods should never have the significance of taking an action + other than retrieval. These methods should be considered "safe." This + allows user agents to represent other methods, such as POST, in a + special way, so that the user is made aware of the fact that a + possibly unsafe action is being requested. + + + + + +Berners-Lee, et al Informational [Page 49] + +RFC 1945 HTTP/1.0 May 1996 + + + Naturally, it is not possible to ensure that the server does not + generate side-effects as a result of performing a GET request; in + fact, some dynamic resources consider that a feature. The important + distinction here is that the user did not request the side-effects, + so therefore cannot be held accountable for them. + +12.3 Abuse of Server Log Information + + A server is in the position to save personal data about a user's + requests which may identify their reading patterns or subjects of + interest. This information is clearly confidential in nature and its + handling may be constrained by law in certain countries. People using + the HTTP protocol to provide data are responsible for ensuring that + such material is not distributed without the permission of any + individuals that are identifiable by the published results. + +12.4 Transfer of Sensitive Information + + Like any generic data transfer protocol, HTTP cannot regulate the + content of the data that is transferred, nor is there any a priori + method of determining the sensitivity of any particular piece of + information within the context of any given request. Therefore, + applications should supply as much control over this information as + possible to the provider of that information. Three header fields are + worth special mention in this context: Server, Referer and From. + + Revealing the specific software version of the server may allow the + server machine to become more vulnerable to attacks against software + that is known to contain security holes. Implementors should make the + Server header field a configurable option. + + The Referer field allows reading patterns to be studied and reverse + links drawn. Although it can be very useful, its power can be abused + if user details are not separated from the information contained in + the Referer. Even when the personal information has been removed, the + Referer field may indicate a private document's URI whose publication + would be inappropriate. + + The information sent in the From field might conflict with the user's + privacy interests or their site's security policy, and hence it + should not be transmitted without the user being able to disable, + enable, and modify the contents of the field. The user must be able + to set the contents of this field within a user preference or + application defaults configuration. + + We suggest, though do not require, that a convenient toggle interface + be provided for the user to enable or disable the sending of From and + Referer information. + + + +Berners-Lee, et al Informational [Page 50] + +RFC 1945 HTTP/1.0 May 1996 + + +12.5 Attacks Based On File and Path Names + + Implementations of HTTP origin servers should be careful to restrict + the documents returned by HTTP requests to be only those that were + intended by the server administrators. If an HTTP server translates + HTTP URIs directly into file system calls, the server must take + special care not to serve files that were not intended to be + delivered to HTTP clients. For example, Unix, Microsoft Windows, and + other operating systems use ".." as a path component to indicate a + directory level above the current one. On such a system, an HTTP + server must disallow any such construct in the Request-URI if it + would otherwise allow access to a resource outside those intended to + be accessible via the HTTP server. Similarly, files intended for + reference only internally to the server (such as access control + files, configuration files, and script code) must be protected from + inappropriate retrieval, since they might contain sensitive + information. Experience has shown that minor bugs in such HTTP server + implementations have turned into security risks. + +13. Acknowledgments + + This specification makes heavy use of the augmented BNF and generic + constructs defined by David H. Crocker for RFC 822 [7]. Similarly, it + reuses many of the definitions provided by Nathaniel Borenstein and + Ned Freed for MIME [5]. We hope that their inclusion in this + specification will help reduce past confusion over the relationship + between HTTP/1.0 and Internet mail message formats. + + The HTTP protocol has evolved considerably over the past four years. + It has benefited from a large and active developer community--the + many people who have participated on the www-talk mailing list--and + it is that community which has been most responsible for the success + of HTTP and of the World-Wide Web in general. Marc Andreessen, Robert + Cailliau, Daniel W. Connolly, Bob Denny, Jean-Francois Groff, Phillip + M. Hallam-Baker, Hakon W. Lie, Ari Luotonen, Rob McCool, Lou + Montulli, Dave Raggett, Tony Sanders, and Marc VanHeyningen deserve + special recognition for their efforts in defining aspects of the + protocol for early versions of this specification. + + Paul Hoffman contributed sections regarding the informational status + of this document and Appendices C and D. + + + + + + + + + + +Berners-Lee, et al Informational [Page 51] + +RFC 1945 HTTP/1.0 May 1996 + + + This document has benefited greatly from the comments of all those + participating in the HTTP-WG. In addition to those already mentioned, + the following individuals have contributed to this specification: + + Gary Adams Harald Tveit Alvestrand + Keith Ball Brian Behlendorf + Paul Burchard Maurizio Codogno + Mike Cowlishaw Roman Czyborra + Michael A. Dolan John Franks + Jim Gettys Marc Hedlund + Koen Holtman Alex Hopmann + Bob Jernigan Shel Kaphan + Martijn Koster Dave Kristol + Daniel LaLiberte Paul Leach + Albert Lunde John C. Mallery + Larry Masinter Mitra + Jeffrey Mogul Gavin Nicol + Bill Perry Jeffrey Perry + Owen Rees Luigi Rizzo + David Robinson Marc Salomon + Rich Salz Jim Seidman + Chuck Shotton Eric W. Sink + Simon E. Spero Robert S. Thau + Francois Yergeau Mary Ellen Zurko + Jean-Philippe Martin-Flatin + +14. References + + [1] Anklesaria, F., McCahill, M., Lindner, P., Johnson, D., + Torrey, D., and B. Alberti, "The Internet Gopher Protocol: A + Distributed Document Search and Retrieval Protocol", RFC 1436, + University of Minnesota, March 1993. + + [2] Berners-Lee, T., "Universal Resource Identifiers in WWW: A + Unifying Syntax for the Expression of Names and Addresses of + Objects on the Network as used in the World-Wide Web", + RFC 1630, CERN, June 1994. + + [3] Berners-Lee, T., and D. Connolly, "Hypertext Markup Language - + 2.0", RFC 1866, MIT/W3C, November 1995. + + [4] Berners-Lee, T., Masinter, L., and M. McCahill, "Uniform + Resource Locators (URL)", RFC 1738, CERN, Xerox PARC, + University of Minnesota, December 1994. + + + + + + + +Berners-Lee, et al Informational [Page 52] + +RFC 1945 HTTP/1.0 May 1996 + + + [5] Borenstein, N., and N. Freed, "MIME (Multipurpose Internet Mail + Extensions) Part One: Mechanisms for Specifying and Describing + the Format of Internet Message Bodies", RFC 1521, Bellcore, + Innosoft, September 1993. + + [6] Braden, R., "Requirements for Internet hosts - Application and + Support", STD 3, RFC 1123, IETF, October 1989. + + [7] Crocker, D., "Standard for the Format of ARPA Internet Text + Messages", STD 11, RFC 822, UDEL, August 1982. + + [8] F. Davis, B. Kahle, H. Morris, J. Salem, T. Shen, R. Wang, + J. Sui, and M. Grinbaum. "WAIS Interface Protocol Prototype + Functional Specification." (v1.5), Thinking Machines + Corporation, April 1990. + + [9] Fielding, R., "Relative Uniform Resource Locators", RFC 1808, + UC Irvine, June 1995. + + [10] Horton, M., and R. Adams, "Standard for interchange of USENET + Messages", RFC 1036 (Obsoletes RFC 850), AT&T Bell + Laboratories, Center for Seismic Studies, December 1987. + + [11] Kantor, B., and P. Lapsley, "Network News Transfer Protocol: + A Proposed Standard for the Stream-Based Transmission of News", + RFC 977, UC San Diego, UC Berkeley, February 1986. + + [12] Postel, J., "Simple Mail Transfer Protocol." STD 10, RFC 821, + USC/ISI, August 1982. + + [13] Postel, J., "Media Type Registration Procedure." RFC 1590, + USC/ISI, March 1994. + + [14] Postel, J., and J. Reynolds, "File Transfer Protocol (FTP)", + STD 9, RFC 959, USC/ISI, October 1985. + + [15] Reynolds, J., and J. Postel, "Assigned Numbers", STD 2, RFC + 1700, USC/ISI, October 1994. + + [16] Sollins, K., and L. Masinter, "Functional Requirements for + Uniform Resource Names", RFC 1737, MIT/LCS, Xerox Corporation, + December 1994. + + [17] US-ASCII. Coded Character Set - 7-Bit American Standard Code + for Information Interchange. Standard ANSI X3.4-1986, ANSI, + 1986. + + + + + +Berners-Lee, et al Informational [Page 53] + +RFC 1945 HTTP/1.0 May 1996 + + + [18] ISO-8859. International Standard -- Information Processing -- + 8-bit Single-Byte Coded Graphic Character Sets -- + Part 1: Latin alphabet No. 1, ISO 8859-1:1987. + Part 2: Latin alphabet No. 2, ISO 8859-2, 1987. + Part 3: Latin alphabet No. 3, ISO 8859-3, 1988. + Part 4: Latin alphabet No. 4, ISO 8859-4, 1988. + Part 5: Latin/Cyrillic alphabet, ISO 8859-5, 1988. + Part 6: Latin/Arabic alphabet, ISO 8859-6, 1987. + Part 7: Latin/Greek alphabet, ISO 8859-7, 1987. + Part 8: Latin/Hebrew alphabet, ISO 8859-8, 1988. + Part 9: Latin alphabet No. 5, ISO 8859-9, 1990. + +15. Authors' Addresses + + Tim Berners-Lee + Director, W3 Consortium + MIT Laboratory for Computer Science + 545 Technology Square + Cambridge, MA 02139, U.S.A. + + Fax: +1 (617) 258 8682 + EMail: timbl@w3.org + + + Roy T. Fielding + Department of Information and Computer Science + University of California + Irvine, CA 92717-3425, U.S.A. + + Fax: +1 (714) 824-4056 + EMail: fielding@ics.uci.edu + + + Henrik Frystyk Nielsen + W3 Consortium + MIT Laboratory for Computer Science + 545 Technology Square + Cambridge, MA 02139, U.S.A. + + Fax: +1 (617) 258 8682 + EMail: frystyk@w3.org + + + + + + + + + + +Berners-Lee, et al Informational [Page 54] + +RFC 1945 HTTP/1.0 May 1996 + + +Appendices + + These appendices are provided for informational reasons only -- they + do not form a part of the HTTP/1.0 specification. + +A. Internet Media Type message/http + + In addition to defining the HTTP/1.0 protocol, this document serves + as the specification for the Internet media type "message/http". The + following is to be registered with IANA [13]. + + Media Type name: message + + Media subtype name: http + + Required parameters: none + + Optional parameters: version, msgtype + + version: The HTTP-Version number of the enclosed message + (e.g., "1.0"). If not present, the version can be + determined from the first line of the body. + + msgtype: The message type -- "request" or "response". If + not present, the type can be determined from the + first line of the body. + + Encoding considerations: only "7bit", "8bit", or "binary" are + permitted + + Security considerations: none + +B. Tolerant Applications + + Although this document specifies the requirements for the generation + of HTTP/1.0 messages, not all applications will be correct in their + implementation. We therefore recommend that operational applications + be tolerant of deviations whenever those deviations can be + interpreted unambiguously. + + Clients should be tolerant in parsing the Status-Line and servers + tolerant when parsing the Request-Line. In particular, they should + accept any amount of SP or HT characters between fields, even though + only a single SP is required. + + The line terminator for HTTP-header fields is the sequence CRLF. + However, we recommend that applications, when parsing such headers, + recognize a single LF as a line terminator and ignore the leading CR. + + + +Berners-Lee, et al Informational [Page 55] + +RFC 1945 HTTP/1.0 May 1996 + + +C. Relationship to MIME + + HTTP/1.0 uses many of the constructs defined for Internet Mail (RFC + 822 [7]) and the Multipurpose Internet Mail Extensions (MIME [5]) to + allow entities to be transmitted in an open variety of + representations and with extensible mechanisms. However, RFC 1521 + discusses mail, and HTTP has a few features that are different than + those described in RFC 1521. These differences were carefully chosen + to optimize performance over binary connections, to allow greater + freedom in the use of new media types, to make date comparisons + easier, and to acknowledge the practice of some early HTTP servers + and clients. + + At the time of this writing, it is expected that RFC 1521 will be + revised. The revisions may include some of the practices found in + HTTP/1.0 but not in RFC 1521. + + This appendix describes specific areas where HTTP differs from RFC + 1521. Proxies and gateways to strict MIME environments should be + aware of these differences and provide the appropriate conversions + where necessary. Proxies and gateways from MIME environments to HTTP + also need to be aware of the differences because some conversions may + be required. + +C.1 Conversion to Canonical Form + + RFC 1521 requires that an Internet mail entity be converted to + canonical form prior to being transferred, as described in Appendix G + of RFC 1521 [5]. Section 3.6.1 of this document describes the forms + allowed for subtypes of the "text" media type when transmitted over + HTTP. + + RFC 1521 requires that content with a Content-Type of "text" + represent line breaks as CRLF and forbids the use of CR or LF outside + of line break sequences. HTTP allows CRLF, bare CR, and bare LF to + indicate a line break within text content when a message is + transmitted over HTTP. + + Where it is possible, a proxy or gateway from HTTP to a strict RFC + 1521 environment should translate all line breaks within the text + media types described in Section 3.6.1 of this document to the RFC + 1521 canonical form of CRLF. Note, however, that this may be + complicated by the presence of a Content-Encoding and by the fact + that HTTP allows the use of some character sets which do not use + octets 13 and 10 to represent CR and LF, as is the case for some + multi-byte character sets. + + + + + +Berners-Lee, et al Informational [Page 56] + +RFC 1945 HTTP/1.0 May 1996 + + +C.2 Conversion of Date Formats + + HTTP/1.0 uses a restricted set of date formats (Section 3.3) to + simplify the process of date comparison. Proxies and gateways from + other protocols should ensure that any Date header field present in a + message conforms to one of the HTTP/1.0 formats and rewrite the date + if necessary. + +C.3 Introduction of Content-Encoding + + RFC 1521 does not include any concept equivalent to HTTP/1.0's + Content-Encoding header field. Since this acts as a modifier on the + media type, proxies and gateways from HTTP to MIME-compliant + protocols must either change the value of the Content-Type header + field or decode the Entity-Body before forwarding the message. (Some + experimental applications of Content-Type for Internet mail have used + a media-type parameter of ";conversions=" to perform + an equivalent function as Content-Encoding. However, this parameter + is not part of RFC 1521.) + +C.4 No Content-Transfer-Encoding + + HTTP does not use the Content-Transfer-Encoding (CTE) field of RFC + 1521. Proxies and gateways from MIME-compliant protocols to HTTP must + remove any non-identity CTE ("quoted-printable" or "base64") encoding + prior to delivering the response message to an HTTP client. + + Proxies and gateways from HTTP to MIME-compliant protocols are + responsible for ensuring that the message is in the correct format + and encoding for safe transport on that protocol, where "safe + transport" is defined by the limitations of the protocol being used. + Such a proxy or gateway should label the data with an appropriate + Content-Transfer-Encoding if doing so will improve the likelihood of + safe transport over the destination protocol. + +C.5 HTTP Header Fields in Multipart Body-Parts + + In RFC 1521, most header fields in multipart body-parts are generally + ignored unless the field name begins with "Content-". In HTTP/1.0, + multipart body-parts may contain any HTTP header fields which are + significant to the meaning of that part. + +D. Additional Features + + This appendix documents protocol elements used by some existing HTTP + implementations, but not consistently and correctly across most + HTTP/1.0 applications. Implementors should be aware of these + features, but cannot rely upon their presence in, or interoperability + + + +Berners-Lee, et al Informational [Page 57] + +RFC 1945 HTTP/1.0 May 1996 + + + with, other HTTP/1.0 applications. + +D.1 Additional Request Methods + +D.1.1 PUT + + The PUT method requests that the enclosed entity be stored under the + supplied Request-URI. If the Request-URI refers to an already + existing resource, the enclosed entity should be considered as a + modified version of the one residing on the origin server. If the + Request-URI does not point to an existing resource, and that URI is + capable of being defined as a new resource by the requesting user + agent, the origin server can create the resource with that URI. + + The fundamental difference between the POST and PUT requests is + reflected in the different meaning of the Request-URI. The URI in a + POST request identifies the resource that will handle the enclosed + entity as data to be processed. That resource may be a data-accepting + process, a gateway to some other protocol, or a separate entity that + accepts annotations. In contrast, the URI in a PUT request identifies + the entity enclosed with the request -- the user agent knows what URI + is intended and the server should not apply the request to some other + resource. + +D.1.2 DELETE + + The DELETE method requests that the origin server delete the resource + identified by the Request-URI. + +D.1.3 LINK + + The LINK method establishes one or more Link relationships between + the existing resource identified by the Request-URI and other + existing resources. + +D.1.4 UNLINK + + The UNLINK method removes one or more Link relationships from the + existing resource identified by the Request-URI. + +D.2 Additional Header Field Definitions + +D.2.1 Accept + + The Accept request-header field can be used to indicate a list of + media ranges which are acceptable as a response to the request. The + asterisk "*" character is used to group media types into ranges, with + "*/*" indicating all media types and "type/*" indicating all subtypes + + + +Berners-Lee, et al Informational [Page 58] + +RFC 1945 HTTP/1.0 May 1996 + + + of that type. The set of ranges given by the client should represent + what types are acceptable given the context of the request. + +D.2.2 Accept-Charset + + The Accept-Charset request-header field can be used to indicate a + list of preferred character sets other than the default US-ASCII and + ISO-8859-1. This field allows clients capable of understanding more + comprehensive or special-purpose character sets to signal that + capability to a server which is capable of representing documents in + those character sets. + +D.2.3 Accept-Encoding + + The Accept-Encoding request-header field is similar to Accept, but + restricts the content-coding values which are acceptable in the + response. + +D.2.4 Accept-Language + + The Accept-Language request-header field is similar to Accept, but + restricts the set of natural languages that are preferred as a + response to the request. + +D.2.5 Content-Language + + The Content-Language entity-header field describes the natural + language(s) of the intended audience for the enclosed entity. Note + that this may not be equivalent to all the languages used within the + entity. + +D.2.6 Link + + The Link entity-header field provides a means for describing a + relationship between the entity and some other resource. An entity + may include multiple Link values. Links at the metainformation level + typically indicate relationships like hierarchical structure and + navigation paths. + +D.2.7 MIME-Version + + HTTP messages may include a single MIME-Version general-header field + to indicate what version of the MIME protocol was used to construct + the message. Use of the MIME-Version header field, as defined by RFC + 1521 [5], should indicate that the message is MIME-conformant. + Unfortunately, some older HTTP/1.0 servers send it indiscriminately, + and thus this field should be ignored. + + + + +Berners-Lee, et al Informational [Page 59] + +RFC 1945 HTTP/1.0 May 1996 + + +D.2.8 Retry-After + + The Retry-After response-header field can be used with a 503 (service + unavailable) response to indicate how long the service is expected to + be unavailable to the requesting client. The value of this field can + be either an HTTP-date or an integer number of seconds (in decimal) + after the time of the response. + +D.2.9 Title + + The Title entity-header field indicates the title of the entity. + +D.2.10 URI + + The URI entity-header field may contain some or all of the Uniform + Resource Identifiers (Section 3.2) by which the Request-URI resource + can be identified. There is no guarantee that the resource can be + accessed using the URI(s) specified. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Berners-Lee, et al Informational [Page 60] diff --git a/dist/docs/rfc1981-pmtud_v6.txt b/dist/docs/rfc1981-pmtud_v6.txt new file mode 100644 index 0000000..bb66bb2 --- /dev/null +++ b/dist/docs/rfc1981-pmtud_v6.txt @@ -0,0 +1,842 @@ + + + + + + +Network Working Group J. McCann +Request for Comments: 1981 Digital Equipment Corporation +Category: Standards Track S. Deering + Xerox PARC + J. Mogul + Digital Equipment Corporation + August 1996 + + + Path MTU Discovery for IP version 6 + +Status of this Memo + + This document specifies an Internet standards track protocol for the + Internet community, and requests discussion and suggestions for + improvements. Please refer to the current edition of the "Internet + Official Protocol Standards" (STD 1) for the standardization state + and status of this protocol. Distribution of this memo is unlimited. + +Abstract + + This document describes Path MTU Discovery for IP version 6. It is + largely derived from RFC 1191, which describes Path MTU Discovery for + IP version 4. + +Table of Contents + + 1. Introduction.................................................2 + 2. Terminology..................................................2 + 3. Protocol overview............................................3 + 4. Protocol Requirements........................................4 + 5. Implementation Issues........................................5 + 5.1. Layering...................................................5 + 5.2. Storing PMTU information...................................6 + 5.3. Purging stale PMTU information.............................8 + 5.4. TCP layer actions..........................................9 + 5.5. Issues for other transport protocols......................11 + 5.6. Management interface......................................12 + 6. Security Considerations.....................................12 + Acknowledgements...............................................13 + Appendix A - Comparison to RFC 1191............................14 + References.....................................................14 + Authors' Addresses.............................................15 + + + + + + + + +McCann, Deering & Mogul Standards Track [Page 1] + +RFC 1981 Path MTU Discovery for IPv6 August 1996 + + +1. Introduction + + When one IPv6 node has a large amount of data to send to another + node, the data is transmitted in a series of IPv6 packets. It is + usually preferable that these packets be of the largest size that can + successfully traverse the path from the source node to the + destination node. This packet size is referred to as the Path MTU + (PMTU), and it is equal to the minimum link MTU of all the links in a + path. IPv6 defines a standard mechanism for a node to discover the + PMTU of an arbitrary path. + + IPv6 nodes SHOULD implement Path MTU Discovery in order to discover + and take advantage of paths with PMTU greater than the IPv6 minimum + link MTU [IPv6-SPEC]. A minimal IPv6 implementation (e.g., in a boot + ROM) may choose to omit implementation of Path MTU Discovery. + + Nodes not implementing Path MTU Discovery use the IPv6 minimum link + MTU defined in [IPv6-SPEC] as the maximum packet size. In most + cases, this will result in the use of smaller packets than necessary, + because most paths have a PMTU greater than the IPv6 minimum link + MTU. A node sending packets much smaller than the Path MTU allows is + wasting network resources and probably getting suboptimal throughput. + +2. Terminology + + node - a device that implements IPv6. + + router - a node that forwards IPv6 packets not explicitly + addressed to itself. + + host - any node that is not a router. + + 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 or lower-layer protocols being "tunneled" + over (i.e., encapsulated in) IPv6 such as 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 (or higher) layer "tunnels", + such as tunnels over IPv4 or IPv6 itself. + + interface - a node's attachment to a link. + + + + +McCann, Deering & Mogul Standards Track [Page 2] + +RFC 1981 Path MTU Discovery for IPv6 August 1996 + + + 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 in one piece over + a link. + + path - the set of links traversed by a packet between a source + node and a destination node + + path MTU - the minimum link MTU of all the links in a path between + a source node and a destination node. + + PMTU - path MTU + + Path MTU + Discovery - process by which a node learns the PMTU of a path + + flow - a sequence of packets sent from a particular source + to a particular (unicast or multicast) destination for + which the source desires special handling by the + intervening routers. + + flow id - a combination of a source address and a non-zero + flow label. + +3. Protocol overview + + This memo describes a technique to dynamically discover the PMTU of a + path. The basic idea is that a source node initially assumes that + the PMTU of a path is the (known) MTU of the first hop in the path. + If any of the packets sent on that path are too large to be forwarded + by some node along the path, that node will discard them and return + ICMPv6 Packet Too Big messages [ICMPv6]. Upon receipt of such a + message, the source node reduces its assumed PMTU for the path based + on the MTU of the constricting hop as reported in the Packet Too Big + message. + + The Path MTU Discovery process ends when the node's estimate of the + PMTU is less than or equal to the actual PMTU. Note that several + iterations of the packet-sent/Packet-Too-Big-message-received cycle + may occur before the Path MTU Discovery process ends, as there may be + links with smaller MTUs further along the path. + + Alternatively, the node may elect to end the discovery process by + ceasing to send packets larger than the IPv6 minimum link MTU. + + + +McCann, Deering & Mogul Standards Track [Page 3] + +RFC 1981 Path MTU Discovery for IPv6 August 1996 + + + The PMTU of a path may change over time, due to changes in the + routing topology. Reductions of the PMTU are detected by Packet Too + Big messages. To detect increases in a path's PMTU, a node + periodically increases its assumed PMTU. This will almost always + result in packets being discarded and Packet Too Big messages being + generated, because in most cases the PMTU of the path will not have + changed. Therefore, attempts to detect increases in a path's PMTU + should be done infrequently. + + Path MTU Discovery supports multicast as well as unicast + destinations. In the case of a multicast destination, copies of a + packet may traverse many different paths to many different nodes. + Each path may have a different PMTU, and a single multicast packet + may result in multiple Packet Too Big messages, each reporting a + different next-hop MTU. The minimum PMTU value across the set of + paths in use determines the size of subsequent packets sent to the + multicast destination. + + Note that Path MTU Discovery must be performed even in cases where a + node "thinks" a destination is attached to the same link as itself. + In a situation such as when a neighboring router acts as proxy [ND] + for some destination, the destination can to appear to be directly + connected but is in fact more than one hop away. + +4. Protocol Requirements + + As discussed in section 1, IPv6 nodes are not required to implement + Path MTU Discovery. The requirements in this section apply only to + those implementations that include Path MTU Discovery. + + When a node receives a Packet Too Big message, it MUST reduce its + estimate of the PMTU for the relevant path, based on the value of the + MTU field in the message. The precise behavior of a node in this + circumstance is not specified, since different applications may have + different requirements, and since different implementation + architectures may favor different strategies. + + After receiving a Packet Too Big message, a node MUST attempt to + avoid eliciting more such messages in the near future. The node MUST + reduce the size of the packets it is sending along the path. Using a + PMTU estimate larger than the IPv6 minimum link MTU may continue to + elicit Packet Too Big messages. Since each of these messages (and + the dropped packets they respond to) consume network resources, the + node MUST force the Path MTU Discovery process to end. + + Nodes using Path MTU Discovery MUST detect decreases in PMTU as fast + as possible. Nodes MAY detect increases in PMTU, but because doing + so requires sending packets larger than the current estimated PMTU, + + + +McCann, Deering & Mogul Standards Track [Page 4] + +RFC 1981 Path MTU Discovery for IPv6 August 1996 + + + and because the likelihood is that the PMTU will not have increased, + this MUST be done at infrequent intervals. An attempt to detect an + increase (by sending a packet larger than the current estimate) MUST + NOT be done less than 5 minutes after a Packet Too Big message has + been received for the given path. The recommended setting for this + timer is twice its minimum value (10 minutes). + + A node MUST NOT reduce its estimate of the Path MTU below the IPv6 + minimum link MTU. + + Note: A node may receive a Packet Too Big message reporting a + next-hop MTU that is less than the IPv6 minimum link MTU. In that + case, the node is not required to reduce the size of subsequent + packets sent on the path to less than the IPv6 minimun link MTU, + but rather must include a Fragment header in those packets [IPv6- + SPEC]. + + A node MUST NOT increase its estimate of the Path MTU in response to + the contents of a Packet Too Big message. A message purporting to + announce an increase in the Path MTU might be a stale packet that has + been floating around in the network, a false packet injected as part + of a denial-of-service attack, or the result of having multiple paths + to the destination, each with a different PMTU. + +5. Implementation Issues + + This section discusses a number of issues related to the + implementation of Path MTU Discovery. This is not a specification, + but rather a set of notes provided as an aid for implementors. + + The issues include: + + - What layer or layers implement Path MTU Discovery? + + - How is the PMTU information cached? + + - How is stale PMTU information removed? + + - What must transport and higher layers do? + +5.1. Layering + + In the IP architecture, the choice of what size packet to send is + made by a protocol at a layer above IP. This memo refers to such a + protocol as a "packetization protocol". Packetization protocols are + usually transport protocols (for example, TCP) but can also be + higher-layer protocols (for example, protocols built on top of UDP). + + + + +McCann, Deering & Mogul Standards Track [Page 5] + +RFC 1981 Path MTU Discovery for IPv6 August 1996 + + + Implementing Path MTU Discovery in the packetization layers + simplifies some of the inter-layer issues, but has several drawbacks: + the implementation may have to be redone for each packetization + protocol, it becomes hard to share PMTU information between different + packetization layers, and the connection-oriented state maintained by + some packetization layers may not easily extend to save PMTU + information for long periods. + + It is therefore suggested that the IP layer store PMTU information + and that the ICMP layer process received Packet Too Big messages. + The packetization layers may respond to changes in the PMTU, by + changing the size of the messages they send. To support this + layering, packetization layers require a way to learn of changes in + the value of MMS_S, the "maximum send transport-message size". The + MMS_S is derived from the Path MTU by subtracting the size of the + IPv6 header plus space reserved by the IP layer for additional + headers (if any). + + It is possible that a packetization layer, perhaps a UDP application + outside the kernel, is unable to change the size of messages it + sends. This may result in a packet size that exceeds the Path MTU. + To accommodate such situations, IPv6 defines a mechanism that allows + large payloads to be divided into fragments, with each fragment sent + in a separate packet (see [IPv6-SPEC] section "Fragment Header"). + However, packetization layers are encouraged to avoid sending + messages that will require fragmentation (for the case against + fragmentation, see [FRAG]). + +5.2. Storing PMTU information + + Ideally, a PMTU value should be associated with a specific path + traversed by packets exchanged between the source and destination + nodes. However, in most cases a node will not have enough + information to completely and accurately identify such a path. + Rather, a node must associate a PMTU value with some local + representation of a path. It is left to the implementation to select + the local representation of a path. + + In the case of a multicast destination address, copies of a packet + may traverse many different paths to reach many different nodes. The + local representation of the "path" to a multicast destination must in + fact represent a potentially large set of paths. + + Minimally, an implementation could maintain a single PMTU value to be + used for all packets originated from the node. This PMTU value would + be the minimum PMTU learned across the set of all paths in use by the + node. This approach is likely to result in the use of smaller + packets than is necessary for many paths. + + + +McCann, Deering & Mogul Standards Track [Page 6] + +RFC 1981 Path MTU Discovery for IPv6 August 1996 + + + An implementation could use the destination address as the local + representation of a path. The PMTU value associated with a + destination would be the minimum PMTU learned across the set of all + paths in use to that destination. The set of paths in use to a + particular destination is expected to be small, in many cases + consisting of a single path. This approach will result in the use of + optimally sized packets on a per-destination basis. This approach + integrates nicely with the conceptual model of a host as described in + [ND]: a PMTU value could be stored with the corresponding entry in + the destination cache. + + If flows [IPv6-SPEC] are in use, an implementation could use the flow + id as the local representation of a path. Packets sent to a + particular destination but belonging to different flows may use + different paths, with the choice of path depending on the flow id. + This approach will result in the use of optimally sized packets on a + per-flow basis, providing finer granularity than PMTU values + maintained on a per-destination basis. + + For source routed packets (i.e. packets containing an IPv6 Routing + header [IPv6-SPEC]), the source route may further qualify the local + representation of a path. In particular, a packet containing a type + 0 Routing header in which all bits in the Strict/Loose Bit Map are + equal to 1 contains a complete path specification. An implementation + could use source route information in the local representation of a + path. + + Note: Some paths may be further distinguished by different + security classifications. The details of such classifications are + beyond the scope of this memo. + + Initially, the PMTU value for a path is assumed to be the (known) MTU + of the first-hop link. + + When a Packet Too Big message is received, the node determines which + path the message applies to based on the contents of the Packet Too + Big message. For example, if the destination address is used as the + local representation of a path, the destination address from the + original packet would be used to determine which path the message + applies to. + + Note: if the original packet contained a Routing header, the + Routing header should be used to determine the location of the + destination address within the original packet. If Segments Left + is equal to zero, the destination address is in the Destination + Address field in the IPv6 header. If Segments Left is greater + than zero, the destination address is the last address + (Address[n]) in the Routing header. + + + +McCann, Deering & Mogul Standards Track [Page 7] + +RFC 1981 Path MTU Discovery for IPv6 August 1996 + + + The node then uses the value in the MTU field in the Packet Too Big + message as a tentative PMTU value, and compares the tentative PMTU to + the existing PMTU. If the tentative PMTU is less than the existing + PMTU estimate, the tentative PMTU replaces the existing PMTU as the + PMTU value for the path. + + The packetization layers must be notified about decreases in the + PMTU. Any packetization layer instance (for example, a TCP + connection) that is actively using the path must be notified if the + PMTU estimate is decreased. + + Note: even if the Packet Too Big message contains an Original + Packet Header that refers to a UDP packet, the TCP layer must be + notified if any of its connections use the given path. + + Also, the instance that sent the packet that elicited the Packet Too + Big message should be notified that its packet has been dropped, even + if the PMTU estimate has not changed, so that it may retransmit the + dropped data. + + Note: An implementation can avoid the use of an asynchronous + notification mechanism for PMTU decreases by postponing + notification until the next attempt to send a packet larger than + the PMTU estimate. In this approach, when an attempt is made to + SEND a packet that is larger than the PMTU estimate, the SEND + function should fail and return a suitable error indication. This + approach may be more suitable to a connectionless packetization + layer (such as one using UDP), which (in some implementations) may + be hard to "notify" from the ICMP layer. In this case, the normal + timeout-based retransmission mechanisms would be used to recover + from the dropped packets. + + It is important to understand that the notification of the + packetization layer instances using the path about the change in the + PMTU is distinct from the notification of a specific instance that a + packet has been dropped. The latter should be done as soon as + practical (i.e., asynchronously from the point of view of the + packetization layer instance), while the former may be delayed until + a packetization layer instance wants to create a packet. + Retransmission should be done for only for those packets that are + known to be dropped, as indicated by a Packet Too Big message. + +5.3. Purging stale PMTU information + + Internetwork topology is dynamic; routes change over time. While the + local representation of a path may remain constant, the actual + path(s) in use may change. Thus, PMTU information cached by a node + can become stale. + + + +McCann, Deering & Mogul Standards Track [Page 8] + +RFC 1981 Path MTU Discovery for IPv6 August 1996 + + + If the stale PMTU value is too large, this will be discovered almost + immediately once a large enough packet is sent on the path. No such + mechanism exists for realizing that a stale PMTU value is too small, + so an implementation should "age" cached values. When a PMTU value + has not been decreased for a while (on the order of 10 minutes), the + PMTU estimate should be set to the MTU of the first-hop link, and the + packetization layers should be notified of the change. This will + cause the complete Path MTU Discovery process to take place again. + + Note: an implementation should provide a means for changing the + timeout duration, including setting it to "infinity". For + example, nodes attached to an FDDI link which is then attached to + the rest of the Internet via a small MTU serial line are never + going to discover a new non-local PMTU, so they should not have to + put up with dropped packets every 10 minutes. + + An upper layer must not retransmit data in response to an increase in + the PMTU estimate, since this increase never comes in response to an + indication of a dropped packet. + + One approach to implementing PMTU aging is to associate a timestamp + field with a PMTU value. This field is initialized to a "reserved" + value, indicating that the PMTU is equal to the MTU of the first hop + link. Whenever the PMTU is decreased in response to a Packet Too Big + message, the timestamp is set to the current time. + + Once a minute, a timer-driven procedure runs through all cached PMTU + values, and for each PMTU whose timestamp is not "reserved" and is + older than the timeout interval: + + - The PMTU estimate is set to the MTU of the first hop link. + + - The timestamp is set to the "reserved" value. + + - Packetization layers using this path are notified of the increase. + +5.4. TCP layer actions + + The TCP layer must track the PMTU for the path(s) in use by a + connection; it should not send segments that would result in packets + larger than the PMTU. A simple implementation could ask the IP layer + for this value each time it created a new segment, but this could be + inefficient. Moreover, TCP implementations that follow the "slow- + start" congestion-avoidance algorithm [CONG] typically calculate and + cache several other values derived from the PMTU. It may be simpler + to receive asynchronous notification when the PMTU changes, so that + these variables may be updated. + + + + +McCann, Deering & Mogul Standards Track [Page 9] + +RFC 1981 Path MTU Discovery for IPv6 August 1996 + + + A TCP implementation must also store the MSS value received from its + peer, and must not send any segment larger than this MSS, regardless + of the PMTU. In 4.xBSD-derived implementations, this may require + adding an additional field to the TCP state record. + + The value sent in the TCP MSS option is independent of the PMTU. + This MSS option value is used by the other end of the connection, + which may be using an unrelated PMTU value. See [IPv6-SPEC] sections + "Packet Size Issues" and "Maximum Upper-Layer Payload Size" for + information on selecting a value for the TCP MSS option. + + When a Packet Too Big message is received, it implies that a packet + was dropped by the node that sent the ICMP message. It is sufficient + to treat this as any other dropped segment, and wait until the + retransmission timer expires to cause retransmission of the segment. + If the Path MTU Discovery process requires several steps to find the + PMTU of the full path, this could delay the connection by many + round-trip times. + + Alternatively, the retransmission could be done in immediate response + to a notification that the Path MTU has changed, but only for the + specific connection specified by the Packet Too Big message. The + packet size used in the retransmission should be no larger than the + new PMTU. + + Note: A packetization layer must not retransmit in response to + every Packet Too Big message, since a burst of several oversized + segments will give rise to several such messages and hence several + retransmissions of the same data. If the new estimated PMTU is + still wrong, the process repeats, and there is an exponential + growth in the number of superfluous segments sent. + + This means that the TCP layer must be able to recognize when a + Packet Too Big notification actually decreases the PMTU that it + has already used to send a packet on the given connection, and + should ignore any other notifications. + + Many TCP implementations incorporate "congestion avoidance" and + "slow-start" algorithms to improve performance [CONG]. Unlike a + retransmission caused by a TCP retransmission timeout, a + retransmission caused by a Packet Too Big message should not change + the congestion window. It should, however, trigger the slow-start + mechanism (i.e., only one segment should be retransmitted until + acknowledgements begin to arrive again). + + TCP performance can be reduced if the sender's maximum window size is + not an exact multiple of the segment size in use (this is not the + congestion window size, which is always a multiple of the segment + + + +McCann, Deering & Mogul Standards Track [Page 10] + +RFC 1981 Path MTU Discovery for IPv6 August 1996 + + + size). In many systems (such as those derived from 4.2BSD), the + segment size is often set to 1024 octets, and the maximum window size + (the "send space") is usually a multiple of 1024 octets, so the + proper relationship holds by default. If Path MTU Discovery is used, + however, the segment size may not be a submultiple of the send space, + and it may change during a connection; this means that the TCP layer + may need to change the transmission window size when Path MTU + Discovery changes the PMTU value. The maximum window size should be + set to the greatest multiple of the segment size that is less than or + equal to the sender's buffer space size. + +5.5. Issues for other transport protocols + + Some transport protocols (such as ISO TP4 [ISOTP]) are not allowed to + repacketize when doing a retransmission. That is, once an attempt is + made to transmit a segment of a certain size, the transport cannot + split the contents of the segment into smaller segments for + retransmission. In such a case, the original segment can be + fragmented by the IP layer during retransmission. Subsequent + segments, when transmitted for the first time, should be no larger + than allowed by the Path MTU. + + The Sun Network File System (NFS) uses a Remote Procedure Call (RPC) + protocol [RPC] that, when used over UDP, in many cases will generate + payloads that must be fragmented even for the first-hop link. This + might improve performance in certain cases, but it is known to cause + reliability and performance problems, especially when the client and + server are separated by routers. + + It is recommended that NFS implementations use Path MTU Discovery + whenever routers are involved. Most NFS implementations allow the + RPC datagram size to be changed at mount-time (indirectly, by + changing the effective file system block size), but might require + some modification to support changes later on. + + Also, since a single NFS operation cannot be split across several UDP + datagrams, certain operations (primarily, those operating on file + names and directories) require a minimum payload size that if sent in + a single packet would exceed the PMTU. NFS implementations should + not reduce the payload size below this threshold, even if Path MTU + Discovery suggests a lower value. In this case the payload will be + fragmented by the IP layer. + + + + + + + + + +McCann, Deering & Mogul Standards Track [Page 11] + +RFC 1981 Path MTU Discovery for IPv6 August 1996 + + +5.6. Management interface + + It is suggested that an implementation provide a way for a system + utility program to: + + - Specify that Path MTU Discovery not be done on a given path. + + - Change the PMTU value associated with a given path. + + The former can be accomplished by associating a flag with the path; + when a packet is sent on a path with this flag set, the IP layer does + not send packets larger than the IPv6 minimum link MTU. + + These features might be used to work around an anomalous situation, + or by a routing protocol implementation that is able to obtain Path + MTU values. + + The implementation should also provide a way to change the timeout + period for aging stale PMTU information. + +6. Security Considerations + + This Path MTU Discovery mechanism makes possible two denial-of- + service attacks, both based on a malicious party sending false Packet + Too Big messages to a node. + + In the first attack, the false message indicates a PMTU much smaller + than reality. This should not entirely stop data flow, since the + victim node should never set its PMTU estimate below the IPv6 minimum + link MTU. It will, however, result in suboptimal performance. + + In the second attack, the false message indicates a PMTU larger than + reality. If believed, this could cause temporary blockage as the + victim sends packets that will be dropped by some router. Within one + round-trip time, the node would discover its mistake (receiving + Packet Too Big messages from that router), but frequent repetition of + this attack could cause lots of packets to be dropped. A node, + however, should never raise its estimate of the PMTU based on a + Packet Too Big message, so should not be vulnerable to this attack. + + A malicious party could also cause problems if it could stop a victim + from receiving legitimate Packet Too Big messages, but in this case + there are simpler denial-of-service attacks available. + + + + + + + + +McCann, Deering & Mogul Standards Track [Page 12] + +RFC 1981 Path MTU Discovery for IPv6 August 1996 + + +Acknowledgements + + We would like to acknowledge the authors of and contributors to + [RFC-1191], from which the majority of this document was derived. We + would also like to acknowledge the members of the IPng working group + for their careful review and constructive criticisms. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +McCann, Deering & Mogul Standards Track [Page 13] + +RFC 1981 Path MTU Discovery for IPv6 August 1996 + + +Appendix A - Comparison to RFC 1191 + + This document is based in large part on RFC 1191, which describes + Path MTU Discovery for IPv4. Certain portions of RFC 1191 were not + needed in this document: + + router specification - Packet Too Big messages and corresponding + router behavior are defined in [ICMPv6] + + Don't Fragment bit - there is no DF bit in IPv6 packets + + TCP MSS discussion - selecting a value to send in the TCP MSS + option is discussed in [IPv6-SPEC] + + old-style messages - all Packet Too Big messages report the + MTU of the constricting link + + MTU plateau tables - not needed because there are no old-style + messages + +References + + [CONG] Van Jacobson. Congestion Avoidance and Control. Proc. + SIGCOMM '88 Symposium on Communications Architectures and + Protocols, pages 314-329. Stanford, CA, August, 1988. + + [FRAG] C. Kent and J. Mogul. Fragmentation Considered Harmful. + In Proc. SIGCOMM '87 Workshop on Frontiers in Computer + Communications Technology. August, 1987. + + [ICMPv6] Conta, A., and S. Deering, "Internet Control Message + Protocol (ICMPv6) for the Internet Protocol Version 6 + (IPv6) Specification", RFC 1885, December 1995. + + [IPv6-SPEC] Deering, S., and R. Hinden, "Internet Protocol, Version + 6 (IPv6) Specification", RFC 1883, December 1995. + + [ISOTP] ISO. ISO Transport Protocol Specification: ISO DP 8073. + RFC 905, SRI Network Information Center, April, 1984. + + [ND] Narten, T., Nordmark, E., and W. Simpson, "Neighbor + Discovery for IP Version 6 (IPv6)", Work in Progress. + + [RFC-1191] Mogul, J., and S. Deering, "Path MTU Discovery", + RFC 1191, November 1990. + + + + + + +McCann, Deering & Mogul Standards Track [Page 14] + +RFC 1981 Path MTU Discovery for IPv6 August 1996 + + + [RPC] Sun Microsystems, Inc., "RPC: Remote Procedure Call + Protocol", RFC 1057, SRI Network Information Center, + June, 1988. + +Authors' Addresses + + Jack McCann + Digital Equipment Corporation + 110 Spitbrook Road, ZKO3-3/U14 + Nashua, NH 03062 + Phone: +1 603 881 2608 + + Fax: +1 603 881 0120 + Email: mccann@zk3.dec.com + + + Stephen E. Deering + Xerox Palo Alto Research Center + 3333 Coyote Hill Road + Palo Alto, CA 94304 + Phone: +1 415 812 4839 + + Fax: +1 415 812 4471 + EMail: deering@parc.xerox.com + + + Jeffrey Mogul + Digital Equipment Corporation Western Research Laboratory + 250 University Avenue + Palo Alto, CA 94301 + Phone: +1 415 617 3304 + + EMail: mogul@pa.dec.com + + + + + + + + + + + + + + + + + + +McCann, Deering & Mogul Standards Track [Page 15] diff --git a/dist/docs/rfc4443-icmpv6.txt b/dist/docs/rfc4443-icmpv6.txt new file mode 100644 index 0000000..2d8e7cd --- /dev/null +++ b/dist/docs/rfc4443-icmpv6.txt @@ -0,0 +1,1346 @@ + + + + + + +Network Working Group A. Conta +Request for Comments: 4443 Transwitch +Obsoletes: 2463 S. Deering +Updates: 2780 Cisco Systems +Category: Standards Track M. Gupta, Ed. + Tropos Networks + March 2006 + + + Internet Control Message Protocol (ICMPv6) + for the Internet Protocol Version 6 (IPv6) Specification + +Status of This Memo + + This document specifies an Internet standards track protocol for the + Internet community, and requests discussion and suggestions for + improvements. Please refer to the current edition of the "Internet + Official Protocol Standards" (STD 1) for the standardization state + and status of this protocol. Distribution of this memo is unlimited. + +Copyright Notice + + Copyright (C) The Internet Society (2006). + +Abstract + + This document describes the format of a set of control messages used + in ICMPv6 (Internet Control Message Protocol). ICMPv6 is the + Internet Control Message Protocol for Internet Protocol version 6 + (IPv6). + + + + + + + + + + + + + + + + + + + + + +Conta, et al. Standards Track [Page 1] + +RFC 4443 ICMPv6 (ICMP for IPv6) March 2006 + + +Table of Contents + + + 1. Introduction ....................................................2 + 2. ICMPv6 (ICMP for IPv6) ..........................................3 + 2.1. Message General Format .....................................3 + 2.2. Message Source Address Determination .......................5 + 2.3. Message Checksum Calculation ...............................5 + 2.4. Message Processing Rules ...................................5 + 3. ICMPv6 Error Messages ...........................................8 + 3.1. Destination Unreachable Message ............................8 + 3.2. Packet Too Big Message ....................................10 + 3.3. Time Exceeded Message .....................................11 + 3.4. Parameter Problem Message .................................12 + 4. ICMPv6 Informational Messages ..................................13 + 4.1. Echo Request Message ......................................13 + 4.2. Echo Reply Message ........................................14 + 5. Security Considerations ........................................15 + 5.1. Authentication and Confidentiality of ICMP Messages .......15 + 5.2. ICMP Attacks ..............................................16 + 6. IANA Considerations ............................................17 + 6.1. Procedure for New ICMPV6 Type and Code Value Assignments ..17 + 6.2. Assignments for This Document .............................18 + 7. References .....................................................19 + 7.1. Normative References ......................................19 + 7.2. Informative References ....................................19 + 8. Acknowledgements ...............................................20 + Appendix A - Changes since RFC 2463................................21 + +1. Introduction + + The Internet Protocol version 6 (IPv6) uses the Internet Control + Message Protocol (ICMP) as defined for IPv4 [RFC-792], with a number + of changes. The resulting protocol is called ICMPv6 and has an IPv6 + Next Header value of 58. + + This document describes the format of a set of control messages used + in ICMPv6. It does not describe the procedures for using these + messages to achieve functions like Path MTU discovery; these + procedures are described in other documents (e.g., [PMTU]). Other + documents may also introduce additional ICMPv6 message types, such as + Neighbor Discovery messages [IPv6-DISC], subject to the general rules + for ICMPv6 messages given in Section 2 of this document. + + Terminology defined in the IPv6 specification [IPv6] and the IPv6 + Routing and Addressing specification [IPv6-ADDR] applies to this + document as well. + + + + +Conta, et al. Standards Track [Page 2] + +RFC 4443 ICMPv6 (ICMP for IPv6) March 2006 + + + This document obsoletes RFC 2463 [RFC-2463] and updates RFC 2780 + [RFC-2780]. + + The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this + document are to be interpreted as described in [RFC-2119]. + +2. ICMPv6 (ICMP for IPv6) + + ICMPv6 is used by IPv6 nodes to report errors encountered in + processing packets, and to perform other internet-layer functions, + such as diagnostics (ICMPv6 "ping"). ICMPv6 is an integral part of + IPv6, and the base protocol (all the messages and behavior required + by this specification) MUST be fully implemented by every IPv6 node. + +2.1. Message General Format + + Every ICMPv6 message is preceded by an IPv6 header and zero or more + IPv6 extension headers. The ICMPv6 header is identified by a Next + Header value of 58 in the immediately preceding header. (This is + different from the value used to identify ICMP for IPv4.) + + The ICMPv6 messages have the following general 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Code | Checksum | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + Message Body + + | | + + The type field indicates the type of the message. Its value + determines the format of the remaining data. + + The code field depends on the message type. It is used to create an + additional level of message granularity. + + The checksum field is used to detect data corruption in the ICMPv6 + message and parts of the IPv6 header. + + ICMPv6 messages are grouped into two classes: error messages and + informational messages. Error messages are identified as such by a + zero in the high-order bit of their message Type field values. Thus, + error messages have message types from 0 to 127; informational + messages have message types from 128 to 255. + + + + +Conta, et al. Standards Track [Page 3] + +RFC 4443 ICMPv6 (ICMP for IPv6) March 2006 + + + This document defines the message formats for the following ICMPv6 + messages: + + ICMPv6 error messages: + + 1 Destination Unreachable (see Section 3.1) + 2 Packet Too Big (see Section 3.2) + 3 Time Exceeded (see Section 3.3) + 4 Parameter Problem (see Section 3.4) + + 100 Private experimentation + 101 Private experimentation + + 127 Reserved for expansion of ICMPv6 error messages + + ICMPv6 informational messages: + + 128 Echo Request (see Section 4.1) + 129 Echo Reply (see Section 4.2) + + 200 Private experimentation + 201 Private experimentation + + 255 Reserved for expansion of ICMPv6 informational messages + + Type values 100, 101, 200, and 201 are reserved for private + experimentation. They are not intended for general use. It is + expected that multiple concurrent experiments will be done with the + same type values. Any wide-scale and/or uncontrolled usage should + obtain real allocations as defined in Section 6. + + Type values 127 and 255 are reserved for future expansion of the type + value range if there is a shortage in the future. The details of + this are left for future work. One possible way of doing this that + would not cause any problems with current implementations is that if + the type equals 127 or 255, the code field should be used for the new + assignment. Existing implementations would ignore the new + assignments as specified in Section 2.4, (b). The new messages using + these expanded type values could assign fields in the message body + for its code values. + + Sections 3 and 4 describe the message formats for the ICMPv6 error + message types 1 through 4 and informational message types 128 and + 129. + + + + + + + +Conta, et al. Standards Track [Page 4] + +RFC 4443 ICMPv6 (ICMP for IPv6) March 2006 + + + Inclusion of, at least, the start of the invoking packet is intended + to allow the originator of a packet that has resulted in an ICMPv6 + error message to identify the upper-layer protocol and process that + sent the packet. + +2.2. Message Source Address Determination + + A node that originates an ICMPv6 message has to determine both the + Source and Destination IPv6 Addresses in the IPv6 header before + calculating the checksum. If the node has more than one unicast + address, it MUST choose the Source Address of the message as follows: + + (a) If the message is a response to a message sent to one of the + node's unicast addresses, the Source Address of the reply MUST be + that same address. + + (b) If the message is a response to a message sent to any other + address, such as + + - a multicast group address, + - an anycast address implemented by the node, or + - a unicast address that does not belong to the node + + the Source Address of the ICMPv6 packet MUST be a unicast address + belonging to the node. The address SHOULD be chosen according to + the rules that would be used to select the source address for any + other packet originated by the node, given the destination address + of the packet. However, it MAY be selected in an alternative way + if this would lead to a more informative choice of address + reachable from the destination of the ICMPv6 packet. + +2.3. Message Checksum Calculation + + The checksum is the 16-bit one's complement of the one's complement + sum of the entire ICMPv6 message, starting with the ICMPv6 message + type field, and prepended with a "pseudo-header" of IPv6 header + fields, as specified in [IPv6, Section 8.1]. The Next Header value + used in the pseudo-header is 58. (The inclusion of a pseudo-header + in the ICMPv6 checksum is a change from IPv4; see [IPv6] for the + rationale for this change.) + + For computing the checksum, the checksum field is first set to zero. + +2.4. Message Processing Rules + + Implementations MUST observe the following rules when processing + ICMPv6 messages (from [RFC-1122]): + + + + +Conta, et al. Standards Track [Page 5] + +RFC 4443 ICMPv6 (ICMP for IPv6) March 2006 + + + (a) If an ICMPv6 error message of unknown type is received at its + destination, it MUST be passed to the upper-layer process that + originated the packet that caused the error, where this can be + identified (see Section 2.4, (d)). + + (b) If an ICMPv6 informational message of unknown type is received, + it MUST be silently discarded. + + (c) Every ICMPv6 error message (type < 128) MUST include as much of + the IPv6 offending (invoking) packet (the packet that caused the + error) as possible without making the error message packet exceed + the minimum IPv6 MTU [IPv6]. + + (d) In cases where the internet-layer protocol is required to pass an + ICMPv6 error message to the upper-layer process, the upper-layer + protocol type is extracted from the original packet (contained in + the body of the ICMPv6 error message) and used to select the + appropriate upper-layer process to handle the error. + + In cases where it is not possible to retrieve the upper-layer + protocol type from the ICMPv6 message, the ICMPv6 message is + silently dropped after any IPv6-layer processing. One example of + such a case is an ICMPv6 message with an unusually large amount + of extension headers that does not have the upper-layer protocol + type due to truncation of the original packet to meet the minimum + IPv6 MTU [IPv6] limit. Another example is an ICMPv6 message with + an ESP extension header for which it is not possible to decrypt + the original packet due to either truncation or the + unavailability of the state necessary to decrypt the packet. + + (e) An ICMPv6 error message MUST NOT be originated as a result of + receiving the following: + + (e.1) An ICMPv6 error message. + + (e.2) An ICMPv6 redirect message [IPv6-DISC]. + + (e.3) A packet destined to an IPv6 multicast address. (There are + two exceptions to this rule: (1) the Packet Too Big Message + (Section 3.2) to allow Path MTU discovery to work for IPv6 + multicast, and (2) the Parameter Problem Message, Code 2 + (Section 3.4) reporting an unrecognized IPv6 option (see + Section 4.2 of [IPv6]) that has the Option Type highest- + order two bits set to 10). + + (e.4) A packet sent as a link-layer multicast (the exceptions + from e.3 apply to this case, too). + + + + +Conta, et al. Standards Track [Page 6] + +RFC 4443 ICMPv6 (ICMP for IPv6) March 2006 + + + (e.5) A packet sent as a link-layer broadcast (the exceptions + from e.3 apply to this case, too). + + (e.6) A packet whose source address does not uniquely identify a + single node -- e.g., the IPv6 Unspecified Address, an IPv6 + multicast address, or an address known by the ICMP message + originator to be an IPv6 anycast address. + + (f) Finally, in order to limit the bandwidth and forwarding costs + incurred by originating ICMPv6 error messages, an IPv6 node MUST + limit the rate of ICMPv6 error messages it originates. This + situation may occur when a source sending a stream of erroneous + packets fails to heed the resulting ICMPv6 error messages. + + Rate-limiting of forwarded ICMP messages is out of scope of this + specification. + + A recommended method for implementing the rate-limiting function + is a token bucket, limiting the average rate of transmission to + N, where N can be either packets/second or a fraction of the + attached link's bandwidth, but allowing up to B error messages to + be transmitted in a burst, as long as the long-term average is + not exceeded. + + Rate-limiting mechanisms that cannot cope with bursty traffic + (e.g., traceroute) are not recommended; for example, a simple + timer-based implementation, allowing an error message every T + milliseconds (even with low values for T), is not reasonable. + + The rate-limiting parameters SHOULD be configurable. In the case + of a token-bucket implementation, the best defaults depend on + where the implementation is expected to be deployed (e.g., a + high-end router vs. an embedded host). For example, in a + small/mid-size device, the possible defaults could be B=10, + N=10/s. + + NOTE: THE RESTRICTIONS UNDER (e) AND (f) ABOVE TAKE PRECEDENCE OVER + ANY REQUIREMENT ELSEWHERE IN THIS DOCUMENT FOR ORIGINATING ICMP ERROR + MESSAGES. + + The following sections describe the message formats for the above + ICMPv6 messages. + + + + + + + + + +Conta, et al. Standards Track [Page 7] + +RFC 4443 ICMPv6 (ICMP for IPv6) March 2006 + + +3. ICMPv6 Error Messages + +3.1. Destination Unreachable Message + + 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Code | Checksum | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Unused | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | As much of invoking packet | + + as possible without the ICMPv6 packet + + | exceeding the minimum IPv6 MTU [IPv6] | + + IPv6 Fields: + + Destination Address + + Copied from the Source Address field of the invoking + packet. + + ICMPv6 Fields: + + Type 1 + + Code 0 - No route to destination + 1 - Communication with destination + administratively prohibited + 2 - Beyond scope of source address + 3 - Address unreachable + 4 - Port unreachable + 5 - Source address failed ingress/egress policy + 6 - Reject route to destination + + Unused This field is unused for all code values. + It must be initialized to zero by the originator + and ignored by the receiver. + Description + + A Destination Unreachable message SHOULD be generated by a router, or + by the IPv6 layer in the originating node, in response to a packet + that cannot be delivered to its destination address for reasons other + than congestion. (An ICMPv6 message MUST NOT be generated if a + packet is dropped due to congestion.) + + If the reason for the failure to deliver is lack of a matching entry + in the forwarding node's routing table, the Code field is set to 0. + + + +Conta, et al. Standards Track [Page 8] + +RFC 4443 ICMPv6 (ICMP for IPv6) March 2006 + + + (This error can occur only in nodes that do not hold a "default + route" in their routing tables.) + + If the reason for the failure to deliver is administrative + prohibition (e.g., a "firewall filter"), the Code field is set to 1. + + If the reason for the failure to deliver is that the destination is + beyond the scope of the source address, the Code field is set to 2. + This condition can occur only when the scope of the source address is + smaller than the scope of the destination address (e.g., when a + packet has a link-local source address and a global-scope destination + address) and the packet cannot be delivered to the destination + without leaving the scope of the source address. + + If the reason for the failure to deliver cannot be mapped to any of + other codes, the Code field is set to 3. Example of such cases are + an inability to resolve the IPv6 destination address into a + corresponding link address, or a link-specific problem of some sort. + + One specific case in which a Destination Unreachable message is sent + with a code 3 is in response to a packet received by a router from a + point-to-point link, destined to an address within a subnet assigned + to that same link (other than one of the receiving router's own + addresses). In such a case, the packet MUST NOT be forwarded back + onto the arrival link. + + A destination node SHOULD originate a Destination Unreachable message + with Code 4 in response to a packet for which the transport protocol + (e.g., UDP) has no listener, if that transport protocol has no + alternative means to inform the sender. + + If the reason for the failure to deliver is that the packet with this + source address is not allowed due to ingress or egress filtering + policies, the Code field is set to 5. + + If the reason for the failure to deliver is that the route to the + destination is a reject route, the Code field is set to 6. This may + occur if the router has been configured to reject all the traffic for + a specific prefix. + + Codes 5 and 6 are more informative subsets of code 1. + + For security reasons, it is recommended that implementations SHOULD + allow sending of ICMP destination unreachable messages to be + disabled, preferably on a per-interface basis. + + + + + + +Conta, et al. Standards Track [Page 9] + +RFC 4443 ICMPv6 (ICMP for IPv6) March 2006 + + + Upper Layer Notification + + A node receiving the ICMPv6 Destination Unreachable message MUST + notify the upper-layer process if the relevant process can be + identified (see Section 2.4, (d)). + +3.2. Packet Too Big Message + + 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Code | Checksum | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | MTU | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | As much of invoking packet | + + as possible without the ICMPv6 packet + + | exceeding the minimum IPv6 MTU [IPv6] | + + IPv6 Fields: + + Destination Address + + Copied from the Source Address field of the invoking + packet. + + ICMPv6 Fields: + + Type 2 + + Code Set to 0 (zero) by the originator and ignored by the + receiver. + + MTU The Maximum Transmission Unit of the next-hop link. + + Description + + A Packet Too Big MUST be sent by a router in response to a packet + that it cannot forward because the packet is larger than the MTU of + the outgoing link. The information in this message is used as part + of the Path MTU Discovery process [PMTU]. + + Originating a Packet Too Big Message makes an exception to one of the + rules as to when to originate an ICMPv6 error message. Unlike other + messages, it is sent in response to a packet received with an IPv6 + multicast destination address, or with a link-layer multicast or + link-layer broadcast address. + + + + +Conta, et al. Standards Track [Page 10] + +RFC 4443 ICMPv6 (ICMP for IPv6) March 2006 + + + Upper Layer Notification + + An incoming Packet Too Big message MUST be passed to the upper-layer + process if the relevant process can be identified (see Section 2.4, + (d)). + +3.3. Time Exceeded Message + + 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Code | Checksum | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Unused | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | As much of invoking packet | + + as possible without the ICMPv6 packet + + | exceeding the minimum IPv6 MTU [IPv6] | + + IPv6 Fields: + + Destination Address + Copied from the Source Address field of the invoking + packet. + + ICMPv6 Fields: + + Type 3 + + Code 0 - Hop limit exceeded in transit + 1 - Fragment reassembly time exceeded + + Unused This field is unused for all code values. + It must be initialized to zero by the originator + and ignored by the receiver. + + Description + + If a router receives a packet with a Hop Limit of zero, or if a + router decrements a packet's Hop Limit to zero, it MUST discard the + packet and originate an ICMPv6 Time Exceeded message with Code 0 to + the source of the packet. This indicates either a routing loop or + too small an initial Hop Limit value. + + An ICMPv6 Time Exceeded message with Code 1 is used to report + fragment reassembly timeout, as specified in [IPv6, Section 4.5]. + + + + + +Conta, et al. Standards Track [Page 11] + +RFC 4443 ICMPv6 (ICMP for IPv6) March 2006 + + + Upper Layer Notification + + An incoming Time Exceeded message MUST be passed to the upper-layer + process if the relevant process can be identified (see Section 2.4, + (d)). + +3.4. Parameter Problem Message + + 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Code | Checksum | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Pointer | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | As much of invoking packet | + + as possible without the ICMPv6 packet + + | exceeding the minimum IPv6 MTU [IPv6] | + + IPv6 Fields: + + Destination Address + + Copied from the Source Address field of the invoking + packet. + + ICMPv6 Fields: + + Type 4 + + Code 0 - Erroneous header field encountered + 1 - Unrecognized Next Header type encountered + 2 - Unrecognized IPv6 option encountered + + Pointer Identifies the octet offset within the + invoking packet where the error was detected. + + The pointer will point beyond the end of the ICMPv6 + packet if the field in error is beyond what can fit + in the maximum size of an ICMPv6 error message. + + Description + + If an IPv6 node processing a packet finds a problem with a field in + the IPv6 header or extension headers such that it cannot complete + processing the packet, it MUST discard the packet and SHOULD + originate an ICMPv6 Parameter Problem message to the packet's source, + indicating the type and location of the problem. + + + +Conta, et al. Standards Track [Page 12] + +RFC 4443 ICMPv6 (ICMP for IPv6) March 2006 + + + Codes 1 and 2 are more informative subsets of Code 0. + + The pointer identifies the octet of the original packet's header + where the error was detected. For example, an ICMPv6 message with a + Type field of 4, Code field of 1, and Pointer field of 40 would + indicate that the IPv6 extension header following the IPv6 header of + the original packet holds an unrecognized Next Header field value. + + Upper Layer Notification + + A node receiving this ICMPv6 message MUST notify the upper-layer + process if the relevant process can be identified (see Section 2.4, + (d)). + +4. ICMPv6 Informational Messages + +4.1. Echo Request Message + + 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Code | Checksum | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Identifier | Sequence Number | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Data ... + +-+-+-+-+- + + IPv6 Fields: + + Destination Address + + Any legal IPv6 address. + + ICMPv6 Fields: + + Type 128 + + Code 0 + + Identifier An identifier to aid in matching Echo Replies + to this Echo Request. May be zero. + + + + + + + + + +Conta, et al. Standards Track [Page 13] + +RFC 4443 ICMPv6 (ICMP for IPv6) March 2006 + + + Sequence Number + + A sequence number to aid in matching Echo Replies + to this Echo Request. May be zero. + + Data Zero or more octets of arbitrary data. + + Description + + Every node MUST implement an ICMPv6 Echo responder function that + receives Echo Requests and originates corresponding Echo Replies. A + node SHOULD also implement an application-layer interface for + originating Echo Requests and receiving Echo Replies, for diagnostic + purposes. + + Upper Layer Notification + + Echo Request messages MAY be passed to processes receiving ICMP + messages. + +4.2. Echo Reply Message + + 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Code | Checksum | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Identifier | Sequence Number | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Data ... + +-+-+-+-+- + + IPv6 Fields: + + Destination Address + + Copied from the Source Address field of the invoking + Echo Request packet. + + ICMPv6 Fields: + + Type 129 + + Code 0 + + Identifier The identifier from the invoking Echo Request message. + + + + + +Conta, et al. Standards Track [Page 14] + +RFC 4443 ICMPv6 (ICMP for IPv6) March 2006 + + + Sequence Number + + The sequence number from the invoking Echo Request + message. + + Data The data from the invoking Echo Request message. + + Description + + Every node MUST implement an ICMPv6 Echo responder function that + receives Echo Requests and originates corresponding Echo Replies. A + node SHOULD also implement an application-layer interface for + originating Echo Requests and receiving Echo Replies, for diagnostic + purposes. + + The source address of an Echo Reply sent in response to a unicast + Echo Request message MUST be the same as the destination address of + that Echo Request message. + + An Echo Reply SHOULD be sent in response to an Echo Request message + sent to an IPv6 multicast or anycast address. In this case, the + source address of the reply MUST be a unicast address belonging to + the interface on which the Echo Request message was received. + + The data received in the ICMPv6 Echo Request message MUST be returned + entirely and unmodified in the ICMPv6 Echo Reply message. + + Upper Layer Notification + + Echo Reply messages MUST be passed to the process that originated an + Echo Request message. An Echo Reply message MAY be passed to + processes that did not originate the Echo Request message. + + Note that there is no limitation on the amount of data that can be + put in Echo Request and Echo Reply Messages. + +5. Security Considerations + +5.1. Authentication and Confidentiality of ICMP Messages + + ICMP protocol packet exchanges can be authenticated using the IP + Authentication Header [IPv6-AUTH] or IP Encapsulating Security + Payload Header [IPv6-ESP]. Confidentiality for the ICMP protocol + packet exchanges can be achieved using the IP Encapsulating Security + Payload Header [IPv6-ESP]. + + [SEC-ARCH] describes the IPsec handling of ICMP traffic in detail. + + + + +Conta, et al. Standards Track [Page 15] + +RFC 4443 ICMPv6 (ICMP for IPv6) March 2006 + + +5.2. ICMP Attacks + + ICMP messages may be subject to various attacks. A complete + discussion can be found in the IP Security Architecture [IPv6-SA]. A + brief discussion of these attacks and their prevention follows: + + 1. ICMP messages may be subject to actions intended to cause the + receiver to believe the message came from a different source from + that of the message originator. The protection against this + attack can be achieved by applying the IPv6 Authentication + mechanism [IPv6-AUTH] to the ICMP message. + + 2. ICMP messages may be subject to actions intended to cause the + message or the reply to it to go to a destination different from + that of the message originator's intention. The protection + against this attack can be achieved by using the Authentication + Header [IPv6-AUTH] or the Encapsulating Security Payload Header + [IPv6-ESP]. The Authentication Header provides the protection + against change for the source and the destination address of the + IP packet. The Encapsulating Security Payload Header does not + provide this protection, but the ICMP checksum calculation + includes the source and the destination addresses, and the + Encapsulating Security Payload Header protects the checksum. + Therefore, the combination of ICMP checksum and the Encapsulating + Security Payload Header provides protection against this attack. + The protection provided by the Encapsulating Security Payload + Header will not be as strong as the protection provided by the + Authentication Header. + + 3. ICMP messages may be subject to changes in the message fields, or + payload. The authentication [IPv6-AUTH] or encryption [IPv6-ESP] + of the ICMP message protects against such actions. + + 4. ICMP messages may be used to attempt denial-of-service attacks by + sending back to back erroneous IP packets. An implementation that + correctly followed Section 2.4, paragraph (f), of this + specification, would be protected by the ICMP error rate limiting + mechanism. + + 5. The exception number 2 of rule e.3 in Section 2.4 gives a + malicious node the opportunity to cause a denial-of-service attack + to a multicast source. A malicious node can send a multicast + packet with an unknown destination option marked as mandatory, + with the IPv6 source address of a valid multicast source. A large + number of destination nodes will send an ICMP Parameter Problem + Message to the multicast source, causing a denial-of-service + attack. The way multicast traffic is forwarded by the multicast + routers requires that the malicious node be part of the correct + + + +Conta, et al. Standards Track [Page 16] + +RFC 4443 ICMPv6 (ICMP for IPv6) March 2006 + + + multicast path, i.e., near to the multicast source. This attack + can only be avoided by securing the multicast traffic. The + multicast source should be careful while sending multicast traffic + with the destination options marked as mandatory, because they can + cause a denial-of-service attack to themselves if the destination + option is unknown to a large number of destinations. + + 6. As the ICMP messages are passed to the upper-layer processes, it + is possible to perform attacks on the upper layer protocols (e.g., + TCP) with ICMP [TCP-attack]. It is recommended that the upper + layers perform some form of validation of ICMP messages (using the + information contained in the payload of the ICMP message) before + acting upon them. The actual validation checks are specific to + the upper layers and are out of the scope of this specification. + Protecting the upper layer with IPsec mitigates these attacks. + + ICMP error messages signal network error conditions that were + encountered while processing an internet datagram. Depending on + the particular scenario, the error conditions being reported might + or might not get solved in the near term. Therefore, reaction to + ICMP error messages may depend not only on the error type and code + but also on other factors, such as the time at which the error + messages are received, previous knowledge of the network error + conditions being reported, and knowledge of the network scenario + in which the receiving host is operating. + +6. IANA Considerations + +6.1. Procedure for New ICMPV6 Type and Code Value Assignments + + The IPv6 ICMP header defined in this document contains the following + fields that carry values assigned from IANA-managed name spaces: Type + and Code. Code field values are defined relative to a specific Type + value. + + Values for the IPv6 ICMP Type fields are allocated using the + following procedure: + + 1. The IANA should allocate and permanently register new ICMPv6 type + codes from IETF RFC publication. This is for all RFC types, + including standards track, informational, and experimental status, + that originate from the IETF and have been approved by the IESG + for publication. + + 2. IETF working groups with working group consensus and area director + approval can request reclaimable ICMPV6 type code assignments from + the IANA. The IANA will tag the values as "reclaimable in + future". + + + +Conta, et al. Standards Track [Page 17] + +RFC 4443 ICMPv6 (ICMP for IPv6) March 2006 + + + The "reclaimable in the future" tag will be removed when an RFC is + published that documents the protocol as defined in 1. This will + make the assignment permanent and update the reference on the IANA + web pages. + + At the point where the ICMPv6 type values are 85% assigned, the + IETF will review the assignments tagged "reclaimable in the + future" and inform the IANA which ones should be reclaimed and + reassigned. + + 3. Requests for new ICMPv6 type value assignments from outside the + IETF are only made through the publication of an IETF document, + per 1 above. Note also that documents published as "RFC Editor + contributions" [RFC-3978] are not considered IETF documents. + + The assignment of new Code values for the Type values defined in this + document require standards action or IESG approval. The policy for + assigning Code values for new IPv6 ICMP Types not defined in this + document should be defined in the document defining the new Type + values. + +6.2. Assignments for This Document + + The following has updated assignments located at: + + http://www.iana.org/assignments/icmpv6-parameters + + The IANA has reassigned ICMPv6 type 1 "Destination Unreachable" code + 2, which was unassigned in [RFC-2463], to: + + 2 - Beyond scope of source address + + The IANA has assigned the following two new codes values for ICMPv6 + type 1 "Destination Unreachable": + + 5 - Source address failed ingress/egress policy + 6 - Reject route to destination + + The IANA has assigned the following new type values: + + 100 Private experimentation + 101 Private experimentation + + 127 Reserved for expansion of ICMPv6 error messages + + 200 Private experimentation + 201 Private experimentation + + + + +Conta, et al. Standards Track [Page 18] + +RFC 4443 ICMPv6 (ICMP for IPv6) March 2006 + + + 255 Reserved for expansion of ICMPv6 informational messages + +7. References + +7.1. Normative References + + [IPv6] Deering, S. and R. Hinden, "Internet Protocol, Version 6 + (IPv6) Specification", RFC 2460, December 1998. + + [IPv6-DISC] Narten, T., Nordmark, E., and W. Simpson, "Neighbor + Discovery for IP Version 6 (IPv6)", RFC 2461, December + 1998. + + [RFC-792] Postel, J., "Internet Control Message Protocol", STD 5, + RFC 792, September 1981. + + [RFC-2463] Conta, A. and S. Deering, "Internet Control Message + Protocol (ICMPv6) for the Internet Protocol Version 6 + (IPv6) Specification", RFC 2463, December 1998. + + [RFC-1122] Braden, R., "Requirements for Internet Hosts - + Communication Layers", STD 3, RFC 1122, October 1989. + + [RFC-2119] Bradner, S., "Key words for use in RFCs to Indicate + Requirement Levels", BCP 14, RFC 2119, March 1997. + + [RFC-3978] Bradner, S., "IETF Rights in Contributions", BCP 78, RFC + 3978, March 2005. + +7.2. Informative References + + [RFC-2780] Bradner, S. and V. Paxson, "IANA Allocation Guidelines + For Values In the Internet Protocol and Related + Headers", BCP 37, RFC 2780, March 2000. + + [IPv6-ADDR] Hinden, R. and S. Deering, "Intpernet Protocol Version 6 + (IPv6) Addressing Architecture", RFC 3513, April 2003. + + [PMTU] McCann, J., Deering, S., and J. Mogul, "Path MTU + Discovery for IP version 6", RFC 1981, August 1996. + + [IPv6-SA] Kent, S. and R. Atkinson, "Security Architecture for the + Internet Protocol", RFC 2401, November 1998. + + [IPv6-AUTH] Kent, S., "IP Authentication Header", RFC 4302, December + 2005. + + + + + +Conta, et al. Standards Track [Page 19] + +RFC 4443 ICMPv6 (ICMP for IPv6) March 2006 + + + [IPv6-ESP] Kent, S., "IP Encapsulating Security Payload (ESP)", RFC + 4203, December 2005. + + [SEC-ARCH] Kent, S. and K. Seo, "Security Architecture for the + Internet Protocol", RFC 4301, December 2005. + + [TCP-attack] Gont, F., "ICMP attacks against TCP", Work in Progress. + +8. Acknowledgements + + The document is derived from previous ICMP documents of the SIPP and + IPng working group. + + The IPng working group, and particularly Robert Elz, Jim Bound, Bill + Simpson, Thomas Narten, Charlie Lynn, Bill Fink, Scott Bradner, + Dimitri Haskin, Bob Hinden, Jun-ichiro Itojun Hagino, Tatuya Jinmei, + Brian Zill, Pekka Savola, Fred Templin, and Elwyn Davies (in + chronological order) provided extensive review information and + feedback. + + Bob Hinden was the document editor for this document. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Conta, et al. Standards Track [Page 20] + +RFC 4443 ICMPv6 (ICMP for IPv6) March 2006 + + +Appendix A - Changes since RFC 2463 + + The following changes were made from RFC 2463: + + - Edited the Abstract to make it a little more elaborate. + + - Corrected typos in Section 2.4, where references to sub-bullet e.2 + were supposed to be references to e.3. + + - Removed the Timer-based and the Bandwidth-based methods from the + example rate-limiting mechanism for ICMP error messages. Added + Token-bucket based method. + + - Added specification that all ICMP error messages shall have exactly + 32 bits of type-specific data, so that receivers can reliably find + the embedded invoking packet even when they don't recognize the + ICMP message Type. + + - In the description of Destination Unreachable messages, Code 3, + added rule prohibiting forwarding of packets back onto point-to- + point links from which they were received, if their destination + addresses belong to the link itself ("anti-ping-ponging" rule). + + - Added description of Time Exceeded Code 1 (fragment reassembly + timeout). + + - Added "beyond scope of source address", "source address failed + ingress/egress policy", and "reject route to destination" messages + to the family of "unreachable destination" type ICMP error messages + (Section 3.1). + + - Reserved some ICMP type values for experimentation. + + - Added a NOTE in Section 2.4 that specifies ICMP message processing + rules precedence. + + - Added ICMP REDIRECT to the list in Section 2.4, (e) of cases in + which ICMP error messages are not to be generated. + + - Made minor editorial changes in Section 2.3 on checksum + calculation, and in Section 5.2. + + - Clarified in Section 4.2, regarding the Echo Reply Message; the + source address of an Echo Reply to an anycast Echo Request should + be a unicast address, as in the case of multicast. + + + + + + +Conta, et al. Standards Track [Page 21] + +RFC 4443 ICMPv6 (ICMP for IPv6) March 2006 + + + - Revised the Security Considerations section. Added the use of the + Encapsulating Security Payload Header for authentication. Changed + the requirement of an option of "not allowing unauthenticated ICMP + messages" to MAY from SHOULD. + + - Added a new attack in the list of possible ICMP attacks in Section + 5.2. + + - Separated References into Normative and Informative. + + - Added reference to RFC 2780 "IANA Allocation Guidelines For Values + In the Internet Protocol and Related Headers". Also added a note + that this document updates RFC 2780. + + - Added a procedure for new ICMPv6 Type and Code value assignments in + the IANA Considerations section. + + - Replaced word "send" with "originate" to make it clear that ICMP + packets being forwarded are out of scope of this specification. + + - Changed the ESP and AH references to the updated ESP and AH + documents. + + - Added reference to the updated IPsec Security Architecture + document. + + - Added a SHOULD requirement for allowing the sending of ICMP + destination unreachable messages to be disabled. + + - Simplified the source address selection of the ICMPv6 packet. + + - Reorganized the General Message Format (Section 2.1). + + - Removed the general packet format from Section 2.1. It refers to + Sections 3 and 4 for packet formats now. + + - Added text about attacks to the transport protocols that could + potentially be caused by ICMP. + + + + + + + + + + + + + +Conta, et al. Standards Track [Page 22] + +RFC 4443 ICMPv6 (ICMP for IPv6) March 2006 + + +Authors' Addresses + + Alex Conta + Transwitch Corporation + 3 Enterprise Drive + Shelton, CT 06484 + USA + + EMail: aconta@txc.com + + + Stephen Deering + Cisco Systems, Inc. + 170 West Tasman Drive + San Jose, CA 95134-1706 + USA + + + Mukesh Gupta, Ed. + Tropos Networks + 555 Del Rey Avenue + Sunnyvale, CA 94085 + + Phone: +1 408-331-6889 + EMail: mukesh.gupta@tropos.com + + + + + + + + + + + + + + + + + + + + + + + + + + +Conta, et al. Standards Track [Page 23] + +RFC 4443 ICMPv6 (ICMP for IPv6) March 2006 + + +Full Copyright Statement + + Copyright (C) The Internet Society (2006). + + This document is subject to the rights, licenses and restrictions + contained in BCP 78, and except as set forth therein, the authors + retain all their rights. + + This document and the information contained herein are provided on an + "AS IS" basis and THE CONTRIBUTOR, THE ORGANIZATION HE/SHE REPRESENTS + OR IS SPONSORED BY (IF ANY), THE INTERNET SOCIETY AND THE INTERNET + ENGINEERING TASK FORCE DISCLAIM ALL WARRANTIES, EXPRESS OR IMPLIED, + INCLUDING BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF THE + INFORMATION HEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY IMPLIED + WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. + +Intellectual Property + + The IETF takes no position regarding the validity or scope of any + Intellectual Property Rights or other rights that might be claimed to + pertain to the implementation or use of the technology described in + this document or the extent to which any license under such rights + might or might not be available; nor does it represent that it has + made any independent effort to identify any such rights. Information + on the procedures with respect to rights in RFC documents can be + found in BCP 78 and BCP 79. + + Copies of IPR disclosures made to the IETF Secretariat and any + assurances of licenses to be made available, or the result of an + attempt made to obtain a general license or permission for the use of + such proprietary rights by implementers or users of this + specification can be obtained from the IETF on-line IPR repository at + http://www.ietf.org/ipr. + + The IETF invites any interested party to bring to its attention any + copyrights, patents or patent applications, or other proprietary + rights that may cover technology that may be required to implement + this standard. Please address the information to the IETF at + ietf-ipr@ietf.org. + +Acknowledgement + + Funding for the RFC Editor function is provided by the IETF + Administrative Support Activity (IASA). + + + + + + + +Conta, et al. Standards Track [Page 24] diff --git a/dist/docs/rfc4861-ndp.txt b/dist/docs/rfc4861-ndp.txt new file mode 100644 index 0000000..634d218 --- /dev/null +++ b/dist/docs/rfc4861-ndp.txt @@ -0,0 +1,5434 @@ + + + + + + +Network Working Group T. Narten +Request for Comments: 4861 IBM +Obsoletes: 2461 E. Nordmark +Category: Standards Track Sun Microsystems + W. Simpson + Daydreamer + H. Soliman + Elevate Technologies + September 2007 + + + Neighbor Discovery for IP version 6 (IPv6) + +Status of This Memo + + This document specifies an Internet standards track protocol for the + Internet community, and requests discussion and suggestions for + improvements. Please refer to the current edition of the "Internet + Official Protocol Standards" (STD 1) for the standardization state + and status of this protocol. Distribution of this memo is unlimited. + +Abstract + + This document specifies the Neighbor Discovery protocol for IP + Version 6. IPv6 nodes on the same link use Neighbor Discovery to + discover each other's presence, to determine each other's link-layer + addresses, to find routers, and to maintain reachability information + about the paths to active neighbors. + + + + + + + + + + + + + + + + + + + + + + + +Narten, et al. Standards Track [Page 1] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + +Table of Contents + + 1. Introduction ....................................................4 + 2. Terminology .....................................................4 + 2.1. General ....................................................4 + 2.2. Link Types .................................................8 + 2.3. Addresses ..................................................9 + 2.4. Requirements ..............................................10 + 3. Protocol Overview ..............................................10 + 3.1. Comparison with IPv4 ......................................14 + 3.2. Supported Link Types ......................................16 + 3.3. Securing Neighbor Discovery Messages ......................18 + 4. Message Formats ................................................18 + 4.1. Router Solicitation Message Format ........................18 + 4.2. Router Advertisement Message Format .......................19 + 4.3. Neighbor Solicitation Message Format ......................22 + 4.4. Neighbor Advertisement Message Format .....................23 + 4.5. Redirect Message Format ...................................26 + 4.6. Option Formats ............................................28 + 4.6.1. Source/Target Link-layer Address ...................28 + 4.6.2. Prefix Information .................................29 + 4.6.3. Redirected Header ..................................31 + 4.6.4. MTU ................................................32 + 5. Conceptual Model of a Host .....................................33 + 5.1. Conceptual Data Structures ................................33 + 5.2. Conceptual Sending Algorithm ..............................36 + 5.3. Garbage Collection and Timeout Requirements ...............37 + 6. Router and Prefix Discovery ....................................38 + 6.1. Message Validation ........................................39 + 6.1.1. Validation of Router Solicitation Messages .........39 + 6.1.2. Validation of Router Advertisement Messages ........39 + 6.2. Router Specification ......................................40 + 6.2.1. Router Configuration Variables .....................40 + 6.2.2. Becoming an Advertising Interface ..................45 + 6.2.3. Router Advertisement Message Content ...............45 + 6.2.4. Sending Unsolicited Router Advertisements ..........47 + 6.2.5. Ceasing To Be an Advertising Interface .............47 + 6.2.6. Processing Router Solicitations ....................48 + 6.2.7. Router Advertisement Consistency ...................50 + 6.2.8. Link-local Address Change ..........................50 + 6.3. Host Specification ........................................51 + 6.3.1. Host Configuration Variables .......................51 + 6.3.2. Host Variables .....................................51 + 6.3.3. Interface Initialization ...........................52 + 6.3.4. Processing Received Router Advertisements ..........53 + 6.3.5. Timing out Prefixes and Default Routers ............56 + 6.3.6. Default Router Selection ...........................56 + 6.3.7. Sending Router Solicitations .......................57 + + + +Narten, et al. Standards Track [Page 2] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + 7. Address Resolution and Neighbor Unreachability Detection .......59 + 7.1. Message Validation ........................................59 + 7.1.1. Validation of Neighbor Solicitations ...............59 + 7.1.2. Validation of Neighbor Advertisements ..............60 + 7.2. Address Resolution ........................................60 + 7.2.1. Interface Initialization ...........................61 + 7.2.2. Sending Neighbor Solicitations .....................61 + 7.2.3. Receipt of Neighbor Solicitations ..................62 + 7.2.4. Sending Solicited Neighbor Advertisements ..........63 + 7.2.5. Receipt of Neighbor Advertisements .................64 + 7.2.6. Sending Unsolicited Neighbor Advertisements ........66 + 7.2.7. Anycast Neighbor Advertisements ....................67 + 7.2.8. Proxy Neighbor Advertisements ......................68 + 7.3. Neighbor Unreachability Detection .........................68 + 7.3.1. Reachability Confirmation ..........................69 + 7.3.2. Neighbor Cache Entry States ........................70 + 7.3.3. Node Behavior ......................................71 + 8. Redirect Function ..............................................73 + 8.1. Validation of Redirect Messages ...........................74 + 8.2. Router Specification ......................................75 + 8.3. Host Specification ........................................76 + 9. Extensibility - Option Processing ..............................76 + 10. Protocol Constants ............................................78 + 11. Security Considerations .......................................79 + 11.1. Threat Analysis ..........................................79 + 11.2. Securing Neighbor Discovery Messages .....................81 + 12. Renumbering Considerations ....................................81 + 13. IANA Considerations ...........................................83 + 14. References ....................................................84 + 14.1. Normative References .....................................84 + 14.2. Informative References ...................................84 + Appendix A: Multihomed Hosts ......................................87 + Appendix B: Future Extensions .....................................88 + Appendix C: State Machine for the Reachability State ..............89 + Appendix D: Summary of IsRouter Rules .............................91 + Appendix E: Implementation Issues .................................92 + Appendix F: Changes from RFC 2461 .................................94 + Acknowledgments ...................................................95 + + + + + + + + + + + + + +Narten, et al. Standards Track [Page 3] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + +1. Introduction + + This specification defines the Neighbor Discovery (ND) protocol for + Internet Protocol Version 6 (IPv6). Nodes (hosts and routers) use + Neighbor Discovery to determine the link-layer addresses for + neighbors known to reside on attached links and to quickly purge + cached values that become invalid. Hosts also use Neighbor Discovery + to find neighboring routers that are willing to forward packets on + their behalf. Finally, nodes use the protocol to actively keep track + of which neighbors are reachable and which are not, and to detect + changed link-layer addresses. When a router or the path to a router + fails, a host actively searches for functioning alternates. + + Unless specified otherwise (in a document that covers operating IP + over a particular link type) this document applies to all link types. + However, because ND uses link-layer multicast for some of its + services, it is possible that on some link types (e.g., Non-Broadcast + Multi-Access (NBMA) links), alternative protocols or mechanisms to + implement those services will be specified (in the appropriate + document covering the operation of IP over a particular link type). + The services described in this document that are not directly + dependent on multicast, such as Redirects, Next-hop determination, + Neighbor Unreachability Detection, etc., are expected to be provided + as specified in this document. The details of how one uses ND on + NBMA links are addressed in [IPv6-NBMA]. In addition, [IPv6-3GPP] + and[IPv6-CELL] discuss the use of this protocol over some cellular + links, which are examples of NBMA links. + +2. Terminology + +2.1. General + + IP - Internet Protocol Version 6. The terms IPv4 and IPv6 + are used only in contexts where necessary to avoid + ambiguity. + + ICMP - Internet Control Message Protocol for the Internet + Protocol Version 6. The terms ICMPv4 and ICMPv6 are + used only in contexts where necessary to avoid + ambiguity. + + node - a device that implements IP. + + router - a node that forwards IP packets not explicitly + addressed to itself. + + host - any node that is not a router. + + + + +Narten, et al. Standards Track [Page 4] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + upper layer - a protocol layer immediately above IP. 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) IP such as + Internetwork Packet Exchange (IPX), AppleTalk, or IP + itself. + + link - a communication facility or medium over which nodes can + communicate at the link layer, i.e., the layer + immediately below IP. Examples are Ethernets (simple + or bridged), PPP links, X.25, Frame Relay, or ATM + networks as well as Internet-layer (or higher-layer) + "tunnels", such as tunnels over IPv4 or IPv6 itself. + + interface - a node's attachment to a link. + + neighbors - nodes attached to the same link. + + address - an IP-layer identifier for an interface or a set of + interfaces. + + anycast address + - an identifier for a set of interfaces (typically + belonging to different nodes). A packet sent to an + anycast address is delivered to one of the interfaces + identified by that address (the "nearest" one, + according to the routing protocol's measure of + distance). See [ADDR-ARCH]. + + Note that an anycast address is syntactically + indistinguishable from a unicast address. Thus, nodes + sending packets to anycast addresses don't generally + know that an anycast address is being used. Throughout + the rest of this document, references to unicast + addresses also apply to anycast addresses in those + cases where the node is unaware that a unicast address + is actually an anycast address. + + prefix - a bit string that consists of some number of initial + bits of an address. + + link-layer address + - a link-layer identifier for an interface. Examples + include IEEE 802 addresses for Ethernet links. + + + + + + +Narten, et al. Standards Track [Page 5] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + on-link - an address that is assigned to an interface on a + specified link. A node considers an address to be on- + link if: + + - it is covered by one of the link's prefixes (e.g., + as indicated by the on-link flag in the Prefix + Information option), or + + - a neighboring router specifies the address as the + target of a Redirect message, or + + - a Neighbor Advertisement message is received for + the (target) address, or + + - any Neighbor Discovery message is received from + the address. + + off-link - the opposite of "on-link"; an address that is not + assigned to any interfaces on the specified link. + + longest prefix match + - the process of determining which prefix (if any) in a + set of prefixes covers a target address. A target + address is covered by a prefix if all of the bits in + the prefix match the left-most bits of the target + address. When multiple prefixes cover an address, the + longest prefix is the one that matches. + + reachability + - whether or not the one-way "forward" path to a neighbor + is functioning properly. In particular, whether + packets sent to a neighbor are reaching the IP layer on + the neighboring machine and are being processed + properly by the receiving IP layer. For neighboring + routers, reachability means that packets sent by a + node's IP layer are delivered to the router's IP layer, + and the router is indeed forwarding packets (i.e., it + is configured as a router, not a host). For hosts, + reachability means that packets sent by a node's IP + layer are delivered to the neighbor host's IP layer. + + packet - an IP header plus payload. + + link MTU - the maximum transmission unit, i.e., maximum packet + size in octets, that can be conveyed in one + transmission unit over a link. + + + + + +Narten, et al. Standards Track [Page 6] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + target - an address about which address resolution information + is sought, or an address that is the new first hop when + being redirected. + + proxy - a node that responds to Neighbor Discovery query + messages on behalf of another node. A router acting on + behalf of a mobile node that has moved off-link could + potentially act as a proxy for the mobile node. + + ICMP destination unreachable indication + - an error indication returned to the original sender of + a packet that cannot be delivered for the reasons + outlined in [ICMPv6]. If the error occurs on a node + other than the node originating the packet, an ICMP + error message is generated. If the error occurs on the + originating node, an implementation is not required to + actually create and send an ICMP error packet to the + source, as long as the upper-layer sender is notified + through an appropriate mechanism (e.g., return value + from a procedure call). Note, however, that an + implementation may find it convenient in some cases to + return errors to the sender by taking the offending + packet, generating an ICMP error message, and then + delivering it (locally) through the generic error- + handling routines. + + random delay + - when sending out messages, it is sometimes necessary to + delay a transmission for a random amount of time in + order to prevent multiple nodes from transmitting at + exactly the same time, or to prevent long-range + periodic transmissions from synchronizing with each + other [SYNC]. When a random component is required, a + node calculates the actual delay in such a way that the + computed delay forms a uniformly distributed random + value that falls between the specified minimum and + maximum delay times. The implementor must take care to + ensure that the granularity of the calculated random + component and the resolution of the timer used are both + high enough to ensure that the probability of multiple + nodes delaying the same amount of time is small. + + random delay seed + - if a pseudo-random number generator is used in + calculating a random delay component, the generator + should be initialized with a unique seed prior to being + used. Note that it is not sufficient to use the + interface identifier alone as the seed, since interface + + + +Narten, et al. Standards Track [Page 7] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + identifiers will not always be unique. To reduce the + probability that duplicate interface identifiers cause + the same seed to be used, the seed should be calculated + from a variety of input sources (e.g., machine + components) that are likely to be different even on + identical "boxes". For example, the seed could be + formed by combining the CPU's serial number with an + interface identifier. Additional information on + randomness and random number generation can be found in + [RAND]. + +2.2. Link Types + + Different link layers have different properties. The ones of concern + to Neighbor Discovery are: + + multicast capable + - a link that supports a native mechanism at the link + layer for sending packets to all (i.e., broadcast) + or a subset of all neighbors. + + point-to-point - a link that connects exactly two interfaces. A + point-to-point link is assumed to have multicast + capability and a link-local address. + + non-broadcast multi-access (NBMA) + - a link to which more than two interfaces can attach, + but that does not support a native form of multicast + or broadcast (e.g., X.25, ATM, frame relay, etc.). + Note that all link types (including NBMA) are + expected to provide multicast service for + applications that need it (e.g., using multicast + servers). However, it is an issue for further study + whether ND should use such facilities or an + alternate mechanism that provides the equivalent + multicast capability for ND. + + shared media - a link that allows direct communication among a + number of nodes, but attached nodes are configured + in such a way that they do not have complete prefix + information for all on-link destinations. That is, + at the IP level, nodes on the same link may not know + that they are neighbors; by default, they + communicate through a router. Examples are large + (switched) public data networks such as Switched + Multimegabit Data Service (SMDS) and Broadband + Integrated Services Digital Network (B-ISDN). Also + known as "large clouds". See [SH-MEDIA]. + + + +Narten, et al. Standards Track [Page 8] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + variable MTU - a link that does not have a well-defined MTU (e.g., + IEEE 802.5 token rings). Many links (e.g., + Ethernet) have a standard MTU defined by the link- + layer protocol or by the specific document + describing how to run IP over the link layer. + + asymmetric reachability + - a link where non-reflexive and/or non-transitive + reachability is part of normal operation. (Non- + reflexive reachability means packets from A reach B, + but packets from B don't reach A. Non-transitive + reachability means packets from A reach B, and + packets from B reach C, but packets from A don't + reach C.) Many radio links exhibit these + properties. + +2.3. Addresses + + Neighbor Discovery makes use of a number of different addresses + defined in [ADDR-ARCH], including: + + all-nodes multicast address + - the link-local scope address to reach all nodes, + FF02::1. + + all-routers multicast address + - the link-local scope address to reach all routers, + FF02::2. + + solicited-node multicast address + - a link-local scope multicast address that is computed + as a function of the solicited target's address. The + function is described in [ADDR-ARCH]. The function is + chosen so that IP addresses that differ only in the + most significant bits, e.g., due to multiple prefixes + associated with different providers, will map to the + same solicited-node address thereby reducing the number + of multicast addresses a node must join at the link + layer. + + link-local address + - a unicast address having link-only scope that can be + used to reach neighbors. All interfaces on routers + MUST have a link-local address. Also, [ADDRCONF] + requires that interfaces on hosts have a link-local + address. + + + + + +Narten, et al. Standards Track [Page 9] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + unspecified address + - a reserved address value that indicates the lack of an + address (e.g., the address is unknown). It is never + used as a destination address, but may be used as a + source address if the sender does not (yet) know its + own address (e.g., while verifying an address is unused + during stateless address autoconfiguration [ADDRCONF]). + The unspecified address has a value of 0:0:0:0:0:0:0:0. + + Note that this specification does not strictly comply with the + consistency requirements in [ADDR-SEL] for the scopes of source and + destination addresses. It is possible in some cases for hosts to use + a source address of a larger scope than the destination address in + the IPv6 header. + +2.4. Requirements + + The keywords MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, + SHOULD NOT, RECOMMENDED, MAY, and OPTIONAL, when they appear in this + document, are to be interpreted as described in [KEYWORDS]. + + This document also makes use of internal conceptual variables to + describe protocol behavior and external variables that an + implementation must allow system administrators to change. The + specific variable names, how their values change, and how their + settings influence protocol behavior are provided to demonstrate + protocol behavior. An implementation is not required to have them in + the exact form described here, so long as its external behavior is + consistent with that described in this document. + +3. Protocol Overview + + This protocol solves a set of problems related to the interaction + between nodes attached to the same link. It defines mechanisms for + solving each of the following problems: + + Router Discovery: How hosts locate routers that reside on an + attached link. + + Prefix Discovery: How hosts discover the set of address prefixes + that define which destinations are on-link for an + attached link. (Nodes use prefixes to distinguish + destinations that reside on-link from those only + reachable through a router.) + + Parameter Discovery: How a node learns link parameters (such as the + link MTU) or Internet parameters (such as the hop limit + value) to place in outgoing packets. + + + +Narten, et al. Standards Track [Page 10] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + Address Autoconfiguration: Introduces the mechanisms needed in + order to allow nodes to configure an address for an + interface in a stateless manner. Stateless address + autoconfiguration is specified in [ADDRCONF]. + + Address resolution: How nodes determine the link-layer address of + an on-link destination (e.g., a neighbor) given only the + destination's IP address. + + Next-hop determination: The algorithm for mapping an IP destination + address into the IP address of the neighbor to which + traffic for the destination should be sent. The next- + hop can be a router or the destination itself. + + Neighbor Unreachability Detection: How nodes determine that a + neighbor is no longer reachable. For neighbors used as + routers, alternate default routers can be tried. For + both routers and hosts, address resolution can be + performed again. + + Duplicate Address Detection: How a node determines whether or not + an address it wishes to use is already in use by another + node. + + Redirect: How a router informs a host of a better first-hop node + to reach a particular destination. + + Neighbor Discovery defines five different ICMP packet types: A pair + of Router Solicitation and Router Advertisement messages, a pair of + Neighbor Solicitation and Neighbor Advertisements messages, and a + Redirect message. The messages serve the following purpose: + + Router Solicitation: When an interface becomes enabled, hosts may + send out Router Solicitations that request routers to + generate Router Advertisements immediately rather than + at their next scheduled time. + + Router Advertisement: Routers advertise their presence together + with various link and Internet parameters either + periodically, or in response to a Router Solicitation + message. Router Advertisements contain prefixes that + are used for determining whether another address shares + the same link (on-link determination) and/or address + configuration, a suggested hop limit value, etc. + + + + + + + +Narten, et al. Standards Track [Page 11] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + Neighbor Solicitation: Sent by a node to determine the link-layer + address of a neighbor, or to verify that a neighbor is + still reachable via a cached link-layer address. + Neighbor Solicitations are also used for Duplicate + Address Detection. + + Neighbor Advertisement: A response to a Neighbor Solicitation + message. A node may also send unsolicited Neighbor + Advertisements to announce a link-layer address change. + + Redirect: Used by routers to inform hosts of a better first hop + for a destination. + + On multicast-capable links, each router periodically multicasts a + Router Advertisement packet announcing its availability. A host + receives Router Advertisements from all routers, building a list of + default routers. Routers generate Router Advertisements frequently + enough that hosts will learn of their presence within a few minutes, + but not frequently enough to rely on an absence of advertisements to + detect router failure; a separate Neighbor Unreachability Detection + algorithm provides failure detection. + + Router Advertisements contain a list of prefixes used for on-link + determination and/or autonomous address configuration; flags + associated with the prefixes specify the intended uses of a + particular prefix. Hosts use the advertised on-link prefixes to + build and maintain a list that is used in deciding when a packet's + destination is on-link or beyond a router. Note that a destination + can be on-link even though it is not covered by any advertised on- + link prefix. In such cases, a router can send a Redirect informing + the sender that the destination is a neighbor. + + Router Advertisements (and per-prefix flags) allow routers to inform + hosts how to perform Address Autoconfiguration. For example, routers + can specify whether hosts should use DHCPv6 and/or autonomous + (stateless) address configuration. + + Router Advertisement messages also contain Internet parameters such + as the hop limit that hosts should use in outgoing packets and, + optionally, link parameters such as the link MTU. This facilitates + centralized administration of critical parameters that can be set on + routers and automatically propagated to all attached hosts. + + Nodes accomplish address resolution by multicasting a Neighbor + Solicitation that asks the target node to return its link-layer + address. Neighbor Solicitation messages are multicast to the + solicited-node multicast address of the target address. The target + returns its link-layer address in a unicast Neighbor Advertisement + + + +Narten, et al. Standards Track [Page 12] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + message. A single request-response pair of packets is sufficient for + both the initiator and the target to resolve each other's link-layer + addresses; the initiator includes its link-layer address in the + Neighbor Solicitation. + + Neighbor Solicitation messages can also be used to determine if more + than one node has been assigned the same unicast address. The use of + Neighbor Solicitation messages for Duplicate Address Detection is + specified in [ADDRCONF]. + + Neighbor Unreachability Detection detects the failure of a neighbor + or the failure of the forward path to the neighbor. Doing so + requires positive confirmation that packets sent to a neighbor are + actually reaching that neighbor and being processed properly by its + IP layer. Neighbor Unreachability Detection uses confirmation from + two sources. When possible, upper-layer protocols provide a positive + confirmation that a connection is making "forward progress", that is, + previously sent data is known to have been delivered correctly (e.g., + new acknowledgments were received recently). When positive + confirmation is not forthcoming through such "hints", a node sends + unicast Neighbor Solicitation messages that solicit Neighbor + Advertisements as reachability confirmation from the next hop. To + reduce unnecessary network traffic, probe messages are only sent to + neighbors to which the node is actively sending packets. + + In addition to addressing the above general problems, Neighbor + Discovery also handles the following situations: + + Link-layer address change - A node that knows its link-layer + address has changed can multicast a few (unsolicited) + Neighbor Advertisement packets to all nodes to quickly update + cached link-layer addresses that have become invalid. Note + that the sending of unsolicited advertisements is a + performance enhancement only (e.g., unreliable). The + Neighbor Unreachability Detection algorithm ensures that all + nodes will reliably discover the new address, though the + delay may be somewhat longer. + + Inbound load balancing - Nodes with replicated interfaces may want + to load balance the reception of incoming packets across + multiple network interfaces on the same link. Such nodes + have multiple link-layer addresses assigned to the same + interface. For example, a single network driver could + represent multiple network interface cards as a single + logical interface having multiple link-layer addresses. + + + + + + +Narten, et al. Standards Track [Page 13] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + Neighbor Discovery allows a router to perform load balancing + for traffic addressed to itself by allowing routers to omit + the source link-layer address from Router Advertisement + packets, thereby forcing neighbors to use Neighbor + Solicitation messages to learn link-layer addresses of + routers. Returned Neighbor Advertisement messages can then + contain link-layer addresses that differ depending on, e.g., + who issued the solicitation. This specification does not + define a mechanism that allows hosts to Load-balance incoming + packets. See [LD-SHRE]. + + Anycast addresses - Anycast addresses identify one of a set of + nodes providing an equivalent service, and multiple nodes on + the same link may be configured to recognize the same anycast + address. Neighbor Discovery handles anycasts by having nodes + expect to receive multiple Neighbor Advertisements for the + same target. All advertisements for anycast addresses are + tagged as being non-Override advertisements. A non-Override + advertisement is one that does not update or replace the + information sent by another advertisement. These + advertisements are discussed later in the context of Neighbor + advertisement messages. This invokes specific rules to + determine which of potentially multiple advertisements should + be used. + + Proxy advertisements - A node willing to accept packets on behalf + of a target address that is unable to respond to Neighbor + Solicitations can issue non-Override Neighbor Advertisements. + Proxy advertisements are used by Mobile IPv6 Home Agents to + defend mobile nodes' addresses when they move off-link. + However, it is not intended as a general mechanism to handle + nodes that, e.g., do not implement this protocol. + +3.1. Comparison with IPv4 + + The IPv6 Neighbor Discovery protocol corresponds to a combination of + the IPv4 protocols Address Resolution Protocol [ARP], ICMP Router + Discovery [RDISC], and ICMP Redirect [ICMPv4]. In IPv4 there is no + generally agreed upon protocol or mechanism for Neighbor + Unreachability Detection, although the Hosts Requirements document + [HR-CL] does specify some possible algorithms for Dead Gateway + Detection (a subset of the problems Neighbor Unreachability Detection + tackles). + + + + + + + + +Narten, et al. Standards Track [Page 14] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + The Neighbor Discovery protocol provides a multitude of improvements + over the IPv4 set of protocols: + + Router Discovery is part of the base protocol set; there is no + need for hosts to "snoop" the routing protocols. + + Router Advertisements carry link-layer addresses; no additional + packet exchange is needed to resolve the router's link-layer + address. + + Router Advertisements carry prefixes for a link; there is no need + to have a separate mechanism to configure the "netmask". + + Router Advertisements enable Address Autoconfiguration. + + Routers can advertise an MTU for hosts to use on the link, + ensuring that all nodes use the same MTU value on links lacking a + well-defined MTU. + + Address resolution multicasts are "spread" over 16 million (2^24) + multicast addresses, greatly reducing address-resolution-related + interrupts on nodes other than the target. Moreover, non-IPv6 + machines should not be interrupted at all. + + Redirects contain the link-layer address of the new first hop; + separate address resolution is not needed upon receiving a + redirect. + + Multiple prefixes can be associated with the same link. By + default, hosts learn all on-link prefixes from Router + Advertisements. However, routers may be configured to omit some + or all prefixes from Router Advertisements. In such cases hosts + assume that destinations are off-link and send traffic to routers. + A router can then issue redirects as appropriate. + + Unlike IPv4, the recipient of an IPv6 redirect assumes that the + new next-hop is on-link. In IPv4, a host ignores redirects + specifying a next-hop that is not on-link according to the link's + network mask. The IPv6 redirect mechanism is analogous to the + XRedirect facility specified in [SH-MEDIA]. It is expected to be + useful on non-broadcast and shared media links in which it is + undesirable or not possible for nodes to know all prefixes for + on-link destinations. + + Neighbor Unreachability Detection is part of the base, which + significantly improves the robustness of packet delivery in the + presence of failing routers, partially failing or partitioned + links, or nodes that change their link-layer addresses. For + + + +Narten, et al. Standards Track [Page 15] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + instance, mobile nodes can move off-link without losing any + connectivity due to stale ARP caches. + + Unlike ARP, Neighbor Discovery detects half-link failures (using + Neighbor Unreachability Detection) and avoids sending traffic to + neighbors with which two-way connectivity is absent. + + Unlike in IPv4 Router Discovery, the Router Advertisement messages + do not contain a preference field. The preference field is not + needed to handle routers of different "stability"; the Neighbor + Unreachability Detection will detect dead routers and switch to a + working one. + + The use of link-local addresses to uniquely identify routers (for + Router Advertisement and Redirect messages) makes it possible for + hosts to maintain the router associations in the event of the site + renumbering to use new global prefixes. + + By setting the Hop Limit to 255, Neighbor Discovery is immune to + off-link senders that accidentally or intentionally send ND + messages. In IPv4, off-link senders can send both ICMP Redirects + and Router Advertisement messages. + + Placing address resolution at the ICMP layer makes the protocol + more media-independent than ARP and makes it possible to use + generic IP-layer authentication and security mechanisms as + appropriate. + +3.2. Supported Link Types + + Neighbor Discovery supports links with different properties. In the + presence of certain properties, only a subset of the ND protocol + mechanisms are fully specified in this document: + + point-to-point - Neighbor Discovery handles such links just like + multicast links. (Multicast can be trivially + provided on point-to-point links, and interfaces + can be assigned link-local addresses.) + + multicast - Neighbor Discovery operates over multicast capable + links as described in this document. + + non-broadcast multiple access (NBMA) + - Redirect, Neighbor Unreachability Detection and + next-hop determination should be implemented as + described in this document. Address resolution, + and the mechanism for delivering Router + Solicitations and Advertisements on NBMA links are + + + +Narten, et al. Standards Track [Page 16] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + not specified in this document. Note that if + hosts support manual configuration of a list of + default routers, hosts can dynamically acquire the + link-layer addresses for their neighbors from + Redirect messages. + + shared media - The Redirect message is modeled after the + XRedirect message in [SH-MEDIA] in order to + simplify use of the protocol on shared media + links. + + This specification does not address shared media + issues that only relate to routers, such as: + + - How routers exchange reachability information + on a shared media link. + + - How a router determines the link-layer address + of a host, which it needs to send redirect + messages to the host. + + - How a router determines that it is the first- + hop router for a received packet. + + The protocol is extensible (through the definition + of new options) so that other solutions might be + possible in the future. + + variable MTU - Neighbor Discovery allows routers to specify an + MTU for the link, which all nodes then use. All + nodes on a link must use the same MTU (or Maximum + Receive Unit) in order for multicast to work + properly. Otherwise, when multicasting, a sender, + which can not know which nodes will receive the + packet, could not determine a minimum packet size + that all receivers can process (or Maximum Receive + Unit). + + asymmetric reachability + - Neighbor Discovery detects the absence of + symmetric reachability; a node avoids paths to a + neighbor with which it does not have symmetric + connectivity. + + The Neighbor Unreachability Detection will + typically identify such half-links and the node + will refrain from using them. + + + + +Narten, et al. Standards Track [Page 17] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + The protocol can presumably be extended in the + future to find viable paths in environments that + lack reflexive and transitive connectivity. + +3.3. Securing Neighbor Discovery Messages + + Neighbor Discovery messages are needed for various functions. + Several functions are designed to allow hosts to ascertain the + ownership of an address or the mapping between link-layer and IP- + layer addresses. Vulnerabilities related to Neighbor Discovery are + discussed in Section 11.1. A general solution for securing Neighbor + Discovery is outside the scope of this specification and is discussed + in [SEND]. However, Section 11.2 explains how and under which + constraints IPsec Authentication Header (AH) or Encapsulating + Security Payload (ESP) can be used to secure Neighbor Discovery. + +4. Message Formats + + This section introduces message formats for all messages used in this + specification. + +4.1. Router Solicitation Message Format + + Hosts send Router Solicitations in order to prompt routers to + generate Router Advertisements quickly. + + 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Code | Checksum | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Reserved | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Options ... + +-+-+-+-+-+-+-+-+-+-+-+- + + IP Fields: + + Source Address + An IP address assigned to the sending interface, or + the unspecified address if no address is assigned + to the sending interface. + + Destination Address + Typically the all-routers multicast address. + + Hop Limit 255 + + + + +Narten, et al. Standards Track [Page 18] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + ICMP Fields: + + Type 133 + + Code 0 + + Checksum The ICMP checksum. See [ICMPv6]. + + Reserved This field is unused. It MUST be initialized to + zero by the sender and MUST be ignored by the + receiver. + Valid Options: + + Source link-layer address The link-layer address of the sender, if + known. MUST NOT be included if the Source Address + is the unspecified address. Otherwise, it SHOULD + be included on link layers that have addresses. + + Future versions of this protocol may define new option types. + Receivers MUST silently ignore any options they do not recognize + and continue processing the message. + +4.2. Router Advertisement Message Format + + Routers send out Router Advertisement messages periodically, or in + response to Router Solicitations. + + 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Code | Checksum | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Cur Hop Limit |M|O| Reserved | Router Lifetime | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Reachable Time | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Retrans Timer | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Options ... + +-+-+-+-+-+-+-+-+-+-+-+- + + IP Fields: + + Source Address + MUST be the link-local address assigned to the + interface from which this message is sent. + + + + + +Narten, et al. Standards Track [Page 19] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + Destination Address + Typically the Source Address of an invoking Router + Solicitation or the all-nodes multicast address. + + Hop Limit 255 + + ICMP Fields: + + Type 134 + + Code 0 + + Checksum The ICMP checksum. See [ICMPv6]. + + Cur Hop Limit 8-bit unsigned integer. The default value that + should be placed in the Hop Count field of the IP + header for outgoing IP packets. A value of zero + means unspecified (by this router). + + M 1-bit "Managed address configuration" flag. When + set, it indicates that addresses are available via + Dynamic Host Configuration Protocol [DHCPv6]. + + If the M flag is set, the O flag is redundant and + can be ignored because DHCPv6 will return all + available configuration information. + + O 1-bit "Other configuration" flag. When set, it + indicates that other configuration information is + available via DHCPv6. Examples of such information + are DNS-related information or information on other + servers within the network. + + Note: If neither M nor O flags are set, this indicates that no + information is available via DHCPv6. + + Reserved A 6-bit unused field. It MUST be initialized to + zero by the sender and MUST be ignored by the + receiver. + + Router Lifetime + 16-bit unsigned integer. The lifetime associated + with the default router in units of seconds. The + field can contain values up to 65535 and receivers + should handle any value, while the sending rules in + Section 6 limit the lifetime to 9000 seconds. A + Lifetime of 0 indicates that the router is not a + default router and SHOULD NOT appear on the default + + + +Narten, et al. Standards Track [Page 20] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + router list. The Router Lifetime applies only to + the router's usefulness as a default router; it + does not apply to information contained in other + message fields or options. Options that need time + limits for their information include their own + lifetime fields. + + Reachable Time 32-bit unsigned integer. The time, in + milliseconds, that a node assumes a neighbor is + reachable after having received a reachability + confirmation. Used by the Neighbor Unreachability + Detection algorithm (see Section 7.3). A value of + zero means unspecified (by this router). + + Retrans Timer 32-bit unsigned integer. The time, in + milliseconds, between retransmitted Neighbor + Solicitation messages. Used by address resolution + and the Neighbor Unreachability Detection algorithm + (see Sections 7.2 and 7.3). A value of zero means + unspecified (by this router). + + Possible options: + + Source link-layer address + The link-layer address of the interface from which + the Router Advertisement is sent. Only used on + link layers that have addresses. A router MAY omit + this option in order to enable inbound load sharing + across multiple link-layer addresses. + + MTU SHOULD be sent on links that have a variable MTU + (as specified in the document that describes how to + run IP over the particular link type). MAY be sent + on other links. + + Prefix Information + These options specify the prefixes that are on-link + and/or are used for stateless address + autoconfiguration. A router SHOULD include all its + on-link prefixes (except the link-local prefix) so + that multihomed hosts have complete prefix + information about on-link destinations for the + links to which they attach. If complete + information is lacking, a host with multiple + interfaces may not be able to choose the correct + outgoing interface when sending traffic to its + neighbors. + + + + +Narten, et al. Standards Track [Page 21] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + Future versions of this protocol may define new option types. + Receivers MUST silently ignore any options they do not recognize + and continue processing the message. + +4.3. Neighbor Solicitation Message Format + + Nodes send Neighbor Solicitations to request the link-layer address + of a target node while also providing their own link-layer address to + the target. Neighbor Solicitations are multicast when the node needs + to resolve an address and unicast when the node seeks to verify the + reachability of a neighbor. + + 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Code | Checksum | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Reserved | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + + + | | + + Target Address + + | | + + + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Options ... + +-+-+-+-+-+-+-+-+-+-+-+- + + IP Fields: + + Source Address + Either an address assigned to the interface from + which this message is sent or (if Duplicate Address + Detection is in progress [ADDRCONF]) the + unspecified address. + Destination Address + Either the solicited-node multicast address + corresponding to the target address, or the target + address. + Hop Limit 255 + + ICMP Fields: + + Type 135 + + Code 0 + + + +Narten, et al. Standards Track [Page 22] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + Checksum The ICMP checksum. See [ICMPv6]. + + Reserved This field is unused. It MUST be initialized to + zero by the sender and MUST be ignored by the + receiver. + + Target Address The IP address of the target of the solicitation. + It MUST NOT be a multicast address. + + Possible options: + + Source link-layer address + The link-layer address for the sender. MUST NOT be + included when the source IP address is the + unspecified address. Otherwise, on link layers + that have addresses this option MUST be included in + multicast solicitations and SHOULD be included in + unicast solicitations. + + Future versions of this protocol may define new option types. + Receivers MUST silently ignore any options they do not recognize + and continue processing the message. + +4.4. Neighbor Advertisement Message Format + + A node sends Neighbor Advertisements in response to Neighbor + Solicitations and sends unsolicited Neighbor Advertisements in order + to (unreliably) propagate new information quickly. + + 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Code | Checksum | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |R|S|O| Reserved | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + + + | | + + Target Address + + | | + + + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Options ... + +-+-+-+-+-+-+-+-+-+-+-+- + + + + + +Narten, et al. Standards Track [Page 23] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + IP Fields: + + Source Address + An address assigned to the interface from which the + advertisement is sent. + Destination Address + For solicited advertisements, the Source Address of + an invoking Neighbor Solicitation or, if the + solicitation's Source Address is the unspecified + address, the all-nodes multicast address. + + For unsolicited advertisements typically the all- + nodes multicast address. + + Hop Limit 255 + + ICMP Fields: + + Type 136 + + Code 0 + + Checksum The ICMP checksum. See [ICMPv6]. + + R Router flag. When set, the R-bit indicates that + the sender is a router. The R-bit is used by + Neighbor Unreachability Detection to detect a + router that changes to a host. + + S Solicited flag. When set, the S-bit indicates that + the advertisement was sent in response to a + Neighbor Solicitation from the Destination address. + The S-bit is used as a reachability confirmation + for Neighbor Unreachability Detection. It MUST NOT + be set in multicast advertisements or in + unsolicited unicast advertisements. + + O Override flag. When set, the O-bit indicates that + the advertisement should override an existing cache + entry and update the cached link-layer address. + When it is not set the advertisement will not + update a cached link-layer address though it will + update an existing Neighbor Cache entry for which + no link-layer address is known. It SHOULD NOT be + set in solicited advertisements for anycast + addresses and in solicited proxy advertisements. + It SHOULD be set in other solicited advertisements + and in unsolicited advertisements. + + + +Narten, et al. Standards Track [Page 24] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + Reserved 29-bit unused field. It MUST be initialized to + zero by the sender and MUST be ignored by the + receiver. + + Target Address + For solicited advertisements, the Target Address + field in the Neighbor Solicitation message that + prompted this advertisement. For an unsolicited + advertisement, the address whose link-layer address + has changed. The Target Address MUST NOT be a + multicast address. + + Possible options: + + Target link-layer address + The link-layer address for the target, i.e., the + sender of the advertisement. This option MUST be + included on link layers that have addresses when + responding to multicast solicitations. When + responding to a unicast Neighbor Solicitation this + option SHOULD be included. + + The option MUST be included for multicast + solicitations in order to avoid infinite Neighbor + Solicitation "recursion" when the peer node does + not have a cache entry to return a Neighbor + Advertisements message. When responding to unicast + solicitations, the option can be omitted since the + sender of the solicitation has the correct link- + layer address; otherwise, it would not be able to + send the unicast solicitation in the first place. + However, including the link-layer address in this + case adds little overhead and eliminates a + potential race condition where the sender deletes + the cached link-layer address prior to receiving a + response to a previous solicitation. + + Future versions of this protocol may define new option types. + Receivers MUST silently ignore any options they do not recognize + and continue processing the message. + + + + + + + + + + + +Narten, et al. Standards Track [Page 25] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + +4.5. Redirect Message Format + + Routers send Redirect packets to inform a host of a better first-hop + node on the path to a destination. Hosts can be redirected to a + better first-hop router but can also be informed by a redirect that + the destination is in fact a neighbor. The latter is accomplished by + setting the ICMP Target Address equal to the ICMP Destination + Address. + + 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Code | Checksum | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Reserved | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + + + | | + + Target Address + + | | + + + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + + + | | + + Destination Address + + | | + + + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Options ... + +-+-+-+-+-+-+-+-+-+-+-+- + + IP Fields: + + Source Address + MUST be the link-local address assigned to the + interface from which this message is sent. + + Destination Address + The Source Address of the packet that triggered the + redirect. + + Hop Limit 255 + + + + + +Narten, et al. Standards Track [Page 26] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + ICMP Fields: + + Type 137 + + Code 0 + + Checksum The ICMP checksum. See [ICMPv6]. + + Reserved This field is unused. It MUST be initialized to + zero by the sender and MUST be ignored by the + receiver. + + Target Address + An IP address that is a better first hop to use for + the ICMP Destination Address. When the target is + the actual endpoint of communication, i.e., the + destination is a neighbor, the Target Address field + MUST contain the same value as the ICMP Destination + Address field. Otherwise, the target is a better + first-hop router and the Target Address MUST be the + router's link-local address so that hosts can + uniquely identify routers. + + Destination Address + The IP address of the destination that is + redirected to the target. + + Possible options: + + Target link-layer address + The link-layer address for the target. It SHOULD + be included (if known). Note that on NBMA links, + hosts may rely on the presence of the Target Link- + Layer Address option in Redirect messages as the + means for determining the link-layer addresses of + neighbors. In such cases, the option MUST be + included in Redirect messages. + + Redirected Header + As much as possible of the IP packet that triggered + the sending of the Redirect without making the + redirect packet exceed the minimum MTU specified in + [IPv6]. + + + + + + + + +Narten, et al. Standards Track [Page 27] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + +4.6. Option Formats + + Neighbor Discovery messages include zero or more options, some of + which may appear multiple times in the same message. Options should + be padded when necessary to ensure that they end on their natural + 64-bit boundaries. All options are of the form: + + 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | ... | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + ~ ... ~ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Fields: + + Type 8-bit identifier of the type of option. The + options defined in this document are: + + Option Name Type + + Source Link-Layer Address 1 + Target Link-Layer Address 2 + Prefix Information 3 + Redirected Header 4 + MTU 5 + + Length 8-bit unsigned integer. The length of the option + (including the type and length fields) in units of + 8 octets. The value 0 is invalid. Nodes MUST + silently discard an ND packet that contains an + option with length zero. + +4.6.1. Source/Target Link-layer Address + + 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Link-Layer Address ... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Fields: + + Type + 1 for Source Link-layer Address + 2 for Target Link-layer Address + + + + +Narten, et al. Standards Track [Page 28] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + Length The length of the option (including the type and + length fields) in units of 8 octets. For example, + the length for IEEE 802 addresses is 1 + [IPv6-ETHER]. + + Link-Layer Address + The variable length link-layer address. + + The content and format of this field (including + byte and bit ordering) is expected to be specified + in specific documents that describe how IPv6 + operates over different link layers. For instance, + [IPv6-ETHER]. + + Description + The Source Link-Layer Address option contains the + link-layer address of the sender of the packet. It + is used in the Neighbor Solicitation, Router + Solicitation, and Router Advertisement packets. + + The Target Link-Layer Address option contains the + link-layer address of the target. It is used in + Neighbor Advertisement and Redirect packets. + + These options MUST be silently ignored for other + Neighbor Discovery messages. + +4.6.2. Prefix Information + + 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Prefix Length |L|A| Reserved1 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Valid Lifetime | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Preferred Lifetime | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Reserved2 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + + + | | + + Prefix + + | | + + + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + + +Narten, et al. Standards Track [Page 29] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + Fields: + + Type 3 + + Length 4 + + Prefix Length 8-bit unsigned integer. The number of leading bits + in the Prefix that are valid. The value ranges + from 0 to 128. The prefix length field provides + necessary information for on-link determination + (when combined with the L flag in the prefix + information option). It also assists with address + autoconfiguration as specified in [ADDRCONF], for + which there may be more restrictions on the prefix + length. + + L 1-bit on-link flag. When set, indicates that this + prefix can be used for on-link determination. When + not set the advertisement makes no statement about + on-link or off-link properties of the prefix. In + other words, if the L flag is not set a host MUST + NOT conclude that an address derived from the + prefix is off-link. That is, it MUST NOT update a + previous indication that the address is on-link. + + A 1-bit autonomous address-configuration flag. When + set indicates that this prefix can be used for + stateless address configuration as specified in + [ADDRCONF]. + + Reserved1 6-bit unused field. It MUST be initialized to zero + by the sender and MUST be ignored by the receiver. + + Valid Lifetime + 32-bit unsigned integer. The length of time in + seconds (relative to the time the packet is sent) + that the prefix is valid for the purpose of on-link + determination. A value of all one bits + (0xffffffff) represents infinity. The Valid + Lifetime is also used by [ADDRCONF]. + + Preferred Lifetime + 32-bit unsigned integer. The length of time in + seconds (relative to the time the packet is sent) + that addresses generated from the prefix via + stateless address autoconfiguration remain + preferred [ADDRCONF]. A value of all one bits + (0xffffffff) represents infinity. See [ADDRCONF]. + + + +Narten, et al. Standards Track [Page 30] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + Note that the value of this field MUST NOT exceed + the Valid Lifetime field to avoid preferring + addresses that are no longer valid. + + Reserved2 This field is unused. It MUST be initialized to + zero by the sender and MUST be ignored by the + receiver. + + Prefix An IP address or a prefix of an IP address. The + Prefix Length field contains the number of valid + leading bits in the prefix. The bits in the prefix + after the prefix length are reserved and MUST be + initialized to zero by the sender and ignored by + the receiver. A router SHOULD NOT send a prefix + option for the link-local prefix and a host SHOULD + ignore such a prefix option. + + Description + The Prefix Information option provide hosts with + on-link prefixes and prefixes for Address + Autoconfiguration. The Prefix Information option + appears in Router Advertisement packets and MUST be + silently ignored for other messages. + +4.6.3. Redirected Header + + 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Reserved | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Reserved | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + ~ IP header + data ~ + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Fields: + + Type 4 + + Length The length of the option in units of 8 octets. + + Reserved These fields are unused. They MUST be initialized + to zero by the sender and MUST be ignored by the + receiver. + + + + +Narten, et al. Standards Track [Page 31] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + IP header + data + The original packet truncated to ensure that the + size of the redirect message does not exceed the + minimum MTU required to support IPv6 as specified + in [IPv6]. + + Description + The Redirected Header option is used in Redirect + messages and contains all or part of the packet + that is being redirected. + + This option MUST be silently ignored for other + Neighbor Discovery messages. + +4.6.4. MTU + + 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Reserved | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | MTU | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Fields: + + Type 5 + + Length 1 + + Reserved This field is unused. It MUST be initialized to + zero by the sender and MUST be ignored by the + receiver. + + MTU 32-bit unsigned integer. The recommended MTU for + the link. + + Description + The MTU option is used in Router Advertisement + messages to ensure that all nodes on a link use the + same MTU value in those cases where the link MTU is + not well known. + + This option MUST be silently ignored for other + Neighbor Discovery messages. + + + + + + +Narten, et al. Standards Track [Page 32] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + In configurations in which heterogeneous + technologies are bridged together, the maximum + supported MTU may differ from one segment to + another. If the bridges do not generate ICMP + Packet Too Big messages, communicating nodes will + be unable to use Path MTU to dynamically determine + the appropriate MTU on a per-neighbor basis. In + such cases, routers can be configured to use the + MTU option to specify the maximum MTU value that is + supported by all segments. + +5. Conceptual Model of a Host + + This section describes a conceptual model of one possible data + structure organization that hosts (and, to some extent, routers) will + maintain in interacting with neighboring nodes. The described + organization is provided to facilitate the explanation of how the + Neighbor Discovery protocol should behave. This document does not + mandate that implementations adhere to this model as long as their + external behavior is consistent with that described in this document. + + This model is only concerned with the aspects of host behavior + directly related to Neighbor Discovery. In particular, it does not + concern itself with such issues as source address selection or the + selecting of an outgoing interface on a multihomed host. + +5.1. Conceptual Data Structures + + Hosts will need to maintain the following pieces of information for + each interface: + + Neighbor Cache + - A set of entries about individual neighbors to + which traffic has been sent recently. Entries are + keyed on the neighbor's on-link unicast IP address + and contain such information as its link-layer + address, a flag indicating whether the neighbor is + a router or a host (called IsRouter in this + document), a pointer to any queued packets waiting + for address resolution to complete, etc. A + Neighbor Cache entry also contains information used + by the Neighbor Unreachability Detection algorithm, + including the reachability state, the number of + unanswered probes, and the time the next Neighbor + Unreachability Detection event is scheduled to take + place. + + + + + +Narten, et al. Standards Track [Page 33] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + Destination Cache + - A set of entries about destinations to which + traffic has been sent recently. The Destination + Cache includes both on-link and off-link + destinations and provides a level of indirection + into the Neighbor Cache; the Destination Cache maps + a destination IP address to the IP address of the + next-hop neighbor. This cache is updated with + information learned from Redirect messages. + Implementations may find it convenient to store + additional information not directly related to + Neighbor Discovery in Destination Cache entries, + such as the Path MTU (PMTU) and round-trip timers + maintained by transport protocols. + + Prefix List - A list of the prefixes that define a set of + addresses that are on-link. Prefix List entries + are created from information received in Router + Advertisements. Each entry has an associated + invalidation timer value (extracted from the + advertisement) used to expire prefixes when they + become invalid. A special "infinity" timer value + specifies that a prefix remains valid forever, + unless a new (finite) value is received in a + subsequent advertisement. + + The link-local prefix is considered to be on the + prefix list with an infinite invalidation timer + regardless of whether routers are advertising a + prefix for it. Received Router Advertisements + SHOULD NOT modify the invalidation timer for the + link-local prefix. + + Default Router List + - A list of routers to which packets may be sent. + Router list entries point to entries in the + Neighbor Cache; the algorithm for selecting a + default router favors routers known to be reachable + over those whose reachability is suspect. Each + entry also has an associated invalidation timer + value (extracted from Router Advertisements) used + to delete entries that are no longer advertised. + + + + + + + + + +Narten, et al. Standards Track [Page 34] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + Note that the above conceptual data structures can be implemented + using a variety of techniques. One possible implementation is to use + a single longest-match routing table for all of the above data + structures. Regardless of the specific implementation, it is + critical that the Neighbor Cache entry for a router is shared by all + Destination Cache entries using that router in order to prevent + redundant Neighbor Unreachability Detection probes. + + Note also that other protocols (e.g., Mobile IPv6) might add + additional conceptual data structures. An implementation is at + liberty to implement such data structures in any way it pleases. For + example, an implementation could merge all conceptual data structures + into a single routing table. + + The Neighbor Cache contains information maintained by the Neighbor + Unreachability Detection algorithm. A key piece of information is a + neighbor's reachability state, which is one of five possible values. + The following definitions are informal; precise definitions can be + found in Section 7.3.2. + + INCOMPLETE Address resolution is in progress and the link-layer + address of the neighbor has not yet been determined. + + REACHABLE Roughly speaking, the neighbor is known to have been + reachable recently (within tens of seconds ago). + + STALE The neighbor is no longer known to be reachable but + until traffic is sent to the neighbor, no attempt + should be made to verify its reachability. + + DELAY The neighbor is no longer known to be reachable, and + traffic has recently been sent to the neighbor. + Rather than probe the neighbor immediately, however, + delay sending probes for a short while in order to + give upper-layer protocols a chance to provide + reachability confirmation. + + PROBE The neighbor is no longer known to be reachable, and + unicast Neighbor Solicitation probes are being sent to + verify reachability. + + + + + + + + + + + +Narten, et al. Standards Track [Page 35] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + +5.2. Conceptual Sending Algorithm + + When sending a packet to a destination, a node uses a combination of + the Destination Cache, the Prefix List, and the Default Router List + to determine the IP address of the appropriate next hop, an operation + known as "next-hop determination". Once the IP address of the next + hop is known, the Neighbor Cache is consulted for link-layer + information about that neighbor. + + Next-hop determination for a given unicast destination operates as + follows. The sender performs a longest prefix match against the + Prefix List to determine whether the packet's destination is on- or + off-link. If the destination is on-link, the next-hop address is the + same as the packet's destination address. Otherwise, the sender + selects a router from the Default Router List (following the rules + described in Section 6.3.6). + + For efficiency reasons, next-hop determination is not performed on + every packet that is sent. Instead, the results of next-hop + determination computations are saved in the Destination Cache (which + also contains updates learned from Redirect messages). When the + sending node has a packet to send, it first examines the Destination + Cache. If no entry exists for the destination, next-hop + determination is invoked to create a Destination Cache entry. + + Once the IP address of the next-hop node is known, the sender + examines the Neighbor Cache for link-layer information about that + neighbor. If no entry exists, the sender creates one, sets its state + to INCOMPLETE, initiates Address Resolution, and then queues the data + packet pending completion of address resolution. For multicast- + capable interfaces Address Resolution consists of sending a Neighbor + Solicitation message and waiting for a Neighbor Advertisement. When + a Neighbor Advertisement response is received, the link-layer + addresses is entered in the Neighbor Cache entry and the queued + packet is transmitted. The address resolution mechanism is described + in detail in Section 7.2. + + For multicast packets, the next-hop is always the (multicast) + destination address and is considered to be on-link. The procedure + for determining the link-layer address corresponding to a given IP + multicast address can be found in a separate document that covers + operating IP over a particular link type (e.g., [IPv6-ETHER]). + + + + + + + + + +Narten, et al. Standards Track [Page 36] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + Each time a Neighbor Cache entry is accessed while transmitting a + unicast packet, the sender checks Neighbor Unreachability Detection + related information according to the Neighbor Unreachability + Detection algorithm (Section 7.3). This unreachability check might + result in the sender transmitting a unicast Neighbor Solicitation to + verify that the neighbor is still reachable. + + Next-hop determination is done the first time traffic is sent to a + destination. As long as subsequent communication to that destination + proceeds successfully, the Destination Cache entry continues to be + used. If at some point communication ceases to proceed, as + determined by the Neighbor Unreachability Detection algorithm, next- + hop determination may need to be performed again. For example, + traffic through a failed router should be switched to a working + router. Likewise, it may be possible to reroute traffic destined for + a mobile node to a "mobility agent". + + Note that when a node redoes next-hop determination there is no need + to discard the complete Destination Cache entry. In fact, it is + generally beneficial to retain such cached information as the PMTU + and round-trip timer values that may also be kept in the Destination + Cache entry. + + Routers and multihomed hosts have multiple interfaces. The remainder + of this document assumes that all sent and received Neighbor + Discovery messages refer to the interface of appropriate context. + For example, when responding to a Router Solicitation, the + corresponding Router Advertisement is sent out the interface on which + the solicitation was received. + +5.3. Garbage Collection and Timeout Requirements + + The conceptual data structures described above use different + mechanisms for discarding potentially stale or unused information. + + From the perspective of correctness, there is no need to periodically + purge Destination and Neighbor Cache entries. Although stale + information can potentially remain in the cache indefinitely, the + Neighbor Unreachability Detection algorithm ensures that stale + information is purged quickly if it is actually being used. + + To limit the storage needed for the Destination and Neighbor Caches, + a node may need to garbage-collect old entries. However, care must + be taken to ensure that sufficient space is always present to hold + the working set of active entries. A small cache may result in an + excessive number of Neighbor Discovery messages if entries are + discarded and rebuilt in quick succession. Any Least Recently Used + (LRU)-based policy that only reclaims entries that have not been used + + + +Narten, et al. Standards Track [Page 37] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + in some time (e.g., ten minutes or more) should be adequate for + garbage-collecting unused entries. + + A node should retain entries in the Default Router List and the + Prefix List until their lifetimes expire. However, a node may + garbage-collect entries prematurely if it is low on memory. If not + all routers are kept on the Default Router list, a node should retain + at least two entries in the Default Router List (and preferably more) + in order to maintain robust connectivity for off-link destinations. + + When removing an entry from the Prefix List, there is no need to + purge any entries from the Destination or Neighbor Caches. Neighbor + Unreachability Detection will efficiently purge any entries in these + caches that have become invalid. When removing an entry from the + Default Router List, however, any entries in the Destination Cache + that go through that router must perform next-hop determination again + to select a new default router. + +6. Router and Prefix Discovery + + This section describes router and host behavior related to the Router + Discovery portion of Neighbor Discovery. Router Discovery is used to + locate neighboring routers as well as learn prefixes and + configuration parameters related to stateless address + autoconfiguration. + + Prefix Discovery is the process through which hosts learn the ranges + of IP addresses that reside on-link and can be reached directly + without going through a router. Routers send Router Advertisements + that indicate whether the sender is willing to be a default router. + Router Advertisements also contain Prefix Information options that + list the set of prefixes that identify on-link IP addresses. + + Stateless Address Autoconfiguration must also obtain subnet prefixes + as part of configuring addresses. Although the prefixes used for + address autoconfiguration are logically distinct from those used for + on-link determination, autoconfiguration information is piggybacked + on Router Discovery messages to reduce network traffic. Indeed, the + same prefixes can be advertised for on-link determination and address + autoconfiguration by specifying the appropriate flags in the Prefix + Information options. See [ADDRCONF] for details on how + autoconfiguration information is processed. + + + + + + + + + +Narten, et al. Standards Track [Page 38] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + +6.1. Message Validation + +6.1.1. Validation of Router Solicitation Messages + + Hosts MUST silently discard any received Router Solicitation + Messages. + + A router MUST silently discard any received Router Solicitation + messages that do not satisfy all of the following validity checks: + + - The IP Hop Limit field has a value of 255, i.e., the packet + could not possibly have been forwarded by a router. + + - ICMP Checksum is valid. + + - ICMP Code is 0. + + - ICMP length (derived from the IP length) is 8 or more octets. + + - All included options have a length that is greater than zero. + + - If the IP source address is the unspecified address, there is no + source link-layer address option in the message. + + The contents of the Reserved field, and of any unrecognized options, + MUST be ignored. Future, backward-compatible changes to the protocol + may specify the contents of the Reserved field or add new options; + backward-incompatible changes may use different Code values. + + The contents of any defined options that are not specified to be used + with Router Solicitation messages MUST be ignored and the packet + processed as normal. The only defined option that may appear is the + Source Link-Layer Address option. + + A solicitation that passes the validity checks is called a "valid + solicitation". + +6.1.2. Validation of Router Advertisement Messages + + A node MUST silently discard any received Router Advertisement + messages that do not satisfy all of the following validity checks: + + - IP Source Address is a link-local address. Routers must use + their link-local address as the source for Router Advertisement + and Redirect messages so that hosts can uniquely identify + routers. + + + + + +Narten, et al. Standards Track [Page 39] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + - The IP Hop Limit field has a value of 255, i.e., the packet + could not possibly have been forwarded by a router. + + - ICMP Checksum is valid. + + - ICMP Code is 0. + + - ICMP length (derived from the IP length) is 16 or more octets. + + - All included options have a length that is greater than zero. + + The contents of the Reserved field, and of any unrecognized options, + MUST be ignored. Future, backward-compatible changes to the protocol + may specify the contents of the Reserved field or add new options; + backward-incompatible changes may use different Code values. + + The contents of any defined options that are not specified to be used + with Router Advertisement messages MUST be ignored and the packet + processed as normal. The only defined options that may appear are + the Source Link-Layer Address, Prefix Information and MTU options. + + An advertisement that passes the validity checks is called a "valid + advertisement". + +6.2. Router Specification + +6.2.1. Router Configuration Variables + + A router MUST allow for the following conceptual variables to be + configured by system management. The specific variable names are + used for demonstration purposes only, and an implementation is not + required to have them, so long as its external behavior is consistent + with that described in this document. Default values are specified + to simplify configuration in common cases. + + The default values for some of the variables listed below may be + overridden by specific documents that describe how IPv6 operates over + different link layers. This rule simplifies the configuration of + Neighbor Discovery over link types with widely differing performance + characteristics. + + + + + + + + + + + +Narten, et al. Standards Track [Page 40] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + For each interface: + + IsRouter A flag indicating whether routing is enabled on + this interface. Enabling routing on the interface + would imply that a router can forward packets to or + from the interface. + + Default: FALSE + + AdvSendAdvertisements + A flag indicating whether or not the router sends + periodic Router Advertisements and responds to + Router Solicitations. + + Default: FALSE + + Note that AdvSendAdvertisements MUST be FALSE by + default so that a node will not accidentally start + acting as a router unless it is explicitly + configured by system management to send Router + Advertisements. + + MaxRtrAdvInterval + The maximum time allowed between sending + unsolicited multicast Router Advertisements from + the interface, in seconds. MUST be no less than 4 + seconds and no greater than 1800 seconds. + + Default: 600 seconds + + MinRtrAdvInterval + The minimum time allowed between sending + unsolicited multicast Router Advertisements from + the interface, in seconds. MUST be no less than 3 + seconds and no greater than .75 * + MaxRtrAdvInterval. + + Default: 0.33 * MaxRtrAdvInterval If + MaxRtrAdvInterval >= 9 seconds; otherwise, the + Default is MaxRtrAdvInterval. + + AdvManagedFlag + The TRUE/FALSE value to be placed in the "Managed + address configuration" flag field in the Router + Advertisement. See [ADDRCONF]. + + Default: FALSE + + + + +Narten, et al. Standards Track [Page 41] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + AdvOtherConfigFlag + The TRUE/FALSE value to be placed in the "Other + configuration" flag field in the Router + Advertisement. See [ADDRCONF]. + + Default: FALSE + + AdvLinkMTU The value to be placed in MTU options sent by the + router. A value of zero indicates that no MTU + options are sent. + + Default: 0 + + AdvReachableTime + The value to be placed in the Reachable Time field + in the Router Advertisement messages sent by the + router. The value zero means unspecified (by this + router). MUST be no greater than 3,600,000 + milliseconds (1 hour). + + Default: 0 + + AdvRetransTimer The value to be placed in the Retrans Timer field + in the Router Advertisement messages sent by the + router. The value zero means unspecified (by this + router). + + Default: 0 + + AdvCurHopLimit + The default value to be placed in the Cur Hop Limit + field in the Router Advertisement messages sent by + the router. The value should be set to the current + diameter of the Internet. The value zero means + unspecified (by this router). + + Default: The value specified in the "Assigned + Numbers" [ASSIGNED] that was in effect at the time + of implementation. + + + + + + + + + + + + +Narten, et al. Standards Track [Page 42] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + AdvDefaultLifetime + The value to be placed in the Router Lifetime field + of Router Advertisements sent from the interface, + in seconds. MUST be either zero or between + MaxRtrAdvInterval and 9000 seconds. A value of + zero indicates that the router is not to be used as + a default router. These limits may be overridden + by specific documents that describe how IPv6 + operates over different link layers. For instance, + in a point-to-point link the peers may have enough + information about the number and status of devices + at the other end so that advertisements are needed + less frequently. + + Default: 3 * MaxRtrAdvInterval + + AdvPrefixList + A list of prefixes to be placed in Prefix + Information options in Router Advertisement + messages sent from the interface. + + Default: all prefixes that the router advertises + via routing protocols as being on-link for the + interface from which the advertisement is sent. + The link-local prefix SHOULD NOT be included in the + list of advertised prefixes. + + Each prefix has an associated: + + AdvValidLifetime + The value to be placed in the Valid + Lifetime in the Prefix Information option, + in seconds. The designated value of all + 1's (0xffffffff) represents infinity. + Implementations MAY allow AdvValidLifetime + to be specified in two ways: + + - a time that decrements in real time, + that is, one that will result in a + Lifetime of zero at the specified time + in the future, or + + - a fixed time that stays the same in + consecutive advertisements. + + Default: 2592000 seconds (30 days), fixed + (i.e., stays the same in consecutive + advertisements). + + + +Narten, et al. Standards Track [Page 43] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + AdvOnLinkFlag + The value to be placed in the on-link flag + ("L-bit") field in the Prefix Information + option. + + Default: TRUE + + Stateless address configuration [ADDRCONF] defines + additional information associated with each of the + prefixes: + + AdvPreferredLifetime + The value to be placed in the Preferred + Lifetime in the Prefix Information option, + in seconds. The designated value of all + 1's (0xffffffff) represents infinity. See + [ADDRCONF] for details on how this value is + used. Implementations MAY allow + AdvPreferredLifetime to be specified in two + ways: + + - a time that decrements in real time, + that is, one that will result in a + Lifetime of zero at a specified time in + the future, or + + - a fixed time that stays the same in + consecutive advertisements. + + Default: 604800 seconds (7 days), fixed + (i.e., stays the same in consecutive + advertisements). This value MUST NOT be + larger than AdvValidLifetime. + + AdvAutonomousFlag + The value to be placed in the Autonomous + Flag field in the Prefix Information + option. See [ADDRCONF]. + + Default: TRUE + + The above variables contain information that is placed in outgoing + Router Advertisement messages. Hosts use the received information to + initialize a set of analogous variables that control their external + behavior (see Section 6.3.2). Some of these host variables (e.g., + CurHopLimit, RetransTimer, and ReachableTime) apply to all nodes + including routers. In practice, these variables may not actually be + present on routers, since their contents can be derived from the + + + +Narten, et al. Standards Track [Page 44] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + variables described above. However, external router behavior MUST be + the same as host behavior with respect to these variables. In + particular, this includes the occasional randomization of the + ReachableTime value as described in Section 6.3.2. + + Protocol constants are defined in Section 10. + +6.2.2. Becoming an Advertising Interface + + The term "advertising interface" refers to any functioning and + enabled interface that has at least one unicast IP address assigned + to it and whose corresponding AdvSendAdvertisements flag is TRUE. A + router MUST NOT send Router Advertisements out any interface that is + not an advertising interface. + + An interface may become an advertising interface at times other than + system startup. For example: + + - changing the AdvSendAdvertisements flag on an enabled interface + from FALSE to TRUE, or + + - administratively enabling the interface, if it had been + administratively disabled, and its AdvSendAdvertisements flag is + TRUE, or + + - enabling IP forwarding capability (i.e., changing the system + from being a host to being a router), when the interface's + AdvSendAdvertisements flag is TRUE. + + A router MUST join the all-routers multicast address on an + advertising interface. Routers respond to Router Solicitations sent + to the all-routers address and verify the consistency of Router + Advertisements sent by neighboring routers. + +6.2.3. Router Advertisement Message Content + + A router sends periodic as well as solicited Router Advertisements + out its advertising interfaces. Outgoing Router Advertisements are + filled with the following values consistent with the message format + given in Section 4.2: + + - In the Router Lifetime field: the interface's configured + AdvDefaultLifetime. + + - In the M and O flags: the interface's configured AdvManagedFlag + and AdvOtherConfigFlag, respectively. + + + + + +Narten, et al. Standards Track [Page 45] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + - In the Cur Hop Limit field: the interface's configured + CurHopLimit. + + - In the Reachable Time field: the interface's configured + AdvReachableTime. + + - In the Retrans Timer field: the interface's configured + AdvRetransTimer. + + - In the options: + + o Source Link-Layer Address option: link-layer address of the + sending interface. This option MAY be omitted to + facilitate in-bound load balancing over replicated + interfaces. + + o MTU option: the interface's configured AdvLinkMTU value if + the value is non-zero. If AdvLinkMTU is zero, the MTU + option is not sent. + + o Prefix Information options: one Prefix Information option + for each prefix listed in AdvPrefixList with the option + fields set from the information in the AdvPrefixList entry + as follows: + + - In the "on-link" flag: the entry's AdvOnLinkFlag. + + - In the Valid Lifetime field: the entry's + AdvValidLifetime. + + - In the "Autonomous address configuration" flag: the + entry's AdvAutonomousFlag. + + - In the Preferred Lifetime field: the entry's + AdvPreferredLifetime. + + A router might want to send Router Advertisements without advertising + itself as a default router. For instance, a router might advertise + prefixes for stateless address autoconfiguration while not wishing to + forward packets. Such a router sets the Router Lifetime field in + outgoing advertisements to zero. + + A router MAY choose not to include some or all options when sending + unsolicited Router Advertisements. For example, if prefix lifetimes + are much longer than AdvDefaultLifetime, including them every few + advertisements may be sufficient. However, when responding to a + Router Solicitation or while sending the first few initial + + + + +Narten, et al. Standards Track [Page 46] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + unsolicited advertisements, a router SHOULD include all options so + that all information (e.g., prefixes) is propagated quickly during + system initialization. + + If including all options causes the size of an advertisement to + exceed the link MTU, multiple advertisements can be sent, each + containing a subset of the options. + +6.2.4. Sending Unsolicited Router Advertisements + + A host MUST NOT send Router Advertisement messages at any time. + + Unsolicited Router Advertisements are not strictly periodic: the + interval between subsequent transmissions is randomized to reduce the + probability of synchronization with the advertisements from other + routers on the same link [SYNC]. Each advertising interface has its + own timer. Whenever a multicast advertisement is sent from an + interface, the timer is reset to a uniformly distributed random value + between the interface's configured MinRtrAdvInterval and + MaxRtrAdvInterval; expiration of the timer causes the next + advertisement to be sent and a new random value to be chosen. + + For the first few advertisements (up to + MAX_INITIAL_RTR_ADVERTISEMENTS) sent from an interface when it + becomes an advertising interface, if the randomly chosen interval is + greater than MAX_INITIAL_RTR_ADVERT_INTERVAL, the timer SHOULD be set + to MAX_INITIAL_RTR_ADVERT_INTERVAL instead. Using a smaller interval + for the initial advertisements increases the likelihood of a router + being discovered quickly when it first becomes available, in the + presence of possible packet loss. + + The information contained in Router Advertisements may change through + actions of system management. For instance, the lifetime of + advertised prefixes may change, new prefixes could be added, a router + could cease to be a router (i.e., switch from being a router to being + a host), etc. In such cases, the router MAY transmit up to + MAX_INITIAL_RTR_ADVERTISEMENTS unsolicited advertisements, using the + same rules as when an interface becomes an advertising interface. + +6.2.5. Ceasing To Be an Advertising Interface + + An interface may cease to be an advertising interface, through + actions of system management such as: + + - changing the AdvSendAdvertisements flag of an enabled interface + from TRUE to FALSE, or + + - administratively disabling the interface, or + + + +Narten, et al. Standards Track [Page 47] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + - shutting down the system. + + In such cases, the router SHOULD transmit one or more (but not more + than MAX_FINAL_RTR_ADVERTISEMENTS) final multicast Router + Advertisements on the interface with a Router Lifetime field of zero. + In the case of a router becoming a host, the system SHOULD also + depart from the all-routers IP multicast group on all interfaces on + which the router supports IP multicast (whether or not they had been + advertising interfaces). In addition, the host MUST ensure that + subsequent Neighbor Advertisement messages sent from the interface + have the Router flag set to zero. + + Note that system management may disable a router's IP forwarding + capability (i.e., changing the system from being a router to being a + host), a step that does not necessarily imply that the router's + interfaces stop being advertising interfaces. In such cases, + subsequent Router Advertisements MUST set the Router Lifetime field + to zero. + +6.2.6. Processing Router Solicitations + + A host MUST silently discard any received Router Solicitation + messages. + + In addition to sending periodic, unsolicited advertisements, a router + sends advertisements in response to valid solicitations received on + an advertising interface. A router MAY choose to unicast the + response directly to the soliciting host's address (if the + solicitation's source address is not the unspecified address), but + the usual case is to multicast the response to the all-nodes group. + In the latter case, the interface's interval timer is reset to a new + random value, as if an unsolicited advertisement had just been sent + (see Section 6.2.4). + + In all cases, Router Advertisements sent in response to a Router + Solicitation MUST be delayed by a random time between 0 and + MAX_RA_DELAY_TIME seconds. (If a single advertisement is sent in + response to multiple solicitations, the delay is relative to the + first solicitation.) In addition, consecutive Router Advertisements + sent to the all-nodes multicast address MUST be rate limited to no + more than one advertisement every MIN_DELAY_BETWEEN_RAS seconds. + + + + + + + + + + +Narten, et al. Standards Track [Page 48] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + A router might process Router Solicitations as follows: + + - Upon receipt of a Router Solicitation, compute a random delay + within the range 0 through MAX_RA_DELAY_TIME. If the computed + value corresponds to a time later than the time the next multicast + Router Advertisement is scheduled to be sent, ignore the random + delay and send the advertisement at the already-scheduled time. + + - If the router sent a multicast Router Advertisement (solicited or + unsolicited) within the last MIN_DELAY_BETWEEN_RAS seconds, + schedule the advertisement to be sent at a time corresponding to + MIN_DELAY_BETWEEN_RAS plus the random value after the previous + advertisement was sent. This ensures that the multicast Router + Advertisements are rate limited. + + - Otherwise, schedule the sending of a Router Advertisement at the + time given by the random value. + + Note that a router is permitted to send multicast Router + Advertisements more frequently than indicated by the + MinRtrAdvInterval configuration variable so long as the more frequent + advertisements are responses to Router Solicitations. In all cases, + however, unsolicited multicast advertisements MUST NOT be sent more + frequently than indicated by MinRtrAdvInterval. + + Router Solicitations in which the Source Address is the unspecified + address MUST NOT update the router's Neighbor Cache; solicitations + with a proper source address update the Neighbor Cache as follows. + If the router already has a Neighbor Cache entry for the + solicitation's sender, the solicitation contains a Source Link-Layer + Address option, and the received link-layer address differs from that + already in the cache, then the link-layer address SHOULD be updated + in the appropriate Neighbor Cache entry, and its reachability state + MUST also be set to STALE. If there is no existing Neighbor Cache + entry for the solicitation's sender, the router creates one, installs + the link- layer address and sets its reachability state to STALE as + specified in Section 7.3.3. If there is no existing Neighbor Cache + entry and no Source Link-Layer Address option was present in the + solicitation, the router may respond with either a multicast or a + unicast router advertisement. Whether or not a Source Link-Layer + Address option is provided, if a Neighbor Cache entry for the + solicitation's sender exists (or is created) the entry's IsRouter + flag MUST be set to FALSE. + + + + + + + + +Narten, et al. Standards Track [Page 49] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + +6.2.7. Router Advertisement Consistency + + Routers SHOULD inspect valid Router Advertisements sent by other + routers and verify that the routers are advertising consistent + information on a link. Detected inconsistencies indicate that one or + more routers might be misconfigured and SHOULD be logged to system or + network management. The minimum set of information to check + includes: + + - Cur Hop Limit values (except for the unspecified value of zero + other inconsistencies SHOULD be logged to system network + management). + + - Values of the M or O flags. + + - Reachable Time values (except for the unspecified value of zero). + + - Retrans Timer values (except for the unspecified value of zero). + + - Values in the MTU options. + + - Preferred and Valid Lifetimes for the same prefix. If + AdvPreferredLifetime and/or AdvValidLifetime decrement in real + time as specified in Section 6.2.1 then the comparison of the + lifetimes cannot compare the content of the fields in the Router + Advertisement, but must instead compare the time at which the + prefix will become deprecated and invalidated, respectively. Due + to link propagation delays and potentially poorly synchronized + clocks between the routers such comparison SHOULD allow some time + skew. + + Note that it is not an error for different routers to advertise + different sets of prefixes. Also, some routers might leave some + fields as unspecified, i.e., with the value zero, while other routers + specify values. The logging of errors SHOULD be restricted to + conflicting information that causes hosts to switch from one value to + another with each received advertisement. + + Any other action on reception of Router Advertisement messages by a + router is beyond the scope of this document. + +6.2.8. Link-local Address Change + + The link-local address on a router should rarely change, if ever. + Nodes receiving Neighbor Discovery messages use the source address to + identify the sender. If multiple packets from the same router + contain different source addresses, nodes will assume they come from + different routers, leading to undesirable behavior. For example, a + + + +Narten, et al. Standards Track [Page 50] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + node will ignore Redirect messages that are believed to have been + sent by a router other than the current first-hop router. Thus, the + source address used in Router Advertisements sent by a particular + router must be identical to the target address in a Redirect message + when redirecting to that router. + + Using the link-local address to uniquely identify routers on the link + has the benefit that the address a router is known by should not + change when a site renumbers. + + If a router changes the link-local address for one of its interfaces, + it SHOULD inform hosts of this change. The router SHOULD multicast a + few Router Advertisements from the old link-local address with the + Router Lifetime field set to zero and also multicast a few Router + Advertisements from the new link-local address. The overall effect + should be the same as if one interface ceases being an advertising + interface, and a different one starts being an advertising interface. + +6.3. Host Specification + +6.3.1. Host Configuration Variables + + None. + +6.3.2. Host Variables + + A host maintains certain Neighbor-Discovery-related variables in + addition to the data structures defined in Section 5.1. The specific + variable names are used for demonstration purposes only, and an + implementation is not required to have them, so long as its external + behavior is consistent with that described in this document. + + These variables have default values that are overridden by + information received in Router Advertisement messages. The default + values are used when there is no router on the link or when all + received Router Advertisements have left a particular value + unspecified. + + The default values in this specification may be overridden by + specific documents that describe how IP operates over different link + layers. This rule allows Neighbor Discovery to operate over links + with widely varying performance characteristics. + + + + + + + + + +Narten, et al. Standards Track [Page 51] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + For each interface: + + LinkMTU The MTU of the link. + Default: The valued defined in the specific + document that describes how IPv6 operates over + the particular link layer (e.g., [IPv6-ETHER]). + + CurHopLimit The default hop limit to be used when sending IP + packets. + + Default: The value specified in the "Assigned + Numbers" [ASSIGNED] that was in effect at the + time of implementation. + + BaseReachableTime + A base value used for computing the random + ReachableTime value. + + Default: REACHABLE_TIME milliseconds. + + ReachableTime The time a neighbor is considered reachable after + receiving a reachability confirmation. + + This value should be a uniformly distributed + random value between MIN_RANDOM_FACTOR and + MAX_RANDOM_FACTOR times BaseReachableTime + milliseconds. A new random value should be + calculated when BaseReachableTime changes (due to + Router Advertisements) or at least every few + hours even if no Router Advertisements are + received. + + RetransTimer The time between retransmissions of Neighbor + Solicitation messages to a neighbor when + resolving the address or when probing the + reachability of a neighbor. + + Default: RETRANS_TIMER milliseconds + +6.3.3. Interface Initialization + + The host joins the all-nodes multicast address on all multicast- + capable interfaces. + + + + + + + + +Narten, et al. Standards Track [Page 52] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + +6.3.4. Processing Received Router Advertisements + + When multiple routers are present, the information advertised + collectively by all routers may be a superset of the information + contained in a single Router Advertisement. Moreover, information + may also be obtained through other dynamic means like DHCPv6. Hosts + accept the union of all received information; the receipt of a Router + Advertisement MUST NOT invalidate all information received in a + previous advertisement or from another source. However, when + received information for a specific parameter (e.g., Link MTU) or + option (e.g., Lifetime on a specific Prefix) differs from information + received earlier, and the parameter/option can only have one value, + the most recently received information is considered authoritative. + + A Router Advertisement field (e.g., Cur Hop Limit, Reachable Time, + and Retrans Timer) may contain a value denoting that it is + unspecified. In such cases, the parameter should be ignored and the + host should continue using whatever value it is already using. In + particular, a host MUST NOT interpret the unspecified value as + meaning change back to the default value that was in use before the + first Router Advertisement was received. This rule prevents hosts + from continually changing an internal variable when one router + advertises a specific value, but other routers advertise the + unspecified value. + + On receipt of a valid Router Advertisement, a host extracts the + source address of the packet and does the following: + + - If the address is not already present in the host's Default + Router List, and the advertisement's Router Lifetime is non- + zero, create a new entry in the list, and initialize its + invalidation timer value from the advertisement's Router + Lifetime field. + + - If the address is already present in the host's Default Router + List as a result of a previously received advertisement, reset + its invalidation timer to the Router Lifetime value in the newly + received advertisement. + + - If the address is already present in the host's Default Router + List and the received Router Lifetime value is zero, immediately + time-out the entry as specified in Section 6.3.5. + + To limit the storage needed for the Default Router List, a host MAY + choose not to store all of the router addresses discovered via + advertisements. However, a host MUST retain at least two router + addresses and SHOULD retain more. Default router selections are made + whenever communication to a destination appears to be failing. Thus, + + + +Narten, et al. Standards Track [Page 53] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + the more routers on the list, the more likely an alternative working + router can be found quickly (e.g., without having to wait for the + next advertisement to arrive). + + If the received Cur Hop Limit value is non-zero, the host SHOULD set + its CurHopLimit variable to the received value. + + If the received Reachable Time value is non-zero, the host SHOULD set + its BaseReachableTime variable to the received value. If the new + value differs from the previous value, the host SHOULD re-compute a + new random ReachableTime value. ReachableTime is computed as a + uniformly distributed random value between MIN_RANDOM_FACTOR and + MAX_RANDOM_FACTOR times the BaseReachableTime. Using a random + component eliminates the possibility that Neighbor Unreachability + Detection messages will synchronize with each other. + + In most cases, the advertised Reachable Time value will be the same + in consecutive Router Advertisements, and a host's BaseReachableTime + rarely changes. In such cases, an implementation SHOULD ensure that + a new random value gets re-computed at least once every few hours. + + The RetransTimer variable SHOULD be copied from the Retrans Timer + field, if the received value is non-zero. + + After extracting information from the fixed part of the Router + Advertisement message, the advertisement is scanned for valid + options. If the advertisement contains a Source Link-Layer Address + option, the link-layer address SHOULD be recorded in the Neighbor + Cache entry for the router (creating an entry if necessary) and the + IsRouter flag in the Neighbor Cache entry MUST be set to TRUE. If no + Source Link-Layer Address is included, but a corresponding Neighbor + Cache entry exists, its IsRouter flag MUST be set to TRUE. The + IsRouter flag is used by Neighbor Unreachability Detection to + determine when a router changes to being a host (i.e., no longer + capable of forwarding packets). If a Neighbor Cache entry is created + for the router, its reachability state MUST be set to STALE as + specified in Section 7.3.3. If a cache entry already exists and is + updated with a different link-layer address, the reachability state + MUST also be set to STALE. + + If the MTU option is present, hosts SHOULD copy the option's value + into LinkMTU so long as the value is greater than or equal to the + minimum link MTU [IPv6] and does not exceed the maximum LinkMTU value + specified in the link-type-specific document (e.g., [IPv6-ETHER]). + + Prefix Information options that have the "on-link" (L) flag set + indicate a prefix identifying a range of addresses that should be + considered on-link. Note, however, that a Prefix Information option + + + +Narten, et al. Standards Track [Page 54] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + with the on-link flag set to zero conveys no information concerning + on-link determination and MUST NOT be interpreted to mean that + addresses covered by the prefix are off-link. The only way to cancel + a previous on-link indication is to advertise that prefix with the + L-bit set and the Lifetime set to zero. The default behavior (see + Section 5.2) when sending a packet to an address for which no + information is known about the on-link status of the address is to + forward the packet to a default router; the reception of a Prefix + Information option with the "on-link" (L) flag set to zero does not + change this behavior. The reasons for an address being treated as + on-link is specified in the definition of "on-link" in Section 2.1. + Prefixes with the on-link flag set to zero would normally have the + autonomous flag set and be used by [ADDRCONF]. + + For each Prefix Information option with the on-link flag set, a host + does the following: + + - If the prefix is the link-local prefix, silently ignore the + Prefix Information option. + + - If the prefix is not already present in the Prefix List, and the + Prefix Information option's Valid Lifetime field is non-zero, + create a new entry for the prefix and initialize its + invalidation timer to the Valid Lifetime value in the Prefix + Information option. + + - If the prefix is already present in the host's Prefix List as + the result of a previously received advertisement, reset its + invalidation timer to the Valid Lifetime value in the Prefix + Information option. If the new Lifetime value is zero, time-out + the prefix immediately (see Section 6.3.5). + + - If the Prefix Information option's Valid Lifetime field is zero, + and the prefix is not present in the host's Prefix List, + silently ignore the option. + + Stateless address autoconfiguration [ADDRCONF] may in some + circumstances use a larger Valid Lifetime of a prefix or ignore it + completely in order to prevent a particular denial-of-service attack. + However, since the effect of the same denial of service targeted at + the on-link prefix list is not catastrophic (hosts would send packets + to a default router and receive a redirect rather than sending + packets directly to a neighbor), the Neighbor Discovery protocol does + not impose such a check on the prefix lifetime values. Similarly, + [ADDRCONF] may impose certain restrictions on the prefix length for + address configuration purposes. Therefore, the prefix might be + rejected by [ADDRCONF] implementation in the host. However, the + + + + +Narten, et al. Standards Track [Page 55] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + prefix length is still valid for on-link determination when combined + with other flags in the prefix option. + + Note: Implementations can choose to process the on-link aspects of + the prefixes separately from the stateless address + autoconfiguration aspects of the prefixes by, e.g., passing a copy + of each valid Router Advertisement message to both an "on-link" + and an "addrconf" function. Each function can then operate + independently on the prefixes that have the appropriate flag set. + +6.3.5. Timing out Prefixes and Default Routers + + Whenever the invalidation timer expires for a Prefix List entry, that + entry is discarded. No existing Destination Cache entries need be + updated, however. Should a reachability problem arise with an + existing Neighbor Cache entry, Neighbor Unreachability Detection will + perform any needed recovery. + + Whenever the Lifetime of an entry in the Default Router List expires, + that entry is discarded. When removing a router from the Default + Router list, the node MUST update the Destination Cache in such a way + that all entries using the router perform next-hop determination + again rather than continue sending traffic to the (deleted) router. + +6.3.6. Default Router Selection + + The algorithm for selecting a router depends in part on whether or + not a router is known to be reachable. The exact details of how a + node keeps track of a neighbor's reachability state are covered in + Section 7.3. The algorithm for selecting a default router is invoked + during next-hop determination when no Destination Cache entry exists + for an off-link destination or when communication through an existing + router appears to be failing. Under normal conditions, a router + would be selected the first time traffic is sent to a destination, + with subsequent traffic for that destination using the same router as + indicated in the Destination Cache modulo any changes to the + Destination Cache caused by Redirect messages. + + The policy for selecting routers from the Default Router List is as + follows: + + 1) Routers that are reachable or probably reachable (i.e., in any + state other than INCOMPLETE) SHOULD be preferred over routers + whose reachability is unknown or suspect (i.e., in the + INCOMPLETE state, or for which no Neighbor Cache entry exists). + Further implementation hints on default router selection when + multiple equivalent routers are available are discussed in + [LD-SHRE]. + + + +Narten, et al. Standards Track [Page 56] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + 2) When no routers on the list are known to be reachable or + probably reachable, routers SHOULD be selected in a round-robin + fashion, so that subsequent requests for a default router do not + return the same router until all other routers have been + selected. + + Cycling through the router list in this case ensures that all + available routers are actively probed by the Neighbor + Unreachability Detection algorithm. A request for a default + router is made in conjunction with the sending of a packet to a + router, and the selected router will be probed for reachability + as a side effect. + +6.3.7. Sending Router Solicitations + + When an interface becomes enabled, a host may be unwilling to wait + for the next unsolicited Router Advertisement to locate default + routers or learn prefixes. To obtain Router Advertisements quickly, + a host SHOULD transmit up to MAX_RTR_SOLICITATIONS Router + Solicitation messages, each separated by at least + RTR_SOLICITATION_INTERVAL seconds. Router Solicitations may be sent + after any of the following events: + + - The interface is initialized at system startup time. + + - The interface is reinitialized after a temporary interface + failure or after being temporarily disabled by system + management. + + - The system changes from being a router to being a host, by + having its IP forwarding capability turned off by system + management. + + - The host attaches to a link for the first time. + + - The host re-attaches to a link after being detached for some + time. + + A host sends Router Solicitations to the all-routers multicast + address. The IP source address is set to either one of the + interface's unicast addresses or the unspecified address. The Source + Link-Layer Address option SHOULD be set to the host's link-layer + address, if the IP source address is not the unspecified address. + + Before a host sends an initial solicitation, it SHOULD delay the + transmission for a random amount of time between 0 and + MAX_RTR_SOLICITATION_DELAY. This serves to alleviate congestion when + many hosts start up on a link at the same time, such as might happen + + + +Narten, et al. Standards Track [Page 57] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + after recovery from a power failure. If a host has already performed + a random delay since the interface became (re)enabled (e.g., as part + of Duplicate Address Detection [ADDRCONF]), there is no need to delay + again before sending the first Router Solicitation message. + + In some cases, the random delay MAY be omitted if necessary. For + instance, a mobile node, using [MIPv6], moving to a new link would + need to discover such movement as soon as possible to minimize the + amount of packet losses resulting from the change in its topological + movement. Router Solicitations provide a useful tool for movement + detection in Mobile IPv6 as they allow mobile nodes to determine + movement to new links. Hence, if a mobile node received link-layer + information indicating that movement might have taken place, it MAY + send a Router Solicitation immediately, without random delays. The + strength of such indications should be assessed by the mobile node's + implementation depending on the level of certainty of the link-layer + hints, and it is outside the scope of this specification. Note that + using this mechanism inappropriately (e.g., based on weak or + transient indications) may result in Router Solicitation storms. + Furthermore, simultaneous mobility of a large number of mobile nodes + that use this mechanism can result in a large number of solicitations + sent simultaneously. + + Once the host sends a Router Solicitation, and receives a valid + Router Advertisement with a non-zero Router Lifetime, the host MUST + desist from sending additional solicitations on that interface, until + the next time one of the above events occurs. Moreover, a host + SHOULD send at least one solicitation in the case where an + advertisement is received prior to having sent a solicitation. + Responses to solicited advertisements may contain more information + than unsolicited advertisements. + + If a host sends MAX_RTR_SOLICITATIONS solicitations, and receives no + Router Advertisements after having waited MAX_RTR_SOLICITATION_DELAY + seconds after sending the last solicitation, the host concludes that + there are no routers on the link for the purpose of [ADDRCONF]. + However, the host continues to receive and process Router + Advertisements messages in the event that routers appear on the link. + + + + + + + + + + + + + +Narten, et al. Standards Track [Page 58] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + +7. Address Resolution and Neighbor Unreachability Detection + + This section describes the functions related to Neighbor Solicitation + and Neighbor Advertisement messages and includes descriptions of + address resolution and the Neighbor Unreachability Detection + algorithm. + + Neighbor Solicitation and Advertisement messages are also used for + Duplicate Address Detection as specified by [ADDRCONF]. In + particular, Duplicate Address Detection sends Neighbor Solicitation + messages with an unspecified source address targeting its own + "tentative" address. Such messages trigger nodes already using the + address to respond with a multicast Neighbor Advertisement indicating + that the address is in use. + +7.1. Message Validation + +7.1.1. Validation of Neighbor Solicitations + + A node MUST silently discard any received Neighbor Solicitation + messages that do not satisfy all of the following validity checks: + + - The IP Hop Limit field has a value of 255, i.e., the packet + could not possibly have been forwarded by a router. + + - ICMP Checksum is valid. + + - ICMP Code is 0. + + - ICMP length (derived from the IP length) is 24 or more octets. + + - Target Address is not a multicast address. + + - All included options have a length that is greater than zero. + + - If the IP source address is the unspecified address, the IP + destination address is a solicited-node multicast address. + + - If the IP source address is the unspecified address, there is no + source link-layer address option in the message. + + The contents of the Reserved field, and of any unrecognized options, + MUST be ignored. Future, backward-compatible changes to the protocol + may specify the contents of the Reserved field or add new options; + backward-incompatible changes may use different Code values. + + + + + + +Narten, et al. Standards Track [Page 59] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + The contents of any defined options that are not specified to be used + with Neighbor Solicitation messages MUST be ignored and the packet + processed as normal. The only defined option that may appear is the + Source Link-Layer Address option. + + A Neighbor Solicitation that passes the validity checks is called a + "valid solicitation". + +7.1.2. Validation of Neighbor Advertisements + + A node MUST silently discard any received Neighbor Advertisement + messages that do not satisfy all of the following validity checks: + + - The IP Hop Limit field has a value of 255, i.e., the packet + could not possibly have been forwarded by a router. + + - ICMP Checksum is valid. + + - ICMP Code is 0. + + - ICMP length (derived from the IP length) is 24 or more octets. + + - Target Address is not a multicast address. + + - If the IP Destination Address is a multicast address the + Solicited flag is zero. + + - All included options have a length that is greater than zero. + + The contents of the Reserved field, and of any unrecognized options, + MUST be ignored. Future, backward-compatible changes to the protocol + may specify the contents of the Reserved field or add new options; + backward-incompatible changes may use different Code values. + + The contents of any defined options that are not specified to be used + with Neighbor Advertisement messages MUST be ignored and the packet + processed as normal. The only defined option that may appear is the + Target Link-Layer Address option. + + A Neighbor Advertisements that passes the validity checks is called a + "valid advertisement". + +7.2. Address Resolution + + Address resolution is the process through which a node determines the + link-layer address of a neighbor given only its IP address. Address + resolution is performed only on addresses that are determined to be + on-link and for which the sender does not know the corresponding + + + +Narten, et al. Standards Track [Page 60] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + link-layer address (see Section 5.2). Address resolution is never + performed on multicast addresses. + + It is possible that a host may receive a solicitation, a router + advertisement, or a Redirect message without a link-layer address + option included. These messages MUST NOT create or update neighbor + cache entries, except with respect to the IsRouter flag as specified + in Sections 6.3.4 and 7.2.5. If a Neighbor Cache entry does not + exist for the source of such a message, Address Resolution will be + required before unicast communications with that address can begin. + This is particularly relevant for unicast responses to solicitations + where an additional packet exchange is required for advertisement + delivery. + +7.2.1. Interface Initialization + + When a multicast-capable interface becomes enabled, the node MUST + join the all-nodes multicast address on that interface, as well as + the solicited-node multicast address corresponding to each of the IP + addresses assigned to the interface. + + The set of addresses assigned to an interface may change over time. + New addresses might be added and old addresses might be removed + [ADDRCONF]. In such cases the node MUST join and leave the + solicited-node multicast address corresponding to the new and old + addresses, respectively. Joining the solicited-node multicast + address is done using a Multicast Listener Discovery such as [MLD] or + [MLDv2] protocols. Note that multiple unicast addresses may map into + the same solicited-node multicast address; a node MUST NOT leave the + solicited-node multicast group until all assigned addresses + corresponding to that multicast address have been removed. + +7.2.2. Sending Neighbor Solicitations + + When a node has a unicast packet to send to a neighbor, but does not + know the neighbor's link-layer address, it performs address + resolution. For multicast-capable interfaces, this entails creating + a Neighbor Cache entry in the INCOMPLETE state and transmitting a + Neighbor Solicitation message targeted at the neighbor. The + solicitation is sent to the solicited-node multicast address + corresponding to the target address. + + If the source address of the packet prompting the solicitation is the + same as one of the addresses assigned to the outgoing interface, that + address SHOULD be placed in the IP Source Address of the outgoing + solicitation. Otherwise, any one of the addresses assigned to the + interface should be used. Using the prompting packet's source + address when possible ensures that the recipient of the Neighbor + + + +Narten, et al. Standards Track [Page 61] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + Solicitation installs in its Neighbor Cache the IP address that is + highly likely to be used in subsequent return traffic belonging to + the prompting packet's "connection". + + If the solicitation is being sent to a solicited-node multicast + address, the sender MUST include its link-layer address (if it has + one) as a Source Link-Layer Address option. Otherwise, the sender + SHOULD include its link-layer address (if it has one) as a Source + Link-Layer Address option. Including the source link-layer address + in a multicast solicitation is required to give the target an address + to which it can send the Neighbor Advertisement. On unicast + solicitations, an implementation MAY omit the Source Link-Layer + Address option. The assumption here is that if the sender has a + peer's link-layer address in its cache, there is a high probability + that the peer will also have an entry in its cache for the sender. + Consequently, it need not be sent. + + While waiting for address resolution to complete, the sender MUST, + for each neighbor, retain a small queue of packets waiting for + address resolution to complete. The queue MUST hold at least one + packet, and MAY contain more. However, the number of queued packets + per neighbor SHOULD be limited to some small value. When a queue + overflows, the new arrival SHOULD replace the oldest entry. Once + address resolution completes, the node transmits any queued packets. + + While awaiting a response, the sender SHOULD retransmit Neighbor + Solicitation messages approximately every RetransTimer milliseconds, + even in the absence of additional traffic to the neighbor. + Retransmissions MUST be rate-limited to at most one solicitation per + neighbor every RetransTimer milliseconds. + + If no Neighbor Advertisement is received after MAX_MULTICAST_SOLICIT + solicitations, address resolution has failed. The sender MUST return + ICMP destination unreachable indications with code 3 (Address + Unreachable) for each packet queued awaiting address resolution. + +7.2.3. Receipt of Neighbor Solicitations + + A valid Neighbor Solicitation that does not meet any of the following + requirements MUST be silently discarded: + + - The Target Address is a "valid" unicast or anycast address + assigned to the receiving interface [ADDRCONF], + + - The Target Address is a unicast or anycast address for which the + node is offering proxy service, or + + + + + +Narten, et al. Standards Track [Page 62] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + - The Target Address is a "tentative" address on which Duplicate + Address Detection is being performed [ADDRCONF]. + + If the Target Address is tentative, the Neighbor Solicitation should + be processed as described in [ADDRCONF]. Otherwise, the following + description applies. If the Source Address is not the unspecified + address and, on link layers that have addresses, the solicitation + includes a Source Link-Layer Address option, then the recipient + SHOULD create or update the Neighbor Cache entry for the IP Source + Address of the solicitation. If an entry does not already exist, the + node SHOULD create a new one and set its reachability state to STALE + as specified in Section 7.3.3. If an entry already exists, and the + cached link-layer address differs from the one in the received Source + Link-Layer option, the cached address should be replaced by the + received address, and the entry's reachability state MUST be set to + STALE. + + If a Neighbor Cache entry is created, the IsRouter flag SHOULD be set + to FALSE. This will be the case even if the Neighbor Solicitation is + sent by a router since the Neighbor Solicitation messages do not + contain an indication of whether or not the sender is a router. In + the event that the sender is a router, subsequent Neighbor + Advertisement or Router Advertisement messages will set the correct + IsRouter value. If a Neighbor Cache entry already exists, its + IsRouter flag MUST NOT be modified. + + If the Source Address is the unspecified address, the node MUST NOT + create or update the Neighbor Cache entry. + + After any updates to the Neighbor Cache, the node sends a Neighbor + Advertisement response as described in the next section. + +7.2.4. Sending Solicited Neighbor Advertisements + + A node sends a Neighbor Advertisement in response to a valid Neighbor + Solicitation targeting one of the node's assigned addresses. The + Target Address of the advertisement is copied from the Target Address + of the solicitation. If the solicitation's IP Destination Address is + not a multicast address, the Target Link-Layer Address option MAY be + omitted; the neighboring node's cached value must already be current + in order for the solicitation to have been received. If the + solicitation's IP Destination Address is a multicast address, the + Target Link-Layer option MUST be included in the advertisement. + Furthermore, if the node is a router, it MUST set the Router flag to + one; otherwise, it MUST set the flag to zero. + + + + + + +Narten, et al. Standards Track [Page 63] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + If the Target Address is either an anycast address or a unicast + address for which the node is providing proxy service, or the Target + Link-Layer Address option is not included, the Override flag SHOULD + be set to zero. Otherwise, the Override flag SHOULD be set to one. + Proper setting of the Override flag ensures that nodes give + preference to non-proxy advertisements, even when received after + proxy advertisements, and also ensures that the first advertisement + for an anycast address "wins". + + If the source of the solicitation is the unspecified address, the + node MUST set the Solicited flag to zero and multicast the + advertisement to the all-nodes address. Otherwise, the node MUST set + the Solicited flag to one and unicast the advertisement to the Source + Address of the solicitation. + + If the Target Address is an anycast address, the sender SHOULD delay + sending a response for a random time between 0 and + MAX_ANYCAST_DELAY_TIME seconds. + + Because unicast Neighbor Solicitations are not required to include a + Source Link-Layer Address, it is possible that a node sending a + solicited Neighbor Advertisement does not have a corresponding link- + layer address for its neighbor in its Neighbor Cache. In such + situations, a node will first have to use Neighbor Discovery to + determine the link-layer address of its neighbor (i.e., send out a + multicast Neighbor Solicitation). + +7.2.5. Receipt of Neighbor Advertisements + + When a valid Neighbor Advertisement is received (either solicited or + unsolicited), the Neighbor Cache is searched for the target's entry. + If no entry exists, the advertisement SHOULD be silently discarded. + There is no need to create an entry if none exists, since the + recipient has apparently not initiated any communication with the + target. + + Once the appropriate Neighbor Cache entry has been located, the + specific actions taken depend on the state of the Neighbor Cache + entry, the flags in the advertisement, and the actual link-layer + address supplied. + + If the target's Neighbor Cache entry is in the INCOMPLETE state when + the advertisement is received, one of two things happens. If the + link layer has addresses and no Target Link-Layer Address option is + included, the receiving node SHOULD silently discard the received + advertisement. Otherwise, the receiving node performs the following + steps: + + + + +Narten, et al. Standards Track [Page 64] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + - It records the link-layer address in the Neighbor Cache entry. + + - If the advertisement's Solicited flag is set, the state of the + entry is set to REACHABLE; otherwise, it is set to STALE. + + - It sets the IsRouter flag in the cache entry based on the Router + flag in the received advertisement. + + - It sends any packets queued for the neighbor awaiting address + resolution. + + Note that the Override flag is ignored if the entry is in the + INCOMPLETE state. + + If the target's Neighbor Cache entry is in any state other than + INCOMPLETE when the advertisement is received, the following actions + take place: + + I. If the Override flag is clear and the supplied link-layer address + differs from that in the cache, then one of two actions takes + place: + a. If the state of the entry is REACHABLE, set it to STALE, but + do not update the entry in any other way. + b. Otherwise, the received advertisement should be ignored and + MUST NOT update the cache. + + II. If the Override flag is set, or the supplied link-layer address + is the same as that in the cache, or no Target Link-Layer Address + option was supplied, the received advertisement MUST update the + Neighbor Cache entry as follows: + + - The link-layer address in the Target Link-Layer Address option + MUST be inserted in the cache (if one is supplied and differs + from the already recorded address). + + - If the Solicited flag is set, the state of the entry MUST be + set to REACHABLE. If the Solicited flag is zero and the link- + layer address was updated with a different address, the state + MUST be set to STALE. Otherwise, the entry's state remains + unchanged. + + An advertisement's Solicited flag should only be set if the + advertisement is a response to a Neighbor Solicitation. + Because Neighbor Unreachability Detection Solicitations are + sent to the cached link-layer address, receipt of a solicited + advertisement indicates that the forward path is working. + Receipt of an unsolicited advertisement, however, may indicate + that a neighbor has urgent information to announce (e.g., a + + + +Narten, et al. Standards Track [Page 65] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + changed link-layer address). If the urgent information + indicates a change from what a node is currently using, the + node should verify the reachability of the (new) path when it + sends the next packet. There is no need to update the state + for unsolicited advertisements that do not change the contents + of the cache. + + - The IsRouter flag in the cache entry MUST be set based on the + Router flag in the received advertisement. In those cases + where the IsRouter flag changes from TRUE to FALSE as a result + of this update, the node MUST remove that router from the + Default Router List and update the Destination Cache entries + for all destinations using that neighbor as a router as + specified in Section 7.3.3. This is needed to detect when a + node that is used as a router stops forwarding packets due to + being configured as a host. + + The above rules ensure that the cache is updated either when the + Neighbor Advertisement takes precedence (i.e., the Override flag is + set) or when the Neighbor Advertisement refers to the same link-layer + address that is currently recorded in the cache. If none of the + above apply, the advertisement prompts future Neighbor Unreachability + Detection (if it is not already in progress) by changing the state in + the cache entry. + +7.2.6. Sending Unsolicited Neighbor Advertisements + + In some cases, a node may be able to determine that its link-layer + address has changed (e.g., hot-swap of an interface card) and may + wish to inform its neighbors of the new link-layer address quickly. + In such cases, a node MAY send up to MAX_NEIGHBOR_ADVERTISEMENT + unsolicited Neighbor Advertisement messages to the all-nodes + multicast address. These advertisements MUST be separated by at + least RetransTimer seconds. + + The Target Address field in the unsolicited advertisement is set to + an IP address of the interface, and the Target Link-Layer Address + option is filled with the new link-layer address. The Solicited flag + MUST be set to zero, in order to avoid confusing the Neighbor + Unreachability Detection algorithm. If the node is a router, it MUST + set the Router flag to one; otherwise, it MUST set it to zero. The + Override flag MAY be set to either zero or one. In either case, + neighboring nodes will immediately change the state of their Neighbor + Cache entries for the Target Address to STALE, prompting them to + verify the path for reachability. If the Override flag is set to + one, neighboring nodes will install the new link-layer address in + their caches. Otherwise, they will ignore the new link-layer + address, choosing instead to probe the cached address. + + + +Narten, et al. Standards Track [Page 66] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + A node that has multiple IP addresses assigned to an interface MAY + multicast a separate Neighbor Advertisement for each address. In + such a case, the node SHOULD introduce a small delay between the + sending of each advertisement to reduce the probability of the + advertisements being lost due to congestion. + + A proxy MAY multicast Neighbor Advertisements when its link-layer + address changes or when it is configured (by system management or + other mechanisms) to proxy for an address. If there are multiple + nodes that are providing proxy services for the same set of + addresses, the proxies should provide a mechanism that prevents + multiple proxies from multicasting advertisements for any one + address, in order to reduce the risk of excessive multicast traffic. + This is a requirement on other protocols that need to use proxies for + Neighbor Advertisements. An example of a node that performs proxy + advertisements is the Home Agent specified in [MIPv6]. + + Also, a node belonging to an anycast address MAY multicast + unsolicited Neighbor Advertisements for the anycast address when the + node's link-layer address changes. + + Note that because unsolicited Neighbor Advertisements do not reliably + update caches in all nodes (the advertisements might not be received + by all nodes), they should only be viewed as a performance + optimization to quickly update the caches in most neighbors. The + Neighbor Unreachability Detection algorithm ensures that all nodes + obtain a reachable link-layer address, though the delay may be + slightly longer. + +7.2.7. Anycast Neighbor Advertisements + + From the perspective of Neighbor Discovery, anycast addresses are + treated just like unicast addresses in most cases. Because an + anycast address is syntactically the same as a unicast address, nodes + performing address resolution or Neighbor Unreachability Detection on + an anycast address treat it as if it were a unicast address. No + special processing takes place. + + Nodes that have an anycast address assigned to an interface treat + them exactly the same as if they were unicast addresses with two + exceptions. First, Neighbor Advertisements sent in response to a + Neighbor Solicitation SHOULD be delayed by a random time between 0 + and MAX_ANYCAST_DELAY_TIME to reduce the probability of network + congestion. Second, the Override flag in Neighbor Advertisements + SHOULD be set to 0, so that when multiple advertisements are + received, the first received advertisement is used rather than the + most recently received advertisement. + + + + +Narten, et al. Standards Track [Page 67] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + As with unicast addresses, Neighbor Unreachability Detection ensures + that a node quickly detects when the current binding for an anycast + address becomes invalid. + +7.2.8. Proxy Neighbor Advertisements + + Under limited circumstances, a router MAY proxy for one or more other + nodes, that is, through Neighbor Advertisements indicate that it is + willing to accept packets not explicitly addressed to itself. For + example, a router might accept packets on behalf of a mobile node + that has moved off-link. The mechanisms used by proxy are + essentially the same as the mechanisms used with anycast addresses. + + A proxy MUST join the solicited-node multicast address(es) that + correspond to the IP address(es) assigned to the node for which it is + proxying. This SHOULD be done using a multicast listener discovery + protocol such as [MLD] or [MLDv2]. + + All solicited proxy Neighbor Advertisement messages MUST have the + Override flag set to zero. This ensures that if the node itself is + present on the link, its Neighbor Advertisement (with the Override + flag set to one) will take precedence of any advertisement received + from a proxy. A proxy MAY send unsolicited advertisements with the + Override flag set to one as specified in Section 7.2.6, but doing so + may cause the proxy advertisement to override a valid entry created + by the node itself. + + Finally, when sending a proxy advertisement in response to a Neighbor + Solicitation, the sender should delay its response by a random time + between 0 and MAX_ANYCAST_DELAY_TIME seconds to avoid collisions due + to multiple responses sent by several proxies. However, in some + cases (e.g., Mobile IPv6) where only one proxy is present, such delay + is not necessary. + +7.3. Neighbor Unreachability Detection + + Communication to or through a neighbor may fail for numerous reasons + at any time, including hardware failure, hot-swap of an interface + card, etc. If the destination has failed, no recovery is possible + and communication fails. On the other hand, if it is the path that + has failed, recovery may be possible. Thus, a node actively tracks + the reachability "state" for the neighbors to which it is sending + packets. + + Neighbor Unreachability Detection is used for all paths between hosts + and neighboring nodes, including host-to-host, host-to-router, and + router-to-host communication. Neighbor Unreachability Detection may + also be used between routers, but is not required if an equivalent + + + +Narten, et al. Standards Track [Page 68] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + mechanism is available, for example, as part of the routing + protocols. + + When a path to a neighbor appears to be failing, the specific + recovery procedure depends on how the neighbor is being used. If the + neighbor is the ultimate destination, for example, address resolution + should be performed again. If the neighbor is a router, however, + attempting to switch to another router would be appropriate. The + specific recovery that takes place is covered under next-hop + determination; Neighbor Unreachability Detection signals the need for + next-hop determination by deleting a Neighbor Cache entry. + + Neighbor Unreachability Detection is performed only for neighbors to + which unicast packets are sent; it is not used when sending to + multicast addresses. + +7.3.1. Reachability Confirmation + + A neighbor is considered reachable if the node has recently received + a confirmation that packets sent recently to the neighbor were + received by its IP layer. Positive confirmation can be gathered in + two ways: hints from upper-layer protocols that indicate a connection + is making "forward progress", or receipt of a Neighbor Advertisement + message that is a response to a Neighbor Solicitation message. + + A connection makes "forward progress" if the packets received from a + remote peer can only be arriving if recent packets sent to that peer + are actually reaching it. In TCP, for example, receipt of a (new) + acknowledgment indicates that previously sent data reached the peer. + Likewise, the arrival of new (non-duplicate) data indicates that + earlier acknowledgments are being delivered to the remote peer. If + packets are reaching the peer, they must also be reaching the + sender's next-hop neighbor; thus, "forward progress" is a + confirmation that the next-hop neighbor is reachable. For off-link + destinations, forward progress implies that the first-hop router is + reachable. When available, this upper-layer information SHOULD be + used. + + In some cases (e.g., UDP-based protocols and routers forwarding + packets to hosts), such reachability information may not be readily + available from upper-layer protocols. When no hints are available + and a node is sending packets to a neighbor, the node actively probes + the neighbor using unicast Neighbor Solicitation messages to verify + that the forward path is still working. + + The receipt of a solicited Neighbor Advertisement serves as + reachability confirmation, since advertisements with the Solicited + flag set to one are sent only in response to a Neighbor Solicitation. + + + +Narten, et al. Standards Track [Page 69] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + Receipt of other Neighbor Discovery messages, such as Router + Advertisements and Neighbor Advertisement with the Solicited flag set + to zero, MUST NOT be treated as a reachability confirmation. Receipt + of unsolicited messages only confirms the one-way path from the + sender to the recipient node. In contrast, Neighbor Unreachability + Detection requires that a node keep track of the reachability of the + forward path to a neighbor from its perspective, not the neighbor's + perspective. Note that receipt of a solicited advertisement + indicates that a path is working in both directions. The + solicitation must have reached the neighbor, prompting it to generate + an advertisement. Likewise, receipt of an advertisement indicates + that the path from the sender to the recipient is working. However, + the latter fact is known only to the recipient; the advertisement's + sender has no direct way of knowing that the advertisement it sent + actually reached a neighbor. From the perspective of Neighbor + Unreachability Detection, only the reachability of the forward path + is of interest. + +7.3.2. Neighbor Cache Entry States + + A Neighbor Cache entry can be in one of five states: + + INCOMPLETE Address resolution is being performed on the entry. + Specifically, a Neighbor Solicitation has been sent to + the solicited-node multicast address of the target, + but the corresponding Neighbor Advertisement has not + yet been received. + + REACHABLE Positive confirmation was received within the last + ReachableTime milliseconds that the forward path to + the neighbor was functioning properly. While + REACHABLE, no special action takes place as packets + are sent. + + STALE More than ReachableTime milliseconds have elapsed + since the last positive confirmation was received that + the forward path was functioning properly. While + stale, no action takes place until a packet is sent. + + The STALE state is entered upon receiving an + unsolicited Neighbor Discovery message that updates + the cached link-layer address. Receipt of such a + message does not confirm reachability, and entering + the STALE state ensures reachability is verified + quickly if the entry is actually being used. However, + reachability is not actually verified until the entry + is actually used. + + + + +Narten, et al. Standards Track [Page 70] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + DELAY More than ReachableTime milliseconds have elapsed + since the last positive confirmation was received that + the forward path was functioning properly, and a + packet was sent within the last DELAY_FIRST_PROBE_TIME + seconds. If no reachability confirmation is received + within DELAY_FIRST_PROBE_TIME seconds of entering the + DELAY state, send a Neighbor Solicitation and change + the state to PROBE. + + The DELAY state is an optimization that gives upper- + layer protocols additional time to provide + reachability confirmation in those cases where + ReachableTime milliseconds have passed since the last + confirmation due to lack of recent traffic. Without + this optimization, the opening of a TCP connection + after a traffic lull would initiate probes even though + the subsequent three-way handshake would provide a + reachability confirmation almost immediately. + + PROBE A reachability confirmation is actively sought by + retransmitting Neighbor Solicitations every + RetransTimer milliseconds until a reachability + confirmation is received. + +7.3.3. Node Behavior + + Neighbor Unreachability Detection operates in parallel with the + sending of packets to a neighbor. While reasserting a neighbor's + reachability, a node continues sending packets to that neighbor using + the cached link-layer address. If no traffic is sent to a neighbor, + no probes are sent. + + When a node needs to perform address resolution on a neighboring + address, it creates an entry in the INCOMPLETE state and initiates + address resolution as specified in Section 7.2. If address + resolution fails, the entry SHOULD be deleted, so that subsequent + traffic to that neighbor invokes the next-hop determination procedure + again. Invoking next-hop determination at this point ensures that + alternate default routers are tried. + + When a reachability confirmation is received (either through upper- + layer advice or a solicited Neighbor Advertisement), an entry's state + changes to REACHABLE. The one exception is that upper-layer advice + has no effect on entries in the INCOMPLETE state (e.g., for which no + link-layer address is cached). + + + + + + +Narten, et al. Standards Track [Page 71] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + When ReachableTime milliseconds have passed since receipt of the last + reachability confirmation for a neighbor, the Neighbor Cache entry's + state changes from REACHABLE to STALE. + + Note: An implementation may actually defer changing the state from + REACHABLE to STALE until a packet is sent to the neighbor, i.e., + there need not be an explicit timeout event associated with the + expiration of ReachableTime. + + The first time a node sends a packet to a neighbor whose entry is + STALE, the sender changes the state to DELAY and sets a timer to + expire in DELAY_FIRST_PROBE_TIME seconds. If the entry is still in + the DELAY state when the timer expires, the entry's state changes to + PROBE. If reachability confirmation is received, the entry's state + changes to REACHABLE. + + Upon entering the PROBE state, a node sends a unicast Neighbor + Solicitation message to the neighbor using the cached link-layer + address. While in the PROBE state, a node retransmits Neighbor + Solicitation messages every RetransTimer milliseconds until + reachability confirmation is obtained. Probes are retransmitted even + if no additional packets are sent to the neighbor. If no response is + received after waiting RetransTimer milliseconds after sending the + MAX_UNICAST_SOLICIT solicitations, retransmissions cease and the + entry SHOULD be deleted. Subsequent traffic to that neighbor will + recreate the entry and perform address resolution again. + + Note that all Neighbor Solicitations are rate-limited on a per- + neighbor basis. A node MUST NOT send Neighbor Solicitations to the + same neighbor more frequently than once every RetransTimer + milliseconds. + + A Neighbor Cache entry enters the STALE state when created as a + result of receiving packets other than solicited Neighbor + Advertisements (i.e., Router Solicitations, Router Advertisements, + Redirects, and Neighbor Solicitations). These packets contain the + link-layer address of either the sender or, in the case of Redirect, + the redirection target. However, receipt of these link-layer + addresses does not confirm reachability of the forward-direction path + to that node. Placing a newly created Neighbor Cache entry for which + the link-layer address is known in the STALE state provides assurance + that path failures are detected quickly. In addition, should a + cached link-layer address be modified due to receiving one of the + above messages, the state SHOULD also be set to STALE to provide + prompt verification that the path to the new link-layer address is + working. + + + + + +Narten, et al. Standards Track [Page 72] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + To properly detect the case where a router switches from being a + router to being a host (e.g., if its IP forwarding capability is + turned off by system management), a node MUST compare the Router flag + field in all received Neighbor Advertisement messages with the + IsRouter flag recorded in the Neighbor Cache entry. When a node + detects that a neighbor has changed from being a router to being a + host, the node MUST remove that router from the Default Router List + and update the Destination Cache as described in Section 6.3.5. Note + that a router may not be listed in the Default Router List, even + though a Destination Cache entry is using it (e.g., a host was + redirected to it). In such cases, all Destination Cache entries that + reference the (former) router must perform next-hop determination + again before using the entry. + + In some cases, link-specific information may indicate that a path to + a neighbor has failed (e.g., the resetting of a virtual circuit). In + such cases, link-specific information may be used to purge Neighbor + Cache entries before the Neighbor Unreachability Detection would do + so. However, link-specific information MUST NOT be used to confirm + the reachability of a neighbor; such information does not provide + end-to-end confirmation between neighboring IP layers. + +8. Redirect Function + + This section describes the functions related to the sending and + processing of Redirect messages. + + Redirect messages are sent by routers to redirect a host to a better + first-hop router for a specific destination or to inform hosts that a + destination is in fact a neighbor (i.e., on-link). The latter is + accomplished by having the ICMP Target Address be equal to the ICMP + Destination Address. + + A router MUST be able to determine the link-local address for each of + its neighboring routers in order to ensure that the target address in + a Redirect message identifies the neighbor router by its link-local + address. For static routing, this requirement implies that the next- + hop router's address should be specified using the link-local address + of the router. For dynamic routing, this requirement implies that + all IPv6 routing protocols must somehow exchange the link-local + addresses of neighboring routers. + + + + + + + + + + +Narten, et al. Standards Track [Page 73] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + +8.1. Validation of Redirect Messages + + A host MUST silently discard any received Redirect message that does + not satisfy all of the following validity checks: + + - IP Source Address is a link-local address. Routers must use + their link-local address as the source for Router Advertisement + and Redirect messages so that hosts can uniquely identify + routers. + + - The IP Hop Limit field has a value of 255, i.e., the packet + could not possibly have been forwarded by a router. + + - ICMP Checksum is valid. + + - ICMP Code is 0. + + - ICMP length (derived from the IP length) is 40 or more octets. + + - The IP source address of the Redirect is the same as the current + first-hop router for the specified ICMP Destination Address. + + - The ICMP Destination Address field in the redirect message does + not contain a multicast address. + + - The ICMP Target Address is either a link-local address (when + redirected to a router) or the same as the ICMP Destination + Address (when redirected to the on-link destination). + + - All included options have a length that is greater than zero. + + The contents of the Reserved field, and of any unrecognized options, + MUST be ignored. Future, backward-compatible changes to the protocol + may specify the contents of the Reserved field or add new options; + backward-incompatible changes may use different Code values. + + The contents of any defined options that are not specified to be used + with Redirect messages MUST be ignored and the packet processed as + normal. The only defined options that may appear are the Target + Link-Layer Address option and the Redirected Header option. + + A host MUST NOT consider a redirect invalid just because the Target + Address of the redirect is not covered under one of the link's + prefixes. Part of the semantics of the Redirect message is that the + Target Address is on-link. + + A redirect that passes the validity checks is called a "valid + redirect". + + + +Narten, et al. Standards Track [Page 74] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + +8.2. Router Specification + + A router SHOULD send a redirect message, subject to rate limiting, + whenever it forwards a packet that is not explicitly addressed to + itself (i.e., a packet that is not source routed through the router) + in which: + + - the Source Address field of the packet identifies a neighbor, + and + + - the router determines (by means outside the scope of this + specification) that a better first-hop node resides on the same + link as the sending node for the Destination Address of the + packet being forwarded, and + + - the Destination Address of the packet is not a multicast + address. + + The transmitted redirect packet contains, consistent with the message + format given in Section 4.5: + + - In the Target Address field: the address to which subsequent + packets for the destination should be sent. If the target is a + router, that router's link-local address MUST be used. If the + target is a host, the target address field MUST be set to the + same value as the Destination Address field. + + - In the Destination Address field: the destination address of the + invoking IP packet. + + - In the options: + + o Target Link-Layer Address option: link-layer address of the + target, if known. + + o Redirected Header: as much of the forwarded packet as can + fit without the redirect packet exceeding the minimum MTU + required to support IPv6 as specified in [IPv6]. + + A router MUST limit the rate at which Redirect messages are sent, in + order to limit the bandwidth and processing costs incurred by the + Redirect messages when the source does not correctly respond to the + Redirects, or the source chooses to ignore unauthenticated Redirect + messages. More details on the rate-limiting of ICMP error messages + can be found in [ICMPv6]. + + A router MUST NOT update its routing tables upon receipt of a + Redirect. + + + +Narten, et al. Standards Track [Page 75] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + +8.3. Host Specification + + A host receiving a valid redirect SHOULD update its Destination Cache + accordingly so that subsequent traffic goes to the specified target. + If no Destination Cache entry exists for the destination, an + implementation SHOULD create such an entry. + + If the redirect contains a Target Link-Layer Address option, the host + either creates or updates the Neighbor Cache entry for the target. + In both cases, the cached link-layer address is copied from the + Target Link-Layer Address option. If a Neighbor Cache entry is + created for the target, its reachability state MUST be set to STALE + as specified in Section 7.3.3. If a cache entry already existed and + it is updated with a different link-layer address, its reachability + state MUST also be set to STALE. If the link-layer address is the + same as that already in the cache, the cache entry's state remains + unchanged. + + If the Target and Destination Addresses are the same, the host MUST + treat the Target as on-link. If the Target Address is not the same + as the Destination Address, the host MUST set IsRouter to TRUE for + the target. If the Target and Destination Addresses are the same, + however, one cannot reliably determine whether the Target Address is + a router. Consequently, newly created Neighbor Cache entries should + set the IsRouter flag to FALSE, while existing cache entries should + leave the flag unchanged. If the Target is a router, subsequent + Neighbor Advertisement or Router Advertisement messages will update + IsRouter accordingly. + + Redirect messages apply to all flows that are being sent to a given + destination. That is, upon receipt of a Redirect for a Destination + Address, all Destination Cache entries to that address should be + updated to use the specified next-hop, regardless of the contents of + the Flow Label field that appears in the Redirected Header option. + + A host MUST NOT send Redirect messages. + +9. Extensibility - Option Processing + + Options provide a mechanism for encoding variable length fields, + fields that may appear multiple times in the same packet, or + information that may not appear in all packets. Options can also be + used to add additional functionality to future versions of ND. + + In order to ensure that future extensions properly coexist with + current implementations, all nodes MUST silently ignore any options + they do not recognize in received ND packets and continue processing + the packet. All options specified in this document MUST be + + + +Narten, et al. Standards Track [Page 76] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + recognized. A node MUST NOT ignore valid options just because the ND + message contains unrecognized ones. + + The current set of options is defined in such a way that receivers + can process multiple options in the same packet independently of each + other. In order to maintain these properties, future options SHOULD + follow the simple rule: + + The option MUST NOT depend on the presence or absence of any other + options. The semantics of an option should depend only on the + information in the fixed part of the ND packet and on the + information contained in the option itself. + + Adhering to the above rule has the following benefits: + + 1) Receivers can process options independently of one another. For + example, an implementation can choose to process the Prefix + Information option contained in a Router Advertisement message + in a user-space process while the link-layer address option in + the same message is processed by routines in the kernel. + + 2) Should the number of options cause a packet to exceed a link's + MTU, multiple packets can carry subsets of the options without + any change in semantics. + + 3) Senders MAY send a subset of options in different packets. For + instance, if a prefix's Valid and Preferred Lifetime are high + enough, it might not be necessary to include the Prefix + Information option in every Router Advertisement. In addition, + different routers might send different sets of options. Thus, a + receiver MUST NOT associate any action with the absence of an + option in a particular packet. This protocol specifies that + receivers should only act on the expiration of timers and on the + information that is received in the packets. + + Options in Neighbor Discovery packets can appear in any order; + receivers MUST be prepared to process them independently of their + order. There can also be multiple instances of the same option in a + message (e.g., Prefix Information options). + + If the number of included options in a Router Advertisement causes + the advertisement's size to exceed the link MTU, the router can send + multiple separate advertisements, each containing a subset of the + options. + + The amount of data to include in the Redirected Header option MUST be + limited so that the entire redirect packet does not exceed the + minimum MTU required to support IPv6 as specified in [IPv6]. + + + +Narten, et al. Standards Track [Page 77] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + All options are a multiple of 8 octets of length, ensuring + appropriate alignment without any "pad" options. The fields in the + options (as well as the fields in ND packets) are defined to align on + their natural boundaries (e.g., a 16-bit field is aligned on a 16-bit + boundary) with the exception of the 128-bit IP addresses/prefixes, + which are aligned on a 64-bit boundary. The link-layer address field + contains an uninterpreted octet string; it is aligned on an 8-bit + boundary. + + The size of an ND packet including the IP header is limited to the + link MTU. When adding options to an ND packet, a node MUST NOT + exceed the link MTU. + + Future versions of this protocol may define new option types. + Receivers MUST silently ignore any options they do not recognize and + continue processing the message. + +10. Protocol Constants + + Router constants: + + MAX_INITIAL_RTR_ADVERT_INTERVAL 16 seconds + + MAX_INITIAL_RTR_ADVERTISEMENTS 3 transmissions + + MAX_FINAL_RTR_ADVERTISEMENTS 3 transmissions + + MIN_DELAY_BETWEEN_RAS 3 seconds + + MAX_RA_DELAY_TIME .5 seconds + + Host constants: + + MAX_RTR_SOLICITATION_DELAY 1 second + + RTR_SOLICITATION_INTERVAL 4 seconds + + MAX_RTR_SOLICITATIONS 3 transmissions + + Node constants: + + MAX_MULTICAST_SOLICIT 3 transmissions + + MAX_UNICAST_SOLICIT 3 transmissions + + MAX_ANYCAST_DELAY_TIME 1 second + + MAX_NEIGHBOR_ADVERTISEMENT 3 transmissions + + + +Narten, et al. Standards Track [Page 78] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + REACHABLE_TIME 30,000 milliseconds + + RETRANS_TIMER 1,000 milliseconds + + DELAY_FIRST_PROBE_TIME 5 seconds + + MIN_RANDOM_FACTOR .5 + + MAX_RANDOM_FACTOR 1.5 + + Additional protocol constants are defined with the message formats in + Section 4. + + All protocol constants are subject to change in future revisions of + the protocol. + + The constants in this specification may be overridden by specific + documents that describe how IPv6 operates over different link layers. + This rule allows Neighbor Discovery to operate over links with widely + varying performance characteristics. + +11. Security Considerations + + Neighbor Discovery is subject to attacks that cause IP packets to + flow to unexpected places. Such attacks can be used to cause denial + of service but also allow nodes to intercept and optionally modify + packets destined for other nodes. This section deals with the main + threats related to Neighbor Discovery messages and possible security + mechanisms that can mitigate these threats. + +11.1. Threat Analysis + + This section discusses the main threats associated with Neighbor + Discovery. A more detailed analysis can be found in [PSREQ]. The + main vulnerabilities of the protocol fall under three categories: + + - Denial-of-Service (DoS) attacks. + - Address spoofing attacks. + - Router spoofing attacks. + + An example of denial of service attacks is that a node on the link + that can send packets with an arbitrary IP source address can both + advertise itself as a default router and also send "forged" Router + Advertisement messages that immediately time out all other default + routers as well as all on-link prefixes. An intruder can achieve + this by sending out multiple Router Advertisements, one for each + legitimate router, with the source address set to the address of + another router, the Router Lifetime field set to zero, and the + + + +Narten, et al. Standards Track [Page 79] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + Preferred and Valid lifetimes set to zero for all the prefixes. Such + an attack would cause all packets, for both on-link and off-link + destinations, to go to the rogue router. That router can then + selectively examine, modify, or drop all packets sent on the link. + The Neighbor Unreachability Detection (NUD) will not detect such a + black hole as long as the rogue router politely answers the NUD + probes with a Neighbor Advertisement with the R-bit set. + + It is also possible for any host to launch a DoS attack on another + host by preventing it from configuring an address using [ADDRCONF]. + The protocol does not allow hosts to verify whether the sender of a + Neighbor Advertisement is the true owner of the IP address included + in the message. + + Redirect attacks can also be achieved by any host in order to flood a + victim or steal its traffic. A host can send a Neighbor + Advertisement (in response to a solicitation) that contains its IP + address and a victim's link-layer address in order to flood the + victim with unwanted traffic. Alternatively, the host can send a + Neighbor Advertisement that includes a victim's IP address and its + own link-layer address to overwrite an existing entry in the sender's + destination cache, thereby forcing the sender to forward all of the + victim's traffic to itself. + + The trust model for redirects is the same as in IPv4. A redirect is + accepted only if received from the same router that is currently + being used for that destination. If a host has been redirected to + another node (i.e., the destination is on-link), there is no way to + prevent the target from issuing another redirect to some other + destination. However, this exposure is no worse than it was before + being redirected; the target host, once subverted, could always act + as a hidden router to forward traffic elsewhere. + + The protocol contains no mechanism to determine which neighbors are + authorized to send a particular type of message (e.g., Router + Advertisements); any neighbor, presumably even in the presence of + authentication, can send Router Advertisement messages thereby being + able to cause denial of service. Furthermore, any neighbor can send + proxy Neighbor Advertisements as well as unsolicited Neighbor + Advertisements as a potential denial-of-service attack. + + Many link layers are also subject to different denial-of-service + attacks such as continuously occupying the link in CSMA/CD (Carrier + Sense Multiple Access with Collision Detection) networks (e.g., by + sending packets closely back-to-back or asserting the collision + signal on the link), or originating packets with somebody else's + source MAC address to confuse, e.g., Ethernet switches. On the other + hand, many of the threats discussed in this section are less + + + +Narten, et al. Standards Track [Page 80] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + effective, or non-existent, on point-to-point links, or cellular + links where a host shares a link with only one neighbor, i.e., the + default router. + +11.2. Securing Neighbor Discovery Messages + + The protocol reduces the exposure to the above threats in the absence + of authentication by ignoring ND packets received from off-link + senders. The Hop Limit field of all received packets is verified to + contain 255, the maximum legal value. Because routers decrement the + Hop Limit on all packets they forward, received packets containing a + Hop Limit of 255 must have originated from a neighbor. + + Cryptographic security mechanisms for Neighbor Discovery are outside + the scope of this document and are defined in [SEND]. Alternatively, + IPsec can be used for IP layer authentication [IPv6-SA]. The use of + the Internet Key Exchange (IKE) is not suited for creating dynamic + security associations that can be used to secure address resolution + or neighbor solicitation messages as documented in [ICMPIKE]. + + In some cases, it may be acceptable to use statically configured + security associations with either [IPv6-AUTH] or [IPv6-ESP] to secure + Neighbor Discovery messages. However, it is important to note that + statically configured security associations are not scalable + (especially when considering multicast links) and are therefore + limited to small networks with known hosts. In any case, if either + [IPv6-AUTH] or [IPv6-ESP] is used, ND packets MUST be verified for + the purpose of authentication. Packets that fail authentication + checks MUST be silently discarded. + +12. Renumbering Considerations + + The Neighbor Discovery protocol together with IPv6 Address + Autoconfiguration [ADDRCONF] provides mechanisms to aid in + renumbering -- new prefixes and addresses can be introduced and old + ones can be deprecated and removed. + + The robustness of these mechanisms is based on all the nodes on the + link receiving the Router Advertisement messages in a timely manner. + However, a host might be turned off or be unreachable for an extended + period of time (i.e., a machine is powered down for months after a + project terminates). It is possible to preserve robust renumbering + in such cases, but it does place some constraints on how long + prefixes must be advertised. + + Consider the following example in which a prefix is initially + advertised with a lifetime of 2 months, but on August 1st it is + determined that the prefix needs to be deprecated and removed due to + + + +Narten, et al. Standards Track [Page 81] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + renumbering by September 1st. This can be done by reducing the + advertised lifetime to 1 week starting on August 1st, and as the + cutoff gets closer, the lifetimes can be made shorter until by + September 1st the prefix is advertised with a lifetime of 0. The + point is that, if one or more nodes were unplugged from the link + prior to September 1st, they might still think that the prefix is + valid since the last lifetime they received was 2 months. Thus, if a + node was unplugged on July 31st, it thinks the prefix is valid until + September 30th. If that node is plugged back in prior to September + 30th, it may continue to use the old prefix. The only way to force a + node to stop using a prefix that was previously advertised with a + long lifetime is to have that node receive an advertisement for that + prefix that changes the lifetime downward. The solution in this + example is simple: continue advertising the prefix with a lifetime of + 0 from September 1st until October 1st. + + In general, in order to be robust against nodes that might be + unplugged from the link, it is important to track the furthest into + the future that a particular prefix can be viewed as valid by any + node on the link. The prefix must then be advertised with a 0 + lifetime until that point in the future. This "furthest into the + future" time is simply the maximum, over all Router Advertisements, + of the time the advertisement was sent, plus the prefix's lifetime + contained in the advertisement. + + The above has an important implication on using infinite lifetimes. + If a prefix is advertised with an infinite lifetime, and that prefix + later needs to be renumbered, it is undesirable to continue + advertising that prefix with a zero lifetime forever. Thus, either + infinite lifetimes should be avoided or there must be a limit on how + long of a time a node can be unplugged from the link before it is + plugged back in again. However, it is unclear how the network + administrator can enforce a limit on how long time hosts such as + laptops can be unplugged from the link. + + Network administrators should give serious consideration to using + relatively short lifetimes (i.e., no more than a few weeks). While + it might appear that using long lifetimes would help ensure + robustness, in reality, a host will be unable to communicate in the + absence of properly functioning routers. Such routers will be + sending Router Advertisements that contain appropriate (and current) + prefixes. A host connected to a network that has no functioning + routers is likely to have more serious problems than just a lack of a + valid prefix and address. + + + + + + + +Narten, et al. Standards Track [Page 82] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + The above discussion does not distinguish between the preferred and + valid lifetimes. For all practical purposes, it is probably + sufficient to track the valid lifetime since the preferred lifetime + will not exceed the valid lifetime. + +13. IANA Considerations + + This document does not require any new ICMPv6 types or codes to be + allocated. However, existing ICMPv6 types have been updated to point + to this document instead of RFC 2461. The procedure for the + assignment of ICMPv6 types/codes is described in Section 6 of + [ICMPv6]. + + This document continues to use the following ICMPv6 message types + introduced in RFC 2461 and already assigned by IANA: + + Message name ICMPv6 Type + + Router Solicitation 133 + Router Advertisement 134 + Neighbor Solicitation 135 + Neighbor Advertisement 136 + Redirect 137 + + This document continues to use the following Neighbor Discovery + option types introduced in RFC 2461 and already assigned by IANA: + + Option Name Type + + Source Link-Layer Address 1 + Target Link-Layer Address 2 + Prefix Information 3 + Redirected Header 4 + MTU 5 + + Neighbor Discovery option types are allocated using the following + procedure: + + 1. The IANA should allocate and permanently register new option types + from IETF RFC publication. This is for all RFC types including + standards track, informational, and experimental status that + originate from the IETF and have been approved by the IESG for + publication. + + 2. IETF working groups with working group consensus and area director + approval can request reclaimable Neighbor Discovery option type + assignments from the IANA. The IANA will tag the values as + "reclaimable in future". + + + +Narten, et al. Standards Track [Page 83] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + The "reclaimable in the future" tag will be removed when an RFC is + published documenting the protocol as defined in 1). This will make + the assignment permanent and update the reference on the IANA Web + pages. + + At the point where the option type values are 85% assigned, the IETF + will review the assignments tagged "reclaimable in the future" and + inform the IANA which ones should be reclaimed and reassigned. + + 3. Requests for new option type value assignments from outside the + IETF are only made through the publication of an IETF document, per + 1) above. Note also that documents published as "RFC Editor + contributions" [RFC3667] are not considered to be IETF documents. + +14. References + +14.1. Normative References + + [ADDR-ARCH] Hinden, R. and S. Deering, "IP Version 6 Addressing + Architecture", RFC 4291, February 2006. + + [ICMPv6] Conta, A., Deering, S., and M. Gupta, Ed., "Internet + Control Message Protocol (ICMPv6) for the Internet + Protocol Version 6 (IPv6) Specification", RFC 4443, + March 2006. + + [IPv6] Deering, S. and R. Hinden, "Internet Protocol, Version 6 + (IPv6) Specification", RFC 2460, December 1998. + + [KEYWORDS] Bradner, S., "Key words for use in RFCs to Indicate + Requirement Levels", BCP 14, RFC 2119, March 1997. + +14.2. Informative References + + [ADDRCONF] Thomson, S., Narten, T., and T. Jinmei, "IPv6 Stateless + Address Autoconfiguration", RFC 4862, September 2007. + + [ADDR-SEL] Draves, R., "Default Address Selection for Internet + Protocol version 6 (IPv6)", RFC 3484, February 2003. + + [ARP] Plummer, D., "Ethernet Address Resolution Protocol: Or + Converting Network Protocol Addresses to 48.bit Ethernet + Address for Transmission on Ethernet Hardware", STD 37, + RFC 826, November 1982. + + [ASSIGNED] Reynolds, J., Ed., "Assigned Numbers: RFC 1700 is + Replaced by an On-line Database", RFC 3232, January + 2002. + + + +Narten, et al. Standards Track [Page 84] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + [DHCPv6] Droms, R., Ed., Bound, J., Volz, B., Lemon, T., Perkins, + C., and M. Carney, "Dynamic Host Configuration Protocol + for IPv6 (DHCPv6)", RFC 3315, July 2003. + + [HR-CL] Braden, R., Ed., "Requirements for Internet Hosts - + Communication Layers", STD 3, RFC 1122, October 1989. + + [ICMPIKE] Arkko, J., "Effects of ICMPv6 on IKE", Work in Progress, + March 2003. + + [ICMPv4] Postel, J., "Internet Control Message Protocol", STD 5, + RFC 792, September 1981. + + [IPv6-3GPP] Wasserman, M., Ed., "Recommendations for IPv6 in Third + Generation Partnership Project (3GPP) Standards", RFC + 3314, September 2002. + + [IPv6-CELL] Arkko, J., Kuijpers, G., Soliman, H., Loughney, J., and + J. Wiljakka, "Internet Protocol Version 6 (IPv6) for + Some Second and Third Generation Cellular Hosts", RFC + 3316, April 2003. + + [IPv6-ETHER] Crawford, M., "Transmission of IPv6 Packets over + Ethernet Networks", RFC 2464, December 1998. + + [IPv6-SA] Kent, S. and K. Seo, "Security Architecture for the + Internet Protocol", RFC 4301, December 2005. + + [IPv6-AUTH] Kent, S., "IP Authentication Header", RFC 4302, December + 2005. + + [IPv6-ESP] Kent, S., "IP Encapsulating Security Payload (ESP)", RFC + 4303, December 2005. + + [IPv6-NBMA] Armitage, G., Schulter, P., Jork, M., and G. Harter, + "IPv6 over Non-Broadcast Multiple Access (NBMA) + networks", RFC 2491, January 1999. + + [LD-SHRE] Hinden, R. and D. Thaler, "IPv6 Host-to-Router Load + Sharing", RFC 4311, November 2005. + + [MIPv6] Johnson, D., Perkins, C., and J. Arkko, "Mobility + Support in IPv6", RFC 3775, June 2004. + + [MLD] Deering, S., Fenner, W., and B. Haberman, "Multicast + Listener Discovery (MLD) for IPv6", RFC 2710, October + 1999. + + + + +Narten, et al. Standards Track [Page 85] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + [MLDv2] Vida, R., Ed., and L. Costa, Ed., "Multicast Listener + Discovery Version 2 (MLDv2) for IPv6", RFC 3810, June + 2004. + + [PSREQ] Nikander, P., Ed., Kempf, J., and E. Nordmark, "IPv6 + Neighbor Discovery (ND) Trust Models and Threats", RFC + 3756, May 2004. + + [RAND] Eastlake, D., 3rd, Schiller, J., and S. Crocker, + "Randomness Requirements for Security", BCP 106, RFC + 4086, June 2005. + + [RDISC] Deering, S., Ed., "ICMP Router Discovery Messages", RFC + 1256, September 1991. + + [RFC3667] Bradner, S., "IETF Rights in Contributions", RFC 3667, + February 2004. + + [RTSEL] Draves, R. and D. Thaler, "Default Router Preferences + and More-Specific Routes", RFC 4191, November 2005. + + [SH-MEDIA] Braden, B., Postel, J., and Y. Rekhter, "Internet + Architecture Extensions for Shared Media", RFC 1620, May + 1994. + + [SEND] Arkko, J., Ed., Kempf, J., Zill, B., and P. Nikander, + "SEcure Neighbor Discovery (SEND)", RFC 3971, March + 2005. + + [SYNC] S. Floyd, V. Jacobson, "The Synchronization of Periodic + Routing Messages", IEEE/ACM Transactions on Networking, + April 1994. ftp://ftp.ee.lbl.gov/papers/sync_94.ps.Z + + + + + + + + + + + + + + + + + + + +Narten, et al. Standards Track [Page 86] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + +Appendix A: Multihomed Hosts + + There are a number of complicating issues that arise when Neighbor + Discovery is used by hosts that have multiple interfaces. This + section does not attempt to define the proper operation of multihomed + hosts with regard to Neighbor Discovery. Rather, it identifies + issues that require further study. Implementors are encouraged to + experiment with various approaches to making Neighbor Discovery work + on multihomed hosts and to report their experiences. Further work + related to this problem can be found in [RTSEL]. + + If a multihomed host receives Router Advertisements on all of its + interfaces, it will (probably) have learned on-link prefixes for the + addresses residing on each link. When a packet must be sent through + a router, however, selecting the "wrong" router can result in a + suboptimal or non-functioning path. There are number of issues to + consider: + + 1) In order for a router to send a redirect, it must determine that + the packet it is forwarding originates from a neighbor. The + standard test for this case is to compare the source address of + the packet to the list of on-link prefixes associated with the + interface on which the packet was received. If the originating + host is multihomed, however, the source address it uses may + belong to an interface other than the interface from which it + was sent. In such cases, a router will not send redirects, and + suboptimal routing is likely. In order to be redirected, the + sending host must always send packets out the interface + corresponding to the outgoing packet's source address. Note + that this issue never arises with non-multihomed hosts; they + only have one interface. Additional discussion on this topic + can be found in RFC 1122 under Section 3.3.4.2. + + 2) If the selected first-hop router does not have a route at all + for the destination, it will be unable to deliver the packet. + However, the destination may be reachable through a router on + one of the other interfaces. Neighbor Discovery does not + address this scenario; it does not arise in the non-multihomed + case. + + 3) Even if the first-hop router does have a route for a + destination, there may be a better route via another interface. + No mechanism exists for the multihomed host to detect this + situation. + + If a multihomed host fails to receive Router Advertisements on one or + more of its interfaces, it will not know (in the absence of + configured information) which destinations are on-link on the + + + +Narten, et al. Standards Track [Page 87] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + affected interface(s). This leads to the following problem: If + Router Advertisements are received on some, but not all, interfaces, + a multihomed host could choose to only send packets out on the + interfaces on which it has received Router Advertisements. A key + assumption made here, however, is that routers on those other + interfaces will be able to route packets to the ultimate destination, + even when those destinations reside on the subnet to which the sender + connects, but has no on-link prefix information. Should the + assumption be FALSE, communication would fail. Even if the + assumption holds, packets will traverse a suboptimal path. + +Appendix B: Future Extensions + + Possible extensions for future study are: + + o Using dynamic timers to be able to adapt to links with widely + varying delay. Measuring round-trip times, however, requires + acknowledgments and sequence numbers in order to match received + Neighbor Advertisements with the actual Neighbor Solicitation that + triggered the advertisement. Implementors wishing to experiment + with such a facility could do so in a backwards-compatible way by + defining a new option carrying the necessary information. Nodes + not understanding the option would simply ignore it. + + o Adding capabilities to facilitate the operation over links that + currently require hosts to register with an address resolution + server. This could, for instance, enable routers to ask hosts to + send them periodic unsolicited advertisements. Once again, this + can be added using a new option sent in the Router Advertisements. + + o Adding additional procedures for links where asymmetric and non- + transitive reachability is part of normal operations. Such + procedures might allow hosts and routers to find usable paths on, + e.g., radio links. + + + + + + + + + + + + + + + + + +Narten, et al. Standards Track [Page 88] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + +Appendix C: State Machine for the Reachability State + + This appendix contains a summary of the rules specified in Sections + 7.2 and 7.3. This document does not mandate that implementations + adhere to this model as long as their external behavior is consistent + with that described in this document. + + When performing address resolution and Neighbor Unreachability + Detection the following state transitions apply using the conceptual + model: + + State Event Action New state + + - Packet to send. Create entry. INCOMPLETE + Send multicast NS. + Start retransmit timer + + INCOMPLETE Retransmit timeout, Retransmit NS INCOMPLETE + less than N Start retransmit + retransmissions. timer + + INCOMPLETE Retransmit timeout, Discard entry - + N or more Send ICMP error + retransmissions. + + INCOMPLETE NA, Solicited=0, Record link-layer STALE + Override=any address. Send queued + packets. + + INCOMPLETE NA, Solicited=1, Record link-layer REACHABLE + Override=any address. Send queued + packets. + + INCOMPLETE NA, Solicited=any, Update content of unchanged + Override=any, No IsRouter flag + Link-layer address + + - NS, RS, Redirect - - + No link-layer address + + !INCOMPLETE NA, Solicited=1, - REACHABLE + Override=0 + Same link-layer + address as cached. + + !INCOMPLETE NA, Solicited=any, Update content of unchanged + Override=any, No IsRouter flag. + link-layer address + + + +Narten, et al. Standards Track [Page 89] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + REACHABLE NA, Solicited=1, - STALE + Override=0 + Different link-layer + address than cached. + + STALE, PROBE NA, Solicited=1, - unchanged + Or DELAY Override=0 + Different link-layer + address than cached. + + !INCOMPLETE NA, Solicited=1, Record link-layer REACHABLE + Override=1 address (if + different). + + !INCOMPLETE NA, Solicited=0, - unchanged + Override=0 + + !INCOMPLETE NA, Solicited=0, - unchanged + Override=1 + Same link-layer + address as cached. + + !INCOMPLETE NA, Solicited=0, Record link-layer STALE + Override=1 address. + Different link-layer + address than cached. + + !INCOMPLETE upper-layer reachability - REACHABLE + confirmation + + REACHABLE timeout, more than - STALE + N seconds since + reachability confirm. + + STALE Sending packet Start delay timer DELAY + + DELAY Delay timeout Send unicast NS probe PROBE + Start retransmit timer + + PROBE Retransmit timeout, Retransmit NS PROBE + less than N + retransmissions. + + PROBE Retransmit timeout, Discard entry - + N or more + retransmissions. + + + + + +Narten, et al. Standards Track [Page 90] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + The state transitions for receiving unsolicited information other + than Neighbor Advertisement messages apply to either the source of + the packet (for Neighbor Solicitation, Router Solicitation, and + Router Advertisement messages) or the target address (for Redirect + messages) as follows: + + State Event Action New state + + - NS, RS, RA, Redirect Create entry. STALE + + INCOMPLETE NS, RS, RA, Redirect Record link-layer STALE + address. Send queued + packets. + + !INCOMPLETE NS, RS, RA, Redirect Update link-layer STALE + Different link-layer address + address than cached. + + INCOMPLETE NS, RS No link-layer - unchanged + address + + !INCOMPLETE NS, RS, RA, Redirect - unchanged + Same link-layer + address as cached. + +Appendix D: Summary of IsRouter Rules + + This appendix presents a summary of the rules for maintaining the + IsRouter flag as specified in this document. + + The background for these rules is that the ND messages contain, + either implicitly or explicitly, information that indicates whether + or not the sender (or Target Address) is a host or a router. The + following assumptions are used: + + - The sender of a Router Advertisement is implicitly assumed to be a + router. + + - Neighbor Solicitation messages do not contain either an implicit + or explicit indication about the sender. Both hosts and routers + send such messages. + + - Neighbor Advertisement messages contain an explicit "IsRouter + flag", the R-bit. + + + + + + + +Narten, et al. Standards Track [Page 91] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + - The target of the redirect, when the target differs from the + destination address in the packet being redirected, is implicitly + assumed to be a router. This is a natural assumption since that + node is expected to be able to forward the packets towards the + destination. + + - The target of the redirect, when the target is the same as the + destination, does not carry any host vs. router information. All + that is known is that the destination (i.e., target) is on-link + but it could be either a host or a router. + + The rules for setting the IsRouter flag are based on the information + content above. If an ND message contains explicit or implicit + information, the receipt of the message will cause the IsRouter flag + to be updated. But when there is no host vs. router information in + the ND message, the receipt of the message MUST NOT cause a change to + the IsRouter state. When the receipt of such a message causes a + Neighbor Cache entry to be created, this document specifies that the + IsRouter flag be set to FALSE. There is greater potential for + mischief when a node incorrectly thinks a host is a router, than the + other way around. In these cases, a subsequent Neighbor + Advertisement or Router Advertisement message will set the correct + IsRouter value. + +Appendix E: Implementation Issues + +E.1. Reachability Confirmations + + Neighbor Unreachability Detection requires explicit confirmation that + a forward-path is functioning properly. To avoid the need for + Neighbor Solicitation probe messages, upper-layer protocols should + provide such an indication when the cost of doing so is small. + Reliable connection-oriented protocols such as TCP are generally + aware when the forward-path is working. When TCP sends (or receives) + data, for instance, it updates its window sequence numbers, sets and + cancels retransmit timers, etc. Specific scenarios that usually + indicate a properly functioning forward-path include: + + - Receipt of an acknowledgment that covers a sequence number (e.g., + data) not previously acknowledged indicates that the forward path + was working at the time the data was sent. + + - Completion of the initial three-way handshake is a special case of + the previous rule; although no data is sent during the handshake, + the SYN flags are counted as data from the sequence number + perspective. This applies to both the SYN+ACK for the active open + and the ACK of that packet on the passively opening peer. + + + + +Narten, et al. Standards Track [Page 92] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + - Receipt of new data (i.e., data not previously received) indicates + that the forward-path was working at the time an acknowledgment + was sent that advanced the peer's send window that allowed the new + data to be sent. + + To minimize the cost of communicating reachability information + between the TCP and IP layers, an implementation may wish to rate- + limit the reachability confirmations its sends IP. One possibility + is to process reachability only every few packets. For example, one + might update reachability information once per round-trip time, if an + implementation only has one round-trip timer per connection. For + those implementations that cache Destination Cache entries within + control blocks, it may be possible to update the Neighbor Cache entry + directly (i.e., without an expensive lookup) once the TCP packet has + been demultiplexed to its corresponding control block. For other + implementations, it may be possible to piggyback the reachability + confirmation on the next packet submitted to IP assuming that the + implementation guards against the piggybacked confirmation becoming + stale when no packets are sent to IP for an extended period of time. + + TCP must also guard against thinking "stale" information indicates + current reachability. For example, new data received 30 minutes + after a window has opened up does not constitute a confirmation that + the path is currently working; it merely indicates that 30 minutes + ago the window update reached the peer, i.e., the path was working at + that point in time. An implementation must also take into account + TCP zero-window probes that are sent even if the path is broken and + the window update did not reach the peer. + + For UDP-based applications (Remote Procedure Call (RPC), DNS), it is + relatively simple to make the client send reachability confirmations + when the response packet is received. It is more difficult and in + some cases impossible for the server to generate such confirmations + since there is no flow control, i.e., the server cannot determine + whether a received request indicates that a previous response reached + the client. + + Note that an implementation cannot use negative upper-layer advice as + a replacement for the Neighbor Unreachability Detection algorithm. + Negative advice (e.g., from TCP when there are excessive + retransmissions) could serve as a hint that the forward path from the + sender of the data might not be working. But it would fail to detect + when the path from the receiver of the data is not functioning, + causing none of the acknowledgment packets to reach the sender. + + + + + + + +Narten, et al. Standards Track [Page 93] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + +Appendix F: Changes from RFC 2461 + + o Removed references to IPsec AH and ESP for securing messages or as + part of validating the received message. + + o Added Section 3.3. + + o Updated Section 11 to include more detailed discussion on threats, + IPsec limitations, and use of SEND. + + o Removed the on-link assumption in Section 5.2 based on RFC 4942, + "IPv6 Neighbor Discovery On-Link Assumption Considered Harmful". + + o Clarified the definition of the Router Lifetime field in Section + 4.2. + + o Updated the text in Sections 4.6.2 and 6.2.1 to indicate that the + preferred lifetime must not be larger than valid lifetime. + + o Removed the reference to stateful configuration and added reference + for DHCPv6 instead. + + o Added the IsRouter flag definition to Section 6.2.1 to allow for + mixed host/router behavior. + + o Allowed mobile nodes to be exempt from adding random delays before + sending an RS during a handover. + + o Updated the definition of the prefix length in the prefix option. + + o Updated the applicability to NBMA links in the introduction and + added references to 3GPP RFCs. + + o Clarified that support for load balancing is limited to routers. + + o Clarified router behavior when receiving a Router Solicitation + without Source Link-Layer Address Option (SLLAO). + + o Clarified that inconsistency checks for CurHopLimit are done for + non-zero values only. + + o Rearranged Section 7.2.5 for clarity, and described the processing + when receiving the NA in INCOMPLETE state. + + o Added clarifications in Section 7.2 on how a node should react upon + receiving a message without SLLAO. + + o Added new IANA section. + + + +Narten, et al. Standards Track [Page 94] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + + o Miscellaneous editorials. + +Acknowledgments + + The authors of RFC 2461 would like to acknowledge the contributions + of the IPV6 working group and, in particular, (in alphabetical order) + Ran Atkinson, Jim Bound, Scott Bradner, Alex Conta, Stephen Deering, + Richard Draves, Francis Dupont, Robert Elz, Robert Gilligan, Robert + Hinden, Tatuya Jinmei, Allison Mankin, Dan McDonald, Charles Perkins, + Matt Thomas, and Susan Thomson. + + The editor of this document (Hesham Soliman) would like to thank the + IPV6 working group for the numerous contributions to this revision -- + in particular (in alphabetical order), Greg Daley, Elwyn Davies, + Ralph Droms, Brian Haberman, Bob Hinden, Tatuya Jinmei, Pekka Savola, + Fred Templin, and Christian Vogt. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Narten, et al. Standards Track [Page 95] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + +Authors' Addresses + + Thomas Narten + IBM Corporation + P.O. Box 12195 + Research Triangle Park, NC 27709-2195 + USA + + Phone: +1 919 254 7798 + EMail: narten@us.ibm.com + + + Erik Nordmark + Sun Microsystems, Inc. + 17 Network Circle + Menlo Park, CA 94025 + USA + + Phone: +1 650 786 2921 + Fax: +1 650 786 5896 + EMail: erik.nordmark@sun.com + + + William Allen Simpson + Daydreamer + Computer Systems Consulting Services + 1384 Fontaine + Madison Heights, Michigan 48071 + USA + + EMail: william.allen.simpson@gmail.com + + + Hesham Soliman + Elevate Technologies + + EMail: hesham@elevatemobile.com + + + + + + + + + + + + + + +Narten, et al. Standards Track [Page 96] + +RFC 4861 Neighbor Discovery in IPv6 September 2007 + + +Full Copyright Statement + + Copyright (C) The IETF Trust (2007). + + This document is subject to the rights, licenses and restrictions + contained in BCP 78, and except as set forth therein, the authors + retain all their rights. + + This document and the information contained herein are provided on an + "AS IS" basis and THE CONTRIBUTOR, THE ORGANIZATION HE/SHE REPRESENTS + OR IS SPONSORED BY (IF ANY), THE INTERNET SOCIETY, THE IETF TRUST AND + THE INTERNET ENGINEERING TASK FORCE DISCLAIM ALL WARRANTIES, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF + THE INFORMATION HEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY IMPLIED + WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. + +Intellectual Property + + The IETF takes no position regarding the validity or scope of any + Intellectual Property Rights or other rights that might be claimed to + pertain to the implementation or use of the technology described in + this document or the extent to which any license under such rights + might or might not be available; nor does it represent that it has + made any independent effort to identify any such rights. Information + on the procedures with respect to rights in RFC documents can be + found in BCP 78 and BCP 79. + + Copies of IPR disclosures made to the IETF Secretariat and any + assurances of licenses to be made available, or the result of an + attempt made to obtain a general license or permission for the use of + such proprietary rights by implementers or users of this + specification can be obtained from the IETF on-line IPR repository at + http://www.ietf.org/ipr. + + The IETF invites any interested party to bring to its attention any + copyrights, patents or patent applications, or other proprietary + rights that may cover technology that may be required to implement + this standard. Please address the information to the IETF at + ietf-ipr@ietf.org. + + + + + + + + + + + + +Narten, et al. Standards Track [Page 97] 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/rfc792-icmpv4.txt b/dist/docs/rfc792-icmpv4.txt new file mode 100644 index 0000000..08873c4 --- /dev/null +++ b/dist/docs/rfc792-icmpv4.txt @@ -0,0 +1,1217 @@ + + +Network Working Group J. Postel +Request for Comments: 792 ISI + September 1981 +Updates: RFCs 777, 760 +Updates: IENs 109, 128 + + INTERNET CONTROL MESSAGE PROTOCOL + + DARPA INTERNET PROGRAM + PROTOCOL SPECIFICATION + + + +Introduction + + The Internet Protocol (IP) [1] is used for host-to-host datagram + service in a system of interconnected networks called the + Catenet [2]. The network connecting devices are called Gateways. + These gateways communicate between themselves for control purposes + via a Gateway to Gateway Protocol (GGP) [3,4]. Occasionally a + gateway or destination host will communicate with a source host, for + example, to report an error in datagram processing. For such + purposes this protocol, the Internet Control Message Protocol (ICMP), + is used. ICMP, uses the basic support of IP as if it were a higher + level protocol, however, ICMP is actually an integral part of IP, and + must be implemented by every IP module. + + ICMP messages are sent in several situations: for example, when a + datagram cannot reach its destination, when the gateway does not have + the buffering capacity to forward a datagram, and when the gateway + can direct the host to send traffic on a shorter route. + + The Internet Protocol is not designed to be absolutely reliable. The + purpose of these control messages is to provide feedback about + problems in the communication environment, not to make IP reliable. + There are still no guarantees that a datagram will be delivered or a + control message will be returned. Some datagrams may still be + undelivered without any report of their loss. The higher level + protocols that use IP must implement their own reliability procedures + if reliable communication is required. + + The ICMP messages typically report errors in the processing of + datagrams. To avoid the infinite regress of messages about messages + etc., no ICMP messages are sent about ICMP messages. Also ICMP + messages are only sent about errors in handling fragment zero of + fragemented datagrams. (Fragment zero has the fragment offeset equal + zero). + + + + + + + + [Page 1] + + + September 1981 +RFC 792 + + + +Message Formats + + ICMP messages are sent using the basic IP header. The first octet of + the data portion of the datagram is a ICMP type field; the value of + this field determines the format of the remaining data. Any field + labeled "unused" is reserved for later extensions and must be zero + when sent, but receivers should not use these fields (except to + include them in the checksum). Unless otherwise noted under the + individual format descriptions, the values of the internet header + fields are as follows: + + Version + + 4 + + IHL + + Internet header length in 32-bit words. + + Type of Service + + 0 + + Total Length + + Length of internet header and data in octets. + + Identification, Flags, Fragment Offset + + Used in fragmentation, see [1]. + + Time to Live + + Time to live in seconds; as this field is decremented at each + machine in which the datagram is processed, the value in this + field should be at least as great as the number of gateways which + this datagram will traverse. + + Protocol + + ICMP = 1 + + Header Checksum + + The 16 bit one's complement of the one's complement sum of all 16 + bit words in the header. For computing the checksum, the checksum + field should be zero. This checksum may be replaced in the + future. + + +[Page 2] + + +September 1981 +RFC 792 + + + + Source Address + + The address of the gateway or host that composes the ICMP message. + Unless otherwise noted, this can be any of a gateway's addresses. + + Destination Address + + The address of the gateway or host to which the message should be + sent. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + [Page 3] + + + September 1981 +RFC 792 + + + +Destination Unreachable Message + + 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Code | Checksum | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | unused | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Internet Header + 64 bits of Original Data Datagram | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + IP Fields: + + Destination Address + + The source network and address from the original datagram's data. + + ICMP Fields: + + Type + + 3 + + Code + + 0 = net unreachable; + + 1 = host unreachable; + + 2 = protocol unreachable; + + 3 = port unreachable; + + 4 = fragmentation needed and DF set; + + 5 = source route failed. + + Checksum + + The checksum is the 16-bit ones's complement of the one's + complement sum of the ICMP message starting with the ICMP Type. + For computing the checksum , the checksum field should be zero. + This checksum may be replaced in the future. + + Internet Header + 64 bits of Data Datagram + + The internet header plus the first 64 bits of the original + + +[Page 4] + + +September 1981 +RFC 792 + + + + datagram's data. This data is used by the host to match the + message to the appropriate process. If a higher level protocol + uses port numbers, they are assumed to be in the first 64 data + bits of the original datagram's data. + + Description + + If, according to the information in the gateway's routing tables, + the network specified in the internet destination field of a + datagram is unreachable, e.g., the distance to the network is + infinity, the gateway may send a destination unreachable message + to the internet source host of the datagram. In addition, in some + networks, the gateway may be able to determine if the internet + destination host is unreachable. Gateways in these networks may + send destination unreachable messages to the source host when the + destination host is unreachable. + + If, in the destination host, the IP module cannot deliver the + datagram because the indicated protocol module or process port is + not active, the destination host may send a destination + unreachable message to the source host. + + Another case is when a datagram must be fragmented to be forwarded + by a gateway yet the Don't Fragment flag is on. In this case the + gateway must discard the datagram and may return a destination + unreachable message. + + Codes 0, 1, 4, and 5 may be received from a gateway. Codes 2 and + 3 may be received from a host. + + + + + + + + + + + + + + + + + + + + + + [Page 5] + + + September 1981 +RFC 792 + + + +Time Exceeded Message + + 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Code | Checksum | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | unused | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Internet Header + 64 bits of Original Data Datagram | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + IP Fields: + + Destination Address + + The source network and address from the original datagram's data. + + ICMP Fields: + + Type + + 11 + + Code + + 0 = time to live exceeded in transit; + + 1 = fragment reassembly time exceeded. + + Checksum + + The checksum is the 16-bit ones's complement of the one's + complement sum of the ICMP message starting with the ICMP Type. + For computing the checksum , the checksum field should be zero. + This checksum may be replaced in the future. + + Internet Header + 64 bits of Data Datagram + + The internet header plus the first 64 bits of the original + datagram's data. This data is used by the host to match the + message to the appropriate process. If a higher level protocol + uses port numbers, they are assumed to be in the first 64 data + bits of the original datagram's data. + + Description + + If the gateway processing a datagram finds the time to live field + + +[Page 6] + + +September 1981 +RFC 792 + + + + is zero it must discard the datagram. The gateway may also notify + the source host via the time exceeded message. + + If a host reassembling a fragmented datagram cannot complete the + reassembly due to missing fragments within its time limit it + discards the datagram, and it may send a time exceeded message. + + If fragment zero is not available then no time exceeded need be + sent at all. + + Code 0 may be received from a gateway. Code 1 may be received + from a host. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + [Page 7] + + + September 1981 +RFC 792 + + + +Parameter Problem Message + + 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Code | Checksum | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Pointer | unused | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Internet Header + 64 bits of Original Data Datagram | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + IP Fields: + + Destination Address + + The source network and address from the original datagram's data. + + ICMP Fields: + + Type + + 12 + + Code + + 0 = pointer indicates the error. + + Checksum + + The checksum is the 16-bit ones's complement of the one's + complement sum of the ICMP message starting with the ICMP Type. + For computing the checksum , the checksum field should be zero. + This checksum may be replaced in the future. + + Pointer + + If code = 0, identifies the octet where an error was detected. + + Internet Header + 64 bits of Data Datagram + + The internet header plus the first 64 bits of the original + datagram's data. This data is used by the host to match the + message to the appropriate process. If a higher level protocol + uses port numbers, they are assumed to be in the first 64 data + bits of the original datagram's data. + + + + +[Page 8] + + +September 1981 +RFC 792 + + + + Description + + If the gateway or host processing a datagram finds a problem with + the header parameters such that it cannot complete processing the + datagram it must discard the datagram. One potential source of + such a problem is with incorrect arguments in an option. The + gateway or host may also notify the source host via the parameter + problem message. This message is only sent if the error caused + the datagram to be discarded. + + The pointer identifies the octet of the original datagram's header + where the error was detected (it may be in the middle of an + option). For example, 1 indicates something is wrong with the + Type of Service, and (if there are options present) 20 indicates + something is wrong with the type code of the first option. + + Code 0 may be received from a gateway or a host. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + [Page 9] + + + September 1981 +RFC 792 + + + +Source Quench Message + + 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Code | Checksum | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | unused | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Internet Header + 64 bits of Original Data Datagram | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + IP Fields: + + Destination Address + + The source network and address of the original datagram's data. + + ICMP Fields: + + Type + + 4 + + Code + + 0 + + Checksum + + The checksum is the 16-bit ones's complement of the one's + complement sum of the ICMP message starting with the ICMP Type. + For computing the checksum , the checksum field should be zero. + This checksum may be replaced in the future. + + Internet Header + 64 bits of Data Datagram + + The internet header plus the first 64 bits of the original + datagram's data. This data is used by the host to match the + message to the appropriate process. If a higher level protocol + uses port numbers, they are assumed to be in the first 64 data + bits of the original datagram's data. + + Description + + A gateway may discard internet datagrams if it does not have the + buffer space needed to queue the datagrams for output to the next + network on the route to the destination network. If a gateway + + +[Page 10] + + +September 1981 +RFC 792 + + + + discards a datagram, it may send a source quench message to the + internet source host of the datagram. A destination host may also + send a source quench message if datagrams arrive too fast to be + processed. The source quench message is a request to the host to + cut back the rate at which it is sending traffic to the internet + destination. The gateway may send a source quench message for + every message that it discards. On receipt of a source quench + message, the source host should cut back the rate at which it is + sending traffic to the specified destination until it no longer + receives source quench messages from the gateway. The source host + can then gradually increase the rate at which it sends traffic to + the destination until it again receives source quench messages. + + The gateway or host may send the source quench message when it + approaches its capacity limit rather than waiting until the + capacity is exceeded. This means that the data datagram which + triggered the source quench message may be delivered. + + Code 0 may be received from a gateway or a host. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + [Page 11] + + + September 1981 +RFC 792 + + + +Redirect Message + + 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Code | Checksum | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Gateway Internet Address | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Internet Header + 64 bits of Original Data Datagram | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + IP Fields: + + Destination Address + + The source network and address of the original datagram's data. + + ICMP Fields: + + Type + + 5 + + Code + + 0 = Redirect datagrams for the Network. + + 1 = Redirect datagrams for the Host. + + 2 = Redirect datagrams for the Type of Service and Network. + + 3 = Redirect datagrams for the Type of Service and Host. + + Checksum + + The checksum is the 16-bit ones's complement of the one's + complement sum of the ICMP message starting with the ICMP Type. + For computing the checksum , the checksum field should be zero. + This checksum may be replaced in the future. + + Gateway Internet Address + + Address of the gateway to which traffic for the network specified + in the internet destination network field of the original + datagram's data should be sent. + + + + +[Page 12] + + +September 1981 +RFC 792 + + + + Internet Header + 64 bits of Data Datagram + + The internet header plus the first 64 bits of the original + datagram's data. This data is used by the host to match the + message to the appropriate process. If a higher level protocol + uses port numbers, they are assumed to be in the first 64 data + bits of the original datagram's data. + + Description + + The gateway sends a redirect message to a host in the following + situation. A gateway, G1, receives an internet datagram from a + host on a network to which the gateway is attached. The gateway, + G1, checks its routing table and obtains the address of the next + gateway, G2, on the route to the datagram's internet destination + network, X. If G2 and the host identified by the internet source + address of the datagram are on the same network, a redirect + message is sent to the host. The redirect message advises the + host to send its traffic for network X directly to gateway G2 as + this is a shorter path to the destination. The gateway forwards + the original datagram's data to its internet destination. + + For datagrams with the IP source route options and the gateway + address in the destination address field, a redirect message is + not sent even if there is a better route to the ultimate + destination than the next address in the source route. + + Codes 0, 1, 2, and 3 may be received from a gateway. + + + + + + + + + + + + + + + + + + + + + + + [Page 13] + + + September 1981 +RFC 792 + + + +Echo or Echo Reply Message + + 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Code | Checksum | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Identifier | Sequence Number | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Data ... + +-+-+-+-+- + + IP Fields: + + Addresses + + The address of the source in an echo message will be the + destination of the echo reply message. To form an echo reply + message, the source and destination addresses are simply reversed, + the type code changed to 0, and the checksum recomputed. + + IP Fields: + + Type + + 8 for echo message; + + 0 for echo reply message. + + Code + + 0 + + Checksum + + The checksum is the 16-bit ones's complement of the one's + complement sum of the ICMP message starting with the ICMP Type. + For computing the checksum , the checksum field should be zero. + If the total length is odd, the received data is padded with one + octet of zeros for computing the checksum. This checksum may be + replaced in the future. + + Identifier + + If code = 0, an identifier to aid in matching echos and replies, + may be zero. + + Sequence Number + + +[Page 14] + + +September 1981 +RFC 792 + + + + If code = 0, a sequence number to aid in matching echos and + replies, may be zero. + + Description + + The data received in the echo message must be returned in the echo + reply message. + + The identifier and sequence number may be used by the echo sender + to aid in matching the replies with the echo requests. For + example, the identifier might be used like a port in TCP or UDP to + identify a session, and the sequence number might be incremented + on each echo request sent. The echoer returns these same values + in the echo reply. + + Code 0 may be received from a gateway or a host. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + [Page 15] + + + September 1981 +RFC 792 + + + +Timestamp or Timestamp Reply Message + + 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Code | Checksum | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Identifier | Sequence Number | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Originate Timestamp | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Receive Timestamp | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Transmit Timestamp | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + IP Fields: + + Addresses + + The address of the source in a timestamp message will be the + destination of the timestamp reply message. To form a timestamp + reply message, the source and destination addresses are simply + reversed, the type code changed to 14, and the checksum + recomputed. + + IP Fields: + + Type + + 13 for timestamp message; + + 14 for timestamp reply message. + + Code + + 0 + + Checksum + + The checksum is the 16-bit ones's complement of the one's + complement sum of the ICMP message starting with the ICMP Type. + For computing the checksum , the checksum field should be zero. + This checksum may be replaced in the future. + + Identifier + + + + +[Page 16] + + +September 1981 +RFC 792 + + + + If code = 0, an identifier to aid in matching timestamp and + replies, may be zero. + + Sequence Number + + If code = 0, a sequence number to aid in matching timestamp and + replies, may be zero. + + Description + + The data received (a timestamp) in the message is returned in the + reply together with an additional timestamp. The timestamp is 32 + bits of milliseconds since midnight UT. One use of these + timestamps is described by Mills [5]. + + The Originate Timestamp is the time the sender last touched the + message before sending it, the Receive Timestamp is the time the + echoer first touched it on receipt, and the Transmit Timestamp is + the time the echoer last touched the message on sending it. + + If the time is not available in miliseconds or cannot be provided + with respect to midnight UT then any time can be inserted in a + timestamp provided the high order bit of the timestamp is also set + to indicate this non-standard value. + + The identifier and sequence number may be used by the echo sender + to aid in matching the replies with the requests. For example, + the identifier might be used like a port in TCP or UDP to identify + a session, and the sequence number might be incremented on each + request sent. The destination returns these same values in the + reply. + + Code 0 may be received from a gateway or a host. + + + + + + + + + + + + + + + + + + [Page 17] + + + September 1981 +RFC 792 + + + +Information Request or Information Reply Message + + 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Code | Checksum | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Identifier | Sequence Number | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + IP Fields: + + Addresses + + The address of the source in a information request message will be + the destination of the information reply message. To form a + information reply message, the source and destination addresses + are simply reversed, the type code changed to 16, and the checksum + recomputed. + + IP Fields: + + Type + + 15 for information request message; + + 16 for information reply message. + + Code + + 0 + + Checksum + + The checksum is the 16-bit ones's complement of the one's + complement sum of the ICMP message starting with the ICMP Type. + For computing the checksum , the checksum field should be zero. + This checksum may be replaced in the future. + + Identifier + + If code = 0, an identifier to aid in matching request and replies, + may be zero. + + Sequence Number + + If code = 0, a sequence number to aid in matching request and + replies, may be zero. + + +[Page 18] + + +September 1981 +RFC 792 + + + + Description + + This message may be sent with the source network in the IP header + source and destination address fields zero (which means "this" + network). The replying IP module should send the reply with the + addresses fully specified. This message is a way for a host to + find out the number of the network it is on. + + The identifier and sequence number may be used by the echo sender + to aid in matching the replies with the requests. For example, + the identifier might be used like a port in TCP or UDP to identify + a session, and the sequence number might be incremented on each + request sent. The destination returns these same values in the + reply. + + Code 0 may be received from a gateway or a host. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + [Page 19] + + + September 1981 +RFC 792 + + + +Summary of Message Types + + 0 Echo Reply + + 3 Destination Unreachable + + 4 Source Quench + + 5 Redirect + + 8 Echo + + 11 Time Exceeded + + 12 Parameter Problem + + 13 Timestamp + + 14 Timestamp Reply + + 15 Information Request + + 16 Information Reply + + + + + + + + + + + + + + + + + + + + + + + + + + + +[Page 20] + + +September 1981 +RFC 792 + + + +References + + [1] Postel, J. (ed.), "Internet Protocol - DARPA Internet Program + Protocol Specification," RFC 791, USC/Information Sciences + Institute, September 1981. + + [2] Cerf, V., "The Catenet Model for Internetworking," IEN 48, + Information Processing Techniques Office, Defense Advanced + Research Projects Agency, July 1978. + + [3] Strazisar, V., "Gateway Routing: An Implementation + Specification", IEN 30, Bolt Beranek and Newman, April 1979. + + [4] Strazisar, V., "How to Build a Gateway", IEN 109, Bolt Beranek + and Newman, August 1979. + + [5] Mills, D., "DCNET Internet Clock Service," RFC 778, COMSAT + Laboratories, April 1981. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + [Page 21] 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/rfc826-arp.txt b/dist/docs/rfc826-arp.txt new file mode 100644 index 0000000..aaaea14 --- /dev/null +++ b/dist/docs/rfc826-arp.txt @@ -0,0 +1,469 @@ +Network Working Group David C. Plummer +Request For Comments: 826 (DCP@MIT-MC) + November 1982 + + + An Ethernet Address Resolution Protocol + -- or -- + Converting Network Protocol Addresses + to 48.bit Ethernet Address + for Transmission on + Ethernet Hardware + + + + + + Abstract + +The implementation of protocol P on a sending host S decides, +through protocol P's routing mechanism, that it wants to transmit +to a target host T located some place on a connected piece of +10Mbit Ethernet cable. To actually transmit the Ethernet packet +a 48.bit Ethernet address must be generated. The addresses of +hosts within protocol P are not always compatible with the +corresponding Ethernet address (being different lengths or +values). Presented here is a protocol that allows dynamic +distribution of the information needed to build tables to +translate an address A in protocol P's address space into a +48.bit Ethernet address. + +Generalizations have been made which allow the protocol to be +used for non-10Mbit Ethernet hardware. Some packet radio +networks are examples of such hardware. + +-------------------------------------------------------------------- + +The protocol proposed here is the result of a great deal of +discussion with several other people, most notably J. Noel +Chiappa, Yogen Dalal, and James E. Kulp, and helpful comments +from David Moon. + + + + +[The purpose of this RFC is to present a method of Converting +Protocol Addresses (e.g., IP addresses) to Local Network +Addresses (e.g., Ethernet addresses). This is a issue of general +concern in the ARPA Internet community at this time. The +method proposed here is presented for your consideration and +comment. This is not the specification of a Internet Standard.] + +Notes: +------ + +This protocol was originally designed for the DEC/Intel/Xerox +10Mbit Ethernet. It has been generalized to allow it to be used +for other types of networks. Much of the discussion will be +directed toward the 10Mbit Ethernet. Generalizations, where +applicable, will follow the Ethernet-specific discussion. + +DOD Internet Protocol will be referred to as Internet. + +Numbers here are in the Ethernet standard, which is high byte +first. This is the opposite of the byte addressing of machines +such as PDP-11s and VAXes. Therefore, special care must be taken +with the opcode field (ar$op) described below. + +An agreed upon authority is needed to manage hardware name space +values (see below). Until an official authority exists, requests +should be submitted to + David C. Plummer + Symbolics, Inc. + 243 Vassar Street + Cambridge, Massachusetts 02139 +Alternatively, network mail can be sent to DCP@MIT-MC. + +The Problem: +------------ + +The world is a jungle in general, and the networking game +contributes many animals. At nearly every layer of a network +architecture there are several potential protocols that could be +used. For example, at a high level, there is TELNET and SUPDUP +for remote login. Somewhere below that there is a reliable byte +stream protocol, which might be CHAOS protocol, DOD TCP, Xerox +BSP or DECnet. Even closer to the hardware is the logical +transport layer, which might be CHAOS, DOD Internet, Xerox PUP, +or DECnet. The 10Mbit Ethernet allows all of these protocols +(and more) to coexist on a single cable by means of a type field +in the Ethernet packet header. However, the 10Mbit Ethernet +requires 48.bit addresses on the physical cable, yet most +protocol addresses are not 48.bits long, nor do they necessarily +have any relationship to the 48.bit Ethernet address of the +hardware. For example, CHAOS addresses are 16.bits, DOD Internet +addresses are 32.bits, and Xerox PUP addresses are 8.bits. A +protocol is needed to dynamically distribute the correspondences +between a pair and a 48.bit Ethernet address. + +Motivation: +----------- + +Use of the 10Mbit Ethernet is increasing as more manufacturers +supply interfaces that conform to the specification published by +DEC, Intel and Xerox. With this increasing availability, more +and more software is being written for these interfaces. There +are two alternatives: (1) Every implementor invents his/her own +method to do some form of address resolution, or (2) every +implementor uses a standard so that his/her code can be +distributed to other systems without need for modification. This +proposal attempts to set the standard. + +Definitions: +------------ + +Define the following for referring to the values put in the TYPE +field of the Ethernet packet header: + ether_type$XEROX_PUP, + ether_type$DOD_INTERNET, + ether_type$CHAOS, +and a new one: + ether_type$ADDRESS_RESOLUTION. +Also define the following values (to be discussed later): + ares_op$REQUEST (= 1, high byte transmitted first) and + ares_op$REPLY (= 2), +and + ares_hrd$Ethernet (= 1). + +Packet format: +-------------- + +To communicate mappings from pairs to 48.bit +Ethernet addresses, a packet format that embodies the Address +Resolution protocol is needed. The format of the packet follows. + + Ethernet transmission layer (not necessarily accessible to + the user): + 48.bit: Ethernet address of destination + 48.bit: Ethernet address of sender + 16.bit: Protocol type = ether_type$ADDRESS_RESOLUTION + Ethernet packet data: + 16.bit: (ar$hrd) Hardware address space (e.g., Ethernet, + Packet Radio Net.) + 16.bit: (ar$pro) Protocol address space. For Ethernet + hardware, this is from the set of type + fields ether_typ$. + 8.bit: (ar$hln) byte length of each hardware address + 8.bit: (ar$pln) byte length of each protocol address + 16.bit: (ar$op) opcode (ares_op$REQUEST | ares_op$REPLY) + nbytes: (ar$sha) Hardware address of sender of this + packet, n from the ar$hln field. + mbytes: (ar$spa) Protocol address of sender of this + packet, m from the ar$pln field. + nbytes: (ar$tha) Hardware address of target of this + packet (if known). + mbytes: (ar$tpa) Protocol address of target. + + +Packet Generation: +------------------ + +As a packet is sent down through the network layers, routing +determines the protocol address of the next hop for the packet +and on which piece of hardware it expects to find the station +with the immediate target protocol address. In the case of the +10Mbit Ethernet, address resolution is needed and some lower +layer (probably the hardware driver) must consult the Address +Resolution module (perhaps implemented in the Ethernet support +module) to convert the +pair to a 48.bit Ethernet address. The Address Resolution module +tries to find this pair in a table. If it finds the pair, it +gives the corresponding 48.bit Ethernet address back to the +caller (hardware driver) which then transmits the packet. If it +does not, it probably informs the caller that it is throwing the +packet away (on the assumption the packet will be retransmitted +by a higher network layer), and generates an Ethernet packet with +a type field of ether_type$ADDRESS_RESOLUTION. The Address +Resolution module then sets the ar$hrd field to +ares_hrd$Ethernet, ar$pro to the protocol type that is being +resolved, ar$hln to 6 (the number of bytes in a 48.bit Ethernet +address), ar$pln to the length of an address in that protocol, +ar$op to ares_op$REQUEST, ar$sha with the 48.bit ethernet address +of itself, ar$spa with the protocol address of itself, and ar$tpa +with the protocol address of the machine that is trying to be +accessed. It does not set ar$tha to anything in particular, +because it is this value that it is trying to determine. It +could set ar$tha to the broadcast address for the hardware (all +ones in the case of the 10Mbit Ethernet) if that makes it +convenient for some aspect of the implementation. It then causes +this packet to be broadcast to all stations on the Ethernet cable +originally determined by the routing mechanism. + + + +Packet Reception: +----------------- + +When an address resolution packet is received, the receiving +Ethernet module gives the packet to the Address Resolution module +which goes through an algorithm similar to the following. +Negative conditionals indicate an end of processing and a +discarding of the packet. + +?Do I have the hardware type in ar$hrd? +Yes: (almost definitely) + [optionally check the hardware length ar$hln] + ?Do I speak the protocol in ar$pro? + Yes: + [optionally check the protocol length ar$pln] + Merge_flag := false + If the pair is + already in my translation table, update the sender + hardware address field of the entry with the new + information in the packet and set Merge_flag to true. + ?Am I the target protocol address? + Yes: + If Merge_flag is false, add the triplet to + the translation table. + ?Is the opcode ares_op$REQUEST? (NOW look at the opcode!!) + Yes: + Swap hardware and protocol fields, putting the local + hardware and protocol addresses in the sender fields. + Set the ar$op field to ares_op$REPLY + Send the packet to the (new) target hardware address on + the same hardware on which the request was received. + +Notice that the triplet is merged into the table before the +opcode is looked at. This is on the assumption that communcation +is bidirectional; if A has some reason to talk to B, then B will +probably have some reason to talk to A. Notice also that if an +entry already exists for the pair, then the new hardware address supersedes the old +one. Related Issues gives some motivation for this. + +Generalization: The ar$hrd and ar$hln fields allow this protocol +and packet format to be used for non-10Mbit Ethernets. For the +10Mbit Ethernet takes on the value <1, 6>. For +other hardware networks, the ar$pro field may no longer +correspond to the Ethernet type field, but it should be +associated with the protocol whose address resolution is being +sought. + + +Why is it done this way?? +------------------------- + +Periodic broadcasting is definitely not desired. Imagine 100 +workstations on a single Ethernet, each broadcasting address +resolution information once per 10 minutes (as one possible set +of parameters). This is one packet every 6 seconds. This is +almost reasonable, but what use is it? The workstations aren't +generally going to be talking to each other (and therefore have +100 useless entries in a table); they will be mainly talking to a +mainframe, file server or bridge, but only to a small number of +other workstations (for interactive conversations, for example). +The protocol described in this paper distributes information as +it is needed, and only once (probably) per boot of a machine. + +This format does not allow for more than one resolution to be +done in the same packet. This is for simplicity. If things were +multiplexed the packet format would be considerably harder to +digest, and much of the information could be gratuitous. Think +of a bridge that talks four protocols telling a workstation all +four protocol addresses, three of which the workstation will +probably never use. + +This format allows the packet buffer to be reused if a reply is +generated; a reply has the same length as a request, and several +of the fields are the same. + +The value of the hardware field (ar$hrd) is taken from a list for +this purpose. Currently the only defined value is for the 10Mbit +Ethernet (ares_hrd$Ethernet = 1). There has been talk of using +this protocol for Packet Radio Networks as well, and this will +require another value as will other future hardware mediums that +wish to use this protocol. + +For the 10Mbit Ethernet, the value in the protocol field (ar$pro) +is taken from the set ether_type$. This is a natural reuse of +the assigned protocol types. Combining this with the opcode +(ar$op) would effectively halve the number of protocols that can +be resolved under this protocol and would make a monitor/debugger +more complex (see Network Monitoring and Debugging below). It is +hoped that we will never see 32768 protocols, but Murphy made +some laws which don't allow us to make this assumption. + +In theory, the length fields (ar$hln and ar$pln) are redundant, +since the length of a protocol address should be determined by +the hardware type (found in ar$hrd) and the protocol type (found +in ar$pro). It is included for optional consistency checking, +and for network monitoring and debugging (see below). + +The opcode is to determine if this is a request (which may cause +a reply) or a reply to a previous request. 16 bits for this is +overkill, but a flag (field) is needed. + +The sender hardware address and sender protocol address are +absolutely necessary. It is these fields that get put in a +translation table. + +The target protocol address is necessary in the request form of +the packet so that a machine can determine whether or not to +enter the sender information in a table or to send a reply. It +is not necessarily needed in the reply form if one assumes a +reply is only provoked by a request. It is included for +completeness, network monitoring, and to simplify the suggested +processing algorithm described above (which does not look at the +opcode until AFTER putting the sender information in a table). + +The target hardware address is included for completeness and +network monitoring. It has no meaning in the request form, since +it is this number that the machine is requesting. Its meaning in +the reply form is the address of the machine making the request. +In some implementations (which do not get to look at the 14.byte +ethernet header, for example) this may save some register +shuffling or stack space by sending this field to the hardware +driver as the hardware destination address of the packet. + +There are no padding bytes between addresses. The packet data +should be viewed as a byte stream in which only 3 byte pairs are +defined to be words (ar$hrd, ar$pro and ar$op) which are sent +most significant byte first (Ethernet/PDP-10 byte style). + + +Network monitoring and debugging: +--------------------------------- + +The above Address Resolution protocol allows a machine to gain +knowledge about the higher level protocol activity (e.g., CHAOS, +Internet, PUP, DECnet) on an Ethernet cable. It can determine +which Ethernet protocol type fields are in use (by value) and the +protocol addresses within each protocol type. In fact, it is not +necessary for the monitor to speak any of the higher level +protocols involved. It goes something like this: + +When a monitor receives an Address Resolution packet, it always +enters the in a table. It can determine the length of the +hardware and protocol address from the ar$hln and ar$pln fields +of the packet. If the opcode is a REPLY the monitor can then +throw the packet away. If the opcode is a REQUEST and the target +protocol address matches the protocol address of the monitor, the +monitor sends a REPLY as it normally would. The monitor will +only get one mapping this way, since the REPLY to the REQUEST +will be sent directly to the requesting host. The monitor could +try sending its own REQUEST, but this could get two monitors into +a REQUEST sending loop, and care must be taken. + +Because the protocol and opcode are not combined into one field, +the monitor does not need to know which request opcode is +associated with which reply opcode for the same higher level +protocol. The length fields should also give enough information +to enable it to "parse" a protocol addresses, although it has no +knowledge of what the protocol addresses mean. + +A working implementation of the Address Resolution protocol can +also be used to debug a non-working implementation. Presumably a +hardware driver will successfully broadcast a packet with Ethernet +type field of ether_type$ADDRESS_RESOLUTION. The format of the +packet may not be totally correct, because initial +implementations may have bugs, and table management may be +slightly tricky. Because requests are broadcast a monitor will +receive the packet and can display it for debugging if desired. + + +An Example: +----------- + +Let there exist machines X and Y that are on the same 10Mbit +Ethernet cable. They have Ethernet address EA(X) and EA(Y) and +DOD Internet addresses IPA(X) and IPA(Y) . Let the Ethernet type +of Internet be ET(IP). Machine X has just been started, and +sooner or later wants to send an Internet packet to machine Y on +the same cable. X knows that it wants to send to IPA(Y) and +tells the hardware driver (here an Ethernet driver) IPA(Y). The +driver consults the Address Resolution module to convert into a 48.bit Ethernet address, but because X was just +started, it does not have this information. It throws the +Internet packet away and instead creates an ADDRESS RESOLUTION +packet with + (ar$hrd) = ares_hrd$Ethernet + (ar$pro) = ET(IP) + (ar$hln) = length(EA(X)) + (ar$pln) = length(IPA(X)) + (ar$op) = ares_op$REQUEST + (ar$sha) = EA(X) + (ar$spa) = IPA(X) + (ar$tha) = don't care + (ar$tpa) = IPA(Y) +and broadcasts this packet to everybody on the cable. + +Machine Y gets this packet, and determines that it understands +the hardware type (Ethernet), that it speaks the indicated +protocol (Internet) and that the packet is for it +((ar$tpa)=IPA(Y)). It enters (probably replacing any existing +entry) the information that maps to EA(X). It +then notices that it is a request, so it swaps fields, putting +EA(Y) in the new sender Ethernet address field (ar$sha), sets the +opcode to reply, and sends the packet directly (not broadcast) to +EA(X). At this point Y knows how to send to X, but X still +doesn't know how to send to Y. + +Machine X gets the reply packet from Y, forms the map from + to EA(Y), notices the packet is a reply and +throws it away. The next time X's Internet module tries to send +a packet to Y on the Ethernet, the translation will succeed, and +the packet will (hopefully) arrive. If Y's Internet module then +wants to talk to X, this will also succeed since Y has remembered +the information from X's request for Address Resolution. + +Related issue: +--------------- + +It may be desirable to have table aging and/or timeouts. The +implementation of these is outside the scope of this protocol. +Here is a more detailed description (thanks to MOON@SCRC@MIT-MC). + +If a host moves, any connections initiated by that host will +work, assuming its own address resolution table is cleared when +it moves. However, connections initiated to it by other hosts +will have no particular reason to know to discard their old +address. However, 48.bit Ethernet addresses are supposed to be +unique and fixed for all time, so they shouldn't change. A host +could "move" if a host name (and address in some other protocol) +were reassigned to a different physical piece of hardware. Also, +as we know from experience, there is always the danger of +incorrect routing information accidentally getting transmitted +through hardware or software error; it should not be allowed to +persist forever. Perhaps failure to initiate a connection should +inform the Address Resolution module to delete the information on +the basis that the host is not reachable, possibly because it is +down or the old translation is no longer valid. Or perhaps +receiving of a packet from a host should reset a timeout in the +address resolution entry used for transmitting packets to that +host; if no packets are received from a host for a suitable +length of time, the address resolution entry is forgotten. This +may cause extra overhead to scan the table for each incoming +packet. Perhaps a hash or index can make this faster. + +The suggested algorithm for receiving address resolution packets +tries to lessen the time it takes for recovery if a host does +move. Recall that if the is already in the translation table, then the sender +hardware address supersedes the existing entry. Therefore, on a +perfect Ethernet where a broadcast REQUEST reaches all stations +on the cable, each station will be get the new hardware address. + +Another alternative is to have a daemon perform the timeouts. +After a suitable time, the daemon considers removing an entry. +It first sends (with a small number of retransmissions if needed) +an address resolution packet with opcode REQUEST directly to the +Ethernet address in the table. If a REPLY is not seen in a short +amount of time, the entry is deleted. The request is sent +directly so as not to bother every station on the Ethernet. Just +forgetting entries will likely cause useful information to be +forgotten, which must be regained. + +Since hosts don't transmit information about anyone other than +themselves, rebooting a host will cause its address mapping table +to be up to date. Bad information can't persist forever by being +passed around from machine to machine; the only bad information +that can exist is in a machine that doesn't know that some other +machine has changed its 48.bit Ethernet address. Perhaps +manually resetting (or clearing) the address mapping table will +suffice. + +This issue clearly needs more thought if it is believed to be +important. It is caused by any address resolution-like protocol. diff --git a/dist/docs/rfc9112-http_1_1.txt b/dist/docs/rfc9112-http_1_1.txt new file mode 100644 index 0000000..ee05c4c --- /dev/null +++ b/dist/docs/rfc9112-http_1_1.txt @@ -0,0 +1,2461 @@ + + + + +Internet Engineering Task Force (IETF) R. Fielding, Ed. +Request for Comments: 9112 Adobe +STD: 99 M. Nottingham, Ed. +Obsoletes: 7230 Fastly +Category: Standards Track J. Reschke, Ed. +ISSN: 2070-1721 greenbytes + June 2022 + + + HTTP/1.1 + +Abstract + + The Hypertext Transfer Protocol (HTTP) is a stateless application- + level protocol for distributed, collaborative, hypertext information + systems. This document specifies the HTTP/1.1 message syntax, + message parsing, connection management, and related security + concerns. + + This document obsoletes portions of RFC 7230. + +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/rfc9112. + +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. Introduction + 1.1. Requirements Notation + 1.2. Syntax Notation + 2. Message + 2.1. Message Format + 2.2. Message Parsing + 2.3. HTTP Version + 3. Request Line + 3.1. Method + 3.2. Request Target + 3.2.1. origin-form + 3.2.2. absolute-form + 3.2.3. authority-form + 3.2.4. asterisk-form + 3.3. Reconstructing the Target URI + 4. Status Line + 5. Field Syntax + 5.1. Field Line Parsing + 5.2. Obsolete Line Folding + 6. Message Body + 6.1. Transfer-Encoding + 6.2. Content-Length + 6.3. Message Body Length + 7. Transfer Codings + 7.1. Chunked Transfer Coding + 7.1.1. Chunk Extensions + 7.1.2. Chunked Trailer Section + 7.1.3. Decoding Chunked + 7.2. Transfer Codings for Compression + 7.3. Transfer Coding Registry + 7.4. Negotiating Transfer Codings + 8. Handling Incomplete Messages + 9. Connection Management + 9.1. Establishment + 9.2. Associating a Response to a Request + 9.3. Persistence + 9.3.1. Retrying Requests + 9.3.2. Pipelining + 9.4. Concurrency + 9.5. Failures and Timeouts + 9.6. Tear-down + 9.7. TLS Connection Initiation + 9.8. TLS Connection Closure + 10. Enclosing Messages as Data + 10.1. Media Type message/http + 10.2. Media Type application/http + 11. Security Considerations + 11.1. Response Splitting + 11.2. Request Smuggling + 11.3. Message Integrity + 11.4. Message Confidentiality + 12. IANA Considerations + 12.1. Field Name Registration + 12.2. Media Type Registration + 12.3. Transfer Coding Registration + 12.4. ALPN Protocol ID Registration + 13. References + 13.1. Normative References + 13.2. Informative References + Appendix A. Collected ABNF + Appendix B. Differences between HTTP and MIME + B.1. MIME-Version + B.2. Conversion to Canonical Form + B.3. Conversion of Date Formats + B.4. Conversion of Content-Encoding + B.5. Conversion of Content-Transfer-Encoding + B.6. MHTML and Line Length Limitations + Appendix C. Changes from Previous RFCs + C.1. Changes from HTTP/0.9 + C.2. Changes from HTTP/1.0 + C.2.1. Multihomed Web Servers + C.2.2. Keep-Alive Connections + C.2.3. Introduction of Transfer-Encoding + C.3. Changes from RFC 7230 + Acknowledgements + Index + Authors' Addresses + +1. Introduction + + The Hypertext Transfer Protocol (HTTP) is a stateless application- + level request/response protocol that uses extensible semantics and + self-descriptive messages for flexible interaction with network-based + hypertext information systems. HTTP/1.1 is defined by: + + * This document + + * "HTTP Semantics" [HTTP] + + * "HTTP Caching" [CACHING] + + This document specifies how HTTP semantics are conveyed using the + HTTP/1.1 message syntax, framing, and connection management + mechanisms. Its goal is to define the complete set of requirements + for HTTP/1.1 message parsers and message-forwarding intermediaries. + + This document obsoletes the portions of RFC 7230 related to HTTP/1.1 + messaging and connection management, with the changes being + summarized in Appendix C.3. The other parts of RFC 7230 are + obsoleted by "HTTP Semantics" [HTTP]. + +1.1. Requirements Notation + + 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 [RFC2119] [RFC8174] when, and only when, they appear in all + capitals, as shown here. + + Conformance criteria and considerations regarding error handling are + defined in Section 2 of [HTTP]. + +1.2. Syntax Notation + + This specification uses the Augmented Backus-Naur Form (ABNF) + notation of [RFC5234], extended with the notation for case- + sensitivity in strings defined in [RFC7405]. + + It also uses a list extension, defined in Section 5.6.1 of [HTTP], + that allows for compact definition of comma-separated lists using a + "#" operator (similar to how the "*" operator indicates repetition). + Appendix A shows the collected grammar with all list operators + expanded to standard ABNF notation. + + As a convention, ABNF rule names prefixed with "obs-" denote obsolete + grammar rules that appear for historical reasons. + + The following core rules are included by reference, as defined in + [RFC5234], Appendix B.1: ALPHA (letters), CR (carriage return), CRLF + (CR LF), CTL (controls), DIGIT (decimal 0-9), DQUOTE (double quote), + HEXDIG (hexadecimal 0-9/A-F/a-f), HTAB (horizontal tab), LF (line + feed), OCTET (any 8-bit sequence of data), SP (space), and VCHAR (any + visible [USASCII] character). + + The rules below are defined in [HTTP]: + + BWS = + OWS = + RWS = + absolute-path = + field-name = + field-value = + obs-text = + quoted-string = + token = + transfer-coding = + + + The rules below are defined in [URI]: + + absolute-URI = + authority = + uri-host = + port = + query = + +2. Message + + HTTP/1.1 clients and servers communicate by sending messages. See + Section 3 of [HTTP] for the general terminology and core concepts of + HTTP. + +2.1. Message Format + + An HTTP/1.1 message consists of a start-line followed by a CRLF and a + sequence of octets in a format similar to the Internet Message Format + [RFC5322]: zero or more header field lines (collectively referred to + as the "headers" or the "header section"), an empty line indicating + the end of the header section, and an optional message body. + + HTTP-message = start-line CRLF + *( field-line CRLF ) + CRLF + [ message-body ] + + A message can be either a request from client to server or a response + from server to client. Syntactically, the two types of messages + differ only in the start-line, which is either a request-line (for + requests) or a status-line (for responses), and in the algorithm for + determining the length of the message body (Section 6). + + start-line = request-line / status-line + + In theory, a client could receive requests and a server could receive + responses, distinguishing them by their different start-line formats. + In practice, servers are implemented to only expect a request (a + response is interpreted as an unknown or invalid request method), and + clients are implemented to only expect a response. + + HTTP makes use of some protocol elements similar to the Multipurpose + Internet Mail Extensions (MIME) [RFC2045]. See Appendix B for the + differences between HTTP and MIME messages. + +2.2. Message Parsing + + The normal procedure for parsing an HTTP message is to read the + start-line into a structure, read each header field line into a hash + table by field name until the empty line, and then use the parsed + data to determine if a message body is expected. If a message body + has been indicated, then it is read as a stream until an amount of + octets equal to the message body length is read or the connection is + closed. + + A recipient MUST parse an HTTP message as a sequence of octets in an + encoding that is a superset of US-ASCII [USASCII]. Parsing an HTTP + message as a stream of Unicode characters, without regard for the + specific encoding, creates security vulnerabilities due to the + varying ways that string processing libraries handle invalid + multibyte character sequences that contain the octet LF (%x0A). + String-based parsers can only be safely used within protocol elements + after the element has been extracted from the message, such as within + a header field line value after message parsing has delineated the + individual field lines. + + Although the line terminator for the start-line and fields is the + sequence CRLF, a recipient MAY recognize a single LF as a line + terminator and ignore any preceding CR. + + A sender MUST NOT generate a bare CR (a CR character not immediately + followed by LF) within any protocol elements other than the content. + A recipient of such a bare CR MUST consider that element to be + invalid or replace each bare CR with SP before processing the element + or forwarding the message. + + Older HTTP/1.0 user agent implementations might send an extra CRLF + after a POST request as a workaround for some early server + applications that failed to read message body content that was not + terminated by a line-ending. An HTTP/1.1 user agent MUST NOT preface + or follow a request with an extra CRLF. If terminating the request + message body with a line-ending is desired, then the user agent MUST + count the terminating CRLF octets as part of the message body length. + + In the interest of robustness, a server that is expecting to receive + and parse a request-line SHOULD ignore at least one empty line (CRLF) + received prior to the request-line. + + A sender MUST NOT send whitespace between the start-line and the + first header field. + + A recipient that receives whitespace between the start-line and the + first header field MUST either reject the message as invalid or + consume each whitespace-preceded line without further processing of + it (i.e., ignore the entire line, along with any subsequent lines + preceded by whitespace, until a properly formed header field is + received or the header section is terminated). Rejection or removal + of invalid whitespace-preceded lines is necessary to prevent their + misinterpretation by downstream recipients that might be vulnerable + to request smuggling (Section 11.2) or response splitting + (Section 11.1) attacks. + + When a server listening only for HTTP request messages, or processing + what appears from the start-line to be an HTTP request message, + receives a sequence of octets that does not match the HTTP-message + grammar aside from the robustness exceptions listed above, the server + SHOULD respond with a 400 (Bad Request) response and close the + connection. + +2.3. HTTP Version + + HTTP uses a "." numbering scheme to indicate versions + of the protocol. This specification defines version "1.1". + Section 2.5 of [HTTP] specifies the semantics of HTTP version + numbers. + + The version of an HTTP/1.x message is indicated by an HTTP-version + field in the start-line. HTTP-version is case-sensitive. + + HTTP-version = HTTP-name "/" DIGIT "." DIGIT + HTTP-name = %s"HTTP" + + When an HTTP/1.1 message is sent to an HTTP/1.0 recipient [HTTP/1.0] + or a recipient whose version is unknown, the HTTP/1.1 message is + constructed such that it can be interpreted as a valid HTTP/1.0 + message if all of the newer features are ignored. This specification + places recipient-version requirements on some new features so that a + conformant sender will only use compatible features until it has + determined, through configuration or the receipt of a message, that + the recipient supports HTTP/1.1. + + Intermediaries that process HTTP messages (i.e., all intermediaries + other than those acting as tunnels) MUST send their own HTTP-version + in forwarded messages, unless it is purposefully downgraded as a + workaround for an upstream issue. In other words, an intermediary is + not allowed to blindly forward the start-line without ensuring that + the protocol version in that message matches a version to which that + intermediary is conformant for both the receiving and sending of + messages. Forwarding an HTTP message without rewriting the HTTP- + version might result in communication errors when downstream + recipients use the message sender's version to determine what + features are safe to use for later communication with that sender. + + A server MAY send an HTTP/1.0 response to an HTTP/1.1 request if it + is known or suspected that the client incorrectly implements the HTTP + specification and is incapable of correctly processing later version + responses, such as when a client fails to parse the version number + correctly or when an intermediary is known to blindly forward the + HTTP-version even when it doesn't conform to the given minor version + of the protocol. Such protocol downgrades SHOULD NOT be performed + unless triggered by specific client attributes, such as when one or + more of the request header fields (e.g., User-Agent) uniquely match + the values sent by a client known to be in error. + +3. Request Line + + A request-line begins with a method token, followed by a single space + (SP), the request-target, and another single space (SP), and ends + with the protocol version. + + request-line = method SP request-target SP HTTP-version + + Although the request-line grammar rule requires that each of the + component elements be separated by a single SP octet, recipients MAY + instead parse on whitespace-delimited word boundaries and, aside from + the CRLF terminator, treat any form of whitespace as the SP separator + while ignoring preceding or trailing whitespace; such whitespace + includes one or more of the following octets: SP, HTAB, VT (%x0B), FF + (%x0C), or bare CR. However, lenient parsing can result in request + smuggling security vulnerabilities if there are multiple recipients + of the message and each has its own unique interpretation of + robustness (see Section 11.2). + + HTTP does not place a predefined limit on the length of a request- + line, as described in Section 2.3 of [HTTP]. A server that receives + a method longer than any that it implements SHOULD respond with a 501 + (Not Implemented) status code. A server that receives a request- + target longer than any URI it wishes to parse MUST respond with a 414 + (URI Too Long) status code (see Section 15.5.15 of [HTTP]). + + Various ad hoc limitations on request-line length are found in + practice. It is RECOMMENDED that all HTTP senders and recipients + support, at a minimum, request-line lengths of 8000 octets. + +3.1. Method + + The method token indicates the request method to be performed on the + target resource. The request method is case-sensitive. + + method = token + + The request methods defined by this specification can be found in + Section 9 of [HTTP], along with information regarding the HTTP method + registry and considerations for defining new methods. + +3.2. Request Target + + The request-target identifies the target resource upon which to apply + the request. The client derives a request-target from its desired + target URI. There are four distinct formats for the request-target, + depending on both the method being requested and whether the request + is to a proxy. + + request-target = origin-form + / absolute-form + / authority-form + / asterisk-form + + No whitespace is allowed in the request-target. Unfortunately, some + user agents fail to properly encode or exclude whitespace found in + hypertext references, resulting in those disallowed characters being + sent as the request-target in a malformed request-line. + + Recipients of an invalid request-line SHOULD respond with either a + 400 (Bad Request) error or a 301 (Moved Permanently) redirect with + the request-target properly encoded. A recipient SHOULD NOT attempt + to autocorrect and then process the request without a redirect, since + the invalid request-line might be deliberately crafted to bypass + security filters along the request chain. + + A client MUST send a Host header field (Section 7.2 of [HTTP]) in all + HTTP/1.1 request messages. If the target URI includes an authority + component, then a client MUST send a field value for Host that is + identical to that authority component, excluding any userinfo + subcomponent and its "@" delimiter (Section 4.2 of [HTTP]). If the + authority component is missing or undefined for the target URI, then + a client MUST send a Host header field with an empty field value. + + A server MUST respond with a 400 (Bad Request) status code to any + HTTP/1.1 request message that lacks a Host header field and to any + request message that contains more than one Host header field line or + a Host header field with an invalid field value. + +3.2.1. origin-form + + The most common form of request-target is the "origin-form". + + origin-form = absolute-path [ "?" query ] + + When making a request directly to an origin server, other than a + CONNECT or server-wide OPTIONS request (as detailed below), a client + MUST send only the absolute path and query components of the target + URI as the request-target. If the target URI's path component is + empty, the client MUST send "/" as the path within the origin-form of + request-target. A Host header field is also sent, as defined in + Section 7.2 of [HTTP]. + + For example, a client wishing to retrieve a representation of the + resource identified as + + http://www.example.org/where?q=now + + directly from the origin server would open (or reuse) a TCP + connection to port 80 of the host "www.example.org" and send the + lines: + + GET /where?q=now HTTP/1.1 + Host: www.example.org + + followed by the remainder of the request message. + +3.2.2. absolute-form + + When making a request to a proxy, other than a CONNECT or server-wide + OPTIONS request (as detailed below), a client MUST send the target + URI in "absolute-form" as the request-target. + + absolute-form = absolute-URI + + The proxy is requested to either service that request from a valid + cache, if possible, or make the same request on the client's behalf + either to the next inbound proxy server or directly to the origin + server indicated by the request-target. Requirements on such + "forwarding" of messages are defined in Section 7.6 of [HTTP]. + + An example absolute-form of request-line would be: + + GET http://www.example.org/pub/WWW/TheProject.html HTTP/1.1 + + A client MUST send a Host header field in an HTTP/1.1 request even if + the request-target is in the absolute-form, since this allows the + Host information to be forwarded through ancient HTTP/1.0 proxies + that might not have implemented Host. + + When a proxy receives a request with an absolute-form of request- + target, the proxy MUST ignore the received Host header field (if any) + and instead replace it with the host information of the request- + target. A proxy that forwards such a request MUST generate a new + Host field value based on the received request-target rather than + forward the received Host field value. + + When an origin server receives a request with an absolute-form of + request-target, the origin server MUST ignore the received Host + header field (if any) and instead use the host information of the + request-target. Note that if the request-target does not have an + authority component, an empty Host header field will be sent in this + case. + + A server MUST accept the absolute-form in requests even though most + HTTP/1.1 clients will only send the absolute-form to a proxy. + +3.2.3. authority-form + + The "authority-form" of request-target is only used for CONNECT + requests (Section 9.3.6 of [HTTP]). It consists of only the uri-host + and port number of the tunnel destination, separated by a colon + (":"). + + authority-form = uri-host ":" port + + When making a CONNECT request to establish a tunnel through one or + more proxies, a client MUST send only the host and port of the tunnel + destination as the request-target. The client obtains the host and + port from the target URI's authority component, except that it sends + the scheme's default port if the target URI elides the port. For + example, a CONNECT request to "http://www.example.com" looks like the + following: + + CONNECT www.example.com:80 HTTP/1.1 + Host: www.example.com + +3.2.4. asterisk-form + + The "asterisk-form" of request-target is only used for a server-wide + OPTIONS request (Section 9.3.7 of [HTTP]). + + asterisk-form = "*" + + When a client wishes to request OPTIONS for the server as a whole, as + opposed to a specific named resource of that server, the client MUST + send only "*" (%x2A) as the request-target. For example, + + OPTIONS * HTTP/1.1 + + If a proxy receives an OPTIONS request with an absolute-form of + request-target in which the URI has an empty path and no query + component, then the last proxy on the request chain MUST send a + request-target of "*" when it forwards the request to the indicated + origin server. + + For example, the request + + OPTIONS http://www.example.org:8001 HTTP/1.1 + + would be forwarded by the final proxy as + + OPTIONS * HTTP/1.1 + Host: www.example.org:8001 + + after connecting to port 8001 of host "www.example.org". + +3.3. Reconstructing the Target URI + + The target URI is the request-target when the request-target is in + absolute-form. In that case, a server will parse the URI into its + generic components for further evaluation. + + Otherwise, the server reconstructs the target URI from the connection + context and various parts of the request message in order to identify + the target resource (Section 7.1 of [HTTP]): + + * If the server's configuration provides for a fixed URI scheme, or + a scheme is provided by a trusted outbound gateway, that scheme is + used for the target URI. This is common in large-scale + deployments because a gateway server will receive the client's + connection context and replace that with their own connection to + the inbound server. Otherwise, if the request is received over a + secured connection, the target URI's scheme is "https"; if not, + the scheme is "http". + + * If the request-target is in authority-form, the target URI's + authority component is the request-target. Otherwise, the target + URI's authority component is the field value of the Host header + field. If there is no Host header field or if its field value is + empty or invalid, the target URI's authority component is empty. + + * If the request-target is in authority-form or asterisk-form, the + target URI's combined path and query component is empty. + Otherwise, the target URI's combined path and query component is + the request-target. + + * The components of a reconstructed target URI, once determined as + above, can be recombined into absolute-URI form by concatenating + the scheme, "://", authority, and combined path and query + component. + + Example 1: The following message received over a secure connection + + GET /pub/WWW/TheProject.html HTTP/1.1 + Host: www.example.org + + has a target URI of + + https://www.example.org/pub/WWW/TheProject.html + + Example 2: The following message received over an insecure connection + + OPTIONS * HTTP/1.1 + Host: www.example.org:8080 + + has a target URI of + + http://www.example.org:8080 + + If the target URI's authority component is empty and its URI scheme + requires a non-empty authority (as is the case for "http" and + "https"), the server can reject the request or determine whether a + configured default applies that is consistent with the incoming + connection's context. Context might include connection details like + address and port, what security has been applied, and locally defined + information specific to that server's configuration. An empty + authority is replaced with the configured default before further + processing of the request. + + Supplying a default name for authority within the context of a + secured connection is inherently unsafe if there is any chance that + the user agent's intended authority might differ from the default. A + server that can uniquely identify an authority from the request + context MAY use that identity as a default without this risk. + Alternatively, it might be better to redirect the request to a safe + resource that explains how to obtain a new client. + + Note that reconstructing the client's target URI is only half of the + process for identifying a target resource. The other half is + determining whether that target URI identifies a resource for which + the server is willing and able to send a response, as defined in + Section 7.4 of [HTTP]. + +4. Status Line + + The first line of a response message is the status-line, consisting + of the protocol version, a space (SP), the status code, and another + space and ending with an OPTIONAL textual phrase describing the + status code. + + status-line = HTTP-version SP status-code SP [ reason-phrase ] + + Although the status-line grammar rule requires that each of the + component elements be separated by a single SP octet, recipients MAY + instead parse on whitespace-delimited word boundaries and, aside from + the line terminator, treat any form of whitespace as the SP separator + while ignoring preceding or trailing whitespace; such whitespace + includes one or more of the following octets: SP, HTAB, VT (%x0B), FF + (%x0C), or bare CR. However, lenient parsing can result in response + splitting security vulnerabilities if there are multiple recipients + of the message and each has its own unique interpretation of + robustness (see Section 11.1). + + The status-code element is a 3-digit integer code describing the + result of the server's attempt to understand and satisfy the client's + corresponding request. A recipient parses and interprets the + remainder of the response message in light of the semantics defined + for that status code, if the status code is recognized by that + recipient, or in accordance with the class of that status code when + the specific code is unrecognized. + + status-code = 3DIGIT + + HTTP's core status codes are defined in Section 15 of [HTTP], along + with the classes of status codes, considerations for the definition + of new status codes, and the IANA registry for collecting such + definitions. + + The reason-phrase element exists for the sole purpose of providing a + textual description associated with the numeric status code, mostly + out of deference to earlier Internet application protocols that were + more frequently used with interactive text clients. + + reason-phrase = 1*( HTAB / SP / VCHAR / obs-text ) + + A client SHOULD ignore the reason-phrase content because it is not a + reliable channel for information (it might be translated for a given + locale, overwritten by intermediaries, or discarded when the message + is forwarded via other versions of HTTP). A server MUST send the + space that separates the status-code from the reason-phrase even when + the reason-phrase is absent (i.e., the status-line would end with the + space). + +5. Field Syntax + + Each field line consists of a case-insensitive field name followed by + a colon (":"), optional leading whitespace, the field line value, and + optional trailing whitespace. + + field-line = field-name ":" OWS field-value OWS + + Rules for parsing within field values are defined in Section 5.5 of + [HTTP]. This section covers the generic syntax for header field + inclusion within, and extraction from, HTTP/1.1 messages. + +5.1. Field Line Parsing + + Messages are parsed using a generic algorithm, independent of the + individual field names. The contents within a given field line value + are not parsed until a later stage of message interpretation (usually + after the message's entire field section has been processed). + + No whitespace is allowed between the field name and colon. In the + past, differences in the handling of such whitespace have led to + security vulnerabilities in request routing and response handling. A + server MUST reject, with a response status code of 400 (Bad Request), + any received request message that contains whitespace between a + header field name and colon. A proxy MUST remove any such whitespace + from a response message before forwarding the message downstream. + + A field line value might be preceded and/or followed by optional + whitespace (OWS); a single SP preceding the field line value is + preferred for consistent readability by humans. The field line value + does not include that leading or trailing whitespace: OWS occurring + before the first non-whitespace octet of the field line value, or + after the last non-whitespace octet of the field line value, is + excluded by parsers when extracting the field line value from a field + line. + +5.2. Obsolete Line Folding + + Historically, HTTP/1.x field values could be extended over multiple + lines by preceding each extra line with at least one space or + horizontal tab (obs-fold). This specification deprecates such line + folding except within the "message/http" media type (Section 10.1). + + obs-fold = OWS CRLF RWS + ; obsolete line folding + + A sender MUST NOT generate a message that includes line folding + (i.e., that has any field line value that contains a match to the + obs-fold rule) unless the message is intended for packaging within + the "message/http" media type. + + A server that receives an obs-fold in a request message that is not + within a "message/http" container MUST either reject the message by + sending a 400 (Bad Request), preferably with a representation + explaining that obsolete line folding is unacceptable, or replace + each received obs-fold with one or more SP octets prior to + interpreting the field value or forwarding the message downstream. + + A proxy or gateway that receives an obs-fold in a response message + that is not within a "message/http" container MUST either discard the + message and replace it with a 502 (Bad Gateway) response, preferably + with a representation explaining that unacceptable line folding was + received, or replace each received obs-fold with one or more SP + octets prior to interpreting the field value or forwarding the + message downstream. + + A user agent that receives an obs-fold in a response message that is + not within a "message/http" container MUST replace each received + obs-fold with one or more SP octets prior to interpreting the field + value. + +6. Message Body + + The message body (if any) of an HTTP/1.1 message is used to carry + content (Section 6.4 of [HTTP]) for the request or response. The + message body is identical to the content unless a transfer coding has + been applied, as described in Section 6.1. + + message-body = *OCTET + + The rules for determining when a message body is present in an + HTTP/1.1 message differ for requests and responses. + + The presence of a message body in a request is signaled by a + Content-Length or Transfer-Encoding header field. Request message + framing is independent of method semantics. + + The presence of a message body in a response, as detailed in + Section 6.3, depends on both the request method to which it is + responding and the response status code. This corresponds to when + response content is allowed by HTTP semantics (Section 6.4.1 of + [HTTP]). + +6.1. Transfer-Encoding + + The Transfer-Encoding header field lists the transfer coding names + corresponding to the sequence of transfer codings that have been (or + will be) applied to the content in order to form the message body. + Transfer codings are defined in Section 7. + + Transfer-Encoding = #transfer-coding + ; defined in [HTTP], Section 10.1.4 + + Transfer-Encoding is analogous to the Content-Transfer-Encoding field + of MIME, which was designed to enable safe transport of binary data + over a 7-bit transport service ([RFC2045], Section 6). However, safe + transport has a different focus for an 8bit-clean transfer protocol. + In HTTP's case, Transfer-Encoding is primarily intended to accurately + delimit dynamically generated content. It also serves to distinguish + encodings that are only applied in transit from the encodings that + are a characteristic of the selected representation. + + A recipient MUST be able to parse the chunked transfer coding + (Section 7.1) because it plays a crucial role in framing messages + when the content size is not known in advance. A sender MUST NOT + apply the chunked transfer coding more than once to a message body + (i.e., chunking an already chunked message is not allowed). If any + transfer coding other than chunked is applied to a request's content, + the sender MUST apply chunked as the final transfer coding to ensure + that the message is properly framed. If any transfer coding other + than chunked is applied to a response's content, the sender MUST + either apply chunked as the final transfer coding or terminate the + message by closing the connection. + + For example, + + Transfer-Encoding: gzip, chunked + + indicates that the content has been compressed using the gzip coding + and then chunked using the chunked coding while forming the message + body. + + Unlike Content-Encoding (Section 8.4.1 of [HTTP]), Transfer-Encoding + is a property of the message, not of the representation. Any + recipient along the request/response chain MAY decode the received + transfer coding(s) or apply additional transfer coding(s) to the + message body, assuming that corresponding changes are made to the + Transfer-Encoding field value. Additional information about the + encoding parameters can be provided by other header fields not + defined by this specification. + + Transfer-Encoding MAY be sent in a response to a HEAD request or in a + 304 (Not Modified) response (Section 15.4.5 of [HTTP]) to a GET + request, neither of which includes a message body, to indicate that + the origin server would have applied a transfer coding to the message + body if the request had been an unconditional GET. This indication + is not required, however, because any recipient on the response chain + (including the origin server) can remove transfer codings when they + are not needed. + + A server MUST NOT send a Transfer-Encoding header field in any + response with a status code of 1xx (Informational) or 204 (No + Content). A server MUST NOT send a Transfer-Encoding header field in + any 2xx (Successful) response to a CONNECT request (Section 9.3.6 of + [HTTP]). + + A server that receives a request message with a transfer coding it + does not understand SHOULD respond with 501 (Not Implemented). + + Transfer-Encoding was added in HTTP/1.1. It is generally assumed + that implementations advertising only HTTP/1.0 support will not + understand how to process transfer-encoded content, and that an + HTTP/1.0 message received with a Transfer-Encoding is likely to have + been forwarded without proper handling of the chunked transfer coding + in transit. + + A client MUST NOT send a request containing Transfer-Encoding unless + it knows the server will handle HTTP/1.1 requests (or later minor + revisions); such knowledge might be in the form of specific user + configuration or by remembering the version of a prior received + response. A server MUST NOT send a response containing Transfer- + Encoding unless the corresponding request indicates HTTP/1.1 (or + later minor revisions). + + Early implementations of Transfer-Encoding would occasionally send + both a chunked transfer coding for message framing and an estimated + Content-Length header field for use by progress bars. This is why + Transfer-Encoding is defined as overriding Content-Length, as opposed + to them being mutually incompatible. Unfortunately, forwarding such + a message can lead to vulnerabilities regarding request smuggling + (Section 11.2) or response splitting (Section 11.1) attacks if any + downstream recipient fails to parse the message according to this + specification, particularly when a downstream recipient only + implements HTTP/1.0. + + A server MAY reject a request that contains both Content-Length and + Transfer-Encoding or process such a request in accordance with the + Transfer-Encoding alone. Regardless, the server MUST close the + connection after responding to such a request to avoid the potential + attacks. + + A server or client that receives an HTTP/1.0 message containing a + Transfer-Encoding header field MUST treat the message as if the + framing is faulty, even if a Content-Length is present, and close the + connection after processing the message. The message sender might + have retained a portion of the message, in buffer, that could be + misinterpreted by further use of the connection. + +6.2. Content-Length + + When a message does not have a Transfer-Encoding header field, a + Content-Length header field (Section 8.6 of [HTTP]) can provide the + anticipated size, as a decimal number of octets, for potential + content. For messages that do include content, the Content-Length + field value provides the framing information necessary for + determining where the data (and message) ends. For messages that do + not include content, the Content-Length indicates the size of the + selected representation (Section 8.6 of [HTTP]). + + A sender MUST NOT send a Content-Length header field in any message + that contains a Transfer-Encoding header field. + + | *Note:* HTTP's use of Content-Length for message framing + | differs significantly from the same field's use in MIME, where + | it is an optional field used only within the "message/external- + | body" media-type. + +6.3. Message Body Length + + The length of a message body is determined by one of the following + (in order of precedence): + + 1. Any response to a HEAD request and any response with a 1xx + (Informational), 204 (No Content), or 304 (Not Modified) status + code is always terminated by the first empty line after the + header fields, regardless of the header fields present in the + message, and thus cannot contain a message body or trailer + section. + + 2. Any 2xx (Successful) response to a CONNECT request implies that + the connection will become a tunnel immediately after the empty + line that concludes the header fields. A client MUST ignore any + Content-Length or Transfer-Encoding header fields received in + such a message. + + 3. If a message is received with both a Transfer-Encoding and a + Content-Length header field, the Transfer-Encoding overrides the + Content-Length. Such a message might indicate an attempt to + perform request smuggling (Section 11.2) or response splitting + (Section 11.1) and ought to be handled as an error. An + intermediary that chooses to forward the message MUST first + remove the received Content-Length field and process the + Transfer-Encoding (as described below) prior to forwarding the + message downstream. + + 4. If a Transfer-Encoding header field is present and the chunked + transfer coding (Section 7.1) is the final encoding, the message + body length is determined by reading and decoding the chunked + data until the transfer coding indicates the data is complete. + + If a Transfer-Encoding header field is present in a response and + the chunked transfer coding is not the final encoding, the + message body length is determined by reading the connection until + it is closed by the server. + + If a Transfer-Encoding header field is present in a request and + the chunked transfer coding is not the final encoding, the + message body length cannot be determined reliably; the server + MUST respond with the 400 (Bad Request) status code and then + close the connection. + + 5. If a message is received without Transfer-Encoding and with an + invalid Content-Length header field, then the message framing is + invalid and the recipient MUST treat it as an unrecoverable + error, unless the field value can be successfully parsed as a + comma-separated list (Section 5.6.1 of [HTTP]), all values in the + list are valid, and all values in the list are the same (in which + case, the message is processed with that single value used as the + Content-Length field value). If the unrecoverable error is in a + request message, the server MUST respond with a 400 (Bad Request) + status code and then close the connection. If it is in a + response message received by a proxy, the proxy MUST close the + connection to the server, discard the received response, and send + a 502 (Bad Gateway) response to the client. If it is in a + response message received by a user agent, the user agent MUST + close the connection to the server and discard the received + response. + + 6. If a valid Content-Length header field is present without + Transfer-Encoding, its decimal value defines the expected message + body length in octets. If the sender closes the connection or + the recipient times out before the indicated number of octets are + received, the recipient MUST consider the message to be + incomplete and close the connection. + + 7. If this is a request message and none of the above are true, then + the message body length is zero (no message body is present). + + 8. Otherwise, this is a response message without a declared message + body length, so the message body length is determined by the + number of octets received prior to the server closing the + connection. + + Since there is no way to distinguish a successfully completed, close- + delimited response message from a partially received message + interrupted by network failure, a server SHOULD generate encoding or + length-delimited messages whenever possible. The close-delimiting + feature exists primarily for backwards compatibility with HTTP/1.0. + + | *Note:* Request messages are never close-delimited because they + | are always explicitly framed by length or transfer coding, with + | the absence of both implying the request ends immediately after + | the header section. + + A server MAY reject a request that contains a message body but not a + Content-Length by responding with 411 (Length Required). + + Unless a transfer coding other than chunked has been applied, a + client that sends a request containing a message body SHOULD use a + valid Content-Length header field if the message body length is known + in advance, rather than the chunked transfer coding, since some + existing services respond to chunked with a 411 (Length Required) + status code even though they understand the chunked transfer coding. + This is typically because such services are implemented via a gateway + that requires a content length in advance of being called, and the + server is unable or unwilling to buffer the entire request before + processing. + + A user agent that sends a request that contains a message body MUST + send either a valid Content-Length header field or use the chunked + transfer coding. A client MUST NOT use the chunked transfer coding + unless it knows the server will handle HTTP/1.1 (or later) requests; + such knowledge can be in the form of specific user configuration or + by remembering the version of a prior received response. + + If the final response to the last request on a connection has been + completely received and there remains additional data to read, a user + agent MAY discard the remaining data or attempt to determine if that + data belongs as part of the prior message body, which might be the + case if the prior message's Content-Length value is incorrect. A + client MUST NOT process, cache, or forward such extra data as a + separate response, since such behavior would be vulnerable to cache + poisoning. + +7. Transfer Codings + + Transfer coding names are used to indicate an encoding transformation + that has been, can be, or might need to be applied to a message's + content in order to ensure "safe transport" through the network. + This differs from a content coding in that the transfer coding is a + property of the message rather than a property of the representation + that is being transferred. + + All transfer-coding names are case-insensitive and ought to be + registered within the HTTP Transfer Coding registry, as defined in + Section 7.3. They are used in the Transfer-Encoding (Section 6.1) + and TE (Section 10.1.4 of [HTTP]) header fields (the latter also + defining the "transfer-coding" grammar). + +7.1. Chunked Transfer Coding + + The chunked transfer coding wraps content in order to transfer it as + a series of chunks, each with its own size indicator, followed by an + OPTIONAL trailer section containing trailer fields. Chunked enables + content streams of unknown size to be transferred as a sequence of + length-delimited buffers, which enables the sender to retain + connection persistence and the recipient to know when it has received + the entire message. + + chunked-body = *chunk + last-chunk + trailer-section + CRLF + + chunk = chunk-size [ chunk-ext ] CRLF + chunk-data CRLF + chunk-size = 1*HEXDIG + last-chunk = 1*("0") [ chunk-ext ] CRLF + + chunk-data = 1*OCTET ; a sequence of chunk-size octets + + The chunk-size field is a string of hex digits indicating the size of + the chunk-data in octets. The chunked transfer coding is complete + when a chunk with a chunk-size of zero is received, possibly followed + by a trailer section, and finally terminated by an empty line. + + A recipient MUST be able to parse and decode the chunked transfer + coding. + + HTTP/1.1 does not define any means to limit the size of a chunked + response such that an intermediary can be assured of buffering the + entire response. Additionally, very large chunk sizes may cause + overflows or loss of precision if their values are not represented + accurately in a receiving implementation. Therefore, recipients MUST + anticipate potentially large hexadecimal numerals and prevent parsing + errors due to integer conversion overflows or precision loss due to + integer representation. + + The chunked coding does not define any parameters. Their presence + SHOULD be treated as an error. + +7.1.1. Chunk Extensions + + The chunked coding allows each chunk to include zero or more chunk + extensions, immediately following the chunk-size, for the sake of + supplying per-chunk metadata (such as a signature or hash), mid- + message control information, or randomization of message body size. + + chunk-ext = *( BWS ";" BWS chunk-ext-name + [ BWS "=" BWS chunk-ext-val ] ) + + chunk-ext-name = token + chunk-ext-val = token / quoted-string + + The chunked coding is specific to each connection and is likely to be + removed or recoded by each recipient (including intermediaries) + before any higher-level application would have a chance to inspect + the extensions. Hence, the use of chunk extensions is generally + limited to specialized HTTP services such as "long polling" (where + client and server can have shared expectations regarding the use of + chunk extensions) or for padding within an end-to-end secured + connection. + + A recipient MUST ignore unrecognized chunk extensions. A server + ought to limit the total length of chunk extensions received in a + request to an amount reasonable for the services provided, in the + same way that it applies length limitations and timeouts for other + parts of a message, and generate an appropriate 4xx (Client Error) + response if that amount is exceeded. + +7.1.2. Chunked Trailer Section + + A trailer section allows the sender to include additional fields at + the end of a chunked message in order to supply metadata that might + be dynamically generated while the content is sent, such as a message + integrity check, digital signature, or post-processing status. The + proper use and limitations of trailer fields are defined in + Section 6.5 of [HTTP]. + + trailer-section = *( field-line CRLF ) + + A recipient that removes the chunked coding from a message MAY + selectively retain or discard the received trailer fields. A + recipient that retains a received trailer field MUST either store/ + forward the trailer field separately from the received header fields + or merge the received trailer field into the header section. A + recipient MUST NOT merge a received trailer field into the header + section unless its corresponding header field definition explicitly + permits and instructs how the trailer field value can be safely + merged. + +7.1.3. Decoding Chunked + + A process for decoding the chunked transfer coding can be represented + in pseudo-code as: + + length := 0 + read chunk-size, chunk-ext (if any), and CRLF + while (chunk-size > 0) { + read chunk-data and CRLF + append chunk-data to content + length := length + chunk-size + read chunk-size, chunk-ext (if any), and CRLF + } + read trailer field + while (trailer field is not empty) { + if (trailer fields are stored/forwarded separately) { + append trailer field to existing trailer fields + } + else if (trailer field is understood and defined as mergeable) { + merge trailer field with existing header fields + } + else { + discard trailer field + } + read trailer field + } + Content-Length := length + Remove "chunked" from Transfer-Encoding + +7.2. Transfer Codings for Compression + + The following transfer coding names for compression are defined by + the same algorithm as their corresponding content coding: + + compress (and x-compress) + See Section 8.4.1.1 of [HTTP]. + + deflate + See Section 8.4.1.2 of [HTTP]. + + gzip (and x-gzip) + See Section 8.4.1.3 of [HTTP]. + + The compression codings do not define any parameters. The presence + of parameters with any of these compression codings SHOULD be treated + as an error. + +7.3. Transfer Coding Registry + + The "HTTP Transfer Coding Registry" defines the namespace for + transfer coding names. It is maintained at + . + + Registrations MUST include the following fields: + + * Name + + * Description + + * Pointer to specification text + + Names of transfer codings MUST NOT overlap with names of content + codings (Section 8.4.1 of [HTTP]) unless the encoding transformation + is identical, as is the case for the compression codings defined in + Section 7.2. + + The TE header field (Section 10.1.4 of [HTTP]) uses a pseudo- + parameter named "q" as the rank value when multiple transfer codings + are acceptable. Future registrations of transfer codings SHOULD NOT + define parameters called "q" (case-insensitively) in order to avoid + ambiguities. + + Values to be added to this namespace require IETF Review (see + Section 4.8 of [RFC8126]) and MUST conform to the purpose of transfer + coding defined in this specification. + + Use of program names for the identification of encoding formats is + not desirable and is discouraged for future encodings. + +7.4. Negotiating Transfer Codings + + The TE field (Section 10.1.4 of [HTTP]) is used in HTTP/1.1 to + indicate what transfer codings, besides chunked, the client is + willing to accept in the response and whether the client is willing + to preserve trailer fields in a chunked transfer coding. + + A client MUST NOT send the chunked transfer coding name in TE; + chunked is always acceptable for HTTP/1.1 recipients. + + Three examples of TE use are below. + + TE: deflate + TE: + TE: trailers, deflate;q=0.5 + + When multiple transfer codings are acceptable, the client MAY rank + the codings by preference using a case-insensitive "q" parameter + (similar to the qvalues used in content negotiation fields; see + Section 12.4.2 of [HTTP]). The rank value is a real number in the + range 0 through 1, where 0.001 is the least preferred and 1 is the + most preferred; a value of 0 means "not acceptable". + + If the TE field value is empty or if no TE field is present, the only + acceptable transfer coding is chunked. A message with no transfer + coding is always acceptable. + + The keyword "trailers" indicates that the sender will not discard + trailer fields, as described in Section 6.5 of [HTTP]. + + Since the TE header field only applies to the immediate connection, a + sender of TE MUST also send a "TE" connection option within the + Connection header field (Section 7.6.1 of [HTTP]) in order to prevent + the TE header field from being forwarded by intermediaries that do + not support its semantics. + +8. Handling Incomplete Messages + + A server that receives an incomplete request message, usually due to + a canceled request or a triggered timeout exception, MAY send an + error response prior to closing the connection. + + A client that receives an incomplete response message, which can + occur when a connection is closed prematurely or when decoding a + supposedly chunked transfer coding fails, MUST record the message as + incomplete. Cache requirements for incomplete responses are defined + in Section 3.3 of [CACHING]. + + If a response terminates in the middle of the header section (before + the empty line is received) and the status code might rely on header + fields to convey the full meaning of the response, then the client + cannot assume that meaning has been conveyed; the client might need + to repeat the request in order to determine what action to take next. + + A message body that uses the chunked transfer coding is incomplete if + the zero-sized chunk that terminates the encoding has not been + received. A message that uses a valid Content-Length is incomplete + if the size of the message body received (in octets) is less than the + value given by Content-Length. A response that has neither chunked + transfer coding nor Content-Length is terminated by closure of the + connection and, if the header section was received intact, is + considered complete unless an error was indicated by the underlying + connection (e.g., an "incomplete close" in TLS would leave the + response incomplete, as described in Section 9.8). + +9. Connection Management + + HTTP messaging is independent of the underlying transport- or + session-layer connection protocol(s). HTTP only presumes a reliable + transport with in-order delivery of requests and the corresponding + in-order delivery of responses. The mapping of HTTP request and + response structures onto the data units of an underlying transport + protocol is outside the scope of this specification. + + As described in Section 7.3 of [HTTP], the specific connection + protocols to be used for an HTTP interaction are determined by client + configuration and the target URI. For example, the "http" URI scheme + (Section 4.2.1 of [HTTP]) indicates a default connection of TCP over + IP, with a default TCP port of 80, but the client might be configured + to use a proxy via some other connection, port, or protocol. + + HTTP implementations are expected to engage in connection management, + which includes maintaining the state of current connections, + establishing a new connection or reusing an existing connection, + processing messages received on a connection, detecting connection + failures, and closing each connection. Most clients maintain + multiple connections in parallel, including more than one connection + per server endpoint. Most servers are designed to maintain thousands + of concurrent connections, while controlling request queues to enable + fair use and detect denial-of-service attacks. + +9.1. Establishment + + It is beyond the scope of this specification to describe how + connections are established via various transport- or session-layer + protocols. Each HTTP connection maps to one underlying transport + connection. + +9.2. Associating a Response to a Request + + HTTP/1.1 does not include a request identifier for associating a + given request message with its corresponding one or more response + messages. Hence, it relies on the order of response arrival to + correspond exactly to the order in which requests are made on the + same connection. More than one response message per request only + occurs when one or more informational responses (1xx; see + Section 15.2 of [HTTP]) precede a final response to the same request. + + A client that has more than one outstanding request on a connection + MUST maintain a list of outstanding requests in the order sent and + MUST associate each received response message on that connection to + the first outstanding request that has not yet received a final (non- + 1xx) response. + + If a client receives data on a connection that doesn't have + outstanding requests, the client MUST NOT consider that data to be a + valid response; the client SHOULD close the connection, since message + delimitation is now ambiguous, unless the data consists only of one + or more CRLF (which can be discarded per Section 2.2). + +9.3. Persistence + + HTTP/1.1 defaults to the use of "persistent connections", allowing + multiple requests and responses to be carried over a single + connection. HTTP implementations SHOULD support persistent + connections. + + A recipient determines whether a connection is persistent or not + based on the protocol version and Connection header field + (Section 7.6.1 of [HTTP]) in the most recently received message, if + any: + + * If the "close" connection option is present (Section 9.6), the + connection will not persist after the current response; else, + + * If the received protocol is HTTP/1.1 (or later), the connection + will persist after the current response; else, + + * If the received protocol is HTTP/1.0, the "keep-alive" connection + option is present, either the recipient is not a proxy or the + message is a response, and the recipient wishes to honor the + HTTP/1.0 "keep-alive" mechanism, the connection will persist after + the current response; otherwise, + + * The connection will close after the current response. + + A client that does not support persistent connections MUST send the + "close" connection option in every request message. + + A server that does not support persistent connections MUST send the + "close" connection option in every response message that does not + have a 1xx (Informational) status code. + + A client MAY send additional requests on a persistent connection + until it sends or receives a "close" connection option or receives an + HTTP/1.0 response without a "keep-alive" connection option. + + In order to remain persistent, all messages on a connection need to + have a self-defined message length (i.e., one not defined by closure + of the connection), as described in Section 6. A server MUST read + the entire request message body or close the connection after sending + its response; otherwise, the remaining data on a persistent + connection would be misinterpreted as the next request. Likewise, a + client MUST read the entire response message body if it intends to + reuse the same connection for a subsequent request. + + A proxy server MUST NOT maintain a persistent connection with an + HTTP/1.0 client (see Appendix C.2.2 for information and discussion of + the problems with the Keep-Alive header field implemented by many + HTTP/1.0 clients). + + See Appendix C.2.2 for more information on backwards compatibility + with HTTP/1.0 clients. + +9.3.1. Retrying Requests + + Connections can be closed at any time, with or without intention. + Implementations ought to anticipate the need to recover from + asynchronous close events. The conditions under which a client can + automatically retry a sequence of outstanding requests are defined in + Section 9.2.2 of [HTTP]. + +9.3.2. Pipelining + + A client that supports persistent connections MAY "pipeline" its + requests (i.e., send multiple requests without waiting for each + response). A server MAY process a sequence of pipelined requests in + parallel if they all have safe methods (Section 9.2.1 of [HTTP]), but + it MUST send the corresponding responses in the same order that the + requests were received. + + A client that pipelines requests SHOULD retry unanswered requests if + the connection closes before it receives all of the corresponding + responses. When retrying pipelined requests after a failed + connection (a connection not explicitly closed by the server in its + last complete response), a client MUST NOT pipeline immediately after + connection establishment, since the first remaining request in the + prior pipeline might have caused an error response that can be lost + again if multiple requests are sent on a prematurely closed + connection (see the TCP reset problem described in Section 9.6). + + Idempotent methods (Section 9.2.2 of [HTTP]) are significant to + pipelining because they can be automatically retried after a + connection failure. A user agent SHOULD NOT pipeline requests after + a non-idempotent method, until the final response status code for + that method has been received, unless the user agent has a means to + detect and recover from partial failure conditions involving the + pipelined sequence. + + An intermediary that receives pipelined requests MAY pipeline those + requests when forwarding them inbound, since it can rely on the + outbound user agent(s) to determine what requests can be safely + pipelined. If the inbound connection fails before receiving a + response, the pipelining intermediary MAY attempt to retry a sequence + of requests that have yet to receive a response if the requests all + have idempotent methods; otherwise, the pipelining intermediary + SHOULD forward any received responses and then close the + corresponding outbound connection(s) so that the outbound user + agent(s) can recover accordingly. + +9.4. Concurrency + + A client ought to limit the number of simultaneous open connections + that it maintains to a given server. + + Previous revisions of HTTP gave a specific number of connections as a + ceiling, but this was found to be impractical for many applications. + As a result, this specification does not mandate a particular maximum + number of connections but, instead, encourages clients to be + conservative when opening multiple connections. + + Multiple connections are typically used to avoid the "head-of-line + blocking" problem, wherein a request that takes significant server- + side processing and/or transfers very large content would block + subsequent requests on the same connection. However, each connection + consumes server resources. + + Furthermore, using multiple connections can cause undesirable side + effects in congested networks. Using larger numbers of connections + can also cause side effects in otherwise uncongested networks, + because their aggregate and initially synchronized sending behavior + can cause congestion that would not have been present if fewer + parallel connections had been used. + + Note that a server might reject traffic that it deems abusive or + characteristic of a denial-of-service attack, such as an excessive + number of open connections from a single client. + +9.5. Failures and Timeouts + + Servers will usually have some timeout value beyond which they will + no longer maintain an inactive connection. Proxy servers might make + this a higher value since it is likely that the client will be making + more connections through the same proxy server. The use of + persistent connections places no requirements on the length (or + existence) of this timeout for either the client or the server. + + A client or server that wishes to time out SHOULD issue a graceful + close on the connection. Implementations SHOULD constantly monitor + open connections for a received closure signal and respond to it as + appropriate, since prompt closure of both sides of a connection + enables allocated system resources to be reclaimed. + + A client, server, or proxy MAY close the transport connection at any + time. For example, a client might have started to send a new request + at the same time that the server has decided to close the "idle" + connection. From the server's point of view, the connection is being + closed while it was idle, but from the client's point of view, a + request is in progress. + + A server SHOULD sustain persistent connections, when possible, and + allow the underlying transport's flow-control mechanisms to resolve + temporary overloads rather than terminate connections with the + expectation that clients will retry. The latter technique can + exacerbate network congestion or server load. + + A client sending a message body SHOULD monitor the network connection + for an error response while it is transmitting the request. If the + client sees a response that indicates the server does not wish to + receive the message body and is closing the connection, the client + SHOULD immediately cease transmitting the body and close its side of + the connection. + +9.6. Tear-down + + The "close" connection option is defined as a signal that the sender + will close this connection after completion of the response. A + sender SHOULD send a Connection header field (Section 7.6.1 of + [HTTP]) containing the "close" connection option when it intends to + close a connection. For example, + + Connection: close + + as a request header field indicates that this is the last request + that the client will send on this connection, while in a response, + the same field indicates that the server is going to close this + connection after the response message is complete. + + Note that the field name "Close" is reserved, since using that name + as a header field might conflict with the "close" connection option. + + A client that sends a "close" connection option MUST NOT send further + requests on that connection (after the one containing the "close") + and MUST close the connection after reading the final response + message corresponding to this request. + + A server that receives a "close" connection option MUST initiate + closure of the connection (see below) after it sends the final + response to the request that contained the "close" connection option. + The server SHOULD send a "close" connection option in its final + response on that connection. The server MUST NOT process any further + requests received on that connection. + + A server that sends a "close" connection option MUST initiate closure + of the connection (see below) after it sends the response containing + the "close" connection option. The server MUST NOT process any + further requests received on that connection. + + A client that receives a "close" connection option MUST cease sending + requests on that connection and close the connection after reading + the response message containing the "close" connection option; if + additional pipelined requests had been sent on the connection, the + client SHOULD NOT assume that they will be processed by the server. + + If a server performs an immediate close of a TCP connection, there is + a significant risk that the client will not be able to read the last + HTTP response. If the server receives additional data from the + client on a fully closed connection, such as another request sent by + the client before receiving the server's response, the server's TCP + stack will send a reset packet to the client; unfortunately, the + reset packet might erase the client's unacknowledged input buffers + before they can be read and interpreted by the client's HTTP parser. + + To avoid the TCP reset problem, servers typically close a connection + in stages. First, the server performs a half-close by closing only + the write side of the read/write connection. The server then + continues to read from the connection until it receives a + corresponding close by the client, or until the server is reasonably + certain that its own TCP stack has received the client's + acknowledgement of the packet(s) containing the server's last + response. Finally, the server fully closes the connection. + + It is unknown whether the reset problem is exclusive to TCP or might + also be found in other transport connection protocols. + + Note that a TCP connection that is half-closed by the client does not + delimit a request message, nor does it imply that the client is no + longer interested in a response. In general, transport signals + cannot be relied upon to signal edge cases, since HTTP/1.1 is + independent of transport. + +9.7. TLS Connection Initiation + + Conceptually, HTTP/TLS is simply sending HTTP messages over a + connection secured via TLS [TLS13]. + + The HTTP client also acts as the TLS client. It initiates a + connection to the server on the appropriate port and sends the TLS + ClientHello to begin the TLS handshake. When the TLS handshake has + finished, the client may then initiate the first HTTP request. All + HTTP data MUST be sent as TLS "application data" but is otherwise + treated like a normal connection for HTTP (including potential reuse + as a persistent connection). + +9.8. TLS Connection Closure + + TLS uses an exchange of closure alerts prior to (non-error) + connection closure to provide secure connection closure; see + Section 6.1 of [TLS13]. When a valid closure alert is received, an + implementation can be assured that no further data will be received + on that connection. + + When an implementation knows that it has sent or received all the + message data that it cares about, typically by detecting HTTP message + boundaries, it might generate an "incomplete close" by sending a + closure alert and then closing the connection without waiting to + receive the corresponding closure alert from its peer. + + An incomplete close does not call into question the security of the + data already received, but it could indicate that subsequent data + might have been truncated. As TLS is not directly aware of HTTP + message framing, it is necessary to examine the HTTP data itself to + determine whether messages are complete. Handling of incomplete + messages is defined in Section 8. + + When encountering an incomplete close, a client SHOULD treat as + completed all requests for which it has received either + + 1. as much data as specified in the Content-Length header field or + + 2. the terminal zero-length chunk (when Transfer-Encoding of chunked + is used). + + A response that has neither chunked transfer coding nor Content- + Length is complete only if a valid closure alert has been received. + Treating an incomplete message as complete could expose + implementations to attack. + + A client detecting an incomplete close SHOULD recover gracefully. + + Clients MUST send a closure alert before closing the connection. + Clients that do not expect to receive any more data MAY choose not to + wait for the server's closure alert and simply close the connection, + thus generating an incomplete close on the server side. + + Servers SHOULD be prepared to receive an incomplete close from the + client, since the client can often locate the end of server data. + + Servers MUST attempt to initiate an exchange of closure alerts with + the client before closing the connection. Servers MAY close the + connection after sending the closure alert, thus generating an + incomplete close on the client side. + +10. Enclosing Messages as Data + +10.1. Media Type message/http + + The "message/http" media type can be used to enclose a single HTTP + request or response message, provided that it obeys the MIME + restrictions for all "message" types regarding line length and + encodings. Because of the line length limitations, field values + within "message/http" are allowed to use line folding (obs-fold), as + described in Section 5.2, to convey the field value over multiple + lines. A recipient of "message/http" data MUST replace any obsolete + line folding with one or more SP characters when the message is + consumed. + + Type name: message + + Subtype name: http + + Required parameters: N/A + + Optional parameters: version, msgtype + + version: The HTTP-version number of the enclosed message (e.g., + "1.1"). If not present, the version can be determined from the + first line of the body. + + msgtype: The message type -- "request" or "response". If not + present, the type can be determined from the first line of the + body. + + Encoding considerations: only "7bit", "8bit", or "binary" are + permitted + + Security considerations: see Section 11 + + Interoperability considerations: N/A + + Published specification: RFC 9112 (see Section 10.1). + + Applications that use this media type: N/A + + Fragment identifier considerations: N/A + + Additional information: Magic number(s): N/A + + Deprecated alias names for this type: N/A + + File extension(s): N/A + + Macintosh file type code(s): N/A + + Person and email address to contact for further information: See Aut + hors' Addresses section. + + Intended usage: COMMON + + Restrictions on usage: N/A + + Author: See Authors' Addresses section. + + Change controller: IESG + +10.2. Media Type application/http + + The "application/http" media type can be used to enclose a pipeline + of one or more HTTP request or response messages (not intermixed). + + Type name: application + + Subtype name: http + + Required parameters: N/A + + Optional parameters: version, msgtype + + version: The HTTP-version number of the enclosed messages (e.g., + "1.1"). If not present, the version can be determined from the + first line of the body. + + msgtype: The message type -- "request" or "response". If not + present, the type can be determined from the first line of the + body. + + Encoding considerations: HTTP messages enclosed by this type are in + "binary" format; use of an appropriate Content-Transfer-Encoding + is required when transmitted via email. + + Security considerations: see Section 11 + + Interoperability considerations: N/A + + Published specification: RFC 9112 (see Section 10.2). + + Applications that use this media type: N/A + + Fragment identifier considerations: N/A + + Additional information: Deprecated alias names for this type: N/A + + Magic number(s): N/A + + File extension(s): N/A + + Macintosh file type code(s): N/A + + Person and email address to contact for further information: See Aut + hors' Addresses section. + + Intended usage: COMMON + + Restrictions on usage: N/A + + Author: See Authors' Addresses section. + + Change controller: IESG + +11. Security Considerations + + This section is meant to inform developers, information providers, + and users about known security considerations relevant to HTTP + message syntax and parsing. Security considerations about HTTP + semantics, content, and routing are addressed in [HTTP]. + +11.1. Response Splitting + + Response splitting (a.k.a. CRLF injection) is a common technique, + used in various attacks on Web usage, that exploits the line-based + nature of HTTP message framing and the ordered association of + requests to responses on persistent connections [Klein]. This + technique can be particularly damaging when the requests pass through + a shared cache. + + Response splitting exploits a vulnerability in servers (usually + within an application server) where an attacker can send encoded data + within some parameter of the request that is later decoded and echoed + within any of the response header fields of the response. If the + decoded data is crafted to look like the response has ended and a + subsequent response has begun, the response has been split, and the + content within the apparent second response is controlled by the + attacker. The attacker can then make any other request on the same + persistent connection and trick the recipients (including + intermediaries) into believing that the second half of the split is + an authoritative answer to the second request. + + For example, a parameter within the request-target might be read by + an application server and reused within a redirect, resulting in the + same parameter being echoed in the Location header field of the + response. If the parameter is decoded by the application and not + properly encoded when placed in the response field, the attacker can + send encoded CRLF octets and other content that will make the + application's single response look like two or more responses. + + A common defense against response splitting is to filter requests for + data that looks like encoded CR and LF (e.g., "%0D" and "%0A"). + However, that assumes the application server is only performing URI + decoding rather than more obscure data transformations like charset + transcoding, XML entity translation, base64 decoding, sprintf + reformatting, etc. A more effective mitigation is to prevent + anything other than the server's core protocol libraries from sending + a CR or LF within the header section, which means restricting the + output of header fields to APIs that filter for bad octets and not + allowing application servers to write directly to the protocol + stream. + +11.2. Request Smuggling + + Request smuggling ([Linhart]) is a technique that exploits + differences in protocol parsing among various recipients to hide + additional requests (which might otherwise be blocked or disabled by + policy) within an apparently harmless request. Like response + splitting, request smuggling can lead to a variety of attacks on HTTP + usage. + + This specification has introduced new requirements on request + parsing, particularly with regard to message framing in Section 6.3, + to reduce the effectiveness of request smuggling. + +11.3. Message Integrity + + HTTP does not define a specific mechanism for ensuring message + integrity, instead relying on the error-detection ability of + underlying transport protocols and the use of length or chunk- + delimited framing to detect completeness. Historically, the lack of + a single integrity mechanism has been justified by the informal + nature of most HTTP communication. However, the prevalence of HTTP + as an information access mechanism has resulted in its increasing use + within environments where verification of message integrity is + crucial. + + The mechanisms provided with the "https" scheme, such as + authenticated encryption, provide protection against modification of + messages. Care is needed, however, to ensure that connection closure + cannot be used to truncate messages (see Section 9.8). User agents + might refuse to accept incomplete messages or treat them specially. + For example, a browser being used to view medical history or drug + interaction information needs to indicate to the user when such + information is detected by the protocol to be incomplete, expired, or + corrupted during transfer. Such mechanisms might be selectively + enabled via user agent extensions or the presence of message + integrity metadata in a response. + + The "http" scheme provides no protection against accidental or + malicious modification of messages. + + Extensions to the protocol might be used to mitigate the risk of + unwanted modification of messages by intermediaries, even when the + "https" scheme is used. Integrity might be assured by using message + authentication codes or digital signatures that are selectively added + to messages via extensible metadata fields. + +11.4. Message Confidentiality + + HTTP relies on underlying transport protocols to provide message + confidentiality when that is desired. HTTP has been specifically + designed to be independent of the transport protocol, such that it + can be used over many forms of encrypted connection, with the + selection of such transports being identified by the choice of URI + scheme or within user agent configuration. + + The "https" scheme can be used to identify resources that require a + confidential connection, as described in Section 4.2.2 of [HTTP]. + +12. IANA Considerations + + The change controller for the following registrations is: "IETF + (iesg@ietf.org) - Internet Engineering Task Force". + +12.1. Field Name Registration + + IANA has added the following field names to the "Hypertext Transfer + Protocol (HTTP) Field Name Registry" at + , as described in + Section 18.4 of [HTTP]. + + +===================+===========+=========+============+ + | Field Name | Status | Section | Comments | + +===================+===========+=========+============+ + | Close | permanent | 9.6 | (reserved) | + +-------------------+-----------+---------+------------+ + | MIME-Version | permanent | B.1 | | + +-------------------+-----------+---------+------------+ + | Transfer-Encoding | permanent | 6.1 | | + +-------------------+-----------+---------+------------+ + + Table 1 + +12.2. Media Type Registration + + IANA has updated the "Media Types" registry at + with the registration + information in Sections 10.1 and 10.2 for the media types "message/ + http" and "application/http", respectively. + +12.3. Transfer Coding Registration + + IANA has updated the "HTTP Transfer Coding Registry" at + with the + registration procedure of Section 7.3 and the content coding names + summarized in the table below. + + +============+===========================================+=========+ + | Name | Description | Section | + +============+===========================================+=========+ + | chunked | Transfer in a series of chunks | 7.1 | + +------------+-------------------------------------------+---------+ + | compress | UNIX "compress" data format [Welch] | 7.2 | + +------------+-------------------------------------------+---------+ + | deflate | "deflate" compressed data ([RFC1951]) | 7.2 | + | | inside the "zlib" data format ([RFC1950]) | | + +------------+-------------------------------------------+---------+ + | gzip | GZIP file format [RFC1952] | 7.2 | + +------------+-------------------------------------------+---------+ + | trailers | (reserved) | 12.3 | + +------------+-------------------------------------------+---------+ + | x-compress | Deprecated (alias for compress) | 7.2 | + +------------+-------------------------------------------+---------+ + | x-gzip | Deprecated (alias for gzip) | 7.2 | + +------------+-------------------------------------------+---------+ + + Table 2 + + | *Note:* the coding name "trailers" is reserved because its use + | would conflict with the keyword "trailers" in the TE header + | field (Section 10.1.4 of [HTTP]). + +12.4. ALPN Protocol ID Registration + + IANA has updated the "TLS Application-Layer Protocol Negotiation + (ALPN) Protocol IDs" registry at with the registration below: + + +==========+=============================+===========+ + | Protocol | Identification Sequence | Reference | + +==========+=============================+===========+ + | HTTP/1.1 | 0x68 0x74 0x74 0x70 0x2f | RFC 9112 | + | | 0x31 0x2e 0x31 ("http/1.1") | | + +----------+-----------------------------+-----------+ + + Table 3 + +13. References + +13.1. Normative References + + [CACHING] Fielding, R., Ed., Nottingham, M., Ed., and J. Reschke, + Ed., "HTTP Caching", STD 98, RFC 9111, + DOI 10.17487/RFC9111, June 2022, + . + + [HTTP] Fielding, R., Ed., Nottingham, M., Ed., and J. Reschke, + Ed., "HTTP Semantics", STD 97, RFC 9110, + DOI 10.17487/RFC9110, June 2022, + . + + [RFC1950] Deutsch, P. and J-L. Gailly, "ZLIB Compressed Data Format + Specification version 3.3", RFC 1950, + DOI 10.17487/RFC1950, May 1996, + . + + [RFC1951] Deutsch, P., "DEFLATE Compressed Data Format Specification + version 1.3", RFC 1951, DOI 10.17487/RFC1951, May 1996, + . + + [RFC1952] Deutsch, P., "GZIP file format specification version 4.3", + RFC 1952, DOI 10.17487/RFC1952, May 1996, + . + + [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate + Requirement Levels", BCP 14, RFC 2119, + DOI 10.17487/RFC2119, March 1997, + . + + [RFC5234] Crocker, D., Ed. and P. Overell, "Augmented BNF for Syntax + Specifications: ABNF", STD 68, RFC 5234, + DOI 10.17487/RFC5234, January 2008, + . + + [RFC7405] Kyzivat, P., "Case-Sensitive String Support in ABNF", + RFC 7405, DOI 10.17487/RFC7405, December 2014, + . + + [RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC + 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, + May 2017, . + + [TLS13] Rescorla, E., "The Transport Layer Security (TLS) Protocol + Version 1.3", RFC 8446, DOI 10.17487/RFC8446, August 2018, + . + + [URI] Berners-Lee, T., Fielding, R., and L. Masinter, "Uniform + Resource Identifier (URI): Generic Syntax", STD 66, + RFC 3986, DOI 10.17487/RFC3986, January 2005, + . + + [USASCII] American National Standards Institute, "Coded Character + Set -- 7-bit American Standard Code for Information + Interchange", ANSI X3.4, 1986. + + [Welch] Welch, T., "A Technique for High-Performance Data + Compression", IEEE Computer 17(6), + DOI 10.1109/MC.1984.1659158, June 1984, + . + +13.2. Informative References + + [HTTP/1.0] Berners-Lee, T., Fielding, R., and H. Frystyk, "Hypertext + Transfer Protocol -- HTTP/1.0", RFC 1945, + DOI 10.17487/RFC1945, May 1996, + . + + [Klein] Klein, A., "Divide and Conquer - HTTP Response Splitting, + Web Cache Poisoning Attacks, and Related Topics", March + 2004, . + + [Linhart] Linhart, C., Klein, A., Heled, R., and S. Orrin, "HTTP + Request Smuggling", June 2005, + . + + [RFC2045] Freed, N. and N. Borenstein, "Multipurpose Internet Mail + Extensions (MIME) Part One: Format of Internet Message + Bodies", RFC 2045, DOI 10.17487/RFC2045, November 1996, + . + + [RFC2046] Freed, N. and N. Borenstein, "Multipurpose Internet Mail + Extensions (MIME) Part Two: Media Types", RFC 2046, + DOI 10.17487/RFC2046, November 1996, + . + + [RFC2049] Freed, N. and N. Borenstein, "Multipurpose Internet Mail + Extensions (MIME) Part Five: Conformance Criteria and + Examples", RFC 2049, DOI 10.17487/RFC2049, November 1996, + . + + [RFC2068] Fielding, R., Gettys, J., Mogul, J., Frystyk, H., and T. + Berners-Lee, "Hypertext Transfer Protocol -- HTTP/1.1", + RFC 2068, DOI 10.17487/RFC2068, January 1997, + . + + [RFC2557] Palme, J., Hopmann, A., and N. Shelness, "MIME + Encapsulation of Aggregate Documents, such as HTML + (MHTML)", RFC 2557, DOI 10.17487/RFC2557, March 1999, + . + + [RFC5322] Resnick, P., Ed., "Internet Message Format", RFC 5322, + DOI 10.17487/RFC5322, October 2008, + . + + [RFC7230] Fielding, R., Ed. and J. Reschke, Ed., "Hypertext Transfer + Protocol (HTTP/1.1): Message Syntax and Routing", + RFC 7230, DOI 10.17487/RFC7230, June 2014, + . + + [RFC8126] Cotton, M., Leiba, B., and T. Narten, "Guidelines for + Writing an IANA Considerations Section in RFCs", BCP 26, + RFC 8126, DOI 10.17487/RFC8126, June 2017, + . + +Appendix A. Collected ABNF + + In the collected ABNF below, list rules are expanded per + Section 5.6.1 of [HTTP]. + + BWS = + + HTTP-message = start-line CRLF *( field-line CRLF ) CRLF [ + message-body ] + HTTP-name = %x48.54.54.50 ; HTTP + HTTP-version = HTTP-name "/" DIGIT "." DIGIT + + OWS = + + RWS = + + Transfer-Encoding = [ transfer-coding *( OWS "," OWS transfer-coding + ) ] + + absolute-URI = + absolute-form = absolute-URI + absolute-path = + asterisk-form = "*" + authority = + authority-form = uri-host ":" port + + chunk = chunk-size [ chunk-ext ] CRLF chunk-data CRLF + chunk-data = 1*OCTET + chunk-ext = *( BWS ";" BWS chunk-ext-name [ BWS "=" BWS chunk-ext-val + ] ) + chunk-ext-name = token + chunk-ext-val = token / quoted-string + chunk-size = 1*HEXDIG + chunked-body = *chunk last-chunk trailer-section CRLF + + field-line = field-name ":" OWS field-value OWS + field-name = + field-value = + + last-chunk = 1*"0" [ chunk-ext ] CRLF + + message-body = *OCTET + method = token + + obs-fold = OWS CRLF RWS + obs-text = + origin-form = absolute-path [ "?" query ] + + port = + + query = + quoted-string = + + reason-phrase = 1*( HTAB / SP / VCHAR / obs-text ) + request-line = method SP request-target SP HTTP-version + request-target = origin-form / absolute-form / authority-form / + asterisk-form + + start-line = request-line / status-line + status-code = 3DIGIT + status-line = HTTP-version SP status-code SP [ reason-phrase ] + + token = + trailer-section = *( field-line CRLF ) + transfer-coding = + + uri-host = + +Appendix B. Differences between HTTP and MIME + + HTTP/1.1 uses many of the constructs defined for the Internet Message + Format [RFC5322] and Multipurpose Internet Mail Extensions (MIME) + [RFC2045] to allow a message body to be transmitted in an open + variety of representations and with extensible fields. However, some + of these constructs have been reinterpreted to better fit the needs + of interactive communication, leading to some differences in how MIME + constructs are used within HTTP. These differences were carefully + chosen to optimize performance over binary connections, allow greater + freedom in the use of new media types, ease date comparisons, and + accommodate common implementations. + + This appendix describes specific areas where HTTP differs from MIME. + Proxies and gateways to and from strict MIME environments need to be + aware of these differences and provide the appropriate conversions + where necessary. + +B.1. MIME-Version + + HTTP is not a MIME-compliant protocol. However, messages can include + a single MIME-Version header field to indicate what version of the + MIME protocol was used to construct the message. Use of the MIME- + Version header field indicates that the message is in full + conformance with the MIME protocol (as defined in [RFC2045]). + Senders are responsible for ensuring full conformance (where + possible) when exporting HTTP messages to strict MIME environments. + +B.2. Conversion to Canonical Form + + MIME requires that an Internet mail body part be converted to + canonical form prior to being transferred, as described in Section 4 + of [RFC2049], and that content with a type of "text" represents line + breaks as CRLF, forbidding the use of CR or LF outside of line break + sequences [RFC2046]. In contrast, HTTP does not care whether CRLF, + bare CR, or bare LF are used to indicate a line break within content. + + A proxy or gateway from HTTP to a strict MIME environment ought to + translate all line breaks within text media types to the RFC 2049 + canonical form of CRLF. Note, however, this might be complicated by + the presence of a Content-Encoding and by the fact that HTTP allows + the use of some charsets that do not use octets 13 and 10 to + represent CR and LF, respectively. + + Conversion will break any cryptographic checksums applied to the + original content unless the original content is already in canonical + form. Therefore, the canonical form is recommended for any content + that uses such checksums in HTTP. + +B.3. Conversion of Date Formats + + HTTP/1.1 uses a restricted set of date formats (Section 5.6.7 of + [HTTP]) to simplify the process of date comparison. Proxies and + gateways from other protocols ought to ensure that any Date header + field present in a message conforms to one of the HTTP/1.1 formats + and rewrite the date if necessary. + +B.4. Conversion of Content-Encoding + + MIME does not include any concept equivalent to HTTP's Content- + Encoding header field. Since this acts as a modifier on the media + type, proxies and gateways from HTTP to MIME-compliant protocols + ought to either change the value of the Content-Type header field or + decode the representation before forwarding the message. (Some + experimental applications of Content-Type for Internet mail have used + a media-type parameter of ";conversions=" to perform + a function equivalent to Content-Encoding. However, this parameter + is not part of the MIME standards.) + +B.5. Conversion of Content-Transfer-Encoding + + HTTP does not use the Content-Transfer-Encoding field of MIME. + Proxies and gateways from MIME-compliant protocols to HTTP need to + remove any Content-Transfer-Encoding prior to delivering the response + message to an HTTP client. + + Proxies and gateways from HTTP to MIME-compliant protocols are + responsible for ensuring that the message is in the correct format + and encoding for safe transport on that protocol, where "safe + transport" is defined by the limitations of the protocol being used. + Such a proxy or gateway ought to transform and label the data with an + appropriate Content-Transfer-Encoding if doing so will improve the + likelihood of safe transport over the destination protocol. + +B.6. MHTML and Line Length Limitations + + HTTP implementations that share code with MHTML [RFC2557] + implementations need to be aware of MIME line length limitations. + Since HTTP does not have this limitation, HTTP does not fold long + lines. MHTML messages being transported by HTTP follow all + conventions of MHTML, including line length limitations and folding, + canonicalization, etc., since HTTP transfers message-bodies without + modification and, aside from the "multipart/byteranges" type + (Section 14.6 of [HTTP]), does not interpret the content or any MIME + header lines that might be contained therein. + +Appendix C. Changes from Previous RFCs + +C.1. Changes from HTTP/0.9 + + Since HTTP/0.9 did not support header fields in a request, there is + no mechanism for it to support name-based virtual hosts (selection of + resource by inspection of the Host header field). Any server that + implements name-based virtual hosts ought to disable support for + HTTP/0.9. Most requests that appear to be HTTP/0.9 are, in fact, + badly constructed HTTP/1.x requests caused by a client failing to + properly encode the request-target. + +C.2. Changes from HTTP/1.0 + +C.2.1. Multihomed Web Servers + + The requirements that clients and servers support the Host header + field (Section 7.2 of [HTTP]), report an error if it is missing from + an HTTP/1.1 request, and accept absolute URIs (Section 3.2) are among + the most important changes defined by HTTP/1.1. + + Older HTTP/1.0 clients assumed a one-to-one relationship of IP + addresses and servers; there was no established mechanism for + distinguishing the intended server of a request other than the IP + address to which that request was directed. The Host header field + was introduced during the development of HTTP/1.1 and, though it was + quickly implemented by most HTTP/1.0 browsers, additional + requirements were placed on all HTTP/1.1 requests in order to ensure + complete adoption. At the time of this writing, most HTTP-based + services are dependent upon the Host header field for targeting + requests. + +C.2.2. Keep-Alive Connections + + In HTTP/1.0, each connection is established by the client prior to + the request and closed by the server after sending the response. + However, some implementations implement the explicitly negotiated + ("Keep-Alive") version of persistent connections described in + Section 19.7.1 of [RFC2068]. + + Some clients and servers might wish to be compatible with these + previous approaches to persistent connections, by explicitly + negotiating for them with a "Connection: keep-alive" request header + field. However, some experimental implementations of HTTP/1.0 + persistent connections are faulty; for example, if an HTTP/1.0 proxy + server doesn't understand Connection, it will erroneously forward + that header field to the next inbound server, which would result in a + hung connection. + + One attempted solution was the introduction of a Proxy-Connection + header field, targeted specifically at proxies. In practice, this + was also unworkable, because proxies are often deployed in multiple + layers, bringing about the same problem discussed above. + + As a result, clients are encouraged not to send the Proxy-Connection + header field in any requests. + + Clients are also encouraged to consider the use of "Connection: keep- + alive" in requests carefully; while they can enable persistent + connections with HTTP/1.0 servers, clients using them will need to + monitor the connection for "hung" requests (which indicate that the + client ought to stop sending the header field), and this mechanism + ought not be used by clients at all when a proxy is being used. + +C.2.3. Introduction of Transfer-Encoding + + HTTP/1.1 introduces the Transfer-Encoding header field (Section 6.1). + Transfer codings need to be decoded prior to forwarding an HTTP + message over a MIME-compliant protocol. + +C.3. Changes from RFC 7230 + + Most of the sections introducing HTTP's design goals, history, + architecture, conformance criteria, protocol versioning, URIs, + message routing, and header fields have been moved to [HTTP]. This + document has been reduced to just the messaging syntax and connection + management requirements specific to HTTP/1.1. + + Bare CRs have been prohibited outside of content. (Section 2.2) + + The ABNF definition of authority-form has changed from the more + general authority component of a URI (in which port is optional) to + the specific host:port format that is required by CONNECT. + (Section 3.2.3) + + Recipients are required to avoid smuggling/splitting attacks when + processing an ambiguous message framing. (Section 6.1) + + In the ABNF for chunked extensions, (bad) whitespace around ";" and + "=" has been reintroduced. Whitespace was removed in [RFC7230], but + that change was found to break existing implementations. + (Section 7.1.1) + + Trailer field semantics now transcend the specifics of chunked + transfer coding. The decoding algorithm for chunked (Section 7.1.3) + has been updated to encourage storage/forwarding of trailer fields + separately from the header section, to only allow merging into the + header section if the recipient knows the corresponding field + definition permits and defines how to merge, and otherwise to discard + the trailer fields instead of merging. The trailer part is now + called the trailer section to be more consistent with the header + section and more distinct from a body part. (Section 7.1.2) + + Transfer coding parameters called "q" are disallowed in order to + avoid conflicts with the use of ranks in the TE header field. + (Section 7.3) + +Acknowledgements + + See Appendix "Acknowledgements" of [HTTP], which applies to this + document as well. + +Index + + A C D F G H M O R T X + + A + + absolute-form (of request-target) Section 3.2.2 + application/http Media Type *_Section 10.2_* + asterisk-form (of request-target) Section 3.2.4 + authority-form (of request-target) Section 3.2.3 + + C + + chunked (Coding Format) Section 6.1; Section 6.3 + chunked (transfer coding) *_Section 7.1_* + close Section 9.3; *_Section 9.6_* + compress (transfer coding) *_Section 7.2_* + Connection header field Section 9.6 + Content-Length header field Section 6.2 + Content-Transfer-Encoding header field Appendix B.5 + + D + + deflate (transfer coding) *_Section 7.2_* + + F + + Fields + Close *_Section 9.6, Paragraph 4_* + MIME-Version *_Appendix B.1_* + Transfer-Encoding *_Section 6.1_* + + G + + Grammar + ALPHA *_Section 1.2_* + CR *_Section 1.2_* + CRLF *_Section 1.2_* + CTL *_Section 1.2_* + DIGIT *_Section 1.2_* + DQUOTE *_Section 1.2_* + HEXDIG *_Section 1.2_* + HTAB *_Section 1.2_* + HTTP-message *_Section 2.1_* + HTTP-name *_Section 2.3_* + HTTP-version *_Section 2.3_* + LF *_Section 1.2_* + OCTET *_Section 1.2_* + SP *_Section 1.2_* + Transfer-Encoding *_Section 6.1_* + VCHAR *_Section 1.2_* + absolute-form Section 3.2; *_Section 3.2.2_* + asterisk-form Section 3.2; *_Section 3.2.4_* + authority-form Section 3.2; *_Section 3.2.3_* + chunk *_Section 7.1_* + chunk-data *_Section 7.1_* + chunk-ext Section 7.1; *_Section 7.1.1_* + chunk-ext-name *_Section 7.1.1_* + chunk-ext-val *_Section 7.1.1_* + chunk-size *_Section 7.1_* + chunked-body *_Section 7.1_* + field-line *_Section 5_*; Section 7.1.2 + field-name Section 5 + field-value Section 5 + last-chunk *_Section 7.1_* + message-body *_Section 6_* + method *_Section 3.1_* + obs-fold *_Section 5.2_* + origin-form Section 3.2; *_Section 3.2.1_* + reason-phrase *_Section 4_* + request-line *_Section 3_* + request-target *_Section 3.2_* + start-line *_Section 2.1_* + status-code *_Section 4_* + status-line *_Section 4_* + trailer-section Section 7.1; *_Section 7.1.2_* + gzip (transfer coding) *_Section 7.2_* + + H + + Header Fields + MIME-Version *_Appendix B.1_* + Transfer-Encoding *_Section 6.1_* + header line Section 2.1 + header section Section 2.1 + headers Section 2.1 + + M + + Media Type + application/http *_Section 10.2_* + message/http *_Section 10.1_* + message/http Media Type *_Section 10.1_* + method *_Section 3.1_* + MIME-Version header field *_Appendix B.1_* + + O + + origin-form (of request-target) Section 3.2.1 + + R + + request-target *_Section 3.2_* + + T + + Transfer-Encoding header field *_Section 6.1_* + + X + + x-compress (transfer coding) *_Section 7.2_* + x-gzip (transfer coding) *_Section 7.2_* + +Authors' Addresses + + Roy T. Fielding (editor) + Adobe + 345 Park Ave + San Jose, CA 95110 + United States of America + Email: fielding@gbiv.com + URI: https://roy.gbiv.com/ + + + Mark Nottingham (editor) + Fastly + Prahran + Australia + Email: mnot@mnot.net + URI: https://www.mnot.net/ + + + Julian Reschke (editor) + greenbytes GmbH + Hafenweg 16 + 48155 Münster + Germany + Email: julian.reschke@greenbytes.de + URI: https://greenbytes.de/tech/webdav/ diff --git a/dist/docs/rfc9113-http_2_0.txt b/dist/docs/rfc9113-http_2_0.txt new file mode 100644 index 0000000..9763cf5 --- /dev/null +++ b/dist/docs/rfc9113-http_2_0.txt @@ -0,0 +1,4188 @@ + + + + +Internet Engineering Task Force (IETF) M. Thomson, Ed. +Request for Comments: 9113 Mozilla +Obsoletes: 7540, 8740 C. Benfield, Ed. +Category: Standards Track Apple Inc. +ISSN: 2070-1721 June 2022 + + + HTTP/2 + +Abstract + + This specification describes an optimized expression of the semantics + of the Hypertext Transfer Protocol (HTTP), referred to as HTTP + version 2 (HTTP/2). HTTP/2 enables a more efficient use of network + resources and a reduced latency by introducing field compression and + allowing multiple concurrent exchanges on the same connection. + + This document obsoletes RFCs 7540 and 8740. + +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/rfc9113. + +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. + +Table of Contents + + 1. Introduction + 2. HTTP/2 Protocol Overview + 2.1. Document Organization + 2.2. Conventions and Terminology + 3. Starting HTTP/2 + 3.1. HTTP/2 Version Identification + 3.2. Starting HTTP/2 for "https" URIs + 3.3. Starting HTTP/2 with Prior Knowledge + 3.4. HTTP/2 Connection Preface + 4. HTTP Frames + 4.1. Frame Format + 4.2. Frame Size + 4.3. Field Section Compression and Decompression + 4.3.1. Compression State + 5. Streams and Multiplexing + 5.1. Stream States + 5.1.1. Stream Identifiers + 5.1.2. Stream Concurrency + 5.2. Flow Control + 5.2.1. Flow-Control Principles + 5.2.2. Appropriate Use of Flow Control + 5.2.3. Flow-Control Performance + 5.3. Prioritization + 5.3.1. Background on Priority in RFC 7540 + 5.3.2. Priority Signaling in This Document + 5.4. Error Handling + 5.4.1. Connection Error Handling + 5.4.2. Stream Error Handling + 5.4.3. Connection Termination + 5.5. Extending HTTP/2 + 6. Frame Definitions + 6.1. DATA + 6.2. HEADERS + 6.3. PRIORITY + 6.4. RST_STREAM + 6.5. SETTINGS + 6.5.1. SETTINGS Format + 6.5.2. Defined Settings + 6.5.3. Settings Synchronization + 6.6. PUSH_PROMISE + 6.7. PING + 6.8. GOAWAY + 6.9. WINDOW_UPDATE + 6.9.1. The Flow-Control Window + 6.9.2. Initial Flow-Control Window Size + 6.9.3. Reducing the Stream Window Size + 6.10. CONTINUATION + 7. Error Codes + 8. Expressing HTTP Semantics in HTTP/2 + 8.1. HTTP Message Framing + 8.1.1. Malformed Messages + 8.2. HTTP Fields + 8.2.1. Field Validity + 8.2.2. Connection-Specific Header Fields + 8.2.3. Compressing the Cookie Header Field + 8.3. HTTP Control Data + 8.3.1. Request Pseudo-Header Fields + 8.3.2. Response Pseudo-Header Fields + 8.4. Server Push + 8.4.1. Push Requests + 8.4.2. Push Responses + 8.5. The CONNECT Method + 8.6. The Upgrade Header Field + 8.7. Request Reliability + 8.8. Examples + 8.8.1. Simple Request + 8.8.2. Simple Response + 8.8.3. Complex Request + 8.8.4. Response with Body + 8.8.5. Informational Responses + 9. HTTP/2 Connections + 9.1. Connection Management + 9.1.1. Connection Reuse + 9.2. Use of TLS Features + 9.2.1. TLS 1.2 Features + 9.2.2. TLS 1.2 Cipher Suites + 9.2.3. TLS 1.3 Features + 10. Security Considerations + 10.1. Server Authority + 10.2. Cross-Protocol Attacks + 10.3. Intermediary Encapsulation Attacks + 10.4. Cacheability of Pushed Responses + 10.5. Denial-of-Service Considerations + 10.5.1. Limits on Field Block Size + 10.5.2. CONNECT Issues + 10.6. Use of Compression + 10.7. Use of Padding + 10.8. Privacy Considerations + 10.9. Remote Timing Attacks + 11. IANA Considerations + 11.1. HTTP2-Settings Header Field Registration + 11.2. The h2c Upgrade Token + 12. References + 12.1. Normative References + 12.2. Informative References + Appendix A. Prohibited TLS 1.2 Cipher Suites + Appendix B. Changes from RFC 7540 + Acknowledgments + Contributors + Authors' Addresses + +1. Introduction + + The performance of applications using the Hypertext Transfer Protocol + (HTTP, [HTTP]) is linked to how each version of HTTP uses the + underlying transport, and the conditions under which the transport + operates. + + Making multiple concurrent requests can reduce latency and improve + application performance. HTTP/1.0 allowed only one request to be + outstanding at a time on a given TCP [TCP] connection. HTTP/1.1 + [HTTP/1.1] added request pipelining, but this only partially + addressed request concurrency and still suffers from application- + layer head-of-line blocking. Therefore, HTTP/1.0 and HTTP/1.1 + clients use multiple connections to a server to make concurrent + requests. + + Furthermore, HTTP fields are often repetitive and verbose, causing + unnecessary network traffic as well as causing the initial TCP + congestion window to quickly fill. This can result in excessive + latency when multiple requests are made on a new TCP connection. + + HTTP/2 addresses these issues by defining an optimized mapping of + HTTP's semantics to an underlying connection. Specifically, it + allows interleaving of messages on the same connection and uses an + efficient coding for HTTP fields. It also allows prioritization of + requests, letting more important requests complete more quickly, + further improving performance. + + The resulting protocol is more friendly to the network because fewer + TCP connections can be used in comparison to HTTP/1.x. This means + less competition with other flows and longer-lived connections, which + in turn lead to better utilization of available network capacity. + Note, however, that TCP head-of-line blocking is not addressed by + this protocol. + + Finally, HTTP/2 also enables more efficient processing of messages + through use of binary message framing. + + This document obsoletes RFCs 7540 and 8740. Appendix B lists notable + changes. + +2. HTTP/2 Protocol Overview + + HTTP/2 provides an optimized transport for HTTP semantics. HTTP/2 + supports all of the core features of HTTP but aims to be more + efficient than HTTP/1.1. + + HTTP/2 is a connection-oriented application-layer protocol that runs + over a TCP connection ([TCP]). The client is the TCP connection + initiator. + + The basic protocol unit in HTTP/2 is a frame (Section 4.1). Each + frame type serves a different purpose. For example, HEADERS and DATA + frames form the basis of HTTP requests and responses (Section 8.1); + other frame types like SETTINGS, WINDOW_UPDATE, and PUSH_PROMISE are + used in support of other HTTP/2 features. + + Multiplexing of requests is achieved by having each HTTP request/ + response exchange associated with its own stream (Section 5). + Streams are largely independent of each other, so a blocked or + stalled request or response does not prevent progress on other + streams. + + Effective use of multiplexing depends on flow control and + prioritization. Flow control (Section 5.2) ensures that it is + possible to efficiently use multiplexed streams by restricting data + that is transmitted to what the receiver is able to handle. + Prioritization (Section 5.3) ensures that limited resources are used + most effectively. This revision of HTTP/2 deprecates the priority + signaling scheme from [RFC7540]. + + Because HTTP fields used in a connection can contain large amounts of + redundant data, frames that contain them are compressed + (Section 4.3). This has especially advantageous impact upon request + sizes in the common case, allowing many requests to be compressed + into one packet. + + Finally, HTTP/2 adds a new, optional interaction mode whereby a + server can push responses to a client (Section 8.4). This is + intended to allow a server to speculatively send data to a client + that the server anticipates the client will need, trading off some + network usage against a potential latency gain. The server does this + by synthesizing a request, which it sends as a PUSH_PROMISE frame. + The server is then able to send a response to the synthetic request + on a separate stream. + +2.1. Document Organization + + The HTTP/2 specification is split into four parts: + + * Starting HTTP/2 (Section 3) covers how an HTTP/2 connection is + initiated. + + * The frame (Section 4) and stream (Section 5) layers describe the + way HTTP/2 frames are structured and formed into multiplexed + streams. + + * Frame (Section 6) and error (Section 7) definitions include + details of the frame and error types used in HTTP/2. + + * HTTP mappings (Section 8) and additional requirements (Section 9) + describe how HTTP semantics are expressed using frames and + streams. + + While some of the frame- and stream-layer concepts are isolated from + HTTP, this specification does not define a completely generic frame + layer. The frame and stream layers are tailored to the needs of + HTTP. + +2.2. Conventions and Terminology + + 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 [RFC2119] [RFC8174] when, and only when, they appear in all + capitals, as shown here. + + All numeric values are in network byte order. Values are unsigned + unless otherwise indicated. Literal values are provided in decimal + or hexadecimal as appropriate. Hexadecimal literals are prefixed + with "0x" to distinguish them from decimal literals. + + This specification describes binary formats using the conventions + described in Section 1.3 of RFC 9000 [QUIC]. Note that this format + uses network byte order and that high-valued bits are listed before + low-valued bits. + + The following terms are used: + + client: The endpoint that initiates an HTTP/2 connection. Clients + send HTTP requests and receive HTTP responses. + + connection: A transport-layer connection between two endpoints. + + connection error: An error that affects the entire HTTP/2 + connection. + + endpoint: Either the client or server of the connection. + + frame: The smallest unit of communication within an HTTP/2 + connection, consisting of a header and a variable-length sequence + of octets structured according to the frame type. + + peer: An endpoint. When discussing a particular endpoint, "peer" + refers to the endpoint that is remote to the primary subject of + discussion. + + receiver: An endpoint that is receiving frames. + + sender: An endpoint that is transmitting frames. + + server: The endpoint that accepts an HTTP/2 connection. Servers + receive HTTP requests and send HTTP responses. + + stream: A bidirectional flow of frames within the HTTP/2 connection. + + stream error: An error on the individual HTTP/2 stream. + + Finally, the terms "gateway", "intermediary", "proxy", and "tunnel" + are defined in Section 3.7 of [HTTP]. Intermediaries act as both + client and server at different times. + + The term "content" as it applies to message bodies is defined in + Section 6.4 of [HTTP]. + +3. Starting HTTP/2 + + Implementations that generate HTTP requests need to discover whether + a server supports HTTP/2. + + HTTP/2 uses the "http" and "https" URI schemes defined in Section 4.2 + of [HTTP], with the same default port numbers as HTTP/1.1 [HTTP/1.1]. + These URIs do not include any indication about what HTTP versions an + upstream server (the immediate peer to which the client wishes to + establish a connection) supports. + + The means by which support for HTTP/2 is determined is different for + "http" and "https" URIs. Discovery for "https" URIs is described in + Section 3.2. HTTP/2 support for "http" URIs can only be discovered + by out-of-band means and requires prior knowledge of the support as + described in Section 3.3. + +3.1. HTTP/2 Version Identification + + The protocol defined in this document has two identifiers. Creating + a connection based on either implies the use of the transport, + framing, and message semantics described in this document. + + * The string "h2" identifies the protocol where HTTP/2 uses + Transport Layer Security (TLS); see Section 9.2. This identifier + is used in the TLS Application-Layer Protocol Negotiation (ALPN) + extension [TLS-ALPN] field and in any place where HTTP/2 over TLS + is identified. + + The "h2" string is serialized into an ALPN protocol identifier as + the two-octet sequence: 0x68, 0x32. + + * The "h2c" string was previously used as a token for use in the + HTTP Upgrade mechanism's Upgrade header field (Section 7.8 of + [HTTP]). This usage was never widely deployed and is deprecated + by this document. The same applies to the HTTP2-Settings header + field, which was used with the upgrade to "h2c". + +3.2. Starting HTTP/2 for "https" URIs + + A client that makes a request to an "https" URI uses TLS [TLS13] with + the ALPN extension [TLS-ALPN]. + + HTTP/2 over TLS uses the "h2" protocol identifier. The "h2c" + protocol identifier MUST NOT be sent by a client or selected by a + server; the "h2c" protocol identifier describes a protocol that does + not use TLS. + + Once TLS negotiation is complete, both the client and the server MUST + send a connection preface (Section 3.4). + +3.3. Starting HTTP/2 with Prior Knowledge + + A client can learn that a particular server supports HTTP/2 by other + means. For example, a client could be configured with knowledge that + a server supports HTTP/2. + + A client that knows that a server supports HTTP/2 can establish a TCP + connection and send the connection preface (Section 3.4) followed by + HTTP/2 frames. Servers can identify these connections by the + presence of the connection preface. This only affects the + establishment of HTTP/2 connections over cleartext TCP; HTTP/2 + connections over TLS MUST use protocol negotiation in TLS [TLS-ALPN]. + + Likewise, the server MUST send a connection preface (Section 3.4). + + Without additional information, prior support for HTTP/2 is not a + strong signal that a given server will support HTTP/2 for future + connections. For example, it is possible for server configurations + to change, for configurations to differ between instances in + clustered servers, or for network conditions to change. + +3.4. HTTP/2 Connection Preface + + In HTTP/2, each endpoint is required to send a connection preface as + a final confirmation of the protocol in use and to establish the + initial settings for the HTTP/2 connection. The client and server + each send a different connection preface. + + The client connection preface starts with a sequence of 24 octets, + which in hex notation is: + + 0x505249202a20485454502f322e300d0a0d0a534d0d0a0d0a + + That is, the connection preface starts with the string "PRI * + HTTP/2.0\r\n\r\nSM\r\n\r\n". This sequence MUST be followed by a + SETTINGS frame (Section 6.5), which MAY be empty. The client sends + the client connection preface as the first application data octets of + a connection. + + | Note: The client connection preface is selected so that a large + | proportion of HTTP/1.1 or HTTP/1.0 servers and intermediaries + | do not attempt to process further frames. Note that this does + | not address the concerns raised in [TALKING]. + + The server connection preface consists of a potentially empty + SETTINGS frame (Section 6.5) that MUST be the first frame the server + sends in the HTTP/2 connection. + + The SETTINGS frames received from a peer as part of the connection + preface MUST be acknowledged (see Section 6.5.3) after sending the + connection preface. + + To avoid unnecessary latency, clients are permitted to send + additional frames to the server immediately after sending the client + connection preface, without waiting to receive the server connection + preface. It is important to note, however, that the server + connection preface SETTINGS frame might include settings that + necessarily alter how a client is expected to communicate with the + server. Upon receiving the SETTINGS frame, the client is expected to + honor any settings established. In some configurations, it is + possible for the server to transmit SETTINGS before the client sends + additional frames, providing an opportunity to avoid this issue. + + Clients and servers MUST treat an invalid connection preface as a + connection error (Section 5.4.1) of type PROTOCOL_ERROR. A GOAWAY + frame (Section 6.8) MAY be omitted in this case, since an invalid + preface indicates that the peer is not using HTTP/2. + +4. HTTP Frames + + Once the HTTP/2 connection is established, endpoints can begin + exchanging frames. + +4.1. Frame Format + + All frames begin with a fixed 9-octet header followed by a variable- + length frame payload. + + HTTP Frame { + Length (24), + Type (8), + + Flags (8), + + Reserved (1), + Stream Identifier (31), + + Frame Payload (..), + } + + Figure 1: Frame Layout + + The fields of the frame header are defined as: + + Length: The length of the frame payload expressed as an unsigned + 24-bit integer in units of octets. Values greater than 2^14 + (16,384) MUST NOT be sent unless the receiver has set a larger + value for SETTINGS_MAX_FRAME_SIZE. + + The 9 octets of the frame header are not included in this value. + + Type: The 8-bit type of the frame. The frame type determines the + format and semantics of the frame. Frames defined in this + document are listed in Section 6. Implementations MUST ignore and + discard frames of unknown types. + + Flags: An 8-bit field reserved for boolean flags specific to the + frame type. + + Flags are assigned semantics specific to the indicated frame type. + Unused flags are those that have no defined semantics for a + particular frame type. Unused flags MUST be ignored on receipt + and MUST be left unset (0x00) when sending. + + Reserved: A reserved 1-bit field. The semantics of this bit are + undefined, and the bit MUST remain unset (0x00) when sending and + MUST be ignored when receiving. + + Stream Identifier: A stream identifier (see Section 5.1.1) expressed + as an unsigned 31-bit integer. The value 0x00 is reserved for + frames that are associated with the connection as a whole as + opposed to an individual stream. + + The structure and content of the frame payload are dependent entirely + on the frame type. + +4.2. Frame Size + + The size of a frame payload is limited by the maximum size that a + receiver advertises in the SETTINGS_MAX_FRAME_SIZE setting. This + setting can have any value between 2^14 (16,384) and 2^24-1 + (16,777,215) octets, inclusive. + + All implementations MUST be capable of receiving and minimally + processing frames up to 2^14 octets in length, plus the 9-octet frame + header (Section 4.1). The size of the frame header is not included + when describing frame sizes. + + | Note: Certain frame types, such as PING (Section 6.7), impose + | additional limits on the amount of frame payload data allowed. + + An endpoint MUST send an error code of FRAME_SIZE_ERROR if a frame + exceeds the size defined in SETTINGS_MAX_FRAME_SIZE, exceeds any + limit defined for the frame type, or is too small to contain + mandatory frame data. A frame size error in a frame that could alter + the state of the entire connection MUST be treated as a connection + error (Section 5.4.1); this includes any frame carrying a field block + (Section 4.3) (that is, HEADERS, PUSH_PROMISE, and CONTINUATION), a + SETTINGS frame, and any frame with a stream identifier of 0. + + Endpoints are not obligated to use all available space in a frame. + Responsiveness can be improved by using frames that are smaller than + the permitted maximum size. Sending large frames can result in + delays in sending time-sensitive frames (such as RST_STREAM, + WINDOW_UPDATE, or PRIORITY), which, if blocked by the transmission of + a large frame, could affect performance. + +4.3. Field Section Compression and Decompression + + Field section compression is the process of compressing a set of + field lines (Section 5.2 of [HTTP]) to form a field block. Field + section decompression is the process of decoding a field block into a + set of field lines. Details of HTTP/2 field section compression and + decompression are defined in [COMPRESSION], which, for historical + reasons, refers to these processes as header compression and + decompression. + + Each field block carries all of the compressed field lines of a + single field section. Header sections also include control data + associated with the message in the form of pseudo-header fields + (Section 8.3) that use the same format as a field line. + + | Note: RFC 7540 [RFC7540] used the term "header block" in place + | of the more generic "field block". + + Field blocks carry control data and header sections for requests, + responses, promised requests, and pushed responses (see Section 8.4). + All these messages, except for interim responses and requests + contained in PUSH_PROMISE (Section 6.6) frames, can optionally + include a field block that carries a trailer section. + + A field section is a collection of field lines. Each of the field + lines in a field block carries a single value. The serialized field + block is then divided into one or more octet sequences, called field + block fragments. The first field block fragment is transmitted + within the frame payload of HEADERS (Section 6.2) or PUSH_PROMISE + (Section 6.6), each of which could be followed by CONTINUATION + (Section 6.10) frames to carry subsequent field block fragments. + + The Cookie header field [COOKIE] is treated specially by the HTTP + mapping (see Section 8.2.3). + + A receiving endpoint reassembles the field block by concatenating its + fragments and then decompresses the block to reconstruct the field + section. + + A complete field section consists of either: + + * a single HEADERS or PUSH_PROMISE frame, with the END_HEADERS flag + set, or + + * a HEADERS or PUSH_PROMISE frame with the END_HEADERS flag unset + and one or more CONTINUATION frames, where the last CONTINUATION + frame has the END_HEADERS flag set. + + Each field block is processed as a discrete unit. Field blocks MUST + be transmitted as a contiguous sequence of frames, with no + interleaved frames of any other type or from any other stream. The + last frame in a sequence of HEADERS or CONTINUATION frames has the + END_HEADERS flag set. The last frame in a sequence of PUSH_PROMISE + or CONTINUATION frames has the END_HEADERS flag set. This allows a + field block to be logically equivalent to a single frame. + + Field block fragments can only be sent as the frame payload of + HEADERS, PUSH_PROMISE, or CONTINUATION frames because these frames + carry data that can modify the compression context maintained by a + receiver. An endpoint receiving HEADERS, PUSH_PROMISE, or + CONTINUATION frames needs to reassemble field blocks and perform + decompression even if the frames are to be discarded. A receiver + MUST terminate the connection with a connection error (Section 5.4.1) + of type COMPRESSION_ERROR if it does not decompress a field block. + + A decoding error in a field block MUST be treated as a connection + error (Section 5.4.1) of type COMPRESSION_ERROR. + +4.3.1. Compression State + + Field compression is stateful. Each endpoint has an HPACK encoder + context and an HPACK decoder context that are used for encoding and + decoding all field blocks on a connection. Section 4 of + [COMPRESSION] defines the dynamic table, which is the primary state + for each context. + + The dynamic table has a maximum size that is set by an HPACK decoder. + An endpoint communicates the size chosen by its HPACK decoder context + using the SETTINGS_HEADER_TABLE_SIZE setting; see Section 6.5.2. + When a connection is established, the dynamic table size for the + HPACK decoder and encoder at both endpoints starts at 4,096 bytes, + the initial value of the SETTINGS_HEADER_TABLE_SIZE setting. + + Any change to the maximum value set using SETTINGS_HEADER_TABLE_SIZE + takes effect when the endpoint acknowledges settings (Section 6.5.3). + The HPACK encoder at that endpoint can set the dynamic table to any + size up to the maximum value set by the decoder. An HPACK encoder + declares the size of the dynamic table with a Dynamic Table Size + Update instruction (Section 6.3 of [COMPRESSION]). + + Once an endpoint acknowledges a change to SETTINGS_HEADER_TABLE_SIZE + that reduces the maximum below the current size of the dynamic table, + its HPACK encoder MUST start the next field block with a Dynamic + Table Size Update instruction that sets the dynamic table to a size + that is less than or equal to the reduced maximum; see Section 4.2 of + [COMPRESSION]. An endpoint MUST treat a field block that follows an + acknowledgment of the reduction to the maximum dynamic table size as + a connection error (Section 5.4.1) of type COMPRESSION_ERROR if it + does not start with a conformant Dynamic Table Size Update + instruction. + + | Implementers are advised that reducing the value of + | SETTINGS_HEADER_TABLE_SIZE is not widely interoperable. Use of + | the connection preface to reduce the value below the initial + | value of 4,096 is somewhat better supported, but this might + | fail with some implementations. + +5. Streams and Multiplexing + + A "stream" is an independent, bidirectional sequence of frames + exchanged between the client and server within an HTTP/2 connection. + Streams have several important characteristics: + + * A single HTTP/2 connection can contain multiple concurrently open + streams, with either endpoint interleaving frames from multiple + streams. + + * Streams can be established and used unilaterally or shared by + either endpoint. + + * Streams can be closed by either endpoint. + + * The order in which frames are sent is significant. Recipients + process frames in the order they are received. In particular, the + order of HEADERS and DATA frames is semantically significant. + + * Streams are identified by an integer. Stream identifiers are + assigned to streams by the endpoint initiating the stream. + +5.1. Stream States + + The lifecycle of a stream is shown in Figure 2. + + +--------+ + send PP | | recv PP + ,--------+ idle +--------. + / | | \ + v +--------+ v + +----------+ | +----------+ + | | | send H / | | + ,------+ reserved | | recv H | reserved +------. + | | (local) | | | (remote) | | + | +---+------+ v +------+---+ | + | | +--------+ | | + | | recv ES | | send ES | | + | send H | ,-------+ open +-------. | recv H | + | | / | | \ | | + | v v +---+----+ v v | + | +----------+ | +----------+ | + | | half- | | | half- | | + | | closed | | send R / | closed | | + | | (remote) | | recv R | (local) | | + | +----+-----+ | +-----+----+ | + | | | | | + | | send ES / | recv ES / | | + | | send R / v send R / | | + | | recv R +--------+ recv R | | + | send R / `----------->| |<-----------' send R / | + | recv R | closed | recv R | + `----------------------->| |<-----------------------' + +--------+ + + Figure 2: Stream States + + send: endpoint sends this frame + recv: endpoint receives this frame + H: HEADERS frame (with implied CONTINUATION frames) + ES: END_STREAM flag + R: RST_STREAM frame + PP: PUSH_PROMISE frame (with implied CONTINUATION frames); state + transitions are for the promised stream + + Note that this diagram shows stream state transitions and the frames + and flags that affect those transitions only. In this regard, + CONTINUATION frames do not result in state transitions; they are + effectively part of the HEADERS or PUSH_PROMISE that they follow. + For the purpose of state transitions, the END_STREAM flag is + processed as a separate event to the frame that bears it; a HEADERS + frame with the END_STREAM flag set can cause two state transitions. + + Both endpoints have a subjective view of the state of a stream that + could be different when frames are in transit. Endpoints do not + coordinate the creation of streams; they are created unilaterally by + either endpoint. The negative consequences of a mismatch in states + are limited to the "closed" state after sending RST_STREAM, where + frames might be received for some time after closing. + + Streams have the following states: + + idle: All streams start in the "idle" state. + + The following transitions are valid from this state: + + * Sending a HEADERS frame as a client, or receiving a HEADERS + frame as a server, causes the stream to become "open". The + stream identifier is selected as described in Section 5.1.1. + The same HEADERS frame can also cause a stream to immediately + become "half-closed". + + * Sending a PUSH_PROMISE frame on another stream reserves the + idle stream that is identified for later use. The stream state + for the reserved stream transitions to "reserved (local)". + Only a server may send PUSH_PROMISE frames. + + * Receiving a PUSH_PROMISE frame on another stream reserves an + idle stream that is identified for later use. The stream state + for the reserved stream transitions to "reserved (remote)". + Only a client may receive PUSH_PROMISE frames. + + * Note that the PUSH_PROMISE frame is not sent on the idle stream + but references the newly reserved stream in the Promised Stream + ID field. + + * Opening a stream with a higher-valued stream identifier causes + the stream to transition immediately to a "closed" state; note + that this transition is not shown in the diagram. + + Receiving any frame other than HEADERS or PRIORITY on a stream in + this state MUST be treated as a connection error (Section 5.4.1) + of type PROTOCOL_ERROR. If this stream is initiated by the + server, as described in Section 5.1.1, then receiving a HEADERS + frame MUST also be treated as a connection error (Section 5.4.1) + of type PROTOCOL_ERROR. + + reserved (local): A stream in the "reserved (local)" state is one + that has been promised by sending a PUSH_PROMISE frame. A + PUSH_PROMISE frame reserves an idle stream by associating the + stream with an open stream that was initiated by the remote peer + (see Section 8.4). + + In this state, only the following transitions are possible: + + * The endpoint can send a HEADERS frame. This causes the stream + to open in a "half-closed (remote)" state. + + * Either endpoint can send a RST_STREAM frame to cause the stream + to become "closed". This releases the stream reservation. + + An endpoint MUST NOT send any type of frame other than HEADERS, + RST_STREAM, or PRIORITY in this state. + + A PRIORITY or WINDOW_UPDATE frame MAY be received in this state. + Receiving any type of frame other than RST_STREAM, PRIORITY, or + WINDOW_UPDATE on a stream in this state MUST be treated as a + connection error (Section 5.4.1) of type PROTOCOL_ERROR. + + reserved (remote): A stream in the "reserved (remote)" state has + been reserved by a remote peer. + + In this state, only the following transitions are possible: + + * Receiving a HEADERS frame causes the stream to transition to + "half-closed (local)". + + * Either endpoint can send a RST_STREAM frame to cause the stream + to become "closed". This releases the stream reservation. + + An endpoint MUST NOT send any type of frame other than RST_STREAM, + WINDOW_UPDATE, or PRIORITY in this state. + + Receiving any type of frame other than HEADERS, RST_STREAM, or + PRIORITY on a stream in this state MUST be treated as a connection + error (Section 5.4.1) of type PROTOCOL_ERROR. + + open: A stream in the "open" state may be used by both peers to send + frames of any type. In this state, sending peers observe + advertised stream-level flow-control limits (Section 5.2). + + From this state, either endpoint can send a frame with an + END_STREAM flag set, which causes the stream to transition into + one of the "half-closed" states. An endpoint sending an + END_STREAM flag causes the stream state to become "half-closed + (local)"; an endpoint receiving an END_STREAM flag causes the + stream state to become "half-closed (remote)". + + Either endpoint can send a RST_STREAM frame from this state, + causing it to transition immediately to "closed". + + half-closed (local): A stream that is in the "half-closed (local)" + state cannot be used for sending frames other than WINDOW_UPDATE, + PRIORITY, and RST_STREAM. + + A stream transitions from this state to "closed" when a frame is + received with the END_STREAM flag set or when either peer sends a + RST_STREAM frame. + + An endpoint can receive any type of frame in this state. + Providing flow-control credit using WINDOW_UPDATE frames is + necessary to continue receiving flow-controlled frames. In this + state, a receiver can ignore WINDOW_UPDATE frames, which might + arrive for a short period after a frame with the END_STREAM flag + set is sent. + + PRIORITY frames can be received in this state. + + half-closed (remote): A stream that is "half-closed (remote)" is no + longer being used by the peer to send frames. In this state, an + endpoint is no longer obligated to maintain a receiver flow- + control window. + + If an endpoint receives additional frames, other than + WINDOW_UPDATE, PRIORITY, or RST_STREAM, for a stream that is in + this state, it MUST respond with a stream error (Section 5.4.2) of + type STREAM_CLOSED. + + A stream that is "half-closed (remote)" can be used by the + endpoint to send frames of any type. In this state, the endpoint + continues to observe advertised stream-level flow-control limits + (Section 5.2). + + A stream can transition from this state to "closed" by sending a + frame with the END_STREAM flag set or when either peer sends a + RST_STREAM frame. + + closed: The "closed" state is the terminal state. + + A stream enters the "closed" state after an endpoint both sends + and receives a frame with an END_STREAM flag set. A stream also + enters the "closed" state after an endpoint either sends or + receives a RST_STREAM frame. + + An endpoint MUST NOT send frames other than PRIORITY on a closed + stream. An endpoint MAY treat receipt of any other type of frame + on a closed stream as a connection error (Section 5.4.1) of type + STREAM_CLOSED, except as noted below. + + An endpoint that sends a frame with the END_STREAM flag set or a + RST_STREAM frame might receive a WINDOW_UPDATE or RST_STREAM frame + from its peer in the time before the peer receives and processes + the frame that closes the stream. + + An endpoint that sends a RST_STREAM frame on a stream that is in + the "open" or "half-closed (local)" state could receive any type + of frame. The peer might have sent or enqueued for sending these + frames before processing the RST_STREAM frame. An endpoint MUST + minimally process and then discard any frames it receives in this + state. This means updating header compression state for HEADERS + and PUSH_PROMISE frames. Receiving a PUSH_PROMISE frame also + causes the promised stream to become "reserved (remote)", even + when the PUSH_PROMISE frame is received on a closed stream. + Additionally, the content of DATA frames counts toward the + connection flow-control window. + + An endpoint can perform this minimal processing for all streams + that are in the "closed" state. Endpoints MAY use other signals + to detect that a peer has received the frames that caused the + stream to enter the "closed" state and treat receipt of any frame + other than PRIORITY as a connection error (Section 5.4.1) of type + PROTOCOL_ERROR. Endpoints can use frames that indicate that the + peer has received the closing signal to drive this. Endpoints + SHOULD NOT use timers for this purpose. For example, an endpoint + that sends a SETTINGS frame after closing a stream can safely + treat receipt of a DATA frame on that stream as an error after + receiving an acknowledgment of the settings. Other things that + might be used are PING frames, receiving data on streams that were + created after closing the stream, or responses to requests created + after closing the stream. + + In the absence of more specific rules, implementations SHOULD treat + the receipt of a frame that is not expressly permitted in the + description of a state as a connection error (Section 5.4.1) of type + PROTOCOL_ERROR. Note that PRIORITY can be sent and received in any + stream state. + + The rules in this section only apply to frames defined in this + document. Receipt of frames for which the semantics are unknown + cannot be treated as an error, as the conditions for sending and + receiving those frames are also unknown; see Section 5.5. + + An example of the state transitions for an HTTP request/response + exchange can be found in Section 8.8. An example of the state + transitions for server push can be found in Sections 8.4.1 and 8.4.2. + +5.1.1. Stream Identifiers + + Streams are identified by an unsigned 31-bit integer. Streams + initiated by a client MUST use odd-numbered stream identifiers; those + initiated by the server MUST use even-numbered stream identifiers. A + stream identifier of zero (0x00) is used for connection control + messages; the stream identifier of zero cannot be used to establish a + new stream. + + The identifier of a newly established stream MUST be numerically + greater than all streams that the initiating endpoint has opened or + reserved. This governs streams that are opened using a HEADERS frame + and streams that are reserved using PUSH_PROMISE. An endpoint that + receives an unexpected stream identifier MUST respond with a + connection error (Section 5.4.1) of type PROTOCOL_ERROR. + + A HEADERS frame will transition the client-initiated stream + identified by the stream identifier in the frame header from "idle" + to "open". A PUSH_PROMISE frame will transition the server-initiated + stream identified by the Promised Stream ID field in the frame + payload from "idle" to "reserved (local)" or "reserved (remote)". + When a stream transitions out of the "idle" state, all streams in the + "idle" state that might have been opened by the peer with a lower- + valued stream identifier immediately transition to "closed". That + is, an endpoint may skip a stream identifier, with the effect being + that the skipped stream is immediately closed. + + Stream identifiers cannot be reused. Long-lived connections can + result in an endpoint exhausting the available range of stream + identifiers. A client that is unable to establish a new stream + identifier can establish a new connection for new streams. A server + that is unable to establish a new stream identifier can send a GOAWAY + frame so that the client is forced to open a new connection for new + streams. + +5.1.2. Stream Concurrency + + A peer can limit the number of concurrently active streams using the + SETTINGS_MAX_CONCURRENT_STREAMS parameter (see Section 6.5.2) within + a SETTINGS frame. The maximum concurrent streams setting is specific + to each endpoint and applies only to the peer that receives the + setting. That is, clients specify the maximum number of concurrent + streams the server can initiate, and servers specify the maximum + number of concurrent streams the client can initiate. + + Streams that are in the "open" state or in either of the "half- + closed" states count toward the maximum number of streams that an + endpoint is permitted to open. Streams in any of these three states + count toward the limit advertised in the + SETTINGS_MAX_CONCURRENT_STREAMS setting. Streams in either of the + "reserved" states do not count toward the stream limit. + + Endpoints MUST NOT exceed the limit set by their peer. An endpoint + that receives a HEADERS frame that causes its advertised concurrent + stream limit to be exceeded MUST treat this as a stream error + (Section 5.4.2) of type PROTOCOL_ERROR or REFUSED_STREAM. The choice + of error code determines whether the endpoint wishes to enable + automatic retry (see Section 8.7 for details). + + An endpoint that wishes to reduce the value of + SETTINGS_MAX_CONCURRENT_STREAMS to a value that is below the current + number of open streams can either close streams that exceed the new + value or allow streams to complete. + +5.2. Flow Control + + Using streams for multiplexing introduces contention over use of the + TCP connection, resulting in blocked streams. A flow-control scheme + ensures that streams on the same connection do not destructively + interfere with each other. Flow control is used for both individual + streams and the connection as a whole. + + HTTP/2 provides for flow control through use of the WINDOW_UPDATE + frame (Section 6.9). + +5.2.1. Flow-Control Principles + + HTTP/2 stream flow control aims to allow a variety of flow-control + algorithms to be used without requiring protocol changes. Flow + control in HTTP/2 has the following characteristics: + + 1. Flow control is specific to a connection. HTTP/2 flow control + operates between the endpoints of a single hop and not over the + entire end-to-end path. + + 2. Flow control is based on WINDOW_UPDATE frames. Receivers + advertise how many octets they are prepared to receive on a + stream and for the entire connection. This is a credit-based + scheme. + + 3. Flow control is directional with overall control provided by the + receiver. A receiver MAY choose to set any window size that it + desires for each stream and for the entire connection. A sender + MUST respect flow-control limits imposed by a receiver. Clients, + servers, and intermediaries all independently advertise their + flow-control window as a receiver and abide by the flow-control + limits set by their peer when sending. + + 4. The initial value for the flow-control window is 65,535 octets + for both new streams and the overall connection. + + 5. The frame type determines whether flow control applies to a + frame. Of the frames specified in this document, only DATA + frames are subject to flow control; all other frame types do not + consume space in the advertised flow-control window. This + ensures that important control frames are not blocked by flow + control. + + 6. An endpoint can choose to disable its own flow control, but an + endpoint cannot ignore flow-control signals from its peer. + + 7. HTTP/2 defines only the format and semantics of the WINDOW_UPDATE + frame (Section 6.9). This document does not stipulate how a + receiver decides when to send this frame or the value that it + sends, nor does it specify how a sender chooses to send packets. + Implementations are able to select any algorithm that suits their + needs. + + Implementations are also responsible for prioritizing the sending of + requests and responses, choosing how to avoid head-of-line blocking + for requests, and managing the creation of new streams. Algorithm + choices for these could interact with any flow-control algorithm. + +5.2.2. Appropriate Use of Flow Control + + Flow control is defined to protect endpoints that are operating under + resource constraints. For example, a proxy needs to share memory + between many connections and also might have a slow upstream + connection and a fast downstream one. Flow control addresses cases + where the receiver is unable to process data on one stream yet wants + to continue to process other streams in the same connection. + + Deployments that do not require this capability can advertise a flow- + control window of the maximum size (2^31-1) and can maintain this + window by sending a WINDOW_UPDATE frame when any data is received. + This effectively disables flow control for that receiver. + Conversely, a sender is always subject to the flow-control window + advertised by the receiver. + + Deployments with constrained resources (for example, memory) can + employ flow control to limit the amount of memory a peer can consume. + Note, however, that this can lead to suboptimal use of available + network resources if flow control is enabled without knowledge of the + bandwidth * delay product (see [RFC7323]). + + Even with full awareness of the current bandwidth * delay product, + implementation of flow control can be difficult. Endpoints MUST read + and process HTTP/2 frames from the TCP receive buffer as soon as data + is available. Failure to read promptly could lead to a deadlock when + critical frames, such as WINDOW_UPDATE, are not read and acted upon. + Reading frames promptly does not expose endpoints to resource + exhaustion attacks, as HTTP/2 flow control limits resource + commitments. + +5.2.3. Flow-Control Performance + + If an endpoint cannot ensure that its peer always has available flow- + control window space that is greater than the peer's bandwidth * + delay product on this connection, its receive throughput will be + limited by HTTP/2 flow control. This will result in degraded + performance. + + Sending timely WINDOW_UPDATE frames can improve performance. + Endpoints will want to balance the need to improve receive throughput + with the need to manage resource exhaustion risks and should take + careful note of Section 10.5 in defining their strategy to manage + window sizes. + +5.3. Prioritization + + In a multiplexed protocol like HTTP/2, prioritizing allocation of + bandwidth and computation resources to streams can be critical to + attaining good performance. A poor prioritization scheme can result + in HTTP/2 providing poor performance. With no parallelism at the TCP + layer, performance could be significantly worse than HTTP/1.1. + + A good prioritization scheme benefits from the application of + contextual knowledge such as the content of resources, how resources + are interrelated, and how those resources will be used by a peer. In + particular, clients can possess knowledge about the priority of + requests that is relevant to server prioritization. In those cases, + having clients provide priority information can improve performance. + +5.3.1. Background on Priority in RFC 7540 + + RFC 7540 defined a rich system for signaling priority of requests. + However, this system proved to be complex, and it was not uniformly + implemented. + + The flexible scheme meant that it was possible for clients to express + priorities in very different ways, with little consistency in the + approaches that were adopted. For servers, implementing generic + support for the scheme was complex. Implementation of priorities was + uneven in both clients and servers. Many server deployments ignored + client signals when prioritizing their handling of requests. + + In short, the prioritization signaling in RFC 7540 [RFC7540] was not + successful. + +5.3.2. Priority Signaling in This Document + + This update to HTTP/2 deprecates the priority signaling defined in + RFC 7540 [RFC7540]. The bulk of the text related to priority signals + is not included in this document. The description of frame fields + and some of the mandatory handling is retained to ensure that + implementations of this document remain interoperable with + implementations that use the priority signaling described in RFC + 7540. + + A thorough description of the RFC 7540 priority scheme remains in + Section 5.3 of [RFC7540]. + + Signaling priority information is necessary to attain good + performance in many cases. Where signaling priority information is + important, endpoints are encouraged to use an alternative scheme, + such as the scheme described in [HTTP-PRIORITY]. + + Though the priority signaling from RFC 7540 was not widely adopted, + the information it provides can still be useful in the absence of + better information. Endpoints that receive priority signals in + HEADERS or PRIORITY frames can benefit from applying that + information. In particular, implementations that consume these + signals would not benefit from discarding these priority signals in + the absence of alternatives. + + Servers SHOULD use other contextual information in determining + priority of requests in the absence of any priority signals. Servers + MAY interpret the complete absence of signals as an indication that + the client has not implemented the feature. The defaults described + in Section 5.3.5 of [RFC7540] are known to have poor performance + under most conditions, and their use is unlikely to be deliberate. + +5.4. Error Handling + + HTTP/2 framing permits two classes of errors: + + * An error condition that renders the entire connection unusable is + a connection error. + + * An error in an individual stream is a stream error. + + A list of error codes is included in Section 7. + + It is possible that an endpoint will encounter frames that would + cause multiple errors. Implementations MAY discover multiple errors + during processing, but they SHOULD report at most one stream and one + connection error as a result. + + The first stream error reported for a given stream prevents any other + errors on that stream from being reported. In comparison, the + protocol permits multiple GOAWAY frames, though an endpoint SHOULD + report just one type of connection error unless an error is + encountered during graceful shutdown. If this occurs, an endpoint + MAY send an additional GOAWAY frame with the new error code, in + addition to any prior GOAWAY that contained NO_ERROR. + + If an endpoint detects multiple different errors, it MAY choose to + report any one of those errors. If a frame causes a connection + error, that error MUST be reported. Additionally, an endpoint MAY + use any applicable error code when it detects an error condition; a + generic error code (such as PROTOCOL_ERROR or INTERNAL_ERROR) can + always be used in place of more specific error codes. + +5.4.1. Connection Error Handling + + A connection error is any error that prevents further processing of + the frame layer or corrupts any connection state. + + An endpoint that encounters a connection error SHOULD first send a + GOAWAY frame (Section 6.8) with the stream identifier of the last + stream that it successfully received from its peer. The GOAWAY frame + includes an error code (Section 7) that indicates why the connection + is terminating. After sending the GOAWAY frame for an error + condition, the endpoint MUST close the TCP connection. + + It is possible that the GOAWAY will not be reliably received by the + receiving endpoint. In the event of a connection error, GOAWAY only + provides a best-effort attempt to communicate with the peer about why + the connection is being terminated. + + An endpoint can end a connection at any time. In particular, an + endpoint MAY choose to treat a stream error as a connection error. + Endpoints SHOULD send a GOAWAY frame when ending a connection, + providing that circumstances permit it. + +5.4.2. Stream Error Handling + + A stream error is an error related to a specific stream that does not + affect processing of other streams. + + An endpoint that detects a stream error sends a RST_STREAM frame + (Section 6.4) that contains the stream identifier of the stream where + the error occurred. The RST_STREAM frame includes an error code that + indicates the type of error. + + A RST_STREAM is the last frame that an endpoint can send on a stream. + The peer that sends the RST_STREAM frame MUST be prepared to receive + any frames that were sent or enqueued for sending by the remote peer. + These frames can be ignored, except where they modify connection + state (such as the state maintained for field section compression + (Section 4.3) or flow control). + + Normally, an endpoint SHOULD NOT send more than one RST_STREAM frame + for any stream. However, an endpoint MAY send additional RST_STREAM + frames if it receives frames on a closed stream after more than a + round-trip time. This behavior is permitted to deal with misbehaving + implementations. + + To avoid looping, an endpoint MUST NOT send a RST_STREAM in response + to a RST_STREAM frame. + +5.4.3. Connection Termination + + If the TCP connection is closed or reset while streams remain in the + "open" or "half-closed" states, then the affected streams cannot be + automatically retried (see Section 8.7 for details). + +5.5. Extending HTTP/2 + + HTTP/2 permits extension of the protocol. Within the limitations + described in this section, protocol extensions can be used to provide + additional services or alter any aspect of the protocol. Extensions + are effective only within the scope of a single HTTP/2 connection. + + This applies to the protocol elements defined in this document. This + does not affect the existing options for extending HTTP, such as + defining new methods, status codes, or fields (see Section 16 of + [HTTP]). + + Extensions are permitted to use new frame types (Section 4.1), new + settings (Section 6.5), or new error codes (Section 7). Registries + for managing these extension points are defined in Section 11 of + [RFC7540]. + + Implementations MUST ignore unknown or unsupported values in all + extensible protocol elements. Implementations MUST discard frames + that have unknown or unsupported types. This means that any of these + extension points can be safely used by extensions without prior + arrangement or negotiation. However, extension frames that appear in + the middle of a field block (Section 4.3) are not permitted; these + MUST be treated as a connection error (Section 5.4.1) of type + PROTOCOL_ERROR. + + Extensions SHOULD avoid changing protocol elements defined in this + document or elements for which no extension mechanism is defined. + This includes changes to the layout of frames, additions or changes + to the way that frames are composed into HTTP messages (Section 8.1), + the definition of pseudo-header fields, or changes to any protocol + element that a compliant endpoint might treat as a connection error + (Section 5.4.1). + + An extension that changes existing protocol elements or state MUST be + negotiated before being used. For example, an extension that changes + the layout of the HEADERS frame cannot be used until the peer has + given a positive signal that this is acceptable. In this case, it + could also be necessary to coordinate when the revised layout comes + into effect. For example, treating frames other than DATA frames as + flow controlled requires a change in semantics that both endpoints + need to understand, so this can only be done through negotiation. + + This document doesn't mandate a specific method for negotiating the + use of an extension but notes that a setting (Section 6.5.2) could be + used for that purpose. If both peers set a value that indicates + willingness to use the extension, then the extension can be used. If + a setting is used for extension negotiation, the initial value MUST + be defined in such a fashion that the extension is initially + disabled. + +6. Frame Definitions + + This specification defines a number of frame types, each identified + by a unique 8-bit type code. Each frame type serves a distinct + purpose in the establishment and management of either the connection + as a whole or individual streams. + + The transmission of specific frame types can alter the state of a + connection. If endpoints fail to maintain a synchronized view of the + connection state, successful communication within the connection will + no longer be possible. Therefore, it is important that endpoints + have a shared comprehension of how the state is affected by the use + of any given frame. + +6.1. DATA + + DATA frames (type=0x00) convey arbitrary, variable-length sequences + of octets associated with a stream. One or more DATA frames are + used, for instance, to carry HTTP request or response message + contents. + + DATA frames MAY also contain padding. Padding can be added to DATA + frames to obscure the size of messages. Padding is a security + feature; see Section 10.7. + + DATA Frame { + Length (24), + Type (8) = 0x00, + + Unused Flags (4), + PADDED Flag (1), + Unused Flags (2), + END_STREAM Flag (1), + + Reserved (1), + Stream Identifier (31), + + [Pad Length (8)], + Data (..), + Padding (..2040), + } + + Figure 3: DATA Frame Format + + The Length, Type, Unused Flag(s), Reserved, and Stream Identifier + fields are described in Section 4. The DATA frame contains the + following additional fields: + + Pad Length: An 8-bit field containing the length of the frame + padding in units of octets. This field is conditional and is only + present if the PADDED flag is set. + + Data: Application data. The amount of data is the remainder of the + frame payload after subtracting the length of the other fields + that are present. + + Padding: Padding octets that contain no application semantic value. + Padding octets MUST be set to zero when sending. A receiver is + not obligated to verify padding but MAY treat non-zero padding as + a connection error (Section 5.4.1) of type PROTOCOL_ERROR. + + The DATA frame defines the following flags: + + PADDED (0x08): When set, the PADDED flag indicates that the Pad + Length field and any padding that it describes are present. + + END_STREAM (0x01): When set, the END_STREAM flag indicates that this + frame is the last that the endpoint will send for the identified + stream. Setting this flag causes the stream to enter one of the + "half-closed" states or the "closed" state (Section 5.1). + + | Note: An endpoint that learns of stream closure after sending + | all data can close a stream by sending a STREAM frame with a + | zero-length Data field and the END_STREAM flag set. This is + | only possible if the endpoint does not send trailers, as the + | END_STREAM flag appears on a HEADERS frame in that case; see + | Section 8.1. + + DATA frames MUST be associated with a stream. If a DATA frame is + received whose Stream Identifier field is 0x00, the recipient MUST + respond with a connection error (Section 5.4.1) of type + PROTOCOL_ERROR. + + DATA frames are subject to flow control and can only be sent when a + stream is in the "open" or "half-closed (remote)" state. The entire + DATA frame payload is included in flow control, including the Pad + Length and Padding fields if present. If a DATA frame is received + whose stream is not in the "open" or "half-closed (local)" state, the + recipient MUST respond with a stream error (Section 5.4.2) of type + STREAM_CLOSED. + + The total number of padding octets is determined by the value of the + Pad Length field. If the length of the padding is the length of the + frame payload or greater, the recipient MUST treat this as a + connection error (Section 5.4.1) of type PROTOCOL_ERROR. + + | Note: A frame can be increased in size by one octet by + | including a Pad Length field with a value of zero. + +6.2. HEADERS + + The HEADERS frame (type=0x01) is used to open a stream (Section 5.1), + and additionally carries a field block fragment. Despite the name, a + HEADERS frame can carry a header section or a trailer section. + HEADERS frames can be sent on a stream in the "idle", "reserved + (local)", "open", or "half-closed (remote)" state. + + HEADERS Frame { + Length (24), + Type (8) = 0x01, + + Unused Flags (2), + PRIORITY Flag (1), + Unused Flag (1), + PADDED Flag (1), + END_HEADERS Flag (1), + Unused Flag (1), + END_STREAM Flag (1), + + Reserved (1), + Stream Identifier (31), + + [Pad Length (8)], + [Exclusive (1)], + [Stream Dependency (31)], + [Weight (8)], + Field Block Fragment (..), + Padding (..2040), + } + + Figure 4: HEADERS Frame Format + + The Length, Type, Unused Flag(s), Reserved, and Stream Identifier + fields are described in Section 4. The HEADERS frame payload has the + following additional fields: + + Pad Length: An 8-bit field containing the length of the frame + padding in units of octets. This field is only present if the + PADDED flag is set. + + Exclusive: A single-bit flag. This field is only present if the + PRIORITY flag is set. Priority signals in HEADERS frames are + deprecated; see Section 5.3.2. + + Stream Dependency: A 31-bit stream identifier. This field is only + present if the PRIORITY flag is set. + + Weight: An unsigned 8-bit integer. This field is only present if + the PRIORITY flag is set. + + Field Block Fragment: A field block fragment (Section 4.3). + + Padding: Padding octets that contain no application semantic value. + Padding octets MUST be set to zero when sending. A receiver is + not obligated to verify padding but MAY treat non-zero padding as + a connection error (Section 5.4.1) of type PROTOCOL_ERROR. + + The HEADERS frame defines the following flags: + + PRIORITY (0x20): When set, the PRIORITY flag indicates that the + Exclusive, Stream Dependency, and Weight fields are present. + + PADDED (0x08): When set, the PADDED flag indicates that the Pad + Length field and any padding that it describes are present. + + END_HEADERS (0x04): When set, the END_HEADERS flag indicates that + this frame contains an entire field block (Section 4.3) and is not + followed by any CONTINUATION frames. + + A HEADERS frame without the END_HEADERS flag set MUST be followed + by a CONTINUATION frame for the same stream. A receiver MUST + treat the receipt of any other type of frame or a frame on a + different stream as a connection error (Section 5.4.1) of type + PROTOCOL_ERROR. + + END_STREAM (0x01): When set, the END_STREAM flag indicates that the + field block (Section 4.3) is the last that the endpoint will send + for the identified stream. + + A HEADERS frame with the END_STREAM flag set signals the end of a + stream. However, a HEADERS frame with the END_STREAM flag set can + be followed by CONTINUATION frames on the same stream. Logically, + the CONTINUATION frames are part of the HEADERS frame. + + The frame payload of a HEADERS frame contains a field block fragment + (Section 4.3). A field block that does not fit within a HEADERS + frame is continued in a CONTINUATION frame (Section 6.10). + + HEADERS frames MUST be associated with a stream. If a HEADERS frame + is received whose Stream Identifier field is 0x00, the recipient MUST + respond with a connection error (Section 5.4.1) of type + PROTOCOL_ERROR. + + The HEADERS frame changes the connection state as described in + Section 4.3. + + The total number of padding octets is determined by the value of the + Pad Length field. If the length of the padding is the length of the + frame payload or greater, the recipient MUST treat this as a + connection error (Section 5.4.1) of type PROTOCOL_ERROR. + + | Note: A frame can be increased in size by one octet by + | including a Pad Length field with a value of zero. + +6.3. PRIORITY + + The PRIORITY frame (type=0x02) is deprecated; see Section 5.3.2. A + PRIORITY frame can be sent in any stream state, including idle or + closed streams. + + PRIORITY Frame { + Length (24) = 0x05, + Type (8) = 0x02, + + Unused Flags (8), + + Reserved (1), + Stream Identifier (31), + + Exclusive (1), + Stream Dependency (31), + Weight (8), + } + + Figure 5: PRIORITY Frame Format + + The Length, Type, Unused Flag(s), Reserved, and Stream Identifier + fields are described in Section 4. The frame payload of a PRIORITY + frame contains the following additional fields: + + Exclusive: A single-bit flag. + + Stream Dependency: A 31-bit stream identifier. + + Weight: An unsigned 8-bit integer. + + The PRIORITY frame does not define any flags. + + The PRIORITY frame always identifies a stream. If a PRIORITY frame + is received with a stream identifier of 0x00, the recipient MUST + respond with a connection error (Section 5.4.1) of type + PROTOCOL_ERROR. + + Sending or receiving a PRIORITY frame does not affect the state of + any stream (Section 5.1). The PRIORITY frame can be sent on a stream + in any state, including "idle" or "closed". A PRIORITY frame cannot + be sent between consecutive frames that comprise a single field block + (Section 4.3). + + A PRIORITY frame with a length other than 5 octets MUST be treated as + a stream error (Section 5.4.2) of type FRAME_SIZE_ERROR. + +6.4. RST_STREAM + + The RST_STREAM frame (type=0x03) allows for immediate termination of + a stream. RST_STREAM is sent to request cancellation of a stream or + to indicate that an error condition has occurred. + + RST_STREAM Frame { + Length (24) = 0x04, + Type (8) = 0x03, + + Unused Flags (8), + + Reserved (1), + Stream Identifier (31), + + Error Code (32), + } + + Figure 6: RST_STREAM Frame Format + + The Length, Type, Unused Flag(s), Reserved, and Stream Identifier + fields are described in Section 4. Additionally, the RST_STREAM + frame contains a single unsigned, 32-bit integer identifying the + error code (Section 7). The error code indicates why the stream is + being terminated. + + The RST_STREAM frame does not define any flags. + + The RST_STREAM frame fully terminates the referenced stream and + causes it to enter the "closed" state. After receiving a RST_STREAM + on a stream, the receiver MUST NOT send additional frames for that + stream, except for PRIORITY. However, after sending the RST_STREAM, + the sending endpoint MUST be prepared to receive and process + additional frames sent on the stream that might have been sent by the + peer prior to the arrival of the RST_STREAM. + + RST_STREAM frames MUST be associated with a stream. If a RST_STREAM + frame is received with a stream identifier of 0x00, the recipient + MUST treat this as a connection error (Section 5.4.1) of type + PROTOCOL_ERROR. + + RST_STREAM frames MUST NOT be sent for a stream in the "idle" state. + If a RST_STREAM frame identifying an idle stream is received, the + recipient MUST treat this as a connection error (Section 5.4.1) of + type PROTOCOL_ERROR. + + A RST_STREAM frame with a length other than 4 octets MUST be treated + as a connection error (Section 5.4.1) of type FRAME_SIZE_ERROR. + +6.5. SETTINGS + + The SETTINGS frame (type=0x04) conveys configuration parameters that + affect how endpoints communicate, such as preferences and constraints + on peer behavior. The SETTINGS frame is also used to acknowledge the + receipt of those settings. Individually, a configuration parameter + from a SETTINGS frame is referred to as a "setting". + + Settings are not negotiated; they describe characteristics of the + sending peer, which are used by the receiving peer. Different values + for the same setting can be advertised by each peer. For example, a + client might set a high initial flow-control window, whereas a server + might set a lower value to conserve resources. + + A SETTINGS frame MUST be sent by both endpoints at the start of a + connection and MAY be sent at any other time by either endpoint over + the lifetime of the connection. Implementations MUST support all of + the settings defined by this specification. + + Each parameter in a SETTINGS frame replaces any existing value for + that parameter. Settings are processed in the order in which they + appear, and a receiver of a SETTINGS frame does not need to maintain + any state other than the current value of each setting. Therefore, + the value of a SETTINGS parameter is the last value that is seen by a + receiver. + + SETTINGS frames are acknowledged by the receiving peer. To enable + this, the SETTINGS frame defines the ACK flag: + + ACK (0x01): When set, the ACK flag indicates that this frame + acknowledges receipt and application of the peer's SETTINGS frame. + When this bit is set, the frame payload of the SETTINGS frame MUST + be empty. Receipt of a SETTINGS frame with the ACK flag set and a + length field value other than 0 MUST be treated as a connection + error (Section 5.4.1) of type FRAME_SIZE_ERROR. For more + information, see Section 6.5.3 ("Settings Synchronization"). + + SETTINGS frames always apply to a connection, never a single stream. + The stream identifier for a SETTINGS frame MUST be zero (0x00). If + an endpoint receives a SETTINGS frame whose Stream Identifier field + is anything other than 0x00, the endpoint MUST respond with a + connection error (Section 5.4.1) of type PROTOCOL_ERROR. + + The SETTINGS frame affects connection state. A badly formed or + incomplete SETTINGS frame MUST be treated as a connection error + (Section 5.4.1) of type PROTOCOL_ERROR. + + A SETTINGS frame with a length other than a multiple of 6 octets MUST + be treated as a connection error (Section 5.4.1) of type + FRAME_SIZE_ERROR. + +6.5.1. SETTINGS Format + + The frame payload of a SETTINGS frame consists of zero or more + settings, each consisting of an unsigned 16-bit setting identifier + and an unsigned 32-bit value. + + SETTINGS Frame { + Length (24), + Type (8) = 0x04, + + Unused Flags (7), + ACK Flag (1), + + Reserved (1), + Stream Identifier (31) = 0, + + Setting (48) ..., + } + + Setting { + Identifier (16), + Value (32), + } + + Figure 7: SETTINGS Frame Format + + The Length, Type, Unused Flag(s), Reserved, and Stream Identifier + fields are described in Section 4. The frame payload of a SETTINGS + frame contains any number of Setting fields, each of which consists + of: + + Identifier: A 16-bit setting identifier; see Section 6.5.2. + + Value: A 32-bit value for the setting. + +6.5.2. Defined Settings + + The following settings are defined: + + SETTINGS_HEADER_TABLE_SIZE (0x01): This setting allows the sender to + inform the remote endpoint of the maximum size of the compression + table used to decode field blocks, in units of octets. The + encoder can select any size equal to or less than this value by + using signaling specific to the compression format inside a field + block (see [COMPRESSION]). The initial value is 4,096 octets. + + SETTINGS_ENABLE_PUSH (0x02): This setting can be used to enable or + disable server push. A server MUST NOT send a PUSH_PROMISE frame + if it receives this parameter set to a value of 0; see + Section 8.4. A client that has both set this parameter to 0 and + had it acknowledged MUST treat the receipt of a PUSH_PROMISE frame + as a connection error (Section 5.4.1) of type PROTOCOL_ERROR. + + The initial value of SETTINGS_ENABLE_PUSH is 1. For a client, + this value indicates that it is willing to receive PUSH_PROMISE + frames. For a server, this initial value has no effect, and is + equivalent to the value 0. Any value other than 0 or 1 MUST be + treated as a connection error (Section 5.4.1) of type + PROTOCOL_ERROR. + + A server MUST NOT explicitly set this value to 1. A server MAY + choose to omit this setting when it sends a SETTINGS frame, but if + a server does include a value, it MUST be 0. A client MUST treat + receipt of a SETTINGS frame with SETTINGS_ENABLE_PUSH set to 1 as + a connection error (Section 5.4.1) of type PROTOCOL_ERROR. + + SETTINGS_MAX_CONCURRENT_STREAMS (0x03): This setting indicates the + maximum number of concurrent streams that the sender will allow. + This limit is directional: it applies to the number of streams + that the sender permits the receiver to create. Initially, there + is no limit to this value. It is recommended that this value be + no smaller than 100, so as to not unnecessarily limit parallelism. + + A value of 0 for SETTINGS_MAX_CONCURRENT_STREAMS SHOULD NOT be + treated as special by endpoints. A zero value does prevent the + creation of new streams; however, this can also happen for any + limit that is exhausted with active streams. Servers SHOULD only + set a zero value for short durations; if a server does not wish to + accept requests, closing the connection is more appropriate. + + SETTINGS_INITIAL_WINDOW_SIZE (0x04): This setting indicates the + sender's initial window size (in units of octets) for stream-level + flow control. The initial value is 2^16-1 (65,535) octets. + + This setting affects the window size of all streams (see + Section 6.9.2). + + Values above the maximum flow-control window size of 2^31-1 MUST + be treated as a connection error (Section 5.4.1) of type + FLOW_CONTROL_ERROR. + + SETTINGS_MAX_FRAME_SIZE (0x05): This setting indicates the size of + the largest frame payload that the sender is willing to receive, + in units of octets. + + The initial value is 2^14 (16,384) octets. The value advertised + by an endpoint MUST be between this initial value and the maximum + allowed frame size (2^24-1 or 16,777,215 octets), inclusive. + Values outside this range MUST be treated as a connection error + (Section 5.4.1) of type PROTOCOL_ERROR. + + SETTINGS_MAX_HEADER_LIST_SIZE (0x06): This advisory setting informs + a peer of the maximum field section size that the sender is + prepared to accept, in units of octets. The value is based on the + uncompressed size of field lines, including the length of the name + and value in units of octets plus an overhead of 32 octets for + each field line. + + For any given request, a lower limit than what is advertised MAY + be enforced. The initial value of this setting is unlimited. + + An endpoint that receives a SETTINGS frame with any unknown or + unsupported identifier MUST ignore that setting. + +6.5.3. Settings Synchronization + + Most values in SETTINGS benefit from or require an understanding of + when the peer has received and applied the changed parameter values. + In order to provide such synchronization timepoints, the recipient of + a SETTINGS frame in which the ACK flag is not set MUST apply the + updated settings as soon as possible upon receipt. SETTINGS frames + are acknowledged in the order in which they are received. + + The values in the SETTINGS frame MUST be processed in the order they + appear, with no other frame processing between values. Unsupported + settings MUST be ignored. Once all values have been processed, the + recipient MUST immediately emit a SETTINGS frame with the ACK flag + set. Upon receiving a SETTINGS frame with the ACK flag set, the + sender of the altered settings can rely on the values from the oldest + unacknowledged SETTINGS frame having been applied. + + If the sender of a SETTINGS frame does not receive an acknowledgment + within a reasonable amount of time, it MAY issue a connection error + (Section 5.4.1) of type SETTINGS_TIMEOUT. In setting a timeout, some + allowance needs to be made for processing delays at the peer; a + timeout that is solely based on the round-trip time between endpoints + might result in spurious errors. + +6.6. PUSH_PROMISE + + The PUSH_PROMISE frame (type=0x05) is used to notify the peer + endpoint in advance of streams the sender intends to initiate. The + PUSH_PROMISE frame includes the unsigned 31-bit identifier of the + stream the endpoint plans to create along with a field section that + provides additional context for the stream. Section 8.4 contains a + thorough description of the use of PUSH_PROMISE frames. + + PUSH_PROMISE Frame { + Length (24), + Type (8) = 0x05, + + Unused Flags (4), + PADDED Flag (1), + END_HEADERS Flag (1), + Unused Flags (2), + + Reserved (1), + Stream Identifier (31), + + [Pad Length (8)], + Reserved (1), + Promised Stream ID (31), + Field Block Fragment (..), + Padding (..2040), + } + + Figure 8: PUSH_PROMISE Frame Format + + The Length, Type, Unused Flag(s), Reserved, and Stream Identifier + fields are described in Section 4. The PUSH_PROMISE frame payload + has the following additional fields: + + Pad Length: An 8-bit field containing the length of the frame + padding in units of octets. This field is only present if the + PADDED flag is set. + + Promised Stream ID: An unsigned 31-bit integer that identifies the + stream that is reserved by the PUSH_PROMISE. The promised stream + identifier MUST be a valid choice for the next stream sent by the + sender (see "new stream identifier" in Section 5.1.1). + + Field Block Fragment: A field block fragment (Section 4.3) + containing the request control data and a header section. + + Padding: Padding octets that contain no application semantic value. + Padding octets MUST be set to zero when sending. A receiver is + not obligated to verify padding but MAY treat non-zero padding as + a connection error (Section 5.4.1) of type PROTOCOL_ERROR. + + The PUSH_PROMISE frame defines the following flags: + + PADDED (0x08): When set, the PADDED flag indicates that the Pad + Length field and any padding that it describes are present. + + END_HEADERS (0x04): When set, the END_HEADERS flag indicates that + this frame contains an entire field block (Section 4.3) and is not + followed by any CONTINUATION frames. + + A PUSH_PROMISE frame without the END_HEADERS flag set MUST be + followed by a CONTINUATION frame for the same stream. A receiver + MUST treat the receipt of any other type of frame or a frame on a + different stream as a connection error (Section 5.4.1) of type + PROTOCOL_ERROR. + + PUSH_PROMISE frames MUST only be sent on a peer-initiated stream that + is in either the "open" or "half-closed (remote)" state. The stream + identifier of a PUSH_PROMISE frame indicates the stream it is + associated with. If the Stream Identifier field specifies the value + 0x00, a recipient MUST respond with a connection error + (Section 5.4.1) of type PROTOCOL_ERROR. + + Promised streams are not required to be used in the order they are + promised. The PUSH_PROMISE only reserves stream identifiers for + later use. + + PUSH_PROMISE MUST NOT be sent if the SETTINGS_ENABLE_PUSH setting of + the peer endpoint is set to 0. An endpoint that has set this setting + and has received acknowledgment MUST treat the receipt of a + PUSH_PROMISE frame as a connection error (Section 5.4.1) of type + PROTOCOL_ERROR. + + Recipients of PUSH_PROMISE frames can choose to reject promised + streams by returning a RST_STREAM referencing the promised stream + identifier back to the sender of the PUSH_PROMISE. + + A PUSH_PROMISE frame modifies the connection state in two ways. + First, the inclusion of a field block (Section 4.3) potentially + modifies the state maintained for field section compression. Second, + PUSH_PROMISE also reserves a stream for later use, causing the + promised stream to enter the "reserved (local)" or "reserved + (remote)" state. A sender MUST NOT send a PUSH_PROMISE on a stream + unless that stream is either "open" or "half-closed (remote)"; the + sender MUST ensure that the promised stream is a valid choice for a + new stream identifier (Section 5.1.1) (that is, the promised stream + MUST be in the "idle" state). + + Since PUSH_PROMISE reserves a stream, ignoring a PUSH_PROMISE frame + causes the stream state to become indeterminate. A receiver MUST + treat the receipt of a PUSH_PROMISE on a stream that is neither + "open" nor "half-closed (local)" as a connection error + (Section 5.4.1) of type PROTOCOL_ERROR. However, an endpoint that + has sent RST_STREAM on the associated stream MUST handle PUSH_PROMISE + frames that might have been created before the RST_STREAM frame is + received and processed. + + A receiver MUST treat the receipt of a PUSH_PROMISE that promises an + illegal stream identifier (Section 5.1.1) as a connection error + (Section 5.4.1) of type PROTOCOL_ERROR. Note that an illegal stream + identifier is an identifier for a stream that is not currently in the + "idle" state. + + The total number of padding octets is determined by the value of the + Pad Length field. If the length of the padding is the length of the + frame payload or greater, the recipient MUST treat this as a + connection error (Section 5.4.1) of type PROTOCOL_ERROR. + + | Note: A frame can be increased in size by one octet by + | including a Pad Length field with a value of zero. + +6.7. PING + + The PING frame (type=0x06) is a mechanism for measuring a minimal + round-trip time from the sender, as well as determining whether an + idle connection is still functional. PING frames can be sent from + any endpoint. + + PING Frame { + Length (24) = 0x08, + Type (8) = 0x06, + + Unused Flags (7), + ACK Flag (1), + + Reserved (1), + Stream Identifier (31) = 0, + + Opaque Data (64), + } + + Figure 9: PING Frame Format + + The Length, Type, Unused Flag(s), Reserved, and Stream Identifier + fields are described in Section 4. + + In addition to the frame header, PING frames MUST contain 8 octets of + opaque data in the frame payload. A sender can include any value it + chooses and use those octets in any fashion. + + Receivers of a PING frame that does not include an ACK flag MUST send + a PING frame with the ACK flag set in response, with an identical + frame payload. PING responses SHOULD be given higher priority than + any other frame. + + The PING frame defines the following flags: + + ACK (0x01): When set, the ACK flag indicates that this PING frame is + a PING response. An endpoint MUST set this flag in PING + responses. An endpoint MUST NOT respond to PING frames containing + this flag. + + PING frames are not associated with any individual stream. If a PING + frame is received with a Stream Identifier field value other than + 0x00, the recipient MUST respond with a connection error + (Section 5.4.1) of type PROTOCOL_ERROR. + + Receipt of a PING frame with a length field value other than 8 MUST + be treated as a connection error (Section 5.4.1) of type + FRAME_SIZE_ERROR. + +6.8. GOAWAY + + The GOAWAY frame (type=0x07) is used to initiate shutdown of a + connection or to signal serious error conditions. GOAWAY allows an + endpoint to gracefully stop accepting new streams while still + finishing processing of previously established streams. This enables + administrative actions, like server maintenance. + + There is an inherent race condition between an endpoint starting new + streams and the remote peer sending a GOAWAY frame. To deal with + this case, the GOAWAY contains the stream identifier of the last + peer-initiated stream that was or might be processed on the sending + endpoint in this connection. For instance, if the server sends a + GOAWAY frame, the identified stream is the highest-numbered stream + initiated by the client. + + Once the GOAWAY is sent, the sender will ignore frames sent on + streams initiated by the receiver if the stream has an identifier + higher than the included last stream identifier. Receivers of a + GOAWAY frame MUST NOT open additional streams on the connection, + although a new connection can be established for new streams. + + If the receiver of the GOAWAY has sent data on streams with a higher + stream identifier than what is indicated in the GOAWAY frame, those + streams are not or will not be processed. The receiver of the GOAWAY + frame can treat the streams as though they had never been created at + all, thereby allowing those streams to be retried later on a new + connection. + + Endpoints SHOULD always send a GOAWAY frame before closing a + connection so that the remote peer can know whether a stream has been + partially processed or not. For example, if an HTTP client sends a + POST at the same time that a server closes a connection, the client + cannot know if the server started to process that POST request if the + server does not send a GOAWAY frame to indicate what streams it might + have acted on. + + An endpoint might choose to close a connection without sending a + GOAWAY for misbehaving peers. + + A GOAWAY frame might not immediately precede closing of the + connection; a receiver of a GOAWAY that has no more use for the + connection SHOULD still send a GOAWAY frame before terminating the + connection. + + GOAWAY Frame { + Length (24), + Type (8) = 0x07, + + Unused Flags (8), + + Reserved (1), + Stream Identifier (31) = 0, + + Reserved (1), + Last-Stream-ID (31), + Error Code (32), + Additional Debug Data (..), + } + + Figure 10: GOAWAY Frame Format + + The Length, Type, Unused Flag(s), Reserved, and Stream Identifier + fields are described in Section 4. + + The GOAWAY frame does not define any flags. + + The GOAWAY frame applies to the connection, not a specific stream. + An endpoint MUST treat a GOAWAY frame with a stream identifier other + than 0x00 as a connection error (Section 5.4.1) of type + PROTOCOL_ERROR. + + The last stream identifier in the GOAWAY frame contains the highest- + numbered stream identifier for which the sender of the GOAWAY frame + might have taken some action on or might yet take action on. All + streams up to and including the identified stream might have been + processed in some way. The last stream identifier can be set to 0 if + no streams were processed. + + | Note: In this context, "processed" means that some data from + | the stream was passed to some higher layer of software that + | might have taken some action as a result. + + If a connection terminates without a GOAWAY frame, the last stream + identifier is effectively the highest possible stream identifier. + + On streams with lower- or equal-numbered identifiers that were not + closed completely prior to the connection being closed, reattempting + requests, transactions, or any protocol activity is not possible, + except for idempotent actions like HTTP GET, PUT, or DELETE. Any + protocol activity that uses higher-numbered streams can be safely + retried using a new connection. + + Activity on streams numbered lower than or equal to the last stream + identifier might still complete successfully. The sender of a GOAWAY + frame might gracefully shut down a connection by sending a GOAWAY + frame, maintaining the connection in an "open" state until all in- + progress streams complete. + + An endpoint MAY send multiple GOAWAY frames if circumstances change. + For instance, an endpoint that sends GOAWAY with NO_ERROR during + graceful shutdown could subsequently encounter a condition that + requires immediate termination of the connection. The last stream + identifier from the last GOAWAY frame received indicates which + streams could have been acted upon. Endpoints MUST NOT increase the + value they send in the last stream identifier, since the peers might + already have retried unprocessed requests on another connection. + + A client that is unable to retry requests loses all requests that are + in flight when the server closes the connection. This is especially + true for intermediaries that might not be serving clients using + HTTP/2. A server that is attempting to gracefully shut down a + connection SHOULD send an initial GOAWAY frame with the last stream + identifier set to 2^31-1 and a NO_ERROR code. This signals to the + client that a shutdown is imminent and that initiating further + requests is prohibited. After allowing time for any in-flight stream + creation (at least one round-trip time), the server MAY send another + GOAWAY frame with an updated last stream identifier. This ensures + that a connection can be cleanly shut down without losing requests. + + After sending a GOAWAY frame, the sender can discard frames for + streams initiated by the receiver with identifiers higher than the + identified last stream. However, any frames that alter connection + state cannot be completely ignored. For instance, HEADERS, + PUSH_PROMISE, and CONTINUATION frames MUST be minimally processed to + ensure that the state maintained for field section compression is + consistent (see Section 4.3); similarly, DATA frames MUST be counted + toward the connection flow-control window. Failure to process these + frames can cause flow control or field section compression state to + become unsynchronized. + + The GOAWAY frame also contains a 32-bit error code (Section 7) that + contains the reason for closing the connection. + + Endpoints MAY append opaque data to the frame payload of any GOAWAY + frame. Additional debug data is intended for diagnostic purposes + only and carries no semantic value. Debug information could contain + security- or privacy-sensitive data. Logged or otherwise + persistently stored debug data MUST have adequate safeguards to + prevent unauthorized access. + +6.9. WINDOW_UPDATE + + The WINDOW_UPDATE frame (type=0x08) is used to implement flow + control; see Section 5.2 for an overview. + + Flow control operates at two levels: on each individual stream and on + the entire connection. + + Both types of flow control are hop by hop, that is, only between the + two endpoints. Intermediaries do not forward WINDOW_UPDATE frames + between dependent connections. However, throttling of data transfer + by any receiver can indirectly cause the propagation of flow-control + information toward the original sender. + + Flow control only applies to frames that are identified as being + subject to flow control. Of the frame types defined in this + document, this includes only DATA frames. Frames that are exempt + from flow control MUST be accepted and processed, unless the receiver + is unable to assign resources to handling the frame. A receiver MAY + respond with a stream error (Section 5.4.2) or connection error + (Section 5.4.1) of type FLOW_CONTROL_ERROR if it is unable to accept + a frame. + + WINDOW_UPDATE Frame { + Length (24) = 0x04, + Type (8) = 0x08, + + Unused Flags (8), + + Reserved (1), + Stream Identifier (31), + + Reserved (1), + Window Size Increment (31), + } + + Figure 11: WINDOW_UPDATE Frame Format + + The Length, Type, Unused Flag(s), Reserved, and Stream Identifier + fields are described in Section 4. The frame payload of a + WINDOW_UPDATE frame is one reserved bit plus an unsigned 31-bit + integer indicating the number of octets that the sender can transmit + in addition to the existing flow-control window. The legal range for + the increment to the flow-control window is 1 to 2^31-1 + (2,147,483,647) octets. + + The WINDOW_UPDATE frame does not define any flags. + + The WINDOW_UPDATE frame can be specific to a stream or to the entire + connection. In the former case, the frame's stream identifier + indicates the affected stream; in the latter, the value "0" indicates + that the entire connection is the subject of the frame. + + A receiver MUST treat the receipt of a WINDOW_UPDATE frame with a + flow-control window increment of 0 as a stream error (Section 5.4.2) + of type PROTOCOL_ERROR; errors on the connection flow-control window + MUST be treated as a connection error (Section 5.4.1). + + WINDOW_UPDATE can be sent by a peer that has sent a frame with the + END_STREAM flag set. This means that a receiver could receive a + WINDOW_UPDATE frame on a stream in a "half-closed (remote)" or + "closed" state. A receiver MUST NOT treat this as an error (see + Section 5.1). + + A receiver that receives a flow-controlled frame MUST always account + for its contribution against the connection flow-control window, + unless the receiver treats this as a connection error + (Section 5.4.1). This is necessary even if the frame is in error. + The sender counts the frame toward the flow-control window, but if + the receiver does not, the flow-control window at the sender and + receiver can become different. + + A WINDOW_UPDATE frame with a length other than 4 octets MUST be + treated as a connection error (Section 5.4.1) of type + FRAME_SIZE_ERROR. + +6.9.1. The Flow-Control Window + + Flow control in HTTP/2 is implemented using a window kept by each + sender on every stream. The flow-control window is a simple integer + value that indicates how many octets of data the sender is permitted + to transmit; as such, its size is a measure of the buffering capacity + of the receiver. + + Two flow-control windows are applicable: the stream flow-control + window and the connection flow-control window. The sender MUST NOT + send a flow-controlled frame with a length that exceeds the space + available in either of the flow-control windows advertised by the + receiver. Frames with zero length with the END_STREAM flag set (that + is, an empty DATA frame) MAY be sent if there is no available space + in either flow-control window. + + For flow-control calculations, the 9-octet frame header is not + counted. + + After sending a flow-controlled frame, the sender reduces the space + available in both windows by the length of the transmitted frame. + + The receiver of a frame sends a WINDOW_UPDATE frame as it consumes + data and frees up space in flow-control windows. Separate + WINDOW_UPDATE frames are sent for the stream- and connection-level + flow-control windows. Receivers are advised to have mechanisms in + place to avoid sending WINDOW_UPDATE frames with very small + increments; see Section 4.2.3.3 of [RFC1122]. + + A sender that receives a WINDOW_UPDATE frame updates the + corresponding window by the amount specified in the frame. + + A sender MUST NOT allow a flow-control window to exceed 2^31-1 + octets. If a sender receives a WINDOW_UPDATE that causes a flow- + control window to exceed this maximum, it MUST terminate either the + stream or the connection, as appropriate. For streams, the sender + sends a RST_STREAM with an error code of FLOW_CONTROL_ERROR; for the + connection, a GOAWAY frame with an error code of FLOW_CONTROL_ERROR + is sent. + + Flow-controlled frames from the sender and WINDOW_UPDATE frames from + the receiver are completely asynchronous with respect to each other. + This property allows a receiver to aggressively update the window + size kept by the sender to prevent streams from stalling. + +6.9.2. Initial Flow-Control Window Size + + When an HTTP/2 connection is first established, new streams are + created with an initial flow-control window size of 65,535 octets. + The connection flow-control window is also 65,535 octets. Both + endpoints can adjust the initial window size for new streams by + including a value for SETTINGS_INITIAL_WINDOW_SIZE in the SETTINGS + frame. The connection flow-control window can only be changed using + WINDOW_UPDATE frames. + + Prior to receiving a SETTINGS frame that sets a value for + SETTINGS_INITIAL_WINDOW_SIZE, an endpoint can only use the default + initial window size when sending flow-controlled frames. Similarly, + the connection flow-control window is set based on the default + initial window size until a WINDOW_UPDATE frame is received. + + In addition to changing the flow-control window for streams that are + not yet active, a SETTINGS frame can alter the initial flow-control + window size for streams with active flow-control windows (that is, + streams in the "open" or "half-closed (remote)" state). When the + value of SETTINGS_INITIAL_WINDOW_SIZE changes, a receiver MUST adjust + the size of all stream flow-control windows that it maintains by the + difference between the new value and the old value. + + A change to SETTINGS_INITIAL_WINDOW_SIZE can cause the available + space in a flow-control window to become negative. A sender MUST + track the negative flow-control window and MUST NOT send new flow- + controlled frames until it receives WINDOW_UPDATE frames that cause + the flow-control window to become positive. + + For example, if the client sends 60 KB immediately on connection + establishment and the server sets the initial window size to be 16 + KB, the client will recalculate the available flow-control window to + be -44 KB on receipt of the SETTINGS frame. The client retains a + negative flow-control window until WINDOW_UPDATE frames restore the + window to being positive, after which the client can resume sending. + + A SETTINGS frame cannot alter the connection flow-control window. + + An endpoint MUST treat a change to SETTINGS_INITIAL_WINDOW_SIZE that + causes any flow-control window to exceed the maximum size as a + connection error (Section 5.4.1) of type FLOW_CONTROL_ERROR. + +6.9.3. Reducing the Stream Window Size + + A receiver that wishes to use a smaller flow-control window than the + current size can send a new SETTINGS frame. However, the receiver + MUST be prepared to receive data that exceeds this window size, since + the sender might send data that exceeds the lower limit prior to + processing the SETTINGS frame. + + After sending a SETTINGS frame that reduces the initial flow-control + window size, a receiver MAY continue to process streams that exceed + flow-control limits. Allowing streams to continue does not allow the + receiver to immediately reduce the space it reserves for flow-control + windows. Progress on these streams can also stall, since + WINDOW_UPDATE frames are needed to allow the sender to resume + sending. The receiver MAY instead send a RST_STREAM with an error + code of FLOW_CONTROL_ERROR for the affected streams. + +6.10. CONTINUATION + + The CONTINUATION frame (type=0x09) is used to continue a sequence of + field block fragments (Section 4.3). Any number of CONTINUATION + frames can be sent, as long as the preceding frame is on the same + stream and is a HEADERS, PUSH_PROMISE, or CONTINUATION frame without + the END_HEADERS flag set. + + CONTINUATION Frame { + Length (24), + Type (8) = 0x09, + + Unused Flags (5), + END_HEADERS Flag (1), + Unused Flags (2), + + Reserved (1), + Stream Identifier (31), + + Field Block Fragment (..), + } + + Figure 12: CONTINUATION Frame Format + + The Length, Type, Unused Flag(s), Reserved, and Stream Identifier + fields are described in Section 4. The CONTINUATION frame payload + contains a field block fragment (Section 4.3). + + The CONTINUATION frame defines the following flag: + + END_HEADERS (0x04): When set, the END_HEADERS flag indicates that + this frame ends a field block (Section 4.3). + + If the END_HEADERS flag is not set, this frame MUST be followed by + another CONTINUATION frame. A receiver MUST treat the receipt of + any other type of frame or a frame on a different stream as a + connection error (Section 5.4.1) of type PROTOCOL_ERROR. + + The CONTINUATION frame changes the connection state as defined in + Section 4.3. + + CONTINUATION frames MUST be associated with a stream. If a + CONTINUATION frame is received with a Stream Identifier field of + 0x00, the recipient MUST respond with a connection error + (Section 5.4.1) of type PROTOCOL_ERROR. + + A CONTINUATION frame MUST be preceded by a HEADERS, PUSH_PROMISE or + CONTINUATION frame without the END_HEADERS flag set. A recipient + that observes violation of this rule MUST respond with a connection + error (Section 5.4.1) of type PROTOCOL_ERROR. + +7. Error Codes + + Error codes are 32-bit fields that are used in RST_STREAM and GOAWAY + frames to convey the reasons for the stream or connection error. + + Error codes share a common code space. Some error codes apply only + to either streams or the entire connection and have no defined + semantics in the other context. + + The following error codes are defined: + + NO_ERROR (0x00): The associated condition is not a result of an + error. For example, a GOAWAY might include this code to indicate + graceful shutdown of a connection. + + PROTOCOL_ERROR (0x01): The endpoint detected an unspecific protocol + error. This error is for use when a more specific error code is + not available. + + INTERNAL_ERROR (0x02): The endpoint encountered an unexpected + internal error. + + FLOW_CONTROL_ERROR (0x03): The endpoint detected that its peer + violated the flow-control protocol. + + SETTINGS_TIMEOUT (0x04): The endpoint sent a SETTINGS frame but did + not receive a response in a timely manner. See Section 6.5.3 + ("Settings Synchronization"). + + STREAM_CLOSED (0x05): The endpoint received a frame after a stream + was half-closed. + + FRAME_SIZE_ERROR (0x06): The endpoint received a frame with an + invalid size. + + REFUSED_STREAM (0x07): The endpoint refused the stream prior to + performing any application processing (see Section 8.7 for + details). + + CANCEL (0x08): The endpoint uses this error code to indicate that + the stream is no longer needed. + + COMPRESSION_ERROR (0x09): The endpoint is unable to maintain the + field section compression context for the connection. + + CONNECT_ERROR (0x0a): The connection established in response to a + CONNECT request (Section 8.5) was reset or abnormally closed. + + ENHANCE_YOUR_CALM (0x0b): The endpoint detected that its peer is + exhibiting a behavior that might be generating excessive load. + + INADEQUATE_SECURITY (0x0c): The underlying transport has properties + that do not meet minimum security requirements (see Section 9.2). + + HTTP_1_1_REQUIRED (0x0d): The endpoint requires that HTTP/1.1 be + used instead of HTTP/2. + + Unknown or unsupported error codes MUST NOT trigger any special + behavior. These MAY be treated by an implementation as being + equivalent to INTERNAL_ERROR. + +8. Expressing HTTP Semantics in HTTP/2 + + HTTP/2 is an instantiation of the HTTP message abstraction (Section 6 + of [HTTP]). + +8.1. HTTP Message Framing + + A client sends an HTTP request on a new stream, using a previously + unused stream identifier (Section 5.1.1). A server sends an HTTP + response on the same stream as the request. + + An HTTP message (request or response) consists of: + + 1. one HEADERS frame (followed by zero or more CONTINUATION frames) + containing the header section (see Section 6.3 of [HTTP]), + + 2. zero or more DATA frames containing the message content (see + Section 6.4 of [HTTP]), and + + 3. optionally, one HEADERS frame (followed by zero or more + CONTINUATION frames) containing the trailer section, if present + (see Section 6.5 of [HTTP]). + + For a response only, a server MAY send any number of interim + responses before the HEADERS frame containing a final response. An + interim response consists of a HEADERS frame (which might be followed + by zero or more CONTINUATION frames) containing the control data and + header section of an interim (1xx) HTTP response (see Section 15 of + [HTTP]). A HEADERS frame with the END_STREAM flag set that carries + an informational status code is malformed (Section 8.1.1). + + The last frame in the sequence bears an END_STREAM flag, noting that + a HEADERS frame with the END_STREAM flag set can be followed by + CONTINUATION frames that carry any remaining fragments of the field + block. + + Other frames (from any stream) MUST NOT occur between the HEADERS + frame and any CONTINUATION frames that might follow. + + HTTP/2 uses DATA frames to carry message content. The chunked + transfer encoding defined in Section 7.1 of [HTTP/1.1] cannot be used + in HTTP/2; see Section 8.2.2. + + Trailer fields are carried in a field block that also terminates the + stream. That is, trailer fields comprise a sequence starting with a + HEADERS frame, followed by zero or more CONTINUATION frames, where + the HEADERS frame bears an END_STREAM flag. Trailers MUST NOT + include pseudo-header fields (Section 8.3). An endpoint that + receives pseudo-header fields in trailers MUST treat the request or + response as malformed (Section 8.1.1). + + An endpoint that receives a HEADERS frame without the END_STREAM flag + set after receiving the HEADERS frame that opens a request or after + receiving a final (non-informational) status code MUST treat the + corresponding request or response as malformed (Section 8.1.1). + + An HTTP request/response exchange fully consumes a single stream. A + request starts with the HEADERS frame that puts the stream into the + "open" state. The request ends with a frame with the END_STREAM flag + set, which causes the stream to become "half-closed (local)" for the + client and "half-closed (remote)" for the server. A response stream + starts with zero or more interim responses in HEADERS frames, + followed by a HEADERS frame containing a final status code. + + An HTTP response is complete after the server sends -- or the client + receives -- a frame with the END_STREAM flag set (including any + CONTINUATION frames needed to complete a field block). A server can + send a complete response prior to the client sending an entire + request if the response does not depend on any portion of the request + that has not been sent and received. When this is true, a server MAY + request that the client abort transmission of a request without error + by sending a RST_STREAM with an error code of NO_ERROR after sending + a complete response (i.e., a frame with the END_STREAM flag set). + Clients MUST NOT discard responses as a result of receiving such a + RST_STREAM, though clients can always discard responses at their + discretion for other reasons. + +8.1.1. Malformed Messages + + A malformed request or response is one that is an otherwise valid + sequence of HTTP/2 frames but is invalid due to the presence of + extraneous frames, prohibited fields or pseudo-header fields, the + absence of mandatory pseudo-header fields, the inclusion of uppercase + field names, or invalid field names and/or values (in certain + circumstances; see Section 8.2). + + A request or response that includes message content can include a + content-length header field. A request or response is also malformed + if the value of a content-length header field does not equal the sum + of the DATA frame payload lengths that form the content, unless the + message is defined as having no content. For example, 204 or 304 + responses contain no content, as does the response to a HEAD request. + A response that is defined to have no content, as described in + Section 6.4.1 of [HTTP], MAY have a non-zero content-length header + field, even though no content is included in DATA frames. + + Intermediaries that process HTTP requests or responses (i.e., any + intermediary not acting as a tunnel) MUST NOT forward a malformed + request or response. Malformed requests or responses that are + detected MUST be treated as a stream error (Section 5.4.2) of type + PROTOCOL_ERROR. + + For malformed requests, a server MAY send an HTTP response prior to + closing or resetting the stream. Clients MUST NOT accept a malformed + response. + + Endpoints that progressively process messages might have performed + some processing before identifying a request or response as + malformed. For instance, it might be possible to generate an + informational or 404 status code without having received a complete + request. Similarly, intermediaries might forward incomplete messages + before detecting errors. A server MAY generate a final response + before receiving an entire request when the response does not depend + on the remainder of the request being correct. + + These requirements are intended to protect against several types of + common attacks against HTTP; they are deliberately strict because + being permissive can expose implementations to these vulnerabilities. + +8.2. HTTP Fields + + HTTP fields (Section 5 of [HTTP]) are conveyed by HTTP/2 in the + HEADERS, CONTINUATION, and PUSH_PROMISE frames, compressed with HPACK + [COMPRESSION]. + + Field names MUST be converted to lowercase when constructing an + HTTP/2 message. + +8.2.1. Field Validity + + The definitions of field names and values in HTTP prohibit some + characters that HPACK might be able to convey. HTTP/2 + implementations SHOULD validate field names and values according to + their definitions in Sections 5.1 and 5.5 of [HTTP], respectively, + and treat messages that contain prohibited characters as malformed + (Section 8.1.1). + + Failure to validate fields can be exploited for request smuggling + attacks. In particular, unvalidated fields might enable attacks when + messages are forwarded using HTTP/1.1 [HTTP/1.1], where characters + such as carriage return (CR), line feed (LF), and COLON are used as + delimiters. Implementations MUST perform the following minimal + validation of field names and values: + + * A field name MUST NOT contain characters in the ranges 0x00-0x20, + 0x41-0x5a, or 0x7f-0xff (all ranges inclusive). This specifically + excludes all non-visible ASCII characters, ASCII SP (0x20), and + uppercase characters ('A' to 'Z', ASCII 0x41 to 0x5a). + + * With the exception of pseudo-header fields (Section 8.3), which + have a name that starts with a single colon, field names MUST NOT + include a colon (ASCII COLON, 0x3a). + + * A field value MUST NOT contain the zero value (ASCII NUL, 0x00), + line feed (ASCII LF, 0x0a), or carriage return (ASCII CR, 0x0d) at + any position. + + * A field value MUST NOT start or end with an ASCII whitespace + character (ASCII SP or HTAB, 0x20 or 0x09). + + | Note: An implementation that validates fields according to the + | definitions in Sections 5.1 and 5.5 of [HTTP] only needs an + | additional check that field names do not include uppercase + | characters. + + A request or response that contains a field that violates any of + these conditions MUST be treated as malformed (Section 8.1.1). In + particular, an intermediary that does not process fields when + forwarding messages MUST NOT forward fields that contain any of the + values that are listed as prohibited above. + + When a request message violates one of these requirements, an + implementation SHOULD generate a 400 (Bad Request) status code (see + Section 15.5.1 of [HTTP]), unless a more suitable status code is + defined or the status code cannot be sent (e.g., because the error + occurs in a trailer field). + + | Note: Field values that are not valid according to the + | definition of the corresponding field do not cause a request to + | be malformed; the requirements above only apply to the generic + | syntax for fields as defined in Section 5 of [HTTP]. + +8.2.2. Connection-Specific Header Fields + + HTTP/2 does not use the Connection header field (Section 7.6.1 of + [HTTP]) to indicate connection-specific header fields; in this + protocol, connection-specific metadata is conveyed by other means. + An endpoint MUST NOT generate an HTTP/2 message containing + connection-specific header fields. This includes the Connection + header field and those listed as having connection-specific semantics + in Section 7.6.1 of [HTTP] (that is, Proxy-Connection, Keep-Alive, + Transfer-Encoding, and Upgrade). Any message containing connection- + specific header fields MUST be treated as malformed (Section 8.1.1). + + The only exception to this is the TE header field, which MAY be + present in an HTTP/2 request; when it is, it MUST NOT contain any + value other than "trailers". + + An intermediary transforming an HTTP/1.x message to HTTP/2 MUST + remove connection-specific header fields as discussed in + Section 7.6.1 of [HTTP], or their messages will be treated by other + HTTP/2 endpoints as malformed (Section 8.1.1). + + | Note: HTTP/2 purposefully does not support upgrade to another + | protocol. The handshake methods described in Section 3 are + | believed sufficient to negotiate the use of alternative + | protocols. + +8.2.3. Compressing the Cookie Header Field + + The Cookie header field [COOKIE] uses a semicolon (";") to delimit + cookie-pairs (or "crumbs"). This header field contains multiple + values, but does not use a COMMA (",") as a separator, thereby + preventing cookie-pairs from being sent on multiple field lines (see + Section 5.2 of [HTTP]). This can significantly reduce compression + efficiency, as updates to individual cookie-pairs would invalidate + any field lines that are stored in the HPACK table. + + To allow for better compression efficiency, the Cookie header field + MAY be split into separate header fields, each with one or more + cookie-pairs. If there are multiple Cookie header fields after + decompression, these MUST be concatenated into a single octet string + using the two-octet delimiter of 0x3b, 0x20 (the ASCII string "; ") + before being passed into a non-HTTP/2 context, such as an HTTP/1.1 + connection, or a generic HTTP server application. + + Therefore, the following two lists of Cookie header fields are + semantically equivalent. + + cookie: a=b; c=d; e=f + + cookie: a=b + cookie: c=d + cookie: e=f + +8.3. HTTP Control Data + + HTTP/2 uses special pseudo-header fields beginning with a ':' + character (ASCII 0x3a) to convey message control data (see + Section 6.2 of [HTTP]). + + Pseudo-header fields are not HTTP header fields. Endpoints MUST NOT + generate pseudo-header fields other than those defined in this + document. Note that an extension could negotiate the use of + additional pseudo-header fields; see Section 5.5. + + Pseudo-header fields are only valid in the context in which they are + defined. Pseudo-header fields defined for requests MUST NOT appear + in responses; pseudo-header fields defined for responses MUST NOT + appear in requests. Pseudo-header fields MUST NOT appear in a + trailer section. Endpoints MUST treat a request or response that + contains undefined or invalid pseudo-header fields as malformed + (Section 8.1.1). + + All pseudo-header fields MUST appear in a field block before all + regular field lines. Any request or response that contains a pseudo- + header field that appears in a field block after a regular field line + MUST be treated as malformed (Section 8.1.1). + + The same pseudo-header field name MUST NOT appear more than once in a + field block. A field block for an HTTP request or response that + contains a repeated pseudo-header field name MUST be treated as + malformed (Section 8.1.1). + +8.3.1. Request Pseudo-Header Fields + + The following pseudo-header fields are defined for HTTP/2 requests: + + * The ":method" pseudo-header field includes the HTTP method + (Section 9 of [HTTP]). + + * The ":scheme" pseudo-header field includes the scheme portion of + the request target. The scheme is taken from the target URI + (Section 3.1 of [RFC3986]) when generating a request directly, or + from the scheme of a translated request (for example, see + Section 3.3 of [HTTP/1.1]). Scheme is omitted for CONNECT + requests (Section 8.5). + + ":scheme" is not restricted to "http" and "https" schemed URIs. A + proxy or gateway can translate requests for non-HTTP schemes, + enabling the use of HTTP to interact with non-HTTP services. + + * The ":authority" pseudo-header field conveys the authority portion + (Section 3.2 of [RFC3986]) of the target URI (Section 7.1 of + [HTTP]). The recipient of an HTTP/2 request MUST NOT use the Host + header field to determine the target URI if ":authority" is + present. + + Clients that generate HTTP/2 requests directly MUST use the + ":authority" pseudo-header field to convey authority information, + unless there is no authority information to convey (in which case + it MUST NOT generate ":authority"). + + Clients MUST NOT generate a request with a Host header field that + differs from the ":authority" pseudo-header field. A server + SHOULD treat a request as malformed if it contains a Host header + field that identifies an entity that differs from the entity in + the ":authority" pseudo-header field. The values of fields need + to be normalized to compare them (see Section 6.2 of [RFC3986]). + An origin server can apply any normalization method, whereas other + servers MUST perform scheme-based normalization (see Section 6.2.3 + of [RFC3986]) of the two fields. + + An intermediary that forwards a request over HTTP/2 MUST construct + an ":authority" pseudo-header field using the authority + information from the control data of the original request, unless + the original request's target URI does not contain authority + information (in which case it MUST NOT generate ":authority"). + Note that the Host header field is not the sole source of this + information; see Section 7.2 of [HTTP]. + + An intermediary that needs to generate a Host header field (which + might be necessary to construct an HTTP/1.1 request) MUST use the + value from the ":authority" pseudo-header field as the value of + the Host field, unless the intermediary also changes the request + target. This replaces any existing Host field to avoid potential + vulnerabilities in HTTP routing. + + An intermediary that forwards a request over HTTP/2 MAY retain any + Host header field. + + Note that request targets for CONNECT or asterisk-form OPTIONS + requests never include authority information; see Sections 7.1 and + 7.2 of [HTTP]. + + ":authority" MUST NOT include the deprecated userinfo subcomponent + for "http" or "https" schemed URIs. + + * The ":path" pseudo-header field includes the path and query parts + of the target URI (the absolute-path production and, optionally, a + '?' character followed by the query production; see Section 4.1 of + [HTTP]). A request in asterisk form (for OPTIONS) includes the + value '*' for the ":path" pseudo-header field. + + This pseudo-header field MUST NOT be empty for "http" or "https" + URIs; "http" or "https" URIs that do not contain a path component + MUST include a value of '/'. The exceptions to this rule are: + + - an OPTIONS request for an "http" or "https" URI that does not + include a path component; these MUST include a ":path" pseudo- + header field with a value of '*' (see Section 7.1 of [HTTP]). + + - CONNECT requests (Section 8.5), where the ":path" pseudo-header + field is omitted. + + All HTTP/2 requests MUST include exactly one valid value for the + ":method", ":scheme", and ":path" pseudo-header fields, unless they + are CONNECT requests (Section 8.5). An HTTP request that omits + mandatory pseudo-header fields is malformed (Section 8.1.1). + + Individual HTTP/2 requests do not carry an explicit indicator of + protocol version. All HTTP/2 requests implicitly have a protocol + version of "2.0" (see Section 6.2 of [HTTP]). + +8.3.2. Response Pseudo-Header Fields + + For HTTP/2 responses, a single ":status" pseudo-header field is + defined that carries the HTTP status code field (see Section 15 of + [HTTP]). This pseudo-header field MUST be included in all responses, + including interim responses; otherwise, the response is malformed + (Section 8.1.1). + + HTTP/2 responses implicitly have a protocol version of "2.0". + +8.4. Server Push + + HTTP/2 allows a server to preemptively send (or "push") responses + (along with corresponding "promised" requests) to a client in + association with a previous client-initiated request. + + Server push was designed to allow a server to improve client- + perceived performance by predicting what requests will follow those + that it receives, thereby removing a round trip for them. For + example, a request for HTML is often followed by requests for + stylesheets and scripts referenced by that page. When these requests + are pushed, the client does not need to wait to receive the + references to them in the HTML and issue separate requests. + + In practice, server push is difficult to use effectively, because it + requires the server to correctly anticipate the additional requests + the client will make, taking into account factors such as caching, + content negotiation, and user behavior. Errors in prediction can + lead to performance degradation, due to the opportunity cost that the + additional data on the wire represents. In particular, pushing any + significant amount of data can cause contention issues with responses + that are more important. + + A client can request that server push be disabled, though this is + negotiated for each hop independently. The SETTINGS_ENABLE_PUSH + setting can be set to 0 to indicate that server push is disabled. + + Promised requests MUST be safe (see Section 9.2.1 of [HTTP]) and + cacheable (see Section 9.2.3 of [HTTP]). Promised requests cannot + include any content or a trailer section. Clients that receive a + promised request that is not cacheable, that is not known to be safe, + or that indicates the presence of request content MUST reset the + promised stream with a stream error (Section 5.4.2) of type + PROTOCOL_ERROR. Note that this could result in the promised stream + being reset if the client does not recognize a newly defined method + as being safe. + + Pushed responses that are cacheable (see Section 3 of [CACHING]) can + be stored by the client, if it implements an HTTP cache. Pushed + responses are considered successfully validated on the origin server + (e.g., if the "no-cache" cache response directive is present; see + Section 5.2.2.4 of [CACHING]) while the stream identified by the + promised stream identifier is still open. + + Pushed responses that are not cacheable MUST NOT be stored by any + HTTP cache. They MAY be made available to the application + separately. + + The server MUST include a value in the ":authority" pseudo-header + field for which the server is authoritative (see Section 10.1). A + client MUST treat a PUSH_PROMISE for which the server is not + authoritative as a stream error (Section 5.4.2) of type + PROTOCOL_ERROR. + + An intermediary can receive pushes from the server and choose not to + forward them on to the client. In other words, how to make use of + the pushed information is up to that intermediary. Equally, the + intermediary might choose to make additional pushes to the client, + without any action taken by the server. + + A client cannot push. Thus, servers MUST treat the receipt of a + PUSH_PROMISE frame as a connection error (Section 5.4.1) of type + PROTOCOL_ERROR. A server cannot set the SETTINGS_ENABLE_PUSH setting + to a value other than 0 (see Section 6.5.2). + +8.4.1. Push Requests + + Server push is semantically equivalent to a server responding to a + request; however, in this case, that request is also sent by the + server, as a PUSH_PROMISE frame. + + The PUSH_PROMISE frame includes a field block that contains control + data and a complete set of request header fields that the server + attributes to the request. It is not possible to push a response to + a request that includes message content. + + Promised requests are always associated with an explicit request from + the client. The PUSH_PROMISE frames sent by the server are sent on + that explicit request's stream. The PUSH_PROMISE frame also includes + a promised stream identifier, chosen from the stream identifiers + available to the server (see Section 5.1.1). + + The header fields in PUSH_PROMISE and any subsequent CONTINUATION + frames MUST be a valid and complete set of request header fields + (Section 8.3.1). The server MUST include a method in the ":method" + pseudo-header field that is safe and cacheable. If a client receives + a PUSH_PROMISE that does not include a complete and valid set of + header fields or the ":method" pseudo-header field identifies a + method that is not safe, it MUST respond on the promised stream with + a stream error (Section 5.4.2) of type PROTOCOL_ERROR. + + The server SHOULD send PUSH_PROMISE (Section 6.6) frames prior to + sending any frames that reference the promised responses. This + avoids a race where clients issue requests prior to receiving any + PUSH_PROMISE frames. + + For example, if the server receives a request for a document + containing embedded links to multiple image files and the server + chooses to push those additional images to the client, sending + PUSH_PROMISE frames before the DATA frames that contain the image + links ensures that the client is able to see that a resource will be + pushed before discovering embedded links. Similarly, if the server + pushes resources referenced by the field block (for instance, in Link + header fields), sending a PUSH_PROMISE before sending the header + ensures that clients do not request those resources. + + PUSH_PROMISE frames MUST NOT be sent by the client. + + PUSH_PROMISE frames can be sent by the server on any client-initiated + stream, but the stream MUST be in either the "open" or "half-closed + (remote)" state with respect to the server. PUSH_PROMISE frames are + interspersed with the frames that comprise a response, though they + cannot be interspersed with HEADERS and CONTINUATION frames that + comprise a single field block. + + Sending a PUSH_PROMISE frame creates a new stream and puts the stream + into the "reserved (local)" state for the server and the "reserved + (remote)" state for the client. + +8.4.2. Push Responses + + After sending the PUSH_PROMISE frame, the server can begin delivering + the pushed response as a response (Section 8.3.2) on a server- + initiated stream that uses the promised stream identifier. The + server uses this stream to transmit an HTTP response, using the same + sequence of frames as that defined in Section 8.1. This stream + becomes "half-closed" to the client (Section 5.1) after the initial + HEADERS frame is sent. + + Once a client receives a PUSH_PROMISE frame and chooses to accept the + pushed response, the client SHOULD NOT issue any requests for the + promised response until after the promised stream has closed. + + If the client determines, for any reason, that it does not wish to + receive the pushed response from the server or if the server takes + too long to begin sending the promised response, the client can send + a RST_STREAM frame, using either the CANCEL or REFUSED_STREAM code + and referencing the pushed stream's identifier. + + A client can use the SETTINGS_MAX_CONCURRENT_STREAMS setting to limit + the number of responses that can be concurrently pushed by a server. + Advertising a SETTINGS_MAX_CONCURRENT_STREAMS value of zero prevents + the server from opening the streams necessary to push responses. + However, this does not prevent the server from reserving streams + using PUSH_PROMISE frames, because reserved streams do not count + toward the concurrent stream limit. Clients that do not wish to + receive pushed resources need to reset any unwanted reserved streams + or set SETTINGS_ENABLE_PUSH to 0. + + Clients receiving a pushed response MUST validate that either the + server is authoritative (see Section 10.1) or the proxy that provided + the pushed response is configured for the corresponding request. For + example, a server that offers a certificate for only the example.com + DNS-ID (see [RFC6125]) is not permitted to push a response for + . + + The response for a PUSH_PROMISE stream begins with a HEADERS frame, + which immediately puts the stream into the "half-closed (remote)" + state for the server and "half-closed (local)" state for the client, + and ends with a frame with the END_STREAM flag set, which places the + stream in the "closed" state. + + | Note: The client never sends a frame with the END_STREAM flag + | set for a server push. + +8.5. The CONNECT Method + + The CONNECT method (Section 9.3.6 of [HTTP]) is used to convert an + HTTP connection into a tunnel to a remote host. CONNECT is primarily + used with HTTP proxies to establish a TLS session with an origin + server for the purposes of interacting with "https" resources. + + In HTTP/2, the CONNECT method establishes a tunnel over a single + HTTP/2 stream to a remote host, rather than converting the entire + connection to a tunnel. A CONNECT header section is constructed as + defined in Section 8.3.1 ("Request Pseudo-Header Fields"), with a few + differences. Specifically: + + * The ":method" pseudo-header field is set to CONNECT. + + * The ":scheme" and ":path" pseudo-header fields MUST be omitted. + + * The ":authority" pseudo-header field contains the host and port to + connect to (equivalent to the authority-form of the request-target + of CONNECT requests; see Section 3.2.3 of [HTTP/1.1]). + + A CONNECT request that does not conform to these restrictions is + malformed (Section 8.1.1). + + A proxy that supports CONNECT establishes a TCP connection [TCP] to + the host and port identified in the ":authority" pseudo-header field. + Once this connection is successfully established, the proxy sends a + HEADERS frame containing a 2xx-series status code to the client, as + defined in Section 9.3.6 of [HTTP]. + + After the initial HEADERS frame sent by each peer, all subsequent + DATA frames correspond to data sent on the TCP connection. The frame + payload of any DATA frames sent by the client is transmitted by the + proxy to the TCP server; data received from the TCP server is + assembled into DATA frames by the proxy. Frame types other than DATA + or stream management frames (RST_STREAM, WINDOW_UPDATE, and PRIORITY) + MUST NOT be sent on a connected stream and MUST be treated as a + stream error (Section 5.4.2) if received. + + The TCP connection can be closed by either peer. The END_STREAM flag + on a DATA frame is treated as being equivalent to the TCP FIN bit. A + client is expected to send a DATA frame with the END_STREAM flag set + after receiving a frame with the END_STREAM flag set. A proxy that + receives a DATA frame with the END_STREAM flag set sends the attached + data with the FIN bit set on the last TCP segment. A proxy that + receives a TCP segment with the FIN bit set sends a DATA frame with + the END_STREAM flag set. Note that the final TCP segment or DATA + frame could be empty. + + A TCP connection error is signaled with RST_STREAM. A proxy treats + any error in the TCP connection, which includes receiving a TCP + segment with the RST bit set, as a stream error (Section 5.4.2) of + type CONNECT_ERROR. Correspondingly, a proxy MUST send a TCP segment + with the RST bit set if it detects an error with the stream or the + HTTP/2 connection. + +8.6. The Upgrade Header Field + + HTTP/2 does not support the 101 (Switching Protocols) informational + status code (Section 15.2.2 of [HTTP]). + + The semantics of 101 (Switching Protocols) aren't applicable to a + multiplexed protocol. Similar functionality might be enabled through + the use of extended CONNECT [RFC8441], and other protocols are able + to use the same mechanisms that HTTP/2 uses to negotiate their use + (see Section 3). + +8.7. Request Reliability + + In general, an HTTP client is unable to retry a non-idempotent + request when an error occurs because there is no means to determine + the nature of the error (see Section 9.2.2 of [HTTP]). It is + possible that some server processing occurred prior to the error, + which could result in undesirable effects if the request were + reattempted. + + HTTP/2 provides two mechanisms for providing a guarantee to a client + that a request has not been processed: + + * The GOAWAY frame indicates the highest stream number that might + have been processed. Requests on streams with higher numbers are + therefore guaranteed to be safe to retry. + + * The REFUSED_STREAM error code can be included in a RST_STREAM + frame to indicate that the stream is being closed prior to any + processing having occurred. Any request that was sent on the + reset stream can be safely retried. + + Requests that have not been processed have not failed; clients MAY + automatically retry them, even those with non-idempotent methods. + + A server MUST NOT indicate that a stream has not been processed + unless it can guarantee that fact. If frames that are on a stream + are passed to the application layer for any stream, then + REFUSED_STREAM MUST NOT be used for that stream, and a GOAWAY frame + MUST include a stream identifier that is greater than or equal to the + given stream identifier. + + In addition to these mechanisms, the PING frame provides a way for a + client to easily test a connection. Connections that remain idle can + become broken, because some middleboxes (for instance, network + address translators or load balancers) silently discard connection + bindings. The PING frame allows a client to safely test whether a + connection is still active without sending a request. + +8.8. Examples + + This section shows HTTP/1.1 requests and responses, with + illustrations of equivalent HTTP/2 requests and responses. + +8.8.1. Simple Request + + An HTTP GET request includes control data and a request header with + no message content and is therefore transmitted as a single HEADERS + frame, followed by zero or more CONTINUATION frames containing the + serialized block of request header fields. The HEADERS frame in the + following has both the END_HEADERS and END_STREAM flags set; no + CONTINUATION frames are sent. + + GET /resource HTTP/1.1 HEADERS + Host: example.org ==> + END_STREAM + Accept: image/jpeg + END_HEADERS + :method = GET + :scheme = https + :authority = example.org + :path = /resource + host = example.org + accept = image/jpeg + +8.8.2. Simple Response + + Similarly, a response that includes only control data and a response + header is transmitted as a HEADERS frame (again, followed by zero or + more CONTINUATION frames) containing the serialized block of response + header fields. + + HTTP/1.1 304 Not Modified HEADERS + ETag: "xyzzy" ==> + END_STREAM + Expires: Thu, 23 Jan ... + END_HEADERS + :status = 304 + etag = "xyzzy" + expires = Thu, 23 Jan ... + +8.8.3. Complex Request + + An HTTP POST request that includes control data and a request header + with message content is transmitted as one HEADERS frame, followed by + zero or more CONTINUATION frames containing the request header, + followed by one or more DATA frames, with the last CONTINUATION (or + HEADERS) frame having the END_HEADERS flag set and the final DATA + frame having the END_STREAM flag set: + + POST /resource HTTP/1.1 HEADERS + Host: example.org ==> - END_STREAM + Content-Type: image/jpeg - END_HEADERS + Content-Length: 123 :method = POST + :authority = example.org + :path = /resource + {binary data} :scheme = https + + CONTINUATION + + END_HEADERS + content-type = image/jpeg + host = example.org + content-length = 123 + + DATA + + END_STREAM + {binary data} + + Note that data contributing to any given field line could be spread + between field block fragments. The allocation of field lines to + frames in this example is illustrative only. + +8.8.4. Response with Body + + A response that includes control data and a response header with + message content is transmitted as a HEADERS frame, followed by zero + or more CONTINUATION frames, followed by one or more DATA frames, + with the last DATA frame in the sequence having the END_STREAM flag + set: + + HTTP/1.1 200 OK HEADERS + Content-Type: image/jpeg ==> - END_STREAM + Content-Length: 123 + END_HEADERS + :status = 200 + {binary data} content-type = image/jpeg + content-length = 123 + + DATA + + END_STREAM + {binary data} + +8.8.5. Informational Responses + + An informational response using a 1xx status code other than 101 is + transmitted as a HEADERS frame, followed by zero or more CONTINUATION + frames. + + A trailer section is sent as a field block after both the request or + response field block and all the DATA frames have been sent. The + HEADERS frame starting the field block that comprises the trailer + section has the END_STREAM flag set. + + The following example includes both a 100 (Continue) status code, + which is sent in response to a request containing a "100-continue" + token in the Expect header field, and a trailer section: + + HTTP/1.1 100 Continue HEADERS + Extension-Field: bar ==> - END_STREAM + + END_HEADERS + :status = 100 + extension-field = bar + + HTTP/1.1 200 OK HEADERS + Content-Type: image/jpeg ==> - END_STREAM + Transfer-Encoding: chunked + END_HEADERS + Trailer: Foo :status = 200 + content-type = image/jpeg + 123 trailer = Foo + {binary data} + 0 DATA + Foo: bar - END_STREAM + {binary data} + + HEADERS + + END_STREAM + + END_HEADERS + foo = bar + +9. HTTP/2 Connections + + This section outlines attributes of HTTP that improve + interoperability, reduce exposure to known security vulnerabilities, + or reduce the potential for implementation variation. + +9.1. Connection Management + + HTTP/2 connections are persistent. For best performance, it is + expected that clients will not close connections until it is + determined that no further communication with a server is necessary + (for example, when a user navigates away from a particular web page) + or until the server closes the connection. + + Clients SHOULD NOT open more than one HTTP/2 connection to a given + host and port pair, where the host is derived from a URI, a selected + alternative service [ALT-SVC], or a configured proxy. + + A client can create additional connections as replacements, either to + replace connections that are near to exhausting the available stream + identifier space (Section 5.1.1), to refresh the keying material for + a TLS connection, or to replace connections that have encountered + errors (Section 5.4.1). + + A client MAY open multiple connections to the same IP address and TCP + port using different Server Name Indication [TLS-EXT] values or to + provide different TLS client certificates but SHOULD avoid creating + multiple connections with the same configuration. + + Servers are encouraged to maintain open connections for as long as + possible but are permitted to terminate idle connections if + necessary. When either endpoint chooses to close the transport-layer + TCP connection, the terminating endpoint SHOULD first send a GOAWAY + (Section 6.8) frame so that both endpoints can reliably determine + whether previously sent frames have been processed and gracefully + complete or terminate any necessary remaining tasks. + +9.1.1. Connection Reuse + + Connections that are made to an origin server, either directly or + through a tunnel created using the CONNECT method (Section 8.5), MAY + be reused for requests with multiple different URI authority + components. A connection can be reused as long as the origin server + is authoritative (Section 10.1). For TCP connections without TLS, + this depends on the host having resolved to the same IP address. + + For "https" resources, connection reuse additionally depends on + having a certificate that is valid for the host in the URI. The + certificate presented by the server MUST satisfy any checks that the + client would perform when forming a new TLS connection for the host + in the URI. A single certificate can be used to establish authority + for multiple origins. Section 4.3 of [HTTP] describes how a client + determines whether a server is authoritative for a URI. + + In some deployments, reusing a connection for multiple origins can + result in requests being directed to the wrong origin server. For + example, TLS termination might be performed by a middlebox that uses + the TLS Server Name Indication [TLS-EXT] extension to select an + origin server. This means that it is possible for clients to send + requests to servers that might not be the intended target for the + request, even though the server is otherwise authoritative. + + A server that does not wish clients to reuse connections can indicate + that it is not authoritative for a request by sending a 421 + (Misdirected Request) status code in response to the request (see + Section 15.5.20 of [HTTP]). + + A client that is configured to use a proxy over HTTP/2 directs + requests to that proxy through a single connection. That is, all + requests sent via a proxy reuse the connection to the proxy. + +9.2. Use of TLS Features + + Implementations of HTTP/2 MUST use TLS version 1.2 [TLS12] or higher + for HTTP/2 over TLS. The general TLS usage guidance in [TLSBCP] + SHOULD be followed, with some additional restrictions that are + specific to HTTP/2. + + The TLS implementation MUST support the Server Name Indication (SNI) + [TLS-EXT] extension to TLS. If the server is identified by a domain + name [DNS-TERMS], clients MUST send the server_name TLS extension + unless an alternative mechanism to indicate the target host is used. + + Requirements for deployments of HTTP/2 that negotiate TLS 1.3 [TLS13] + are included in Section 9.2.3. Deployments of TLS 1.2 are subject to + the requirements in Sections 9.2.1 and 9.2.2. Implementations are + encouraged to provide defaults that comply, but it is recognized that + deployments are ultimately responsible for compliance. + +9.2.1. TLS 1.2 Features + + This section describes restrictions on the TLS 1.2 feature set that + can be used with HTTP/2. Due to deployment limitations, it might not + be possible to fail TLS negotiation when these restrictions are not + met. An endpoint MAY immediately terminate an HTTP/2 connection that + does not meet these TLS requirements with a connection error + (Section 5.4.1) of type INADEQUATE_SECURITY. + + A deployment of HTTP/2 over TLS 1.2 MUST disable compression. TLS + compression can lead to the exposure of information that would not + otherwise be revealed [RFC3749]. Generic compression is unnecessary, + since HTTP/2 provides compression features that are more aware of + context and therefore likely to be more appropriate for use for + performance, security, or other reasons. + + A deployment of HTTP/2 over TLS 1.2 MUST disable renegotiation. An + endpoint MUST treat a TLS renegotiation as a connection error + (Section 5.4.1) of type PROTOCOL_ERROR. Note that disabling + renegotiation can result in long-lived connections becoming unusable + due to limits on the number of messages the underlying cipher suite + can encipher. + + An endpoint MAY use renegotiation to provide confidentiality + protection for client credentials offered in the handshake, but any + renegotiation MUST occur prior to sending the connection preface. A + server SHOULD request a client certificate if it sees a renegotiation + request immediately after establishing a connection. + + This effectively prevents the use of renegotiation in response to a + request for a specific protected resource. A future specification + might provide a way to support this use case. Alternatively, a + server might use an error (Section 5.4) of type HTTP_1_1_REQUIRED to + request that the client use a protocol that supports renegotiation. + + Implementations MUST support ephemeral key exchange sizes of at least + 2048 bits for cipher suites that use ephemeral finite field Diffie- + Hellman (DHE) (Section 8.1.2 of [TLS12]) and 224 bits for cipher + suites that use ephemeral elliptic curve Diffie-Hellman (ECDHE) + [RFC8422]. Clients MUST accept DHE sizes of up to 4096 bits. + Endpoints MAY treat negotiation of key sizes smaller than the lower + limits as a connection error (Section 5.4.1) of type + INADEQUATE_SECURITY. + +9.2.2. TLS 1.2 Cipher Suites + + A deployment of HTTP/2 over TLS 1.2 SHOULD NOT use any of the + prohibited cipher suites listed in Appendix A. + + Endpoints MAY choose to generate a connection error (Section 5.4.1) + of type INADEQUATE_SECURITY if one of the prohibited cipher suites is + negotiated. A deployment that chooses to use a prohibited cipher + suite risks triggering a connection error unless the set of potential + peers is known to accept that cipher suite. + + Implementations MUST NOT generate this error in reaction to the + negotiation of a cipher suite that is not prohibited. Consequently, + when clients offer a cipher suite that is not prohibited, they have + to be prepared to use that cipher suite with HTTP/2. + + The list of prohibited cipher suites includes the cipher suite that + TLS 1.2 makes mandatory, which means that TLS 1.2 deployments could + have non-intersecting sets of permitted cipher suites. To avoid this + problem, which causes TLS handshake failures, deployments of HTTP/2 + that use TLS 1.2 MUST support TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 + [TLS-ECDHE] with the P-256 elliptic curve [RFC8422]. + + Note that clients might advertise support of cipher suites that are + prohibited in order to allow for connection to servers that do not + support HTTP/2. This allows servers to select HTTP/1.1 with a cipher + suite that is prohibited in HTTP/2. However, this can result in + HTTP/2 being negotiated with a prohibited cipher suite if the + application protocol and cipher suite are independently selected. + +9.2.3. TLS 1.3 Features + + TLS 1.3 includes a number of features not available in earlier + versions. This section discusses the use of these features. + + HTTP/2 servers MUST NOT send post-handshake TLS 1.3 + CertificateRequest messages. HTTP/2 clients MUST treat a TLS post- + handshake CertificateRequest message as a connection error + (Section 5.4.1) of type PROTOCOL_ERROR. + + The prohibition on post-handshake authentication applies even if the + client offered the "post_handshake_auth" TLS extension. Post- + handshake authentication support might be advertised independently of + ALPN [TLS-ALPN]. Clients might offer the capability for use in other + protocols, but inclusion of the extension cannot imply support within + HTTP/2. + + [TLS13] defines other post-handshake messages, NewSessionTicket and + KeyUpdate, which can be used as they have no direct interaction with + HTTP/2. Unless the use of a new type of TLS message depends on an + interaction with the application-layer protocol, that TLS message can + be sent after the handshake completes. + + TLS early data MAY be used to send requests, provided that the + guidance in [RFC8470] is observed. Clients send requests in early + data assuming initial values for all server settings. + +10. Security Considerations + + The use of TLS is necessary to provide many of the security + properties of this protocol. Many of the claims in this section do + not hold unless TLS is used as described in Section 9.2. + +10.1. Server Authority + + HTTP/2 relies on the HTTP definition of authority for determining + whether a server is authoritative in providing a given response (see + Section 4.3 of [HTTP]). This relies on local name resolution for the + "http" URI scheme and the authenticated server identity for the + "https" scheme. + +10.2. Cross-Protocol Attacks + + In a cross-protocol attack, an attacker causes a client to initiate a + transaction in one protocol toward a server that understands a + different protocol. An attacker might be able to cause the + transaction to appear as a valid transaction in the second protocol. + In combination with the capabilities of the web context, this can be + used to interact with poorly protected servers in private networks. + + Completing a TLS handshake with an ALPN identifier for HTTP/2 can be + considered sufficient protection against cross-protocol attacks. + ALPN provides a positive indication that a server is willing to + proceed with HTTP/2, which prevents attacks on other TLS-based + protocols. + + The encryption in TLS makes it difficult for attackers to control the + data that could be used in a cross-protocol attack on a cleartext + protocol. + + The cleartext version of HTTP/2 has minimal protection against cross- + protocol attacks. The connection preface (Section 3.4) contains a + string that is designed to confuse HTTP/1.1 servers, but no special + protection is offered for other protocols. + +10.3. Intermediary Encapsulation Attacks + + HPACK permits encoding of field names and values that might be + treated as delimiters in other HTTP versions. An intermediary that + translates an HTTP/2 request or response MUST validate fields + according to the rules in Section 8.2 before translating a message to + another HTTP version. Translating a field that includes invalid + delimiters could be used to cause recipients to incorrectly interpret + a message, which could be exploited by an attacker. + + Section 8.2 does not include specific rules for validation of pseudo- + header fields. If the values of these fields are used, additional + validation is necessary. This is particularly important where + ":scheme", ":authority", and ":path" are combined to form a single + URI string [RFC3986]. Similar problems might occur when that URI or + just ":path" is combined with ":method" to construct a request line + (as in Section 3 of [HTTP/1.1]). Simple concatenation is not secure + unless the input values are fully validated. + + An intermediary can reject fields that contain invalid field names or + values for other reasons -- in particular, those fields that do not + conform to the HTTP ABNF grammar from Section 5 of [HTTP]. + Intermediaries that do not perform any validation of fields other + than the minimum required by Section 8.2 could forward messages that + contain invalid field names or values. + + An intermediary that receives any fields that require removal before + forwarding (see Section 7.6.1 of [HTTP]) MUST remove or replace those + header fields when forwarding messages. Additionally, intermediaries + should take care when forwarding messages containing Content-Length + fields to ensure that the message is well-formed (Section 8.1.1). + This ensures that if the message is translated into HTTP/1.1 at any + point, the framing will be correct. + +10.4. Cacheability of Pushed Responses + + Pushed responses do not have an explicit request from the client; the + request is provided by the server in the PUSH_PROMISE frame. + + Caching responses that are pushed is possible based on the guidance + provided by the origin server in the Cache-Control header field. + However, this can cause issues if a single server hosts more than one + tenant. For example, a server might offer multiple users each a + small portion of its URI space. + + Where multiple tenants share space on the same server, that server + MUST ensure that tenants are not able to push representations of + resources that they do not have authority over. Failure to enforce + this would allow a tenant to provide a representation that would be + served out of cache, overriding the actual representation that the + authoritative tenant provides. + + Pushed responses for which an origin server is not authoritative (see + Section 10.1) MUST NOT be used or cached. + +10.5. Denial-of-Service Considerations + + An HTTP/2 connection can demand a greater commitment of resources to + operate than an HTTP/1.1 connection. Both field section compression + and flow control depend on a commitment of a greater amount of state. + Settings for these features ensure that memory commitments for these + features are strictly bounded. + + The number of PUSH_PROMISE frames is not constrained in the same + fashion. A client that accepts server push SHOULD limit the number + of streams it allows to be in the "reserved (remote)" state. An + excessive number of server push streams can be treated as a stream + error (Section 5.4.2) of type ENHANCE_YOUR_CALM. + + A number of HTTP/2 implementations were found to be vulnerable to + denial of service [NFLX-2019-002]. Below is a list of known ways + that implementations might be subject to denial-of-service attacks: + + * Inefficient tracking of outstanding outbound frames can lead to + overload if an adversary can cause large numbers of frames to be + enqueued for sending. A peer could use one of several techniques + to cause large numbers of frames to be generated: + + - Providing tiny increments to flow control in WINDOW_UPDATE + frames can cause a sender to generate a large number of DATA + frames. + + - An endpoint is required to respond to a PING frame. + + - Each SETTINGS frame requires acknowledgment. + + - An invalid request (or server push) can cause a peer to send + RST_STREAM frames in response. + + * An attacker can provide large amounts of flow-control credit at + the HTTP/2 layer but withhold credit at the TCP layer, preventing + frames from being sent. An endpoint that constructs and remembers + frames for sending without considering TCP limits might be subject + to resource exhaustion. + + * Large numbers of small or empty frames can be abused to cause a + peer to expend time processing frame headers. Caution is required + here as some uses of small frames are entirely legitimate, such as + the sending of an empty DATA or CONTINUATION frame at the end of a + stream. + + * The SETTINGS frame might also be abused to cause a peer to expend + additional processing time. This might be done by pointlessly + changing settings, sending multiple undefined settings, or + changing the same setting multiple times in the same frame. + + * Handling reprioritization with PRIORITY frames can require + significant processing time and can lead to overload if many + PRIORITY frames are sent. + + * Field section compression also provides opportunities for an + attacker to waste processing resources; see Section 7 of + [COMPRESSION] for more details on potential abuses. + + * Limits in SETTINGS cannot be reduced instantaneously, which leaves + an endpoint exposed to behavior from a peer that could exceed the + new limits. In particular, immediately after establishing a + connection, limits set by a server are not known to clients and + could be exceeded without being an obvious protocol violation. + + Most of the features that might be exploited for denial of service -- + such as SETTINGS changes, small frames, field section compression -- + have legitimate uses. These features become a burden only when they + are used unnecessarily or to excess. + + An endpoint that doesn't monitor use of these features exposes itself + to a risk of denial of service. Implementations SHOULD track the use + of these features and set limits on their use. An endpoint MAY treat + activity that is suspicious as a connection error (Section 5.4.1) of + type ENHANCE_YOUR_CALM. + +10.5.1. Limits on Field Block Size + + A large field block (Section 4.3) can cause an implementation to + commit a large amount of state. Field lines that are critical for + routing can appear toward the end of a field block, which prevents + streaming of fields to their ultimate destination. This ordering and + other reasons, such as ensuring cache correctness, mean that an + endpoint might need to buffer the entire field block. Since there is + no hard limit to the size of a field block, some endpoints could be + forced to commit a large amount of available memory for field blocks. + + An endpoint can use the SETTINGS_MAX_HEADER_LIST_SIZE to advise peers + of limits that might apply on the size of uncompressed field blocks. + This setting is only advisory, so endpoints MAY choose to send field + blocks that exceed this limit and risk the request or response being + treated as malformed. This setting is specific to a connection, so + any request or response could encounter a hop with a lower, unknown + limit. An intermediary can attempt to avoid this problem by passing + on values presented by different peers, but they are not obliged to + do so. + + A server that receives a larger field block than it is willing to + handle can send an HTTP 431 (Request Header Fields Too Large) status + code [RFC6585]. A client can discard responses that it cannot + process. The field block MUST be processed to ensure a consistent + connection state, unless the connection is closed. + +10.5.2. CONNECT Issues + + The CONNECT method can be used to create disproportionate load on a + proxy, since stream creation is relatively inexpensive when compared + to the creation and maintenance of a TCP connection. A proxy might + also maintain some resources for a TCP connection beyond the closing + of the stream that carries the CONNECT request, since the outgoing + TCP connection remains in the TIME_WAIT state. Therefore, a proxy + cannot rely on SETTINGS_MAX_CONCURRENT_STREAMS alone to limit the + resources consumed by CONNECT requests. + +10.6. Use of Compression + + Compression can allow an attacker to recover secret data when it is + compressed in the same context as data under attacker control. + HTTP/2 enables compression of field lines (Section 4.3); the + following concerns also apply to the use of HTTP compressed content- + codings (Section 8.4.1 of [HTTP]). + + There are demonstrable attacks on compression that exploit the + characteristics of the Web (e.g., [BREACH]). The attacker induces + multiple requests containing varying plaintext, observing the length + of the resulting ciphertext in each, which reveals a shorter length + when a guess about the secret is correct. + + Implementations communicating on a secure channel MUST NOT compress + content that includes both confidential and attacker-controlled data + unless separate compression dictionaries are used for each source of + data. Compression MUST NOT be used if the source of data cannot be + reliably determined. Generic stream compression, such as that + provided by TLS, MUST NOT be used with HTTP/2 (see Section 9.2). + + Further considerations regarding the compression of header fields are + described in [COMPRESSION]. + +10.7. Use of Padding + + Padding within HTTP/2 is not intended as a replacement for general + purpose padding, such as that provided by TLS [TLS13]. Redundant + padding could even be counterproductive. Correct application can + depend on having specific knowledge of the data that is being padded. + + To mitigate attacks that rely on compression, disabling or limiting + compression might be preferable to padding as a countermeasure. + + Padding can be used to obscure the exact size of frame content and is + provided to mitigate specific attacks within HTTP -- for example, + attacks where compressed content includes both attacker-controlled + plaintext and secret data (e.g., [BREACH]). + + Use of padding can result in less protection than might seem + immediately obvious. At best, padding only makes it more difficult + for an attacker to infer length information by increasing the number + of frames an attacker has to observe. Incorrectly implemented + padding schemes can be easily defeated. In particular, randomized + padding with a predictable distribution provides very little + protection; similarly, padding frame payloads to a fixed size exposes + information as frame payload sizes cross the fixed-sized boundary, + which could be possible if an attacker can control plaintext. + + Intermediaries SHOULD retain padding for DATA frames but MAY drop + padding for HEADERS and PUSH_PROMISE frames. A valid reason for an + intermediary to change the amount of padding of frames is to improve + the protections that padding provides. + +10.8. Privacy Considerations + + Several characteristics of HTTP/2 provide an observer an opportunity + to correlate actions of a single client or server over time. These + include the values of settings, the manner in which flow-control + windows are managed, the way priorities are allocated to streams, the + timing of reactions to stimulus, and the handling of any features + that are controlled by settings. + + As far as these create observable differences in behavior, they could + be used as a basis for fingerprinting a specific client, as defined + in Section 3.2 of [PRIVACY]. + + HTTP/2's preference for using a single TCP connection allows + correlation of a user's activity on a site. Reusing connections for + different origins allows tracking across those origins. + + Because the PING and SETTINGS frames solicit immediate responses, + they can be used by an endpoint to measure latency to their peer. + This might have privacy implications in certain scenarios. + +10.9. Remote Timing Attacks + + Remote timing attacks extract secrets from servers by observing + variations in the time that servers take when processing requests + that use secrets. HTTP/2 enables concurrent request creation and + processing, which can give attackers better control over when request + processing commences. Multiple HTTP/2 requests can be included in + the same IP packet or TLS record. HTTP/2 can therefore make remote + timing attacks more efficient by eliminating variability in request + delivery, leaving only request order and the delivery of responses as + sources of timing variability. + + Ensuring that processing time is not dependent on the value of a + secret is the best defense against any form of timing attack. + +11. IANA Considerations + + This revision of HTTP/2 marks the HTTP2-Settings header field and the + h2c upgrade token, both defined in [RFC7540], as obsolete. + + Section 11 of [RFC7540] registered the h2 and h2c ALPN identifiers + along with the PRI HTTP method. RFC 7540 also established a registry + for frame types, settings, and error codes. These registrations and + registries apply to HTTP/2, but are not redefined in this document. + + IANA has updated references to RFC 7540 in the following registries + to refer to this document: "TLS Application-Layer Protocol + Negotiation (ALPN) Protocol IDs", "HTTP/2 Frame Type", "HTTP/2 + Settings", "HTTP/2 Error Code", and "HTTP Method Registry". The + registration of the PRI method has been updated to refer to + Section 3.4; all other section numbers have not changed. + + IANA has changed the policy on those portions of the "HTTP/2 Frame + Type" and "HTTP/2 Settings" registries that were reserved for + Experimental Use in RFC 7540. These portions of the registries shall + operate on the same policy as the remainder of each registry. + +11.1. HTTP2-Settings Header Field Registration + + This section marks the HTTP2-Settings header field registered by + Section 11.5 of [RFC7540] in the "Hypertext Transfer Protocol (HTTP) + Field Name Registry" as obsolete. This capability has been removed: + see Section 3.1. The registration is updated to include the details + as required by Section 18.4 of [HTTP]: + + Field Name: HTTP2-Settings + + Status: obsoleted + + Reference: Section 3.2.1 of [RFC7540] + + Comments: Obsolete; see Section 11.1 of this document. + +11.2. The h2c Upgrade Token + + This section records the h2c upgrade token registered by Section 11.8 + of [RFC7540] in the "Hypertext Transfer Protocol (HTTP) Upgrade Token + Registry" as obsolete. This capability has been removed: see + Section 3.1. The registration is updated as follows: + + Value: h2c + + Description: (OBSOLETE) Hypertext Transfer Protocol version 2 + (HTTP/2) + + Expected Version Tokens: None + + Reference: Section 3.1 of this document + +12. References + +12.1. Normative References + + [CACHING] Fielding, R., Ed., Nottingham, M., Ed., and J. Reschke, + Ed., "HTTP Caching", STD 98, RFC 9111, + DOI 10.17487/RFC9111, June 2022, + . + + [COMPRESSION] + Peon, R. and H. Ruellan, "HPACK: Header Compression for + HTTP/2", RFC 7541, DOI 10.17487/RFC7541, May 2015, + . + + [COOKIE] Barth, A., "HTTP State Management Mechanism", RFC 6265, + DOI 10.17487/RFC6265, April 2011, + . + + [HTTP] Fielding, R., Ed., Nottingham, M., Ed., and J. Reschke, + Ed., "HTTP Semantics", STD 97, RFC 9110, + DOI 10.17487/RFC9110, June 2022, + . + + [QUIC] Iyengar, J., Ed. and M. Thomson, Ed., "QUIC: A UDP-Based + Multiplexed and Secure Transport", RFC 9000, + DOI 10.17487/RFC9000, May 2021, + . + + [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate + Requirement Levels", BCP 14, RFC 2119, + DOI 10.17487/RFC2119, March 1997, + . + + [RFC3986] Berners-Lee, T., Fielding, R., and L. Masinter, "Uniform + Resource Identifier (URI): Generic Syntax", STD 66, + RFC 3986, DOI 10.17487/RFC3986, January 2005, + . + + [RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC + 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, + May 2017, . + + [RFC8422] Nir, Y., Josefsson, S., and M. Pegourie-Gonnard, "Elliptic + Curve Cryptography (ECC) Cipher Suites for Transport Layer + Security (TLS) Versions 1.2 and Earlier", RFC 8422, + DOI 10.17487/RFC8422, August 2018, + . + + [RFC8470] Thomson, M., Nottingham, M., and W. Tarreau, "Using Early + Data in HTTP", RFC 8470, DOI 10.17487/RFC8470, September + 2018, . + + [TCP] Postel, J., "Transmission Control Protocol", STD 7, + RFC 793, DOI 10.17487/RFC0793, September 1981, + . + + [TLS-ALPN] Friedl, S., Popov, A., Langley, A., and E. Stephan, + "Transport Layer Security (TLS) Application-Layer Protocol + Negotiation Extension", RFC 7301, DOI 10.17487/RFC7301, + July 2014, . + + [TLS-ECDHE] + Rescorla, E., "TLS Elliptic Curve Cipher Suites with SHA- + 256/384 and AES Galois Counter Mode (GCM)", RFC 5289, + DOI 10.17487/RFC5289, August 2008, + . + + [TLS-EXT] Eastlake 3rd, D., "Transport Layer Security (TLS) + Extensions: Extension Definitions", RFC 6066, + DOI 10.17487/RFC6066, January 2011, + . + + [TLS12] Dierks, T. and E. Rescorla, "The Transport Layer Security + (TLS) Protocol Version 1.2", RFC 5246, + DOI 10.17487/RFC5246, August 2008, + . + + [TLS13] Rescorla, E., "The Transport Layer Security (TLS) Protocol + Version 1.3", RFC 8446, DOI 10.17487/RFC8446, August 2018, + . + + [TLSBCP] Sheffer, Y., Holz, R., and P. Saint-Andre, + "Recommendations for Secure Use of Transport Layer + Security (TLS) and Datagram Transport Layer Security + (DTLS)", BCP 195, RFC 7525, DOI 10.17487/RFC7525, May + 2015, . + +12.2. Informative References + + [ALT-SVC] Nottingham, M., McManus, P., and J. Reschke, "HTTP + Alternative Services", RFC 7838, DOI 10.17487/RFC7838, + April 2016, . + + [BREACH] Gluck, Y., Harris, N., and A. Prado, "BREACH: Reviving the + CRIME Attack", 12 July 2013, + . + + [DNS-TERMS] + Hoffman, P., Sullivan, A., and K. Fujiwara, "DNS + Terminology", BCP 219, RFC 8499, DOI 10.17487/RFC8499, + January 2019, . + + [HTTP-PRIORITY] + Oku, K. and L. Pardue, "Extensible Prioritization Scheme + for HTTP", RFC 9218, DOI 10.17487/RFC9218, June 2022, + . + + [HTTP/1.1] Fielding, R., Ed., Nottingham, M., Ed., and J. Reschke, + Ed., "HTTP/1.1", STD 99, RFC 9112, DOI 10.17487/RFC9112, + June 2022, . + + [NFLX-2019-002] + Netflix, "HTTP/2 Denial of Service Advisory", 13 August + 2019, . + + [PRIVACY] Cooper, A., Tschofenig, H., Aboba, B., Peterson, J., + Morris, J., Hansen, M., and R. Smith, "Privacy + Considerations for Internet Protocols", RFC 6973, + DOI 10.17487/RFC6973, July 2013, + . + + [RFC1122] Braden, R., Ed., "Requirements for Internet Hosts - + Communication Layers", STD 3, RFC 1122, + DOI 10.17487/RFC1122, October 1989, + . + + [RFC3749] Hollenbeck, S., "Transport Layer Security Protocol + Compression Methods", RFC 3749, DOI 10.17487/RFC3749, May + 2004, . + + [RFC6125] Saint-Andre, P. and J. Hodges, "Representation and + Verification of Domain-Based Application Service Identity + within Internet Public Key Infrastructure Using X.509 + (PKIX) Certificates in the Context of Transport Layer + Security (TLS)", RFC 6125, DOI 10.17487/RFC6125, March + 2011, . + + [RFC6585] Nottingham, M. and R. Fielding, "Additional HTTP Status + Codes", RFC 6585, DOI 10.17487/RFC6585, April 2012, + . + + [RFC7323] Borman, D., Braden, B., Jacobson, V., and R. + Scheffenegger, Ed., "TCP Extensions for High Performance", + RFC 7323, DOI 10.17487/RFC7323, September 2014, + . + + [RFC7540] Belshe, M., Peon, R., and M. Thomson, Ed., "Hypertext + Transfer Protocol Version 2 (HTTP/2)", RFC 7540, + DOI 10.17487/RFC7540, May 2015, + . + + [RFC8441] McManus, P., "Bootstrapping WebSockets with HTTP/2", + RFC 8441, DOI 10.17487/RFC8441, September 2018, + . + + [RFC8740] Benjamin, D., "Using TLS 1.3 with HTTP/2", RFC 8740, + DOI 10.17487/RFC8740, February 2020, + . + + [TALKING] Huang, L., Chen, E., Barth, A., Rescorla, E., and C. + Jackson, "Talking to Yourself for Fun and Profit", 2011, + . + +Appendix A. Prohibited TLS 1.2 Cipher Suites + + An HTTP/2 implementation MAY treat the negotiation of any of the + following cipher suites with TLS 1.2 as a connection error + (Section 5.4.1) of type INADEQUATE_SECURITY: + + * TLS_NULL_WITH_NULL_NULL + * TLS_RSA_WITH_NULL_MD5 + * TLS_RSA_WITH_NULL_SHA + * TLS_RSA_EXPORT_WITH_RC4_40_MD5 + * TLS_RSA_WITH_RC4_128_MD5 + * TLS_RSA_WITH_RC4_128_SHA + * TLS_RSA_EXPORT_WITH_RC2_CBC_40_MD5 + * TLS_RSA_WITH_IDEA_CBC_SHA + * TLS_RSA_EXPORT_WITH_DES40_CBC_SHA + * TLS_RSA_WITH_DES_CBC_SHA + * TLS_RSA_WITH_3DES_EDE_CBC_SHA + * TLS_DH_DSS_EXPORT_WITH_DES40_CBC_SHA + * TLS_DH_DSS_WITH_DES_CBC_SHA + * TLS_DH_DSS_WITH_3DES_EDE_CBC_SHA + * TLS_DH_RSA_EXPORT_WITH_DES40_CBC_SHA + * TLS_DH_RSA_WITH_DES_CBC_SHA + * TLS_DH_RSA_WITH_3DES_EDE_CBC_SHA + * TLS_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA + * TLS_DHE_DSS_WITH_DES_CBC_SHA + * TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA + * TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA + * TLS_DHE_RSA_WITH_DES_CBC_SHA + * TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA + * TLS_DH_anon_EXPORT_WITH_RC4_40_MD5 + * TLS_DH_anon_WITH_RC4_128_MD5 + * TLS_DH_anon_EXPORT_WITH_DES40_CBC_SHA + * TLS_DH_anon_WITH_DES_CBC_SHA + * TLS_DH_anon_WITH_3DES_EDE_CBC_SHA + * TLS_KRB5_WITH_DES_CBC_SHA + * TLS_KRB5_WITH_3DES_EDE_CBC_SHA + * TLS_KRB5_WITH_RC4_128_SHA + * TLS_KRB5_WITH_IDEA_CBC_SHA + * TLS_KRB5_WITH_DES_CBC_MD5 + * TLS_KRB5_WITH_3DES_EDE_CBC_MD5 + * TLS_KRB5_WITH_RC4_128_MD5 + * TLS_KRB5_WITH_IDEA_CBC_MD5 + * TLS_KRB5_EXPORT_WITH_DES_CBC_40_SHA + * TLS_KRB5_EXPORT_WITH_RC2_CBC_40_SHA + * TLS_KRB5_EXPORT_WITH_RC4_40_SHA + * TLS_KRB5_EXPORT_WITH_DES_CBC_40_MD5 + * TLS_KRB5_EXPORT_WITH_RC2_CBC_40_MD5 + * TLS_KRB5_EXPORT_WITH_RC4_40_MD5 + * TLS_PSK_WITH_NULL_SHA + * TLS_DHE_PSK_WITH_NULL_SHA + * TLS_RSA_PSK_WITH_NULL_SHA + * TLS_RSA_WITH_AES_128_CBC_SHA + * TLS_DH_DSS_WITH_AES_128_CBC_SHA + * TLS_DH_RSA_WITH_AES_128_CBC_SHA + * TLS_DHE_DSS_WITH_AES_128_CBC_SHA + * TLS_DHE_RSA_WITH_AES_128_CBC_SHA + * TLS_DH_anon_WITH_AES_128_CBC_SHA + * TLS_RSA_WITH_AES_256_CBC_SHA + * TLS_DH_DSS_WITH_AES_256_CBC_SHA + * TLS_DH_RSA_WITH_AES_256_CBC_SHA + * TLS_DHE_DSS_WITH_AES_256_CBC_SHA + * TLS_DHE_RSA_WITH_AES_256_CBC_SHA + * TLS_DH_anon_WITH_AES_256_CBC_SHA + * TLS_RSA_WITH_NULL_SHA256 + * TLS_RSA_WITH_AES_128_CBC_SHA256 + * TLS_RSA_WITH_AES_256_CBC_SHA256 + * TLS_DH_DSS_WITH_AES_128_CBC_SHA256 + * TLS_DH_RSA_WITH_AES_128_CBC_SHA256 + * TLS_DHE_DSS_WITH_AES_128_CBC_SHA256 + * TLS_RSA_WITH_CAMELLIA_128_CBC_SHA + * TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA + * TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA + * TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA + * TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA + * TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA + * TLS_DHE_RSA_WITH_AES_128_CBC_SHA256 + * TLS_DH_DSS_WITH_AES_256_CBC_SHA256 + * TLS_DH_RSA_WITH_AES_256_CBC_SHA256 + * TLS_DHE_DSS_WITH_AES_256_CBC_SHA256 + * TLS_DHE_RSA_WITH_AES_256_CBC_SHA256 + * TLS_DH_anon_WITH_AES_128_CBC_SHA256 + * TLS_DH_anon_WITH_AES_256_CBC_SHA256 + * TLS_RSA_WITH_CAMELLIA_256_CBC_SHA + * TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA + * TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA + * TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA + * TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA + * TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA + * TLS_PSK_WITH_RC4_128_SHA + * TLS_PSK_WITH_3DES_EDE_CBC_SHA + * TLS_PSK_WITH_AES_128_CBC_SHA + * TLS_PSK_WITH_AES_256_CBC_SHA + * TLS_DHE_PSK_WITH_RC4_128_SHA + * TLS_DHE_PSK_WITH_3DES_EDE_CBC_SHA + * TLS_DHE_PSK_WITH_AES_128_CBC_SHA + * TLS_DHE_PSK_WITH_AES_256_CBC_SHA + * TLS_RSA_PSK_WITH_RC4_128_SHA + * TLS_RSA_PSK_WITH_3DES_EDE_CBC_SHA + * TLS_RSA_PSK_WITH_AES_128_CBC_SHA + * TLS_RSA_PSK_WITH_AES_256_CBC_SHA + * TLS_RSA_WITH_SEED_CBC_SHA + * TLS_DH_DSS_WITH_SEED_CBC_SHA + * TLS_DH_RSA_WITH_SEED_CBC_SHA + * TLS_DHE_DSS_WITH_SEED_CBC_SHA + * TLS_DHE_RSA_WITH_SEED_CBC_SHA + * TLS_DH_anon_WITH_SEED_CBC_SHA + * TLS_RSA_WITH_AES_128_GCM_SHA256 + * TLS_RSA_WITH_AES_256_GCM_SHA384 + * TLS_DH_RSA_WITH_AES_128_GCM_SHA256 + * TLS_DH_RSA_WITH_AES_256_GCM_SHA384 + * TLS_DH_DSS_WITH_AES_128_GCM_SHA256 + * TLS_DH_DSS_WITH_AES_256_GCM_SHA384 + * TLS_DH_anon_WITH_AES_128_GCM_SHA256 + * TLS_DH_anon_WITH_AES_256_GCM_SHA384 + * TLS_PSK_WITH_AES_128_GCM_SHA256 + * TLS_PSK_WITH_AES_256_GCM_SHA384 + * TLS_RSA_PSK_WITH_AES_128_GCM_SHA256 + * TLS_RSA_PSK_WITH_AES_256_GCM_SHA384 + * TLS_PSK_WITH_AES_128_CBC_SHA256 + * TLS_PSK_WITH_AES_256_CBC_SHA384 + * TLS_PSK_WITH_NULL_SHA256 + * TLS_PSK_WITH_NULL_SHA384 + * TLS_DHE_PSK_WITH_AES_128_CBC_SHA256 + * TLS_DHE_PSK_WITH_AES_256_CBC_SHA384 + * TLS_DHE_PSK_WITH_NULL_SHA256 + * TLS_DHE_PSK_WITH_NULL_SHA384 + * TLS_RSA_PSK_WITH_AES_128_CBC_SHA256 + * TLS_RSA_PSK_WITH_AES_256_CBC_SHA384 + * TLS_RSA_PSK_WITH_NULL_SHA256 + * TLS_RSA_PSK_WITH_NULL_SHA384 + * TLS_RSA_WITH_CAMELLIA_128_CBC_SHA256 + * TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA256 + * TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA256 + * TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA256 + * TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA256 + * TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA256 + * TLS_RSA_WITH_CAMELLIA_256_CBC_SHA256 + * TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA256 + * TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA256 + * TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA256 + * TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA256 + * TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA256 + * TLS_EMPTY_RENEGOTIATION_INFO_SCSV + * TLS_ECDH_ECDSA_WITH_NULL_SHA + * TLS_ECDH_ECDSA_WITH_RC4_128_SHA + * TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA + * TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA + * TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA + * TLS_ECDHE_ECDSA_WITH_NULL_SHA + * TLS_ECDHE_ECDSA_WITH_RC4_128_SHA + * TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA + * TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA + * TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA + * TLS_ECDH_RSA_WITH_NULL_SHA + * TLS_ECDH_RSA_WITH_RC4_128_SHA + * TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA + * TLS_ECDH_RSA_WITH_AES_128_CBC_SHA + * TLS_ECDH_RSA_WITH_AES_256_CBC_SHA + * TLS_ECDHE_RSA_WITH_NULL_SHA + * TLS_ECDHE_RSA_WITH_RC4_128_SHA + * TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA + * TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA + * TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA + * TLS_ECDH_anon_WITH_NULL_SHA + * TLS_ECDH_anon_WITH_RC4_128_SHA + * TLS_ECDH_anon_WITH_3DES_EDE_CBC_SHA + * TLS_ECDH_anon_WITH_AES_128_CBC_SHA + * TLS_ECDH_anon_WITH_AES_256_CBC_SHA + * TLS_SRP_SHA_WITH_3DES_EDE_CBC_SHA + * TLS_SRP_SHA_RSA_WITH_3DES_EDE_CBC_SHA + * TLS_SRP_SHA_DSS_WITH_3DES_EDE_CBC_SHA + * TLS_SRP_SHA_WITH_AES_128_CBC_SHA + * TLS_SRP_SHA_RSA_WITH_AES_128_CBC_SHA + * TLS_SRP_SHA_DSS_WITH_AES_128_CBC_SHA + * TLS_SRP_SHA_WITH_AES_256_CBC_SHA + * TLS_SRP_SHA_RSA_WITH_AES_256_CBC_SHA + * TLS_SRP_SHA_DSS_WITH_AES_256_CBC_SHA + * TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 + * TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384 + * TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256 + * TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384 + * TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 + * TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 + * TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256 + * TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384 + * TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256 + * TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384 + * TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256 + * TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384 + * TLS_ECDHE_PSK_WITH_RC4_128_SHA + * TLS_ECDHE_PSK_WITH_3DES_EDE_CBC_SHA + * TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA + * TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA + * TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256 + * TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA384 + * TLS_ECDHE_PSK_WITH_NULL_SHA + * TLS_ECDHE_PSK_WITH_NULL_SHA256 + * TLS_ECDHE_PSK_WITH_NULL_SHA384 + * TLS_RSA_WITH_ARIA_128_CBC_SHA256 + * TLS_RSA_WITH_ARIA_256_CBC_SHA384 + * TLS_DH_DSS_WITH_ARIA_128_CBC_SHA256 + * TLS_DH_DSS_WITH_ARIA_256_CBC_SHA384 + * TLS_DH_RSA_WITH_ARIA_128_CBC_SHA256 + * TLS_DH_RSA_WITH_ARIA_256_CBC_SHA384 + * TLS_DHE_DSS_WITH_ARIA_128_CBC_SHA256 + * TLS_DHE_DSS_WITH_ARIA_256_CBC_SHA384 + * TLS_DHE_RSA_WITH_ARIA_128_CBC_SHA256 + * TLS_DHE_RSA_WITH_ARIA_256_CBC_SHA384 + * TLS_DH_anon_WITH_ARIA_128_CBC_SHA256 + * TLS_DH_anon_WITH_ARIA_256_CBC_SHA384 + * TLS_ECDHE_ECDSA_WITH_ARIA_128_CBC_SHA256 + * TLS_ECDHE_ECDSA_WITH_ARIA_256_CBC_SHA384 + * TLS_ECDH_ECDSA_WITH_ARIA_128_CBC_SHA256 + * TLS_ECDH_ECDSA_WITH_ARIA_256_CBC_SHA384 + * TLS_ECDHE_RSA_WITH_ARIA_128_CBC_SHA256 + * TLS_ECDHE_RSA_WITH_ARIA_256_CBC_SHA384 + * TLS_ECDH_RSA_WITH_ARIA_128_CBC_SHA256 + * TLS_ECDH_RSA_WITH_ARIA_256_CBC_SHA384 + * TLS_RSA_WITH_ARIA_128_GCM_SHA256 + * TLS_RSA_WITH_ARIA_256_GCM_SHA384 + * TLS_DH_RSA_WITH_ARIA_128_GCM_SHA256 + * TLS_DH_RSA_WITH_ARIA_256_GCM_SHA384 + * TLS_DH_DSS_WITH_ARIA_128_GCM_SHA256 + * TLS_DH_DSS_WITH_ARIA_256_GCM_SHA384 + * TLS_DH_anon_WITH_ARIA_128_GCM_SHA256 + * TLS_DH_anon_WITH_ARIA_256_GCM_SHA384 + * TLS_ECDH_ECDSA_WITH_ARIA_128_GCM_SHA256 + * TLS_ECDH_ECDSA_WITH_ARIA_256_GCM_SHA384 + * TLS_ECDH_RSA_WITH_ARIA_128_GCM_SHA256 + * TLS_ECDH_RSA_WITH_ARIA_256_GCM_SHA384 + * TLS_PSK_WITH_ARIA_128_CBC_SHA256 + * TLS_PSK_WITH_ARIA_256_CBC_SHA384 + * TLS_DHE_PSK_WITH_ARIA_128_CBC_SHA256 + * TLS_DHE_PSK_WITH_ARIA_256_CBC_SHA384 + * TLS_RSA_PSK_WITH_ARIA_128_CBC_SHA256 + * TLS_RSA_PSK_WITH_ARIA_256_CBC_SHA384 + * TLS_PSK_WITH_ARIA_128_GCM_SHA256 + * TLS_PSK_WITH_ARIA_256_GCM_SHA384 + * TLS_RSA_PSK_WITH_ARIA_128_GCM_SHA256 + * TLS_RSA_PSK_WITH_ARIA_256_GCM_SHA384 + * TLS_ECDHE_PSK_WITH_ARIA_128_CBC_SHA256 + * TLS_ECDHE_PSK_WITH_ARIA_256_CBC_SHA384 + * TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_CBC_SHA256 + * TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_CBC_SHA384 + * TLS_ECDH_ECDSA_WITH_CAMELLIA_128_CBC_SHA256 + * TLS_ECDH_ECDSA_WITH_CAMELLIA_256_CBC_SHA384 + * TLS_ECDHE_RSA_WITH_CAMELLIA_128_CBC_SHA256 + * TLS_ECDHE_RSA_WITH_CAMELLIA_256_CBC_SHA384 + * TLS_ECDH_RSA_WITH_CAMELLIA_128_CBC_SHA256 + * TLS_ECDH_RSA_WITH_CAMELLIA_256_CBC_SHA384 + * TLS_RSA_WITH_CAMELLIA_128_GCM_SHA256 + * TLS_RSA_WITH_CAMELLIA_256_GCM_SHA384 + * TLS_DH_RSA_WITH_CAMELLIA_128_GCM_SHA256 + * TLS_DH_RSA_WITH_CAMELLIA_256_GCM_SHA384 + * TLS_DH_DSS_WITH_CAMELLIA_128_GCM_SHA256 + * TLS_DH_DSS_WITH_CAMELLIA_256_GCM_SHA384 + * TLS_DH_anon_WITH_CAMELLIA_128_GCM_SHA256 + * TLS_DH_anon_WITH_CAMELLIA_256_GCM_SHA384 + * TLS_ECDH_ECDSA_WITH_CAMELLIA_128_GCM_SHA256 + * TLS_ECDH_ECDSA_WITH_CAMELLIA_256_GCM_SHA384 + * TLS_ECDH_RSA_WITH_CAMELLIA_128_GCM_SHA256 + * TLS_ECDH_RSA_WITH_CAMELLIA_256_GCM_SHA384 + * TLS_PSK_WITH_CAMELLIA_128_GCM_SHA256 + * TLS_PSK_WITH_CAMELLIA_256_GCM_SHA384 + * TLS_RSA_PSK_WITH_CAMELLIA_128_GCM_SHA256 + * TLS_RSA_PSK_WITH_CAMELLIA_256_GCM_SHA384 + * TLS_PSK_WITH_CAMELLIA_128_CBC_SHA256 + * TLS_PSK_WITH_CAMELLIA_256_CBC_SHA384 + * TLS_DHE_PSK_WITH_CAMELLIA_128_CBC_SHA256 + * TLS_DHE_PSK_WITH_CAMELLIA_256_CBC_SHA384 + * TLS_RSA_PSK_WITH_CAMELLIA_128_CBC_SHA256 + * TLS_RSA_PSK_WITH_CAMELLIA_256_CBC_SHA384 + * TLS_ECDHE_PSK_WITH_CAMELLIA_128_CBC_SHA256 + * TLS_ECDHE_PSK_WITH_CAMELLIA_256_CBC_SHA384 + * TLS_RSA_WITH_AES_128_CCM + * TLS_RSA_WITH_AES_256_CCM + * TLS_RSA_WITH_AES_128_CCM_8 + * TLS_RSA_WITH_AES_256_CCM_8 + * TLS_PSK_WITH_AES_128_CCM + * TLS_PSK_WITH_AES_256_CCM + * TLS_PSK_WITH_AES_128_CCM_8 + * TLS_PSK_WITH_AES_256_CCM_8 + + | Note: This list was assembled from the set of registered TLS + | cipher suites when [RFC7540] was developed. This list includes + | those cipher suites that do not offer an ephemeral key exchange + | and those that are based on the TLS null, stream, or block + | cipher type (as defined in Section 6.2.3 of [TLS12]). + | Additional cipher suites with these properties could be + | defined; these would not be explicitly prohibited. + + For more details, see Section 9.2.2. + +Appendix B. Changes from RFC 7540 + + This revision includes the following substantive changes: + + * Use of TLS 1.3 was defined based on [RFC8740], which this document + obsoletes. + + * The priority scheme defined in RFC 7540 is deprecated. + Definitions for the format of the PRIORITY frame and the priority + fields in the HEADERS frame have been retained, plus the rules + governing when PRIORITY frames can be sent and received, but the + semantics of these fields are only described in RFC 7540. The + priority signaling scheme from RFC 7540 was not successful. Using + the simpler signaling in [HTTP-PRIORITY] is recommended. + + * The HTTP/1.1 Upgrade mechanism is deprecated and no longer + specified in this document. It was never widely deployed, with + plaintext HTTP/2 users choosing to use the prior-knowledge + implementation instead. + + * Validation for field names and values has been narrowed. The + validation that is mandatory for intermediaries is precisely + defined, and error reporting for requests has been amended to + encourage sending 400-series status codes. + + * The ranges of codepoints for settings and frame types that were + reserved for Experimental Use are now available for general use. + + * Connection-specific header fields -- which are prohibited -- are + more precisely and comprehensively identified. + + * Host and ":authority" are no longer permitted to disagree. + + * Rules for sending Dynamic Table Size Update instructions after + changes in settings have been clarified in Section 4.3.1. + + Editorial changes are also included. In particular, changes to + terminology and document structure are in response to updates to core + HTTP semantics [HTTP]. Those documents now include some concepts + that were first defined in RFC 7540, such as the 421 status code or + connection coalescing. + +Acknowledgments + + Credit for non-trivial input to this document is owed to a large + number of people who have contributed to the HTTP Working Group over + the years. [RFC7540] contains a more extensive list of people that + deserve acknowledgment for their contributions. + +Contributors + + Mike Belshe and Roberto Peon authored the text that this document is + based on. + +Authors' Addresses + + Martin Thomson (editor) + Mozilla + Australia + Email: mt@lowentropy.net + + + Cory Benfield (editor) + Apple Inc. + Email: cbenfield@apple.com 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..cd992cd 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="/tmp/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/dist/sync.sh b/dist/sync.sh index 870ebc6..5d92a57 100644 --- a/dist/sync.sh +++ b/dist/sync.sh @@ -4,32 +4,26 @@ SCRIPT_DIR=$(dirname $0) TF_DIR=${SCRIPT_DIR}/tf REPO_DIR=${SCRIPT_DIR}/.. TARGET_DIR=${REPO_DIR}/target -XDP_TOOLS_DIR=${REPO_DIR}/vendor/xdp-tools SSH_USER="ec2-user" KEY_PATH="" REBUILD="false" ECHO="" +EXAMPLES="http-server tcp-echo-server tcp-echo-client hyper udp-server udp-client" + function usage() { echo "Usage: $0 [-h] -k [-u ]" echo " -h, --help Show this help message and exit" echo " -k, --key-path Path to the SSH key. Required." echo " -u, --user User to sync with. Default: ${SSH_USER}" - echo " -r, --rebuild Rebuild the examples and xdp-tools. Default: ${REBUILD}" - echo " -d, --dry-run Dry run the script. Default: ${DRY_RUN}" + echo " -r, --rebuild Rebuild the examples. Default: ${REBUILD}" + echo " -d, --dry-run Dry run the script." } function build_examples() { - pushd ${REPO_DIR} - ${ECHO} cargo build --release --examples - popd > /dev/null -} - -function build_xdp_tools() { - pushd ${XDP_TOOLS_DIR} - ${ECHO} ./configure - ${ECHO} make + pushd ${REPO_DIR} > /dev/null + ${ECHO} cargo build --profile profiling --examples popd > /dev/null } @@ -38,15 +32,12 @@ function sync_examples() { local USER=$2 local IP=$3 - ${ECHO} scp -i ${KEY_PATH} ${TARGET_DIR}/release/examples/{rx-bench,tx-bench,echo,rx-mt,tx-mt} ${USER}@${IP}:~/ -} - -function sync_xdp_tools() { - local KEY_PATH=$1 - local USER=$2 - local IP=$3 + local BINS="" + for ex in ${EXAMPLES}; do + BINS="${BINS} ${TARGET_DIR}/profiling/examples/${ex}" + done - ${ECHO} scp -i ${KEY_PATH} ${XDP_TOOLS_DIR}/{xdp-bench/xdp-bench,xdp-trafficgen/xdp-trafficgen} ${USER}@${IP}:~/ + ${ECHO} scp -i ${KEY_PATH} ${BINS} ${USER}@${IP}:~/ } function get_ips() { @@ -61,15 +52,6 @@ function get_ips() { popd > /dev/null } -function sync_all() { - local KEY_PATH=$1 - local USER=$2 - local IP=$3 - - sync_examples ${KEY_PATH} ${USER} ${IP} - sync_xdp_tools ${KEY_PATH} ${USER} ${IP} -} - function main() { local KEY_PATH=$1 local USER=$2 @@ -77,12 +59,11 @@ function main() { if [ "${REBUILD}" == "true" ]; then build_examples - build_xdp_tools fi for IP in $(get_ips); do echo "Syncing to ${USER}@${IP}" - sync_all ${KEY_PATH} ${USER} ${IP} + sync_examples ${KEY_PATH} ${USER} ${IP} done } diff --git a/examples/common/mod.rs b/examples/common/mod.rs index 170dbb2..6e81deb 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), @@ -54,6 +64,14 @@ impl Stats { } } + pub fn new_with_id_and_packets_per_print(id: usize, packets_per_print: u64) -> Self { + Self { + id: Some(id), + packets_per_print, + ..Self::new() + } + } + #[inline(always)] pub fn update(&mut self, bytes: usize, is_fragment: bool) { self.bytes_received += bytes as u64; @@ -71,8 +89,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 +142,7 @@ impl Stats { } } +#[inline(always)] pub fn swap_addresses(frame: &mut [u8]) -> Option<()> { let mut ether = MutableEthernetPacket::new(frame)?; @@ -235,7 +253,7 @@ pub fn build_frame(args: &GeneratorArgs) -> Vec { data } -#[derive(clap::Args)] +#[derive(clap::Args, Clone)] pub struct BaseArgs { #[arg(short, long, help = "The name of the network interface to use.")] pub if_name: String, @@ -309,7 +327,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/http-server.rs b/examples/http-server.rs new file mode 100644 index 0000000..c645eb7 --- /dev/null +++ b/examples/http-server.rs @@ -0,0 +1,210 @@ +use std::{ + ops::{Deref, DerefMut}, + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, +}; + +use clap::Parser; +use coarsetime::Duration; + +use libvoid::{ + net::{TcpError, http::HttpError}, + rt::{LocalRuntime, Runtime}, +}; +use libvoid::{ + net::{http::HttpListener, wire::ip::SocketAddr}, + rt::spawn, +}; + +mod common; +use common::{BaseArgs, Stats}; + +#[derive(Parser, Clone)] +#[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")] + multi_threaded: 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 mt(args: Args) { + let runtime = Runtime::builder(&args.if_name) + .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, move |queue| async move { + let listener = HttpListener::listen(args.local_addr.ip, args.local_addr.port) + .expect("Failed to listen"); + println!( + "HTTP server listening on {} (queue {queue})", + args.local_addr + ); + + loop { + let mut conn = listener.accept().await.expect("Failed to accept"); + println!("Accepted connection (queue {queue})",); + + spawn(async move { + let mut stats = Stats::new_with_id_and_packets_per_print(0, 1_000_000); + loop { + match conn.next_request().await { + Ok(Some(req)) => { + let path = conn.request_path(&req); + let body = match path { + b"/" => b"Hello, World!\n" as &[u8], + _ => b"Not Found\n" as &[u8], + }; + + let mut writer = conn.respond(&req); + if writer.write_body(body).await.is_err() { + break; + } + if writer.finish().await.is_err() { + break; + } + + stats.update(body.len(), false); + stats.maybe_print(); + } + Ok(None) => break, + Err(e) => { + if e != HttpError::Tcp(TcpError::Reset) { + println!("Request error: {:?}", e); + } + break; + } + } + } + }); + } + }) + .expect("Failed to run runtime"); + + println!("Exiting..."); +} + +fn st(args: Args) { + 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 = HttpListener::listen(args.local_addr.ip, args.local_addr.port) + .expect("Failed to listen"); + println!("HTTP server listening on {}", args.local_addr); + + loop { + let mut conn = listener.accept().await.expect("Failed to accept"); + + spawn(async move { + let mut stats = Stats::new_with_id_and_packets_per_print(0, 1_000_000); + loop { + match conn.next_request().await { + Ok(Some(req)) => { + let path = conn.request_path(&req); + let body = match path { + b"/" => b"Hello, World!\n" as &[u8], + _ => b"Not Found\n" as &[u8], + }; + + let mut writer = conn.respond(&req); + if writer.write_body(body).await.is_err() { + break; + } + if writer.finish().await.is_err() { + break; + } + + stats.update(body.len(), false); + stats.maybe_print(); + } + Ok(None) => break, + Err(e) => { + if e != HttpError::Tcp(TcpError::Reset) { + println!("Request error: {:?}", e); + } + break; + } + } + } + }); + } + }) + .expect("Failed to run runtime"); + + println!("Exiting..."); +} + +fn main() { + let args = Args::parse(); + if args.multi_threaded { + mt(args); + } else { + st(args); + } +} diff --git a/examples/hyper.rs b/examples/hyper.rs new file mode 100644 index 0000000..774bd26 --- /dev/null +++ b/examples/hyper.rs @@ -0,0 +1,79 @@ +#![deny(warnings)] + +use std::convert::Infallible; +use std::net::SocketAddr; + +use bytes::Bytes; +use clap::Parser; +use http_body_util::Full; +use hyper::server::conn::http1; +use hyper::service::service_fn; +use hyper::{Request, Response}; +use tokio::net::TcpListener; + +use hyper_util::rt::{TokioIo, TokioTimer}; +use tokio::runtime::Builder; + +// An async function that consumes a request, does nothing with it and returns a +// response. +async fn hello(_: Request) -> Result>, Infallible> { + Ok(Response::new(Full::new(Bytes::from("Hello World!")))) +} + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +struct Args { + #[arg(short, long, default_value = "[fc00:dead:cafe:1::1]:8080")] + local_addr: SocketAddr, + #[arg(short, long, default_value = "1")] + num_threads: usize, +} + +pub fn main() -> Result<(), Box> { + // This address is localhost + let args = Args::parse(); + + let rt = if args.num_threads > 1 { + Builder::new_multi_thread() + .enable_all() + .worker_threads(args.num_threads.min(num_cpus::get())) + .build()? + } else { + Builder::new_current_thread().enable_all().build()? + }; + + rt.block_on(async move { + // Bind to the port and listen for incoming TCP connections + let listener = TcpListener::bind(args.local_addr).await?; + println!("Listening on http://{}", args.local_addr); + loop { + // When an incoming TCP connection is received grab a TCP stream for + // client<->server communication. + // + // Note, this is a .await point, this loop will loop forever but is not a busy loop. The + // .await point allows the Tokio runtime to pull the task off of the thread until the task + // has work to do. In this case, a connection arrives on the port we are listening on and + // the task is woken up, at which point the task is then put back on a thread, and is + // driven forward by the runtime, eventually yielding a TCP stream. + let (tcp, _) = listener.accept().await?; + // Use an adapter to access something implementing `tokio::io` traits as if they implement + // `hyper::rt` IO traits. + let io = TokioIo::new(tcp); + + // Spin up a new task in Tokio so we can continue to listen for new TCP connection on the + // current task without waiting for the processing of the HTTP1 connection we just received + // to finish + tokio::task::spawn(async move { + // Handle the connection from the client using HTTP1 and pass any + // HTTP requests received on that connection to the `hello` function + if let Err(err) = http1::Builder::new() + .timer(TokioTimer::new()) + .serve_connection(io, service_fn(hello)) + .await + { + println!("Error serving connection: {:?}", err); + } + }); + } + }) +} diff --git a/examples/local-rx.rs b/examples/local-rx.rs deleted file mode 100644 index 10f6f96..0000000 --- a/examples/local-rx.rs +++ /dev/null @@ -1,156 +0,0 @@ -use std::{ - ops::{Deref, DerefMut}, - sync::{ - Arc, - atomic::{AtomicBool, Ordering}, - }, -}; - -use clap::Parser; - -use libvoid::xdp::{ - context::XdpContext, - frame::{BasicFrameBuffer, FrameBuffer}, - futures::LocalExecutor, - socket::Socket, - umem::Umem, -}; - -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 - } -} - -fn main() { - let mut stats = Stats::new(); - let args = Args::parse(); - - // We are relying on this invariant in the benchmark code below, though to be absolutely clear this isn't - // actually a requirement, in the XDP subsystem itself, though its HIGHLY encouraged. - debug_assert!( - args.fill_ring_size >= args.busy_poll_batch_size as u32, - "Fill ring size must be greater than or equal to the busy poll batch size" - ); - - // Every application starts with setting up an XdpContext, this loads the XDP kernel program and attaches it to the named - // interface. - let mut xdp_ctx = XdpContext::builder(&args.if_name) - .attach_mode(args.attach_mode) - .enable_fragmentation(args.enable_fragmentation) - .build() - .expect("Failed to create xdp context"); - - // A Umem is created to manage sharing memory buffers between the kernel and user space. - // You will need one of these for each unique device you want to use. - // - // Each Umem comes associated with three key components: - // - Owner (umem) > The owner of the UMEM, this is used to create frames and is responsible for cleaning up the UMEM once all is said and done. - // - Fill Queue (fq) > Used to pass frames from user space to the kernel for reading packet data into. - // - Completion Queue (cq) > Used to retrieve frames from the kernel after transmission finishes. - let mut umem = Umem::builder() - .completion_ring_size(args.completion_ring_size) - .fill_ring_size(args.fill_ring_size) - .frame_size(args.frame_size) - .busy_poll(args.busy_poll) - .num_frames(args.fill_ring_size as usize) // We are benching reads so just set the num frames to the fill ring size. - .huge_tables(args.huge_tables) - .unaligned(args.unaligned) - .build_local() - .expect("Failed to create umem"); - - // A socket represents a standard means of reading/writing packets from/to a network interface. - // - // This is the main handle for interacting with the network data, if needed this can be split into its owner, rx, and tx - // components using the split() function. - let mut socket = Socket::builder(&args.if_name, args.queue) - .rx_ring_size(args.rx_ring_size) - .tx_ring_size(args.tx_ring_size) - .busy_poll(args.busy_poll) - .busy_poll_batch_size(args.busy_poll_batch_size) - .busy_poll_timeout_us(args.busy_poll_timeout_us) - .copy_mode(args.copy_mode) - .build_local(&mut xdp_ctx, umem.owner().clone()) - .expect("Failed to create socket"); - - 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 fd = socket.fd(); - let future = async move { - // We are benchmarking receive throughput so lets give all of our frames to the kernel. - // - // This is where the first part of the invariant above comes into play because our num_frames is the same as the fill ring size, - // the initial_frames buffer will be empty at the end of this call. - let mut frames = umem.init_buffer::().unwrap(); - umem.process_fill_queue(&mut frames, &[socket.fd()]) - .await - .unwrap(); - - while !exit.load(Ordering::Relaxed) { - // Start by reading some frames from the socket. The result of this call is guaranteed to be between 1 and the batch size. - match socket.recv(&mut frames).await { - Ok(received) => { - debug_assert!( - received > 0 && received <= args.busy_poll_batch_size as u32, - "Received more frames than the batch size or 0 frames, this should never happen!" - ); - } - Err(_) => { - continue; - } - } - - // You now have a batch of raw frames, at this level this is an L2 frame, almost assuredly Ethernet based. - for frame in frames.iter_frames_mut() { - // Do something with the frame! - // - // Note checking the frame.is_fragment() is needed, iff we have fragmentation enabled. In this mode it is possible - // to receive a fragment of a packat if the MTU of the device is larger than the frame size. - stats.update(frame.len(), frame.is_fragment()); - } - - // You now have two options in general: - // - Update the packet data in some way in place, then send all or some number of frames back out the socket achieving true end to end zero copy. - // - See the [examples/echo.rs](examples/echo.rs) example for a full end to end zero copy example. - // - Return the data to the kernel by means of the fill queue. Which is what will be doing here. - - // Process the frame buffer, this will consume the entire buffer and submit them to the fill queue. - umem.process_fill_queue(&mut frames, &[socket.fd()]) - .await - .expect("Failed to process fill queue"); - - // Maybe print the stats for this iteration. - stats.maybe_print(); - } - }; - - let mut executor = LocalExecutor::new().expect("Failed to create executor"); - executor.register(fd).expect("Failed to register socket"); - - executor.run(future); -} diff --git a/examples/smol-rx.rs b/examples/smol-rx.rs deleted file mode 100644 index c785e8d..0000000 --- a/examples/smol-rx.rs +++ /dev/null @@ -1,149 +0,0 @@ -use std::{ - ops::{Deref, DerefMut}, - sync::{ - Arc, - atomic::{AtomicBool, Ordering}, - }, -}; - -use clap::Parser; - -use libvoid::xdp::{ - context::XdpContext, - frame::{BasicFrameBuffer, FrameBuffer}, - socket::Socket, - umem::Umem, -}; - -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 - } -} - -fn main() { - smol::block_on(async { - let mut stats = Stats::new(); - let args = Args::parse(); - - // We are relying on this invariant in the benchmark code below, though to be absolutely clear this isn't - // actually a requirement, in the XDP subsystem itself, though its HIGHLY encouraged. - debug_assert!( - args.fill_ring_size >= args.busy_poll_batch_size as u32, - "Fill ring size must be greater than or equal to the busy poll batch size" - ); - - // Every application starts with setting up an XdpContext, this loads the XDP kernel program and attaches it to the named - // interface. - let mut xdp_ctx = XdpContext::builder(&args.if_name) - .attach_mode(args.attach_mode) - .enable_fragmentation(args.enable_fragmentation) - .build() - .expect("Failed to create xdp context"); - - // A Umem is created to manage sharing memory buffers between the kernel and user space. - // You will need one of these for each unique device you want to use. - // - // Each Umem comes associated with three key components: - // - Owner (umem) > The owner of the UMEM, this is used to create frames and is responsible for cleaning up the UMEM once all is said and done. - // - Fill Queue (fq) > Used to pass frames from user space to the kernel for reading packet data into. - // - Completion Queue (cq) > Used to retrieve frames from the kernel after transmission finishes. - let mut umem = Umem::builder() - .completion_ring_size(args.completion_ring_size) - .fill_ring_size(args.fill_ring_size) - .frame_size(args.frame_size) - .busy_poll(args.busy_poll) - .num_frames(args.fill_ring_size as usize) // We are benching reads so just set the num frames to the fill ring size. - .huge_tables(args.huge_tables) - .unaligned(args.unaligned) - .build_smol(&mut xdp_ctx) - .expect("Failed to create umem"); - - // A socket represents a standard means of reading/writing packets from/to a network interface. - // - // This is the main handle for interacting with the network data, if needed this can be split into its owner, rx, and tx - // components using the split() function. - let mut socket = Socket::builder(&args.if_name, args.queue) - .rx_ring_size(args.rx_ring_size) - .tx_ring_size(args.tx_ring_size) - .busy_poll(args.busy_poll) - .busy_poll_batch_size(args.busy_poll_batch_size) - .busy_poll_timeout_us(args.busy_poll_timeout_us) - .copy_mode(args.copy_mode) - .build_smol(&mut xdp_ctx, umem.owner().clone()) - .expect("Failed to create socket"); - - 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"); - - // We are benchmarking receive throughput so lets give all of our frames to the kernel. - // - // This is where the first part of the invariant above comes into play because our num_frames is the same as the fill ring size, - // the initial_frames buffer will be empty at the end of this call. - let mut frames = umem.init_buffer::().unwrap(); - umem.process_fill_queue(&mut frames, &[socket.fd()]) - .await - .unwrap(); - - while !exit.load(Ordering::Relaxed) { - // Start by reading some frames from the socket. The result of this call is guaranteed to be between 1 and the batch size. - match socket.recv(&mut frames).await { - Ok(received) => { - debug_assert!( - received > 0 && received <= args.busy_poll_batch_size as u32, - "Received more frames than the batch size or 0 frames, this should never happen!" - ); - } - Err(_) => { - continue; - } - } - - // You now have a batch of raw frames, at this level this is an L2 frame, almost assuredly Ethernet based. - for frame in frames.iter_frames_mut() { - // Do something with the frame! - // - // Note checking the frame.is_fragment() is needed, iff we have fragmentation enabled. In this mode it is possible - // to receive a fragment of a packat if the MTU of the device is larger than the frame size. - stats.update(frame.len(), frame.is_fragment()); - } - - // You now have two options in general: - // - Update the packet data in some way in place, then send all or some number of frames back out the socket achieving true end to end zero copy. - // - See the [examples/echo.rs](examples/echo.rs) example for a full end to end zero copy example. - // - Return the data to the kernel by means of the fill queue. Which is what will be doing here. - - // Process the frame buffer, this will consume the entire buffer and submit them to the fill queue. - umem.process_fill_queue(&mut frames, &[socket.fd()]) - .await - .expect("Failed to process fill queue"); - - // Maybe print the stats for this iteration. - stats.maybe_print(); - } - }); -} 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..3280d21 --- /dev/null +++ b/examples/tcp-echo-server.rs @@ -0,0 +1,119 @@ +use std::{ + ops::{Deref, DerefMut}, + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, +}; + +use clap::Parser; +use coarsetime::Duration; + +use libvoid::rt::LocalRuntime; +use libvoid::{ + net::{socket::TcpListener, wire::ip::SocketAddr}, + rt::spawn, +}; + +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 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() + ); + + spawn(async move { + let mut stats = Stats::new_with_id_and_packets_per_print( + stream.remote_port() as usize, + 1_000_000, + ); + 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 deleted file mode 100644 index 806cb94..0000000 --- a/examples/tokio-rx-ms.rs +++ /dev/null @@ -1,201 +0,0 @@ -use std::{ - ffi::c_int, - num::NonZero, - ops::{Deref, DerefMut}, - sync::{ - Arc, - atomic::{AtomicBool, Ordering}, - }, -}; - -use clap::Parser; -use futures::lock::Mutex; - -use libvoid::xdp::{ - context::XdpContext, - frame::{BasicFrameBuffer, FrameBuffer}, - futures::{TokioSocket, TokioUmem}, - socket::Socket, - umem::Umem, -}; - -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 = "2")] - num_threads: NonZero, -} - -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 - } -} - -async fn worker_task<'umem>( - exit: Arc, - mut stats: Stats, - mut socket: TokioSocket<'umem>, - frame_stack: Arc>>, - batch_size: usize, -) { - let mut frames = BasicFrameBuffer::new(batch_size); - while !exit.load(Ordering::Relaxed) { - // Start by reading some frames from the socket. The result of this call is guaranteed to be between 1 and the batch size. - match socket.recv(&mut frames).await { - Ok(received) => { - debug_assert!( - received > 0 && received <= batch_size as u32, - "Received more frames than the batch size or 0 frames, this should never happen!" - ); - } - Err(e) => { - eprintln!("Error receiving frames: {}", e); - continue; - } - } - - // You now have a batch of raw frames, at this level this is an L2 frame, almost assuredly Ethernet based. - for frame in frames.iter_frames() { - // Do something with the frame! - // - // Note checking the frame.is_fragment() is needed, iff we have fragmentation enabled. In this mode it is possible - // to receive a fragment of a packat if the MTU of the device is larger than the frame size. - stats.update(frame.len(), frame.is_fragment()); - } - - // You now have two options in general: - // - Update the packet data in some way in place, then send all or some number of frames back out the socket achieving true end to end zero copy. - // - See the [examples/echo.rs](examples/echo.rs) example for a full end to end zero copy example. - // - 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(..)); - } - - // Maybe print the stats for this iteration. - stats.maybe_print(); - } -} - -async fn umem_task<'umem>( - exit: Arc, - mut umem: TokioUmem<'umem>, - frame_stack: Arc>>, - fds: Vec, -) { - while !exit.load(Ordering::Relaxed) { - { - let guard = frame_stack.lock().await; - umem.process_fill_queue(guard, &fds).await.unwrap(); - } - - tokio::task::yield_now().await; - } -} - -#[tokio::main(flavor = "multi_thread")] -async fn main() { - let args = Args::parse(); - - // We are relying on this invariant in the benchmark code bellow, though to be absolutely clear this isn't - // actually a requirement, in the XDP subsystem itself, though its HIGHLY encouraged. - debug_assert!( - args.fill_ring_size >= (args.busy_poll_batch_size * args.num_threads.get()) as u32, - "Fill ring size must be greater than or equal to the busy poll batch size * number of threads" - ); - - // Every application starts with setting up an XdpContext, this loads the XDP kernel program and attaches it to the named - // interface. - let mut xdp_ctx = XdpContext::builder(&args.if_name) - .attach_mode(args.attach_mode) - .enable_fragmentation(args.enable_fragmentation) - .build() - .expect("Failed to create xdp context"); - - // A Umem is created to manage sharing memory buffers between the kernel and user space. - // You will need one of these for each unique device you want to use. - // - // Each Umem comes associated with three key components: - // - Owner (umem) > The owner of the UMEM, this is used to create frames and is responsible for cleaning up the UMEM once all is said and done. - // - Fill Queue (fq) > Used to pass frames from user space to the kernel for reading packet data into. - // - Completion Queue (cq) > Used to retrieve frames from the kernel after transmission finishes. - let mut umem = Umem::builder() - .completion_ring_size(args.completion_ring_size) - .fill_ring_size(args.fill_ring_size) - .frame_size(args.frame_size) - .busy_poll(args.busy_poll) - .num_frames(args.fill_ring_size as usize) // We are benching reads so just set the num frames to the fill ring size. - .huge_tables(args.huge_tables) - .unaligned(args.unaligned) - .build_tokio(&mut xdp_ctx) - .expect("Failed to create umem"); - - 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 mut frames = umem.init_buffer::().unwrap(); - umem.process_fill_queue(&mut frames, &[]) - .await - .expect("Failed to process fill queue"); - - let frame_stack = Arc::new(Mutex::new(frames)); - - let mut threads = Vec::with_capacity(args.num_threads.get() + 1); - let mut socket_fds = Vec::with_capacity(args.num_threads.get()); - - for i in 0..args.num_threads.get() { - let exit = exit.clone(); - let stats = Stats::new_with_id(i); - let frame_stack = frame_stack.clone(); - let batch_size = args.busy_poll_batch_size; - - // A socket represents a standard means of reading/writing packets from/to a network interface. - // - // This is the main handle for interacting with the network data, if needed this can be split into its owner, rx, and tx - // components using the split() function. - let socket = Socket::builder(&args.if_name, args.queue) - .rx_ring_size(args.rx_ring_size) - .tx_ring_size(args.tx_ring_size) - .busy_poll(args.busy_poll) - .busy_poll_batch_size(args.busy_poll_batch_size) - .busy_poll_timeout_us(args.busy_poll_timeout_us) - .copy_mode(args.copy_mode) - .build_tokio(&mut xdp_ctx, umem.owner().clone()) - .expect("Failed to create socket"); - socket_fds.push(socket.fd()); - - // Spawn our worker thread, this will handle pulling frames from the rx ring and processing them. - let thread = tokio::spawn(worker_task(exit, stats, socket, frame_stack, batch_size)); - threads.push(thread); - } - - // Spawn our Umem thread, this will handle actually submitting frames to the fill queue. - threads.push(tokio::spawn(umem_task(exit, umem, frame_stack, socket_fds))); - - println!("All threads created, listening for packets..."); - - // Wait for exit of all threads. - for thread in threads.drain(..) { - thread.await.unwrap(); - } -} diff --git a/examples/tokio-rx.rs b/examples/tokio-rx.rs deleted file mode 100644 index f8b843a..0000000 --- a/examples/tokio-rx.rs +++ /dev/null @@ -1,148 +0,0 @@ -use std::{ - ops::{Deref, DerefMut}, - sync::{ - Arc, - atomic::{AtomicBool, Ordering}, - }, -}; - -use clap::Parser; - -use libvoid::xdp::{ - context::XdpContext, - frame::{BasicFrameBuffer, FrameBuffer}, - socket::Socket, - umem::Umem, -}; - -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 = "multi_thread")] -async fn main() { - let mut stats = Stats::new(); - let args = Args::parse(); - - // We are relying on this invariant in the benchmark code below, though to be absolutely clear this isn't - // actually a requirement, in the XDP subsystem itself, though its HIGHLY encouraged. - debug_assert!( - args.fill_ring_size >= args.busy_poll_batch_size as u32, - "Fill ring size must be greater than or equal to the busy poll batch size" - ); - - // Every application starts with setting up an XdpContext, this loads the XDP kernel program and attaches it to the named - // interface. - let mut xdp_ctx = XdpContext::builder(&args.if_name) - .attach_mode(args.attach_mode) - .enable_fragmentation(args.enable_fragmentation) - .build() - .expect("Failed to create xdp context"); - - // A Umem is created to manage sharing memory buffers between the kernel and user space. - // You will need one of these for each unique device you want to use. - // - // Each Umem comes associated with three key components: - // - Owner (umem) > The owner of the UMEM, this is used to create frames and is responsible for cleaning up the UMEM once all is said and done. - // - Fill Queue (fq) > Used to pass frames from user space to the kernel for reading packet data into. - // - Completion Queue (cq) > Used to retrieve frames from the kernel after transmission finishes. - let mut umem = Umem::builder() - .completion_ring_size(args.completion_ring_size) - .fill_ring_size(args.fill_ring_size) - .frame_size(args.frame_size) - .busy_poll(args.busy_poll) - .num_frames(args.fill_ring_size as usize) // We are benching reads so just set the num frames to the fill ring size. - .huge_tables(args.huge_tables) - .unaligned(args.unaligned) - .build_tokio(&mut xdp_ctx) - .expect("Failed to create umem"); - - // A socket represents a standard means of reading/writing packets from/to a network interface. - // - // This is the main handle for interacting with the network data, if needed this can be split into its owner, rx, and tx - // components using the split() function. - let mut socket = Socket::builder(&args.if_name, args.queue) - .rx_ring_size(args.rx_ring_size) - .tx_ring_size(args.tx_ring_size) - .busy_poll(args.busy_poll) - .busy_poll_batch_size(args.busy_poll_batch_size) - .busy_poll_timeout_us(args.busy_poll_timeout_us) - .copy_mode(args.copy_mode) - .build_tokio(&mut xdp_ctx, umem.owner().clone()) - .expect("Failed to create socket"); - - 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"); - - // We are benchmarking receive throughput so lets give all of our frames to the kernel. - // - // This is where the first part of the invariant above comes into play because our num_frames is the same as the fill ring size, - // the initial_frames buffer will be empty at the end of this call. - let mut frames = umem.init_buffer::().unwrap(); - umem.process_fill_queue(&mut frames, &[socket.fd()]) - .await - .unwrap(); - - while !exit.load(Ordering::Relaxed) { - // Start by reading some frames from the socket. The result of this call is guaranteed to be between 1 and the batch size. - match socket.recv(&mut frames).await { - Ok(received) => { - debug_assert!( - received > 0 && received <= args.busy_poll_batch_size as u32, - "Received more frames than the batch size or 0 frames, this should never happen!" - ); - } - Err(_) => { - continue; - } - } - - // You now have a batch of raw frames, at this level this is an L2 frame, almost assuredly Ethernet based. - for frame in frames.iter_frames_mut() { - // Do something with the frame! - // - // Note checking the frame.is_fragment() is needed, iff we have fragmentation enabled. In this mode it is possible - // to receive a fragment of a packat if the MTU of the device is larger than the frame size. - stats.update(frame.len(), frame.is_fragment()); - } - - // You now have two options in general: - // - Update the packet data in some way in place, then send all or some number of frames back out the socket achieving true end to end zero copy. - // - See the [examples/echo.rs](examples/echo.rs) example for a full end to end zero copy example. - // - Return the data to the kernel by means of the fill queue. Which is what will be doing here. - - // Process the frame buffer, this will consume the entire buffer and submit them to the fill queue. - umem.process_fill_queue(&mut frames, &[socket.fd()]) - .await - .expect("Failed to process fill queue"); - - // Maybe print the stats for this iteration. - stats.maybe_print(); - } -} diff --git a/examples/tokio-tx.rs b/examples/tokio-tx.rs deleted file mode 100644 index 7284b96..0000000 --- a/examples/tokio-tx.rs +++ /dev/null @@ -1,144 +0,0 @@ -use std::{ - ops::{Deref, DerefMut}, - sync::{ - Arc, - atomic::{AtomicBool, Ordering}, - }, -}; - -use clap::Parser; - -use libvoid::xdp::{ - context::XdpContext, - frame::{BasicFrameBuffer, FrameBuffer}, - socket::Socket, - umem::Umem, -}; - -mod common; -use common::{BaseArgs, GeneratorArgs, Stats, build_frame}; - -#[derive(Parser)] -#[command(author, version, about, long_about = None)] -struct Args { - #[command(flatten)] - base: BaseArgs, - #[command(flatten)] - generator: GeneratorArgs, -} - -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 = "multi_thread")] -async fn main() { - let mut stats = Stats::new(); - let args = Args::parse(); - - // Every application starts with setting up an XdpContext, this loads the XDP kernel program and attaches it to the named - // interface. - let mut xdp_context = XdpContext::builder(&args.if_name) - .attach_mode(args.attach_mode) - .enable_fragmentation(args.enable_fragmentation) - .build() - .expect("Failed to create xdp context"); - - // A Umem is created to manage sharing memory buffers between the kernel and user space. - // You will need one of these for each unique device you want to use. - // - // Each Umem comes associated with three key components: - // - Owner (umem) > The owner of the UMEM, this is used to create frames and is responsible for cleaning up the UMEM once all is said and done. - // - Fill Queue (fq) > Used to pass frames from user space to the kernel for reading packet data into. - // - Completion Queue (cq) > Used to retrieve frames from the kernel after transmission finishes. - let mut umem = Umem::builder() - .completion_ring_size(args.completion_ring_size) - .fill_ring_size(args.fill_ring_size) - .frame_size(args.frame_size) - .busy_poll(args.busy_poll) - .num_frames(args.busy_poll_batch_size) - .huge_tables(args.huge_tables) - .unaligned(args.unaligned) - .build_tokio(&mut xdp_context) - .expect("Failed to create umem"); - - // A socket represents a standard means of reading/writing packets from/to a network interface. - // - // This is the main handle for interacting with the network data, if needed this can be split into its owner, rx, and tx - // components using the split() function. - let mut socket = Socket::builder(&args.if_name, args.queue) - .rx_ring_size(args.rx_ring_size) - .tx_ring_size(args.tx_ring_size) - .busy_poll_batch_size(args.busy_poll_batch_size) - .busy_poll_timeout_us(args.busy_poll_timeout_us) - .busy_poll(args.busy_poll) - .copy_mode(args.copy_mode) - .enable_fragmentation(args.enable_fragmentation) - .build_tokio(&mut xdp_context, umem.owner().clone()) - .expect("Failed to create socket"); - - // Always catch SIGINT/SIGTERM to ensure we clean up properly, we have a running XDP program attached to the interface. - // - // Note: In other words its very important to ensure that the Drop impl for XdpContext is run to detach the XDP program from - // the interface, OR manually call detach(). - 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"); - - // Generate our mock UDP packet to send. - let packet_data = build_frame(&args.generator); - - println!("Socket created, sending packets..."); - let mut write_frames = umem.init_buffer::().unwrap(); - let frames = write_frames.num_frames(); - while !exit.load(Ordering::Relaxed) { - // Copy the data into the frames, this is intentionally in the loop to show performance of a real world application. - // Since generally at some point a copy into the frame is needed from userspace, how this copy is done is up to the caller. - // - // Note: Technically the frames SHOULD be untouched after each iteration, so in theory if the frame size/fragmentation is known - // here you can just set the frame metadata and avoid the copy if done outside the loop and you are simply re-sending the same - // data.... But be ware this is where dragons live... - for frame in write_frames.iter_frames_mut() { - frame.copy_from(&packet_data); - } - - // Send the prepared frames to the socket. - // - // Note this will completely consume the input buffer. - match socket.send(&mut write_frames).await { - Ok(sent) => { - debug_assert!( - sent == frames as u32, - "Sent a different number of frames than the input buffer, this should never happen!" - ); - - stats.update_batch(sent as usize, packet_data.len()); - } - Err(_) => { - // We would have blocked. - continue; - } - }; - - umem.process_completion_queue(&mut write_frames) - .await - .unwrap(); - - stats.maybe_print(); - } -} 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/profiling-http-server b/profiling-http-server new file mode 100755 index 0000000..3024b0c Binary files /dev/null and b/profiling-http-server differ diff --git a/src/lib.rs b/src/lib.rs index 307b234..379fa94 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1 +1,7 @@ +#![allow(clippy::too_many_arguments)] +#![allow(clippy::module_inception)] + +pub mod net; +pub mod netlink; +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..09614c5 --- /dev/null +++ b/src/net/checksum/common.rs @@ -0,0 +1,255 @@ +use crate::net::wire::ip::{Ipv4Address, Ipv6Address}; + +/// Sum all 16-bit words in `data`, handling trailing bytes. +#[inline] +pub 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. +#[inline] +pub 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)); + } + } + + // NEON fast path for large data on aarch64. + #[cfg(target_arch = "aarch64")] + { + if len - i >= 32 { + // Safety: aarch64 always has NEON. + return unsafe { super::neon::sum_words_carry(&data[i..], sum) }; + } + } + + // TODO: Implement a SIMD-based implementation for x86_64. + + // Fallback path just incase the SIMD implementation is not available, or the data is so small it would cost more to do SIMD. + super::std::sum_words_carry(&data[i..], sum) +} + +/// Fold 64-bit running sum to 16 bits, then one's-complement. +#[inline] +pub 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 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 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 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 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]); + } + + #[test] + fn sum_words_large_data_exercises_wide_loop() { + let data: Vec = (0u8..64).collect(); + let sum = sum_words(&data); + let mut expected = 0u64; + for chunk in data.chunks(2) { + expected += ((chunk[0] as u64) << 8) | (chunk[1] as u64); + } + assert_eq!(sum, expected); + } + + #[test] + fn sum_words_carry_pending_consumed_by_next_byte() { + let (sum, pending) = sum_words_carry(&[0xCD], 0, Some(0xAB)); + assert_eq!(sum, 0xABCD); + assert_eq!(pending, None); + } + + #[test] + fn sum_words_carry_single_byte_becomes_pending() { + let (sum, pending) = sum_words_carry(&[0x42], 100, None); + assert_eq!(sum, 100); + assert_eq!(pending, Some(0x42)); + } + + #[test] + fn fold_checksum_near_u16_max() { + let result = fold_checksum(0xFFFF); + assert_eq!(result, 0x0000); + } + + #[test] + fn fold_checksum_with_carry() { + let result = fold_checksum(0x1FFFE); + assert_eq!(result, 0x0000); + } + + #[test] + fn fold_and_verify_correct_checksum() { + let data = [0x45, 0x00, 0x00, 0x3C, 0x1C, 0x46, 0x40, 0x00, 0x40, 0x06]; + let sum = sum_words(&data); + let cksum = fold_checksum(sum); + let full_sum = sum + cksum as u64; + assert!(fold_and_verify(full_sum, 0x0000)); + } + + #[test] + fn sum_words_carry_three_slices() { + let full = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]; + let expected = sum_words(&full); + let (s1, p1) = sum_words_carry(&full[0..3], 0, None); + let (s2, p2) = sum_words_carry(&full[3..5], s1, p1); + let (mut s3, p3) = sum_words_carry(&full[5..7], s2, p2); + if let Some(hi) = p3 { + s3 += (hi as u64) << 8; + } + assert_eq!(s3, expected); + } +} diff --git a/src/net/checksum/compute.rs b/src/net/checksum/compute.rs new file mode 100644 index 0000000..95377bc --- /dev/null +++ b/src/net/checksum/compute.rs @@ -0,0 +1,291 @@ +use crate::net::wire::ip::{IpProtocols, IpVersion, Ipv6Address}; + +use super::{checksum_to_bytes, fold_checksum, 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 UDP checksum generically over any IP version (from port/payload parts). +#[inline] +pub fn compute_udp_checksum_ip( + src_addr: &V::Address, + dst_addr: &V::Address, + src_port: u16, + dst_port: u16, + udp_len: u16, + payload: &[u8], +) -> [u8; 2] { + let sum = V::pseudo_header_sum(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 TCP checksum generically over any IP version (from header/payload parts). +#[inline] +pub fn compute_tcp_checksum_ip( + src_addr: &V::Address, + dst_addr: &V::Address, + tcp_header_bytes: &[u8], + payload: &[u8], +) -> [u8; 2] { + let total_len = (tcp_header_bytes.len() + payload.len()) as u32; + let sum = V::pseudo_header_sum(src_addr, dst_addr, IpProtocols::Tcp, total_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::{ + ip::{Ipv4, Ipv4Address, Ipv6, Ipv6Address}, + 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_ip::(&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_ip::(&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_ip::(&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_ip::(&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..0be85c3 --- /dev/null +++ b/src/net/checksum/mod.rs @@ -0,0 +1,28 @@ +//! RFC 1071 implementation of the checksum algorithms. +//! +//! Checksumming utilities for all network protocols to use. These are SIMD accelerated where +//! possible. If the target architecture does not support the SIMD instructions, the fallback +//! implementation is used. + +mod common; +mod compute; +mod std; +mod verify; + +#[cfg(target_arch = "aarch64")] +mod neon; + +pub use common::{ + checksum_to_bytes, fold_and_verify, fold_checksum, pseudo_header_sum_v4, pseudo_header_sum_v6, + sum_words, sum_words_carry, +}; +pub use compute::{ + compute_icmpv6_checksum, compute_ipv4_checksum, compute_tcp_checksum_ip, + compute_udp_checksum_ip, +}; +pub use verify::{verify_ipv4_checksum, verify_tcp_checksum_ip, verify_udp_checksum_ip}; + +#[cfg(test)] +mod test_utils; +#[cfg(test)] +pub(crate) use test_utils::{compute_tcp_checksum, compute_udp_checksum, compute_udp_checksum_v6}; diff --git a/src/net/checksum/neon.rs b/src/net/checksum/neon.rs new file mode 100644 index 0000000..f136d81 --- /dev/null +++ b/src/net/checksum/neon.rs @@ -0,0 +1,90 @@ +//! NEON-accelerated RFC 1071 internet checksum. +//! +//! On little-endian aarch64, network (big-endian) data loaded via `vld1q_u8` +//! and reinterpreted as u16 gives byte-swapped words. We use `vrev16q_u8` to +//! byte-reverse within each 16-bit element, producing proper big-endian u16 +//! values that match the scalar path's intermediate representation exactly. +//! This is critical for `sum_words_carry` where partial sums are threaded +//! across fragments. + +use std::arch::aarch64::*; + +/// NEON-accelerated version of `sum_words_carry`. +/// +/// Processes 32 bytes per iteration using 128-bit NEON vectors. +/// Falls back to scalar for the tail (< 32 bytes). +/// +/// Produces the same intermediate `sum` values as the scalar path, +/// so partial sums can be safely mixed across fragments. +/// +/// This is meant to be consumed by the [common::sum_words_carry] function. +/// +/// # Safety +/// +/// Requires aarch64 NEON support (guaranteed on all AArch64 CPUs). +/// +/// [common::sum_words_carry]: crate::net::checksum::common::sum_words_carry +#[inline] +#[target_feature(enable = "neon")] +pub(crate) unsafe fn sum_words_carry(data: &[u8], mut sum: u64) -> (u64, Option) { + let len = data.len(); + let mut i = 0; + + // SAFETY: all NEON intrinsics below require aarch64 NEON, which is + // guaranteed on all AArch64 CPUs. Pointer arithmetic is bounded by + // `len` checks in the loop condition. + unsafe { + // NEON accumulator: 2x u64 lanes. + // We accumulate u32 values (widened from u16) so u64 lanes + // prevent overflow even for jumbo frames. + let mut acc = vdupq_n_u64(0); + + while i + 31 < len { + // Load 32 bytes as two 128-bit u8 vectors. + let v0 = vld1q_u8(data.as_ptr().add(i)); + let v1 = vld1q_u8(data.as_ptr().add(i + 16)); + + // Byte-reverse within each 16-bit element: + // [B0, B1, B2, B3, ...] -> [B1, B0, B3, B2, ...] + // On LE aarch64, reinterpreting [B1, B0] as u16 gives B0*256 + B1, + // which is the big-endian u16 value -- matching scalar exactly. + let rev0 = vrev16q_u8(v0); + let rev1 = vrev16q_u8(v1); + + // Reinterpret as u16 (now proper big-endian values). + let words0 = vreinterpretq_u16_u8(rev0); + let words1 = vreinterpretq_u16_u8(rev1); + + // Widen u16 -> u32 via pairwise add. + let sum32_0 = vpaddlq_u16(words0); + let sum32_1 = vpaddlq_u16(words1); + + // Widen u32 -> u64 and accumulate. + acc = vpadalq_u32(acc, sum32_0); + acc = vpadalq_u32(acc, sum32_1); + + i += 32; + } + + // Horizontal reduce: sum both u64 lanes into the scalar accumulator. + sum += vgetq_lane_u64(acc, 0) + vgetq_lane_u64(acc, 1); + } + + // Scalar tail: handle remaining bytes (< 32). + 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) +} diff --git a/src/net/checksum/std.rs b/src/net/checksum/std.rs new file mode 100644 index 0000000..ff470ee --- /dev/null +++ b/src/net/checksum/std.rs @@ -0,0 +1,81 @@ +//! RFC 1071 implementation of the checksum algorithm. + +/// Sum 16-bit words across a slice, carrying a pending odd byte in/out. +/// +/// This is meant to be consumed by the [common::sum_words_carry] function. +/// +/// [common::sum_words_carry]: crate::net::checksum::common::sum_words_carry +#[inline] +pub fn sum_words_carry(data: &[u8], mut sum: u64) -> (u64, Option) { + let len = data.len(); + let mut i = 0; + + // 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) +} 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..8bf3d68 --- /dev/null +++ b/src/net/checksum/verify.rs @@ -0,0 +1,265 @@ +use crate::net::wire::{ + ip::{IpProtocols, IpVersion}, + tcp::TCP_HEADER_LEN, + udp::UDP_HEADER_LEN, +}; + +use super::{ + common::{fold_and_verify, 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 TCP checksum for any IP version using the `IpVersion` trait. +/// +/// 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_ip( + src_addr: &V::Address, + dst_addr: &V::Address, + 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 = V::pseudo_header_sum( + src_addr, + dst_addr, + IpProtocols::Tcp, + tcp_segment.len() as u32, + ) + sum_words(tcp_segment); + fold_and_verify(sum, 0x0000) +} + +/// Verifies the UDP checksum for any IP version using the `IpVersion` trait. +/// +/// Returns `false` if the checksum field is zero (callers that need the +/// IPv4 zero-means-no-checksum behavior should check before calling). +/// Returns `true` if the one's complement sum of the pseudo-header and +/// full UDP segment yields the expected result. +#[inline] +pub fn verify_udp_checksum_ip( + src_addr: &V::Address, + dst_addr: &V::Address, + udp_segment: &[u8], +) -> bool { + if udp_segment.len() < UDP_HEADER_LEN { + return false; + } + if udp_segment[6] == 0 && udp_segment[7] == 0 { + return V::VERSION == 4; // IPv4 zero-means-no-checksum + } + let sum = V::pseudo_header_sum( + src_addr, + dst_addr, + IpProtocols::Udp, + udp_segment.len() as u32, + ) + sum_words(udp_segment); + fold_and_verify(sum, 0x0000) +} + +#[cfg(test)] +mod tests { + use crate::net::{ + checksum::test_utils::*, + wire::ip::{Ipv4, Ipv4Address, Ipv6, Ipv6Address}, + }; + + 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_ip::(&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_ip::(&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_ip::(&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_ip::(&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_ip::(&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_ip::(&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_ip::(&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_ip::(&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_ip::(&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_ip::(&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_ip::(&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_ip::(&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_ip::(&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_ip::(&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_ip::(&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_ip::(&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..e074ced --- /dev/null +++ b/src/net/fragment/mod.rs @@ -0,0 +1,20 @@ +//! IP fragmentation utilities. +//! +//! Provides utilities for fragmenting and reassembling IP packets of any version based on: +//! - [RFC-8200 (IPv6)](./dist/docs/rfc8200-ipv6.txt) +//! - [RFC-791 (IPv4)](./dist/docs/rfc791-ipv4.txt) +//! +//! This module handles the full fragmentation and defragmentation process, including checksum +//! and MTU handling. + +mod id; +mod pkt; +mod plan; +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..e2ad5d8 --- /dev/null +++ b/src/net/fragment/pkt.rs @@ -0,0 +1,408 @@ +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::BasicFrameBuffer; + 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); + } + + #[test] + fn empty_num_frames() { + let pkt = Packet::Empty; + assert_eq!(pkt.num_frames(), 0); + } + + #[test] + fn empty_len() { + let pkt = Packet::Empty; + assert_eq!(pkt.len(), 0); + } + + #[test] + fn empty_is_empty() { + let pkt = Packet::Empty; + assert!(pkt.is_empty()); + } + + #[test] + fn empty_frames_iter() { + let pkt = Packet::Empty; + assert_eq!(pkt.frames().count(), 0); + assert_eq!(pkt.frames().size_hint(), (0, Some(0))); + } + + #[test] + fn empty_frames_mut_iter() { + let mut pkt = Packet::Empty; + assert_eq!(pkt.frames_mut().count(), 0); + assert_eq!(pkt.frames_mut().size_hint(), (0, Some(0))); + } + + #[test] + fn empty_into_frames() { + let pkt = Packet::Empty; + let iter = pkt.into_frames(); + assert_eq!(iter.size_hint(), (0, Some(0))); + assert_eq!(iter.count(), 0); + } + + #[test] + fn from_empty_iterator() { + let frames: Vec> = vec![]; + let pkt = Packet::from(frames.into_iter()); + assert_eq!(pkt.num_frames(), 0); + assert!(pkt.is_empty()); + } + + #[test] + fn from_single_iterator() { + let mut buf = [0u8; 64]; + buf[0] = 0xCC; + let frames = vec![make_frame(&mut buf, 10)]; + let pkt = Packet::from(frames.into_iter()); + assert_eq!(pkt.num_frames(), 1); + assert!(!pkt.is_empty()); + } + + #[test] + fn drain_to_single() { + let mut buf = [0u8; 64]; + buf[0] = 0xDD; + let pkt = Packet::Single(make_frame(&mut buf, 10)); + let mut target = BasicFrameBuffer::new(4); + pkt.drain_to(&mut target); + assert_eq!(target.num_frames(), 1); + } + + #[test] + fn drain_to_multi() { + 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 mut target = BasicFrameBuffer::new(4); + pkt.drain_to(&mut target); + assert_eq!(target.num_frames(), 3); + } + + #[test] + fn drain_to_empty() { + let pkt: Packet<'_> = Packet::Empty; + let mut target = BasicFrameBuffer::new(4); + pkt.drain_to(&mut target); + assert_eq!(target.num_frames(), 0); + } +} diff --git a/src/net/fragment/plan.rs b/src/net/fragment/plan.rs new file mode 100644 index 0000000..f2c4129 --- /dev/null +++ b/src/net/fragment/plan.rs @@ -0,0 +1,129 @@ +/// Pre-computed fragment sizing plan for IP fragmentation. +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. + 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`. + 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..a90ebe5 --- /dev/null +++ b/src/net/fragment/reader.rs @@ -0,0 +1,886 @@ +use std::{collections::BTreeMap, 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. + 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>, + /// End offset of each fragment, keyed by start offset (parallel to `fragments`). + fragment_ends: BTreeMap, + /// Known when the final fragment (MF=0) arrives. + total_len: Option, + first_received: Instant, + /// Set when overlapping fragments are detected. The entry will be + /// discarded on the next eviction pass (or immediately for IPv6 per RFC 5722). + poisoned: bool, +} + +/// Result of attempting to insert a fragment. +enum InsertResult<'umem> { + /// Fragment accepted. + Ok, + /// Duplicate fragment (exact same offset already present). + Duplicate(Frame<'umem>), + /// Fragment overlaps with an existing fragment. + Overlap(Frame<'umem>), +} + +impl<'umem> ReassemblyEntry<'umem> { + fn new() -> Self { + Self { + fragments: BTreeMap::new(), + fragment_ends: BTreeMap::new(), + total_len: None, + first_received: Instant::now(), + poisoned: false, + } + } + + /// Insert a fragment by offset. Checks for overlaps with existing fragments. + fn insert( + &mut self, + offset: usize, + data_len: usize, + more_fragments: bool, + frame: Frame<'umem>, + ) -> InsertResult<'umem> { + // Exact duplicate check. + if self.fragments.contains_key(&offset) { + return InsertResult::Duplicate(frame); + } + + let end = offset + data_len; + + // Check for overlap with the fragment immediately before this one. + // If any fragment starts at or before `offset` and extends past it, we overlap. + if let Some((&prev_start, &prev_end)) = self.fragment_ends.range(..=offset).next_back() + && prev_start != offset + && prev_end > offset + { + return InsertResult::Overlap(frame); + } + + // Check for overlap with the fragment immediately after this one. + // If any fragment starts before `end`, we overlap with it. + if let Some((&next_start, _)) = self.fragment_ends.range(offset + 1..).next() + && next_start < end + { + return InsertResult::Overlap(frame); + } + + self.fragments.insert(offset, frame); + self.fragment_ends.insert(offset, end); + if !more_fragments { + self.total_len = Some(end); + } + InsertResult::Ok + } + + /// Check whether all fragments have been received with no gaps. + /// + /// Walks the sorted fragment_ends map and verifies that each fragment + /// starts exactly where the previous one ended, covering [0..total_len). + fn is_complete(&self) -> bool { + let total = match self.total_len { + Some(t) => t, + None => return false, + }; + + let mut expected = 0; + for (&start, &end) in &self.fragment_ends { + if start != expected { + return false; // gap detected + } + expected = end; + } + expected == total + } + + 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); + + match entry.insert(frag_offset_bytes, payload_len, more_fragments, frame) { + InsertResult::Duplicate(dup_frame) => { + rx_return.push(dup_frame); + return None; + } + InsertResult::Overlap(frame) => { + // For IPv4, discard the overlapping fragment and poison the entry. + entry.poisoned = true; + rx_return.push(frame); + return None; + } + InsertResult::Ok => {} + } + + if !entry.poisoned && 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); + + match entry.insert(frag_offset_bytes, data_len, more_fragments, frame) { + InsertResult::Duplicate(dup_frame) => { + rx_return.push(dup_frame); + return None; + } + InsertResult::Overlap(frame) => { + // RFC 5722: IPv6 overlapping fragments MUST cause the entire + // datagram to be silently discarded. Drop all held fragments. + let entry = self.ipv6_reassembly.remove(&remove_key).unwrap(); + rx_return.push(frame); + for (_, held_frame) in entry.fragments { + rx_return.push(held_frame); + } + return None; + } + InsertResult::Ok => {} + } + + 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); + } + + /// Overlapping IPv4 fragments are detected and the overlapping fragment + /// is rejected. The entry is poisoned so it never completes. + #[test] + fn ipv4_overlapping_fragments_rejected() { + let mut reader = FragmentReader::new(16); + let mut rx = BasicFrameBuffer::new(16); + + // Fragment 1: offset=0, 16 bytes payload, MF=1. + let mut buf1 = [0u8; 256]; + let len1 = build_ipv4_fragment(&mut buf1, 55, IpProtocols::Udp, 0, true, &[0xAA; 16]); + let f1 = Frame::new(0, &mut buf1, len1, false); + let result1 = reader.process_ipv4(f1, &mut rx); + assert!(result1.is_none(), "first fragment should not complete"); + assert_eq!(reader.pending_entries(), 1); + + // Fragment 2: offset=8 bytes (frag_offset_8=1), 16 bytes payload, MF=0. + // Overlaps bytes 8–15 with fragment 1. + let mut buf2 = [0u8; 256]; + let len2 = build_ipv4_fragment(&mut buf2, 55, IpProtocols::Udp, 1, false, &[0xBB; 16]); + let f2 = Frame::new(1, &mut buf2, len2, false); + let result2 = reader.process_ipv4(f2, &mut rx); + + // Overlapping fragment rejected, entry poisoned. + assert!(result2.is_none(), "overlapping fragment must not complete"); + assert_eq!(reader.pending_entries(), 1, "entry remains but is poisoned"); + // The overlapping frame was returned to rx_return. + assert_eq!(rx.num_frames(), 1, "overlapping frame returned to caller"); + } + + /// Fragments with a gap are not declared complete even when the sum of + /// their data lengths equals total_len. + #[test] + fn ipv4_gap_fragments_not_declared_complete() { + let mut reader = FragmentReader::new(16); + let mut rx = BasicFrameBuffer::new(16); + + // Fragment 1: offset=0, 8 bytes payload, MF=1. + let mut buf1 = [0u8; 256]; + let len1 = build_ipv4_fragment(&mut buf1, 66, IpProtocols::Udp, 0, true, &[0x11; 8]); + let f1 = Frame::new(0, &mut buf1, len1, false); + assert!(reader.process_ipv4(f1, &mut rx).is_none()); + + // Fragment 2: offset=24 bytes (frag_offset_8=3), 16 bytes payload, + // MF=0. Sets total_len = 24+16 = 40. + let mut buf2 = [0u8; 256]; + let len2 = build_ipv4_fragment(&mut buf2, 66, IpProtocols::Udp, 3, false, &[0x33; 16]); + let f2 = Frame::new(1, &mut buf2, len2, false); + assert!(reader.process_ipv4(f2, &mut rx).is_none()); + + // Fragment 3: offset=16 bytes (frag_offset_8=2), 16 bytes payload, + // MF=1. Gap at [8..16) remains unfilled. + let mut buf3 = [0u8; 256]; + let len3 = build_ipv4_fragment(&mut buf3, 66, IpProtocols::Udp, 2, true, &[0x22; 16]); + let f3 = Frame::new(2, &mut buf3, len3, false); + let result = reader.process_ipv4(f3, &mut rx); + + // Contiguity check detects the gap — not declared complete. + assert!( + result.is_none(), + "fragments with gap at [8..16) must not be declared complete" + ); + assert_eq!( + reader.pending_entries(), + 1, + "entry stays pending until gap is filled" + ); + } + + #[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..10e6b17 --- /dev/null +++ b/src/net/fragment/transport.rs @@ -0,0 +1,138 @@ +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]); + } + + #[test] + fn tcp_header_as_transport_protocol() { + let hdr: TcpHeader = unsafe { std::mem::zeroed() }; + assert_eq!(hdr.protocol(), IpProtocols::Tcp); + } + + #[test] + fn tcp_header_as_transport_len() { + let hdr: TcpHeader = unsafe { std::mem::zeroed() }; + assert_eq!( + ::header_len(&hdr), + TCP_HEADER_LEN + ); + } + + #[test] + fn tcp_header_as_transport_write() { + let mut hdr: TcpHeader = unsafe { std::mem::zeroed() }; + hdr.src_port = [0x00, 0x50]; + hdr.dst_port = [0x1F, 0x90]; + let mut buf = [0u8; TCP_HEADER_LEN]; + hdr.write_to(&mut buf); + assert_eq!(buf[0..2], [0x00, 0x50]); + assert_eq!(buf[2..4], [0x1F, 0x90]); + assert_eq!(buf.len(), TCP_HEADER_LEN); + } +} diff --git a/src/net/fragment/writer.rs b/src/net/fragment/writer.rs new file mode 100644 index 0000000..6e4a7a9 --- /dev/null +++ b/src/net/fragment/writer.rs @@ -0,0 +1,678 @@ +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]) + } + + #[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"), + } + } + + #[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"), + } + } + + #[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"), + } + } + + #[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"), + } + } + + #[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); + } + + 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"), + } + } + + #[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..64c7bd8 --- /dev/null +++ b/src/net/handler/ethernet.rs @@ -0,0 +1,360 @@ +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}, +}; + +/// A ZST representing the entrypoint of the handlers and handles Ethernet frame parsing. +pub struct EthernetHandler; + +impl EthernetHandler { + /// Handle an incoming raw Ethernet frame, dispatching to the appropriate protocol handler based on the EtherType. + 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: &mut 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}, + handler::{tcp::TcpHandler, udp::UdpHandler}, + 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::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, mut pmtu) = new_handlers(); + let (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, + &mut pmtu, + now, + &mut BasicFrameBuffer::new(4), + &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, mut pmtu) = new_handlers(); + let (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, + &mut pmtu, + now, + &mut BasicFrameBuffer::new(4), + &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, mut pmtu) = new_handlers(); + let (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, + &mut pmtu, + now, + &mut BasicFrameBuffer::new(4), + &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, mut pmtu) = new_handlers(); + let (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, + &mut pmtu, + now, + &mut BasicFrameBuffer::new(4), + &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, mut pmtu) = new_handlers(); + let (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, + &mut pmtu, + now, + &mut BasicFrameBuffer::new(4), + &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, mut pmtu) = + new_handlers(); + let (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, + &mut pmtu, + now, + &mut BasicFrameBuffer::new(4), + &mut rx, + &mut tx, + ); + + let total = 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..a1d6c66 --- /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: &mut 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, + &mut 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, + &mut 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, + &mut 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, + &mut 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, + &mut 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, + &mut 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 mut pmtu = PmtuCache::new(); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + handle_icmpv4(frame, &mut 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..70d91f3 --- /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: &mut 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, + &mut 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, + &mut 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, + &mut 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, + &mut 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, + &mut 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 mut 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, + &mut 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..68afe55 --- /dev/null +++ b/src/net/handler/ipv4.rs @@ -0,0 +1,567 @@ +use coarsetime::Instant; + +use crate::{ + net::{ + NeighborHandler, 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: &NeighborHandler, + pmtu: &mut 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 coarsetime::Duration; + + use crate::{ + net::{ + 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", 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 tcp = new_tcp_handler(); + let nh = new_neighbor_handler(); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let frame = Frame::new(0, &mut data, 30, false); + + handler.handle( + frame, + &mut udp, + &mut tcp, + &nh, + &mut PmtuCache::new(), + Instant::now(), + &mut BasicFrameBuffer::new(4), + &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 tcp = new_tcp_handler(); + let nh = new_neighbor_handler(); + 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, + &mut udp, + &mut tcp, + &nh, + &mut PmtuCache::new(), + Instant::now(), + &mut BasicFrameBuffer::new(4), + &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 tcp = new_tcp_handler(); + let nh = new_neighbor_handler(); + 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, + &mut udp, + &mut tcp, + &nh, + &mut PmtuCache::new(), + Instant::now(), + &mut BasicFrameBuffer::new(4), + &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 tcp = new_tcp_handler(); + let nh = new_neighbor_handler(); + 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, + &mut udp, + &mut tcp, + &nh, + &mut PmtuCache::new(), + Instant::now(), + &mut BasicFrameBuffer::new(4), + &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 tcp = new_tcp_handler(); + let nh = new_neighbor_handler(); + 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, + &mut udp, + &mut tcp, + &nh, + &mut PmtuCache::new(), + Instant::now(), + &mut BasicFrameBuffer::new(4), + &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 tcp = new_tcp_handler(); + let nh = new_neighbor_handler(); + 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, + &mut udp, + &mut tcp, + &nh, + &mut PmtuCache::new(), + Instant::now(), + &mut BasicFrameBuffer::new(4), + &mut rx, + &mut tx, + ); + assert_eq!(rx.num_frames(), 0); + assert_eq!(tx.num_frames(), 0); + assert_eq!(udp.pending_reassembly(), 1); + } + + #[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 tcp = new_tcp_handler(); + let nh = new_neighbor_handler(); + 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, + &mut udp, + &mut tcp, + &nh, + &mut PmtuCache::new(), + Instant::now(), + &mut BasicFrameBuffer::new(4), + &mut rx, + &mut tx, + ); + + assert_eq!(rx.num_frames(), 0); + assert_eq!(tx.num_frames(), 1); + } + + #[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 tcp = new_tcp_handler(); + let nh = new_neighbor_handler(); + 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, + &mut udp, + &mut tcp, + &nh, + &mut PmtuCache::new(), + Instant::now(), + &mut BasicFrameBuffer::new(4), + &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 tcp = new_tcp_handler(); + let nh = new_neighbor_handler(); + 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, + &mut udp, + &mut tcp, + &nh, + &mut PmtuCache::new(), + Instant::now(), + &mut BasicFrameBuffer::new(4), + &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..5f5ecc5 --- /dev/null +++ b/src/net/handler/ipv6.rs @@ -0,0 +1,883 @@ +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(crate) 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: &mut 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 } => { + 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 { + 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}, + handler::{tcp::TcpHandler, udp::UdpHandler}, + 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_udp_handler<'umem>() -> UdpHandler<'umem> { + UdpHandler::new(256, false) + } + + fn new_tcp_handler() -> TcpHandler { + TcpHandler::new(false, false) + } + + /// 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 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, + &mut PmtuCache::new(), + Instant::now(), + &mut BasicFrameBuffer::new(4), + &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 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, + &mut PmtuCache::new(), + Instant::now(), + &mut BasicFrameBuffer::new(4), + &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 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, + &mut PmtuCache::new(), + Instant::now(), + &mut BasicFrameBuffer::new(4), + &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 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, + &mut PmtuCache::new(), + Instant::now(), + &mut BasicFrameBuffer::new(4), + &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() { + 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 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, + &mut PmtuCache::new(), + Instant::now(), + &mut BasicFrameBuffer::new(4), + &mut rx, + &mut tx, + ); + assert_eq!(rx.num_frames(), 1); + assert_eq!(tx.num_frames(), 0); + } + + #[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 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, + &mut PmtuCache::new(), + Instant::now(), + &mut BasicFrameBuffer::new(4), + &mut rx, + &mut tx, + ); + + assert_eq!(rx.num_frames(), 0); + assert_eq!(tx.num_frames(), 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 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, + &mut PmtuCache::new(), + Instant::now(), + &mut BasicFrameBuffer::new(4), + &mut rx, + &mut tx, + ); + assert_eq!(rx.num_frames(), 0); + assert_eq!(tx.num_frames(), 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 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, + &mut PmtuCache::new(), + Instant::now(), + &mut BasicFrameBuffer::new(4), + &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 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, + &mut PmtuCache::new(), + Instant::now(), + &mut BasicFrameBuffer::new(4), + &mut rx, + &mut tx, + ); + assert_eq!(rx.num_frames(), 1); + assert_eq!(tx.num_frames(), 0); + } + + #[test] + fn valid_udp_routed_to_socket() { + let eth_len = size_of::(); + let udp_len = UDP_HEADER_LEN; + let payload_len = udp_len as u16; + let frame_len = eth_len + IPV6_HEADER_LEN + udp_len; + let mut data = vec![0u8; frame_len]; + + data[12] = 0x86; + data[13] = 0xDD; + data[14] = 0x60; + data[18..20].copy_from_slice(&payload_len.to_be_bytes()); + data[20] = IpProtocols::Udp; + 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 udp_off = eth_len + IPV6_HEADER_LEN; + data[udp_off + 4..udp_off + 6].copy_from_slice(&(udp_len as u16).to_be_bytes()); + let cksum = compute_udp_checksum_v6(&REMOTE_IP, &LOCAL_IP, &data[udp_off..frame_len]); + data[udp_off + 6] = cksum[0]; + data[udp_off + 7] = cksum[1]; + + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let mut udp = UdpHandler::new(256, false); + let rx_queue = udp.bind(IpAddress::V6(LOCAL_IP), 0, 256).unwrap(); + let mut tcp = new_tcp_handler(); + 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, + &mut PmtuCache::new(), + Instant::now(), + &mut BasicFrameBuffer::new(4), + &mut rx, + &mut tx, + ); + + assert_eq!(rx.num_frames(), 0); + assert_eq!(tx.num_frames(), 0); + assert_eq!(rx_queue.len(), 1); + } +} 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..a2f3b21 --- /dev/null +++ b/src/net/handler/tcp/congestion.rs @@ -0,0 +1,299 @@ +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..77e74b1 --- /dev/null +++ b/src/net/handler/tcp/connection.rs @@ -0,0 +1,206 @@ +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::congestion::CubicState; +use super::handler::INITIAL_RTO_MS; +use super::recovery::{FRtoState, PrrState, SackRecovery}; +use super::ring_buffer::RingBuffer; +use super::segment::SegmentBuilder; +use super::send_tracker::SendReady; +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<(usize, LocalQueue), 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<(usize, LocalQueue), BindError> { + let id = ConnectionId { + local_addr, + local_port, + remote_addr, + remote_port, + }; + + // Check for existing connection with same 4-tuple. + if self.connection_map.contains_key(&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, + ); + + let key = self.insert_connection(tcb); + // New connection has a retransmit timer — mark for send tracking. + self.send_tracker.mark(SendReady(key)); + Ok((key, event_queue)) + } + + /// Initiate a graceful close for a connection. + pub fn initiate_close(&mut self, key: usize) { + if let Some(tcb) = self.connections.get_mut(key) { + if tcb.pending_fin + || (tcb.state != TcpState::Established && tcb.state != TcpState::CloseWait) + { + return; + } + + tcb.set_pending_fin(); + self.send_tracker.mark(SendReady(key)); + 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..36d5936 --- /dev/null +++ b/src/net/handler/tcp/handler.rs @@ -0,0 +1,175 @@ +use rustc_hash::FxHashMap; +use slab::Slab; + +use crate::{ + net::wire::{ethernet::MacAddress, tcp::flags}, + xdp::frame::FrameBuffer, +}; + +use super::{ + isn::IsnGenerator, + listener, + segment::SegmentBuilder, + send_tracker::SendTracker, + 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: Slab, + pub(super) connection_map: FxHashMap, + pub(super) listeners: Vec, + pub(super) isn_generator: IsnGenerator, + pub(crate) send_tracker: SendTracker, + pub(super) rx_offload: bool, + pub(super) tx_offload: bool, +} + +impl TcpHandler { + pub fn new(rx_offload: bool, tx_offload: bool) -> Self { + Self { + connections: Slab::new(), + connection_map: FxHashMap::default(), + listeners: Vec::new(), + isn_generator: IsnGenerator::new(), + send_tracker: SendTracker::new(), + rx_offload, + tx_offload, + } + } + + /// Get a reference to the connection for a given ConnectionId. + pub fn get_connection(&self, id: &ConnectionId) -> Option<&Tcb> { + let &key = self.connection_map.get(id)?; + self.connections.get(key) + } + + /// Get a mutable reference to the connection for a given ConnectionId. + pub fn get_connection_mut(&mut self, id: &ConnectionId) -> Option<&mut Tcb> { + let &key = self.connection_map.get(id)?; + self.connections.get_mut(key) + } + + /// Look up slab key for a ConnectionId. + pub fn connection_key(&self, id: &ConnectionId) -> Option { + self.connection_map.get(id).copied() + } + + /// Get a reference to the connection by slab key. + #[inline(always)] + pub fn get_by_key(&self, key: usize) -> Option<&Tcb> { + self.connections.get(key) + } + + /// Get a mutable reference to the connection by slab key. + #[inline(always)] + pub fn get_by_key_mut(&mut self, key: usize) -> Option<&mut Tcb> { + self.connections.get_mut(key) + } + + /// Insert a new connection, returning its slab key. + pub fn insert_connection(&mut self, tcb: Tcb) -> usize { + let id = tcb.id; + let key = self.connections.insert(tcb); + self.connection_map.insert(id, key); + key + } + + /// Remove a connection by slab key. + pub fn remove_connection_by_key(&mut self, key: usize) -> Option { + if self.connections.contains(key) { + let tcb = self.connections.remove(key); + self.connection_map.remove(&tcb.id); + Some(tcb) + } else { + None + } + } + + /// Remove a connection by ConnectionId and send RST if synchronized. + 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>, + ) { + let Some(&key) = self.connection_map.get(id) else { + return; + }; + let should_rst = self.connections.get(key).map(|tcb| { + let needs_rst = tcb.state.is_synchronized() || tcb.state == TcpState::SynReceived; + (needs_rst, tcb.snd_nxt) + }); + if let Some((true, snd_nxt)) = should_rst { + SegmentBuilder::build_rst( + id.local_addr, + id.remote_addr, + id.local_port, + id.remote_port, + 0, + snd_nxt, + flags::ACK, + 0, + src_mac, + dst_mac, + self.tx_offload, + free_frames, + tx_return, + ); + } + self.send_tracker.unmark(key); + self.remove_connection_by_key(key); + } + + /// Write data to a connection's send buffer and mark it for sending. + /// Returns the number of bytes written. + pub fn write_to_send_buffer(&mut self, key: usize, data: &[u8]) -> Option { + let n = self.connections.get_mut(key)?.send_buffer.write(data); + if n > 0 { + self.send_tracker.mark(super::send_tracker::SendReady(key)); + } + Some(n) + } + + /// Transfer data from recv_buffer to send_buffer and mark for sending. + /// Returns the number of bytes transferred, or None if connection not found. + pub fn splice_buffers(&mut self, key: usize, max_len: usize) -> Option<(usize, bool)> { + let tcb = self.connections.get_mut(key)?; + let n = tcb.recv_buffer.transfer(&mut tcb.send_buffer, max_len); + if n > 0 { + self.send_tracker.mark(super::send_tracker::SendReady(key)); + } + let is_remote_closed = tcb.state.is_remote_closed(); + Some((n, is_remote_closed)) + } + + /// Get the first connection (test helper). + #[cfg(test)] + pub fn first_connection(&self) -> &Tcb { + self.connections.iter().next().unwrap().1 + } + + /// Get the first connection mutably (test helper). + #[cfg(test)] + pub fn first_connection_mut(&mut self) -> &mut Tcb { + self.connections.iter_mut().next().unwrap().1 + } + + /// Get the first connection's slab key (test helper). + #[cfg(test)] + pub fn first_connection_key(&self) -> usize { + self.connections.iter().next().unwrap().0 + } +} diff --git a/src/net/handler/tcp/inbound/established.rs b/src/net/handler/tcp/inbound/established.rs new file mode 100644 index 0000000..f6ad087 --- /dev/null +++ b/src/net/handler/tcp/inbound/established.rs @@ -0,0 +1,896 @@ +use coarsetime::Instant; + +use crate::{ + net::wire::tcp::flags, + xdp::frame::{Frame, FrameBuffer}, +}; + +use super::super::handler::TcpHandler; +use super::super::options::ParsedOptions; +use super::super::recovery::FRtoAction; +use super::super::segment::SegmentBuilder; +use super::super::state::TcpState; +use super::super::tcb::{Tcb, TcpEvent}; + +use super::segment::{PostAction, is_segment_acceptable}; + +impl TcpHandler { + /// Handle RST in established state (RFC 5961). + /// Returns PostAction indicating if connection should be removed. + #[inline(never)] + fn handle_rst_established<'umem>( + tcb: &mut Tcb, + key: usize, + seg_seq: u32, + tsval: u32, + ack_flags: u8, + src_mac: crate::net::wire::ethernet::MacAddress, + dst_mac: crate::net::wire::ethernet::MacAddress, + tx_offload: bool, + free_frames: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) -> PostAction { + if seg_seq == tcb.rcv_nxt { + // Exact match: reset connection. + tcb.event_queue.push(TcpEvent::Reset); + return PostAction::RemoveAndDecrement(key); + } + // In-window but not exact: send challenge ACK, drop segment. + 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, + tx_offload, + free_frames, + tx_return, + ); + PostAction::None + } + + /// Handle SYN in established state — send challenge ACK (RFC 5961). + #[inline(never)] + fn handle_syn_established<'umem>( + tcb: &mut Tcb, + tsval: u32, + ack_flags: u8, + src_mac: crate::net::wire::ethernet::MacAddress, + dst_mac: crate::net::wire::ethernet::MacAddress, + tx_offload: bool, + free_frames: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) { + 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, + 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>( + tcb: &mut Tcb, + 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, + tx_offload: bool, + free_frames: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) { + let rcv_nxt = tcb.rcv_nxt; + let offset = seg_seq.wrapping_sub(rcv_nxt) as usize; + let payload = &frame[payload_offset..payload_offset + payload_len]; + 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, + tx_offload, + free_frames, + tx_return, + ); + } + + pub(super) fn process_established<'umem>( + tcb: &mut Tcb, + key: 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, + tx_offload: bool, + free_frames: &mut impl FrameBuffer<'umem>, + rx_return: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) -> PostAction { + 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 tcb.ecn_enabled && ecn_bits == 0x03 { + tcb.ecn_ce_received = true; + } + + // ECN CWR processing: peer acknowledges our ECE by sending CWR. + if tcb.ecn_enabled && seg_flags & flags::CWR != 0 { + tcb.ecn_ce_received = false; + } + + // Compute ACK flags: include ECE when we need to signal congestion back. + let ack_flags = if tcb.ecn_ce_received { + flags::ACK | flags::ECE + } else { + flags::ACK + }; + + // Fast-path: common case — in-order data + valid new ACK, no special flags. + { + 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 tcb.ts_enabled { + if let Some((ts_val, _)) = opts.timestamp { + let ts_diff = ts_val.wrapping_sub(tcb.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 tcb.recv_buffer.free_space() == 0 { + // No window space — fall through to slow path. + } else { + // Update ts_recent. + 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; + + // RFC 6298 §5.3: manage retransmit timer on new ACK. + tcb.rto_backoff = 0; + if tcb.snd_una == tcb.snd_nxt { + tcb.retransmit_deadline = None; + } else { + tcb.retransmit_deadline = + Some(now + coarsetime::Duration::from_millis(tcb.rto)); + } + + // 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 PostAction::None; + } + } + } + // 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 tcb.recv_buffer.free_space() > 0 { + // 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; + + // RFC 6298 §5.3: manage retransmit timer on new ACK. + tcb.rto_backoff = 0; + if tcb.snd_una == tcb.snd_nxt { + tcb.retransmit_deadline = None; + } else { + tcb.retransmit_deadline = + Some(now + coarsetime::Duration::from_millis(tcb.rto)); + } + + // 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 PostAction::None; + } + // No window space — fall through to slow path. + } + } + } + // Fall through to existing slow path. + + // PAWS check (RFC 7323 §5). + if tcb.ts_enabled + && let Some((tsval, _)) = opts.timestamp + { + // 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 PostAction::None; + } + 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, + tx_offload, + free_frames, + tx_return, + ); + rx_return.push(frame); + return PostAction::None; + } + } + } + + // Segment acceptability check (RFC 9293 §3.10.7.4). + { + 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 PostAction::None; + } + 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, + tx_offload, + free_frames, + tx_return, + ); + rx_return.push(frame); + return PostAction::None; + } + } + + // Step 2: RST check (RFC 5961). + if seg_flags & flags::RST != 0 { + let action = Self::handle_rst_established( + tcb, + key, + seg_seq, + tsval, + ack_flags, + src_mac, + dst_mac, + tx_offload, + free_frames, + tx_return, + ); + rx_return.push(frame); + return action; + } + + // Step 4: SYN check (RFC 5961 — challenge ACK for SYN in synchronized state). + if seg_flags & flags::SYN != 0 { + Self::handle_syn_established( + tcb, + tsval, + ack_flags, + src_mac, + dst_mac, + tx_offload, + free_frames, + tx_return, + ); + rx_return.push(frame); + return PostAction::None; + } + + // Step 5 preamble: if ACK bit is off, drop segment and return. + if seg_flags & flags::ACK == 0 { + rx_return.push(frame); + return PostAction::None; + } + + // Update ts_recent from incoming segment. + { + 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 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; + 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; + + // RFC 6298 §5.3: manage retransmit timer on new ACK. + tcb.rto_backoff = 0; + if tcb.snd_una == tcb.snd_nxt { + tcb.retransmit_deadline = None; + } else { + tcb.retransmit_deadline = + Some(now + coarsetime::Duration::from_millis(tcb.rto)); + } + + // 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(); + } + + // 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 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, + tx_offload, + free_frames, + tx_return, + ); + rx_return.push(frame); + return PostAction::None; + } else if seg_ack == snd_una && payload_len == 0 { + // Duplicate ACK. + 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 rcv_nxt = tcb.rcv_nxt; + + if seg_seq == rcv_nxt { + // In-order data. + let payload = &frame[payload_offset..payload_offset + payload_len]; + 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 = tcb.rcv_nxt; + if let Some(&ooo_len) = tcb.ooo_ranges.get(¤t_nxt) { + tcb.ooo_ranges.remove(¤t_nxt); + tcb.recv_buffer.commit(ooo_len as usize); + tcb.rcv_nxt = current_nxt.wrapping_add(ooo_len); + } else { + break; + } + } + + // Defer ACK (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)); + } + } else if crate::net::wire::tcp::seq_lt(rcv_nxt, seg_seq) { + // Out-of-order data. + Self::handle_ooo_data( + tcb, + &frame, + seg_seq, + tsval, + payload_offset, + payload_len, + ack_flags, + src_mac, + dst_mac, + tx_offload, + free_frames, + tx_return, + ); + } else { + // Duplicate data (seg_seq < rcv_nxt) — just ACK. + 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, + tx_offload, + free_frames, + tx_return, + ); + } + } + + // Step 4: Process FIN flag. + if seg_flags & flags::FIN != 0 && seg_seq.wrapping_add(payload_len as u32) == tcb.rcv_nxt { + 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, + tx_offload, + free_frames, + tx_return, + ); + } + + // Always return incoming frame to rx_return. + rx_return.push(frame); + PostAction::None + } +} diff --git a/src/net/handler/tcp/inbound/listen.rs b/src/net/handler/tcp/inbound/listen.rs new file mode 100644 index 0000000..778e8ec --- /dev/null +++ b/src/net/handler/tcp/inbound/listen.rs @@ -0,0 +1,238 @@ +use std::collections::BTreeMap; + +use coarsetime::Instant; +use rustc_hash::FxHashMap; +use slab::Slab; + +use crate::{ + net::{ + socket::LocalQueue, + wire::{ + ip::IpAddress, + tcp::{flags, parse_mss, parse_sack_permitted, parse_timestamp, parse_window_scale}, + }, + }, + xdp::frame::FrameBuffer, +}; + +use super::super::congestion::CubicState; +use super::super::handler::{INITIAL_RTO_MS, TcpHandler}; +use super::super::isn::IsnGenerator; +use super::super::listener::ListenEntry; +use super::super::recovery::{FRtoState, PrrState, SackRecovery}; +use super::super::ring_buffer::RingBuffer; +use super::super::segment::SegmentBuilder; +use super::super::state::TcpState; +use super::super::tcb::{ + ConnectionId, DEFAULT_RCV_MSS, DEFAULT_RCV_WND, DEFAULT_RCV_WSCALE, TS_OPTION_LEN, Tcb, +}; + +impl TcpHandler { + pub(super) fn process_listen<'umem>( + connections: &mut Slab, + connection_map: &mut FxHashMap, + listeners: &mut [ListenEntry], + isn_generator: &mut IsnGenerator, + 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, + tx_offload: bool, + 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, + 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 = &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 = 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, + tx_offload, + free_frames, + tx_return, + ); + + let tcb_id = tcb.id; + let key = connections.insert(tcb); + connection_map.insert(tcb_id, key); + listeners[listener_idx].syn_received_count += 1; + } + + // Step 4: Other → drop (frame returned by caller). + } +} diff --git a/src/net/handler/tcp/inbound/mod.rs b/src/net/handler/tcp/inbound/mod.rs new file mode 100644 index 0000000..e4fcf9e --- /dev/null +++ b/src/net/handler/tcp/inbound/mod.rs @@ -0,0 +1,7 @@ +mod established; +mod listen; +pub(crate) mod segment; +mod syn_received; +mod syn_sent; +mod teardown; +mod validate; diff --git a/src/net/handler/tcp/inbound/segment.rs b/src/net/handler/tcp/inbound/segment.rs new file mode 100644 index 0000000..4aa5845 --- /dev/null +++ b/src/net/handler/tcp/inbound/segment.rs @@ -0,0 +1,308 @@ +use crate::{ + net::wire::{ip::IpAddress, tcp::flags}, + xdp::frame::{Frame, FrameBuffer}, +}; + +use coarsetime::Instant; + +use super::super::handler::TcpHandler; +use super::super::options::ParsedOptions; +use super::super::segment::SegmentBuilder; +use super::super::send_tracker::SendReady; +use super::super::state::TcpState; +use super::super::tcb::ConnectionId; + +/// Actions that must be performed after releasing the `&mut Tcb` borrow. +pub(super) enum PostAction { + None, + RemoveConnection(usize), + RemoveAndDecrement(usize), +} + +/// Check segment acceptability per RFC 9293 §3.10.7.4. +#[inline] +pub(crate) 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 { + /// Unified segment processing for both IPv4 and IPv6. + #[inline] + pub(super) 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, + }; + + // Destructure self for split borrows. + let Self { + connections, + connection_map, + listeners, + isn_generator, + send_tracker, + tx_offload, + rx_offload: _, + .. + } = self; + let tx_offload = *tx_offload; + + if let Some(&key) = connection_map.get(&conn_id) + && let Some(tcb) = connections.get_mut(key) + { + let tsval = if tcb.ts_enabled { + now.duration_since(tcb.ts_offset).as_millis() as u32 + } else { + 0 + }; + let state = tcb.state; + let action = match state { + TcpState::SynSent => { + let action = Self::process_syn_sent( + tcb, + key, + now, + tsval, + seg_seq, + seg_ack, + seg_flags, + seg_wnd, + options, + src_mac, + dst_mac, + tx_offload, + free_frames, + tx_return, + ); + rx_return.push(frame); + action + } + TcpState::SynReceived => { + let action = Self::process_syn_received( + tcb, + key, + listeners, + tsval, + seg_seq, + seg_ack, + seg_flags, + seg_wnd, + seg_len, + src_mac, + dst_mac, + tx_offload, + free_frames, + tx_return, + ); + rx_return.push(frame); + action + } + 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( + tcb, + key, + frame, + now, + tsval, + seg_seq, + seg_ack, + seg_flags, + seg_wnd, + payload_offset, + payload_len, + &opts, + ecn_bits, + src_mac, + dst_mac, + tx_offload, + 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( + tcb, + key, + frame, + now, + tsval, + seg_seq, + seg_ack, + seg_flags, + seg_wnd, + payload_offset, + payload_len, + &opts, + src_mac, + dst_mac, + tx_offload, + free_frames, + rx_return, + tx_return, + ) + } + _ => { + rx_return.push(frame); + PostAction::None + } + }; + + // Handle deferred actions after tcb borrow is released. + match action { + PostAction::RemoveConnection(rm_key) => { + if let Some(tcb) = connections.get(rm_key) { + connection_map.remove(&tcb.id); + } + send_tracker.unmark(rm_key); + connections.remove(rm_key); + } + PostAction::RemoveAndDecrement(rm_key) => { + if let Some(tcb) = connections.get(rm_key) { + Self::decrement_syn_received(listeners, &tcb.id); + connection_map.remove(&tcb.id); + } + send_tracker.unmark(rm_key); + connections.remove(rm_key); + } + PostAction::None => { + // Mark connection for send processing if it has pending work. + if let Some(tcb) = connections.get(key) + && (tcb.ack_pending + || tcb.pending_fin + || tcb.send_buffer.available() > 0 + || tcb.ecn_cwr_sent + || tcb.persist_deadline.is_some() + || tcb.retransmit_deadline.is_some()) + { + send_tracker.mark(SendReady(key)); + } + } + } + return; + } + + // No connection found — check listeners (LISTEN state, §16.2). + if let Some(listener_idx) = listeners + .iter() + .position(|l| l.port == dst_port && (l.addr.is_unspecified() || l.addr == incoming_dst)) + { + Self::process_listen( + connections, + connection_map, + listeners, + isn_generator, + 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, + tx_offload, + free_frames, + tx_return, + ); + // New SynReceived connection needs retransmit timer tracking. + let new_conn_id = ConnectionId { + local_addr: incoming_dst, + local_port: dst_port, + remote_addr: incoming_src, + remote_port: src_port, + }; + if let Some(&key) = connection_map.get(&new_conn_id) { + send_tracker.mark(SendReady(key)); + } + 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, + tx_offload, + free_frames, + tx_return, + ); + rx_return.push(frame); + } +} diff --git a/src/net/handler/tcp/inbound/syn_received.rs b/src/net/handler/tcp/inbound/syn_received.rs new file mode 100644 index 0000000..fa6f976 --- /dev/null +++ b/src/net/handler/tcp/inbound/syn_received.rs @@ -0,0 +1,173 @@ +use crate::xdp::frame::FrameBuffer; + +use super::super::handler::TcpHandler; +use super::super::listener::ListenEntry; +use super::super::segment::SegmentBuilder; +use super::super::state::TcpState; +use super::super::tcb::{DEFAULT_RCV_WND, Tcb, TcpEvent}; + +use super::segment::PostAction; + +use crate::net::wire::tcp::flags; + +impl TcpHandler { + pub(super) fn process_syn_received<'umem>( + tcb: &mut Tcb, + key: usize, + listeners: &mut [ListenEntry], + 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, + tx_offload: bool, + free_frames: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) -> PostAction { + let id = tcb.id; + + // Step 1: Check sequence number acceptability. + 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 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, + tx_offload, + free_frames, + tx_return, + ); + } + return PostAction::None; + } + + // 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. + return PostAction::RemoveAndDecrement(key); + } else { + // Active open → signal refused. + tcb.event_queue.push(TcpEvent::ConnectionRefused); + return PostAction::RemoveConnection(key); + } + } + + // Step 4: Check the SYN bit. + if seg_flags & flags::SYN != 0 && seg_flags & flags::ACK == 0 { + if tcb.from_passive_open { + // Retransmit SYN-ACK so the client can complete the handshake. + let wscale_opt = if tcb.wscale_enabled { + Some(tcb.rcv_wscale) + } else { + None + }; + let ts_opt = tcb.ts_option(tsval); + SegmentBuilder::build_syn_ack( + tcb.id.local_addr, + tcb.id.remote_addr, + tcb.id.local_port, + tcb.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, + tx_offload, + free_frames, + tx_return, + ); + } else { + // Active open (simultaneous): send challenge ACK per RFC 5961. + 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, + tx_offload, + free_frames, + tx_return, + ); + } + return PostAction::None; + } + + // Step 5: Check ACK. + 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) + { + // ACK is acceptable → transition to ESTABLISHED. + tcb.state = TcpState::Established; + tcb.snd_una = seg_ack; + // RFC 7323 §2.2: window scaling applies to all non-SYN segments. + 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; + tcb.retransmit_deadline = None; + tcb.rto_backoff = 0; + + let from_passive = tcb.from_passive_open; + if from_passive { + // Push slab key to listener's accept_queue. + Self::push_to_accept_queue_on(listeners, &id, key); + Self::decrement_syn_received(listeners, &id); + } else { + // Simultaneous open — notify the active opener. + tcb.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, + tx_offload, + free_frames, + tx_return, + ); + } + } + PostAction::None + } +} diff --git a/src/net/handler/tcp/inbound/syn_sent.rs b/src/net/handler/tcp/inbound/syn_sent.rs new file mode 100644 index 0000000..68b2ec5 --- /dev/null +++ b/src/net/handler/tcp/inbound/syn_sent.rs @@ -0,0 +1,200 @@ +use coarsetime::Instant; + +use crate::{ + net::wire::tcp::{flags, parse_mss, parse_sack_permitted, parse_timestamp, parse_window_scale}, + xdp::frame::FrameBuffer, +}; + +use super::super::handler::{INITIAL_RTO_MS, TcpHandler}; +use super::super::segment::SegmentBuilder; +use super::super::state::TcpState; +use super::super::tcb::{DEFAULT_RCV_WND, TS_OPTION_LEN, Tcb, TcpEvent}; + +use super::segment::PostAction; + +impl TcpHandler { + // --- SYN-SENT state processing (RFC §16.3) --- + + pub(super) fn process_syn_sent<'umem>( + tcb: &mut Tcb, + key: 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, + tx_offload: bool, + free_frames: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) -> PostAction { + 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, + tx_offload, + free_frames, + tx_return, + ); + } + return PostAction::None; + } + + // Step 2: Check RST. + if seg_flags & flags::RST != 0 { + if seg_flags & flags::ACK != 0 { + // ACK was acceptable (passed step 1) → connection refused. + tcb.event_queue.push(TcpEvent::ConnectionRefused); + return PostAction::RemoveConnection(key); + } + // RST without ACK → drop silently. + return PostAction::None; + } + + // Step 3: Check SYN. + if seg_flags & flags::SYN != 0 { + 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. + 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, + tx_offload, + free_frames, + tx_return, + ); + + tcb.event_queue.push(TcpEvent::Connected); + } else { + // Simultaneous open → SYN-RECEIVED (MUST-10). + 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, + 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. + PostAction::None + } +} diff --git a/src/net/handler/tcp/inbound/teardown.rs b/src/net/handler/tcp/inbound/teardown.rs new file mode 100644 index 0000000..2cfeb2c --- /dev/null +++ b/src/net/handler/tcp/inbound/teardown.rs @@ -0,0 +1,397 @@ +use coarsetime::Instant; + +use crate::{ + net::wire::tcp::flags, + xdp::frame::{Frame, FrameBuffer}, +}; + +use super::super::handler::TcpHandler; +use super::super::options::ParsedOptions; +use super::super::segment::SegmentBuilder; +use super::super::state::TcpState; +use super::super::tcb::{Tcb, TcpEvent}; + +use super::segment::{PostAction, is_segment_acceptable}; + +impl TcpHandler { + // --- Connection teardown --- + + // --- Teardown state processing (FinWait1, FinWait2, CloseWait, Closing, LastAck, TimeWait) --- + + pub(super) fn process_teardown<'umem>( + tcb: &mut Tcb, + key: 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, + tx_offload: bool, + free_frames: &mut impl FrameBuffer<'umem>, + rx_return: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) -> PostAction { + let state = tcb.state; + + // PAWS check (RFC 7323 §5). + if tcb.ts_enabled + && let Some((tsval, _)) = opts.timestamp + { + 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 PostAction::None; + } + 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, + tx_offload, + free_frames, + tx_return, + ); + rx_return.push(frame); + return PostAction::None; + } + } + } + + // Segment acceptability check (RFC 9293 §3.10.7.4). + { + 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 PostAction::None; + } + 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, + tx_offload, + free_frames, + tx_return, + ); + rx_return.push(frame); + return PostAction::None; + } + } + + // 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 PostAction::None; + } + if seg_seq == tcb.rcv_nxt { + // Exact match: reset connection. + tcb.event_queue.push(TcpEvent::Reset); + rx_return.push(frame); + return PostAction::RemoveConnection(key); + } + // In-window but not exact: send challenge ACK, drop segment. + 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, + tx_offload, + free_frames, + tx_return, + ); + rx_return.push(frame); + return PostAction::None; + } + + // Step 4: SYN check (RFC 5961 — challenge ACK for SYN in synchronized state). + if seg_flags & flags::SYN != 0 { + 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, + tx_offload, + free_frames, + tx_return, + ); + rx_return.push(frame); + return PostAction::None; + } + + // Step 5 preamble: if ACK bit is off, drop segment and return. + if seg_flags & flags::ACK == 0 { + rx_return.push(frame); + return PostAction::None; + } + + match state { + TcpState::FinWait1 => { + 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. + 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 = tcb.id; + let snd_nxt = tcb.snd_nxt; + let rcv_nxt = tcb.rcv_nxt; + let ts = tcb.ts_option(tsval); + let window = tcb.advertised_window(); + SegmentBuilder::build_ack( + id.local_addr, + id.remote_addr, + id.local_port, + id.remote_port, + snd_nxt, + rcv_nxt, + window, + flags::ACK, + ts, + src_mac, + dst_mac, + tx_offload, + free_frames, + tx_return, + ); + } + + rx_return.push(frame); + } + + TcpState::FinWait2 => { + // 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, + tx_offload, + free_frames, + tx_return, + ); + } + + rx_return.push(frame); + } + + TcpState::Closing => { + // 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 Some(fin_seq) = tcb.fin_seq + && crate::net::wire::tcp::seq_lt(fin_seq, seg_ack) + { + rx_return.push(frame); + return PostAction::RemoveConnection(key); + } + rx_return.push(frame); + } + + TcpState::TimeWait => { + // 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, + 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 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); + } + } + PostAction::None + } +} diff --git a/src/net/handler/tcp/inbound/validate.rs b/src/net/handler/tcp/inbound/validate.rs new file mode 100644 index 0000000..3be412f --- /dev/null +++ b/src/net/handler/tcp/inbound/validate.rs @@ -0,0 +1,166 @@ +use coarsetime::Instant; + +use crate::{ + net::{ + NeighborHandler, + checksum::verify_tcp_checksum_ip, + wire::{ + ethernet::EthernetFrame, + ip::{IpAddress, IpVersion, Ipv4, Ipv4Header, Ipv6, Ipv6Header}, + tcp::{TCP_HEADER_LEN, TcpHeader}, + }, + }, + xdp::frame::{Frame, FrameBuffer}, +}; + +use super::super::handler::TcpHandler; +use super::super::tcb::Tcb; + +impl TcpHandler { + /// 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(); + + self.process_inbound::( + frame, + tcp_offset, + &src_addr, + &dst_addr, + now, + neighbor_handler, + 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; + + self.process_inbound::( + frame, + tcp_offset, + &src_addr, + &dst_addr, + now, + neighbor_handler, + free_frames, + rx_return, + tx_return, + ); + } + + /// Generic inbound TCP segment processing, parameterized by IP version. + #[inline] + fn process_inbound<'umem, V: IpVersion>( + &mut self, + frame: Frame<'umem>, + tcp_offset: usize, + src_addr: &V::Address, + dst_addr: &V::Address, + now: Instant, + neighbor_handler: &NeighborHandler, + free_frames: &mut impl FrameBuffer<'umem>, + rx_return: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) { + 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; + } + + // Checksum verification. + if !self.rx_offload { + let tcp_segment = &frame[tcp_offset..]; + if !verify_tcp_checksum_ip::(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); + + // ECN bits extraction (version-specific via trait). + let ecn_bits = V::get_ecn_bits(&frame, std::mem::size_of::()); + + let incoming_src: IpAddress = (*src_addr).into(); + let incoming_dst: IpAddress = (*dst_addr).into(); + 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, + ); + } +} diff --git a/src/net/handler/tcp/isn.rs b/src/net/handler/tcp/isn.rs new file mode 100644 index 0000000..fe1f1a3 --- /dev/null +++ b/src/net/handler/tcp/isn.rs @@ -0,0 +1,97 @@ +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]; + let ret = unsafe { libc::getrandom(buf.as_mut_ptr().cast(), 8, 0) }; + assert!( + ret == 8, + "getrandom failed to fill ISN secret: returned {ret}" + ); + 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..3d26e2e --- /dev/null +++ b/src/net/handler/tcp/listener.rs @@ -0,0 +1,139 @@ +use smallvec::SmallVec; + +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. + let keys_to_remove: SmallVec<[usize; 8]> = self + .connection_map + .iter() + .filter_map(|(_id, &key)| { + let tcb = &self.connections[key]; + if tcb.state == TcpState::SynReceived + && tcb.from_passive_open + && tcb.id.local_port == port + && (addr.is_unspecified() || tcb.id.local_addr == addr) + { + Some(key) + } else { + None + } + }) + .collect(); + for key in keys_to_remove { + self.send_tracker.unmark(key); + self.remove_connection_by_key(key); + } + } + + /// Push a slab key to the matching listener's accept queue (associated function for split-borrow). + pub(super) fn push_to_accept_queue_on( + listeners: &[ListenEntry], + id: &ConnectionId, + key: usize, + ) { + for listener in listeners { + if listener.port == id.local_port + && (listener.addr.is_unspecified() || listener.addr == id.local_addr) + { + listener.accept_queue.push(key); + return; + } + } + } + + /// Decrement syn_received_count on the matching listener (associated function for split-borrow). + pub(super) fn decrement_syn_received(listeners: &mut [ListenEntry], id: &ConnectionId) { + for listener in listeners.iter_mut() { + 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..f339228 --- /dev/null +++ b/src/net/handler/tcp/mod.rs @@ -0,0 +1,24 @@ +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 send_tracker; +pub(crate) mod state; +pub(crate) mod tcb; +mod timers; +mod transmit; + +pub use handler::TcpHandler; + +// Exposed for benchmarks. +#[doc(hidden)] +pub use segment::SegmentBuilder; + +#[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..f7e35a5 --- /dev/null +++ b/src/net/handler/tcp/ring_buffer.rs @@ -0,0 +1,498 @@ +use std::task::Waker; + +/// 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, + /// Waker for tasks waiting to read (fires when data is written/committed). + read_waker: Option, + /// Waker for tasks waiting to write (fires when space is freed via read/advance). + write_waker: Option, +} + +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, + read_waker: None, + write_waker: None, + } + } + + /// Register a waker to be called when data becomes available for reading. + pub fn register_read_waker(&mut self, waker: &Waker) { + self.read_waker = Some(waker.clone()); + } + + /// Register a waker to be called when space becomes available for writing. + pub fn register_write_waker(&mut self, waker: &Waker) { + self.write_waker = Some(waker.clone()); + } + + /// 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; + if let Some(waker) = self.read_waker.take() { + waker.wake(); + } + 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; + if n > 0 + && let Some(waker) = self.write_waker.take() + { + waker.wake(); + } + } + + /// 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; + if n > 0 + && let Some(waker) = self.read_waker.take() + { + waker.wake(); + } + } + + /// 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; + if let Some(waker) = self.write_waker.take() { + waker.wake(); + } + to_read + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }; + use std::task::Wake; + + struct TestWaker { + woken: AtomicBool, + } + impl Wake for TestWaker { + fn wake(self: Arc) { + self.woken.store(true, Ordering::Relaxed); + } + } + + #[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); + } + + #[test] + fn ring_buffer_wakes_read_on_write() { + let mut buf = RingBuffer::new(64); + let tw = Arc::new(TestWaker { + woken: AtomicBool::new(false), + }); + let waker = Waker::from(tw.clone()); + buf.register_read_waker(&waker); + buf.write(b"hello"); + assert!(tw.woken.load(Ordering::Relaxed)); + } + + #[test] + fn ring_buffer_wakes_write_on_read() { + let mut buf = RingBuffer::new(64); + buf.write(b"hello"); + let tw = Arc::new(TestWaker { + woken: AtomicBool::new(false), + }); + let waker = Waker::from(tw.clone()); + buf.register_write_waker(&waker); + let mut out = [0u8; 5]; + buf.read(&mut out); + assert!(tw.woken.load(Ordering::Relaxed)); + } + + #[test] + fn ring_buffer_read_waker_cleared_after_wake() { + let mut buf = RingBuffer::new(64); + let tw = Arc::new(TestWaker { + woken: AtomicBool::new(false), + }); + let waker = Waker::from(tw.clone()); + buf.register_read_waker(&waker); + buf.write(b"hello"); + assert!(tw.woken.load(Ordering::Relaxed)); + tw.woken.store(false, Ordering::Relaxed); + buf.write(b"world"); + assert!(!tw.woken.load(Ordering::Relaxed)); + } +} diff --git a/src/net/handler/tcp/segment.rs b/src/net/handler/tcp/segment.rs new file mode 100644 index 0000000..5acaefa --- /dev/null +++ b/src/net/handler/tcp/segment.rs @@ -0,0 +1,2067 @@ +use crate::net::{ + checksum::compute_tcp_checksum_ip, + wire::{ + ethernet::{EthernetFrame, MacAddress, write_ethernet_header}, + ip::{IpAddress, IpVersion, Ipv4, Ipv6}, + 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(l), IpAddress::V4(r)) => { + Self::build_segment::( + l, + r, + incoming_dst_port, + incoming_src_port, + seq, + ack, + rst_flags, + 0, + &[], + src_mac, + dst_mac, + tx_offload, + free_frames, + tx_return, + ); + } + (IpAddress::V6(l), IpAddress::V6(r)) => { + Self::build_segment::( + l, + r, + incoming_dst_port, + incoming_src_port, + seq, + ack, + rst_flags, + 0, + &[], + src_mac, + dst_mac, + tx_offload, + free_frames, + tx_return, + ); + } + _ => {} + } + } + + /// 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(l), IpAddress::V4(r)) => { + Self::build_segment::( + l, + r, + 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(l), IpAddress::V6(r)) => { + Self::build_segment::( + l, + r, + 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(l), IpAddress::V4(r)) => { + Self::build_segment::( + l, + r, + 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(l), IpAddress::V6(r)) => { + Self::build_segment::( + l, + r, + 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(l), IpAddress::V4(r)) => { + Self::build_segment::( + l, + r, + local_port, + remote_port, + seq, + ack, + tcp_flags, + window, + tcp_options, + src_mac, + dst_mac, + tx_offload, + free_frames, + tx_return, + ); + } + (IpAddress::V6(l), IpAddress::V6(r)) => { + Self::build_segment::( + l, + r, + 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(l), IpAddress::V4(r)) => { + Self::build_segment::( + l, + r, + local_port, + remote_port, + seq, + ack, + tcp_flags, + window, + tcp_options, + src_mac, + dst_mac, + tx_offload, + free_frames, + tx_return, + ); + } + (IpAddress::V6(l), IpAddress::V6(r)) => { + Self::build_segment::( + l, + r, + 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(l), IpAddress::V4(r)) => { + Self::build_segment::( + l, + r, + 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(l), IpAddress::V6(r)) => { + Self::build_segment::( + l, + r, + 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(l), IpAddress::V4(r)) => { + Self::build_data_segment::( + l, + r, + local_port, + remote_port, + seq, + ack, + flags::ACK, + window, + payload, + tcp_options, + src_mac, + dst_mac, + tx_offload, + free_frames, + tx_return, + ); + } + (IpAddress::V6(l), IpAddress::V6(r)) => { + Self::build_data_segment::( + l, + r, + 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(l), IpAddress::V4(r)) => { + Self::build_data_segment_slices::( + l, + r, + 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(l), IpAddress::V6(r)) => { + Self::build_data_segment_slices::( + l, + r, + 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_segment<'umem, V: IpVersion>( + src_ip: V::Address, + dst_ip: V::Address, + 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 frame_len = ETH_LEN + V::IP_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, V::ETHER_TYPE); + + // IP header (version-specific via trait). + V::write_ip_header(&mut frame, &src_ip, &dst_ip, tcp_header_len); + + // TCP header. + let tcp_offset = ETH_LEN + V::IP_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_ip::(&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_data_segment<'umem, V: IpVersion>( + src_ip: V::Address, + dst_ip: V::Address, + 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 frame_len = ETH_LEN + V::IP_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, V::ETHER_TYPE); + + // IP header (version-specific via trait). + V::write_ip_header(&mut frame, &src_ip, &dst_ip, tcp_header_len + payload.len()); + + // TCP header. + let tcp_offset = ETH_LEN + V::IP_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_ip::( + &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_data_segment_slices<'umem, V: IpVersion>( + src_ip: V::Address, + dst_ip: V::Address, + 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 frame_len = ETH_LEN + V::IP_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, V::ETHER_TYPE); + + // IP header (version-specific via trait). + V::write_ip_header(&mut frame, &src_ip, &dst_ip, tcp_header_len + payload_len); + + // Set ECN ECT(0) if requested (must be after write_ip_header; + // IPv4 impl recomputes the header checksum). + if ecn_ect { + V::set_ecn_ect(&mut frame, ETH_LEN); + } + + // TCP header. + let tcp_offset = ETH_LEN + V::IP_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_ip::( + &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_ip; + use crate::net::wire::ip::Ipv4; + use crate::net::wire::ip::{ + IPV4_MIN_HEADER_LEN, IPV6_HEADER_LEN, IpAddress, Ipv4Address, Ipv4Header, Ipv6, + Ipv6Address, Ipv6Header, + }; + 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::( + &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::( + &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::( + &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::( + &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. + } + + // --------------------------------------------------------------- + // build_rst tests + // --------------------------------------------------------------- + + #[test] + fn build_rst_ack_off_sends_rst_ack() { + // RFC 9293 §3.10.7.1: ACK off → + let mut free = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + free.push(alloc_free_frame(100)); + + let incoming_seq = 1000u32; + let incoming_seg_len = 50u32; + + SegmentBuilder::build_rst( + IpAddress::V4(Ipv4Address::new([10, 0, 0, 2])), // incoming src + IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])), // incoming dst + 80, // incoming src port + 8080, // incoming dst port + incoming_seq, + 0, // incoming ack (irrelevant when ACK off) + flags::SYN, // ACK bit is off + incoming_seg_len, + MacAddress::new([0xAA; 6]), + MacAddress::new([0xBB; 6]), + false, + &mut free, + &mut tx, + ); + + assert_eq!(tx.num_frames(), 1, "RST segment built"); + assert_eq!(free.num_frames(), 0, "free frame consumed"); + + let frame = tx.pop().unwrap(); + // 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.flags(), flags::RST | flags::ACK); + assert_eq!(tcp.seq_num(), 0); + assert_eq!(tcp.ack_num(), incoming_seq.wrapping_add(incoming_seg_len)); + // Ports are swapped: RST goes back to the sender. + assert_eq!(tcp.src_port(), 8080); + assert_eq!(tcp.dst_port(), 80); + + // Verify TCP checksum. + let ip = Ipv4Header::from_bytes(&frame); + assert!(verify_tcp_checksum_ip::( + &ip.src_addr, + &ip.dst_addr, + &frame[tcp_offset..] + )); + } + + #[test] + fn build_rst_ack_on_sends_rst_only() { + // RFC 9293 §3.10.7.1: ACK on → + let mut free = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + free.push(alloc_free_frame(100)); + + let incoming_ack = 5000u32; + + SegmentBuilder::build_rst( + IpAddress::V4(Ipv4Address::new([10, 0, 0, 2])), + IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])), + 80, + 8080, + 1000, + incoming_ack, + flags::ACK, // ACK bit is on + 0, + 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 = ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN; + let tcp = unsafe { TcpHeader::from_bytes_at(&frame, tcp_offset) }; + assert_eq!(tcp.flags(), flags::RST); + assert_eq!(tcp.seq_num(), incoming_ack); + assert_eq!(tcp.ack_num(), 0); + + let ip = Ipv4Header::from_bytes(&frame); + assert!(verify_tcp_checksum_ip::( + &ip.src_addr, + &ip.dst_addr, + &frame[tcp_offset..] + )); + } + + // --------------------------------------------------------------- + // build_syn tests + // --------------------------------------------------------------- + + #[test] + fn build_syn_all_options_ipv4() { + let mut free = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + free.push(alloc_free_frame(100)); + + let iss = 12345u32; + let mss = 1460u16; + let wscale = 7u8; + let tsval = 1000u32; + let tsecr = 0u32; + + SegmentBuilder::build_syn( + IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])), + IpAddress::V4(Ipv4Address::new([10, 0, 0, 2])), + 8080, + 80, + iss, + 65535, + mss, + wscale, + Some((tsval, tsecr)), + true, // sack_permitted + false, + MacAddress::new([0xAA; 6]), + MacAddress::new([0xBB; 6]), + false, + &mut free, + &mut tx, + ); + + assert_eq!(tx.num_frames(), 1, "SYN 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::SYN); + assert_eq!(tcp.seq_num(), iss); + assert_eq!(tcp.ack_num(), 0); + assert_eq!(tcp.src_port(), 8080); + assert_eq!(tcp.dst_port(), 80); + assert_eq!(tcp.window(), 65535); + + // Options: MSS(4) + NOP(1) + WSCALE(3) + NOP(1) + NOP(1) + TS(10) + SACK_PERM(2) = 22, padded to 24 + // data_offset = (20 + 24) / 4 = 11 + assert_eq!(tcp.data_offset(), 11); + + let opt_start = tcp_offset + TCP_HEADER_LEN; + // MSS option: kind=2, len=4, value=1460 + assert_eq!(frame[opt_start], options::MSS); + assert_eq!(frame[opt_start + 1], 4); + assert_eq!( + u16::from_be_bytes([frame[opt_start + 2], frame[opt_start + 3]]), + mss + ); + // NOP + assert_eq!(frame[opt_start + 4], options::NOP); + // Window Scale: kind=3, len=3, shift=7 + assert_eq!(frame[opt_start + 5], options::WINDOW_SCALE); + assert_eq!(frame[opt_start + 6], 3); + assert_eq!(frame[opt_start + 7], wscale); + // NOP, NOP + assert_eq!(frame[opt_start + 8], options::NOP); + assert_eq!(frame[opt_start + 9], options::NOP); + // Timestamp: kind=8, len=10 + assert_eq!(frame[opt_start + 10], 8); // TS kind + assert_eq!(frame[opt_start + 11], 10); // TS len + assert_eq!( + u32::from_be_bytes([ + frame[opt_start + 12], + frame[opt_start + 13], + frame[opt_start + 14], + frame[opt_start + 15] + ]), + tsval + ); + assert_eq!( + u32::from_be_bytes([ + frame[opt_start + 16], + frame[opt_start + 17], + frame[opt_start + 18], + frame[opt_start + 19] + ]), + tsecr + ); + // SACK Permitted: kind=4, len=2 + assert_eq!(frame[opt_start + 20], options::SACK_PERMITTED); + assert_eq!(frame[opt_start + 21], 2); + + let ip = Ipv4Header::from_bytes(&frame); + assert!(verify_tcp_checksum_ip::( + &ip.src_addr, + &ip.dst_addr, + &frame[tcp_offset..] + )); + } + + #[test] + fn build_syn_with_ecn_ipv4() { + let mut free = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + free.push(alloc_free_frame(100)); + + 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, + None, + false, + true, // ecn + 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 = ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN; + let tcp = unsafe { TcpHeader::from_bytes_at(&frame, tcp_offset) }; + assert_eq!(tcp.flags(), flags::SYN | flags::ECE | flags::CWR); + + let ip = Ipv4Header::from_bytes(&frame); + assert!(verify_tcp_checksum_ip::( + &ip.src_addr, + &ip.dst_addr, + &frame[tcp_offset..] + )); + } + + // --------------------------------------------------------------- + // build_syn_ack tests + // --------------------------------------------------------------- + + #[test] + fn build_syn_ack_with_wscale_ipv4() { + let mut free = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + free.push(alloc_free_frame(100)); + + let iss = 9999u32; + let ack = 10000u32; + + SegmentBuilder::build_syn_ack( + IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])), + IpAddress::V4(Ipv4Address::new([10, 0, 0, 2])), + 80, + 8080, + iss, + ack, + 65535, + 1460, + Some(7), // wscale + Some((100, 50)), // timestamp + true, // sack_permitted + false, // ecn + MacAddress::new([0xAA; 6]), + MacAddress::new([0xBB; 6]), + false, + &mut free, + &mut tx, + ); + + assert_eq!(tx.num_frames(), 1, "SYN-ACK 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::SYN | flags::ACK); + assert_eq!(tcp.seq_num(), iss); + assert_eq!(tcp.ack_num(), ack); + assert_eq!(tcp.src_port(), 80); + assert_eq!(tcp.dst_port(), 8080); + + // Options: MSS(4) + NOP(1) + WSCALE(3) + NOP(1) + NOP(1) + TS(10) + SACK_PERM(2) = 22, padded to 24 + assert_eq!(tcp.data_offset(), 11); + + let opt_start = tcp_offset + TCP_HEADER_LEN; + // MSS + assert_eq!(frame[opt_start], options::MSS); + assert_eq!(frame[opt_start + 1], 4); + assert_eq!( + u16::from_be_bytes([frame[opt_start + 2], frame[opt_start + 3]]), + 1460 + ); + + let ip = Ipv4Header::from_bytes(&frame); + assert!(verify_tcp_checksum_ip::( + &ip.src_addr, + &ip.dst_addr, + &frame[tcp_offset..] + )); + } + + #[test] + fn build_syn_ack_without_wscale_ipv4() { + let mut free = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + free.push(alloc_free_frame(100)); + + SegmentBuilder::build_syn_ack( + IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])), + IpAddress::V4(Ipv4Address::new([10, 0, 0, 2])), + 80, + 8080, + 5000, + 6000, + 32768, + 1460, + None, // no wscale + None, // no timestamp + false, // no sack_permitted + false, // no ecn + 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 = ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN; + let tcp = unsafe { TcpHeader::from_bytes_at(&frame, tcp_offset) }; + assert_eq!(tcp.flags(), flags::SYN | flags::ACK); + + // Options: MSS(4) only, padded to 4 bytes + // data_offset = (20 + 4) / 4 = 6 + assert_eq!(tcp.data_offset(), 6); + + let ip = Ipv4Header::from_bytes(&frame); + assert!(verify_tcp_checksum_ip::( + &ip.src_addr, + &ip.dst_addr, + &frame[tcp_offset..] + )); + } + + #[test] + fn build_syn_ack_with_ecn_ipv4() { + let mut free = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + free.push(alloc_free_frame(100)); + + SegmentBuilder::build_syn_ack( + IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])), + IpAddress::V4(Ipv4Address::new([10, 0, 0, 2])), + 80, + 8080, + 5000, + 6000, + 65535, + 1460, + None, + None, + false, + true, // ecn + 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 = ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN; + let tcp = unsafe { TcpHeader::from_bytes_at(&frame, tcp_offset) }; + // SYN-ACK with ECN sets SYN|ACK|ECE (not CWR) + assert_eq!(tcp.flags(), flags::SYN | flags::ACK | flags::ECE); + + let ip = Ipv4Header::from_bytes(&frame); + assert!(verify_tcp_checksum_ip::( + &ip.src_addr, + &ip.dst_addr, + &frame[tcp_offset..] + )); + } + + // --------------------------------------------------------------- + // build_ack tests + // --------------------------------------------------------------- + + #[test] + fn build_ack_with_timestamp_ipv4() { + let mut free = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + free.push(alloc_free_frame(100)); + + let tsval = 555u32; + let tsecr = 444u32; + + SegmentBuilder::build_ack( + IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])), + IpAddress::V4(Ipv4Address::new([10, 0, 0, 2])), + 8080, + 80, + 2000, + 3000, + 65535, + flags::ACK, + Some((tsval, tsecr)), + MacAddress::new([0xAA; 6]), + MacAddress::new([0xBB; 6]), + false, + &mut free, + &mut tx, + ); + + assert_eq!(tx.num_frames(), 1, "ACK with timestamp 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); + assert_eq!(tcp.seq_num(), 2000); + assert_eq!(tcp.ack_num(), 3000); + + // Options: NOP(1) + NOP(1) + TS(10) = 12 bytes, padded to 12 (already aligned) + // data_offset = (20 + 12) / 4 = 8 + assert_eq!(tcp.data_offset(), 8); + + // Verify timestamp option bytes + let opt_start = tcp_offset + TCP_HEADER_LEN; + assert_eq!(frame[opt_start], options::NOP); + assert_eq!(frame[opt_start + 1], options::NOP); + assert_eq!(frame[opt_start + 2], 8); // TS kind + assert_eq!(frame[opt_start + 3], 10); // TS len + assert_eq!( + u32::from_be_bytes([ + frame[opt_start + 4], + frame[opt_start + 5], + frame[opt_start + 6], + frame[opt_start + 7] + ]), + tsval + ); + assert_eq!( + u32::from_be_bytes([ + frame[opt_start + 8], + frame[opt_start + 9], + frame[opt_start + 10], + frame[opt_start + 11] + ]), + tsecr + ); + + let ip = Ipv4Header::from_bytes(&frame); + assert!(verify_tcp_checksum_ip::( + &ip.src_addr, + &ip.dst_addr, + &frame[tcp_offset..] + )); + } + + #[test] + fn build_ack_without_timestamp_ipv4() { + let mut free = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + free.push(alloc_free_frame(100)); + + SegmentBuilder::build_ack( + IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])), + IpAddress::V4(Ipv4Address::new([10, 0, 0, 2])), + 8080, + 80, + 2000, + 3000, + 65535, + flags::ACK, + None, + MacAddress::new([0xAA; 6]), + MacAddress::new([0xBB; 6]), + false, + &mut free, + &mut tx, + ); + + assert_eq!(tx.num_frames(), 1); + + let frame = tx.pop().unwrap(); + // ETH(14) + IPv4(20) + TCP(20) = 54, no options + 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.flags(), flags::ACK); + assert_eq!(tcp.data_offset(), 5); // no options + + let ip = Ipv4Header::from_bytes(&frame); + assert!(verify_tcp_checksum_ip::( + &ip.src_addr, + &ip.dst_addr, + &frame[tcp_offset..] + )); + } + + #[test] + fn build_ack_with_ece_flag_ipv4() { + let mut free = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + free.push(alloc_free_frame(100)); + + SegmentBuilder::build_ack( + IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])), + IpAddress::V4(Ipv4Address::new([10, 0, 0, 2])), + 8080, + 80, + 2000, + 3000, + 65535, + flags::ACK | flags::ECE, + None, + 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 = ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN; + let tcp = unsafe { TcpHeader::from_bytes_at(&frame, tcp_offset) }; + assert_eq!(tcp.flags(), flags::ACK | flags::ECE); + + let ip = Ipv4Header::from_bytes(&frame); + assert!(verify_tcp_checksum_ip::( + &ip.src_addr, + &ip.dst_addr, + &frame[tcp_offset..] + )); + } + + // --------------------------------------------------------------- + // IPv6 segment tests + // --------------------------------------------------------------- + + const LOCAL_V6: Ipv6Address = + Ipv6Address::new([0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + const REMOTE_V6: Ipv6Address = + Ipv6Address::new([0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]); + + #[test] + fn build_syn_ipv6() { + let mut free = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + free.push(alloc_free_frame(200)); + + SegmentBuilder::build_syn( + IpAddress::V6(LOCAL_V6), + IpAddress::V6(REMOTE_V6), + 8080, + 80, + 1000, + 65535, + 1440, + 7, + None, + false, + false, + MacAddress::new([0xAA; 6]), + MacAddress::new([0xBB; 6]), + false, + &mut free, + &mut tx, + ); + + assert_eq!(tx.num_frames(), 1, "SYN IPv6 segment built"); + assert_eq!(free.num_frames(), 0, "free frame consumed"); + + let frame = tx.pop().unwrap(); + let tcp_offset = ETH_HEADER_LEN + IPV6_HEADER_LEN; + let tcp = unsafe { TcpHeader::from_bytes_at(&frame, tcp_offset) }; + assert_eq!(tcp.flags(), flags::SYN); + assert_eq!(tcp.seq_num(), 1000); + assert_eq!(tcp.src_port(), 8080); + assert_eq!(tcp.dst_port(), 80); + + let ip = Ipv6Header::from_bytes(&frame); + assert!(verify_tcp_checksum_ip::( + &ip.src_addr, + &ip.dst_addr, + &frame[tcp_offset..] + )); + } + + #[test] + fn build_ack_ipv6() { + let mut free = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + free.push(alloc_free_frame(201)); + + SegmentBuilder::build_ack( + IpAddress::V6(LOCAL_V6), + IpAddress::V6(REMOTE_V6), + 8080, + 80, + 2000, + 3000, + 65535, + flags::ACK, + None, + MacAddress::new([0xAA; 6]), + MacAddress::new([0xBB; 6]), + false, + &mut free, + &mut tx, + ); + + assert_eq!(tx.num_frames(), 1, "ACK IPv6 segment built"); + + let frame = tx.pop().unwrap(); + // ETH(14) + IPv6(40) + TCP(20) = 74 + assert_eq!(frame.len(), 74); + + let tcp_offset = ETH_HEADER_LEN + IPV6_HEADER_LEN; + let tcp = unsafe { TcpHeader::from_bytes_at(&frame, tcp_offset) }; + assert_eq!(tcp.flags(), flags::ACK); + assert_eq!(tcp.seq_num(), 2000); + assert_eq!(tcp.ack_num(), 3000); + + let ip = Ipv6Header::from_bytes(&frame); + assert!(verify_tcp_checksum_ip::( + &ip.src_addr, + &ip.dst_addr, + &frame[tcp_offset..] + )); + } + + #[test] + fn build_rst_ipv6() { + let mut free = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + free.push(alloc_free_frame(202)); + + SegmentBuilder::build_rst( + IpAddress::V6(REMOTE_V6), + IpAddress::V6(LOCAL_V6), + 80, + 8080, + 1000, + 0, + flags::SYN, // ACK bit off + 100, + MacAddress::new([0xAA; 6]), + MacAddress::new([0xBB; 6]), + false, + &mut free, + &mut tx, + ); + + assert_eq!(tx.num_frames(), 1, "RST IPv6 segment built"); + + let frame = tx.pop().unwrap(); + let tcp_offset = ETH_HEADER_LEN + IPV6_HEADER_LEN; + let tcp = unsafe { TcpHeader::from_bytes_at(&frame, tcp_offset) }; + assert_eq!(tcp.flags(), flags::RST | flags::ACK); + + let ip = Ipv6Header::from_bytes(&frame); + assert!(verify_tcp_checksum_ip::( + &ip.src_addr, + &ip.dst_addr, + &frame[tcp_offset..] + )); + } + + #[test] + fn build_fin_ack_ipv6() { + let mut free = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + free.push(alloc_free_frame(203)); + + SegmentBuilder::build_fin_ack( + IpAddress::V6(LOCAL_V6), + IpAddress::V6(REMOTE_V6), + 80, + 8080, + 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 IPv6 segment built"); + + let frame = tx.pop().unwrap(); + let tcp_offset = ETH_HEADER_LEN + IPV6_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); + + let ip = Ipv6Header::from_bytes(&frame); + assert!(verify_tcp_checksum_ip::( + &ip.src_addr, + &ip.dst_addr, + &frame[tcp_offset..] + )); + } + + #[test] + fn build_data_ipv6() { + let mut free = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + free.push(alloc_free_frame(204)); + + let payload = b"Hello, IPv6 TCP!"; + + SegmentBuilder::build_data( + IpAddress::V6(LOCAL_V6), + IpAddress::V6(REMOTE_V6), + 8080, + 80, + 1000, + 500, + 65535, + payload, + None, + MacAddress::new([0xAA; 6]), + MacAddress::new([0xBB; 6]), + false, + &mut free, + &mut tx, + ); + + assert_eq!(tx.num_frames(), 1, "data IPv6 segment built"); + + let frame = tx.pop().unwrap(); + // ETH(14) + IPv6(40) + TCP(20) + payload(16) = 90 + assert_eq!(frame.len(), 90); + + let payload_start = ETH_HEADER_LEN + IPV6_HEADER_LEN + TCP_HEADER_LEN; + assert_eq!(&frame[payload_start..frame.len()], payload); + + let tcp_offset = ETH_HEADER_LEN + IPV6_HEADER_LEN; + let ip = Ipv6Header::from_bytes(&frame); + assert!(verify_tcp_checksum_ip::( + &ip.src_addr, + &ip.dst_addr, + &frame[tcp_offset..] + )); + } + + #[test] + fn build_mismatched_v4_v6_returns_none() { + let mut free = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + free.push(alloc_free_frame(205)); + + // V4 local, V6 remote — mismatched address families + SegmentBuilder::build_syn( + IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])), + IpAddress::V6(REMOTE_V6), + 8080, + 80, + 1000, + 65535, + 1460, + 7, + None, + false, + false, + MacAddress::new([0xAA; 6]), + MacAddress::new([0xBB; 6]), + false, + &mut free, + &mut tx, + ); + + assert_eq!(tx.num_frames(), 0, "mismatched AF produces no segment"); + assert_eq!(free.num_frames(), 1, "free frame not consumed"); + } +} diff --git a/src/net/handler/tcp/send_tracker.rs b/src/net/handler/tcp/send_tracker.rs new file mode 100644 index 0000000..b2e9a37 --- /dev/null +++ b/src/net/handler/tcp/send_tracker.rs @@ -0,0 +1,186 @@ +/// Marker returned by Tcb methods that make a connection sendable. +/// Must be consumed by passing to `SendTracker::mark()`. +#[must_use = "connection must be marked for sending via SendTracker::mark()"] +pub struct SendReady(pub usize); + +/// Tracks which connections have pending send work using a dual-bitset +/// swap pattern. `poll_send` calls `swap()` then iterates the active +/// bitset directly — no hashing, no heap allocation, O(1) set/clear. +/// New marks go into `pending`, which becomes `active` on the next `swap()`. +pub struct SendTracker { + active: BitSet, + pending: BitSet, +} + +impl SendTracker { + pub fn new() -> Self { + Self { + active: BitSet::new(), + pending: BitSet::new(), + } + } + + /// Swap active/pending sets. Call once at the start of poll_send. + /// O(1) pointer swap — no allocation, no iteration. + #[inline(always)] + pub fn swap(&mut self) { + std::mem::swap(&mut self.active, &mut self.pending); + } + + /// Pop the next set bit from the active set. O(1) amortized. + /// Unlike `drain_active()`, this doesn't hold a borrow across the + /// call site, so `mark()` can be called between pops. + #[inline(always)] + pub fn pop_active(&mut self) -> Option { + for (i, word) in self.active.words.iter_mut().enumerate() { + if *word != 0 { + let bit = word.trailing_zeros() as usize; + *word &= *word - 1; // clear lowest set bit + return Some(i * 64 + bit); + } + } + None + } + + /// Register a connection as needing send processing. + /// Always inserts into `pending` — safe to call during drain_active iteration. + #[inline(always)] + pub fn mark(&mut self, ready: SendReady) { + self.pending.set(ready.0); + } + + /// Remove a connection from both sets (connection closed/removed). + #[inline(always)] + pub fn unmark(&mut self, key: usize) { + self.active.clear(key); + self.pending.clear(key); + } + + /// Check if any connections need sending (across both sets). + #[inline(always)] + #[allow(dead_code)] + pub fn is_empty(&self) -> bool { + self.active.is_empty() && self.pending.is_empty() + } +} + +// --------------------------------------------------------------------------- +// Inline bitset — grows in 64-bit word chunks, no hashing +// --------------------------------------------------------------------------- + +/// A dynamically-sized bitset backed by a `Vec`. +/// Word-level operations make set/clear/iterate very cheap. +struct BitSet { + words: Vec, +} + +impl BitSet { + #[inline(always)] + fn new() -> Self { + Self { words: Vec::new() } + } + + #[inline(always)] + fn set(&mut self, bit: usize) { + let word = bit / 64; + let mask = 1u64 << (bit % 64); + if word >= self.words.len() { + self.words.resize(word + 1, 0); + } + self.words[word] |= mask; + } + + #[inline(always)] + fn clear(&mut self, bit: usize) { + let word = bit / 64; + if word < self.words.len() { + self.words[word] &= !(1u64 << (bit % 64)); + } + } + + #[inline(always)] + fn is_empty(&self) -> bool { + self.words.iter().all(|&w| w == 0) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Helper: drain all active keys via pop_active. + fn collect_active(tracker: &mut SendTracker) -> Vec { + let mut out = Vec::new(); + while let Some(key) = tracker.pop_active() { + out.push(key); + } + out + } + + #[test] + fn mark_and_drain_active() { + let mut tracker = SendTracker::new(); + tracker.mark(SendReady(42)); + tracker.mark(SendReady(7)); + tracker.swap(); + let active = collect_active(&mut tracker); + assert_eq!(active.len(), 2); + assert!(active.contains(&42)); + assert!(active.contains(&7)); + } + + #[test] + fn unmark_removes_from_both_sets() { + let mut tracker = SendTracker::new(); + tracker.mark(SendReady(10)); + tracker.swap(); + tracker.mark(SendReady(10)); + tracker.unmark(10); + assert!(tracker.is_empty()); + } + + #[test] + fn drain_clears_active_set() { + let mut tracker = SendTracker::new(); + tracker.mark(SendReady(0)); + tracker.mark(SendReady(63)); + tracker.mark(SendReady(64)); + tracker.mark(SendReady(200)); + tracker.swap(); + + let active = collect_active(&mut tracker); + assert_eq!(active, vec![0, 63, 64, 200]); + + // Active should be empty after drain. + assert!(tracker.active.is_empty()); + } + + #[test] + fn mark_during_drain_goes_to_pending() { + let mut tracker = SendTracker::new(); + tracker.mark(SendReady(5)); + tracker.swap(); + + // Simulate marking during iteration (goes to pending, not active). + tracker.mark(SendReady(99)); + + let active = collect_active(&mut tracker); + assert_eq!(active, vec![5]); + + // 99 should appear in next cycle. + tracker.swap(); + let active = collect_active(&mut tracker); + assert_eq!(active, vec![99]); + } + + #[test] + fn duplicate_marks_produce_single_entry() { + let mut tracker = SendTracker::new(); + tracker.mark(SendReady(10)); + tracker.mark(SendReady(10)); + tracker.mark(SendReady(10)); + tracker.swap(); + let active = collect_active(&mut tracker); + assert_eq!(active, vec![10]); + } +} diff --git a/src/net/handler/tcp/state.rs b/src/net/handler/tcp/state.rs new file mode 100644 index 0000000..cdbe962 --- /dev/null +++ b/src/net/handler/tcp/state.rs @@ -0,0 +1,117 @@ +/// 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 + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn is_synchronized() { + let synchronized = [ + TcpState::Established, + TcpState::FinWait1, + TcpState::FinWait2, + TcpState::CloseWait, + TcpState::Closing, + TcpState::LastAck, + TcpState::TimeWait, + ]; + let not_synchronized = [ + TcpState::Closed, + TcpState::Listen, + TcpState::SynSent, + TcpState::SynReceived, + ]; + for state in synchronized { + assert!( + state.is_synchronized(), + "{:?} should be synchronized", + state + ); + } + for state in not_synchronized { + assert!( + !state.is_synchronized(), + "{:?} should not be synchronized", + state + ); + } + } + + #[test] + fn is_remote_closed() { + let remote_closed = [ + TcpState::CloseWait, + TcpState::LastAck, + TcpState::TimeWait, + TcpState::Closing, + TcpState::Closed, + ]; + let not_remote_closed = [ + TcpState::Listen, + TcpState::SynSent, + TcpState::SynReceived, + TcpState::Established, + TcpState::FinWait1, + TcpState::FinWait2, + ]; + for state in remote_closed { + assert!( + state.is_remote_closed(), + "{:?} should be remote_closed", + state + ); + } + for state in not_remote_closed { + assert!( + !state.is_remote_closed(), + "{:?} should not be remote_closed", + state + ); + } + } +} diff --git a/src/net/handler/tcp/tcb.rs b/src/net/handler/tcp/tcb.rs new file mode 100644 index 0000000..3b96f8b --- /dev/null +++ b/src/net/handler/tcp/tcb.rs @@ -0,0 +1,571 @@ +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::{FRtoState, PrrState, SackRecovery}; +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 + } + } + + /// Mark this connection as having a pending ACK. + #[inline] + pub fn mark_ack_pending(&mut self) { + self.ack_pending = true; + } + + /// Set the pending FIN flag. + #[inline] + pub fn set_pending_fin(&mut self) { + self.pending_fin = true; + } + + /// Update the send window. Returns `true` when the window + /// transitions from zero to non-zero (peer un-stalls the sender). + #[inline] + pub fn update_send_window(&mut self, wnd: u32) -> bool { + let old = self.snd_wnd; + self.snd_wnd = wnd; + old == 0 && wnd > 0 + } +} + +#[cfg(test)] +mod tests { + use super::super::ring_buffer::RingBuffer; + use super::*; + + 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)); + } + + #[test] + fn update_send_window_zero_to_nonzero_returns_true() { + let mut tcb = make_tcb(false, 0, 0, 1024); + assert_eq!(tcb.snd_wnd, 0); + assert!(tcb.update_send_window(1000)); + } + + #[test] + fn update_send_window_nonzero_to_nonzero_returns_false() { + let mut tcb = make_tcb(false, 0, 0, 1024); + tcb.snd_wnd = 500; + assert!(!tcb.update_send_window(1000)); + } + + #[test] + fn update_send_window_nonzero_to_zero_returns_false() { + let mut tcb = make_tcb(false, 0, 0, 1024); + tcb.snd_wnd = 500; + assert!(!tcb.update_send_window(0)); + } + + #[test] + fn update_send_window_zero_to_zero_returns_false() { + let mut tcb = make_tcb(false, 0, 0, 1024); + assert!(!tcb.update_send_window(0)); + } +} 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..a3f39b2 --- /dev/null +++ b/src/net/handler/tcp/tests/congestion_tests.rs @@ -0,0 +1,254 @@ +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.first_connection().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.first_connection().cubic.cwnd; + let mss = handler.first_connection().eff_snd_mss; + + // Send data and get it ACKed. + handler + .first_connection_mut() + .send_buffer + .write(&[0xAA; 1460]); + handler.first_connection_mut().snd_wnd = 65535; + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + + // ACK the data. + let snd_nxt = handler.first_connection().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.first_connection().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.first_connection().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.first_connection_mut().eff_snd_mss as usize; + handler + .first_connection_mut() + .send_buffer + .write(&vec![0xAA; mss * 2]); + handler.first_connection_mut().snd_wnd = 65535; + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + + let cwnd_before_rto = handler.first_connection().cubic.cwnd; + + // Trigger RTO by setting deadline in the past. + handler.first_connection_mut().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 rx, &mut tx); + while tx.pop().is_some() {} + + // F-RTO should be active. + assert!( + handler.first_connection().frto.is_active(), + "F-RTO should be in Step1" + ); + assert_eq!( + handler.first_connection().cubic.cwnd, + mss as u32, + "cwnd should be 1 MSS after RTO" + ); + + // First ACK advances snd_una. + let snd_una = handler.first_connection().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.first_connection().frto.is_active(), + "F-RTO should be in Step2" + ); + + // Second ACK advances snd_una again => spurious RTO. + let snd_una = handler.first_connection().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.first_connection().frto.is_active(), + "F-RTO should be disabled" + ); + assert_eq!( + handler.first_connection().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..41414cb --- /dev/null +++ b/src/net/handler/tcp/tests/data_transfer.rs @@ -0,0 +1,813 @@ +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.first_connection().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.first_connection().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.first_connection().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.first_connection().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 rx, + &mut tx, + ); + assert_eq!(tx.num_frames(), 1, "ACK flushed by poll_send"); + + // Verify: data is in the receive ring buffer. + let tcb = handler.first_connection(); + 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.first_connection().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.first_connection().rcv_nxt, + 1001, + "rcv_nxt not advanced for OOO" + ); + assert_eq!(handler.first_connection().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.first_connection().rcv_nxt, + 1011, + "rcv_nxt advanced past both segments" + ); + assert_eq!( + handler.first_connection().ooo_ranges.len(), + 0, + "OOO ranges drained" + ); + assert_eq!(handler.first_connection().recv_buffer.available(), 10); + + // Read from recv buffer and verify contents. + let mut buf = [0u8; 10]; + handler.first_connection_mut().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.first_connection().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.first_connection_mut().send_buffer.write(payload); + + // Set snd_wnd so the window allows sending. + handler.first_connection_mut().snd_wnd = 65535; + + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + + assert_eq!(tx.num_frames(), 1, "data segment built"); + let tcb = handler.first_connection(); + 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.first_connection().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.first_connection_mut().send_buffer.write(b"Hello"); + + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &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.first_connection().eff_snd_mss as usize; + let big_data = vec![0x41u8; mss + 100]; + handler.first_connection_mut().send_buffer.write(&big_data); + + // Ensure cwnd is large enough to allow sending. + handler.first_connection_mut().cubic.cwnd = (mss as u32) * 10; + // Disable Nagle so the second (sub-MSS) segment can be sent. + handler.first_connection_mut().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 rx, &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.first_connection().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.first_connection().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.first_connection_mut().recv_buffer.write(&filler); + // Advance rcv_nxt to account for the filler (as if received normally). + handler.first_connection_mut().rcv_nxt = + handler.first_connection_mut().rcv_nxt.wrapping_add(20); + let rcv_nxt_before = handler.first_connection().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.first_connection().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.first_connection().recv_buffer.available(), + 32, + "recv buffer should be completely full (20 filler + 12 new)" + ); +} + +#[test] +fn fin_with_data_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)); + } + + let server_iss = establish_connection(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + // Send data+FIN in a single segment. + let payload = b"final data"; + let data_fin = build_tcp_frame_with_payload( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK | flags::FIN, + 65535, + &[], + payload, + ); + let data_fin_len = data_fin.len(); + handler.process_ipv4( + Frame::new(2, leak(data_fin), data_fin_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + let tcb = handler.first_connection(); + // Data should be received. + assert_eq!( + tcb.recv_buffer.available(), + payload.len(), + "data from FIN segment should be in recv buffer" + ); + // FIN consumes one sequence number. + assert_eq!( + tcb.rcv_nxt, + 1001 + payload.len() as u32 + 1, + "rcv_nxt should advance past data + FIN" + ); + // State should transition to CloseWait. + assert_eq!( + tcb.state, + TcpState::CloseWait, + "should transition to CloseWait" + ); +} + +#[test] +fn fast_path_with_timestamps() { + 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 { + let buf = leak(vec![0u8; 2048]); + free.push(Frame::new(100 + i, buf, 2048, false)); + } + + // Use active open with timestamps enabled (default config has timestamps=true). + let config = TcpConfig { + timestamps: true, + sack: false, + ecn: false, + ..TcpConfig::default() + }; + let _client_iss = + active_open_handshake_with_config(&mut handler, &nh, config, &mut free, &mut rx, &mut tx); + + // Simulate sending data so snd_nxt > snd_una. + let tcb = handler.first_connection_mut(); + tcb.send_buffer.write(&[0u8; 50]); + tcb.snd_nxt = tcb.snd_una.wrapping_add(50); + let snd_una = tcb.snd_una; + let rcv_nxt = tcb.rcv_nxt; + + // Ensure timestamps are enabled on the TCB. + tcb.ts_enabled = true; + tcb.ts_recent = 1000; + + // Build a data segment with valid timestamp (tsval > ts_recent), ACKing part of our data. + let ts_opts = build_ts_option(1100, 500); // tsval=1100 > ts_recent=1000 (PAWS ok) + let new_ack = snd_una.wrapping_add(25); // ack 25 bytes + let data_seg = build_tcp_frame_with_payload( + REMOTE_IP, + LOCAL_IP, + 80, + 5000, + rcv_nxt, + new_ack, + flags::ACK, + 65535, + &ts_opts, + b"fast path ts data", + ); + let data_seg_len = data_seg.len(); + handler.process_ipv4( + Frame::new(60, leak(data_seg), data_seg_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + let tcb = handler.first_connection(); + // If fast path was taken, data should be received and snd_una advanced. + assert_eq!( + tcb.snd_una, new_ack, + "snd_una should advance via fast path with timestamps" + ); + assert_eq!( + tcb.rcv_nxt, + rcv_nxt.wrapping_add(b"fast path ts data".len() as u32), + "rcv_nxt should advance for received data" + ); + assert_eq!( + tcb.ts_recent, 1100, + "ts_recent should be updated on fast path" + ); +} 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..8ea6af3 --- /dev/null +++ b/src/net/handler/tcp/tests/delayed_ack.rs @@ -0,0 +1,672 @@ +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.first_connection().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.first_connection(); + 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.first_connection().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.first_connection().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 rx, + &mut tx, + ); + assert_eq!(tx.num_frames(), 1, "poll_send flushes ACK"); + + let tcb = handler.first_connection(); + 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.first_connection().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.first_connection().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.first_connection().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.first_connection().state, TcpState::SynReceived); + + let server_iss = handler.first_connection().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.first_connection().state, TcpState::Established); + + // Verify delayed ACK and Nagle defaults. + let tcb = handler.first_connection(); + 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.first_connection().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 rx, &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 rx, &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.first_connection().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.first_connection().ack_pending, + "ack_pending should be true after receiving data" + ); + + // 3. Write data to send buffer. + handler + .first_connection_mut() + .send_buffer + .write(b"reply data"); + handler.first_connection_mut().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 rx, &mut tx); + assert!(tx.num_frames() >= 1, "data segment should be sent"); + + // 5. Verify delayed ACK state is cleared. + let tcb = handler.first_connection(); + 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..a321d98 --- /dev/null +++ b/src/net/handler/tcp/tests/ecn.rs @@ -0,0 +1,915 @@ +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.first_connection().ecn_enabled); + + // Send SYN-ACK with ECE (peer supports ECN). + let server_iss = 2000u32; + let client_iss = handler.first_connection().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.first_connection().ecn_enabled, + "ecn_enabled should be true after SYN-ACK with ECE" + ); + assert_eq!( + handler.first_connection().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.first_connection().ecn_enabled); + + // Send SYN-ACK WITHOUT ECE (peer doesn't support ECN). + let server_iss = 3000u32; + let client_iss = handler.first_connection().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.first_connection().ecn_enabled, + "ecn_enabled should be false after SYN-ACK without ECE" + ); + assert_eq!( + handler.first_connection().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.first_connection().ecn_enabled, + "ecn_enabled should be true on passive side" + ); + + // Complete handshake with final ACK. + let server_iss = handler.first_connection().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.first_connection().state, TcpState::Established); + assert!( + handler.first_connection().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.first_connection().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.first_connection_mut().ecn_enabled = true; + handler.first_connection_mut().snd_wnd = 65535; + + // Write data into the send buffer. + let payload = b"Hello ECN!"; + handler.first_connection_mut().send_buffer.write(payload); + + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &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.first_connection().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.first_connection_mut().ecn_enabled = true; + handler.first_connection_mut().snd_wnd = 65535; + handler + .first_connection_mut() + .send_buffer + .write(b"RTO test data"); + + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + // Drain the initial data segment. + while tx.pop().is_some() {} + + // Expire the retransmit timer to trigger RTO retransmit. + handler.first_connection_mut().retransmit_deadline = Some(now); + handler.poll_timers(now, nh.local_mac(), &nh, &mut free, &mut rx, &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.first_connection().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.first_connection_mut().ecn_enabled = true; + assert!( + !handler.first_connection().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.first_connection().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.first_connection().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.first_connection_mut().ecn_enabled = true; + handler.first_connection_mut().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.first_connection_mut().delayed_ack_deadline = Some(now); + handler.poll_timers(now, nh.local_mac(), &nh, &mut free, &mut rx, &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.first_connection().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.first_connection_mut().ecn_enabled = true; + handler.first_connection_mut().snd_wnd = 65535; + let mss = handler.first_connection().eff_snd_mss as u32; + handler.first_connection_mut().cubic.cwnd = 10 * mss; + handler.first_connection_mut().cubic.ssthresh = 20 * mss; + + // Send some data so snd_nxt advances. + handler + .first_connection_mut() + .send_buffer + .write(b"test data for ecn"); + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + + let snd_nxt = handler.first_connection().snd_nxt; + + // Record cwnd before receiving ECE. + let cwnd_before = handler.first_connection().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.first_connection().cubic.cwnd; + assert!( + cwnd_after < cwnd_before, + "cwnd should be reduced: before={}, after={}", + cwnd_before, + cwnd_after, + ); + assert_eq!( + handler.first_connection().cubic.cwnd, + handler.first_connection().cubic.ssthresh, + "cwnd should equal ssthresh after ECN response" + ); + assert!( + handler.first_connection().cubic.ssthresh >= 2 * mss, + "ssthresh should be at least 2*MSS" + ); + assert!( + handler.first_connection().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.first_connection().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.first_connection_mut().ecn_enabled = true; + handler.first_connection_mut().ecn_cwr_sent = true; + handler.first_connection_mut().snd_wnd = 65535; + + // Write data and poll_send. + handler + .first_connection_mut() + .send_buffer + .write(b"cwr test data"); + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &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.first_connection().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.first_connection().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.first_connection_mut().ecn_enabled = true; + handler.first_connection_mut().ecn_ce_received = true; + + // Receive a segment with CWR flag from peer (acknowledging our ECE). + let rcv_nxt = handler.first_connection().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.first_connection().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..ade5728 --- /dev/null +++ b/src/net/handler/tcp/tests/edge_cases.rs @@ -0,0 +1,1377 @@ +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.first_connection().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.first_connection().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.first_connection().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.first_connection().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.first_connection().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.first_connection().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.first_connection().snd_una; + let snd_nxt_before = handler.first_connection().snd_nxt; + let rcv_nxt_before = handler.first_connection().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.first_connection().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.first_connection().snd_una, + snd_una_before, + "snd_una must not change on future ACK" + ); + assert_eq!( + handler.first_connection().snd_nxt, + snd_nxt_before, + "snd_nxt must not change on future ACK" + ); + assert_eq!( + handler.first_connection().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.first_connection().snd_wnd, + 8000, + "window set to 8000 from seg B" + ); + assert_eq!( + handler.first_connection().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.first_connection().snd_wnd, + 8000, + "stale segment must not regress snd_wnd" + ); + assert_eq!( + handler.first_connection().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.first_connection().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.first_connection().eff_snd_mss; + + // Set max_snd_wnd high (simulates the peer previously advertised a large window). + handler.first_connection_mut().max_snd_wnd = 65535; + // Set current snd_wnd to 10 bytes — much less than MSS and max_snd_wnd/2. + handler.first_connection_mut().snd_wnd = 10; + + // Write more data than can_send (10 bytes) so data_available > can_send. + handler + .first_connection_mut() + .send_buffer + .write(&[0x41u8; 100]); + + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &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.first_connection_mut().snd_wnd = mss as u32; + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + assert_eq!( + tx.num_frames(), + 1, + "should send when can_send >= eff_snd_mss" + ); +} + +#[test] +fn handshake_with_wrapping_isn() { + // Verify that a 3-way handshake completes correctly when the client ISN is + // near u32::MAX so that rcv_nxt wraps around the 2^32 boundary. + 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(); + + // Client ISN chosen so that ISN + 1 = u32::MAX (one step before wrapping to 0). + let client_isn: u32 = u32::MAX - 1; + + // Step 1: SYN with wrapping ISN. + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + client_isn, + 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, + ); + + // Handler must have created a connection in SynReceived and sent a SYN-ACK. + assert_eq!(handler.connections.len(), 1, "connection created"); + assert_eq!(handler.first_connection().state, TcpState::SynReceived); + assert_eq!(tx.num_frames(), 1, "SYN-ACK generated"); + + // Inspect the SYN-ACK: ack_num must wrap (ISN + 1 mod 2^32 = 0). + let expected_ack = client_isn.wrapping_add(1); // == 0 + let syn_ack_frame = tx.pop().unwrap(); + let tcp_off = ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN; + let syn_ack_ack_num = + u32::from_be_bytes(syn_ack_frame[tcp_off + 8..tcp_off + 12].try_into().unwrap()); + assert_eq!( + syn_ack_ack_num, expected_ack, + "SYN-ACK ack_number must wrap correctly (expected {})", + expected_ack + ); + let syn_ack_flags = syn_ack_frame[tcp_off + 13]; + assert_eq!( + syn_ack_flags & (flags::SYN | flags::ACK), + flags::SYN | flags::ACK, + "SYN-ACK must have SYN and ACK flags set" + ); + + // Step 2: Complete the handshake with the final ACK. + let server_iss = handler.first_connection().iss; + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + client_isn.wrapping_add(1), // == 0 + 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.first_connection().state, + TcpState::Established, + "handshake must complete to Established" + ); + assert_eq!( + handler.first_connection().rcv_nxt, + client_isn.wrapping_add(1), + "rcv_nxt must be ISN+1 (wrapped)" + ); + assert_eq!(accept_queue.len(), 1, "connection in accept queue"); +} + +#[test] +fn data_transfer_across_sequence_wrap() { + // Verify that a data segment whose sequence numbers cross the u32::MAX boundary + // is received and ACKed correctly. + 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)); + } + + // Use an ISN 10 bytes before u32::MAX so that 20 bytes of data cross the wrap. + // ISN = u32::MAX - 10 → first data seq = ISN+1 = u32::MAX - 9 + // After 20 bytes: last byte seq = (ISN+1+19) mod 2^32 = u32::MAX - 9 + 19 = 10 + let client_isn: u32 = u32::MAX - 10; + + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + + // Manual handshake instead of establish_connection() because that helper + // hard-codes ISN=1000 — we need a custom ISN near u32::MAX. + + // SYN. + let syn_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + client_isn, + 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, + ); + assert_eq!(handler.first_connection().state, TcpState::SynReceived); + let server_iss = handler.first_connection().iss; + while tx.pop().is_some() {} + + // ACK completing handshake. + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + client_isn.wrapping_add(1), + 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.first_connection().state, TcpState::Established); + while tx.pop().is_some() {} + + // Send 20 bytes of data starting at seq = client_isn + 1. + // This crosses the u32::MAX boundary (10 bytes before wrap, 10 bytes after). + let payload = [0x42u8; 20]; + let data_seq = client_isn.wrapping_add(1); + let data_frame = build_tcp_frame_with_payload( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + data_seq, + server_iss.wrapping_add(1), + flags::ACK | flags::PSH, + 65535, + &[], + &payload, + ); + 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, + ); + + // rcv_nxt must have advanced by 20 bytes (wrapping). + let expected_rcv_nxt = data_seq.wrapping_add(20); + assert_eq!( + handler.first_connection().rcv_nxt, + expected_rcv_nxt, + "rcv_nxt must advance past wrap boundary" + ); + + // Flush delayed ACK: send a second data segment (triggers ack_pending), + // then call poll_send to emit the pure ACK. + let payload2 = [0x43u8; 1]; + let data2_seq = expected_rcv_nxt; + let data_frame2 = build_tcp_frame_with_payload( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + data2_seq, + server_iss.wrapping_add(1), + flags::ACK | flags::PSH, + 65535, + &[], + &payload2, + ); + let data2_len = data_frame2.len(); + handler.process_ipv4( + Frame::new(3, leak(data_frame2), data2_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // poll_send flushes the pending delayed ACK. + handler.poll_send( + coarsetime::Instant::now(), + nh.local_mac(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // An ACK must have been sent. + assert!( + tx.num_frames() > 0, + "ACK must be sent after poll_send flushes delayed ACK" + ); + + // The ACK frame's ack_num must equal rcv_nxt after both segments. + let ack_frame = tx.pop().unwrap(); + let tcp_off = ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN; + let ack_num = u32::from_be_bytes(ack_frame[tcp_off + 8..tcp_off + 12].try_into().unwrap()); + let expected_final_rcv_nxt = expected_rcv_nxt.wrapping_add(1); + assert_eq!( + ack_num, expected_final_rcv_nxt, + "ACK ack_num must reflect all received data ({})", + expected_final_rcv_nxt + ); + assert_eq!( + handler.first_connection().rcv_nxt, + expected_final_rcv_nxt, + "rcv_nxt must reflect all received data after wrap" + ); +} + +#[test] +fn remove_connection_sends_rst_with_correct_seq() { + 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); + + // snd_nxt after handshake = server_iss + 1 (SYN consumed one sequence number). + let expected_seq = server_iss.wrapping_add(1); + + // Capture the connection id before calling remove_connection. + let id = handler.first_connection().id; + + // MAC addresses matching what the test infrastructure uses. + let src_mac = + crate::net::wire::ethernet::MacAddress::from([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]); + let dst_mac = + crate::net::wire::ethernet::MacAddress::from([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]); + + handler.remove_connection(&id, src_mac, dst_mac, &mut free, &mut tx); + + // The connection should be removed. + assert_eq!(handler.connections.len(), 0, "connection removed"); + + // A RST frame should have been emitted. + assert_eq!( + tx.num_frames(), + 1, + "RST frame generated on remove_connection" + ); + + // Parse the RST frame and check its sequence number. + let rst_frame = tx.pop().unwrap(); + let tcp_off = ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN; + let rst_seq = u32::from_be_bytes(rst_frame[tcp_off + 4..tcp_off + 8].try_into().unwrap()); + let rst_flags = rst_frame[tcp_off + 13]; + + assert_eq!( + rst_flags & flags::RST, + flags::RST, + "frame must have RST flag set" + ); + assert_ne!( + rst_seq, 0, + "RST sequence number must not be 0 (would be silently dropped by peer)" + ); + assert_eq!( + rst_seq, expected_seq, + "RST seq must equal snd_nxt = server_iss + 1 = {}", + expected_seq + ); +} + +#[test] +fn rst_exact_match_resets_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)); + } + + let server_iss = establish_connection(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + // rcv_nxt is 1001. Send RST with seq = 1001 (exact match). + let rst_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, // == rcv_nxt (exact match) + 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 be removed — exact-match RST resets the connection. + assert_eq!( + handler.connections.len(), + 0, + "exact-match RST must reset and remove the connection" + ); +} + +#[test] +fn duplicate_data_sends_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 data to advance rcv_nxt. + 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, + ); + while tx.pop().is_some() {} + assert_eq!(handler.first_connection().rcv_nxt, 1006); + + // Now send duplicate data with seg_seq < rcv_nxt (already received). + let dup_data = build_tcp_frame_with_payload( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, // < rcv_nxt (1006) + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + b"hello", + ); + let dup_len = dup_data.len(); + handler.process_ipv4( + Frame::new(3, leak(dup_data), dup_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // Should respond with an ACK for duplicate data. + assert!( + tx.num_frames() > 0, + "duplicate data (seg_seq < rcv_nxt) should trigger ACK" + ); + // rcv_nxt must not change. + assert_eq!( + handler.first_connection().rcv_nxt, + 1006, + "rcv_nxt must not change for duplicate data" + ); +} + +#[test] +fn out_of_window_non_rst_sends_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 data segment with seq far outside the window (not RST). + let oow_data = build_tcp_frame_with_payload( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 999_999, // way outside window + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + b"oow", + ); + let oow_len = oow_data.len(); + handler.process_ipv4( + Frame::new(50, leak(oow_data), oow_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // Should respond with an ACK (not silently dropped like RST). + assert!( + tx.num_frames() > 0, + "out-of-window non-RST segment should trigger ACK" + ); + // Connection must survive. + assert_eq!(handler.first_connection().state, TcpState::Established); +} + +#[test] +fn recovery_exit_on_full_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)); + } + + let _server_iss = establish_connection(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + let tcb = handler.first_connection_mut(); + tcb.eff_snd_mss = 100; + let mss = tcb.eff_snd_mss as usize; + + // Write 4 MSS of data and simulate 4 segments sent. + let data_len = 4 * mss; + tcb.send_buffer.write(&vec![0xAA; data_len]); + tcb.snd_wnd = 65535; + tcb.snd_nxt = tcb.snd_una.wrapping_add(data_len as u32); + let snd_una = tcb.snd_una; + let recovery_point = tcb.snd_nxt; + + // Enter recovery. + tcb.recovery.enter(recovery_point); + tcb.prr.enter(recovery_point.wrapping_sub(snd_una)); + tcb.cubic.on_loss(); + assert!(handler.first_connection().recovery.in_recovery); + + // Send a full ACK that covers the recovery_point. + let full_ack = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + recovery_point, // ACK up to recovery_point + flags::ACK, + 65535, + &[], + ); + let full_ack_len = full_ack.len(); + handler.process_ipv4( + Frame::new(20, leak(full_ack), full_ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // Recovery should have exited. + assert!( + !handler.first_connection().recovery.in_recovery, + "full ACK covering recovery_point should exit recovery" + ); +} + +#[test] +fn persist_timer_cleared_on_new_ack_with_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); + + // Set up: send some data so snd_nxt > snd_una, then arm persist timer. + let tcb = handler.first_connection_mut(); + tcb.send_buffer.write(&[0u8; 100]); + tcb.snd_nxt = tcb.snd_una.wrapping_add(100); + tcb.persist_deadline = Some(coarsetime::Instant::now()); + tcb.persist_backoff = 3; + let snd_una = tcb.snd_una; + + // Send a new ACK that advances snd_una, with a non-zero window. + let new_ack = snd_una.wrapping_add(50); + let ack_frame = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + new_ack, + flags::ACK, + 65535, // non-zero window + &[], + ); + let ack_len = ack_frame.len(); + handler.process_ipv4( + Frame::new(5, leak(ack_frame), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + let tcb = handler.first_connection(); + assert_eq!(tcb.snd_una, new_ack, "snd_una should advance"); + assert!( + tcb.persist_deadline.is_none(), + "persist timer should be cleared when window reopens on new ACK" + ); + assert_eq!(tcb.persist_backoff, 0, "persist backoff should be reset"); +} + +#[test] +fn rtt_update_with_existing_srtt_slow_path() { + 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); + + // Set up: some data in flight, with an existing SRTT. + let tcb = handler.first_connection_mut(); + tcb.send_buffer.write(&[0u8; 100]); + tcb.snd_nxt = tcb.snd_una.wrapping_add(100); + tcb.srtt = Some(50); // existing SRTT of 50ms + tcb.rttvar = 25; + tcb.last_send_time = Some(coarsetime::Instant::now()); + let snd_una = tcb.snd_una; + + // Send a new ACK to trigger RTT update with existing SRTT. + let new_ack = snd_una.wrapping_add(50); + let ack_frame = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + new_ack, + flags::ACK, + 65535, + &[], + ); + let ack_len = ack_frame.len(); + handler.process_ipv4( + Frame::new(5, leak(ack_frame), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + let tcb = handler.first_connection(); + // SRTT should have been updated (from the existing 50ms). + assert!(tcb.srtt.is_some(), "SRTT should still be set"); + // last_send_time should be consumed. + assert!( + tcb.last_send_time.is_none(), + "last_send_time should be consumed after RTT measurement" + ); +} + +#[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.first_connection().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.first_connection_mut().max_snd_wnd = 65535; + handler.first_connection_mut().snd_wnd = 5; + + // Write only 3 bytes — all data fits in the window. + handler.first_connection_mut().send_buffer.write(b"abc"); + + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &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/handler.rs b/src/net/handler/tcp/tests/handler.rs new file mode 100644 index 0000000..2c1db28 --- /dev/null +++ b/src/net/handler/tcp/tests/handler.rs @@ -0,0 +1,392 @@ +use std::collections::BTreeMap; + +use coarsetime::Instant; + +use crate::{ + net::{ + handler::tcp::{ + congestion::CubicState, + recovery::{FRtoState, PrrState, SackRecovery}, + ring_buffer::RingBuffer, + state::TcpState, + tcb::{ConnectionId, DEFAULT_DELAYED_ACK_MS, DEFAULT_RCV_MSS, Tcb}, + }, + socket::LocalQueue, + wire::{ethernet::MacAddress, ip::IpAddress}, + }, + xdp::frame::{BasicFrameBuffer, FrameBuffer}, +}; + +use super::{ + LOCAL_IP, REMOTE_IP, alloc_free_frame, establish_connection, new_handler, new_neighbor_handler, +}; + +/// Build a minimal Tcb for unit testing handler methods. +fn make_test_tcb(state: TcpState, local_port: u16, remote_port: u16) -> Tcb { + let now = Instant::now(); + Tcb { + id: ConnectionId { + local_addr: IpAddress::V4(LOCAL_IP), + local_port, + remote_addr: IpAddress::V4(REMOTE_IP), + remote_port, + }, + state, + from_passive_open: false, + iss: 1000, + snd_una: 1000, + snd_nxt: 1001, + snd_wnd: 65535, + snd_wl1: 0, + snd_wl2: 0, + irs: 2000, + rcv_nxt: 2001, + rcv_wnd: 65535, + snd_mss: DEFAULT_RCV_MSS, + rcv_mss: DEFAULT_RCV_MSS, + eff_snd_mss: DEFAULT_RCV_MSS, + snd_wscale: 0, + rcv_wscale: 7, + wscale_enabled: false, + retransmit_deadline: None, + rto_backoff: 0, + event_queue: LocalQueue::new(16), + send_buffer: RingBuffer::new(1024), + recv_buffer: RingBuffer::new(1024), + 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: now, + keep_alive_probes_sent: 0, + linger: None, + linger_deadline: None, + ts_enabled: false, + ts_recent: 0, + ts_recent_age: now, + ts_offset: 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, + } +} + +// --------------------------------------------------------------------------- +// insert_connection / get_connection / get_by_key +// --------------------------------------------------------------------------- + +#[test] +fn insert_and_retrieve_by_id() { + let mut handler = new_handler(); + let tcb = make_test_tcb(TcpState::Established, 5000, 80); + let id = tcb.id; + let key = handler.insert_connection(tcb); + + // Retrieve by ConnectionId. + let conn = handler + .get_connection(&id) + .expect("connection should exist"); + assert_eq!(conn.state, TcpState::Established); + assert_eq!(conn.id, id); + + // Retrieve by slab key. + let conn2 = handler + .get_by_key(key) + .expect("connection should exist by key"); + assert_eq!(conn2.id, id); +} + +#[test] +fn insert_and_retrieve_mut_by_id() { + let mut handler = new_handler(); + let tcb = make_test_tcb(TcpState::Established, 5000, 80); + let id = tcb.id; + handler.insert_connection(tcb); + + let conn = handler + .get_connection_mut(&id) + .expect("connection should exist"); + conn.state = TcpState::CloseWait; + assert_eq!( + handler.get_connection(&id).unwrap().state, + TcpState::CloseWait + ); +} + +#[test] +fn insert_and_retrieve_mut_by_key() { + let mut handler = new_handler(); + let tcb = make_test_tcb(TcpState::Established, 5000, 80); + let key = handler.insert_connection(tcb); + + let conn = handler.get_by_key_mut(key).expect("should exist"); + conn.state = TcpState::FinWait1; + assert_eq!(handler.get_by_key(key).unwrap().state, TcpState::FinWait1); +} + +#[test] +fn connection_key_lookup() { + let mut handler = new_handler(); + let tcb = make_test_tcb(TcpState::Established, 5000, 80); + let id = tcb.id; + let key = handler.insert_connection(tcb); + + assert_eq!(handler.connection_key(&id), Some(key)); + + // Unknown id returns None. + let unknown = ConnectionId { + local_addr: IpAddress::V4(LOCAL_IP), + local_port: 9999, + remote_addr: IpAddress::V4(REMOTE_IP), + remote_port: 80, + }; + assert_eq!(handler.connection_key(&unknown), None); +} + +#[test] +fn get_connection_nonexistent_returns_none() { + let handler = new_handler(); + let id = ConnectionId { + local_addr: IpAddress::V4(LOCAL_IP), + local_port: 5000, + remote_addr: IpAddress::V4(REMOTE_IP), + remote_port: 80, + }; + assert!(handler.get_connection(&id).is_none()); + assert!(handler.get_by_key(999).is_none()); +} + +// --------------------------------------------------------------------------- +// remove_connection_by_key +// --------------------------------------------------------------------------- + +#[test] +fn remove_connection_by_key_returns_tcb() { + let mut handler = new_handler(); + let tcb = make_test_tcb(TcpState::Established, 5000, 80); + let id = tcb.id; + let key = handler.insert_connection(tcb); + + let removed = handler.remove_connection_by_key(key); + assert!(removed.is_some()); + assert_eq!(removed.unwrap().id, id); + + // After removal, lookups return None. + assert!(handler.get_connection(&id).is_none()); + assert!(handler.get_by_key(key).is_none()); +} + +#[test] +fn remove_connection_by_key_nonexistent_returns_none() { + let mut handler = new_handler(); + assert!(handler.remove_connection_by_key(42).is_none()); +} + +// --------------------------------------------------------------------------- +// remove_connection (with RST generation) +// --------------------------------------------------------------------------- + +#[test] +fn remove_connection_synchronized_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); + + // Establish a real connection via 3-way handshake. + let _server_iss = establish_connection(&mut handler, &nh, &mut free, &mut rx, &mut tx); + assert_eq!(handler.first_connection().state, TcpState::Established); + + let id = handler.first_connection().id; + let src_mac = MacAddress::from([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]); + let dst_mac = MacAddress::from([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]); + + // Provide free frames for RST construction. + free.push(alloc_free_frame(100)); + + handler.remove_connection(&id, src_mac, dst_mac, &mut free, &mut tx); + + // Connection should be gone. + assert!(handler.get_connection(&id).is_none()); + + // RST should have been placed on tx. + assert!(tx.pop().is_some(), "RST frame should be on tx queue"); +} + +#[test] +fn remove_connection_syn_received_generates_rst() { + let mut handler = new_handler(); + let tcb = make_test_tcb(TcpState::SynReceived, 80, 12345); + let id = tcb.id; + handler.insert_connection(tcb); + + let src_mac = MacAddress::from([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]); + let dst_mac = MacAddress::from([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]); + let mut free = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + free.push(alloc_free_frame(200)); + handler.remove_connection(&id, src_mac, dst_mac, &mut free, &mut tx); + + assert!(handler.get_connection(&id).is_none()); + assert!(tx.pop().is_some(), "RST should be sent for SynReceived"); +} + +#[test] +fn remove_connection_syn_sent_no_rst() { + let mut handler = new_handler(); + let tcb = make_test_tcb(TcpState::SynSent, 5000, 80); + let id = tcb.id; + handler.insert_connection(tcb); + + let src_mac = MacAddress::from([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]); + let dst_mac = MacAddress::from([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]); + let mut free = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + handler.remove_connection(&id, src_mac, dst_mac, &mut free, &mut tx); + + assert!(handler.get_connection(&id).is_none()); + assert!(tx.pop().is_none(), "No RST for SynSent (not synchronized)"); +} + +#[test] +fn remove_connection_not_found_is_noop() { + let mut handler = new_handler(); + let id = ConnectionId { + local_addr: IpAddress::V4(LOCAL_IP), + local_port: 9999, + remote_addr: IpAddress::V4(REMOTE_IP), + remote_port: 80, + }; + let src_mac = MacAddress::from([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]); + let dst_mac = MacAddress::from([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]); + let mut free = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + // Should not panic — just returns early. + handler.remove_connection(&id, src_mac, dst_mac, &mut free, &mut tx); + assert!(tx.pop().is_none()); +} + +// --------------------------------------------------------------------------- +// write_to_send_buffer +// --------------------------------------------------------------------------- + +#[test] +fn write_to_send_buffer_inserts_data_and_marks_send() { + let mut handler = new_handler(); + let tcb = make_test_tcb(TcpState::Established, 5000, 80); + let key = handler.insert_connection(tcb); + + let n = handler.write_to_send_buffer(key, b"hello").unwrap(); + assert_eq!(n, 5); + + // Verify data is in the send buffer via available(). + let conn = handler.get_by_key(key).unwrap(); + assert_eq!(conn.send_buffer.available(), 5); +} + +#[test] +fn write_to_send_buffer_nonexistent_returns_none() { + let mut handler = new_handler(); + assert!(handler.write_to_send_buffer(999, b"hello").is_none()); +} + +#[test] +fn write_to_send_buffer_empty_data_returns_zero() { + let mut handler = new_handler(); + let tcb = make_test_tcb(TcpState::Established, 5000, 80); + let key = handler.insert_connection(tcb); + + let n = handler.write_to_send_buffer(key, b"").unwrap(); + assert_eq!(n, 0); +} + +// --------------------------------------------------------------------------- +// splice_buffers +// --------------------------------------------------------------------------- + +#[test] +fn splice_buffers_transfers_recv_to_send() { + let mut handler = new_handler(); + let tcb = make_test_tcb(TcpState::Established, 5000, 80); + let key = handler.insert_connection(tcb); + + // Write directly into recv_buffer. + handler + .get_by_key_mut(key) + .unwrap() + .recv_buffer + .write(b"payload"); + + let (n, is_closed) = handler.splice_buffers(key, 1024).unwrap(); + assert_eq!(n, 7); + assert!(!is_closed); + + // recv_buffer should be drained, send_buffer should have the data. + let conn = handler.get_by_key(key).unwrap(); + assert_eq!(conn.recv_buffer.available(), 0); + assert_eq!(conn.send_buffer.available(), 7); +} + +#[test] +fn splice_buffers_reports_remote_closed() { + let mut handler = new_handler(); + let tcb = make_test_tcb(TcpState::CloseWait, 5000, 80); + let key = handler.insert_connection(tcb); + + handler.get_by_key_mut(key).unwrap().recv_buffer.write(b"x"); + let (n, is_closed) = handler.splice_buffers(key, 1024).unwrap(); + assert_eq!(n, 1); + assert!(is_closed, "CloseWait means remote side has closed"); +} + +#[test] +fn splice_buffers_nonexistent_returns_none() { + let mut handler = new_handler(); + assert!(handler.splice_buffers(999, 1024).is_none()); +} + +// --------------------------------------------------------------------------- +// Test helpers: first_connection, first_connection_mut, first_connection_key +// --------------------------------------------------------------------------- + +#[test] +fn first_connection_helpers() { + let mut handler = new_handler(); + let tcb = make_test_tcb(TcpState::Established, 5000, 80); + let expected_id = tcb.id; + let key = handler.insert_connection(tcb); + + assert_eq!(handler.first_connection().id, expected_id); + assert_eq!(handler.first_connection_key(), key); + + handler.first_connection_mut().state = TcpState::FinWait1; + assert_eq!(handler.first_connection().state, TcpState::FinWait1); +} diff --git a/src/net/handler/tcp/tests/handshake.rs b/src/net/handler/tcp/tests/handshake.rs new file mode 100644 index 0000000..7b3f1a0 --- /dev/null +++ b/src/net/handler/tcp/tests/handshake.rs @@ -0,0 +1,1577 @@ +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.first_connection().state, TcpState::SynReceived); + assert_eq!(handler.first_connection().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.first_connection().state, TcpState::SynReceived); + + // Get ISS from the TCB. + let server_iss = handler.first_connection().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.first_connection().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.first_connection().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.first_connection().wscale_enabled); + assert_eq!(handler.first_connection().snd_wscale, 7); + assert_eq!(handler.first_connection().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 (_key_a, 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 (_key_b, 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.first_connection().state, TcpState::SynSent); + assert_eq!(handler_b.first_connection().state, TcpState::SynSent); + + let iss_a = handler_a.first_connection().iss; + let iss_b = handler_b.first_connection().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.first_connection().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.first_connection().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.first_connection().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.first_connection().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"); +} + +#[test] +fn bad_seq_in_syn_received_sends_challenge_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(); + + // SYN to create connection in SynReceived. + 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, + ); + assert_eq!(handler.first_connection().state, TcpState::SynReceived); + while tx.pop().is_some() {} + + let server_iss = handler.first_connection().iss; + let rcv_nxt = handler.first_connection().rcv_nxt; + + // Send ACK with bad sequence number (far from rcv_nxt). + let bad_seq = rcv_nxt.wrapping_add(999); + let bad_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + bad_seq, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + ); + let bad_len = bad_data.len(); + handler.process_ipv4( + Frame::new(2, leak(bad_data), bad_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // Should still be in SynReceived and a challenge ACK should be emitted. + assert_eq!(handler.first_connection().state, TcpState::SynReceived); + assert_eq!(tx.num_frames(), 1, "challenge ACK emitted"); +} + +#[test] +fn bad_seq_rst_in_syn_received_is_ignored() { + 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(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(handler.connections.len(), 1); + while tx.pop().is_some() {} + + let rcv_nxt = handler.first_connection().rcv_nxt; + + // RST with bad sequence number — should be silently ignored (no challenge ACK for RST). + let bad_seq = rcv_nxt.wrapping_add(999); + let rst_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + bad_seq, + 0, + flags::RST, + 0, + &[], + ); + 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, + ); + + // Connection should still exist (RST ignored) and no challenge ACK sent. + assert_eq!(handler.connections.len(), 1, "connection not removed"); + assert_eq!(tx.num_frames(), 0, "no challenge ACK for out-of-window RST"); +} + +#[test] +fn rst_in_syn_received_active_open_signals_refused() { + 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 ip_a = LOCAL_IP; + let ip_b = REMOTE_IP; + let port_a: u16 = 5000; + let port_b: u16 = 6000; + let src_mac = MacAddress::from([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]); + let dst_mac = MacAddress::from([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]); + + nh.seed_cache(coarsetime::Instant::now(), IpAddress::V4(ip_b), dst_mac); + + // Active open: connect sends SYN, puts connection in SynSent. + let (_key, events) = handler + .connect( + IpAddress::V4(ip_a), + port_a, + IpAddress::V4(ip_b), + port_b, + src_mac, + dst_mac, + coarsetime::Instant::now(), + &mut free, + &mut tx, + ) + .unwrap(); + while tx.pop().is_some() {} + + let _iss_a = handler.first_connection().iss; + + // Feed a SYN from B (simultaneous open) to move A to SynReceived. + let syn_b = build_tcp_frame( + ip_b, + ip_a, + port_b, + port_a, + 2000, + 0, + flags::SYN, + 65535, + &[0x02, 0x04, 0x05, 0xB4], + ); + let syn_b_len = syn_b.len(); + handler.process_ipv4( + Frame::new(10, leak(syn_b), syn_b_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(handler.first_connection().state, TcpState::SynReceived); + assert!(!handler.first_connection().from_passive_open); + while tx.pop().is_some() {} + while rx.pop().is_some() {} + + // Now send RST with correct seq (rcv_nxt). + let rcv_nxt = handler.first_connection().rcv_nxt; + let rst_data = build_tcp_frame(ip_b, ip_a, port_b, port_a, rcv_nxt, 0, flags::RST, 0, &[]); + let rst_len = rst_data.len(); + handler.process_ipv4( + Frame::new(11, leak(rst_data), rst_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + assert_eq!(handler.connections.len(), 0, "connection removed"); + // Should have ConnectionRefused event. + let event = events.pop(); + assert!(event.is_some(), "event emitted"); + assert!( + matches!(event.unwrap(), TcpEvent::ConnectionRefused), + "ConnectionRefused event" + ); +} + +#[test] +fn syn_retransmit_in_syn_received_passive_resends_syn_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(); + + // Original SYN. + 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, + ); + assert_eq!(handler.first_connection().state, TcpState::SynReceived); + assert_eq!(tx.num_frames(), 1, "first SYN-ACK"); + while tx.pop().is_some() {} + while rx.pop().is_some() {} + + // Retransmit the same SYN (same seq, pure SYN no ACK). + // The seq matches rcv_nxt - 1 via the SYN special case: seg_seq + 1 == rcv_nxt. + let syn_retransmit = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1000, + 0, + flags::SYN, + 65535, + &[], + ); + let syn_rt_len = syn_retransmit.len(); + handler.process_ipv4( + Frame::new(2, leak(syn_retransmit), syn_rt_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // Should still be SynReceived and a SYN-ACK re-sent. + assert_eq!(handler.first_connection().state, TcpState::SynReceived); + assert_eq!(tx.num_frames(), 1, "SYN-ACK retransmitted"); +} + +#[test] +fn syn_retransmit_in_syn_received_active_sends_challenge_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)); + } + + let ip_a = LOCAL_IP; + let ip_b = REMOTE_IP; + let port_a: u16 = 5000; + let port_b: u16 = 6000; + let src_mac = MacAddress::from([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]); + let dst_mac = MacAddress::from([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]); + + nh.seed_cache(coarsetime::Instant::now(), IpAddress::V4(ip_b), dst_mac); + + // Active open. + let (_key, _events) = handler + .connect( + IpAddress::V4(ip_a), + port_a, + IpAddress::V4(ip_b), + port_b, + src_mac, + dst_mac, + coarsetime::Instant::now(), + &mut free, + &mut tx, + ) + .unwrap(); + while tx.pop().is_some() {} + + // Feed B's SYN to move A to SynReceived (simultaneous open). + let syn_b = build_tcp_frame( + ip_b, + ip_a, + port_b, + port_a, + 2000, + 0, + flags::SYN, + 65535, + &[0x02, 0x04, 0x05, 0xB4], + ); + let syn_b_len = syn_b.len(); + handler.process_ipv4( + Frame::new(10, leak(syn_b), syn_b_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(handler.first_connection().state, TcpState::SynReceived); + assert!(!handler.first_connection().from_passive_open); + while tx.pop().is_some() {} + while rx.pop().is_some() {} + + // Retransmit B's SYN again (same seq, pure SYN). + let syn_b2 = build_tcp_frame( + ip_b, + ip_a, + port_b, + port_a, + 2000, + 0, + flags::SYN, + 65535, + &[0x02, 0x04, 0x05, 0xB4], + ); + let syn_b2_len = syn_b2.len(); + handler.process_ipv4( + Frame::new(11, leak(syn_b2), syn_b2_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // Active open: sends challenge ACK, not SYN-ACK. + assert_eq!(handler.first_connection().state, TcpState::SynReceived); + assert_eq!( + tx.num_frames(), + 1, + "challenge ACK emitted for active open SYN retransmit" + ); +} + +#[test] +fn bad_ack_in_syn_received_sends_rst() { + 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(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(handler.first_connection().state, TcpState::SynReceived); + while tx.pop().is_some() {} + + let rcv_nxt = handler.first_connection().rcv_nxt; + + // ACK with bad ack number (ack=0, which is before snd_una). + let bad_ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + rcv_nxt, + 0, + flags::ACK, + 65535, + &[], + ); + let bad_ack_len = bad_ack_data.len(); + handler.process_ipv4( + Frame::new(2, leak(bad_ack_data), bad_ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // Should still be SynReceived (bad ACK does not transition) and RST emitted. + assert_eq!(handler.first_connection().state, TcpState::SynReceived); + assert_eq!(tx.num_frames(), 1, "RST sent for bad ACK"); +} + +#[test] +fn syn_ack_without_timestamp_disables_timestamps() { + 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 src_mac = MacAddress::from([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]); + let dst_mac = MacAddress::from([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]); + + // Active open with timestamps enabled (default). + let (_key, _events) = handler + .connect( + IpAddress::V4(LOCAL_IP), + 5000, + IpAddress::V4(REMOTE_IP), + 80, + src_mac, + dst_mac, + coarsetime::Instant::now(), + &mut free, + &mut tx, + ) + .unwrap(); + while tx.pop().is_some() {} + + assert!( + handler.first_connection().ts_enabled, + "timestamps enabled after connect" + ); + let client_iss = handler.first_connection().iss; + + // SYN-ACK without timestamp option → timestamps should be disabled. + let syn_ack = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 80, + 5000, + 2000, + client_iss.wrapping_add(1), + flags::SYN | flags::ACK, + 65535, + &[0x02, 0x04, 0x05, 0xB4], // MSS only, no timestamp + ); + let syn_ack_len = syn_ack.len(); + handler.process_ipv4( + Frame::new(50, leak(syn_ack), syn_ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + assert_eq!(handler.first_connection().state, TcpState::Established); + assert!( + !handler.first_connection().ts_enabled, + "timestamps disabled when peer omits TS option" + ); + // eff_snd_mss should NOT subtract TS_OPTION_LEN when timestamps are disabled. + // base_mss = min(peer_mss=1460, rcv_mss); no TS overhead. + assert_eq!( + handler.first_connection().eff_snd_mss, + handler + .first_connection() + .snd_mss + .min(handler.first_connection().rcv_mss) + ); +} + +#[test] +fn syn_ack_without_sack_permitted_disables_sack() { + 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 src_mac = MacAddress::from([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]); + let dst_mac = MacAddress::from([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]); + + // Active open with SACK enabled (default). + let (_key, _events) = handler + .connect( + IpAddress::V4(LOCAL_IP), + 5000, + IpAddress::V4(REMOTE_IP), + 80, + src_mac, + dst_mac, + coarsetime::Instant::now(), + &mut free, + &mut tx, + ) + .unwrap(); + while tx.pop().is_some() {} + + assert!( + handler.first_connection().sack_enabled, + "SACK enabled after connect" + ); + let client_iss = handler.first_connection().iss; + + // SYN-ACK without SACK-permitted option → SACK should be disabled. + let syn_ack = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 80, + 5000, + 2000, + client_iss.wrapping_add(1), + flags::SYN | flags::ACK, + 65535, + &[0x02, 0x04, 0x05, 0xB4], // MSS only, no SACK-permitted + ); + let syn_ack_len = syn_ack.len(); + handler.process_ipv4( + Frame::new(50, leak(syn_ack), syn_ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + assert_eq!(handler.first_connection().state, TcpState::Established); + assert!( + !handler.first_connection().sack_enabled, + "SACK disabled when peer omits SACK-permitted" + ); +} + +#[test] +fn syn_ack_without_ece_disables_ecn() { + 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 src_mac = MacAddress::from([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]); + let dst_mac = MacAddress::from([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]); + + // Active open with ECN enabled (default). + let (_key, _events) = handler + .connect( + IpAddress::V4(LOCAL_IP), + 5000, + IpAddress::V4(REMOTE_IP), + 80, + src_mac, + dst_mac, + coarsetime::Instant::now(), + &mut free, + &mut tx, + ) + .unwrap(); + while tx.pop().is_some() {} + + assert!( + handler.first_connection().ecn_enabled, + "ECN enabled after connect" + ); + let client_iss = handler.first_connection().iss; + + // SYN-ACK without ECE flag → ECN should be disabled. + let syn_ack = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 80, + 5000, + 2000, + client_iss.wrapping_add(1), + flags::SYN | flags::ACK, // no ECE bit + 65535, + &[0x02, 0x04, 0x05, 0xB4], + ); + let syn_ack_len = syn_ack.len(); + handler.process_ipv4( + Frame::new(50, leak(syn_ack), syn_ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + assert_eq!(handler.first_connection().state, TcpState::Established); + assert!( + !handler.first_connection().ecn_enabled, + "ECN disabled when SYN-ACK lacks ECE" + ); +} + +#[test] +fn unacceptable_ack_in_syn_sent_sends_rst() { + 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 src_mac = MacAddress::from([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]); + let dst_mac = MacAddress::from([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]); + + let (_key, _events) = handler + .connect( + IpAddress::V4(LOCAL_IP), + 5000, + IpAddress::V4(REMOTE_IP), + 80, + src_mac, + dst_mac, + coarsetime::Instant::now(), + &mut free, + &mut tx, + ) + .unwrap(); + while tx.pop().is_some() {} + + let client_iss = handler.first_connection().iss; + + // Send SYN-ACK with ack_seq = ISS (not ISS+1), which is unacceptable + // because seg_ack <= ISS triggers the check. + let bad_ack = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 80, + 5000, + 2000, + client_iss, // ack = ISS, unacceptable (must be > ISS) + flags::SYN | flags::ACK, + 65535, + &[0x02, 0x04, 0x05, 0xB4], + ); + 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, + ); + + // Should still be in SynSent (unacceptable ACK is dropped after sending RST). + assert_eq!(handler.first_connection().state, TcpState::SynSent); + assert_eq!(tx.num_frames(), 1, "RST sent for unacceptable ACK"); +} + +#[test] +fn unacceptable_ack_too_high_in_syn_sent_sends_rst() { + 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 src_mac = MacAddress::from([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]); + let dst_mac = MacAddress::from([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]); + + let (_key, _events) = handler + .connect( + IpAddress::V4(LOCAL_IP), + 5000, + IpAddress::V4(REMOTE_IP), + 80, + src_mac, + dst_mac, + coarsetime::Instant::now(), + &mut free, + &mut tx, + ) + .unwrap(); + while tx.pop().is_some() {} + + let snd_nxt = handler.first_connection().snd_nxt; + + // Send SYN-ACK with ack_seq far beyond snd_nxt (unacceptable). + let bad_ack = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 80, + 5000, + 2000, + snd_nxt.wrapping_add(100), // ack too high + flags::SYN | flags::ACK, + 65535, + &[0x02, 0x04, 0x05, 0xB4], + ); + 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, + ); + + assert_eq!(handler.first_connection().state, TcpState::SynSent); + assert_eq!(tx.num_frames(), 1, "RST sent for ack beyond snd_nxt"); +} + +#[test] +fn simultaneous_open_syn_without_ack_transitions_to_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 src_mac = MacAddress::from([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]); + let dst_mac = MacAddress::from([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]); + + let (_key, _events) = handler + .connect( + IpAddress::V4(LOCAL_IP), + 5000, + IpAddress::V4(REMOTE_IP), + 80, + src_mac, + dst_mac, + coarsetime::Instant::now(), + &mut free, + &mut tx, + ) + .unwrap(); + while tx.pop().is_some() {} + + assert_eq!(handler.first_connection().state, TcpState::SynSent); + + // Receive a bare SYN (no ACK) — simultaneous open. + let peer_syn = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 80, + 5000, + 3000, + 0, + flags::SYN, // SYN only, no ACK + 65535, + &[0x02, 0x04, 0x05, 0xB4], // MSS=1460 + ); + let peer_syn_len = peer_syn.len(); + handler.process_ipv4( + Frame::new(50, leak(peer_syn), peer_syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // Should transition to SynReceived (simultaneous open path). + assert_eq!(handler.first_connection().state, TcpState::SynReceived); + assert!( + !handler.first_connection().from_passive_open, + "simultaneous open sets from_passive_open=false" + ); + assert_eq!(handler.first_connection().irs, 3000); + assert_eq!(handler.first_connection().rcv_nxt, 3001); + assert_eq!(handler.first_connection().snd_mss, 1460); + // A SYN-ACK should be emitted. + assert!( + tx.num_frames() >= 1, + "SYN-ACK emitted for simultaneous open" + ); +} + +#[test] +fn rst_with_ack_in_syn_sent_signals_connection_refused() { + 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 src_mac = MacAddress::from([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]); + let dst_mac = MacAddress::from([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]); + + let (_key, events) = handler + .connect( + IpAddress::V4(LOCAL_IP), + 5000, + IpAddress::V4(REMOTE_IP), + 80, + src_mac, + dst_mac, + coarsetime::Instant::now(), + &mut free, + &mut tx, + ) + .unwrap(); + while tx.pop().is_some() {} + + let client_iss = handler.first_connection().iss; + + // RST+ACK with acceptable ACK → connection refused. + let rst_ack = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 80, + 5000, + 0, + client_iss.wrapping_add(1), // acceptable ACK + flags::RST | flags::ACK, + 0, + &[], + ); + let rst_ack_len = rst_ack.len(); + handler.process_ipv4( + Frame::new(50, leak(rst_ack), rst_ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + assert_eq!(handler.connections.len(), 0, "connection removed"); + let event = events.pop(); + assert!(event.is_some(), "event emitted"); + assert!( + matches!(event.unwrap(), TcpEvent::ConnectionRefused), + "ConnectionRefused event" + ); +} + +#[test] +fn rst_without_ack_in_syn_sent_is_dropped() { + 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 src_mac = MacAddress::from([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]); + let dst_mac = MacAddress::from([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]); + + let (_key, _events) = handler + .connect( + IpAddress::V4(LOCAL_IP), + 5000, + IpAddress::V4(REMOTE_IP), + 80, + src_mac, + dst_mac, + coarsetime::Instant::now(), + &mut free, + &mut tx, + ) + .unwrap(); + while tx.pop().is_some() {} + + // RST without ACK → silently dropped. + let rst_only = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 80, + 5000, + 0, + 0, + flags::RST, // RST only, no ACK + 0, + &[], + ); + let rst_only_len = rst_only.len(); + handler.process_ipv4( + Frame::new(50, leak(rst_only), rst_only_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // Connection should still exist and remain in SynSent. + assert_eq!(handler.connections.len(), 1, "connection not removed"); + assert_eq!(handler.first_connection().state, TcpState::SynSent); + assert_eq!(tx.num_frames(), 0, "no RST emitted"); +} + +#[test] +fn window_scale_applied_on_handshake_completion() { + 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 window scale = 7. + 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(); + handler.process_ipv4( + Frame::new(0, leak(syn_data), syn_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(handler.first_connection().state, TcpState::SynReceived); + assert!(handler.first_connection().wscale_enabled); + assert_eq!(handler.first_connection().snd_wscale, 7); + while tx.pop().is_some() {} + + let server_iss = handler.first_connection().iss; + + // Complete handshake with ACK. Window=512 should be scaled by 7 → 512 << 7 = 65536. + let ack_data = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 512, + &[], + ); + 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.first_connection().state, TcpState::Established); + // Window should be scaled: 512 << 7 = 65536. + assert_eq!(handler.first_connection().snd_wnd, 512 << 7); +} + +#[test] +fn bare_ack_in_syn_sent_is_dropped() { + // In SYN-SENT, a segment with neither SYN nor RST (just ACK or bare) + // should be silently dropped (RFC 9293 §3.10.7.2, step 4). + 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 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]); + nh.seed_cache( + coarsetime::Instant::now(), + IpAddress::V4(REMOTE_IP), + MacAddress::from([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]), + ); + + // connect() sends SYN → SynSent. + let (_key, _eq) = handler + .connect( + IpAddress::V4(LOCAL_IP), + 5000, + IpAddress::V4(REMOTE_IP), + 80, + src_mac, + dst_mac, + coarsetime::Instant::now(), + &mut free, + &mut tx, + ) + .unwrap(); + while tx.pop().is_some() {} + assert_eq!(handler.first_connection().state, TcpState::SynSent); + + let client_iss = handler.first_connection().iss; + + // Send a bare ACK (no SYN, no RST) with acceptable ACK value. + // This should be silently dropped — only SYN or RST are processed in SynSent. + let bare_ack = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 80, + 5000, + 2000, + client_iss.wrapping_add(1), + flags::ACK, // just ACK, no SYN + 65535, + &[], + ); + let len = bare_ack.len(); + handler.process_ipv4( + Frame::new(50, leak(bare_ack), len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // Connection should remain in SynSent — bare ACK is dropped. + assert_eq!(handler.connections.len(), 1); + assert_eq!( + handler.first_connection().state, + TcpState::SynSent, + "bare ACK without SYN should be dropped in SynSent" + ); + assert_eq!( + tx.num_frames(), + 0, + "no response expected for bare ACK in SynSent" + ); +} diff --git a/src/net/handler/tcp/tests/keepalive.rs b/src/net/handler/tcp/tests/keepalive.rs new file mode 100644 index 0000000..29b1960 --- /dev/null +++ b/src/net/handler/tcp/tests/keepalive.rs @@ -0,0 +1,960 @@ +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.first_connection().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.first_connection().state, TcpState::Established); + + // Simulate stale keep-alive state: set probes sent to 5. + let old_activity = handler.first_connection().last_activity; + handler.first_connection_mut().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.first_connection(); + 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.first_connection().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.first_connection().state, TcpState::Established); + + // Configure keep-alive with short timeouts. + { + let tcb = handler.first_connection_mut(); + 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 rx, &mut tx); + + assert_eq!( + handler.first_connection().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.first_connection().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.first_connection().state, TcpState::Established); + + // Keep-alive is disabled by default; set ack_pending false to avoid delayed ACK output. + handler.first_connection_mut().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 rx, &mut tx); + + assert_eq!( + handler.first_connection().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.first_connection().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.first_connection().state, TcpState::Established); + + // Capture event queue before connection is removed. + let event_queue = handler.first_connection().event_queue.clone(); + + // Configure keep-alive: already sent max probes. + { + let tcb = handler.first_connection_mut(); + 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 rx, &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.first_connection().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.first_connection().state, TcpState::Established); + + // Set linger to 0 and capture event queue. + handler.first_connection_mut().linger = Some(0); + let event_queue = handler.first_connection().event_queue.clone(); + let conn_id = handler.first_connection().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(handler.first_connection_key()); + assert!( + handler.first_connection().pending_fin, + "pending_fin should be set" + ); + assert!( + handler.first_connection().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 rx, &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.first_connection().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.first_connection().state, TcpState::Established); + + // Set linger to 5000ms. + handler.first_connection_mut().linger = Some(5000); + let conn_id = handler.first_connection().id; + + // Clear tx from handshake. + while tx.pop().is_some() {} + + // Call initiate_close. + handler.initiate_close(handler.first_connection_key()); + assert!( + handler.first_connection().pending_fin, + "pending_fin should be set" + ); + assert!( + handler.first_connection().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 rx, &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.first_connection().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.first_connection().state, TcpState::Established); + + // Ensure linger is None (default). + assert!( + handler.first_connection().linger.is_none(), + "linger should be None by default" + ); + // Call initiate_close. + handler.initiate_close(handler.first_connection_key()); + + // Verify pending_fin is true and linger_deadline is None. + assert!( + handler.first_connection().pending_fin, + "pending_fin should be set" + ); + assert!( + handler.first_connection().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.first_connection().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.first_connection().state, TcpState::Established); + + // Configure keep-alive. + { + let tcb = handler.first_connection_mut(); + 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 rx, &mut tx); + + // Verify probe was sent. + assert!( + tx.num_frames() > tx_before, + "keep-alive probe should be sent" + ); + assert_eq!( + handler.first_connection().keep_alive_probes_sent, + 1, + "probes_sent should be 1" + ); + + // Record last_activity before recovery. + let activity_before = handler.first_connection().last_activity; + + // Simulate receiving an ACK from the remote (recovery). + let rcv_nxt = handler.first_connection().rcv_nxt; + let snd_una = handler.first_connection().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.first_connection(); + 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.first_connection().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.first_connection().state, TcpState::Established); + + // Capture event queue before connection is removed. + let event_queue = handler.first_connection().event_queue.clone(); + + // Configure keep-alive with count=1. + { + let tcb = handler.first_connection_mut(); + 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 rx, &mut tx); + + // First probe should have been sent. + assert_eq!( + handler.first_connection().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 rx, &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.first_connection().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.first_connection().state, TcpState::Established); + + // Write some data to send buffer. + handler + .first_connection_mut() + .send_buffer + .write(b"unsent data"); + + // Set linger to 0 and capture event queue. + handler.first_connection_mut().linger = Some(0); + let event_queue = handler.first_connection().event_queue.clone(); + let conn_id = handler.first_connection().id; + + // Initiate close — linger(0) sets immediate deadline. + handler.initiate_close(handler.first_connection_key()); + assert!( + handler.first_connection().pending_fin, + "pending_fin should be set" + ); + assert!( + handler.first_connection().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 rx, &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/listener.rs b/src/net/handler/tcp/tests/listener.rs new file mode 100644 index 0000000..b8dd383 --- /dev/null +++ b/src/net/handler/tcp/tests/listener.rs @@ -0,0 +1,182 @@ +use super::*; +use crate::net::handler::udp::BindError; +use crate::net::wire::ip::IpAddress; + +#[test] +fn listen_returns_accept_queue() { + let mut handler = new_handler(); + let accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128); + assert!(accept_queue.is_ok()); + assert_eq!(handler.listeners.len(), 1); + assert_eq!(handler.listeners[0].port, 80); + assert_eq!(handler.listeners[0].addr, IpAddress::V4(LOCAL_IP)); + assert_eq!(handler.listeners[0].backlog, 128); +} + +#[test] +fn listen_same_port_twice_returns_address_in_use() { + let mut handler = new_handler(); + let _ = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let result = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128); + assert!(matches!(result, Err(BindError::AddressInUse))); +} + +#[test] +fn listen_unspecified_conflicts_with_specific_address() { + let mut handler = new_handler(); + let _ = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + // Listening on unspecified addr for the same port should conflict. + let unspec = IpAddress::V4(crate::net::wire::ip::Ipv4Address::new([0, 0, 0, 0])); + let result = handler.listen(unspec, 80, 128); + assert!(matches!(result, Err(BindError::AddressInUse))); +} + +#[test] +fn listen_specific_conflicts_with_unspecified() { + let mut handler = new_handler(); + let unspec = IpAddress::V4(crate::net::wire::ip::Ipv4Address::new([0, 0, 0, 0])); + let _ = handler.listen(unspec, 80, 128).unwrap(); + // Listening on a specific addr for the same port should conflict. + let result = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128); + assert!(matches!(result, Err(BindError::AddressInUse))); +} + +#[test] +fn listen_different_ports_allowed() { + let mut handler = new_handler(); + let _ = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + let result = handler.listen(IpAddress::V4(LOCAL_IP), 443, 128); + assert!(result.is_ok()); + assert_eq!(handler.listeners.len(), 2); +} + +#[test] +fn unlisten_removes_listener() { + let mut handler = new_handler(); + let _ = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + assert_eq!(handler.listeners.len(), 1); + handler.unlisten(IpAddress::V4(LOCAL_IP), 80); + assert_eq!(handler.listeners.len(), 0); +} + +#[test] +fn unlisten_nonexistent_is_noop() { + let mut handler = new_handler(); + let _ = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + handler.unlisten(IpAddress::V4(LOCAL_IP), 443); + assert_eq!(handler.listeners.len(), 1); +} + +#[test] +fn unlisten_cleans_up_syn_received_connections() { + use crate::net::wire::tcp::flags; + use crate::xdp::frame::{BasicFrameBuffer, Frame}; + + 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..16u64 { + free.push(alloc_free_frame(i)); + } + + let _accept_queue = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + + // Send a SYN to create a SYN-RECEIVED connection. + 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, + ); + + // Verify we have a SYN-RECEIVED connection. + assert_eq!(handler.first_connection().state, TcpState::SynReceived); + assert!(handler.first_connection().from_passive_open); + assert_eq!(handler.connections.len(), 1); + + // Unlisten should remove the listener and the SYN-RECEIVED connection. + handler.unlisten(IpAddress::V4(LOCAL_IP), 80); + assert_eq!(handler.listeners.len(), 0); + assert_eq!(handler.connections.len(), 0); +} + +#[test] +fn unlisten_preserves_established_connections() { + use crate::xdp::frame::BasicFrameBuffer; + + 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..16u64 { + free.push(alloc_free_frame(i)); + } + + // Complete a full handshake to get an ESTABLISHED connection. + let _server_iss = establish_connection(&mut handler, &nh, &mut free, &mut rx, &mut tx); + assert_eq!(handler.first_connection().state, TcpState::Established); + assert_eq!(handler.connections.len(), 1); + + // Unlisten removes listener but NOT the established connection. + handler.unlisten(IpAddress::V4(LOCAL_IP), 80); + assert_eq!(handler.listeners.len(), 0); + assert_eq!(handler.connections.len(), 1); +} + +#[test] +fn listen_with_config_propagates_settings() { + use super::super::tcb::TcpConfig; + + let mut handler = new_handler(); + let config = TcpConfig { + backlog: 64, + send_buffer_size: 32768, + recv_buffer_size: 16384, + time_wait_duration_ms: 5000, + tcp_no_delay: true, + delayed_ack_ms: 0, + keep_alive: true, + keep_alive_idle_ms: 1000, + keep_alive_interval_ms: 500, + keep_alive_count: 3, + linger: Some(10000), + timestamps: true, + sack: false, + ecn: true, + }; + let _queue = handler + .listen_with_config(IpAddress::V4(LOCAL_IP), 80, config) + .unwrap(); + let entry = &handler.listeners[0]; + assert_eq!(entry.backlog, 64); + assert_eq!(entry.send_buffer_size, 32768); + assert_eq!(entry.recv_buffer_size, 16384); + assert_eq!(entry.time_wait_duration, 5000); + assert!(entry.tcp_no_delay); + assert_eq!(entry.delayed_ack_ms, 0); + assert!(entry.keep_alive); + assert_eq!(entry.keep_alive_idle_ms, 1000); + assert_eq!(entry.keep_alive_interval_ms, 500); + assert_eq!(entry.keep_alive_count, 3); + assert_eq!(entry.linger, Some(10000)); + assert!(entry.timestamps); + assert!(!entry.sack); + assert!(entry.ecn); +} diff --git a/src/net/handler/tcp/tests/mod.rs b/src/net/handler/tcp/tests/mod.rs new file mode 100644 index 0000000..5269356 --- /dev/null +++ b/src/net/handler/tcp/tests/mod.rs @@ -0,0 +1,513 @@ +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::inbound::segment::is_segment_acceptable; +use super::state::TcpState; +use super::tcb::{ConnectionId, DEFAULT_RCV_WSCALE, TcpConfig, TcpEvent}; +use super::*; + +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 { + let nh = NeighborHandler::new("test0", coarsetime::Duration::from_secs(60)).unwrap(); + // Seed cache so lookup_or_resolve returns a MAC for REMOTE_IP. + nh.seed_cache( + coarsetime::Instant::now(), + IpAddress::V4(REMOTE_IP), + MacAddress::from([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]), + ); + nh +} + +/// 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(); + nh.seed_cache( + coarsetime::Instant::now(), + IpAddress::V4(REMOTE_IP), + MacAddress::from([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]), + ); + 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.first_connection().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(); + nh.seed_cache( + coarsetime::Instant::now(), + IpAddress::V4(REMOTE_IP), + MacAddress::from([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]), + ); + 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.first_connection().sack_enabled); + let server_iss = handler.first_connection().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.first_connection().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]); + nh.seed_cache( + coarsetime::Instant::now(), + IpAddress::V4(REMOTE_IP), + MacAddress::from([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]), + ); + + // connect sends SYN. + let (_key, _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.first_connection().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.first_connection().state, TcpState::Established); + handler.first_connection_mut().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]); + nh.seed_cache( + coarsetime::Instant::now(), + IpAddress::V4(REMOTE_IP), + MacAddress::from([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]), + ); + + let (_key, _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.first_connection().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.first_connection().state, TcpState::Established); + handler.first_connection_mut().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 handler; +mod handshake; +mod keepalive; +mod listener; +mod nagle; +mod persist; +mod retransmission; +mod sack; +mod teardown; +mod timers; +mod timestamps; +mod validate; diff --git a/src/net/handler/tcp/tests/nagle.rs b/src/net/handler/tcp/tests/nagle.rs new file mode 100644 index 0000000..70771d7 --- /dev/null +++ b/src/net/handler/tcp/tests/nagle.rs @@ -0,0 +1,120 @@ +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.first_connection_mut().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 rx, &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.first_connection_mut().send_buffer.write(b"world"); + + // 6. poll_send — Nagle holds it. + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &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.first_connection_mut().send_buffer.write(b"hi"); + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + + // 3. Write MSS-worth of data. + let mss = handler.first_connection().eff_snd_mss as usize; + let mss_data = vec![0xAA; mss]; + handler.first_connection_mut().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 rx, &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.first_connection().nagle_enabled, + "nagle should be disabled with tcp_no_delay" + ); + + // 2. Write small data, poll_send (first send), pop tx. + handler.first_connection_mut().send_buffer.write(b"hello"); + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + assert_eq!(tx.num_frames(), 1); + while tx.pop().is_some() {} + + // 3. Write more small data while first is in flight. + handler.first_connection_mut().send_buffer.write(b"world"); + + // 4. poll_send — TCP_NODELAY bypasses Nagle. + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &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..4b5508e --- /dev/null +++ b/src/net/handler/tcp/tests/persist.rs @@ -0,0 +1,180 @@ +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.first_connection_mut().send_buffer.write(b"Hello"); + handler.first_connection_mut().snd_wnd = 0; + + assert!(handler.first_connection().persist_deadline.is_none()); + + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + + // Persist timer should now be armed. + assert!( + handler.first_connection().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.first_connection_mut().send_buffer.write(b"Hello"); + handler.first_connection_mut().snd_wnd = 0; + + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + assert!(handler.first_connection().persist_deadline.is_some()); + assert_eq!(handler.first_connection().persist_backoff, 0); + + // Simulate time passing beyond the deadline by setting it to the past. + handler.first_connection_mut().persist_deadline = Some(now); + + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &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.first_connection().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.first_connection().persist_backoff, 1); + // persist_deadline should be rescheduled (not None). + assert!(handler.first_connection().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.first_connection_mut().send_buffer.write(b"Hello"); + handler.first_connection_mut().snd_wnd = 0; + + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + assert!(handler.first_connection().persist_deadline.is_some()); + handler.first_connection_mut().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.first_connection().persist_deadline.is_none(), + "persist_deadline should be cleared when window reopens" + ); + assert_eq!( + handler.first_connection().persist_backoff, + 0, + "persist_backoff should be reset" + ); +} + +#[test] +fn persist_backoff_caps_at_six() { + 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 enough data to sustain many probes (each probe sends 1 byte). + handler + .first_connection_mut() + .send_buffer + .write(&[0x41u8; 20]); + handler.first_connection_mut().snd_wnd = 0; + + let now = coarsetime::Instant::now(); + + // Arm the persist timer. + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + assert!(handler.first_connection().persist_deadline.is_some()); + assert_eq!(handler.first_connection().persist_backoff, 0); + + // Fire the persist probe multiple times (more than 6) by setting deadline to past. + for i in 0..10 { + handler.first_connection_mut().persist_deadline = Some(now); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + + let expected = (i + 1).min(6); + assert_eq!( + handler.first_connection().persist_backoff, + expected, + "persist_backoff should be {} after {} probes", + expected, + i + 1 + ); + } + + // Final check: backoff must be capped at exactly 6. + assert_eq!( + handler.first_connection().persist_backoff, + 6, + "persist_backoff must be capped at 6" + ); +} diff --git a/src/net/handler/tcp/tests/retransmission.rs b/src/net/handler/tcp/tests/retransmission.rs new file mode 100644 index 0000000..8854bb4 --- /dev/null +++ b/src/net/handler/tcp/tests/retransmission.rs @@ -0,0 +1,475 @@ +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.first_connection().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.first_connection_mut().send_buffer.write(b"AAAA"); + handler.first_connection_mut().snd_wnd = 65535; + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} // consume sent segment + + let cwnd_before = handler.first_connection().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.first_connection(); + 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.first_connection().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.first_connection_mut().send_buffer.write(b"BBBB"); + handler.first_connection_mut().snd_wnd = 65535; + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + + let cwnd_before = handler.first_connection().cubic.cwnd; + + // Simulate timer expiry by setting a deadline in the past. + handler.first_connection_mut().retransmit_deadline = + Some(now - coarsetime::Duration::from_millis(1)); + handler.first_connection_mut().rto_backoff = 0; + + // poll_timers should trigger RTO retransmit. + handler.poll_timers(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + assert!(tx.num_frames() >= 1, "retransmitted segment expected"); + + let tcb = handler.first_connection(); + // 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.first_connection().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 + .first_connection_mut() + .send_buffer + .write(b"test data"); + handler.first_connection_mut().snd_wnd = 65535; + let send_time = coarsetime::Instant::now(); + handler.poll_send(send_time, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + + // Verify last_send_time is set. + assert!( + handler.first_connection().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.first_connection(); + 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.first_connection().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.first_connection().eff_snd_mss as usize; + // Fill send buffer with 6 MSS of data, set cwnd to 3*MSS. + handler + .first_connection_mut() + .send_buffer + .write(&vec![0xAA; mss * 6]); + handler.first_connection_mut().snd_wnd = 65535; + handler.first_connection_mut().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 rx, &mut tx); + while tx.pop().is_some() {} + + let snd_nxt_before = handler.first_connection().snd_nxt; + let snd_una = handler.first_connection().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.first_connection().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 rx, &mut tx); + + let snd_nxt_after = handler.first_connection().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 rto_backoff_resets_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)); + } + + let server_iss = establish_connection(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + // Put data in server's send buffer and transmit it. + handler.first_connection_mut().send_buffer.write(b"CCCC"); + handler.first_connection_mut().snd_wnd = 65535; + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + + // Artificially set rto_backoff as if an RTO had fired. + handler.first_connection_mut().rto_backoff = 2; + handler.first_connection_mut().retransmit_deadline = + Some(coarsetime::Instant::now() + coarsetime::Duration::from_millis(10_000)); + + // Client ACKs the server's data (new ACK that advances snd_una). + let new_ack = server_iss.wrapping_add(1).wrapping_add(4); // ISS+1 + 4 bytes + let client_seq = 1001u32; + let data = build_tcp_frame_with_payload( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + client_seq, + new_ack, + flags::ACK, + 65535, + &[], + b"hello", + ); + let data_len = data.len(); + handler.process_ipv4( + Frame::new(10, leak(data), data_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + let tcb = handler.first_connection(); + assert_eq!(tcb.rto_backoff, 0, "rto_backoff should be reset on new ACK"); + // snd_una == snd_nxt (all data ACKed), so retransmit timer should be off. + assert!( + tcb.retransmit_deadline.is_none(), + "retransmit_deadline should be None when all data is ACKed" + ); +} diff --git a/src/net/handler/tcp/tests/sack.rs b/src/net/handler/tcp/tests/sack.rs new file mode 100644 index 0000000..392084c --- /dev/null +++ b/src/net/handler/tcp/tests/sack.rs @@ -0,0 +1,829 @@ +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.first_connection().sack_enabled, + "SACK should be negotiated" + ); + let server_iss = handler.first_connection().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.first_connection().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 = handler.first_connection_mut(); + 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.first_connection(); + 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 = handler.first_connection_mut(); + 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.first_connection().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.first_connection(); + 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 = handler.first_connection_mut(); + 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.first_connection(); + 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 = handler.first_connection_mut(); + 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.first_connection().sack_scoreboard.len(), 1); + + // Set up RTO: arm the retransmit deadline in the past. + let now = coarsetime::Instant::now(); + handler.first_connection_mut().retransmit_deadline = Some(now); + handler.first_connection_mut().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 rx, &mut tx); + + assert!( + handler.first_connection().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 = handler.first_connection_mut(); + // 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 rx, &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.first_connection(); + 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 = handler.first_connection_mut(); + // 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 rx, &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.first_connection(); + 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.first_connection().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.first_connection_mut().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 + .first_connection_mut() + .send_buffer + .write(&vec![0xAA; data_len]); + handler.first_connection_mut().snd_wnd = 65535; + handler.first_connection_mut().snd_nxt = handler + .first_connection_mut() + .snd_una + .wrapping_add(data_len as u32); + + let snd_una = handler.first_connection().snd_una; + let cwnd_before = handler.first_connection().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.first_connection(); + 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.first_connection().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.first_connection_mut().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 + .first_connection_mut() + .send_buffer + .write(&vec![0xAA; data_len]); + handler.first_connection_mut().snd_wnd = 65535; + handler.first_connection_mut().snd_nxt = handler + .first_connection_mut() + .snd_una + .wrapping_add(data_len as u32); + + let snd_una = handler.first_connection().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.first_connection().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.first_connection().recovery.in_recovery, + "should still be in recovery after partial ACK" + ); + assert_eq!(handler.first_connection().snd_una, partial_ack_seq); +} + +#[test] +fn ooo_multiple_ranges_generates_multiple_sack_blocks() { + use crate::net::wire::tcp::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 enabled. + let server_iss = establish_connection_with_sack(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + // Send first OOO segment: seq=1021, 5 bytes (gap from 1001..1021). + let ooo1 = build_tcp_frame_with_payload( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1021, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + b"BBBBB", + ); + let ooo1_len = ooo1.len(); + handler.process_ipv4( + Frame::new(2, leak(ooo1), ooo1_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} // drain first dup ACK + + // Send second OOO segment: seq=1031, 5 bytes (another gap). + let ooo2 = build_tcp_frame_with_payload( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1031, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + b"CCCCC", + ); + let ooo2_len = ooo2.len(); + handler.process_ipv4( + Frame::new(3, leak(ooo2), ooo2_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // Should have emitted a dup ACK with multiple 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.ack_num(), 1001, "dup ACK has original rcv_nxt"); + + // Parse SACK blocks. + 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); + + // Should have 2 SACK blocks: the most recent [1031, 1036) first, then [1021, 1026). + assert_eq!(count, 2, "two SACK blocks expected"); + assert_eq!(blocks[0], Some((1031, 1036)), "most recent range first"); + assert_eq!(blocks[1], Some((1021, 1026)), "older range second"); +} diff --git a/src/net/handler/tcp/tests/teardown.rs b/src/net/handler/tcp/tests/teardown.rs new file mode 100644 index 0000000..2edf3df --- /dev/null +++ b/src/net/handler/tcp/tests/teardown.rs @@ -0,0 +1,2696 @@ +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.first_connection().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.first_connection().state, TcpState::CloseWait); + assert_eq!(handler.first_connection().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.first_connection().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.first_connection().state, TcpState::CloseWait); + assert_eq!( + handler.first_connection().recv_buffer.available(), + payload.len() + ); + // rcv_nxt = 1001 + 7 bytes data + 1 FIN = 1009 + assert_eq!( + handler.first_connection().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.first_connection().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.first_connection_mut().pending_fin = true; + + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + + // FIN should have been sent. + assert_eq!(tx.num_frames(), 1, "FIN segment sent"); + let tcb = handler.first_connection(); + 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.first_connection().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 + .first_connection_mut() + .send_buffer + .write(b"final data"); + handler.first_connection_mut().snd_wnd = 65535; + handler.first_connection_mut().pending_fin = true; + + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &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.first_connection().state, + TcpState::Established, + "still Established until data ACKed" + ); + assert!( + handler.first_connection().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.first_connection().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.first_connection_mut().pending_fin = true; + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + assert_eq!(handler.first_connection().state, TcpState::FinWait1); + let fin_seq = handler.first_connection().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.first_connection().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.first_connection().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.first_connection_mut().pending_fin = true; + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + let fin_seq = handler.first_connection().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.first_connection().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.first_connection().state, TcpState::TimeWait); + assert!(handler.first_connection().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.first_connection().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.first_connection_mut().pending_fin = true; + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + assert_eq!(handler.first_connection().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.first_connection().state, TcpState::Closing); + while tx.pop().is_some() {} + + // Remote ACKs our FIN → TimeWait. + let fin_seq = handler.first_connection().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.first_connection().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.first_connection().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.first_connection().state, TcpState::CloseWait); + while tx.pop().is_some() {} + + // We close → pending_fin, poll_send sends FIN → LastAck. + handler.first_connection_mut().pending_fin = true; + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + assert_eq!(handler.first_connection().state, TcpState::LastAck); + let fin_seq = handler.first_connection().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.first_connection().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.first_connection_mut().pending_fin = true; + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + let fin_seq = handler.first_connection().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.first_connection().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.first_connection().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.first_connection().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.first_connection_mut().state = TcpState::TimeWait; + handler.first_connection_mut().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.first_connection().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.first_connection().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.first_connection_mut().pending_fin = true; + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + assert_eq!(handler.first_connection().state, TcpState::FinWait1); + while tx.pop().is_some() {} + + // 4. Remote ACKs our FIN -> FinWait2. + let fin_seq = handler.first_connection().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.first_connection().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.first_connection().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.first_connection().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.first_connection().state, TcpState::CloseWait); + while tx.pop().is_some() {} + + // 3. We close -> LastAck. + handler.first_connection_mut().pending_fin = true; + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + assert_eq!(handler.first_connection().state, TcpState::LastAck); + while tx.pop().is_some() {} + let fin_seq = handler.first_connection().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.first_connection().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.first_connection().state, TcpState::Established); + + // Verify pending_fin is initially false. + assert!( + !handler.first_connection().pending_fin, + "pending_fin should start false" + ); + + // Call initiate_close (the handler method that shutdown() delegates to). + handler.initiate_close(handler.first_connection_key()); + + // Verify pending_fin is now true. + assert!( + handler.first_connection().pending_fin, + "pending_fin should be true after initiate_close" + ); + + // Verify connection is still Established (FIN not yet sent). + assert_eq!( + handler.first_connection().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.first_connection().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.first_connection().state, TcpState::Established); + + // Initiate half-close (shutdown write side). + handler.initiate_close(handler.first_connection_key()); + + // Verify pending_fin is set but state is still Established (FIN not sent yet). + assert!( + handler.first_connection().pending_fin, + "pending_fin should be true after initiate_close" + ); + assert_eq!( + handler.first_connection().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.first_connection_mut().recv_buffer.write(incoming); + + // Verify reads still work after half-close. + assert_eq!( + handler.first_connection().recv_buffer.available(), + incoming.len(), + "recv_buffer should still be readable after half-close" + ); + let mut buf = [0u8; 32]; + let read_len = handler.first_connection_mut().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.first_connection().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.first_connection_mut().send_buffer.write(payload); + handler.first_connection_mut().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 rx, &mut tx); + while tx.pop().is_some() {} + + let snd_nxt_after_send = handler.first_connection().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.first_connection().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.first_connection().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.first_connection().snd_una, + snd_nxt_after_send, + "snd_una must advance to cover ACKed data" + ); + // Send buffer should be drained. + assert_eq!( + handler.first_connection().send_buffer.available(), + 0, + "send buffer must be drained after ACK" + ); + // Window should be updated. + assert_eq!( + handler.first_connection().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 rx, &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.first_connection().state, TcpState::Established); + assert_eq!(handler.first_connection().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.first_connection().state, + TcpState::Established, + "FIN must not be processed when there is a gap before it" + ); + assert_eq!( + handler.first_connection().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.first_connection().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.first_connection().state, + TcpState::CloseWait, + "FIN should be processed once all preceding data is received" + ); + assert_eq!( + handler.first_connection().rcv_nxt, + 1012, + "rcv_nxt should advance by 1 for the FIN" + ); +} + +// --------------------------------------------------------------------------- +// Helper: bring a passive-open connection to FinWait1 and return (server_iss, fin_seq). +// --------------------------------------------------------------------------- +fn setup_fin_wait1( + handler: &mut super::super::handler::TcpHandler, + nh: &crate::net::NeighborHandler, + free: &mut BasicFrameBuffer<'static>, + rx: &mut BasicFrameBuffer<'static>, + tx: &mut BasicFrameBuffer<'static>, +) -> (u32, u32) { + let server_iss = establish_connection(handler, nh, free, rx, tx); + handler.first_connection_mut().pending_fin = true; + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), nh, free, rx, tx); + while tx.pop().is_some() {} + assert_eq!(handler.first_connection().state, TcpState::FinWait1); + let fin_seq = handler.first_connection().fin_seq.unwrap(); + (server_iss, fin_seq) +} + +// Helper: bring connection to FinWait2 and return (server_iss, fin_seq). +fn setup_fin_wait2( + handler: &mut super::super::handler::TcpHandler, + nh: &crate::net::NeighborHandler, + free: &mut BasicFrameBuffer<'static>, + rx: &mut BasicFrameBuffer<'static>, + tx: &mut BasicFrameBuffer<'static>, +) -> (u32, u32) { + let (server_iss, fin_seq) = setup_fin_wait1(handler, nh, free, rx, tx); + // Remote ACKs our FIN. + 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(10, leak(ack), ack_len, false), + coarsetime::Instant::now(), + nh, + free, + rx, + tx, + ); + assert_eq!(handler.first_connection().state, TcpState::FinWait2); + while tx.pop().is_some() {} + (server_iss, fin_seq) +} + +// Helper: bring connection to TimeWait and return (server_iss, fin_seq). +fn setup_time_wait( + handler: &mut super::super::handler::TcpHandler, + nh: &crate::net::NeighborHandler, + free: &mut BasicFrameBuffer<'static>, + rx: &mut BasicFrameBuffer<'static>, + tx: &mut BasicFrameBuffer<'static>, +) -> (u32, u32) { + let (server_iss, fin_seq) = setup_fin_wait2(handler, nh, free, rx, tx); + // Remote sends FIN. + 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(11, leak(fin), fin_len, false), + coarsetime::Instant::now(), + nh, + free, + rx, + tx, + ); + assert_eq!(handler.first_connection().state, TcpState::TimeWait); + while tx.pop().is_some() {} + (server_iss, fin_seq) +} + +// Helper: bring connection to CloseWait and return server_iss. +fn setup_close_wait( + handler: &mut super::super::handler::TcpHandler, + nh: &crate::net::NeighborHandler, + free: &mut BasicFrameBuffer<'static>, + rx: &mut BasicFrameBuffer<'static>, + tx: &mut BasicFrameBuffer<'static>, +) -> u32 { + let server_iss = establish_connection(handler, nh, free, rx, tx); + // Remote sends FIN. + 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(10, leak(fin), fin_len, false), + coarsetime::Instant::now(), + nh, + free, + rx, + tx, + ); + assert_eq!(handler.first_connection().state, TcpState::CloseWait); + while tx.pop().is_some() {} + server_iss +} + +fn alloc_buffers() -> ( + BasicFrameBuffer<'static>, + BasicFrameBuffer<'static>, + BasicFrameBuffer<'static>, +) { + let mut free = BasicFrameBuffer::new(32); + let rx = BasicFrameBuffer::new(32); + let tx = BasicFrameBuffer::new(32); + for i in 0..16 { + free.push(alloc_free_frame(100 + i)); + } + (free, rx, tx) +} + +// --------------------------------------------------------------------------- +// FinWait1: receives FIN+ACK simultaneously → TimeWait +// --------------------------------------------------------------------------- +#[test] +fn fin_wait1_fin_and_ack_simultaneously_transitions_to_time_wait() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let (mut free, mut rx, mut tx) = alloc_buffers(); + + let (_server_iss, fin_seq) = setup_fin_wait1(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + // Remote sends FIN+ACK that also ACKs our FIN. + let fin_ack = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + fin_seq.wrapping_add(1), + flags::ACK | flags::FIN, + 65535, + &[], + ); + let len = fin_ack.len(); + handler.process_ipv4( + Frame::new(20, leak(fin_ack), len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + assert_eq!(handler.first_connection().state, TcpState::TimeWait); + assert!(handler.first_connection().time_wait_deadline.is_some()); + assert!(handler.first_connection().retransmit_deadline.is_none()); + // Should have sent an ACK for the FIN. + assert!(tx.num_frames() >= 1, "ACK for remote FIN expected"); +} + +// --------------------------------------------------------------------------- +// FinWait1: receives data while waiting for FIN-ACK +// --------------------------------------------------------------------------- +#[test] +fn fin_wait1_processes_incoming_data() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let (mut free, mut rx, mut tx) = alloc_buffers(); + + let (server_iss, _fin_seq) = setup_fin_wait1(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + let rcv_nxt_before = handler.first_connection().rcv_nxt; + + // Remote sends data (no ACK of our FIN, just regular ACK). + let data = build_tcp_frame_with_payload( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), + flags::ACK, + 65535, + &[], + b"hello", + ); + let len = data.len(); + handler.process_ipv4( + Frame::new(20, leak(data), len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // rcv_nxt should advance by payload length. + assert_eq!( + handler.first_connection().rcv_nxt, + rcv_nxt_before.wrapping_add(5), + "rcv_nxt should advance for data received in FinWait1" + ); + // State should remain FinWait1 (no FIN-ACK yet, no remote FIN). + assert_eq!(handler.first_connection().state, TcpState::FinWait1); + // ACK should be sent for the data. + assert!(tx.num_frames() >= 1, "ACK for data expected"); +} + +// --------------------------------------------------------------------------- +// FinWait2: receives data before FIN +// --------------------------------------------------------------------------- +#[test] +fn fin_wait2_processes_data_before_fin() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let (mut free, mut rx, mut tx) = alloc_buffers(); + + let (_server_iss, fin_seq) = setup_fin_wait2(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + let rcv_nxt_before = handler.first_connection().rcv_nxt; + + // Remote sends data. + let data = build_tcp_frame_with_payload( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + fin_seq.wrapping_add(1), + flags::ACK, + 65535, + &[], + b"still sending", + ); + let len = data.len(); + handler.process_ipv4( + Frame::new(20, leak(data), len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + assert_eq!( + handler.first_connection().rcv_nxt, + rcv_nxt_before.wrapping_add(13), + "rcv_nxt should advance for data in FinWait2" + ); + assert_eq!(handler.first_connection().state, TcpState::FinWait2); + assert!(tx.num_frames() >= 1, "ACK for data expected"); +} + +// --------------------------------------------------------------------------- +// TimeWait: duplicate FIN → re-ACK and restart timer +// --------------------------------------------------------------------------- +#[test] +fn time_wait_duplicate_fin_reacks_and_restarts_timer() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let (mut free, mut rx, mut tx) = alloc_buffers(); + + let (_server_iss, fin_seq) = setup_time_wait(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + let old_deadline = handler.first_connection().time_wait_deadline.unwrap(); + + // Remote retransmits FIN. + let dup_fin = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + fin_seq.wrapping_add(1), + flags::ACK | flags::FIN, + 65535, + &[], + ); + let len = dup_fin.len(); + handler.process_ipv4( + Frame::new(30, leak(dup_fin), len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // Connection still in TimeWait. + assert_eq!(handler.first_connection().state, TcpState::TimeWait); + // Timer restarted (new deadline >= old deadline). + let new_deadline = handler.first_connection().time_wait_deadline.unwrap(); + assert!( + new_deadline >= old_deadline, + "time_wait_deadline should be restarted" + ); + // ACK should be sent. + assert!(tx.num_frames() >= 1, "re-ACK for duplicate FIN expected"); +} + +// --------------------------------------------------------------------------- +// CloseWait: window update with WL1/WL2 guard +// --------------------------------------------------------------------------- +#[test] +fn close_wait_window_update_with_wl_guard() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let (mut free, mut rx, mut tx) = alloc_buffers(); + + let _server_iss = setup_close_wait(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + // Set up known snd_wl1/snd_wl2 values. + let snd_una = handler.first_connection().snd_una; + handler.first_connection_mut().snd_wl1 = 1000; + handler.first_connection_mut().snd_wl2 = snd_una; + + // We need seg_ack to be valid (snd_una < seg_ack <= snd_nxt). + // Write some data so snd_nxt > snd_una. + handler + .first_connection_mut() + .send_buffer + .write(b"test data"); + handler.first_connection_mut().snd_wnd = 65535; + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + let snd_nxt = handler.first_connection().snd_nxt; + + let ack = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1002, + snd_nxt, + flags::ACK, + 32000, + &[], + ); + let ack_len = ack.len(); + handler.process_ipv4( + Frame::new(20, leak(ack), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + assert_eq!(handler.first_connection().state, TcpState::CloseWait); + assert_eq!( + handler.first_connection().snd_wnd, + 32000, + "window should be updated in CloseWait" + ); + assert_eq!(handler.first_connection().snd_wl1, 1002); + assert_eq!(handler.first_connection().snd_wl2, snd_nxt); +} + +// --------------------------------------------------------------------------- +// RST exact match in teardown state → reset + remove connection +// --------------------------------------------------------------------------- +#[test] +fn teardown_rst_exact_match_removes_connection() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let (mut free, mut rx, mut tx) = alloc_buffers(); + + let (_server_iss, _fin_seq) = setup_fin_wait1(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + let rcv_nxt = handler.first_connection().rcv_nxt; + + // Send RST with seq == rcv_nxt (exact match). + let rst = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + rcv_nxt, + 0, + flags::RST, + 0, + &[], + ); + let len = rst.len(); + handler.process_ipv4( + Frame::new(20, leak(rst), len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + assert_eq!(handler.connections.len(), 0, "RST should remove connection"); +} + +// --------------------------------------------------------------------------- +// RST in-window but not exact → challenge ACK +// --------------------------------------------------------------------------- +#[test] +fn teardown_rst_in_window_sends_challenge_ack() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let (mut free, mut rx, mut tx) = alloc_buffers(); + + let (_server_iss, _fin_seq) = setup_fin_wait1(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + let rcv_nxt = handler.first_connection().rcv_nxt; + + // Send RST with seq in window but not exact match. + let rst = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + rcv_nxt.wrapping_add(1), + 0, + flags::RST, + 0, + &[], + ); + let len = rst.len(); + handler.process_ipv4( + Frame::new(20, leak(rst), len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // Connection should NOT be removed. + assert_eq!( + handler.connections.len(), + 1, + "in-window RST should not remove connection" + ); + // Challenge ACK should be sent. + assert!( + tx.num_frames() >= 1, + "challenge ACK expected for in-window RST" + ); +} + +// --------------------------------------------------------------------------- +// SYN in teardown state → challenge ACK (RFC 5961) +// --------------------------------------------------------------------------- +#[test] +fn teardown_syn_sends_challenge_ack() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let (mut free, mut rx, mut tx) = alloc_buffers(); + + let (_server_iss, _fin_seq) = setup_fin_wait1(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + let rcv_nxt = handler.first_connection().rcv_nxt; + + // Send SYN in teardown state. + let syn = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + rcv_nxt, + 0, + flags::SYN, + 65535, + &[], + ); + let len = syn.len(); + handler.process_ipv4( + Frame::new(20, leak(syn), len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // Connection should remain. + assert_eq!(handler.connections.len(), 1); + assert_eq!(handler.first_connection().state, TcpState::FinWait1); + // Challenge ACK sent. + assert!( + tx.num_frames() >= 1, + "challenge ACK expected for SYN in teardown" + ); +} + +// --------------------------------------------------------------------------- +// No ACK bit → segment dropped +// --------------------------------------------------------------------------- +#[test] +fn teardown_no_ack_bit_drops_segment() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let (mut free, mut rx, mut tx) = alloc_buffers(); + + let (_server_iss, _fin_seq) = setup_fin_wait1(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + let rcv_nxt = handler.first_connection().rcv_nxt; + + // Send segment with no flags set (no ACK, no SYN, no RST, no FIN). + // This won't pass SYN/RST checks and will hit the "ACK bit off" check. + // We need a bare segment — just no flags. + let bare = build_tcp_frame(REMOTE_IP, LOCAL_IP, 12345, 80, rcv_nxt, 0, 0, 65535, &[]); + let len = bare.len(); + handler.process_ipv4( + Frame::new(20, leak(bare), len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // Should be silently dropped: no tx frames, connection unchanged. + assert_eq!(handler.connections.len(), 1); + assert_eq!(handler.first_connection().state, TcpState::FinWait1); + assert_eq!(tx.num_frames(), 0, "no ACK should be sent for bare segment"); +} + +// --------------------------------------------------------------------------- +// Out-of-window segment → ACK sent (not RST) +// --------------------------------------------------------------------------- +#[test] +fn teardown_out_of_window_sends_ack() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let (mut free, mut rx, mut tx) = alloc_buffers(); + + let (_server_iss, _fin_seq) = setup_fin_wait1(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + let rcv_nxt = handler.first_connection().rcv_nxt; + + // Send segment far outside the window. + let oow = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + rcv_nxt.wrapping_add(1_000_000), + 0, + flags::ACK, + 65535, + &[], + ); + let len = oow.len(); + handler.process_ipv4( + Frame::new(20, leak(oow), len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // Connection still exists. + assert_eq!(handler.connections.len(), 1); + assert_eq!(handler.first_connection().state, TcpState::FinWait1); + // ACK should be sent for out-of-window non-RST. + assert!( + tx.num_frames() >= 1, + "ACK expected for out-of-window segment" + ); +} + +// --------------------------------------------------------------------------- +// Out-of-window RST → silently dropped +// --------------------------------------------------------------------------- +#[test] +fn teardown_out_of_window_rst_silently_dropped() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let (mut free, mut rx, mut tx) = alloc_buffers(); + + let (_server_iss, _fin_seq) = setup_fin_wait1(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + let rcv_nxt = handler.first_connection().rcv_nxt; + + // Send RST far outside the window. + let rst = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + rcv_nxt.wrapping_add(1_000_000), + 0, + flags::RST, + 0, + &[], + ); + let len = rst.len(); + handler.process_ipv4( + Frame::new(20, leak(rst), len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // Connection still exists, no frames sent. + assert_eq!(handler.connections.len(), 1); + assert_eq!(handler.first_connection().state, TcpState::FinWait1); + assert_eq!( + tx.num_frames(), + 0, + "out-of-window RST should be silently dropped" + ); +} + +// --------------------------------------------------------------------------- +// FinWait1: ACK advances snd_una and updates window +// --------------------------------------------------------------------------- +#[test] +fn fin_wait1_ack_advances_snd_una() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let (mut free, mut rx, mut tx) = alloc_buffers(); + + let _server_iss = establish_connection(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + // Write data, send it, ACK it, then close — so FIN and data are separate. + handler + .first_connection_mut() + .send_buffer + .write(b"data before close"); + handler.first_connection_mut().snd_wnd = 65535; + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + + let snd_nxt_after_data = handler.first_connection().snd_nxt; + + // ACK the data so bytes_in_flight goes to 0. + let data_ack = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + snd_nxt_after_data, + flags::ACK, + 65535, + &[], + ); + let data_ack_len = data_ack.len(); + handler.process_ipv4( + Frame::new(15, leak(data_ack), data_ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + + // Now close: set pending_fin and poll_send to send FIN. + handler.first_connection_mut().pending_fin = true; + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + assert_eq!(handler.first_connection().state, TcpState::FinWait1); + + let fin_seq = handler.first_connection().fin_seq.unwrap(); + + // Remote ACKs the FIN → FinWait2. + let fin_ack = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + fin_seq.wrapping_add(1), + flags::ACK, + 40000, + &[], + ); + let len = fin_ack.len(); + handler.process_ipv4( + Frame::new(20, leak(fin_ack), len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // snd_una should advance past FIN. + assert_eq!( + handler.first_connection().snd_una, + fin_seq.wrapping_add(1), + "snd_una should advance for FIN ACK in FinWait1" + ); + // Window should be updated. + assert_eq!(handler.first_connection().snd_wnd, 40000); + // Should transition to FinWait2. + assert_eq!(handler.first_connection().state, TcpState::FinWait2); + // Send buffer should be drained. + assert_eq!(handler.first_connection().send_buffer.available(), 0); +} + +// --------------------------------------------------------------------------- +// Closing: ACK of our FIN → TimeWait (with snd_una advance) +// --------------------------------------------------------------------------- +#[test] +fn closing_ack_of_fin_transitions_to_time_wait() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let (mut free, mut rx, mut tx) = alloc_buffers(); + + let (server_iss, fin_seq) = setup_fin_wait1(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + // 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 len = fin.len(); + handler.process_ipv4( + Frame::new(20, leak(fin), len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(handler.first_connection().state, TcpState::Closing); + while tx.pop().is_some() {} + + // Now remote ACKs our FIN → TimeWait. + 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(21, leak(ack), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + assert_eq!(handler.first_connection().state, TcpState::TimeWait); + assert!(handler.first_connection().time_wait_deadline.is_some()); + assert!(handler.first_connection().retransmit_deadline.is_none()); + assert_eq!( + handler.first_connection().snd_una, + fin_seq.wrapping_add(1), + "snd_una should advance to cover FIN" + ); +} + +// --------------------------------------------------------------------------- +// FinWait2: data + FIN in same segment → TimeWait +// --------------------------------------------------------------------------- +#[test] +fn fin_wait2_data_and_fin_transitions_to_time_wait() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let (mut free, mut rx, mut tx) = alloc_buffers(); + + let (_server_iss, fin_seq) = setup_fin_wait2(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + let rcv_nxt_before = handler.first_connection().rcv_nxt; + + // Remote sends data + FIN. + let data_fin = build_tcp_frame_with_payload( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + fin_seq.wrapping_add(1), + flags::ACK | flags::FIN, + 65535, + &[], + b"last data", + ); + let len = data_fin.len(); + handler.process_ipv4( + Frame::new(20, leak(data_fin), len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + assert_eq!(handler.first_connection().state, TcpState::TimeWait); + // rcv_nxt should advance by data_len + 1 (for FIN). + assert_eq!( + handler.first_connection().rcv_nxt, + rcv_nxt_before.wrapping_add(9 + 1), + "rcv_nxt should advance by data + FIN" + ); + assert!(handler.first_connection().time_wait_deadline.is_some()); + assert!(tx.num_frames() >= 1, "ACK expected for data+FIN"); +} + +// --------------------------------------------------------------------------- +// LastAck: ACK that does NOT cover FIN → connection stays +// --------------------------------------------------------------------------- +#[test] +fn last_ack_partial_ack_does_not_remove_connection() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let (mut free, mut rx, mut tx) = alloc_buffers(); + + let _server_iss = setup_close_wait(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + // Close → LastAck. + handler.first_connection_mut().pending_fin = true; + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + assert_eq!(handler.first_connection().state, TcpState::LastAck); + let fin_seq = handler.first_connection().fin_seq.unwrap(); + + // Send ACK that does NOT cover the FIN (ack = fin_seq, not fin_seq+1). + let ack = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1002, + fin_seq, + flags::ACK, + 65535, + &[], + ); + let len = ack.len(); + handler.process_ipv4( + Frame::new(20, leak(ack), len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // Connection should still exist in LastAck. + assert_eq!(handler.connections.len(), 1); + assert_eq!(handler.first_connection().state, TcpState::LastAck); +} + +// --------------------------------------------------------------------------- +// Closing: partial ACK (does not cover our FIN) stays in Closing +// --------------------------------------------------------------------------- +#[test] +fn closing_partial_ack_stays_in_closing() { + let mut handler = new_handler(); + let nh = new_neighbor_handler(); + let (mut free, mut rx, mut tx) = alloc_buffers(); + + let (server_iss, fin_seq) = setup_fin_wait1(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + // Remote sends FIN without ACKing ours → Closing. + let fin = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + server_iss.wrapping_add(1), // ACK for our SYN-ACK, but not our FIN + flags::ACK | flags::FIN, + 65535, + &[], + ); + let len = fin.len(); + handler.process_ipv4( + Frame::new(20, leak(fin), len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + assert_eq!(handler.first_connection().state, TcpState::Closing); + while tx.pop().is_some() {} + + // Send ACK that does NOT cover our FIN (ack = fin_seq, not fin_seq+1). + let partial_ack = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1002, + fin_seq, // does not cover fin_seq (needs fin_seq+1) + flags::ACK, + 65535, + &[], + ); + let ack_len = partial_ack.len(); + handler.process_ipv4( + Frame::new(21, leak(partial_ack), ack_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // Connection should stay in Closing — partial ACK doesn't cover our FIN. + assert_eq!(handler.connections.len(), 1); + assert_eq!( + handler.first_connection().state, + TcpState::Closing, + "partial ACK that does not cover fin_seq must not advance to TimeWait" + ); +} diff --git a/src/net/handler/tcp/tests/timers.rs b/src/net/handler/tcp/tests/timers.rs new file mode 100644 index 0000000..7495828 --- /dev/null +++ b/src/net/handler/tcp/tests/timers.rs @@ -0,0 +1,737 @@ +use super::*; + +use super::super::handler::INITIAL_RTO_MS; + +// --------------------------------------------------------------------------- +// RTO exponential backoff — verify successive retransmissions double the RTO +// --------------------------------------------------------------------------- + +#[test] +fn rto_exponential_backoff_doubles_on_successive_retransmits() { + 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)); + } + + establish_connection(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + // Put data in send buffer and send it. + handler.first_connection_mut().send_buffer.write(b"DATA"); + handler.first_connection_mut().snd_wnd = 65535; + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + + // First RTO fire: backoff 0 -> 1. + handler.first_connection_mut().retransmit_deadline = + Some(now - coarsetime::Duration::from_millis(1)); + handler.first_connection_mut().rto_backoff = 0; + let rto_before = handler.first_connection().rto; + + handler.poll_timers(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + + let tcb = handler.first_connection(); + assert_eq!( + tcb.rto_backoff, 1, + "rto_backoff should be 1 after first RTO" + ); + // The new deadline should be now + rto << 1. + let expected_deadline = now + coarsetime::Duration::from_millis(rto_before << 1); + assert_eq!( + tcb.retransmit_deadline, + Some(expected_deadline), + "deadline should use doubled RTO" + ); + + // Second RTO fire: backoff 1 -> 2. + handler.first_connection_mut().retransmit_deadline = + Some(now - coarsetime::Duration::from_millis(1)); + handler.poll_timers(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + + let tcb = handler.first_connection(); + assert_eq!( + tcb.rto_backoff, 2, + "rto_backoff should be 2 after second RTO" + ); + let expected_deadline = now + coarsetime::Duration::from_millis(rto_before << 2); + assert_eq!( + tcb.retransmit_deadline, + Some(expected_deadline), + "deadline should use quadrupled RTO" + ); +} + +// --------------------------------------------------------------------------- +// R2 threshold — verify connection aborted after max retransmissions +// --------------------------------------------------------------------------- + +#[test] +fn r2_threshold_aborts_connection() { + 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 _server_iss = establish_connection(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + // Put data in send buffer and send it. + handler.first_connection_mut().send_buffer.write(b"DATA"); + handler.first_connection_mut().snd_wnd = 65535; + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + + let event_queue = handler.first_connection().event_queue.clone(); + + // Set rto_backoff high enough that total_elapsed_ms >= SYN_R2_THRESHOLD_MS. + // Formula: total = sum(INITIAL_RTO_MS << i) for i=0..=backoff + // With INITIAL_RTO_MS=1000, backoff=8: total = 1000*(1+2+4+8+16+32+64+128+256) = 511_000 > 180_000 + handler.first_connection_mut().rto_backoff = 8; + handler.first_connection_mut().retransmit_deadline = + Some(now - coarsetime::Duration::from_millis(1)); + + handler.poll_timers(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + + // Connection should be removed. + assert!( + handler.connections.is_empty(), + "connection should be removed after R2 threshold exceeded" + ); + + // Timeout event should have been emitted. + let event = event_queue.pop(); + assert_eq!( + event, + Some(TcpEvent::Timeout), + "TcpEvent::Timeout should be emitted on R2 abort" + ); +} + +// --------------------------------------------------------------------------- +// FIN retransmit in FinWait1 state +// --------------------------------------------------------------------------- + +#[test] +fn fin_retransmit_in_fin_wait1() { + 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 _server_iss = establish_connection(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + // Initiate close to send FIN. + handler.initiate_close(handler.first_connection_key()); + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + + assert_eq!( + handler.first_connection().state, + TcpState::FinWait1, + "should be in FinWait1 after sending FIN" + ); + assert!( + handler.first_connection().fin_seq.is_some(), + "fin_seq should be set" + ); + + // Set retransmit deadline in the past to trigger FIN retransmit. + handler.first_connection_mut().retransmit_deadline = + Some(now - coarsetime::Duration::from_millis(1)); + handler.first_connection_mut().rto_backoff = 0; + + handler.poll_timers(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + + // FIN-ACK should be retransmitted. + assert!( + tx.num_frames() >= 1, + "FIN-ACK should be retransmitted in FinWait1" + ); + + let tcb = handler.first_connection(); + assert_eq!(tcb.rto_backoff, 1, "rto_backoff should be incremented"); + assert!( + tcb.retransmit_deadline.is_some(), + "retransmit deadline should be re-armed" + ); +} + +// --------------------------------------------------------------------------- +// FIN retransmit in LastAck state +// --------------------------------------------------------------------------- + +#[test] +fn fin_retransmit_in_last_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)); + } + + let server_iss = establish_connection(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + // Receive FIN from remote to move to 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(10, leak(fin), fin_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + assert_eq!(handler.first_connection().state, TcpState::CloseWait); + + // Now close our side to move to LastAck. + handler.initiate_close(handler.first_connection_key()); + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + assert_eq!(handler.first_connection().state, TcpState::LastAck); + assert!(handler.first_connection().fin_seq.is_some()); + + // Set retransmit deadline in the past. + handler.first_connection_mut().retransmit_deadline = + Some(now - coarsetime::Duration::from_millis(1)); + handler.first_connection_mut().rto_backoff = 0; + + handler.poll_timers(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + + assert!( + tx.num_frames() >= 1, + "FIN-ACK should be retransmitted in LastAck" + ); + let tcb = handler.first_connection(); + assert_eq!(tcb.rto_backoff, 1, "rto_backoff should be incremented"); +} + +// --------------------------------------------------------------------------- +// SYN retransmit with exponential backoff +// --------------------------------------------------------------------------- + +#[test] +fn syn_retransmit_exponential_backoff() { + 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 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]); + nh.seed_cache( + coarsetime::Instant::now(), + IpAddress::V4(REMOTE_IP), + MacAddress::from([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]), + ); + + // Connect sends SYN — state goes to SynSent. + let (_key, _eq) = handler + .connect( + IpAddress::V4(LOCAL_IP), + 5000, + IpAddress::V4(REMOTE_IP), + 80, + src_mac, + dst_mac, + coarsetime::Instant::now(), + &mut free, + &mut tx, + ) + .unwrap(); + while tx.pop().is_some() {} + assert_eq!(handler.first_connection().state, TcpState::SynSent); + + let now = coarsetime::Instant::now(); + + // First retransmit: backoff 0 -> 1. + handler.first_connection_mut().retransmit_deadline = + Some(now - coarsetime::Duration::from_millis(1)); + handler.first_connection_mut().rto_backoff = 0; + + handler.poll_timers(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + assert!(tx.num_frames() >= 1, "SYN should be retransmitted"); + while tx.pop().is_some() {} + + let tcb = handler.first_connection(); + assert_eq!( + tcb.rto_backoff, 1, + "backoff should be 1 after first retransmit" + ); + // Deadline should be now + INITIAL_RTO_MS << 1. + let expected = now + coarsetime::Duration::from_millis(INITIAL_RTO_MS << 1); + assert_eq!( + tcb.retransmit_deadline, + Some(expected), + "SYN deadline should use doubled INITIAL_RTO_MS" + ); + + // Second retransmit: backoff 1 -> 2. + handler.first_connection_mut().retransmit_deadline = + Some(now - coarsetime::Duration::from_millis(1)); + handler.poll_timers(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + + let tcb = handler.first_connection(); + assert_eq!( + tcb.rto_backoff, 2, + "backoff should be 2 after second retransmit" + ); + let expected = now + coarsetime::Duration::from_millis(INITIAL_RTO_MS << 2); + assert_eq!( + tcb.retransmit_deadline, + Some(expected), + "SYN deadline should use quadrupled INITIAL_RTO_MS" + ); +} + +// --------------------------------------------------------------------------- +// SYN R2 threshold — SynSent connection removed after timeout +// --------------------------------------------------------------------------- + +#[test] +fn syn_r2_threshold_removes_connection() { + 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 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]); + nh.seed_cache( + coarsetime::Instant::now(), + IpAddress::V4(REMOTE_IP), + MacAddress::from([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]), + ); + + let (_key, event_queue) = handler + .connect( + IpAddress::V4(LOCAL_IP), + 5000, + IpAddress::V4(REMOTE_IP), + 80, + src_mac, + dst_mac, + coarsetime::Instant::now(), + &mut free, + &mut tx, + ) + .unwrap(); + while tx.pop().is_some() {} + assert_eq!(handler.first_connection().state, TcpState::SynSent); + + let now = coarsetime::Instant::now(); + + // Set backoff high enough for R2 threshold. + handler.first_connection_mut().rto_backoff = 8; + handler.first_connection_mut().retransmit_deadline = + Some(now - coarsetime::Duration::from_millis(1)); + + handler.poll_timers(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + + assert!( + handler.connections.is_empty(), + "SynSent connection should be removed after R2 threshold" + ); + assert_eq!( + event_queue.pop(), + Some(TcpEvent::Timeout), + "TcpEvent::Timeout should be emitted" + ); +} + +// --------------------------------------------------------------------------- +// SYN-ACK retransmit in SynReceived state +// --------------------------------------------------------------------------- + +#[test] +fn syn_ack_retransmit_in_syn_received() { + 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)); + } + + // Send SYN to create a SynReceived connection. + 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, + ); + while tx.pop().is_some() {} + assert_eq!(handler.first_connection().state, TcpState::SynReceived); + + let now = coarsetime::Instant::now(); + + // Trigger retransmit. + handler.first_connection_mut().retransmit_deadline = + Some(now - coarsetime::Duration::from_millis(1)); + handler.first_connection_mut().rto_backoff = 0; + + handler.poll_timers(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + + assert!( + tx.num_frames() >= 1, + "SYN-ACK should be retransmitted in SynReceived" + ); + let tcb = handler.first_connection(); + assert_eq!(tcb.rto_backoff, 1, "rto_backoff should be incremented"); + let expected = now + coarsetime::Duration::from_millis(INITIAL_RTO_MS << 1); + assert_eq!( + tcb.retransmit_deadline, + Some(expected), + "SYN-ACK deadline should be backed off" + ); +} + +// --------------------------------------------------------------------------- +// Delayed ACK with ECE flag set +// --------------------------------------------------------------------------- + +#[test] +fn delayed_ack_timer_includes_ece_flag_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)); + } + + let now = coarsetime::Instant::now(); + let _server_iss = establish_connection(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + // Set delayed ACK state and ECN CE received flag. + 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)); + tcb.ecn_ce_received = true; + } + + // Fire after deadline. + let later = now + coarsetime::Duration::from_millis(50); + handler.poll_timers(later, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + + // ACK should have been sent. + assert_eq!(tx.num_frames(), 1, "ACK should be flushed after deadline"); + + let tcb = handler.get_connection(&id).unwrap(); + assert!(!tcb.ack_pending, "ack_pending should be cleared"); +} + +// --------------------------------------------------------------------------- +// evict_stale removes TIME-WAIT connections past deadline +// --------------------------------------------------------------------------- + +#[test] +fn evict_stale_removes_time_wait_connection() { + 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 _server_iss = establish_connection(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + let now = coarsetime::Instant::now(); + + // Manually set connection to TimeWait with a deadline in the past. + handler.first_connection_mut().state = TcpState::TimeWait; + handler.first_connection_mut().time_wait_deadline = + Some(now - coarsetime::Duration::from_millis(1)); + + handler.evict_stale(now, &mut rx); + + assert!( + handler.connections.is_empty(), + "TIME-WAIT connection should be removed after deadline" + ); +} + +#[test] +fn evict_stale_keeps_time_wait_before_deadline() { + 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 _server_iss = establish_connection(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + let now = coarsetime::Instant::now(); + + // Set TimeWait with deadline in the future. + handler.first_connection_mut().state = TcpState::TimeWait; + handler.first_connection_mut().time_wait_deadline = + Some(now + coarsetime::Duration::from_millis(60_000)); + + handler.evict_stale(now, &mut rx); + + assert_eq!( + handler.connections.len(), + 1, + "TIME-WAIT connection should be kept before deadline" + ); +} + +// --------------------------------------------------------------------------- +// RTO retransmit does not fire before deadline +// --------------------------------------------------------------------------- + +#[test] +fn rto_retransmit_does_not_fire_before_deadline() { + 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 _server_iss = establish_connection(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + handler.first_connection_mut().send_buffer.write(b"DATA"); + handler.first_connection_mut().snd_wnd = 65535; + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + + // Set deadline in the future. + handler.first_connection_mut().retransmit_deadline = + Some(now + coarsetime::Duration::from_millis(5000)); + handler.first_connection_mut().rto_backoff = 0; + handler.first_connection_mut().ack_pending = false; + + handler.poll_timers(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + + assert_eq!(tx.num_frames(), 0, "no retransmission before deadline"); + assert_eq!( + handler.first_connection().rto_backoff, + 0, + "rto_backoff unchanged" + ); +} + +// --------------------------------------------------------------------------- +// FIN retransmit in Closing state +// --------------------------------------------------------------------------- + +#[test] +fn fin_retransmit_in_closing_state() { + 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)); + } + + establish_connection(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + // Initiate close to enter FinWait1. + handler.initiate_close(handler.first_connection_key()); + let now = coarsetime::Instant::now(); + handler.poll_send(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + while tx.pop().is_some() {} + assert_eq!(handler.first_connection().state, TcpState::FinWait1); + + let fin_seq = handler.first_connection().fin_seq.unwrap(); + + // Receive FIN from remote (simultaneous close) to move to Closing. + let remote_fin = build_tcp_frame( + REMOTE_IP, + LOCAL_IP, + 12345, + 80, + 1001, + fin_seq, // ACK our data but not our FIN + flags::ACK | flags::FIN, + 65535, + &[], + ); + let remote_fin_len = remote_fin.len(); + handler.process_ipv4( + Frame::new(20, leak(remote_fin), remote_fin_len, false), + coarsetime::Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + while tx.pop().is_some() {} + assert_eq!(handler.first_connection().state, TcpState::Closing); + + // Trigger FIN retransmit. + handler.first_connection_mut().retransmit_deadline = + Some(now - coarsetime::Duration::from_millis(1)); + handler.first_connection_mut().rto_backoff = 0; + + handler.poll_timers(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + + assert!( + tx.num_frames() >= 1, + "FIN-ACK should be retransmitted in Closing" + ); + let tcb = handler.first_connection(); + assert_eq!(tcb.rto_backoff, 1, "rto_backoff should be incremented"); +} + +// --------------------------------------------------------------------------- +// No retransmit when retransmit_deadline is None +// --------------------------------------------------------------------------- + +#[test] +fn no_retransmit_when_deadline_is_none() { + 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 _server_iss = establish_connection(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + // Ensure no retransmit deadline and no ack_pending. + handler.first_connection_mut().retransmit_deadline = None; + handler.first_connection_mut().ack_pending = false; + + let now = coarsetime::Instant::now(); + handler.poll_timers(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + + assert_eq!( + tx.num_frames(), + 0, + "nothing should be sent with no deadline and no pending ACK" + ); +} + +// --------------------------------------------------------------------------- +// RTO fires with empty send buffer — no retransmit frame sent +// --------------------------------------------------------------------------- + +#[test] +fn rto_fires_with_empty_send_buffer_no_retransmit() { + 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 _server_iss = establish_connection(&mut handler, &nh, &mut free, &mut rx, &mut tx); + + // Set retransmit_deadline in the past with an EMPTY send buffer. + // This simulates the edge case where the RTO fires but there's nothing to retransmit + // (e.g., all data was ACKed but the deadline wasn't cleared). + let now = coarsetime::Instant::now(); + handler.first_connection_mut().retransmit_deadline = + Some(now - coarsetime::Duration::from_millis(1)); + handler.first_connection_mut().rto_backoff = 0; + handler.first_connection_mut().ack_pending = false; + + // send_buffer is empty (no data written). + assert_eq!(handler.first_connection().send_buffer.available(), 0); + + handler.poll_timers(now, nh.local_mac(), &nh, &mut free, &mut rx, &mut tx); + + // No data frame should be sent since send_buffer is empty. + assert_eq!( + tx.num_frames(), + 0, + "no retransmit frame should be sent with empty send buffer" + ); + + // rto_backoff should still be incremented (timer processing still runs). + assert_eq!( + handler.first_connection().rto_backoff, + 1, + "rto_backoff should be incremented even with empty buffer" + ); + + // Retransmit deadline should be rescheduled. + assert!( + handler.first_connection().retransmit_deadline.is_some(), + "retransmit deadline should be rescheduled" + ); +} diff --git a/src/net/handler/tcp/tests/timestamps.rs b/src/net/handler/tcp/tests/timestamps.rs new file mode 100644 index 0000000..bc21ba0 --- /dev/null +++ b/src/net/handler/tcp/tests/timestamps.rs @@ -0,0 +1,314 @@ +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.first_connection().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.first_connection().state, TcpState::Established); + assert!(handler.first_connection().ts_enabled); + + // Set ts_recent to 1000 to make the test deterministic. + handler.first_connection_mut().ts_recent = 1000; + handler.first_connection_mut().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.first_connection().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.first_connection().state, TcpState::Established); + assert!(handler.first_connection().ts_enabled); + + // Set ts_recent to 1000. + handler.first_connection_mut().ts_recent = 1000; + handler.first_connection_mut().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.first_connection().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.first_connection().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.first_connection().state, TcpState::Established); + assert!(handler.first_connection().ts_enabled); + + // Set ts_recent to 1000 and ts_recent_age to > 24 days ago. + handler.first_connection_mut().ts_recent = 1000; + // Set ts_recent_age far in the past using a fixed tick value. + handler.first_connection_mut().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.first_connection(); + assert_eq!( + tcb.recv_buffer.available(), + 5, + "data should be accepted when ts_recent is stale" + ); +} diff --git a/src/net/handler/tcp/tests/validate.rs b/src/net/handler/tcp/tests/validate.rs new file mode 100644 index 0000000..e463c12 --- /dev/null +++ b/src/net/handler/tcp/tests/validate.rs @@ -0,0 +1,365 @@ +use coarsetime::Instant; + +use crate::net::{ + checksum::compute_tcp_checksum_ip, + wire::ip::{Ipv6, Ipv6Address}, +}; + +use super::*; + +// ── IPv4 helpers ───────────────────────────────────────────────────── + +const ETH_LEN: usize = 14; +const IPV4_LEN: usize = IPV4_MIN_HEADER_LEN; +const IPV6_LEN: usize = 40; + +/// Build a minimal Ethernet + IPv4 + TCP SYN frame (raw bytes, no checksum fixup). +fn build_raw_ipv4_tcp_syn( + src_ip: Ipv4Address, + dst_ip: Ipv4Address, + src_port: u16, + dst_port: u16, +) -> Vec { + build_tcp_frame( + src_ip, + dst_ip, + src_port, + dst_port, + 1000, + 0, + flags::SYN, + 65535, + &[], + ) +} + +/// Build an Ethernet + IPv6 + TCP frame with correct checksums. +fn build_ipv6_tcp_frame( + src_ip: Ipv6Address, + dst_ip: Ipv6Address, + 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 payload_length = tcp_header_len as u16; + let total = ETH_LEN + IPV6_LEN + tcp_header_len; + let mut buf = vec![0u8; total]; + + // 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] = 0x86; + buf[13] = 0xDD; // EtherType IPv6 + + // IPv6 header (40 bytes). + let ip = &mut buf[ETH_LEN..ETH_LEN + IPV6_LEN]; + ip[0] = 0x60; // version 6 + ip[4..6].copy_from_slice(&payload_length.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_off = ETH_LEN + IPV6_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 over IPv6 pseudo-header. + let tcp_segment = &mut buf[tcp_off..]; + let cksum = compute_tcp_checksum_ip::(&src_ip, &dst_ip, tcp_segment, &[]); + buf[tcp_off + 16] = cksum[0]; + buf[tcp_off + 17] = cksum[1]; + + buf +} + +// ── Tests ──────────────────────────────────────────────────────────── + +/// Frame too short to contain a TCP header should be returned to rx_return. +#[test] +fn frame_too_short_for_tcp_header_returned_to_rx() { + 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 valid frame then truncate it so len < tcp_offset + TCP_HEADER_LEN. + let full = build_raw_ipv4_tcp_syn(REMOTE_IP, LOCAL_IP, 12345, 80); + let tcp_offset = ETH_LEN + IPV4_LEN; + // Truncate to just before the end of the TCP header. + let short = &full[..tcp_offset + TCP_HEADER_LEN - 1]; + let data = leak(short.to_vec()); + let len = data.len(); + let frame = Frame::new(100, data, len, false); + + handler.process_ipv4(frame, Instant::now(), &nh, &mut free, &mut rx, &mut tx); + + // Frame should be returned to rx_return (free pool), not consumed. + assert!( + rx.pop().is_some(), + "short frame should be returned to rx_return" + ); + assert!( + tx.pop().is_none(), + "no response should be sent for short frame" + ); +} + +/// Data offset < 5 (invalid) should cause the frame to be returned. +#[test] +fn invalid_data_offset_too_small_returned() { + 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 mut data = build_raw_ipv4_tcp_syn(REMOTE_IP, LOCAL_IP, 12345, 80); + let tcp_off = ETH_LEN + IPV4_LEN; + + // Corrupt data offset to 4 (minimum valid is 5). Data offset is in the + // upper 4 bits of byte 12 of the TCP header. + let doff_byte = &mut data[tcp_off + 12]; + *doff_byte = (*doff_byte & 0x0F) | (4 << 4); // data_offset = 4 + + let len = data.len(); + let frame = Frame::new(200, leak(data), len, false); + + handler.process_ipv4(frame, Instant::now(), &nh, &mut free, &mut rx, &mut tx); + + assert!( + rx.pop().is_some(), + "invalid data_offset frame should be returned" + ); + assert!(tx.pop().is_none()); +} + +/// Data offset claims more bytes than available in the frame → returned. +#[test] +fn data_offset_exceeds_frame_length_returned() { + 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 minimal 20-byte TCP header frame (data_offset = 5). + let mut data = build_raw_ipv4_tcp_syn(REMOTE_IP, LOCAL_IP, 12345, 80); + let tcp_off = ETH_LEN + IPV4_LEN; + + // Set data_offset to 15 (60 bytes header), but the frame only has 20 bytes of TCP. + let doff_byte = &mut data[tcp_off + 12]; + *doff_byte = (*doff_byte & 0x0F) | (15 << 4); + + let len = data.len(); + let frame = Frame::new(300, leak(data), len, false); + + handler.process_ipv4(frame, Instant::now(), &nh, &mut free, &mut rx, &mut tx); + + assert!( + rx.pop().is_some(), + "oversized data_offset frame should be returned" + ); + assert!(tx.pop().is_none()); +} + +/// When rx_offload is disabled, a bad checksum should cause the frame to be returned. +#[test] +fn bad_checksum_returned_when_rx_offload_disabled() { + // new_handler() already creates with rx_offload=false. + 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 _accept = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + + let mut data = build_raw_ipv4_tcp_syn(REMOTE_IP, LOCAL_IP, 12345, 80); + let tcp_off = ETH_LEN + IPV4_LEN; + + // Corrupt the TCP checksum. + data[tcp_off + 16] ^= 0xFF; + data[tcp_off + 17] ^= 0xFF; + + let len = data.len(); + let frame = Frame::new(400, leak(data), len, false); + + handler.process_ipv4(frame, Instant::now(), &nh, &mut free, &mut rx, &mut tx); + + assert!(rx.pop().is_some(), "bad checksum frame should be returned"); + assert!(tx.pop().is_none(), "no response for bad checksum"); +} + +/// When rx_offload is enabled, a bad checksum should NOT cause the frame to be +/// rejected (the NIC already verified it). +#[test] +fn bad_checksum_accepted_when_rx_offload_enabled() { + let mut handler = TcpHandler::new(true, false); // rx_offload = true + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(4); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let _accept = handler.listen(IpAddress::V4(LOCAL_IP), 80, 128).unwrap(); + free.push(alloc_free_frame(900)); + + let mut data = build_raw_ipv4_tcp_syn(REMOTE_IP, LOCAL_IP, 12345, 80); + let tcp_off = ETH_LEN + IPV4_LEN; + + // Corrupt the TCP checksum — with offload enabled, this should still be processed. + data[tcp_off + 16] ^= 0xFF; + data[tcp_off + 17] ^= 0xFF; + + let len = data.len(); + let frame = Frame::new(500, leak(data), len, false); + + handler.process_ipv4(frame, Instant::now(), &nh, &mut free, &mut rx, &mut tx); + + // Should produce a SYN-ACK response (frame was not rejected). + assert!( + tx.pop().is_some(), + "offloaded checksum frame should be accepted" + ); +} + +/// IPv6 path: a valid SYN over IPv6 should be processed (covers process_ipv6). +#[test] +fn ipv6_valid_syn_processed() { + 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 src_v6 = Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]); + let dst_v6 = Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + + let _accept = handler.listen(IpAddress::V6(dst_v6), 80, 128).unwrap(); + free.push(alloc_free_frame(901)); + nh.seed_cache( + Instant::now(), + IpAddress::V6(src_v6), + MacAddress::from([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]), + ); + + let data = build_ipv6_tcp_frame(src_v6, dst_v6, 12345, 80, 1000, 0, flags::SYN, 65535, &[]); + let tcp_offset = ETH_LEN + IPV6_LEN; + let len = data.len(); + let frame = Frame::new(600, leak(data), len, false); + + handler.process_ipv6( + frame, + tcp_offset, + Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + // Should produce a SYN-ACK response. + assert!(tx.pop().is_some(), "valid IPv6 SYN should produce SYN-ACK"); +} + +/// IPv6 path: frame too short for TCP header should be returned. +#[test] +fn ipv6_frame_too_short_returned() { + 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 src_v6 = Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]); + let dst_v6 = Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + + let full = build_ipv6_tcp_frame(src_v6, dst_v6, 12345, 80, 1000, 0, flags::SYN, 65535, &[]); + let tcp_offset = ETH_LEN + IPV6_LEN; + // Truncate to just before the end of the TCP header. + let short = &full[..tcp_offset + TCP_HEADER_LEN - 1]; + let data = leak(short.to_vec()); + let len = data.len(); + let frame = Frame::new(700, data, len, false); + + handler.process_ipv6( + frame, + tcp_offset, + Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + assert!(rx.pop().is_some(), "short IPv6 frame should be returned"); + assert!(tx.pop().is_none()); +} + +/// IPv6 path: bad checksum with rx_offload disabled should be returned. +#[test] +fn ipv6_bad_checksum_returned() { + let mut handler = new_handler(); // rx_offload = false + let nh = new_neighbor_handler(); + let mut free = BasicFrameBuffer::new(4); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let src_v6 = Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]); + let dst_v6 = Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + + let _accept = handler.listen(IpAddress::V6(dst_v6), 80, 128).unwrap(); + + let mut data = build_ipv6_tcp_frame(src_v6, dst_v6, 12345, 80, 1000, 0, flags::SYN, 65535, &[]); + let tcp_off = ETH_LEN + IPV6_LEN; + + // Corrupt checksum. + data[tcp_off + 16] ^= 0xFF; + data[tcp_off + 17] ^= 0xFF; + + let len = data.len(); + let frame = Frame::new(800, leak(data), len, false); + + handler.process_ipv6( + frame, + tcp_off, + Instant::now(), + &nh, + &mut free, + &mut rx, + &mut tx, + ); + + assert!(rx.pop().is_some(), "bad IPv6 checksum should be returned"); + assert!(tx.pop().is_none()); +} diff --git a/src/net/handler/tcp/timers.rs b/src/net/handler/tcp/timers.rs new file mode 100644 index 0000000..c01b9bd --- /dev/null +++ b/src/net/handler/tcp/timers.rs @@ -0,0 +1,468 @@ +use coarsetime::Instant; +use smallvec::SmallVec; + +use crate::{ + net::{NeighborHandler, wire::tcp::flags}, + xdp::frame::FrameBuffer, +}; + +use super::{ + TcpHandler, + handler::{INITIAL_RTO_MS, SYN_R2_THRESHOLD_MS}, + segment::SegmentBuilder, + send_tracker::SendReady, + state::TcpState, + tcb::{DEFAULT_RCV_MSS, DEFAULT_RCV_WND, DEFAULT_RCV_WSCALE, TcpEvent}, +}; + +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>, + rx_return: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) { + // Collect IDs that need send-tracking after timer processing. + let mut to_mark: SmallVec<[usize; 4]> = SmallVec::new(); + + // Delayed ACK pass — flush pending ACKs whose deadline has expired. + for (key, tcb) in self.connections.iter_mut() { + 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 Some(dst_mac) = neighbor_handler.lookup_or_resolve( + now, + &id.remote_addr, + &id.local_addr, + free_frames, + rx_return, + tx_return, + ) else { + // Neighbor resolution pending — mark for poll_send to retry. + to_mark.push(key); + continue; // skip to next connection + }; + 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: SmallVec<[usize; 4]> = SmallVec::new(); + for (key, tcb) in self.connections.iter_mut() { + 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); + keep_alive_removals.push(key); + continue; + } + + // Send keep-alive probe: seq = snd_una - 1, no data, ACK. + let id = tcb.id; + let Some(dst_mac) = neighbor_handler.lookup_or_resolve( + now, + &id.remote_addr, + &id.local_addr, + free_frames, + rx_return, + tx_return, + ) else { + continue; // skip to next connection + }; + 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. + for key in &keep_alive_removals { + if let Some(tcb) = self.connections.get(*key) { + Self::decrement_syn_received(&mut self.listeners, &tcb.id); + } + self.send_tracker.unmark(*key); + self.remove_connection_by_key(*key); + } + + // SACK recovery pass — RFC 6675 recovery loop. + for (key, tcb) in self.connections.iter_mut() { + 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. + let mut sent_any = false; + 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 Some(dst_mac) = neighbor_handler.lookup_or_resolve( + now, + &id.remote_addr, + &id.local_addr, + free_frames, + rx_return, + tx_return, + ) else { + continue; // skip to next connection + }; + + 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); + sent_any = true; + } else { + // No more lost segments — could send new data, but that's + // handled by poll_send. Break here. + break; + } + } + if sent_any { + // SACK recovery sent segments — mark for poll_send to send new data. + to_mark.push(key); + } + } + + // RTO retransmit pass — timer-based. + let mut to_remove: SmallVec<[usize; 4]> = SmallVec::new(); + + for (key, tcb) in self.connections.iter_mut() { + 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); + to_remove.push(key); + continue; + } + + // Retransmit. + let id = tcb.id; + let Some(dst_mac) = neighbor_handler.lookup_or_resolve( + now, + &id.remote_addr, + &id.local_addr, + free_frames, + rx_return, + tx_return, + ) else { + // Re-arm retransmit timer so we retry after the solicitation completes. + tcb.retransmit_deadline = Some(now + coarsetime::Duration::from_millis(tcb.rto)); + continue; + }; + + 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)); + // Mark for poll_send — may have more data to send after RTO. + to_mark.push(key); + } + 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. + for key in &to_remove { + if let Some(tcb) = self.connections.get(*key) { + Self::decrement_syn_received(&mut self.listeners, &tcb.id); + } + self.send_tracker.unmark(*key); + self.remove_connection_by_key(*key); + } + + // Mark connections that need poll_send attention after timer processing. + for key in to_mark { + self.send_tracker.mark(SendReady(key)); + } + } + + /// Evict stale connections whose TIME-WAIT deadline has passed. + pub fn evict_stale<'umem>(&mut self, now: Instant, _rx_return: &mut impl FrameBuffer<'umem>) { + let keys_to_remove: SmallVec<[usize; 4]> = self + .connections + .iter() + .filter_map(|(key, tcb)| { + if tcb.state == TcpState::TimeWait + && let Some(deadline) = tcb.time_wait_deadline + && now >= deadline + { + Some(key) + } else { + None + } + }) + .collect(); + for key in keys_to_remove { + self.send_tracker.unmark(key); + self.remove_connection_by_key(key); + } + } +} diff --git a/src/net/handler/tcp/transmit.rs b/src/net/handler/tcp/transmit.rs new file mode 100644 index 0000000..53cb997 --- /dev/null +++ b/src/net/handler/tcp/transmit.rs @@ -0,0 +1,401 @@ +use coarsetime::Instant; +use smallvec::SmallVec; + +use crate::{ + net::{NeighborHandler, wire::tcp::flags}, + xdp::frame::FrameBuffer, +}; + +use super::{ + TcpHandler, + segment::SegmentBuilder, + send_tracker::SendReady, + state::TcpState, + tcb::{MAX_DELAYED_ACK_COUNT, TcpEvent}, +}; + +impl TcpHandler { + // --- Data transmission --- + + /// Poll connections with pending send work for outbound data segments. + /// Called each tick from the runtime loop after receive processing. + /// Only iterates connections tracked by the SendTracker. + 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>, + rx_return: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) { + self.send_tracker.swap(); + let mut closed: SmallVec<[usize; 4]> = SmallVec::new(); + while let Some(key) = self.send_tracker.pop_active() { + let Some(tcb) = self.connections.get_mut(key) else { + continue; // connection was removed + }; + 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 Some(dst_mac) = neighbor_handler.lookup_or_resolve( + now, + &tcb.id.remote_addr, + &tcb.id.local_addr, + free_frames, + rx_return, + tx_return, + ) else { + break; // exit inner loop — TCP retransmit will retry later + }; + + 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 Some(dst_mac) = neighbor_handler.lookup_or_resolve( + now, + &tcb.id.remote_addr, + &tcb.id.local_addr, + free_frames, + rx_return, + tx_return, + ) else { + // Neighbor resolution pending — re-mark for next tick. + self.send_tracker.mark(SendReady(key)); + continue; // skip to next connection + }; + + 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 Some(dst_mac) = neighbor_handler.lookup_or_resolve( + now, + &tcb.id.remote_addr, + &tcb.id.local_addr, + free_frames, + rx_return, + tx_return, + ) else { + // Neighbor resolution pending — re-mark for next tick. + self.send_tracker.mark(SendReady(key)); + continue; // skip to next connection + }; + + 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 Some(dst_mac) = neighbor_handler.lookup_or_resolve( + now, + &tcb.id.remote_addr, + &tcb.id.local_addr, + free_frames, + rx_return, + tx_return, + ) else { + // Neighbor resolution pending — re-mark for next tick. + self.send_tracker.mark(SendReady(key)); + continue; // skip to next connection + }; + + // 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; + closed.push(key); + continue; // connection is Closed, will be removed after loop + } + + // 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 Some(dst_mac) = neighbor_handler.lookup_or_resolve( + now, + &tcb.id.remote_addr, + &tcb.id.local_addr, + free_frames, + rx_return, + tx_return, + ) else { + // Neighbor resolution pending — re-mark for next tick. + self.send_tracker.mark(SendReady(key)); + continue; // skip to next connection + }; + + 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)); + } + } + } + + // Re-add to tracker if this connection still has pending work. + if let Some(tcb) = self.connections.get(key) { + let bytes_in_flight = tcb.snd_nxt.wrapping_sub(tcb.snd_una) as usize; + let has_data = tcb.send_buffer.available().saturating_sub(bytes_in_flight) > 0; + let has_work = has_data + || tcb.ack_pending + || tcb.pending_fin + || tcb.persist_deadline.is_some() + || tcb.retransmit_deadline.is_some(); + if has_work { + self.send_tracker.mark(SendReady(key)); + } + } + } + + // Remove connections aborted by linger deadline. + for key in &closed { + self.send_tracker.unmark(*key); + self.remove_connection_by_key(*key); + } + } +} diff --git a/src/net/handler/udp.rs b/src/net/handler/udp.rs new file mode 100644 index 0000000..b193e94 --- /dev/null +++ b/src/net/handler/udp.rs @@ -0,0 +1,1535 @@ +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_ip, + }, + fragment::{FragmentReader, Packet}, + socket::LocalQueue, + wire::{ + ethernet::EthernetFrame, + ip::{ + EXT_FRAGMENT, FRAGMENT_EXT_LEN, IPV6_HEADER_LEN, IpAddress, Ipv4, Ipv4Header, Ipv6, + 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::( + &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_ip::( + &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_ip::( + &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); + } + + // -- Multi-fragment checksum tests -- + + // Helper constants for fragment building. + const FRAG_EXT_LEN: usize = 8; // IPv6 fragment extension header length + + /// Build an IPv4 fragment frame suitable for UdpHandler::process_ipv4. + /// + /// `frag_offset_8` is the fragment offset in 8-byte units. + /// `more` sets the MF (More Fragments) flag. + /// `data` is the IP payload (UDP header + payload for first frag, just payload for subsequent). + fn build_ipv4_fragment( + src_ip: Ipv4Address, + dst_ip: Ipv4Address, + id: u16, + frag_offset_8: u16, + more: bool, + data: &[u8], + ) -> Vec { + let ip_total_len = (IPV4_MIN_HEADER_LEN + data.len()) as u16; + let mut buf = vec![0u8; ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN + data.len()]; + + // Ethernet header: EtherType = IPv4 + buf[12] = 0x08; + buf[13] = 0x00; + + // IPv4 header + let ip = &mut buf[14..]; + ip[0] = 0x45; // version=4, IHL=5 + ip[2..4].copy_from_slice(&ip_total_len.to_be_bytes()); + ip[4..6].copy_from_slice(&id.to_be_bytes()); // identification + let flags = if more { + 0x2000 | frag_offset_8 + } else { + frag_offset_8 + }; + ip[6..8].copy_from_slice(&flags.to_be_bytes()); + ip[8] = 64; // TTL + 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]; + + // IP payload (UDP header+data for first frag, raw data for subsequent) + let payload_off = ETH_HEADER_LEN + IPV4_MIN_HEADER_LEN; + buf[payload_off..payload_off + data.len()].copy_from_slice(data); + + buf + } + + /// Build an IPv6 fragment frame suitable for UdpHandler::process_ipv6. + /// + /// Produces Ethernet + IPv6 header + Fragment Extension Header + data. + /// `frag_offset_8` is the fragment offset in 8-byte units. + fn build_ipv6_fragment( + src_ip: Ipv6Address, + dst_ip: Ipv6Address, + id: u32, + frag_offset_8: u16, + more: bool, + data: &[u8], + ) -> Vec { + use crate::net::wire::ip::EXT_FRAGMENT; + let payload_len = (FRAG_EXT_LEN + data.len()) as u16; + let mut buf = vec![0u8; ETH_HEADER_LEN + IPV6_HEADER_LEN + FRAG_EXT_LEN + data.len()]; + + // Ethernet header: EtherType = IPv6 + buf[12] = 0x86; + buf[13] = 0xDD; + + // IPv6 header + let ip = &mut buf[14..]; + ip[0] = 0x60; // version=6 + ip[4..6].copy_from_slice(&payload_len.to_be_bytes()); + ip[6] = EXT_FRAGMENT; // next_header = Fragment (44) + 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); + + // Fragment Extension Header (8 bytes) + let frag_off = ETH_HEADER_LEN + IPV6_HEADER_LEN; + buf[frag_off] = IpProtocols::Udp; // next_header = UDP + buf[frag_off + 1] = 0; // reserved + let frag_offset_mf = (frag_offset_8 << 3) | if more { 1 } else { 0 }; + buf[frag_off + 2..frag_off + 4].copy_from_slice(&frag_offset_mf.to_be_bytes()); + buf[frag_off + 4..frag_off + 8].copy_from_slice(&id.to_be_bytes()); + + // Data after fragment extension header + let data_off = frag_off + FRAG_EXT_LEN; + buf[data_off..data_off + data.len()].copy_from_slice(data); + + buf + } + + /// Compute a UDP checksum over fragmented data for IPv4. + /// `udp_data` is the full UDP segment (header + payload) with checksum field zeroed. + fn compute_multi_frag_udp_checksum_v4( + src_ip: &Ipv4Address, + dst_ip: &Ipv4Address, + udp_data: &[u8], + ) -> [u8; 2] { + compute_udp_checksum(src_ip, dst_ip, udp_data) + } + + /// Compute a UDP checksum over fragmented data for IPv6. + fn compute_multi_frag_udp_checksum_v6( + src_ip: &Ipv6Address, + dst_ip: &Ipv6Address, + udp_data: &[u8], + ) -> [u8; 2] { + compute_udp_checksum_v6(src_ip, dst_ip, udp_data) + } + + #[test] + fn process_ipv4_multi_fragment_valid_checksum() { + // Build a 2-fragment IPv4 UDP packet with valid checksum. + // Fragment 1: offset=0, MF=1, contains UDP header (8 bytes) + 8 bytes payload + // Fragment 2: offset=2 (16 bytes), MF=0, contains 8 bytes payload + let payload_part1 = [0xAA; 8]; + let payload_part2 = [0xBB; 8]; + + // Build the full UDP segment to compute checksum. + let udp_len = (UDP_HEADER_LEN + payload_part1.len() + payload_part2.len()) as u16; + let mut full_udp = vec![0u8; udp_len as usize]; + full_udp[0..2].copy_from_slice(&12345u16.to_be_bytes()); // src port + full_udp[2..4].copy_from_slice(&53u16.to_be_bytes()); // dst port + full_udp[4..6].copy_from_slice(&udp_len.to_be_bytes()); // length + // checksum bytes [6..8] zeroed for computation + full_udp[8..16].copy_from_slice(&payload_part1); + full_udp[16..24].copy_from_slice(&payload_part2); + + let cksum = compute_multi_frag_udp_checksum_v4(&REMOTE_IPV4, &LOCAL_IPV4, &full_udp); + full_udp[6] = cksum[0]; + full_udp[7] = cksum[1]; + + // Fragment 1: UDP header + first 8 bytes payload (16 bytes total IP payload) + let frag1_data = &full_udp[..16]; // UDP hdr (8) + 8 bytes + let mut buf1 = build_ipv4_fragment(REMOTE_IPV4, LOCAL_IPV4, 42, 0, true, frag1_data); + + // Fragment 2: last 8 bytes payload, offset = 16/8 = 2 + let frag2_data = &full_udp[16..]; + let mut buf2 = build_ipv4_fragment(REMOTE_IPV4, LOCAL_IPV4, 42, 2, false, frag2_data); + + let mut handler = UdpHandler::new(256, false); + let rx_queue = handler.bind(IpAddress::V4(LOCAL_IPV4), 53, 128).unwrap(); + let mut rx = BasicFrameBuffer::new(8); + + let len1 = buf1.len(); + let f1 = Frame::new(0, &mut buf1, len1, false); + handler.process_ipv4(f1, &mut rx); + + let len2 = buf2.len(); + let f2 = Frame::new(1, &mut buf2, len2, false); + handler.process_ipv4(f2, &mut rx); + + assert_eq!( + rx_queue.len(), + 1, + "valid multi-fragment checksum should be delivered" + ); + assert_eq!(rx.num_frames(), 0, "no frames should be returned"); + } + + #[test] + fn process_ipv4_multi_fragment_invalid_checksum_dropped() { + // Same setup as valid test but corrupt the checksum. + let payload_part1 = [0xAA; 8]; + let payload_part2 = [0xBB; 8]; + + let udp_len = (UDP_HEADER_LEN + payload_part1.len() + payload_part2.len()) as u16; + let mut full_udp = vec![0u8; udp_len as usize]; + full_udp[0..2].copy_from_slice(&12345u16.to_be_bytes()); + full_udp[2..4].copy_from_slice(&53u16.to_be_bytes()); + full_udp[4..6].copy_from_slice(&udp_len.to_be_bytes()); + full_udp[8..16].copy_from_slice(&payload_part1); + full_udp[16..24].copy_from_slice(&payload_part2); + + let cksum = compute_multi_frag_udp_checksum_v4(&REMOTE_IPV4, &LOCAL_IPV4, &full_udp); + // Set checksum, then corrupt it. + full_udp[6] = cksum[0] ^ 0xFF; + full_udp[7] = cksum[1]; + + let frag1_data = &full_udp[..16]; + let mut buf1 = build_ipv4_fragment(REMOTE_IPV4, LOCAL_IPV4, 43, 0, true, frag1_data); + + let frag2_data = &full_udp[16..]; + let mut buf2 = build_ipv4_fragment(REMOTE_IPV4, LOCAL_IPV4, 43, 2, false, frag2_data); + + let mut handler = UdpHandler::new(256, false); + let rx_queue = handler.bind(IpAddress::V4(LOCAL_IPV4), 53, 128).unwrap(); + let mut rx = BasicFrameBuffer::new(8); + + let len1 = buf1.len(); + let f1 = Frame::new(0, &mut buf1, len1, false); + handler.process_ipv4(f1, &mut rx); + + let len2 = buf2.len(); + let f2 = Frame::new(1, &mut buf2, len2, false); + handler.process_ipv4(f2, &mut rx); + + assert_eq!( + rx_queue.len(), + 0, + "invalid multi-fragment checksum should be dropped" + ); + assert_eq!( + rx.num_frames(), + 2, + "both fragment frames should be returned" + ); + } + + #[test] + fn process_ipv4_multi_fragment_zero_checksum_accepted() { + // IPv4 allows zero checksum (means "no checksum"). + let payload_part1 = [0xCC; 8]; + let payload_part2 = [0xDD; 8]; + + let udp_len = (UDP_HEADER_LEN + payload_part1.len() + payload_part2.len()) as u16; + let mut full_udp = vec![0u8; udp_len as usize]; + full_udp[0..2].copy_from_slice(&12345u16.to_be_bytes()); + full_udp[2..4].copy_from_slice(&53u16.to_be_bytes()); + full_udp[4..6].copy_from_slice(&udp_len.to_be_bytes()); + // Leave checksum as zero. + full_udp[8..16].copy_from_slice(&payload_part1); + full_udp[16..24].copy_from_slice(&payload_part2); + + let frag1_data = &full_udp[..16]; + let mut buf1 = build_ipv4_fragment(REMOTE_IPV4, LOCAL_IPV4, 44, 0, true, frag1_data); + + let frag2_data = &full_udp[16..]; + let mut buf2 = build_ipv4_fragment(REMOTE_IPV4, LOCAL_IPV4, 44, 2, false, frag2_data); + + let mut handler = UdpHandler::new(256, false); + let rx_queue = handler.bind(IpAddress::V4(LOCAL_IPV4), 53, 128).unwrap(); + let mut rx = BasicFrameBuffer::new(8); + + let len1 = buf1.len(); + let f1 = Frame::new(0, &mut buf1, len1, false); + handler.process_ipv4(f1, &mut rx); + + let len2 = buf2.len(); + let f2 = Frame::new(1, &mut buf2, len2, false); + handler.process_ipv4(f2, &mut rx); + + assert_eq!( + rx_queue.len(), + 1, + "zero checksum should be accepted for IPv4 multi-fragment" + ); + assert_eq!(rx.num_frames(), 0); + } + + #[test] + fn process_ipv6_multi_fragment_valid_checksum() { + // Build a 2-fragment IPv6 UDP packet with valid checksum. + let payload_part1 = [0xAA; 8]; + let payload_part2 = [0xBB; 8]; + + let udp_len = (UDP_HEADER_LEN + payload_part1.len() + payload_part2.len()) as u16; + let mut full_udp = vec![0u8; udp_len as usize]; + full_udp[0..2].copy_from_slice(&12345u16.to_be_bytes()); + full_udp[2..4].copy_from_slice(&53u16.to_be_bytes()); + full_udp[4..6].copy_from_slice(&udp_len.to_be_bytes()); + full_udp[8..16].copy_from_slice(&payload_part1); + full_udp[16..24].copy_from_slice(&payload_part2); + + let cksum = compute_multi_frag_udp_checksum_v6(&REMOTE_IPV6, &LOCAL_IPV6, &full_udp); + full_udp[6] = cksum[0]; + full_udp[7] = cksum[1]; + + let frag1_data = &full_udp[..16]; // UDP hdr (8) + 8 bytes + let mut buf1 = build_ipv6_fragment(REMOTE_IPV6, LOCAL_IPV6, 100, 0, true, frag1_data); + + let frag2_data = &full_udp[16..]; + let mut buf2 = build_ipv6_fragment(REMOTE_IPV6, LOCAL_IPV6, 100, 2, false, frag2_data); + + let mut handler = UdpHandler::new(256, false); + let rx_queue = handler.bind(IpAddress::V6(LOCAL_IPV6), 53, 128).unwrap(); + let mut rx = BasicFrameBuffer::new(8); + + let frag_ext_offset = ETH_HEADER_LEN + IPV6_HEADER_LEN; + + let len1 = buf1.len(); + let f1 = Frame::new(0, &mut buf1, len1, false); + handler.process_ipv6(f1, Some(frag_ext_offset), frag_ext_offset, &mut rx); + + let len2 = buf2.len(); + let f2 = Frame::new(1, &mut buf2, len2, false); + handler.process_ipv6(f2, Some(frag_ext_offset), frag_ext_offset, &mut rx); + + assert_eq!( + rx_queue.len(), + 1, + "valid multi-fragment IPv6 checksum should be delivered" + ); + assert_eq!(rx.num_frames(), 0); + } + + #[test] + fn process_ipv6_multi_fragment_invalid_checksum_dropped() { + let payload_part1 = [0xAA; 8]; + let payload_part2 = [0xBB; 8]; + + let udp_len = (UDP_HEADER_LEN + payload_part1.len() + payload_part2.len()) as u16; + let mut full_udp = vec![0u8; udp_len as usize]; + full_udp[0..2].copy_from_slice(&12345u16.to_be_bytes()); + full_udp[2..4].copy_from_slice(&53u16.to_be_bytes()); + full_udp[4..6].copy_from_slice(&udp_len.to_be_bytes()); + full_udp[8..16].copy_from_slice(&payload_part1); + full_udp[16..24].copy_from_slice(&payload_part2); + + let cksum = compute_multi_frag_udp_checksum_v6(&REMOTE_IPV6, &LOCAL_IPV6, &full_udp); + // Corrupt checksum. + full_udp[6] = cksum[0] ^ 0xFF; + full_udp[7] = cksum[1]; + + let frag1_data = &full_udp[..16]; + let mut buf1 = build_ipv6_fragment(REMOTE_IPV6, LOCAL_IPV6, 101, 0, true, frag1_data); + + let frag2_data = &full_udp[16..]; + let mut buf2 = build_ipv6_fragment(REMOTE_IPV6, LOCAL_IPV6, 101, 2, false, frag2_data); + + let mut handler = UdpHandler::new(256, false); + let rx_queue = handler.bind(IpAddress::V6(LOCAL_IPV6), 53, 128).unwrap(); + let mut rx = BasicFrameBuffer::new(8); + + let frag_ext_offset = ETH_HEADER_LEN + IPV6_HEADER_LEN; + + let len1 = buf1.len(); + let f1 = Frame::new(0, &mut buf1, len1, false); + handler.process_ipv6(f1, Some(frag_ext_offset), frag_ext_offset, &mut rx); + + let len2 = buf2.len(); + let f2 = Frame::new(1, &mut buf2, len2, false); + handler.process_ipv6(f2, Some(frag_ext_offset), frag_ext_offset, &mut rx); + + assert_eq!( + rx_queue.len(), + 0, + "invalid multi-fragment IPv6 checksum should be dropped" + ); + assert_eq!( + rx.num_frames(), + 2, + "both fragment frames should be returned" + ); + } + + #[test] + fn process_ipv6_multi_fragment_zero_checksum_rejected() { + // IPv6 does NOT allow zero checksum (RFC 2460). + let payload_part1 = [0xCC; 8]; + let payload_part2 = [0xDD; 8]; + + let udp_len = (UDP_HEADER_LEN + payload_part1.len() + payload_part2.len()) as u16; + let mut full_udp = vec![0u8; udp_len as usize]; + full_udp[0..2].copy_from_slice(&12345u16.to_be_bytes()); + full_udp[2..4].copy_from_slice(&53u16.to_be_bytes()); + full_udp[4..6].copy_from_slice(&udp_len.to_be_bytes()); + // Leave checksum as zero — should be rejected. + full_udp[8..16].copy_from_slice(&payload_part1); + full_udp[16..24].copy_from_slice(&payload_part2); + + let frag1_data = &full_udp[..16]; + let mut buf1 = build_ipv6_fragment(REMOTE_IPV6, LOCAL_IPV6, 102, 0, true, frag1_data); + + let frag2_data = &full_udp[16..]; + let mut buf2 = build_ipv6_fragment(REMOTE_IPV6, LOCAL_IPV6, 102, 2, false, frag2_data); + + let mut handler = UdpHandler::new(256, false); + let rx_queue = handler.bind(IpAddress::V6(LOCAL_IPV6), 53, 128).unwrap(); + let mut rx = BasicFrameBuffer::new(8); + + let frag_ext_offset = ETH_HEADER_LEN + IPV6_HEADER_LEN; + + let len1 = buf1.len(); + let f1 = Frame::new(0, &mut buf1, len1, false); + handler.process_ipv6(f1, Some(frag_ext_offset), frag_ext_offset, &mut rx); + + let len2 = buf2.len(); + let f2 = Frame::new(1, &mut buf2, len2, false); + handler.process_ipv6(f2, Some(frag_ext_offset), frag_ext_offset, &mut rx); + + assert_eq!( + rx_queue.len(), + 0, + "zero checksum should be rejected for IPv6 multi-fragment" + ); + assert_eq!( + rx.num_frames(), + 2, + "both fragment frames should be returned" + ); + } +} diff --git a/src/net/http/body.rs b/src/net/http/body.rs new file mode 100644 index 0000000..21fd012 --- /dev/null +++ b/src/net/http/body.rs @@ -0,0 +1,568 @@ +use crate::net::http::{HttpError, buffer::ReadBuffer, error::ParseError, request::BodyFraming}; +use crate::net::socket::TcpStream; +use memchr::memchr; + +/// Reads the request body according to the body framing. +/// +/// Must be fully consumed (or dropped) before writing the response. +pub struct BodyReader<'conn> { + stream: &'conn TcpStream, + read_buf: &'conn mut ReadBuffer, + framing: BodyFraming, + remaining: usize, + chunk_state: ChunkState, + finished: bool, + expect_continue: bool, + continue_sent: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ChunkState { + AwaitingSize, + ReadingData(usize), + AwaitingTrailer, + Done, +} + +impl<'conn> BodyReader<'conn> { + pub(crate) fn new( + stream: &'conn TcpStream, + read_buf: &'conn mut ReadBuffer, + framing: BodyFraming, + expect_continue: bool, + ) -> Self { + let (remaining, finished, chunk_state) = match framing { + BodyFraming::None => (0, true, ChunkState::Done), + BodyFraming::ContentLength(len) => (len, len == 0, ChunkState::Done), + BodyFraming::Chunked => (0, false, ChunkState::AwaitingSize), + }; + Self { + stream, + read_buf, + framing, + remaining, + chunk_state, + finished, + expect_continue, + continue_sent: false, + } + } + + /// Read decoded body bytes into `dest`. Returns the number of bytes read. + /// Returns 0 when the body is fully consumed. + pub async fn read(&mut self, dest: &mut [u8]) -> Result { + if self.finished { + return Ok(0); + } + + // Send 100-continue if needed (lazy, on first read) + if self.expect_continue && !self.continue_sent { + let response = b"HTTP/1.1 100 Continue\r\n\r\n"; + self.stream.write(response).await.map_err(HttpError::Tcp)?; + self.continue_sent = true; + } + + match self.framing { + BodyFraming::None => { + self.finished = true; + Ok(0) + } + BodyFraming::ContentLength(_) => self.read_content_length(dest).await, + BodyFraming::Chunked => self.read_chunked(dest).await, + } + } + + /// Read the entire body up to `limit` bytes. + pub async fn read_all(&mut self, limit: usize) -> Result, HttpError> { + let mut body = Vec::new(); + let mut buf = [0u8; 4096]; + loop { + let n = self.read(&mut buf).await?; + if n == 0 { + break; + } + if body.len() + n > limit { + return Err(HttpError::Parse(ParseError::RequestTooLarge)); + } + body.extend_from_slice(&buf[..n]); + } + Ok(body) + } + + /// Whether the body has been fully consumed. + pub fn is_finished(&self) -> bool { + self.finished + } + + async fn read_content_length(&mut self, dest: &mut [u8]) -> Result { + if self.remaining == 0 { + self.finished = true; + return Ok(0); + } + + let to_read = dest.len().min(self.remaining); + + // First try to read from the existing read buffer + let buffered = self.read_buf.unconsumed(); + if !buffered.is_empty() { + let n = to_read.min(buffered.len()); + dest[..n].copy_from_slice(&buffered[..n]); + self.read_buf.consume(n); + self.remaining -= n; + if self.remaining == 0 { + self.finished = true; + } + return Ok(n); + } + + // Need more data from stream — compact if needed + if self.read_buf.remaining_capacity() == 0 { + self.read_buf.compact(); + } + + // Read from stream + let buf_slice = self.read_buf.writable_slice(); + let n = self.stream.read(buf_slice).await.map_err(HttpError::Tcp)?; + if n == 0 { + self.finished = true; + return Err(HttpError::Closed); + } + self.read_buf.advance_end(n); + + // Now read from buffer + let buffered = self.read_buf.unconsumed(); + let n = to_read.min(buffered.len()); + dest[..n].copy_from_slice(&buffered[..n]); + self.read_buf.consume(n); + self.remaining -= n; + if self.remaining == 0 { + self.finished = true; + } + Ok(n) + } + + async fn read_chunked(&mut self, dest: &mut [u8]) -> Result { + loop { + match self.chunk_state { + ChunkState::Done => { + self.finished = true; + return Ok(0); + } + ChunkState::AwaitingSize => { + let size = self.read_chunk_size().await?; + if size == 0 { + self.chunk_state = ChunkState::Done; + self.finished = true; + return Ok(0); + } + self.chunk_state = ChunkState::ReadingData(size); + } + ChunkState::ReadingData(remaining) => { + let to_read = dest.len().min(remaining); + + // Read from buffer first + let buffered = self.read_buf.unconsumed(); + if buffered.is_empty() { + // Need more data from stream + if self.read_buf.remaining_capacity() == 0 { + self.read_buf.compact(); + } + let buf_slice = self.read_buf.writable_slice(); + let n = self.stream.read(buf_slice).await.map_err(HttpError::Tcp)?; + if n == 0 { + return Err(HttpError::Closed); + } + self.read_buf.advance_end(n); + continue; + } + + let n = to_read.min(buffered.len()); + dest[..n].copy_from_slice(&buffered[..n]); + self.read_buf.consume(n); + + let new_remaining = remaining - n; + if new_remaining == 0 { + self.chunk_state = ChunkState::AwaitingTrailer; + } else { + self.chunk_state = ChunkState::ReadingData(new_remaining); + } + return Ok(n); + } + ChunkState::AwaitingTrailer => { + // Consume the \r\n after chunk data + self.consume_crlf().await?; + self.chunk_state = ChunkState::AwaitingSize; + } + } + } + } + + /// Read a chunk size line (hex digits followed by \r\n). + /// Handles chunk extensions (e.g., "a;ext=val\r\n") by trimming at `;`. + async fn read_chunk_size(&mut self) -> Result { + loop { + let buffered = self.read_buf.unconsumed(); + if let Some(pos) = memchr(b'\n', buffered) { + let line_end = if pos > 0 && buffered[pos - 1] == b'\r' { + pos - 1 + } else { + pos + }; + let line = &buffered[..line_end]; + // Trim chunk extensions at `;` (RFC 7230 Section 4.1.1) + let hex = match line.iter().position(|&b| b == b';') { + Some(semi) => &line[..semi], + None => line, + }; + let size = usize::from_str_radix( + std::str::from_utf8(hex) + .map_err(|_| HttpError::Parse(ParseError::InvalidChunkEncoding))?, + 16, + ) + .map_err(|_| HttpError::Parse(ParseError::InvalidChunkEncoding))?; + self.read_buf.consume(pos + 1); + return Ok(size); + } + + // Need more data + if self.read_buf.remaining_capacity() == 0 { + self.read_buf.compact(); + } + let buf_slice = self.read_buf.writable_slice(); + let n = self.stream.read(buf_slice).await.map_err(HttpError::Tcp)?; + if n == 0 { + return Err(HttpError::Closed); + } + self.read_buf.advance_end(n); + } + } + + /// Consume \r\n from the buffer. + async fn consume_crlf(&mut self) -> Result<(), HttpError> { + loop { + let buffered = self.read_buf.unconsumed(); + if buffered.len() >= 2 { + if buffered[0] == b'\r' && buffered[1] == b'\n' { + self.read_buf.consume(2); + return Ok(()); + } else if buffered[0] == b'\n' { + self.read_buf.consume(1); + return Ok(()); + } else { + return Err(HttpError::Parse(ParseError::InvalidChunkEncoding)); + } + } + if buffered.len() == 1 && buffered[0] == b'\n' { + self.read_buf.consume(1); + return Ok(()); + } + + // Need more data + if self.read_buf.remaining_capacity() == 0 { + self.read_buf.compact(); + } + let buf_slice = self.read_buf.writable_slice(); + let n = self.stream.read(buf_slice).await.map_err(HttpError::Tcp)?; + if n == 0 { + return Err(HttpError::Closed); + } + self.read_buf.advance_end(n); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::net::handler::tcp::TcpHandler; + use crate::net::handler::tcp::tcb::ConnectionId; + use crate::net::socket::LocalQueue; + use crate::net::wire::ip::{IpAddress, Ipv4Address}; + use std::cell::UnsafeCell; + use std::rc::Rc; + + /// Build a fake TcpStream and a pre-filled ReadBuffer for unit testing. + /// + /// The buffer is pre-populated with `data` via `append()`. All chunked + /// parsing logic will consume from the buffer and never fall through to + /// the stream (which has no real TCP connection behind it). + fn make_reader_parts(data: &[u8]) -> (crate::net::socket::TcpStream, ReadBuffer) { + let handler = Rc::new(UnsafeCell::new(TcpHandler::new(false, false))); + let conn_id = ConnectionId { + local_addr: IpAddress::V4(Ipv4Address::unspecified()), + local_port: 0, + remote_addr: IpAddress::V4(Ipv4Address::unspecified()), + remote_port: 0, + }; + let event_queue = LocalQueue::new(16); + let stream = + crate::net::socket::TcpStream::from_accepted_for_test(0, conn_id, event_queue, handler); + + let mut buf = ReadBuffer::new(4096); + let written = buf.append(data); + assert_eq!(written, data.len(), "test data too large for ReadBuffer"); + + (stream, buf) + } + + #[test] + fn body_reader_none_is_immediately_finished() { + let (remaining, finished, _) = match BodyFraming::None { + BodyFraming::None => (0, true, ChunkState::Done), + BodyFraming::ContentLength(len) => (len, len == 0, ChunkState::Done), + BodyFraming::Chunked => (0, false, ChunkState::AwaitingSize), + }; + assert!(finished); + assert_eq!(remaining, 0); + } + + #[test] + fn body_reader_content_length_zero_is_finished() { + let (remaining, finished, _) = match BodyFraming::ContentLength(0) { + BodyFraming::None => (0, true, ChunkState::Done), + BodyFraming::ContentLength(len) => (len, len == 0, ChunkState::Done), + BodyFraming::Chunked => (0, false, ChunkState::AwaitingSize), + }; + assert!(finished); + assert_eq!(remaining, 0); + } + + #[test] + fn body_reader_content_length_nonzero_not_finished() { + let (remaining, finished, _) = match BodyFraming::ContentLength(42) { + BodyFraming::None => (0, true, ChunkState::Done), + BodyFraming::ContentLength(len) => (len, len == 0, ChunkState::Done), + BodyFraming::Chunked => (0, false, ChunkState::AwaitingSize), + }; + assert!(!finished); + assert_eq!(remaining, 42); + } + + #[test] + fn body_reader_chunked_not_finished() { + let (_, finished, chunk_state) = match BodyFraming::Chunked { + BodyFraming::None => (0, true, ChunkState::Done), + BodyFraming::ContentLength(len) => (len, len == 0, ChunkState::Done), + BodyFraming::Chunked => (0, false, ChunkState::AwaitingSize), + }; + assert!(!finished); + assert_eq!(chunk_state, ChunkState::AwaitingSize); + } + + /// Single chunk: "5\r\nhello\r\n0\r\n\r\n" + /// First read should yield "hello" (5 bytes); second read should return 0 (finished). + #[test] + fn chunked_read_single_chunk() { + let payload = b"5\r\nhello\r\n0\r\n\r\n"; + let (stream, mut buf) = make_reader_parts(payload); + let mut reader = BodyReader::new(&stream, &mut buf, BodyFraming::Chunked, false); + + let mut dest = [0u8; 64]; + + let n = futures::executor::block_on(reader.read(&mut dest)).expect("first read failed"); + assert_eq!(n, 5); + assert_eq!(&dest[..n], b"hello"); + + let n2 = futures::executor::block_on(reader.read(&mut dest)).expect("second read failed"); + assert_eq!(n2, 0, "expected finished after terminal chunk"); + assert!(reader.is_finished()); + } + + /// Multiple chunks: "3\r\nabc\r\n4\r\ndefg\r\n0\r\n\r\n" + /// Reads should accumulate "abcdefg" across calls. + #[test] + fn chunked_read_multiple_chunks() { + let payload = b"3\r\nabc\r\n4\r\ndefg\r\n0\r\n\r\n"; + let (stream, mut buf) = make_reader_parts(payload); + let mut reader = BodyReader::new(&stream, &mut buf, BodyFraming::Chunked, false); + + let mut body = Vec::new(); + let mut dest = [0u8; 64]; + loop { + let n = futures::executor::block_on(reader.read(&mut dest)).expect("read failed"); + if n == 0 { + break; + } + body.extend_from_slice(&dest[..n]); + } + + assert_eq!(body, b"abcdefg"); + assert!(reader.is_finished()); + } + + /// Empty body: "0\r\n\r\n" — terminal chunk only. + /// First read must return 0 immediately. + #[test] + fn chunked_read_empty_body() { + let payload = b"0\r\n\r\n"; + let (stream, mut buf) = make_reader_parts(payload); + let mut reader = BodyReader::new(&stream, &mut buf, BodyFraming::Chunked, false); + + let mut dest = [0u8; 64]; + let n = futures::executor::block_on(reader.read(&mut dest)).expect("read failed"); + assert_eq!(n, 0, "expected immediate EOF for empty chunked body"); + assert!(reader.is_finished()); + } + + /// Invalid hex in chunk size line should return a parse error. + #[test] + fn chunked_read_invalid_hex_returns_error() { + let payload = b"XYZ\r\nignored\r\n0\r\n\r\n"; + let (stream, mut buf) = make_reader_parts(payload); + let mut reader = BodyReader::new(&stream, &mut buf, BodyFraming::Chunked, false); + + let mut dest = [0u8; 64]; + let result = futures::executor::block_on(reader.read(&mut dest)); + assert!( + result.is_err(), + "expected a parse error for invalid chunk size, got Ok" + ); + match result.unwrap_err() { + HttpError::Parse(crate::net::http::error::ParseError::InvalidChunkEncoding) => {} + e => panic!("expected InvalidChunkEncoding, got {:?}", e), + } + } + + #[test] + fn content_length_read_exact() { + let payload = b"Hello, World!"; + let (stream, mut buf) = make_reader_parts(payload); + let mut reader = BodyReader::new( + &stream, + &mut buf, + BodyFraming::ContentLength(payload.len()), + false, + ); + + let mut dest = [0u8; 64]; + let n = futures::executor::block_on(reader.read(&mut dest)).expect("read failed"); + assert_eq!(&dest[..n], payload); + assert!(reader.is_finished()); + } + + #[test] + fn content_length_zero_is_immediately_finished() { + let (stream, mut buf) = make_reader_parts(b""); + let mut reader = BodyReader::new(&stream, &mut buf, BodyFraming::ContentLength(0), false); + + let mut dest = [0u8; 64]; + let n = futures::executor::block_on(reader.read(&mut dest)).expect("read failed"); + assert_eq!(n, 0); + assert!(reader.is_finished()); + } + + #[test] + fn body_framing_none_returns_zero() { + let (stream, mut buf) = make_reader_parts(b"ignored data"); + let mut reader = BodyReader::new(&stream, &mut buf, BodyFraming::None, false); + + let mut dest = [0u8; 64]; + let n = futures::executor::block_on(reader.read(&mut dest)).expect("read failed"); + assert_eq!(n, 0); + assert!(reader.is_finished()); + } + + #[test] + fn read_all_content_length_body() { + let payload = b"Hello, World!"; + let (stream, mut buf) = make_reader_parts(payload); + let mut reader = BodyReader::new( + &stream, + &mut buf, + BodyFraming::ContentLength(payload.len()), + false, + ); + + let body = futures::executor::block_on(reader.read_all(1024)).expect("read_all failed"); + assert_eq!(body, payload); + assert!(reader.is_finished()); + } + + #[test] + fn read_all_exceeds_size_limit() { + let payload = b"This is a longer payload than allowed"; + let (stream, mut buf) = make_reader_parts(payload); + let mut reader = BodyReader::new( + &stream, + &mut buf, + BodyFraming::ContentLength(payload.len()), + false, + ); + + // Set limit smaller than payload + let result = futures::executor::block_on(reader.read_all(10)); + assert!(result.is_err(), "expected error when body exceeds limit"); + match result.unwrap_err() { + HttpError::Parse(ParseError::RequestTooLarge) => {} + e => panic!("expected RequestTooLarge, got {:?}", e), + } + } + + #[test] + fn chunked_read_invalid_chunk_size_non_hex() { + // "ZZZ" is not valid hex — should return InvalidChunkEncoding. + let payload = b"ZZZ\r\ndata\r\n0\r\n\r\n"; + let (stream, mut buf) = make_reader_parts(payload); + let mut reader = BodyReader::new(&stream, &mut buf, BodyFraming::Chunked, false); + + let mut dest = [0u8; 64]; + let result = futures::executor::block_on(reader.read(&mut dest)); + assert!(result.is_err()); + match result.unwrap_err() { + HttpError::Parse(ParseError::InvalidChunkEncoding) => {} + e => panic!("expected InvalidChunkEncoding, got {:?}", e), + } + } + + #[test] + fn chunked_read_with_bare_newline_in_size_line() { + // Use bare \n instead of \r\n in the chunk size line. + // The parser should tolerate this (it searches for \n, strips optional \r). + let payload = b"5\nhello\r\n0\n\r\n"; + let (stream, mut buf) = make_reader_parts(payload); + let mut reader = BodyReader::new(&stream, &mut buf, BodyFraming::Chunked, false); + + let mut dest = [0u8; 64]; + let n = futures::executor::block_on(reader.read(&mut dest)).expect("read failed"); + assert_eq!(n, 5); + assert_eq!(&dest[..n], b"hello"); + } + + #[test] + fn chunked_read_with_chunk_extension() { + // Chunk extension (e.g., "5;ext=val\r\n") should be trimmed at `;`. + let payload = b"5;ext=val\r\nhello\r\n0\r\n\r\n"; + let (stream, mut buf) = make_reader_parts(payload); + let mut reader = BodyReader::new(&stream, &mut buf, BodyFraming::Chunked, false); + + let mut dest = [0u8; 64]; + let n = futures::executor::block_on(reader.read(&mut dest)).expect("read failed"); + assert_eq!(n, 5); + assert_eq!(&dest[..n], b"hello"); + } + + #[test] + fn read_all_chunked_body() { + let payload = b"3\r\nabc\r\n4\r\ndefg\r\n0\r\n\r\n"; + let (stream, mut buf) = make_reader_parts(payload); + let mut reader = BodyReader::new(&stream, &mut buf, BodyFraming::Chunked, false); + + let body = futures::executor::block_on(reader.read_all(1024)).expect("read_all failed"); + assert_eq!(body, b"abcdefg"); + assert!(reader.is_finished()); + } + + #[test] + fn chunked_read_large_chunk_size() { + // Chunk size in hex: "10" = 16 bytes + let payload = b"10\r\n0123456789abcdef\r\n0\r\n\r\n"; + let (stream, mut buf) = make_reader_parts(payload); + let mut reader = BodyReader::new(&stream, &mut buf, BodyFraming::Chunked, false); + + let mut dest = [0u8; 64]; + let n = futures::executor::block_on(reader.read(&mut dest)).expect("read failed"); + assert_eq!(n, 16); + assert_eq!(&dest[..n], b"0123456789abcdef"); + } +} diff --git a/src/net/http/buffer.rs b/src/net/http/buffer.rs new file mode 100644 index 0000000..5482a2e --- /dev/null +++ b/src/net/http/buffer.rs @@ -0,0 +1,284 @@ +/// Fixed-capacity read buffer for HTTP parsing. +/// +/// Tracks unconsumed data via `start` and `end` positions. Parsers operate +/// on `unconsumed()` which returns a zero-copy `&[u8]` slice. The codec +/// stores offsets into this buffer; `slice_at()` resolves those offsets. +/// +/// Capacity must be a power of two. +pub struct ReadBuffer { + buf: Vec, + start: usize, + end: usize, +} + +impl ReadBuffer { + /// Create a new ReadBuffer with the given capacity. + /// Capacity must be a power of two. + pub fn new(capacity: usize) -> Self { + assert!( + capacity.is_power_of_two(), + "ReadBuffer capacity must be a power of two" + ); + Self { + buf: vec![0u8; capacity], + start: 0, + end: 0, + } + } + + /// Returns a slice of the unconsumed data in the buffer. + #[inline] + pub fn unconsumed(&self) -> &[u8] { + &self.buf[self.start..self.end] + } + + /// Returns a slice at the given absolute offsets into the buffer. + /// + /// These offsets are absolute positions in the buffer (not relative + /// to `start`). Used by `Request` to resolve path offsets. + #[inline] + pub fn slice_at(&self, from: usize, to: usize) -> &[u8] { + &self.buf[from..to] + } + + /// Mark `n` bytes as consumed, advancing the start position. + #[inline] + pub fn consume(&mut self, n: usize) { + self.start += n; + debug_assert!(self.start <= self.end); + } + + /// Returns the number of bytes that can still be appended. + #[inline] + pub fn remaining_capacity(&self) -> usize { + self.buf.len() - self.end + } + + /// Append bytes from a slice into the buffer. Returns the number + /// of bytes actually written (may be less than `data.len()` if + /// the buffer is nearly full). + #[allow(dead_code)] + pub fn append(&mut self, data: &[u8]) -> usize { + let to_write = data.len().min(self.remaining_capacity()); + if to_write == 0 { + return 0; + } + self.buf[self.end..self.end + to_write].copy_from_slice(&data[..to_write]); + self.end += to_write; + to_write + } + + /// Shift unconsumed data to the front of the buffer, reclaiming + /// space from consumed bytes. + /// + /// Only call when no outstanding offset-based reads are in progress + /// (i.e., between request/response cycles). + pub fn compact(&mut self) { + if self.start == 0 { + return; + } + let len = self.end - self.start; + self.buf.copy_within(self.start..self.end, 0); + self.start = 0; + self.end = len; + } + + /// Returns the current start position (offset of first unconsumed byte). + /// Used by the codec to compute absolute offsets for parsed fields. + #[inline] + pub fn start(&self) -> usize { + self.start + } + + /// Returns a mutable slice of the writable region (from end to capacity). + /// Used by HttpConnection to pass to TcpStream::read(). + pub fn writable_slice(&mut self) -> &mut [u8] { + &mut self.buf[self.end..] + } + + /// Advance the end position after writing into writable_slice(). + pub fn advance_end(&mut self, n: usize) { + self.end += n; + debug_assert!(self.end <= self.buf.len()); + } +} + +/// Fixed-capacity write buffer for HTTP response output. +/// +/// Accumulates response bytes before flushing to the TcpStream. +/// Capacity must be a power of two. +pub struct WriteBuffer { + buf: Vec, + start: usize, + end: usize, +} + +impl WriteBuffer { + /// Create a new WriteBuffer with the given capacity. + /// Capacity must be a power of two. + pub fn new(capacity: usize) -> Self { + assert!( + capacity.is_power_of_two(), + "WriteBuffer capacity must be a power of two" + ); + Self { + buf: vec![0u8; capacity], + start: 0, + end: 0, + } + } + + /// Append bytes to the write buffer. Returns the number of bytes written. + pub fn write(&mut self, data: &[u8]) -> usize { + let to_write = data.len().min(self.buf.len() - self.end); + if to_write == 0 { + return 0; + } + self.buf[self.end..self.end + to_write].copy_from_slice(&data[..to_write]); + self.end += to_write; + to_write + } + + /// Returns the buffered data ready to be flushed. + pub fn pending(&self) -> &[u8] { + &self.buf[self.start..self.end] + } + + /// Mark `n` bytes as flushed. + pub fn advance(&mut self, n: usize) { + self.start += n; + debug_assert!(self.start <= self.end); + if self.start == self.end { + self.start = 0; + self.end = 0; + } + } + + /// Returns true if there is no pending data. + pub fn is_empty(&self) -> bool { + self.start == self.end + } + + /// Returns the number of bytes that can still be written. + pub fn remaining_capacity(&self) -> usize { + self.buf.len() - self.end + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // ReadBuffer tests + + #[test] + fn new_buffer_is_empty() { + let buf = ReadBuffer::new(1024); + assert_eq!(buf.unconsumed().len(), 0); + assert_eq!(buf.remaining_capacity(), 1024); + } + + #[test] + #[should_panic(expected = "must be a power of two")] + fn non_power_of_two_panics() { + ReadBuffer::new(1000); + } + + #[test] + fn append_and_read() { + let mut buf = ReadBuffer::new(1024); + let n = buf.append(b"GET /index.html\r\n"); + assert_eq!(n, 17); + assert_eq!(buf.unconsumed(), b"GET /index.html\r\n"); + } + + #[test] + fn consume_advances_start() { + let mut buf = ReadBuffer::new(1024); + buf.append(b"GET /index.html\r\n"); + buf.consume(4); + assert_eq!(buf.unconsumed(), b"/index.html\r\n"); + } + + #[test] + fn compact_shifts_data() { + let mut buf = ReadBuffer::new(16); + buf.append(b"12345678"); // fill half + buf.consume(8); // consume all — start is now at 8 + buf.append(b"abcdefgh"); // fill remaining 8 bytes + // start=8, end=16, no room to append more + assert_eq!(buf.remaining_capacity(), 0); + buf.compact(); + // after compact, data shifted to front + assert_eq!(buf.unconsumed(), b"abcdefgh"); + assert_eq!(buf.remaining_capacity(), 8); + } + + #[test] + fn append_respects_capacity() { + let mut buf = ReadBuffer::new(8); + let n = buf.append(b"1234567890"); // 10 bytes into 8-byte buffer + assert_eq!(n, 8); + assert_eq!(buf.unconsumed(), b"12345678"); + } + + #[test] + fn slice_at_returns_correct_range() { + let mut buf = ReadBuffer::new(1024); + buf.append(b"GET /path HTTP/1.1\r\n"); + assert_eq!(buf.slice_at(4, 9), b"/path"); + } + + #[test] + fn consume_then_slice_at_uses_absolute_offsets() { + let mut buf = ReadBuffer::new(1024); + buf.append(b"GET /path\n"); + // slice_at uses offsets relative to buffer start (position 0), + // not relative to unconsumed start + assert_eq!(buf.slice_at(4, 9), b"/path"); + } + + // WriteBuffer tests + + #[test] + fn write_buffer_new() { + let buf = WriteBuffer::new(1024); + assert!(buf.is_empty()); + assert_eq!(buf.remaining_capacity(), 1024); + } + + #[test] + fn write_buffer_write_and_pending() { + let mut buf = WriteBuffer::new(1024); + let n = buf.write(b"Hello, World!"); + assert_eq!(n, 13); + assert_eq!(buf.pending(), b"Hello, World!"); + assert!(!buf.is_empty()); + } + + #[test] + fn write_buffer_advance() { + let mut buf = WriteBuffer::new(1024); + buf.write(b"Hello, World!"); + buf.advance(5); + assert_eq!(buf.pending(), b", World!"); + } + + #[test] + fn write_buffer_advance_all_resets() { + let mut buf = WriteBuffer::new(1024); + buf.write(b"Hello"); + buf.advance(5); + assert!(buf.is_empty()); + // After full advance, positions reset so we can write again + assert_eq!(buf.remaining_capacity(), 1024); + } + + #[test] + fn write_buffer_respects_capacity() { + let mut buf = WriteBuffer::new(8); + let n = buf.write(b"1234567890"); + assert_eq!(n, 8); + assert_eq!(buf.pending(), b"12345678"); + } +} diff --git a/src/net/http/codec/mod.rs b/src/net/http/codec/mod.rs new file mode 100644 index 0000000..c5ca49d --- /dev/null +++ b/src/net/http/codec/mod.rs @@ -0,0 +1,200 @@ +#[doc(hidden)] +pub mod parse; +pub(crate) mod v0_9; +pub(crate) mod v1_0; +pub(crate) mod v1_1; + +use crate::net::http::{ + error::ParseError, + request::{Request, Version}, +}; + +/// Result of attempting to decode an HTTP request from a byte buffer. +#[derive(Debug)] +pub enum DecodeResult { + /// A complete request was parsed. + Complete(Request), + /// Not enough data yet — need more bytes. + Incomplete, + /// The input is malformed. + Error(ParseError), +} + +/// Outcome of a decode attempt: the result plus how many bytes were consumed. +#[derive(Debug)] +pub struct DecodeOutcome { + pub result: DecodeResult, + pub consumed: usize, +} + +/// HTTP codec trait — interprets bytes as HTTP structures. +/// +/// The `&mut self` receiver is intentional: HTTP/0.9 is stateless, but +/// future codecs (HTTP/2 HPACK) will need mutable state. +pub(crate) trait Codec { + /// Attempt to decode a request from `buf`. + /// + /// `buf_offset` is the absolute position of `buf[0]` in the ReadBuffer, + /// used to compute absolute offsets for the Request's path fields. + fn decode(&mut self, buf: &[u8], buf_offset: usize) -> DecodeOutcome; + + /// Returns the HTTP version this codec handles. + fn version(&self) -> Version; +} + +/// Enum dispatch for HTTP codecs. Avoids dyn trait overhead. +pub(crate) enum HttpCodec { + /// Initial state: version not yet determined. Peeks at the first line. + Detecting, + Http09(v0_9::Http09Codec), + Http10(v1_0::Http10Codec), + Http11(v1_1::Http11Codec), +} + +impl HttpCodec { + pub fn decode(&mut self, buf: &[u8], buf_offset: usize) -> DecodeOutcome { + match self { + HttpCodec::Detecting => { + match parse::detect_version(buf) { + None => DecodeOutcome { + result: DecodeResult::Incomplete, + consumed: 0, + }, + Some(Err(e)) => DecodeOutcome { + result: DecodeResult::Error(e), + consumed: 0, + }, + Some(Ok(version)) => { + // Transition to the appropriate codec and re-decode. + *self = match version { + Version::Http09 => HttpCodec::Http09(v0_9::Http09Codec::new()), + Version::Http10 => HttpCodec::Http10(v1_0::Http10Codec::new()), + Version::Http11 => HttpCodec::Http11(v1_1::Http11Codec::new()), + }; + self.decode(buf, buf_offset) + } + } + } + HttpCodec::Http09(c) => c.decode(buf, buf_offset), + HttpCodec::Http10(c) => c.decode(buf, buf_offset), + HttpCodec::Http11(c) => c.decode(buf, buf_offset), + } + } + + pub fn version(&self) -> Version { + match self { + HttpCodec::Detecting => Version::Http09, + HttpCodec::Http09(c) => c.version(), + HttpCodec::Http10(c) => c.version(), + HttpCodec::Http11(c) => c.version(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn detecting_codec_http11() { + let mut codec = HttpCodec::Detecting; + let buf = b"GET /index.html HTTP/1.1\r\nHost: example.com\r\n\r\n"; + let outcome = codec.decode(buf, 0); + match outcome.result { + DecodeResult::Complete(req) => { + assert_eq!(req.version, Version::Http11); + } + other => panic!("expected Complete, got {other:?}"), + } + } + + #[test] + fn detecting_codec_http10() { + let mut codec = HttpCodec::Detecting; + let buf = b"GET /index.html HTTP/1.0\r\nHost: example.com\r\n\r\n"; + let outcome = codec.decode(buf, 0); + match outcome.result { + DecodeResult::Complete(req) => { + assert_eq!(req.version, Version::Http10); + } + other => panic!("expected Complete, got {other:?}"), + } + } + + #[test] + fn detecting_codec_http09() { + let mut codec = HttpCodec::Detecting; + let buf = b"GET /index.html\r\n"; + let outcome = codec.decode(buf, 0); + match outcome.result { + DecodeResult::Complete(req) => { + assert_eq!(req.version, Version::Http09); + } + other => panic!("expected Complete, got {other:?}"), + } + } + + #[test] + fn detecting_codec_incomplete() { + let mut codec = HttpCodec::Detecting; + // No newline yet — can't detect version. + let buf = b"GET /index.html HTTP/1.1"; + let outcome = codec.decode(buf, 0); + assert!(matches!(outcome.result, DecodeResult::Incomplete)); + assert_eq!(outcome.consumed, 0); + } + + #[test] + fn decode_after_transition_to_http11() { + // First call transitions Detecting -> Http11 and decodes. + let mut codec = HttpCodec::Detecting; + let buf = b"GET /hello HTTP/1.1\r\nHost: example.com\r\n\r\n"; + let _ = codec.decode(buf, 0); + + // Second call — codec is now Http11; should decode the same buffer again. + let outcome = codec.decode(buf, 0); + match outcome.result { + DecodeResult::Complete(req) => { + assert_eq!(req.version, Version::Http11); + } + other => panic!("expected Complete after transition, got {other:?}"), + } + } + + #[test] + fn decode_after_transition_to_http10() { + let mut codec = HttpCodec::Detecting; + let buf = b"GET /path HTTP/1.0\r\nHost: example.com\r\n\r\n"; + let _ = codec.decode(buf, 0); + + // Codec is now Http10; call decode again. + let outcome = codec.decode(buf, 0); + match outcome.result { + DecodeResult::Complete(req) => { + assert_eq!(req.version, Version::Http10); + } + other => panic!("expected Complete after transition, got {other:?}"), + } + } + + #[test] + fn version_after_transition_to_http11() { + let mut codec = HttpCodec::Detecting; + // version() before any decode returns Http09 (the Detecting fallback). + assert_eq!(codec.version(), Version::Http09); + + let buf = b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"; + let _ = codec.decode(buf, 0); + + // After transition the concrete codec's version is returned. + assert_eq!(codec.version(), Version::Http11); + } + + #[test] + fn version_after_transition_to_http10() { + let mut codec = HttpCodec::Detecting; + let buf = b"GET / HTTP/1.0\r\nHost: example.com\r\n\r\n"; + let _ = codec.decode(buf, 0); + assert_eq!(codec.version(), Version::Http10); + } +} diff --git a/src/net/http/codec/parse.rs b/src/net/http/codec/parse.rs new file mode 100644 index 0000000..abd0d7a --- /dev/null +++ b/src/net/http/codec/parse.rs @@ -0,0 +1,785 @@ +use crate::net::http::{ + error::ParseError, + request::{HeaderOffset, Method, Version}, +}; +use memchr::{memchr, memmem, memrchr}; + +// ── Private helpers ────────────────────────────────────────────────────────── + +/// Case-insensitive byte-slice comparison. +#[inline] +fn bytes_eq_ignore_case(a: &[u8], b: &[u8]) -> bool { + if a.len() != b.len() { + return false; + } + a.iter() + .zip(b.iter()) + .all(|(x, y)| x.eq_ignore_ascii_case(y)) +} + +/// Find the position of the first `\n` byte in `buf`. +#[inline] +pub fn memchr_newline(buf: &[u8]) -> Option { + memchr(b'\n', buf) +} + +// ── Public types ───────────────────────────────────────────────────────────── + +/// Parsed result of a request line. +#[derive(Debug, PartialEq, Eq)] +pub struct RequestLine { + pub method: Method, + /// Absolute offset of path start in the ReadBuffer. + pub path_start: usize, + /// Absolute offset of path end in the ReadBuffer. + pub path_end: usize, + pub version: Version, + /// How many bytes of `buf` were consumed (including the terminating `\r\n`). + pub consumed: usize, +} + +/// How the `Connection` header directs keep-alive behaviour. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ConnectionDirective { + KeepAlive, + Close, + None, +} + +// ── parse_method ───────────────────────────────────────────────────────────── + +/// Map an uppercase ASCII token to a [`Method`]. +/// +/// Only exact uppercase matches are accepted. Returns +/// [`ParseError::UnsupportedMethod`] for anything else. +pub(crate) fn parse_method(token: &[u8]) -> Result { + match token { + b"GET" => Ok(Method::Get), + b"HEAD" => Ok(Method::Head), + b"POST" => Ok(Method::Post), + b"PUT" => Ok(Method::Put), + b"DELETE" => Ok(Method::Delete), + b"OPTIONS" => Ok(Method::Options), + b"TRACE" => Ok(Method::Trace), + b"CONNECT" => Ok(Method::Connect), + b"PATCH" => Ok(Method::Patch), + _ => Err(ParseError::UnsupportedMethod), + } +} + +// ── parse_request_line ─────────────────────────────────────────────────────── + +/// Parse `METHOD /path HTTP/1.x\r\n` from `buf`. +/// +/// Returns: +/// - `None` — incomplete (no `\r\n` yet) +/// - `Some(Ok(…))` — successfully parsed +/// - `Some(Err(…))` — malformed line +/// +/// `buf_offset` is the absolute position of `buf[0]` inside the connection's +/// ReadBuffer; it is added to all path offsets so that callers always receive +/// absolute offsets. +/// +/// Uses `rposition` on the first-line slice for the second space so that paths +/// that themselves contain spaces are handled correctly. +pub fn parse_request_line( + buf: &[u8], + buf_offset: usize, +) -> Option> { + // Need a complete line first. + let newline_pos = memchr_newline(buf)?; + + let line_end = if newline_pos > 0 && buf[newline_pos - 1] == b'\r' { + newline_pos - 1 + } else { + newline_pos + }; + let consumed = newline_pos + 1; + + let line = &buf[..line_end]; + + // Empty line + if line.is_empty() { + return Some(Err(ParseError::InvalidRequestLine)); + } + + // First space separates method from the rest. + let first_space = match memchr(b' ', line) { + Some(p) => p, + None => return Some(Err(ParseError::InvalidRequestLine)), + }; + + let method_bytes = &line[..first_space]; + let method = match parse_method(method_bytes) { + Ok(m) => m, + Err(e) => return Some(Err(e)), + }; + + // Last space separates path from version token. + let after_method = &line[first_space + 1..]; + let last_space = match memrchr(b' ', after_method) { + Some(p) => p, + None => return Some(Err(ParseError::InvalidRequestLine)), + }; + + let path_slice = &after_method[..last_space]; + let version_token = &after_method[last_space + 1..]; + + // Path must not be empty. + if path_slice.is_empty() { + return Some(Err(ParseError::InvalidRequestLine)); + } + + let version = match version_token { + b"HTTP/1.0" => Version::Http10, + b"HTTP/1.1" => Version::Http11, + other if other.starts_with(b"HTTP/") => return Some(Err(ParseError::UnsupportedVersion)), + _ => return Some(Err(ParseError::InvalidRequestLine)), + }; + + // Compute absolute offsets: buf_offset + local position inside buf. + // `path_slice` starts at buf[first_space + 1]. + let path_start = buf_offset + first_space + 1; + let path_end = buf_offset + first_space + 1 + last_space; + + Some(Ok(RequestLine { + method, + path_start, + path_end, + version, + consumed, + })) +} + +// ── parse_headers ───────────────────────────────────────────────────────────── + +const MAX_HEADERS: usize = 64; + +/// Parse the header block that starts at the beginning of `buf`. +/// +/// Returns: +/// - `None` — incomplete (no `\r\n\r\n` terminator yet) +/// - `Some(Ok((headers, n)))` — `n` bytes consumed, offsets are LOCAL (0-based into `buf`) +/// - `Some(Err(…))` — malformed +/// +/// Offsets are **local** (0-based into `buf`). Call [`absolutize_headers`] to +/// convert them to absolute ReadBuffer positions before storing in a +/// [`Request`]. +pub fn parse_headers(buf: &[u8]) -> Option, usize), ParseError>> { + // We need \r\n\r\n (or \n\n) to know we have a complete header block. + let (terminator_pos, terminator_len) = find_header_terminator(buf)?; + let consumed = terminator_pos + terminator_len; + + // The scan region includes everything up to (but not including) the + // blank-line terminator. Individual header lines each end with \r\n or + // \n, which are present in this slice (the terminator starts at the + // *second* \r\n pair, so the first \r\n of \r\n\r\n is part of the last + // header line). + // + // Example layout for "Host: foo\r\nBar: baz\r\n\r\n": + // pos 0..9 -> "Host: foo" + // pos 9..11 -> "\r\n" <- end of Host header line + // pos 11..19 -> "Bar: baz" + // pos 19..21 -> "\r\n" <- end of Bar header line + // pos 21..25 -> "\r\n\r\n" <- terminator (terminator_pos = 21) + // + // So scan_buf = &buf[..21], which includes the \r\n at the end of each + // header. + let scan_buf = &buf[..terminator_pos]; + + let mut headers = Vec::new(); + let mut pos = 0; + + while pos < scan_buf.len() { + // Find the \n that ends this header line. + let rel_nl = match memchr_newline(&scan_buf[pos..]) { + Some(p) => p, + // No \n found: the remaining bytes are the last header line with no + // trailing \r\n (bare last line before the \r\n\r\n terminator). + // We treat scan_buf[pos..] as a header line. + None => { + let line = &scan_buf[pos..]; + if line.is_empty() { + break; + } + if let Some(e) = parse_header_line(line, pos, &mut headers) { + return Some(Err(e)); + } + break; + } + }; + + let line_end_nl = pos + rel_nl; // position of \n in scan_buf + // Strip optional preceding \r. + let raw_line_end = if line_end_nl > pos && scan_buf[line_end_nl - 1] == b'\r' { + line_end_nl - 1 + } else { + line_end_nl + }; + + let line = &scan_buf[pos..raw_line_end]; + + // Empty line: skip (shouldn't normally occur before the terminator). + if line.is_empty() { + pos = line_end_nl + 1; + continue; + } + + if let Some(e) = parse_header_line(line, pos, &mut headers) { + return Some(Err(e)); + } + + pos = line_end_nl + 1; + } + + Some(Ok((headers, consumed))) +} + +/// Parse a single header line (without terminating `\r\n`) and push the +/// resulting [`HeaderOffset`] onto `headers`. +/// +/// `line_start` is the offset of `line[0]` within the original `buf`. +/// +/// Returns `Some(ParseError)` on error, `None` on success. +fn parse_header_line( + line: &[u8], + line_start: usize, + headers: &mut Vec, +) -> Option { + let colon = match memchr(b':', line) { + Some(c) => c, + None => return Some(ParseError::InvalidHeader), + }; + + let name_start = line_start; + let name_end = line_start + colon; + + // Skip the colon and any leading OWS from the value. + let value_raw = &line[colon + 1..]; + let value_trimmed_start = value_raw + .iter() + .position(|&b| b != b' ' && b != b'\t') + .unwrap_or(value_raw.len()); + let value_trimmed_end = value_raw + .iter() + .rposition(|&b| b != b' ' && b != b'\t') + .map(|p| p + 1) + .unwrap_or(0); + + let value_start = line_start + colon + 1 + value_trimmed_start; + let value_end = line_start + colon + 1 + value_trimmed_end; + + if headers.len() >= MAX_HEADERS { + return Some(ParseError::TooManyHeaders); + } + + headers.push(HeaderOffset { + name_start, + name_end, + value_start, + value_end, + }); + None +} + +/// Locate the blank-line terminator for the header block. +/// +/// Looks for `\r\n\r\n` first; falls back to `\n\n`. +/// +/// Returns `(start_of_terminator, terminator_byte_length)`. +fn find_header_terminator(buf: &[u8]) -> Option<(usize, usize)> { + if let Some(pos) = memmem::find(buf, b"\r\n\r\n") { + return Some((pos, 4)); + } + if let Some(pos) = memmem::find(buf, b"\n\n") { + return Some((pos, 2)); + } + None +} + +// ── find_content_length ─────────────────────────────────────────────────────── + +/// Case-insensitive lookup for `Content-Length` in `headers`. +pub(crate) fn find_content_length( + headers: &[HeaderOffset], + buf: &[u8], +) -> Result, ParseError> { + match header_value_for(headers, buf, b"content-length") { + None => Ok(None), + Some(val) => { + let s = core::str::from_utf8(val).map_err(|_| ParseError::InvalidContentLength)?; + s.trim() + .parse::() + .map(Some) + .map_err(|_| ParseError::InvalidContentLength) + } + } +} + +// ── has_header ──────────────────────────────────────────────────────────────── + +/// Return `true` if any header in `headers` has the given name (case-insensitive). +pub(crate) fn has_header(headers: &[HeaderOffset], buf: &[u8], name: &[u8]) -> bool { + headers + .iter() + .any(|h| bytes_eq_ignore_case(&buf[h.name_start..h.name_end], name)) +} + +// ── header_value_for ───────────────────────────────────────────────────────── + +/// Return the value bytes for the first header with the given name +/// (case-insensitive), or `None`. +pub(crate) fn header_value_for<'a>( + headers: &[HeaderOffset], + buf: &'a [u8], + name: &[u8], +) -> Option<&'a [u8]> { + headers.iter().find_map(|h| { + if bytes_eq_ignore_case(&buf[h.name_start..h.name_end], name) { + Some(&buf[h.value_start..h.value_end]) + } else { + None + } + }) +} + +// ── detect_version ──────────────────────────────────────────────────────────── + +/// Non-consuming peek at the first request line to determine the HTTP version. +/// +/// Returns: +/// - `None` — incomplete (no newline yet) +/// - `Some(Ok(…))` — detected version +/// - `Some(Err(…))` — version token starts with `HTTP/` but is not +/// `HTTP/1.0` or `HTTP/1.1` +/// +/// If no version token is present (HTTP/0.9 simple request), returns +/// `Some(Ok(Version::Http09))`. +pub(crate) fn detect_version(buf: &[u8]) -> Option> { + let newline_pos = memchr_newline(buf)?; + + let line_end = if newline_pos > 0 && buf[newline_pos - 1] == b'\r' { + newline_pos - 1 + } else { + newline_pos + }; + + let line = &buf[..line_end]; + + // Look for the last space on the line. + match memrchr(b' ', line) { + None => { + // No space at all — HTTP/0.9 bare request (e.g. "GET /path"). + // (Actually HTTP/0.9 has one space between method and path, but no + // version token. We'll treat "no last-space-followed-by-HTTP" as 0.9.) + Some(Ok(Version::Http09)) + } + Some(last_space) => { + let token = &line[last_space + 1..]; + match token { + b"HTTP/1.0" => Some(Ok(Version::Http10)), + b"HTTP/1.1" => Some(Ok(Version::Http11)), + other if other.starts_with(b"HTTP/") => Some(Err(ParseError::UnsupportedVersion)), + _ => { + // Token after last space is not an HTTP version string — treat + // the whole line as HTTP/0.9 (path might contain spaces). + Some(Ok(Version::Http09)) + } + } + } + } +} + +// ── absolutize_headers ──────────────────────────────────────────────────────── + +/// Add `base` to every offset field in each [`HeaderOffset`]. +/// +/// Used by codecs to convert local (parse-buffer-relative) offsets returned by +/// [`parse_headers`] into absolute ReadBuffer offsets before constructing a +/// [`Request`]. +pub(crate) fn absolutize_headers(headers: &[HeaderOffset], base: usize) -> Vec { + headers + .iter() + .map(|h| HeaderOffset { + name_start: h.name_start + base, + name_end: h.name_end + base, + value_start: h.value_start + base, + value_end: h.value_end + base, + }) + .collect() +} + +// ── detect_connection_directive ─────────────────────────────────────────────── + +/// Inspect the `Connection` header and return the appropriate directive. +pub(crate) fn detect_connection_directive( + headers: &[HeaderOffset], + buf: &[u8], +) -> ConnectionDirective { + match header_value_for(headers, buf, b"connection") { + None => ConnectionDirective::None, + Some(val) => { + if bytes_eq_ignore_case(val, b"keep-alive") { + ConnectionDirective::KeepAlive + } else if bytes_eq_ignore_case(val, b"close") { + ConnectionDirective::Close + } else { + ConnectionDirective::None + } + } + } +} + +// ── Tests ───────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + + // ── parse_method ───────────────────────────────────────────────────────── + + #[test] + fn parse_method_all() { + assert_eq!(parse_method(b"GET"), Ok(Method::Get)); + assert_eq!(parse_method(b"HEAD"), Ok(Method::Head)); + assert_eq!(parse_method(b"POST"), Ok(Method::Post)); + assert_eq!(parse_method(b"PUT"), Ok(Method::Put)); + assert_eq!(parse_method(b"DELETE"), Ok(Method::Delete)); + assert_eq!(parse_method(b"OPTIONS"), Ok(Method::Options)); + assert_eq!(parse_method(b"TRACE"), Ok(Method::Trace)); + assert_eq!(parse_method(b"CONNECT"), Ok(Method::Connect)); + assert_eq!(parse_method(b"PATCH"), Ok(Method::Patch)); + } + + #[test] + fn parse_method_unknown() { + assert_eq!(parse_method(b"FROBBLE"), Err(ParseError::UnsupportedMethod)); + assert_eq!(parse_method(b""), Err(ParseError::UnsupportedMethod)); + } + + #[test] + fn parse_method_lowercase_rejected() { + assert_eq!(parse_method(b"get"), Err(ParseError::UnsupportedMethod)); + assert_eq!(parse_method(b"post"), Err(ParseError::UnsupportedMethod)); + } + + // ── parse_request_line ─────────────────────────────────────────────────── + + #[test] + fn parse_request_line_http11() { + let buf = b"GET /index.html HTTP/1.1\r\n"; + let rl = parse_request_line(buf, 0).unwrap().unwrap(); + assert_eq!(rl.method, Method::Get); + assert_eq!(&buf[rl.path_start..rl.path_end], b"/index.html"); + assert_eq!(rl.version, Version::Http11); + assert_eq!(rl.consumed, buf.len()); + } + + #[test] + fn parse_request_line_http10() { + let buf = b"POST /data HTTP/1.0\r\n"; + let rl = parse_request_line(buf, 0).unwrap().unwrap(); + assert_eq!(rl.method, Method::Post); + assert_eq!(&buf[rl.path_start..rl.path_end], b"/data"); + assert_eq!(rl.version, Version::Http10); + assert_eq!(rl.consumed, buf.len()); + } + + #[test] + fn parse_request_line_with_offset() { + // Simulate the request line starting 20 bytes into the ReadBuffer. + let buf = b"GET /hello HTTP/1.1\r\n"; + let rl = parse_request_line(buf, 20).unwrap().unwrap(); + // "GET " is 4 bytes, so path_start = 20 + 4 = 24 + assert_eq!(rl.path_start, 24); + // "/hello" is 6 bytes, so path_end = 24 + 6 = 30 + assert_eq!(rl.path_end, 30); + } + + #[test] + fn parse_request_line_incomplete() { + let buf = b"GET /index.html HTTP/1.1"; + assert!(parse_request_line(buf, 0).is_none()); + } + + #[test] + fn parse_request_line_unsupported_version() { + let buf = b"GET / HTTP/2.0\r\n"; + assert_eq!( + parse_request_line(buf, 0).unwrap(), + Err(ParseError::UnsupportedVersion) + ); + } + + #[test] + fn parse_request_line_missing_path() { + // No second space → can't split path from version + let buf = b"GET HTTP/1.1\r\n"; + let result = parse_request_line(buf, 0).unwrap(); + // "HTTP/1.1" becomes both the "path" and "version" via rposition, but + // there's no second space after the first, so last_space = 0 inside + // after_method → path is empty. + assert!(result.is_err()); + } + + // ── parse_headers ──────────────────────────────────────────────────────── + + #[test] + fn parse_headers_simple() { + let buf = b"Host: example.com\r\nContent-Length: 5\r\n\r\n"; + let (headers, consumed) = parse_headers(buf).unwrap().unwrap(); + assert_eq!(headers.len(), 2); + assert_eq!(consumed, buf.len()); + + // Verify Host header name and value bytes. + assert_eq!(&buf[headers[0].name_start..headers[0].name_end], b"Host"); + assert_eq!( + &buf[headers[0].value_start..headers[0].value_end], + b"example.com" + ); + } + + #[test] + fn parse_headers_offsets_are_local() { + // When called with a sub-buffer, offsets must be 0-based into that buffer. + let buf = b"X-Foo: bar\r\n\r\n"; + let (headers, _) = parse_headers(buf).unwrap().unwrap(); + assert_eq!(headers[0].name_start, 0); + assert_eq!(headers[0].name_end, 5); + assert_eq!(&buf[headers[0].value_start..headers[0].value_end], b"bar"); + } + + #[test] + fn parse_headers_empty() { + // An empty header block: just \r\n\r\n + let buf = b"\r\n\r\n"; + let (headers, consumed) = parse_headers(buf).unwrap().unwrap(); + assert!(headers.is_empty()); + assert_eq!(consumed, 4); + } + + #[test] + fn parse_headers_value_whitespace_trimmed() { + let buf = b"X-Spaces: hello world \r\n\r\n"; + let (headers, _) = parse_headers(buf).unwrap().unwrap(); + assert_eq!( + &buf[headers[0].value_start..headers[0].value_end], + b"hello world" + ); + } + + #[test] + fn parse_headers_incomplete_no_terminator() { + let buf = b"Host: example.com\r\n"; + assert!(parse_headers(buf).is_none()); + } + + #[test] + fn parse_headers_incomplete_partial_line() { + let buf = b"Host: ex"; + assert!(parse_headers(buf).is_none()); + } + + #[test] + fn parse_headers_malformed_no_colon() { + let buf = b"InvalidHeaderLine\r\n\r\n"; + assert_eq!(parse_headers(buf).unwrap(), Err(ParseError::InvalidHeader)); + } + + #[test] + fn parse_headers_too_many() { + // Build 65 headers to exceed the limit. + let mut buf = String::new(); + for i in 0..65 { + buf.push_str(&format!("X-Header-{i}: value\r\n")); + } + buf.push_str("\r\n"); + assert_eq!( + parse_headers(buf.as_bytes()).unwrap(), + Err(ParseError::TooManyHeaders) + ); + } + + // ── find_content_length ────────────────────────────────────────────────── + + #[test] + fn find_content_length_present() { + let buf = b"Content-Length: 42\r\n\r\n"; + let (headers, _) = parse_headers(buf).unwrap().unwrap(); + assert_eq!(find_content_length(&headers, buf), Ok(Some(42))); + } + + #[test] + fn find_content_length_absent() { + let buf = b"Host: example.com\r\n\r\n"; + let (headers, _) = parse_headers(buf).unwrap().unwrap(); + assert_eq!(find_content_length(&headers, buf), Ok(None)); + } + + #[test] + fn find_content_length_invalid() { + let buf = b"Content-Length: abc\r\n\r\n"; + let (headers, _) = parse_headers(buf).unwrap().unwrap(); + assert_eq!( + find_content_length(&headers, buf), + Err(ParseError::InvalidContentLength) + ); + } + + #[test] + fn find_content_length_case_insensitive() { + let buf = b"content-length: 7\r\n\r\n"; + let (headers, _) = parse_headers(buf).unwrap().unwrap(); + assert_eq!(find_content_length(&headers, buf), Ok(Some(7))); + } + + // ── has_header ─────────────────────────────────────────────────────────── + + #[test] + fn has_header_case_insensitive() { + let buf = b"Host: example.com\r\n\r\n"; + let (headers, _) = parse_headers(buf).unwrap().unwrap(); + assert!(has_header(&headers, buf, b"host")); + assert!(has_header(&headers, buf, b"HOST")); + assert!(has_header(&headers, buf, b"Host")); + assert!(!has_header(&headers, buf, b"Content-Type")); + } + + // ── header_value_for ───────────────────────────────────────────────────── + + #[test] + fn header_value_for_returns_value() { + let buf = b"X-Custom: hello\r\n\r\n"; + let (headers, _) = parse_headers(buf).unwrap().unwrap(); + assert_eq!( + header_value_for(&headers, buf, b"x-custom"), + Some(b"hello".as_ref()) + ); + assert_eq!( + header_value_for(&headers, buf, b"X-CUSTOM"), + Some(b"hello".as_ref()) + ); + assert_eq!(header_value_for(&headers, buf, b"missing"), None); + } + + // ── detect_version ─────────────────────────────────────────────────────── + + #[test] + fn detect_version_http11() { + assert_eq!( + detect_version(b"GET / HTTP/1.1\r\n"), + Some(Ok(Version::Http11)) + ); + } + + #[test] + fn detect_version_http10() { + assert_eq!( + detect_version(b"GET / HTTP/1.0\r\n"), + Some(Ok(Version::Http10)) + ); + } + + #[test] + fn detect_version_http09() { + // HTTP/0.9 simple request — no version token after the last space. + assert_eq!( + detect_version(b"GET /index.html\r\n"), + Some(Ok(Version::Http09)) + ); + } + + #[test] + fn detect_version_incomplete() { + assert_eq!(detect_version(b"GET / HTTP/1.1"), None); + } + + #[test] + fn detect_version_unsupported() { + assert_eq!( + detect_version(b"GET / HTTP/2.0\r\n"), + Some(Err(ParseError::UnsupportedVersion)) + ); + } + + // ── absolutize_headers ─────────────────────────────────────────────────── + + #[test] + fn absolutize_headers_adds_base_correctly() { + let local = vec![HeaderOffset { + name_start: 0, + name_end: 4, + value_start: 6, + value_end: 11, + }]; + let abs = absolutize_headers(&local, 100); + assert_eq!(abs[0].name_start, 100); + assert_eq!(abs[0].name_end, 104); + assert_eq!(abs[0].value_start, 106); + assert_eq!(abs[0].value_end, 111); + } + + #[test] + fn absolutize_headers_base_zero_is_identity() { + let local = vec![HeaderOffset { + name_start: 3, + name_end: 7, + value_start: 9, + value_end: 14, + }]; + let abs = absolutize_headers(&local, 0); + assert_eq!(abs[0], local[0]); + } + + // ── detect_connection_directive ────────────────────────────────────────── + + #[test] + fn detect_connection_directive_keep_alive() { + let buf = b"Connection: keep-alive\r\n\r\n"; + let (headers, _) = parse_headers(buf).unwrap().unwrap(); + assert_eq!( + detect_connection_directive(&headers, buf), + ConnectionDirective::KeepAlive + ); + } + + #[test] + fn detect_connection_directive_close() { + let buf = b"Connection: close\r\n\r\n"; + let (headers, _) = parse_headers(buf).unwrap().unwrap(); + assert_eq!( + detect_connection_directive(&headers, buf), + ConnectionDirective::Close + ); + } + + #[test] + fn detect_connection_directive_absent() { + let buf = b"Host: example.com\r\n\r\n"; + let (headers, _) = parse_headers(buf).unwrap().unwrap(); + assert_eq!( + detect_connection_directive(&headers, buf), + ConnectionDirective::None + ); + } + + #[test] + fn detect_connection_directive_unknown_value() { + let buf = b"Connection: upgrade\r\n\r\n"; + let (headers, _) = parse_headers(buf).unwrap().unwrap(); + assert_eq!( + detect_connection_directive(&headers, buf), + ConnectionDirective::None + ); + } + + #[test] + fn detect_connection_directive_case_insensitive_value() { + let buf = b"connection: Keep-Alive\r\n\r\n"; + let (headers, _) = parse_headers(buf).unwrap().unwrap(); + assert_eq!( + detect_connection_directive(&headers, buf), + ConnectionDirective::KeepAlive + ); + } +} diff --git a/src/net/http/codec/v0_9.rs b/src/net/http/codec/v0_9.rs new file mode 100644 index 0000000..cc5be61 --- /dev/null +++ b/src/net/http/codec/v0_9.rs @@ -0,0 +1,284 @@ +use super::parse::ConnectionDirective; +use crate::net::http::{ + error::ParseError, + request::{BodyFraming, Method, Request, Version}, +}; + +use super::{Codec, DecodeOutcome, DecodeResult}; + +/// HTTP/0.9 codec. +/// +/// Parses the simple HTTP/0.9 request format: `GET \r\n` +/// (also accepts bare `\n` for historical compatibility). +/// +/// Responses have no status line or headers — just raw body bytes. +pub struct Http09Codec; + +impl Http09Codec { + pub fn new() -> Self { + Self + } +} + +impl Codec for Http09Codec { + fn decode(&mut self, buf: &[u8], buf_offset: usize) -> DecodeOutcome { + // Find the end of the request line (\n or \r\n) + let newline_pos = match memchr_newline(buf) { + Some(pos) => pos, + None => { + return DecodeOutcome { + result: DecodeResult::Incomplete, + consumed: 0, + }; + } + }; + + // Determine the line content (strip \r\n or \n) + let line_end = if newline_pos > 0 && buf[newline_pos - 1] == b'\r' { + newline_pos - 1 + } else { + newline_pos + }; + + let line = &buf[..line_end]; + let consumed = newline_pos + 1; // consume through the \n + + // Empty line + if line.is_empty() { + return DecodeOutcome { + result: DecodeResult::Error(ParseError::InvalidRequestLine), + consumed, + }; + } + + // Find the space separating method from path + let space_pos = match memchr(b' ', line) { + Some(pos) => pos, + None => { + // No space — could be just "GET" with no path + return DecodeOutcome { + result: DecodeResult::Error(ParseError::InvalidRequestLine), + consumed, + }; + } + }; + + let method_bytes = &line[..space_pos]; + let path = &line[space_pos + 1..]; + + // Validate method + if method_bytes != b"GET" { + return DecodeOutcome { + result: DecodeResult::Error(ParseError::UnsupportedMethod), + consumed, + }; + } + + // Path must not be empty + if path.is_empty() { + return DecodeOutcome { + result: DecodeResult::Error(ParseError::InvalidRequestLine), + consumed, + }; + } + + // Compute absolute offsets into the ReadBuffer + let path_start = buf_offset + space_pos + 1; + let path_end = buf_offset + line_end; + + DecodeOutcome { + result: DecodeResult::Complete(Request::new( + Method::Get, + path_start, + path_end, + Version::Http09, + Vec::new(), + BodyFraming::None, + false, + ConnectionDirective::None, + )), + consumed, + } + } + + fn version(&self) -> Version { + Version::Http09 + } +} + +use super::parse::memchr_newline; +use memchr::memchr; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn decode_simple_get() { + let mut codec = Http09Codec::new(); + let buf = b"GET /index.html\r\n"; + let outcome = codec.decode(buf, 0); + assert_eq!(outcome.consumed, 17); + match outcome.result { + DecodeResult::Complete(req) => { + assert_eq!(req.method, Method::Get); + assert_eq!(req.path_from_buf(buf), b"/index.html"); + assert_eq!(req.version, Version::Http09); + } + other => panic!("expected Complete, got {other:?}"), + } + } + + #[test] + fn decode_bare_newline() { + let mut codec = Http09Codec::new(); + let buf = b"GET /path\n"; + let outcome = codec.decode(buf, 0); + assert_eq!(outcome.consumed, 10); + match outcome.result { + DecodeResult::Complete(req) => { + assert_eq!(req.path_from_buf(buf), b"/path"); + } + other => panic!("expected Complete, got {other:?}"), + } + } + + #[test] + fn decode_root_path() { + let mut codec = Http09Codec::new(); + let buf = b"GET /\r\n"; + let outcome = codec.decode(buf, 0); + assert_eq!(outcome.consumed, 7); + match outcome.result { + DecodeResult::Complete(req) => { + assert_eq!(req.path_from_buf(buf), b"/"); + } + other => panic!("expected Complete, got {other:?}"), + } + } + + #[test] + fn decode_incomplete() { + let mut codec = Http09Codec::new(); + let buf = b"GET /index"; + let outcome = codec.decode(buf, 0); + assert_eq!(outcome.consumed, 0); + assert!(matches!(outcome.result, DecodeResult::Incomplete)); + } + + #[test] + fn decode_empty() { + let mut codec = Http09Codec::new(); + let buf = b""; + let outcome = codec.decode(buf, 0); + assert_eq!(outcome.consumed, 0); + assert!(matches!(outcome.result, DecodeResult::Incomplete)); + } + + #[test] + fn decode_unsupported_method() { + let mut codec = Http09Codec::new(); + let buf = b"POST /data\r\n"; + let outcome = codec.decode(buf, 0); + match outcome.result { + DecodeResult::Error(ParseError::UnsupportedMethod) => {} + other => panic!("expected UnsupportedMethod, got {other:?}"), + } + } + + #[test] + fn decode_missing_path() { + let mut codec = Http09Codec::new(); + let buf = b"GET\r\n"; + let outcome = codec.decode(buf, 0); + match outcome.result { + DecodeResult::Error(ParseError::InvalidRequestLine) => {} + other => panic!("expected InvalidRequestLine, got {other:?}"), + } + } + + #[test] + fn decode_empty_line() { + let mut codec = Http09Codec::new(); + let buf = b"\r\n"; + let outcome = codec.decode(buf, 0); + match outcome.result { + DecodeResult::Error(ParseError::InvalidRequestLine) => {} + other => panic!("expected InvalidRequestLine, got {other:?}"), + } + } + + #[test] + fn decode_with_offset() { + let mut codec = Http09Codec::new(); + // Simulate buffer where unconsumed starts at offset 10 + let buf = b"GET /test\r\n"; + let outcome = codec.decode(buf, 10); + assert_eq!(outcome.consumed, 11); + match outcome.result { + DecodeResult::Complete(req) => { + // path offsets should be absolute (buf_offset + local position) + assert_eq!(req.path_start(), 14); // 10 + 4 + assert_eq!(req.path_end(), 19); // 10 + 9 + } + other => panic!("expected Complete, got {other:?}"), + } + } + + #[test] + fn decode_path_with_query_string() { + let mut codec = Http09Codec::new(); + let buf = b"GET /search?q=hello&lang=en\r\n"; + let outcome = codec.decode(buf, 0); + match outcome.result { + DecodeResult::Complete(req) => { + assert_eq!(req.method, Method::Get); + assert_eq!(req.path_from_buf(buf), b"/search?q=hello&lang=en"); + } + other => panic!("expected Complete, got {other:?}"), + } + } + + #[test] + fn decode_path_with_fragment() { + let mut codec = Http09Codec::new(); + let buf = b"GET /page#section\r\n"; + let outcome = codec.decode(buf, 0); + match outcome.result { + DecodeResult::Complete(req) => { + assert_eq!(req.path_from_buf(buf), b"/page#section"); + } + other => panic!("expected Complete, got {other:?}"), + } + } + + #[test] + fn decode_path_with_encoded_chars() { + let mut codec = Http09Codec::new(); + let buf = b"GET /path%20with%20spaces\r\n"; + let outcome = codec.decode(buf, 0); + match outcome.result { + DecodeResult::Complete(req) => { + assert_eq!(req.path_from_buf(buf), b"/path%20with%20spaces"); + } + other => panic!("expected Complete, got {other:?}"), + } + } + + #[test] + fn decode_empty_path_after_space() { + let mut codec = Http09Codec::new(); + let buf = b"GET \r\n"; + let outcome = codec.decode(buf, 0); + match outcome.result { + DecodeResult::Error(ParseError::InvalidRequestLine) => {} + other => panic!("expected InvalidRequestLine for empty path, got {other:?}"), + } + } + + #[test] + fn version_returns_http09() { + let codec = Http09Codec::new(); + assert_eq!(codec.version(), Version::Http09); + } +} diff --git a/src/net/http/codec/v1_0.rs b/src/net/http/codec/v1_0.rs new file mode 100644 index 0000000..c364e9c --- /dev/null +++ b/src/net/http/codec/v1_0.rs @@ -0,0 +1,258 @@ +use super::{Codec, DecodeOutcome, DecodeResult, parse}; +use crate::net::http::request::{BodyFraming, Request, Version}; + +/// HTTP/1.0 codec. +/// +/// Parses full HTTP/1.0 requests: request line + headers + optional body. +/// Uses the shared parsing layer in `parse.rs`. +pub struct Http10Codec; + +impl Http10Codec { + pub fn new() -> Self { + Self + } +} + +impl Codec for Http10Codec { + fn decode(&mut self, buf: &[u8], buf_offset: usize) -> DecodeOutcome { + // Step 1: Parse the request line. + let req_line = match parse::parse_request_line(buf, buf_offset) { + None => { + return DecodeOutcome { + result: DecodeResult::Incomplete, + consumed: 0, + }; + } + Some(Err(e)) => { + return DecodeOutcome { + result: DecodeResult::Error(e), + consumed: 0, + }; + } + Some(Ok(rl)) => rl, + }; + + // Step 2: Parse headers from the buffer immediately after the request line. + // parse_headers takes only the sub-buffer; offsets are LOCAL (0-based into header_buf). + let header_buf = &buf[req_line.consumed..]; + let (local_headers, headers_consumed) = match parse::parse_headers(header_buf) { + None => { + return DecodeOutcome { + result: DecodeResult::Incomplete, + consumed: 0, + }; + } + Some(Err(e)) => { + return DecodeOutcome { + result: DecodeResult::Error(e), + consumed: 0, + }; + } + Some(Ok((headers, consumed))) => (headers, consumed), + }; + + // Step 3: Determine body framing via Content-Length (local headers + local buf). + let body_framing = match parse::find_content_length(&local_headers, header_buf) { + Err(e) => { + return DecodeOutcome { + result: DecodeResult::Error(e), + consumed: 0, + }; + } + Ok(Some(len)) => BodyFraming::ContentLength(len), + Ok(None) => BodyFraming::None, + }; + + // Step 4: Absolutize header offsets. + let abs_base = buf_offset + req_line.consumed; + let abs_headers = parse::absolutize_headers(&local_headers, abs_base); + + let consumed = req_line.consumed + headers_consumed; + + // Step 5: Detect connection directive. + let connection_directive = parse::detect_connection_directive(&local_headers, header_buf); + + DecodeOutcome { + result: DecodeResult::Complete(Request::new( + req_line.method, + req_line.path_start, + req_line.path_end, + Version::Http10, + abs_headers, + body_framing, + false, + connection_directive, + )), + consumed, + } + } + + fn version(&self) -> Version { + Version::Http10 + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::net::http::error::ParseError; + use crate::net::http::request::{BodyFraming, Method, Version}; + + #[test] + fn decode_simple_get() { + let mut codec = Http10Codec::new(); + let buf = b"GET /index.html HTTP/1.0\r\nHost: example.com\r\n\r\n"; + let outcome = codec.decode(buf, 0); + match outcome.result { + DecodeResult::Complete(req) => { + assert_eq!(req.method, Method::Get); + assert_eq!(req.version, Version::Http10); + assert_eq!(req.path_from_buf(buf), b"/index.html"); + assert_eq!(req.body_framing, BodyFraming::None); + } + other => panic!("expected Complete, got {other:?}"), + } + } + + #[test] + fn decode_post_with_content_length() { + let mut codec = Http10Codec::new(); + let buf = b"POST /submit HTTP/1.0\r\nContent-Length: 13\r\n\r\n"; + let outcome = codec.decode(buf, 0); + match outcome.result { + DecodeResult::Complete(req) => { + assert_eq!(req.method, Method::Post); + assert_eq!(req.body_framing, BodyFraming::ContentLength(13)); + } + other => panic!("expected Complete, got {other:?}"), + } + } + + #[test] + fn decode_incomplete_request_line() { + let mut codec = Http10Codec::new(); + let buf = b"GET /index.html HTTP/1.0"; + let outcome = codec.decode(buf, 0); + assert!(matches!(outcome.result, DecodeResult::Incomplete)); + assert_eq!(outcome.consumed, 0); + } + + #[test] + fn decode_incomplete_headers() { + let mut codec = Http10Codec::new(); + let buf = b"GET /index.html HTTP/1.0\r\nHost: example.com\r\n"; + let outcome = codec.decode(buf, 0); + assert!(matches!(outcome.result, DecodeResult::Incomplete)); + assert_eq!(outcome.consumed, 0); + } + + #[test] + fn decode_with_offset() { + let mut codec = Http10Codec::new(); + // Simulate the request starting at absolute offset 100. + // The header block needs a proper \r\n\r\n terminator after the request line. + let buf = b"GET /hello HTTP/1.0\r\nHost: x.com\r\n\r\n"; + let outcome = codec.decode(buf, 100); + match outcome.result { + DecodeResult::Complete(req) => { + // "GET " is 4 bytes, so path_start = 100 + 4 = 104 + assert_eq!(req.path_start(), 104); + // "/hello" is 6 bytes, so path_end = 104 + 6 = 110 + assert_eq!(req.path_end(), 110); + } + other => panic!("expected Complete, got {other:?}"), + } + } + + #[test] + fn decode_invalid_content_length() { + let mut codec = Http10Codec::new(); + let buf = b"POST /data HTTP/1.0\r\nContent-Length: abc\r\n\r\n"; + let outcome = codec.decode(buf, 0); + match outcome.result { + DecodeResult::Error(ParseError::InvalidContentLength) => {} + other => panic!("expected InvalidContentLength error, got {other:?}"), + } + } + + #[test] + fn version_returns_http10() { + let codec = Http10Codec::new(); + assert_eq!(codec.version(), Version::Http10); + } + + #[test] + fn decode_post_missing_content_length_uses_none_framing() { + // HTTP/1.0 POST without Content-Length: connection-close semantics, + // body framing defaults to None (not an error). + let mut codec = Http10Codec::new(); + let buf = b"POST /submit HTTP/1.0\r\nHost: example.com\r\n\r\n"; + let outcome = codec.decode(buf, 0); + match outcome.result { + DecodeResult::Complete(req) => { + assert_eq!(req.method, Method::Post); + assert_eq!(req.body_framing, BodyFraming::None); + } + other => panic!("expected Complete, got {other:?}"), + } + } + + #[test] + fn decode_head_request_http10() { + let mut codec = Http10Codec::new(); + let buf = b"HEAD /status HTTP/1.0\r\nHost: example.com\r\n\r\n"; + let outcome = codec.decode(buf, 0); + match outcome.result { + DecodeResult::Complete(req) => { + assert_eq!(req.method, Method::Head); + assert_eq!(req.body_framing, BodyFraming::None); + } + other => panic!("expected Complete, got {other:?}"), + } + } + + #[test] + fn decode_content_length_whitespace_in_value() { + // Content-Length with leading/trailing whitespace — the parser should handle + // or reject it. This tests the actual behavior. + let mut codec = Http10Codec::new(); + let buf = b"POST /data HTTP/1.0\r\nContent-Length: 42 \r\n\r\n"; + let outcome = codec.decode(buf, 0); + match outcome.result { + DecodeResult::Complete(req) => { + // If the parser trims whitespace, it should parse to 42. + assert_eq!(req.body_framing, BodyFraming::ContentLength(42)); + } + DecodeResult::Error(ParseError::InvalidContentLength) => { + // Also acceptable — some parsers are strict about whitespace. + } + other => panic!("expected Complete or InvalidContentLength, got {other:?}"), + } + } + + #[test] + fn decode_large_content_length() { + let mut codec = Http10Codec::new(); + let buf = b"POST /upload HTTP/1.0\r\nContent-Length: 1048576\r\n\r\n"; + let outcome = codec.decode(buf, 0); + match outcome.result { + DecodeResult::Complete(req) => { + assert_eq!(req.body_framing, BodyFraming::ContentLength(1048576)); + } + other => panic!("expected Complete, got {other:?}"), + } + } + + #[test] + fn decode_zero_content_length() { + let mut codec = Http10Codec::new(); + let buf = b"POST /submit HTTP/1.0\r\nContent-Length: 0\r\n\r\n"; + let outcome = codec.decode(buf, 0); + match outcome.result { + DecodeResult::Complete(req) => { + assert_eq!(req.body_framing, BodyFraming::ContentLength(0)); + } + other => panic!("expected Complete, got {other:?}"), + } + } +} diff --git a/src/net/http/codec/v1_1.rs b/src/net/http/codec/v1_1.rs new file mode 100644 index 0000000..98c9b12 --- /dev/null +++ b/src/net/http/codec/v1_1.rs @@ -0,0 +1,369 @@ +use super::{Codec, DecodeOutcome, DecodeResult, parse}; +use crate::net::http::{ + error::ParseError, + request::{BodyFraming, Request, Version}, +}; + +/// HTTP/1.1 codec. +/// +/// Parses full HTTP/1.1 requests: request line + headers + optional body. +/// Enforces that the Host header is present (RFC 7230 §5.4). +/// Detects chunked transfer encoding (RFC 7230 §3.3.3) with priority over +/// Content-Length. +pub struct Http11Codec; + +impl Http11Codec { + pub fn new() -> Self { + Self + } +} + +impl Codec for Http11Codec { + fn decode(&mut self, buf: &[u8], buf_offset: usize) -> DecodeOutcome { + // Step 1: Parse the request line. + let req_line = match parse::parse_request_line(buf, buf_offset) { + None => { + return DecodeOutcome { + result: DecodeResult::Incomplete, + consumed: 0, + }; + } + Some(Err(e)) => { + return DecodeOutcome { + result: DecodeResult::Error(e), + consumed: 0, + }; + } + Some(Ok(rl)) => rl, + }; + + // Step 2: Parse headers from the buffer immediately after the request line. + // parse_headers takes only the sub-buffer; offsets are LOCAL (0-based into header_buf). + let header_buf = &buf[req_line.consumed..]; + let (local_headers, headers_consumed) = match parse::parse_headers(header_buf) { + None => { + return DecodeOutcome { + result: DecodeResult::Incomplete, + consumed: 0, + }; + } + Some(Err(e)) => { + return DecodeOutcome { + result: DecodeResult::Error(e), + consumed: 0, + }; + } + Some(Ok((headers, consumed))) => (headers, consumed), + }; + + // Step 3: HTTP/1.1 requires a Host header (RFC 7230 §5.4). + if !parse::has_header(&local_headers, header_buf, b"host") { + return DecodeOutcome { + result: DecodeResult::Error(ParseError::MissingHostHeader), + consumed: 0, + }; + } + + // Step 4: Check for Expect: 100-continue. + let expect_continue = parse::header_value_for(&local_headers, header_buf, b"expect") + .map(|v| v.eq_ignore_ascii_case(b"100-continue")) + .unwrap_or(false); + + // Step 5: Determine body framing. + // Chunked Transfer-Encoding takes priority over Content-Length (RFC 7230 §3.3.3). + let body_framing = if let Some(te) = + parse::header_value_for(&local_headers, header_buf, b"transfer-encoding") + { + if te.eq_ignore_ascii_case(b"chunked") { + BodyFraming::Chunked + } else { + // Non-chunked TE — fall through to Content-Length check. + match parse::find_content_length(&local_headers, header_buf) { + Err(e) => { + return DecodeOutcome { + result: DecodeResult::Error(e), + consumed: 0, + }; + } + Ok(Some(len)) => BodyFraming::ContentLength(len), + Ok(None) => BodyFraming::None, + } + } + } else { + match parse::find_content_length(&local_headers, header_buf) { + Err(e) => { + return DecodeOutcome { + result: DecodeResult::Error(e), + consumed: 0, + }; + } + Ok(Some(len)) => BodyFraming::ContentLength(len), + Ok(None) => BodyFraming::None, + } + }; + + // Step 6: Detect connection directive. + let connection_directive = parse::detect_connection_directive(&local_headers, header_buf); + + // Step 7: Absolutize header offsets. + let abs_base = buf_offset + req_line.consumed; + let abs_headers = parse::absolutize_headers(&local_headers, abs_base); + + let consumed = req_line.consumed + headers_consumed; + + DecodeOutcome { + result: DecodeResult::Complete(Request::new( + req_line.method, + req_line.path_start, + req_line.path_end, + Version::Http11, + abs_headers, + body_framing, + expect_continue, + connection_directive, + )), + consumed, + } + } + + fn version(&self) -> Version { + Version::Http11 + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::net::http::request::{BodyFraming, Method, Version}; + use parse::ConnectionDirective; + + #[test] + fn decode_simple_get() { + let mut codec = Http11Codec::new(); + let buf = b"GET /index.html HTTP/1.1\r\nHost: example.com\r\n\r\n"; + let outcome = codec.decode(buf, 0); + match outcome.result { + DecodeResult::Complete(req) => { + assert_eq!(req.method, Method::Get); + assert_eq!(req.version, Version::Http11); + assert_eq!(req.path_from_buf(buf), b"/index.html"); + assert_eq!(req.body_framing, BodyFraming::None); + } + other => panic!("expected Complete, got {other:?}"), + } + } + + #[test] + fn decode_missing_host_header() { + let mut codec = Http11Codec::new(); + let buf = b"GET /index.html HTTP/1.1\r\nContent-Length: 0\r\n\r\n"; + let outcome = codec.decode(buf, 0); + match outcome.result { + DecodeResult::Error(ParseError::MissingHostHeader) => {} + other => panic!("expected MissingHostHeader error, got {other:?}"), + } + } + + #[test] + fn decode_chunked_transfer_encoding() { + let mut codec = Http11Codec::new(); + let buf = + b"POST /upload HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\n"; + let outcome = codec.decode(buf, 0); + match outcome.result { + DecodeResult::Complete(req) => { + assert_eq!(req.body_framing, BodyFraming::Chunked); + } + other => panic!("expected Complete, got {other:?}"), + } + } + + #[test] + fn decode_content_length_body() { + let mut codec = Http11Codec::new(); + let buf = b"POST /data HTTP/1.1\r\nHost: example.com\r\nContent-Length: 42\r\n\r\n"; + let outcome = codec.decode(buf, 0); + match outcome.result { + DecodeResult::Complete(req) => { + assert_eq!(req.body_framing, BodyFraming::ContentLength(42)); + } + other => panic!("expected Complete, got {other:?}"), + } + } + + #[test] + fn decode_chunked_takes_priority_over_content_length() { + let mut codec = Http11Codec::new(); + // Both Transfer-Encoding: chunked and Content-Length present — chunked wins (RFC 7230 §3.3.3). + let buf = b"POST /data HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\nContent-Length: 100\r\n\r\n"; + let outcome = codec.decode(buf, 0); + match outcome.result { + DecodeResult::Complete(req) => { + assert_eq!(req.body_framing, BodyFraming::Chunked); + } + other => panic!("expected Complete, got {other:?}"), + } + } + + #[test] + fn decode_incomplete() { + let mut codec = Http11Codec::new(); + // Missing the final \r\n\r\n terminator. + let buf = b"GET /index.html HTTP/1.1\r\nHost: example.com\r\n"; + let outcome = codec.decode(buf, 0); + assert!(matches!(outcome.result, DecodeResult::Incomplete)); + assert_eq!(outcome.consumed, 0); + } + + #[test] + fn version_returns_http11() { + let codec = Http11Codec::new(); + assert_eq!(codec.version(), Version::Http11); + } + + #[test] + fn decode_connection_close_directive() { + let mut codec = Http11Codec::new(); + let buf = b"GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n"; + let outcome = codec.decode(buf, 0); + match outcome.result { + DecodeResult::Complete(req) => { + assert_eq!(req.connection_directive, ConnectionDirective::Close); + } + other => panic!("expected Complete, got {other:?}"), + } + } + + #[test] + fn decode_connection_keep_alive_directive() { + let mut codec = Http11Codec::new(); + let buf = b"GET / HTTP/1.1\r\nHost: example.com\r\nConnection: keep-alive\r\n\r\n"; + let outcome = codec.decode(buf, 0); + match outcome.result { + DecodeResult::Complete(req) => { + assert_eq!(req.connection_directive, ConnectionDirective::KeepAlive); + } + other => panic!("expected Complete, got {other:?}"), + } + } + + #[test] + fn decode_expect_100_continue() { + let mut codec = Http11Codec::new(); + let buf = + b"POST /upload HTTP/1.1\r\nHost: example.com\r\nContent-Length: 100\r\nExpect: 100-continue\r\n\r\n"; + let outcome = codec.decode(buf, 0); + match outcome.result { + DecodeResult::Complete(req) => { + assert!(req.expect_continue); + assert_eq!(req.body_framing, BodyFraming::ContentLength(100)); + } + other => panic!("expected Complete, got {other:?}"), + } + } + + #[test] + fn decode_expect_not_set_when_absent() { + let mut codec = Http11Codec::new(); + let buf = b"POST /upload HTTP/1.1\r\nHost: example.com\r\nContent-Length: 5\r\n\r\n"; + let outcome = codec.decode(buf, 0); + match outcome.result { + DecodeResult::Complete(req) => { + assert!(!req.expect_continue); + } + other => panic!("expected Complete, got {other:?}"), + } + } + + #[test] + fn decode_multiple_headers_same_name() { + let mut codec = Http11Codec::new(); + // Two X-Custom headers — both should be present in the parsed header list. + let buf = + b"GET / HTTP/1.1\r\nHost: example.com\r\nX-Custom: first\r\nX-Custom: second\r\n\r\n"; + let outcome = codec.decode(buf, 0); + match outcome.result { + DecodeResult::Complete(req) => { + let custom_count = req + .headers + .iter() + .filter(|h| buf[h.name_start..h.name_end].eq_ignore_ascii_case(b"X-Custom")) + .count(); + assert_eq!(custom_count, 2); + } + other => panic!("expected Complete, got {other:?}"), + } + } + + #[test] + fn decode_non_chunked_transfer_encoding_falls_through_to_content_length() { + let mut codec = Http11Codec::new(); + // Transfer-Encoding: gzip (not "chunked") should fall through to + // Content-Length handling. + let buf = b"POST /data HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: gzip\r\nContent-Length: 20\r\n\r\n"; + let outcome = codec.decode(buf, 0); + match outcome.result { + DecodeResult::Complete(req) => { + assert_eq!( + req.body_framing, + BodyFraming::ContentLength(20), + "non-chunked TE should fall through to Content-Length" + ); + } + other => panic!("expected Complete, got {other:?}"), + } + } + + #[test] + fn decode_non_chunked_transfer_encoding_no_content_length() { + let mut codec = Http11Codec::new(); + // Transfer-Encoding: gzip with no Content-Length should result in BodyFraming::None. + let buf = b"POST /data HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: gzip\r\n\r\n"; + let outcome = codec.decode(buf, 0); + match outcome.result { + DecodeResult::Complete(req) => { + assert_eq!( + req.body_framing, + BodyFraming::None, + "non-chunked TE without Content-Length should be None" + ); + } + other => panic!("expected Complete, got {other:?}"), + } + } + + #[test] + fn decode_head_request() { + let mut codec = Http11Codec::new(); + let buf = b"HEAD / HTTP/1.1\r\nHost: example.com\r\n\r\n"; + let outcome = codec.decode(buf, 0); + match outcome.result { + DecodeResult::Complete(req) => { + assert_eq!(req.method, Method::Head); + assert_eq!(req.body_framing, BodyFraming::None); + } + other => panic!("expected Complete, got {other:?}"), + } + } + + #[test] + fn decode_empty_header_value() { + let mut codec = Http11Codec::new(); + // X-Empty header with no value after the colon. + let buf = b"GET / HTTP/1.1\r\nHost: example.com\r\nX-Empty:\r\n\r\n"; + let outcome = codec.decode(buf, 0); + match outcome.result { + DecodeResult::Complete(req) => { + // Find the X-Empty header and confirm its value span is empty. + let empty_header = req + .headers + .iter() + .find(|h| buf[h.name_start..h.name_end].eq_ignore_ascii_case(b"X-Empty")); + assert!(empty_header.is_some(), "X-Empty header not found"); + let h = empty_header.unwrap(); + assert_eq!(h.value_start, h.value_end, "expected empty value span"); + } + other => panic!("expected Complete, got {other:?}"), + } + } +} diff --git a/src/net/http/connection.rs b/src/net/http/connection.rs new file mode 100644 index 0000000..a92ed7f --- /dev/null +++ b/src/net/http/connection.rs @@ -0,0 +1,364 @@ +use crate::net::http::{ + HttpError, + buffer::{ReadBuffer, WriteBuffer}, + error::ParseError, + request::{Method, Request}, + response::ResponseWriter, + session::Session, +}; +use crate::net::socket::TcpStream; + +const DEFAULT_BUF_CAPACITY: usize = 8192; // 8 KiB + +/// An HTTP connection wrapping a TCP stream. +/// +/// Owns the TcpStream, read/write buffers, and session state machine. +/// Provides the low-level stream API for processing HTTP requests. +pub struct HttpConnection { + stream: TcpStream, + pub(crate) read_buf: ReadBuffer, + write_buf: WriteBuffer, + pub(crate) session: Session, +} + +impl HttpConnection { + /// Create a new HttpConnection wrapping the given TcpStream. + pub(crate) fn new(stream: TcpStream, session: Session) -> Self { + Self { + stream, + read_buf: ReadBuffer::new(DEFAULT_BUF_CAPACITY), + write_buf: WriteBuffer::new(DEFAULT_BUF_CAPACITY), + session, + } + } + + /// Wait for the next HTTP request. + /// + /// Returns `Ok(Some(request))` when a request is ready, + /// `Ok(None)` when the connection is done (no more requests), + /// or `Err` on parse/TCP errors. + pub async fn next_request(&mut self) -> Result, HttpError> { + if self.session.is_done() { + return Ok(None); + } + + loop { + // Try to decode from existing buffer data + let buf = self.read_buf.unconsumed(); + let buf_offset = self.read_buf.start(); + match self.session.try_decode_request(buf, buf_offset)? { + Some((req, consumed)) => { + self.read_buf.consume(consumed); + return Ok(Some(req)); + } + None => { + // Need more data — compact if needed, then read + if self.read_buf.remaining_capacity() == 0 { + self.read_buf.compact(); + if self.read_buf.remaining_capacity() == 0 { + // Buffer is genuinely full with unconsumed data — request too large + return Err(HttpError::Parse(ParseError::RequestTooLarge)); + } + } + + // Read from TcpStream into read buffer + let buf_slice = self.read_buf.writable_slice(); + let n = self.stream.read(buf_slice).await.map_err(HttpError::Tcp)?; + if n == 0 { + // EOF — connection closed by remote + return Ok(None); + } + self.read_buf.advance_end(n); + } + } + } + } + + /// Begin writing a response for the current request. + /// + /// The returned `ResponseWriter` borrows the connection's write buffer + /// and TcpStream. When dropped or finished, it transitions the session. + pub fn respond(&mut self, req: &Request) -> ResponseWriter<'_> { + let is_head = req.method == Method::Head; + let version = self.session.version(); + ResponseWriter::new( + &mut self.write_buf, + &mut self.read_buf, + &self.stream, + &mut self.session, + version, + is_head, + req.body_framing, + req.expect_continue, + ) + } + + /// Prepare for the next request on a keep-alive connection. + pub(crate) fn prepare_next(&mut self) { + self.read_buf.compact(); + self.session.prepare_next_request(); + } + + /// Resolve a request's path offsets against the read buffer. + pub fn request_path(&self, req: &Request) -> &[u8] { + self.read_buf.slice_at(req.path_start(), req.path_end()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::net::http::codec::parse::ConnectionDirective; + use crate::net::http::codec::{HttpCodec, v1_1::Http11Codec}; + use crate::net::http::request::BodyFraming; + use crate::net::http::{Method, Version}; + + use crate::net::handler::tcp::TcpHandler; + use crate::net::handler::tcp::tcb::ConnectionId; + use crate::net::socket::LocalQueue; + use crate::net::wire::ip::{IpAddress, Ipv4Address}; + use std::cell::UnsafeCell; + use std::rc::Rc; + + fn new_test_connection() -> HttpConnection { + let handler = Rc::new(UnsafeCell::new(TcpHandler::new(false, false))); + let conn_id = ConnectionId { + local_addr: IpAddress::V4(Ipv4Address::unspecified()), + local_port: 0, + remote_addr: IpAddress::V4(Ipv4Address::unspecified()), + remote_port: 0, + }; + let event_queue = LocalQueue::new(16); + let stream = TcpStream::from_accepted_for_test(0, conn_id, event_queue, handler); + HttpConnection::new(stream, Session::http09()) + } + + fn new_test_connection_http11() -> HttpConnection { + let handler = Rc::new(UnsafeCell::new(TcpHandler::new(false, false))); + let conn_id = ConnectionId { + local_addr: IpAddress::V4(Ipv4Address::unspecified()), + local_port: 0, + remote_addr: IpAddress::V4(Ipv4Address::unspecified()), + remote_port: 0, + }; + let event_queue = LocalQueue::new(16); + let stream = TcpStream::from_accepted_for_test(0, conn_id, event_queue, handler); + HttpConnection::new(stream, Session::new(HttpCodec::Http11(Http11Codec::new()))) + } + + #[test] + fn request_path_resolution() { + let mut conn = new_test_connection(); + // Simulate data in the read buffer + conn.read_buf.append(b"GET /hello\r\n"); + let req = Request::new( + Method::Get, + 4, + 10, + Version::Http09, + Vec::new(), + BodyFraming::None, + false, + ConnectionDirective::None, + ); + assert_eq!(conn.request_path(&req), b"/hello"); + } + + #[test] + fn request_path_root() { + let mut conn = new_test_connection(); + conn.read_buf.append(b"GET /\r\n"); + // path_start=4, path_end=5 => b"/" + let req = Request::new( + Method::Get, + 4, + 5, + Version::Http09, + Vec::new(), + BodyFraming::None, + false, + ConnectionDirective::None, + ); + assert_eq!(conn.request_path(&req), b"/"); + } + + #[test] + fn request_path_with_offset_after_consume() { + let mut conn = new_test_connection(); + // Append a first request worth of bytes then consume it, leaving buffer + // start > 0. The path offsets must be absolute (not relative to start). + conn.read_buf.append(b"GET /first\r\n"); + conn.read_buf.consume(12); // consume "GET /first\r\n" + conn.read_buf.append(b"GET /second\r\n"); + // "GET /second\r\n" starts at absolute offset 12 in the buffer. + // path_start = 12+4 = 16, path_end = 16+7 = 23 + let req = Request::new( + Method::Get, + 16, + 23, + Version::Http09, + Vec::new(), + BodyFraming::None, + false, + ConnectionDirective::None, + ); + assert_eq!(conn.request_path(&req), b"/second"); + } + + // --- Session state inspection via pub(crate) field --- + + #[test] + fn initial_session_not_done() { + let conn = new_test_connection(); + assert!(!conn.session.is_done()); + } + + #[test] + fn initial_session_version_http09() { + let conn = new_test_connection(); + assert_eq!(conn.session.version(), Version::Http09); + } + + #[test] + fn initial_session_version_http11() { + let conn = new_test_connection_http11(); + assert_eq!(conn.session.version(), Version::Http11); + } + + // --- ReadBuffer state after construction --- + + #[test] + fn read_buf_initially_empty() { + let conn = new_test_connection(); + assert_eq!(conn.read_buf.unconsumed().len(), 0); + } + + #[test] + fn read_buf_initial_remaining_capacity() { + let conn = new_test_connection(); + // DEFAULT_BUF_CAPACITY is 8192 + assert_eq!(conn.read_buf.remaining_capacity(), 8192); + } + + #[test] + fn read_buf_start_initially_zero() { + let conn = new_test_connection(); + assert_eq!(conn.read_buf.start(), 0); + } + + #[test] + fn read_buf_append_updates_state() { + let mut conn = new_test_connection(); + let data = b"GET /test\r\n"; + let written = conn.read_buf.append(data); + assert_eq!(written, data.len()); + assert_eq!(conn.read_buf.unconsumed(), data); + assert_eq!(conn.read_buf.remaining_capacity(), 8192 - data.len()); + } + + // --- prepare_next() compacts buffer and resets session --- + + // --- Request decoding from pre-filled buffers --- + + #[test] + fn decode_http11_get_from_prefilled_buffer() { + let mut conn = new_test_connection_http11(); + let req_bytes = b"GET /hello HTTP/1.1\r\nHost: example.com\r\n\r\n"; + conn.read_buf.append(req_bytes); + + let buf = conn.read_buf.unconsumed(); + let offset = conn.read_buf.start(); + let result = conn.session.try_decode_request(buf, offset).unwrap(); + let (req, consumed) = result.unwrap(); + + assert_eq!(req.method, Method::Get); + assert_eq!(req.version, Version::Http11); + assert_eq!(req.body_framing, BodyFraming::None); + assert_eq!(consumed, req_bytes.len()); + } + + #[test] + fn decode_http11_post_with_content_length() { + let mut conn = new_test_connection_http11(); + let req_bytes = b"POST /data HTTP/1.1\r\nHost: example.com\r\nContent-Length: 5\r\n\r\n"; + conn.read_buf.append(req_bytes); + + let buf = conn.read_buf.unconsumed(); + let offset = conn.read_buf.start(); + let result = conn.session.try_decode_request(buf, offset).unwrap(); + let (req, _consumed) = result.unwrap(); + + assert_eq!(req.method, Method::Post); + assert_eq!(req.body_framing, BodyFraming::ContentLength(5)); + } + + #[test] + fn decode_http11_incomplete_returns_none() { + let mut conn = new_test_connection_http11(); + // Missing final \r\n\r\n + conn.read_buf + .append(b"GET /hello HTTP/1.1\r\nHost: example.com\r\n"); + + let buf = conn.read_buf.unconsumed(); + let offset = conn.read_buf.start(); + let result = conn.session.try_decode_request(buf, offset).unwrap(); + assert!(result.is_none(), "expected None for incomplete request"); + } + + #[test] + fn decode_http11_path_resolution_after_decode() { + let mut conn = new_test_connection_http11(); + let req_bytes = b"GET /test/path HTTP/1.1\r\nHost: localhost\r\n\r\n"; + conn.read_buf.append(req_bytes); + + let buf = conn.read_buf.unconsumed(); + let offset = conn.read_buf.start(); + let result = conn.session.try_decode_request(buf, offset).unwrap(); + let (req, _consumed) = result.unwrap(); + + assert_eq!(conn.request_path(&req), b"/test/path"); + } + + #[test] + fn decode_http11_missing_host_returns_error() { + let mut conn = new_test_connection_http11(); + conn.read_buf + .append(b"GET / HTTP/1.1\r\nContent-Length: 0\r\n\r\n"); + + let buf = conn.read_buf.unconsumed(); + let offset = conn.read_buf.start(); + let result = conn.session.try_decode_request(buf, offset); + assert!(result.is_err(), "expected error for missing Host header"); + } + + #[test] + fn prepare_next_compacts_buffer() { + let mut conn = new_test_connection_http11(); + // Fill the buffer with a complete HTTP/1.1 request and decode it so the + // session advances through the full lifecycle to AwaitingNext. + let req_bytes = b"GET /keep HTTP/1.1\r\nHost: localhost\r\n\r\n"; + conn.read_buf.append(req_bytes); + + // Drive the session manually through the states using pub(crate) access. + let buf = conn.read_buf.unconsumed(); + let offset = conn.read_buf.start(); + let result = conn.session.try_decode_request(buf, offset).unwrap(); + let (_req, consumed) = result.unwrap(); + conn.read_buf.consume(consumed); + + conn.session.begin_response(); + let keep_alive = conn.session.finish_response(); + assert!(keep_alive, "HTTP/1.1 should keep alive by default"); + + // Buffer start is now > 0 after consuming the request bytes. + assert!(conn.read_buf.start() > 0); + + // prepare_next() compacts the buffer and resets session to AwaitingRequest. + conn.prepare_next(); + + // After compact, start resets to 0. + assert_eq!(conn.read_buf.start(), 0); + // Session must be ready for the next request. + assert!(!conn.session.is_done()); + } +} diff --git a/src/net/http/error.rs b/src/net/http/error.rs new file mode 100644 index 0000000..e1581e0 --- /dev/null +++ b/src/net/http/error.rs @@ -0,0 +1,157 @@ +use std::fmt; + +use crate::net::{BindError, TcpError}; + +/// Errors returned by HTTP operations. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum HttpError { + /// Underlying TCP error (reset, timeout, not connected, etc.) + Tcp(TcpError), + /// Bind error from TcpListener (address in use, etc.) + Bind(BindError), + /// HTTP parse error from the codec + Parse(ParseError), + /// Connection closed + Closed, +} + +impl fmt::Display for HttpError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + HttpError::Tcp(e) => write!(f, "TCP error: {e}"), + HttpError::Bind(e) => write!(f, "bind error: {e}"), + HttpError::Parse(e) => write!(f, "HTTP parse error: {e}"), + HttpError::Closed => write!(f, "connection closed"), + } + } +} + +impl From for HttpError { + fn from(e: TcpError) -> Self { + HttpError::Tcp(e) + } +} + +impl From for HttpError { + fn from(e: BindError) -> Self { + HttpError::Bind(e) + } +} + +impl From for HttpError { + fn from(e: ParseError) -> Self { + HttpError::Parse(e) + } +} + +/// HTTP parse errors from the codec layer. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ParseError { + /// Request line is malformed (invalid method, missing path, etc.) + InvalidRequestLine, + /// Request line exceeds buffer capacity + RequestTooLarge, + /// Unsupported HTTP method (for HTTP/0.9: anything other than GET) + UnsupportedMethod, + /// HTTP version is not supported by this implementation + UnsupportedVersion, + /// A header line is malformed + InvalidHeader, + /// The number of headers exceeds the implementation limit + TooManyHeaders, + /// HTTP/1.1 request is missing the required Host header + MissingHostHeader, + /// The Content-Length header value is not a valid integer + InvalidContentLength, + /// The Transfer-Encoding: chunked encoding is malformed + InvalidChunkEncoding, +} + +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ParseError::InvalidRequestLine => write!(f, "invalid request line"), + ParseError::RequestTooLarge => write!(f, "request too large"), + ParseError::UnsupportedMethod => write!(f, "unsupported method"), + ParseError::UnsupportedVersion => write!(f, "unsupported version"), + ParseError::InvalidHeader => write!(f, "invalid header"), + ParseError::TooManyHeaders => write!(f, "too many headers"), + ParseError::MissingHostHeader => write!(f, "missing host header"), + ParseError::InvalidContentLength => write!(f, "invalid content-length"), + ParseError::InvalidChunkEncoding => write!(f, "invalid chunk encoding"), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn new_parse_error_display() { + assert_eq!( + format!("{}", ParseError::UnsupportedVersion), + "unsupported version" + ); + assert_eq!(format!("{}", ParseError::InvalidHeader), "invalid header"); + assert_eq!( + format!("{}", ParseError::TooManyHeaders), + "too many headers" + ); + assert_eq!( + format!("{}", ParseError::MissingHostHeader), + "missing host header" + ); + assert_eq!( + format!("{}", ParseError::InvalidContentLength), + "invalid content-length" + ); + assert_eq!( + format!("{}", ParseError::InvalidChunkEncoding), + "invalid chunk encoding" + ); + } + + #[test] + fn parse_error_display() { + assert_eq!( + format!("{}", ParseError::InvalidRequestLine), + "invalid request line" + ); + assert_eq!( + format!("{}", ParseError::RequestTooLarge), + "request too large" + ); + assert_eq!( + format!("{}", ParseError::UnsupportedMethod), + "unsupported method" + ); + } + + #[test] + fn http_error_display() { + let e = HttpError::Parse(ParseError::InvalidRequestLine); + assert_eq!(format!("{e}"), "HTTP parse error: invalid request line"); + + let e = HttpError::Closed; + assert_eq!(format!("{e}"), "connection closed"); + } + + #[test] + fn http_error_from_tcp_error() { + let e: HttpError = TcpError::Reset.into(); + assert!(matches!(e, HttpError::Tcp(TcpError::Reset))); + } + + #[test] + fn http_error_from_bind_error() { + let e: HttpError = BindError::AddressInUse.into(); + assert!(matches!(e, HttpError::Bind(BindError::AddressInUse))); + } + + #[test] + fn http_error_from_parse_error() { + let e: HttpError = ParseError::UnsupportedMethod.into(); + assert!(matches!(e, HttpError::Parse(ParseError::UnsupportedMethod))); + } +} diff --git a/src/net/http/handler.rs b/src/net/http/handler.rs new file mode 100644 index 0000000..8ddfa4e --- /dev/null +++ b/src/net/http/handler.rs @@ -0,0 +1,17 @@ +use crate::net::http::{HttpError, Request, ResponseWriter}; + +/// Trait for handling HTTP requests. +/// +/// Implement this trait and pass it to `HttpListener::serve()` for the +/// high-level server API. Each connection spawns a task that calls +/// `handle()` for every request. +/// +/// `Clone` is required because each spawned task gets its own copy. +pub trait HttpHandler: Clone + 'static { + /// Handle an HTTP request and write the response. + fn handle( + &self, + req: Request, + res: ResponseWriter<'_>, + ) -> impl std::future::Future> + '_; +} diff --git a/src/net/http/listener.rs b/src/net/http/listener.rs new file mode 100644 index 0000000..092be59 --- /dev/null +++ b/src/net/http/listener.rs @@ -0,0 +1,71 @@ +use crate::net::http::{HttpConnection, HttpError, handler::HttpHandler, session::Session}; +use crate::net::socket::TcpListener; +use crate::net::wire::ip::IpAddress; +use crate::rt::task::spawn; + +/// An HTTP listener that accepts incoming connections. +/// +/// Wraps `TcpListener` and produces `HttpConnection` instances. +/// Must be created inside `LocalRuntime::run()`. Panics otherwise. +pub struct HttpListener { + inner: TcpListener, +} + +impl HttpListener { + /// Create a new HTTP listener bound to the given address and port. + /// + /// Wraps `TcpListener::listen()` internally. Must be called inside + /// `LocalRuntime::run()`. Panics otherwise. + pub fn listen(addr: IpAddress, port: u16) -> Result { + let inner = TcpListener::listen(addr, port).map_err(HttpError::Bind)?; + Ok(Self { inner }) + } + + /// Accept the next HTTP connection. + /// + /// Returns an `HttpConnection` wrapping the accepted TCP stream + /// with a fresh HTTP/0.9 session. + pub async fn accept(&self) -> Result { + let stream = self.inner.accept().await; + Ok(HttpConnection::new(stream, Session::detecting())) + } + + /// Convenience method: accept loop + spawn a task per connection. + /// + /// Spawned tasks that return errors are silently dropped + /// (the connection is closed by `TcpStream::drop()`). + pub async fn serve(&self, handler: H) -> Result<(), HttpError> { + loop { + let mut conn = self.accept().await?; + let handler = handler.clone(); + spawn(async move { + loop { + match conn.next_request().await { + Ok(Some(req)) => { + let writer = conn.respond(&req); + if handler.handle(req, writer).await.is_err() { + break; + } + if conn.session.is_done() { + break; + } + conn.prepare_next(); + } + Ok(None) => break, + Err(_) => break, + } + } + }); + } + } + + /// Returns the local address this listener is bound to. + pub fn local_addr(&self) -> IpAddress { + self.inner.local_addr() + } + + /// Returns the local port this listener is bound to. + pub fn local_port(&self) -> u16 { + self.inner.local_port() + } +} diff --git a/src/net/http/mod.rs b/src/net/http/mod.rs new file mode 100644 index 0000000..ef467ac --- /dev/null +++ b/src/net/http/mod.rs @@ -0,0 +1,19 @@ +mod body; +mod buffer; +#[doc(hidden)] +pub mod codec; +mod connection; +mod error; +mod handler; +mod listener; +mod request; +mod response; +pub(crate) mod session; + +pub use body::BodyReader; +pub use connection::HttpConnection; +pub use error::{HttpError, ParseError}; +pub use handler::HttpHandler; +pub use listener::HttpListener; +pub use request::{BodyFraming, HeaderOffset, Method, Request, Version}; +pub use response::ResponseWriter; diff --git a/src/net/http/request.rs b/src/net/http/request.rs new file mode 100644 index 0000000..f57804f --- /dev/null +++ b/src/net/http/request.rs @@ -0,0 +1,314 @@ +use std::fmt; + +use crate::net::http::codec::parse::ConnectionDirective; + +/// HTTP request method. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Method { + Get, + Head, + Post, + Put, + Delete, + Options, + Trace, + Connect, + Patch, +} + +impl fmt::Display for Method { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Method::Get => write!(f, "GET"), + Method::Head => write!(f, "HEAD"), + Method::Post => write!(f, "POST"), + Method::Put => write!(f, "PUT"), + Method::Delete => write!(f, "DELETE"), + Method::Options => write!(f, "OPTIONS"), + Method::Trace => write!(f, "TRACE"), + Method::Connect => write!(f, "CONNECT"), + Method::Patch => write!(f, "PATCH"), + } + } +} + +/// HTTP protocol version. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Version { + Http09, + Http10, + Http11, +} + +impl fmt::Display for Version { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Version::Http09 => write!(f, "HTTP/0.9"), + Version::Http10 => write!(f, "HTTP/1.0"), + Version::Http11 => write!(f, "HTTP/1.1"), + } + } +} + +/// Zero-copy header field offsets into the connection's read buffer. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct HeaderOffset { + pub name_start: usize, + pub name_end: usize, + pub value_start: usize, + pub value_end: usize, +} + +/// How the body length is determined for this request. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BodyFraming { + None, + ContentLength(usize), + Chunked, +} + +/// An HTTP request with offset-based zero-copy path and header access. +/// +/// Path and header values are resolved via offset lookup into the +/// connection's read buffer. +#[derive(Debug, Clone)] +pub struct Request { + pub method: Method, + path_start: usize, + path_end: usize, + pub version: Version, + pub headers: Vec, + pub body_framing: BodyFraming, + pub(crate) expect_continue: bool, + pub(crate) connection_directive: ConnectionDirective, +} + +impl Request { + /// Create a new request with the given method, path offsets, version, + /// headers, and body framing. + pub(crate) fn new( + method: Method, + path_start: usize, + path_end: usize, + version: Version, + headers: Vec, + body_framing: BodyFraming, + expect_continue: bool, + connection_directive: ConnectionDirective, + ) -> Self { + Self { + method, + path_start, + path_end, + version, + headers, + body_framing, + expect_continue, + connection_directive, + } + } + + /// Resolve the request path from a byte buffer. + /// + /// The buffer must be the same buffer the offsets were parsed from + /// (i.e., the connection's ReadBuffer). This is enforced by the + /// `HttpConnection::request_path()` API in normal usage. + #[allow(dead_code)] + pub(crate) fn path_from_buf<'a>(&self, buf: &'a [u8]) -> &'a [u8] { + &buf[self.path_start..self.path_end] + } + + /// Returns the byte offset where the path starts in the read buffer. + pub fn path_start(&self) -> usize { + self.path_start + } + + /// Returns the byte offset where the path ends in the read buffer. + pub fn path_end(&self) -> usize { + self.path_end + } + + /// Look up a header value by name (case-insensitive). + /// + /// Returns the raw bytes of the header value if found, or `None`. + /// The `buf` must be the same buffer the offsets were parsed from. + pub fn header_value<'a>(&self, buf: &'a [u8], name: &str) -> Option<&'a [u8]> { + for h in &self.headers { + let header_name = &buf[h.name_start..h.name_end]; + if header_name.eq_ignore_ascii_case(name.as_bytes()) { + return Some(&buf[h.value_start..h.value_end]); + } + } + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::net::http::codec::parse::ConnectionDirective; + + #[test] + fn method_display_all() { + assert_eq!(format!("{}", Method::Get), "GET"); + assert_eq!(format!("{}", Method::Head), "HEAD"); + assert_eq!(format!("{}", Method::Post), "POST"); + assert_eq!(format!("{}", Method::Put), "PUT"); + assert_eq!(format!("{}", Method::Delete), "DELETE"); + assert_eq!(format!("{}", Method::Options), "OPTIONS"); + assert_eq!(format!("{}", Method::Trace), "TRACE"); + assert_eq!(format!("{}", Method::Connect), "CONNECT"); + assert_eq!(format!("{}", Method::Patch), "PATCH"); + } + + #[test] + fn version_display_all() { + assert_eq!(format!("{}", Version::Http09), "HTTP/0.9"); + assert_eq!(format!("{}", Version::Http10), "HTTP/1.0"); + assert_eq!(format!("{}", Version::Http11), "HTTP/1.1"); + } + + #[test] + fn header_offset_is_copy() { + let h = HeaderOffset { + name_start: 0, + name_end: 4, + value_start: 6, + value_end: 15, + }; + let h2 = h; // Copy + assert_eq!(h.name_start, h2.name_start); + assert_eq!(h.value_end, h2.value_end); + } + + #[test] + fn body_framing_default_is_none() { + let framing = BodyFraming::None; + assert_eq!(framing, BodyFraming::None); + } + + #[test] + fn body_framing_content_length() { + let framing = BodyFraming::ContentLength(1024); + assert_eq!(framing, BodyFraming::ContentLength(1024)); + } + + #[test] + fn body_framing_chunked() { + let framing = BodyFraming::Chunked; + assert_eq!(framing, BodyFraming::Chunked); + } + + #[test] + fn request_is_clone() { + let req = Request::new( + Method::Get, + 4, + 15, + Version::Http09, + Vec::new(), + BodyFraming::None, + false, + ConnectionDirective::None, + ); + let req2 = req.clone(); + assert_eq!(req.method, req2.method); + assert_eq!(req.version, req2.version); + } + + #[test] + fn request_is_no_longer_copy_but_clone() { + // Request cannot be Copy because it contains Vec. + // Verify clone works correctly. + let req = Request::new( + Method::Post, + 5, + 10, + Version::Http11, + vec![HeaderOffset { + name_start: 0, + name_end: 4, + value_start: 6, + value_end: 9, + }], + BodyFraming::ContentLength(42), + false, + ConnectionDirective::None, + ); + let req2 = req.clone(); + assert_eq!(req2.method, Method::Post); + assert_eq!(req2.version, Version::Http11); + assert_eq!(req2.headers.len(), 1); + assert_eq!(req2.body_framing, BodyFraming::ContentLength(42)); + } + + #[test] + fn request_path_offsets() { + let req = Request::new( + Method::Get, + 4, + 15, + Version::Http09, + Vec::new(), + BodyFraming::None, + false, + ConnectionDirective::None, + ); + let buf = b"GET /index.html\r\n"; + assert_eq!(req.path_from_buf(buf), b"/index.html"); + } + + #[test] + fn request_empty_path() { + let req = Request::new( + Method::Get, + 4, + 5, + Version::Http09, + Vec::new(), + BodyFraming::None, + false, + ConnectionDirective::None, + ); + let buf = b"GET /\n"; + assert_eq!(req.path_from_buf(buf), b"/"); + } + + #[test] + fn request_header_value_lookup() { + // Simulate buffer: "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n" + // 0123456789... + // "Host" at 16..20, ": " skipped, "example.com" at 22..33 + let buf = b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"; + let headers = vec![HeaderOffset { + name_start: 16, + name_end: 20, + value_start: 22, + value_end: 33, + }]; + let req = Request::new( + Method::Get, + 4, + 5, + Version::Http11, + headers, + BodyFraming::None, + false, + ConnectionDirective::None, + ); + assert_eq!(req.header_value(buf, "Host"), Some(b"example.com".as_ref())); + assert_eq!(req.header_value(buf, "host"), Some(b"example.com".as_ref())); + assert_eq!(req.header_value(buf, "HOST"), Some(b"example.com".as_ref())); + assert_eq!(req.header_value(buf, "Content-Type"), None); + } + + #[test] + fn method_debug() { + assert_eq!(format!("{:?}", Method::Get), "Get"); + } + + #[test] + fn version_display() { + assert_eq!(format!("{}", Version::Http09), "HTTP/0.9"); + } +} diff --git a/src/net/http/response.rs b/src/net/http/response.rs new file mode 100644 index 0000000..a5820d8 --- /dev/null +++ b/src/net/http/response.rs @@ -0,0 +1,515 @@ +use std::borrow::Cow; + +use crate::net::http::{ + HttpError, + body::BodyReader, + buffer::{ReadBuffer, WriteBuffer}, + request::{BodyFraming, Version}, + session::Session, +}; +use crate::net::socket::TcpStream; + +/// Response state machine. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[allow(dead_code)] +enum ResponseState { + /// Status and headers not yet sent. + Headers, + /// Headers flushed, writing body. + Body, + /// Response complete. + Finished, +} + +/// Max hex digits for a usize (2 per byte) plus `\r\n`. +const HEX_BUF_LEN: usize = std::mem::size_of::() * 2 + 2; + +/// Format `n` as lowercase hex followed by `\r\n` into `buf`. +/// Returns the number of bytes written. +fn write_hex_usize(n: usize, buf: &mut [u8; HEX_BUF_LEN]) -> usize { + if n == 0 { + buf[0] = b'0'; + buf[1] = b'\r'; + buf[2] = b'\n'; + return 3; + } + let mut pos = 0; + let mut val = n; + while val > 0 { + let digit = (val & 0xf) as u8; + buf[pos] = if digit < 10 { + b'0' + digit + } else { + b'a' + digit - 10 + }; + pos += 1; + val >>= 4; + } + buf[..pos].reverse(); + buf[pos] = b'\r'; + buf[pos + 1] = b'\n'; + pos + 2 +} + +/// Writes HTTP response data to the client. +pub struct ResponseWriter<'conn> { + write_buf: &'conn mut WriteBuffer, + read_buf: &'conn mut ReadBuffer, + stream: &'conn TcpStream, + session: &'conn mut Session, + finished: bool, + version: Version, + state: ResponseState, + status_code: u16, + status_reason: Cow<'static, str>, + response_headers: Vec<(String, String)>, + is_head: bool, + chunked: bool, + body_framing: BodyFraming, + expect_continue: bool, + body_taken: bool, +} + +impl<'conn> ResponseWriter<'conn> { + pub(crate) fn new( + write_buf: &'conn mut WriteBuffer, + read_buf: &'conn mut ReadBuffer, + stream: &'conn TcpStream, + session: &'conn mut Session, + version: Version, + is_head: bool, + body_framing: BodyFraming, + expect_continue: bool, + ) -> Self { + session.begin_response(); + Self { + write_buf, + read_buf, + stream, + session, + finished: false, + version, + state: ResponseState::Headers, + status_code: 200, + status_reason: Cow::Borrowed("OK"), + response_headers: Vec::new(), + is_head, + chunked: false, + body_framing, + expect_continue, + body_taken: false, + } + } + + /// Set the response status code and reason phrase. + /// Defaults to 200 OK if not called. + /// No-op for HTTP/0.9. + pub fn set_status(&mut self, code: u16, reason: &str) { + if self.version == Version::Http09 { + return; + } + self.status_code = code; + self.status_reason = match reason { + "OK" => Cow::Borrowed("OK"), + "Created" => Cow::Borrowed("Created"), + "No Content" => Cow::Borrowed("No Content"), + "Moved Permanently" => Cow::Borrowed("Moved Permanently"), + "Found" => Cow::Borrowed("Found"), + "Not Modified" => Cow::Borrowed("Not Modified"), + "Bad Request" => Cow::Borrowed("Bad Request"), + "Unauthorized" => Cow::Borrowed("Unauthorized"), + "Forbidden" => Cow::Borrowed("Forbidden"), + "Not Found" => Cow::Borrowed("Not Found"), + "Method Not Allowed" => Cow::Borrowed("Method Not Allowed"), + "Internal Server Error" => Cow::Borrowed("Internal Server Error"), + "Service Unavailable" => Cow::Borrowed("Service Unavailable"), + other => Cow::Owned(other.to_string()), + }; + } + + /// Add a response header. Can be called multiple times. + /// No-op for HTTP/0.9. + pub fn add_header(&mut self, name: &str, value: &str) { + if self.version == Version::Http09 { + return; + } + self.response_headers + .push((name.to_string(), value.to_string())); + } + + /// Access the request body reader. Must be called before write_body(). + /// Can only be called once per response. The returned BodyReader must be + /// fully consumed and dropped before calling set_status/add_header/write_body. + pub fn body(&mut self) -> BodyReader<'_> { + debug_assert!(!self.body_taken, "body() called twice"); + debug_assert_eq!( + self.state, + ResponseState::Headers, + "body() called after headers sent" + ); + self.body_taken = true; + BodyReader::new( + self.stream, + self.read_buf, + self.body_framing, + self.expect_continue, + ) + } + + /// Write body bytes to the response. + /// + /// On first call for HTTP/1.x, flushes the status line and headers. + /// For HEAD requests, body bytes are suppressed but headers are still sent. + pub async fn write_body(&mut self, data: &[u8]) -> Result { + if self.state == ResponseState::Headers && self.version != Version::Http09 { + self.flush_headers().await?; + } + + // HEAD responses: suppress body but still "write" for Content-Length accounting + if self.is_head { + return Ok(data.len()); + } + + if self.chunked { + // Write chunk header: hex size + \r\n (stack-formatted, no alloc). + let mut hex_buf = [0u8; HEX_BUF_LEN]; + let n = write_hex_usize(data.len(), &mut hex_buf); + self.write_all(&hex_buf[..n]).await?; + self.write_all(data).await?; + self.write_all(b"\r\n").await?; + Ok(data.len()) + } else { + self.write_all(data).await + } + } + + /// Signal that the response is complete. Flushes any remaining buffered data. + pub async fn finish(mut self) -> Result<(), HttpError> { + if self.state == ResponseState::Headers && self.version != Version::Http09 { + // Headers-only response (e.g., 204, 304) + if !self + .response_headers + .iter() + .any(|(n, _)| n.eq_ignore_ascii_case("content-length")) + { + self.response_headers + .push(("Content-Length".to_string(), "0".to_string())); + } + self.flush_headers().await?; + } + + if self.chunked { + // Send final chunk: 0\r\n\r\n + self.write_all(b"0\r\n\r\n").await?; + } + + self.flush().await?; + self.session.finish_response(); + self.finished = true; + Ok(()) + } + + /// Flush headers to the wire. Called once on first write_body or finish. + async fn flush_headers(&mut self) -> Result<(), HttpError> { + debug_assert_eq!(self.state, ResponseState::Headers); + + // Status line — direct writes, no String allocation. + let version_bytes: &[u8] = match self.version { + Version::Http10 => b"HTTP/1.0 ", + Version::Http11 => b"HTTP/1.1 ", + Version::Http09 => unreachable!(), + }; + self.write_all(version_bytes).await?; + let code = self.status_code; + self.write_all(&[ + b'0' + (code / 100) as u8, + b'0' + ((code / 10) % 10) as u8, + b'0' + (code % 10) as u8, + ]) + .await?; + self.write_all(b" ").await?; + // Temporarily take status_reason to avoid borrow conflict across await. + let reason = std::mem::replace(&mut self.status_reason, Cow::Borrowed("")); + self.write_all(reason.as_bytes()).await?; + self.status_reason = reason; + self.write_all(b"\r\n").await?; + + // Check if handler set Content-Length. + let has_content_length = self + .response_headers + .iter() + .any(|(n, _)| n.eq_ignore_ascii_case("content-length")); + + // Headers — direct writes per header, no format_header() allocation. + let headers = std::mem::take(&mut self.response_headers); + for (name, value) in &headers { + self.write_all(name.as_bytes()).await?; + self.write_all(b": ").await?; + self.write_all(value.as_bytes()).await?; + self.write_all(b"\r\n").await?; + } + self.response_headers = headers; + + // Auto-inject Transfer-Encoding for HTTP/1.1 without Content-Length. + // Written after user headers (RFC 9112 does not mandate header ordering). + if !has_content_length && self.version == Version::Http11 { + self.chunked = true; + self.write_all(b"Transfer-Encoding: chunked\r\n").await?; + } + + // End of headers. + self.write_all(b"\r\n").await?; + self.state = ResponseState::Body; + Ok(()) + } + + /// Write all bytes to the write buffer, flushing as needed. + async fn write_all(&mut self, data: &[u8]) -> Result { + let mut total_written = 0; + while total_written < data.len() { + let n = self.write_buf.write(&data[total_written..]); + total_written += n; + + if self.write_buf.remaining_capacity() == 0 || total_written == data.len() { + self.flush().await?; + } + } + Ok(total_written) + } + + /// Flush the write buffer to the TcpStream. + async fn flush(&mut self) -> Result<(), HttpError> { + while !self.write_buf.is_empty() { + let pending = self.write_buf.pending(); + let n = self.stream.write(pending).await.map_err(HttpError::Tcp)?; + self.write_buf.advance(n); + } + Ok(()) + } +} + +impl Drop for ResponseWriter<'_> { + fn drop(&mut self) { + if !self.finished { + self.session.finish_response(); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn response_state_initial() { + let state = ResponseState::Headers; + assert!(matches!(state, ResponseState::Headers)); + } + + #[test] + fn write_hex_zero() { + let mut buf = [0u8; HEX_BUF_LEN]; + let n = write_hex_usize(0, &mut buf); + assert_eq!(&buf[..n], b"0\r\n"); + } + + #[test] + fn write_hex_small() { + let mut buf = [0u8; HEX_BUF_LEN]; + let n = write_hex_usize(255, &mut buf); + assert_eq!(&buf[..n], b"ff\r\n"); + } + + #[test] + fn write_hex_large() { + let mut buf = [0u8; HEX_BUF_LEN]; + let n = write_hex_usize(0x1a2b3c, &mut buf); + assert_eq!(&buf[..n], b"1a2b3c\r\n"); + } + + #[test] + fn write_hex_one() { + let mut buf = [0u8; HEX_BUF_LEN]; + let n = write_hex_usize(1, &mut buf); + assert_eq!(&buf[..n], b"1\r\n"); + } + + #[test] + fn write_hex_sixteen() { + let mut buf = [0u8; HEX_BUF_LEN]; + let n = write_hex_usize(16, &mut buf); + assert_eq!(&buf[..n], b"10\r\n"); + } + + #[test] + fn cow_status_common_phrase_is_borrowed() { + let cow: Cow<'static, str> = match "OK" { + "OK" => Cow::Borrowed("OK"), + other => Cow::Owned(other.to_string()), + }; + assert!(matches!(cow, Cow::Borrowed(_))); + } + + #[test] + fn cow_status_custom_phrase_is_owned() { + let reason = "Custom Reason"; + let cow: Cow<'static, str> = match reason { + "OK" => Cow::Borrowed("OK"), + other => Cow::Owned(other.to_string()), + }; + assert!(matches!(cow, Cow::Owned(_))); + assert_eq!(&*cow, "Custom Reason"); + } + + #[test] + fn write_hex_max_usize() { + let mut buf = [0u8; HEX_BUF_LEN]; + let n = write_hex_usize(usize::MAX, &mut buf); + let hex_str = std::str::from_utf8(&buf[..n - 2]).unwrap(); + assert!(hex_str.chars().all(|c| c == 'f')); + assert_eq!(buf[n - 2], b'\r'); + assert_eq!(buf[n - 1], b'\n'); + } + + #[test] + fn write_hex_powers_of_16() { + let cases: &[(usize, &[u8])] = &[ + (0x10, b"10\r\n"), + (0x100, b"100\r\n"), + (0x1000, b"1000\r\n"), + ]; + for &(val, expected) in cases { + let mut buf = [0u8; HEX_BUF_LEN]; + let n = write_hex_usize(val, &mut buf); + assert_eq!(&buf[..n], expected, "failed for value {:#x}", val); + } + } + + /// Verify that common HTTP status reason phrases produce `Cow::Borrowed` + /// via the same match arms used in `set_status()`. + #[test] + fn status_phrase_200_ok_is_borrowed() { + let cow: Cow<'static, str> = match "OK" { + "OK" => Cow::Borrowed("OK"), + "Created" => Cow::Borrowed("Created"), + "No Content" => Cow::Borrowed("No Content"), + "Moved Permanently" => Cow::Borrowed("Moved Permanently"), + "Not Modified" => Cow::Borrowed("Not Modified"), + "Not Found" => Cow::Borrowed("Not Found"), + "Internal Server Error" => Cow::Borrowed("Internal Server Error"), + other => Cow::Owned(other.to_string()), + }; + assert!(matches!(cow, Cow::Borrowed("OK"))); + } + + #[test] + fn status_phrase_404_not_found_is_borrowed() { + let cow: Cow<'static, str> = match "Not Found" { + "OK" => Cow::Borrowed("OK"), + "Not Found" => Cow::Borrowed("Not Found"), + other => Cow::Owned(other.to_string()), + }; + assert!(matches!(cow, Cow::Borrowed("Not Found"))); + } + + #[test] + fn status_phrase_500_internal_server_error_is_borrowed() { + let cow: Cow<'static, str> = match "Internal Server Error" { + "OK" => Cow::Borrowed("OK"), + "Internal Server Error" => Cow::Borrowed("Internal Server Error"), + other => Cow::Owned(other.to_string()), + }; + assert!(matches!(cow, Cow::Borrowed("Internal Server Error"))); + } + + #[test] + fn status_phrase_301_moved_permanently_is_borrowed() { + let cow: Cow<'static, str> = match "Moved Permanently" { + "Moved Permanently" => Cow::Borrowed("Moved Permanently"), + other => Cow::Owned(other.to_string()), + }; + assert!(matches!(cow, Cow::Borrowed("Moved Permanently"))); + } + + #[test] + fn status_phrase_304_not_modified_is_borrowed() { + let cow: Cow<'static, str> = match "Not Modified" { + "Not Modified" => Cow::Borrowed("Not Modified"), + other => Cow::Owned(other.to_string()), + }; + assert!(matches!(cow, Cow::Borrowed("Not Modified"))); + } + + #[test] + fn status_phrase_all_borrowed_variants() { + // Exhaustively verify every Cow::Borrowed arm in set_status(). + let phrases = [ + "OK", + "Created", + "No Content", + "Moved Permanently", + "Found", + "Not Modified", + "Bad Request", + "Unauthorized", + "Forbidden", + "Not Found", + "Method Not Allowed", + "Internal Server Error", + "Service Unavailable", + ]; + for phrase in phrases { + let cow: Cow<'static, str> = match phrase { + "OK" => Cow::Borrowed("OK"), + "Created" => Cow::Borrowed("Created"), + "No Content" => Cow::Borrowed("No Content"), + "Moved Permanently" => Cow::Borrowed("Moved Permanently"), + "Found" => Cow::Borrowed("Found"), + "Not Modified" => Cow::Borrowed("Not Modified"), + "Bad Request" => Cow::Borrowed("Bad Request"), + "Unauthorized" => Cow::Borrowed("Unauthorized"), + "Forbidden" => Cow::Borrowed("Forbidden"), + "Not Found" => Cow::Borrowed("Not Found"), + "Method Not Allowed" => Cow::Borrowed("Method Not Allowed"), + "Internal Server Error" => Cow::Borrowed("Internal Server Error"), + "Service Unavailable" => Cow::Borrowed("Service Unavailable"), + other => Cow::Owned(other.to_string()), + }; + assert!( + matches!(cow, Cow::Borrowed(_)), + "expected Cow::Borrowed for phrase {:?}", + phrase + ); + assert_eq!(&*cow, phrase); + } + } + + #[test] + fn status_code_digit_extraction() { + // Verify the digit extraction logic used in flush_headers(). + let cases: &[(u16, [u8; 3])] = &[ + (200, [b'2', b'0', b'0']), + (404, [b'4', b'0', b'4']), + (500, [b'5', b'0', b'0']), + (301, [b'3', b'0', b'1']), + (304, [b'3', b'0', b'4']), + (201, [b'2', b'0', b'1']), + (503, [b'5', b'0', b'3']), + ]; + for &(code, expected) in cases { + let digits = [ + b'0' + (code / 100) as u8, + b'0' + ((code / 10) % 10) as u8, + b'0' + (code % 10) as u8, + ]; + assert_eq!(digits, expected, "failed for status code {}", code); + } + } + + #[test] + fn write_hex_mixed_digits() { + let mut buf = [0u8; HEX_BUF_LEN]; + let n = write_hex_usize(0xDEAD, &mut buf); + assert_eq!(&buf[..n], b"dead\r\n"); + } +} diff --git a/src/net/http/session.rs b/src/net/http/session.rs new file mode 100644 index 0000000..c2cadbe --- /dev/null +++ b/src/net/http/session.rs @@ -0,0 +1,278 @@ +use crate::net::http::codec::parse::ConnectionDirective; +use crate::net::http::{ + HttpError, + codec::{DecodeResult, HttpCodec, v0_9::Http09Codec}, + request::{Request, Version}, +}; + +/// Connection-level state machine for HTTP request/response lifecycle. +/// +/// Owns the codec and enforces request/response ordering. +pub(crate) struct Session { + codec: HttpCodec, + state: SessionState, + connection_directive: ConnectionDirective, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[allow(dead_code)] +enum SessionState { + /// Ready to decode the next request. + AwaitingRequest, + /// Request has been decoded, waiting for response to begin. + RequestReady, + /// Request body is being read. + ReadingBody, + /// Waiting to send 100 Continue. + ExpectContinue, + /// Response is being written. + ResponseInProgress, + /// A parse error occurred. Connection should close. + Failed, + /// Connection lifecycle is complete. + Done, + /// Response finished, connection kept alive, waiting for next request setup. + AwaitingNext, +} + +impl Session { + /// Create a new session with the given codec. + pub fn new(codec: HttpCodec) -> Self { + Self { + codec, + state: SessionState::AwaitingRequest, + connection_directive: ConnectionDirective::None, + } + } + + /// Create a new session with the default HTTP/0.9 codec. + #[allow(dead_code)] + pub fn http09() -> Self { + Self::new(HttpCodec::Http09(Http09Codec::new())) + } + + /// Create a new session in version-detecting mode. + /// + /// The codec will peek at the first request line to determine the HTTP + /// version and transition to the appropriate codec automatically. + pub fn detecting() -> Self { + Self::new(HttpCodec::Detecting) + } + + /// Try to decode a request from the buffer. + /// + /// `buf` is the unconsumed bytes from the ReadBuffer. + /// `buf_offset` is the absolute position of buf[0] in the ReadBuffer. + /// + /// Returns `Ok(Some((request, consumed)))` on success, + /// `Ok(None)` if more data is needed, or `Err` on parse error + /// (which transitions the session to Failed/Done). + pub fn try_decode_request( + &mut self, + buf: &[u8], + buf_offset: usize, + ) -> Result, HttpError> { + debug_assert!( + self.state == SessionState::AwaitingRequest, + "try_decode_request called in state {:?}", + self.state + ); + + let outcome = self.codec.decode(buf, buf_offset); + match outcome.result { + DecodeResult::Complete(req) => { + self.state = SessionState::RequestReady; + self.connection_directive = req.connection_directive; + Ok(Some((req, outcome.consumed))) + } + DecodeResult::Incomplete => Ok(None), + DecodeResult::Error(e) => { + self.state = SessionState::Failed; + Err(HttpError::Parse(e)) + } + } + } + + /// Mark that the response has started. + pub fn begin_response(&mut self) { + debug_assert!( + self.state == SessionState::RequestReady || self.state == SessionState::ReadingBody, + "begin_response called in state {:?}", + self.state + ); + self.state = SessionState::ResponseInProgress; + } + + /// Mark that the response is finished. + /// + /// Returns `true` if the connection should stay alive (keep-alive). + pub fn finish_response(&mut self) -> bool { + debug_assert_eq!(self.state, SessionState::ResponseInProgress); + + let keep_alive = match self.codec.version() { + Version::Http09 => false, + Version::Http10 => self.connection_directive == ConnectionDirective::KeepAlive, + Version::Http11 => self.connection_directive != ConnectionDirective::Close, + }; + + if keep_alive { + self.state = SessionState::AwaitingNext; + } else { + self.state = SessionState::Done; + } + + keep_alive + } + + /// Transition from AwaitingNext to AwaitingRequest for the next request + /// on a keep-alive connection. + pub fn prepare_next_request(&mut self) { + debug_assert_eq!(self.state, SessionState::AwaitingNext); + self.connection_directive = ConnectionDirective::None; + self.state = SessionState::AwaitingRequest; + } + + /// Whether the session is done (connection should close). + pub fn is_done(&self) -> bool { + matches!(self.state, SessionState::Done | SessionState::Failed) + } + + /// Returns the HTTP version of this session's codec. + pub fn version(&self) -> Version { + self.codec.version() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::net::http::{Method, Version}; + + fn new_session() -> Session { + Session::new(HttpCodec::Http09(Http09Codec::new())) + } + + #[test] + fn initial_state_is_awaiting_request() { + let session = new_session(); + assert!(!session.is_done()); + } + + #[test] + fn decode_complete_request() { + let mut session = new_session(); + let buf = b"GET /test\r\n"; + let result = session.try_decode_request(buf, 0).unwrap(); + assert!(result.is_some()); + let (req, consumed) = result.unwrap(); + assert_eq!(req.method, Method::Get); + assert_eq!(consumed, 11); + } + + #[test] + fn decode_incomplete_returns_none() { + let mut session = new_session(); + let buf = b"GET /test"; + let result = session.try_decode_request(buf, 0).unwrap(); + assert!(result.is_none()); + assert!(!session.is_done()); + } + + #[test] + fn decode_error_transitions_to_done() { + let mut session = new_session(); + let buf = b"POST /test\r\n"; + let result = session.try_decode_request(buf, 0); + assert!(result.is_err()); + assert!(session.is_done()); + } + + #[test] + fn response_lifecycle() { + let mut session = new_session(); + let buf = b"GET /\r\n"; + session.try_decode_request(buf, 0).unwrap(); + + session.begin_response(); + assert!(!session.is_done()); + + let keep_alive = session.finish_response(); + assert!(!keep_alive); // HTTP/0.9 never keeps alive + assert!(session.is_done()); + } + + #[test] + fn version_delegates_to_codec() { + let session = new_session(); + assert_eq!(session.version(), Version::Http09); + } + + #[test] + fn session_http10_close_by_default() { + let mut session = Session::new(HttpCodec::Http10( + crate::net::http::codec::v1_0::Http10Codec::new(), + )); + let buf = b"GET /test HTTP/1.0\r\nHost: test\r\n\r\n"; + session.try_decode_request(buf, 0).unwrap(); + session.begin_response(); + let keep_alive = session.finish_response(); + assert!(!keep_alive); + assert!(session.is_done()); + } + + #[test] + fn session_http11_keep_alive_by_default() { + let mut session = Session::new(HttpCodec::Http11( + crate::net::http::codec::v1_1::Http11Codec::new(), + )); + let buf = b"GET /test HTTP/1.1\r\nHost: test\r\n\r\n"; + let result = session.try_decode_request(buf, 0).unwrap(); + assert!(result.is_some()); + session.begin_response(); + let keep_alive = session.finish_response(); + assert!(keep_alive); + assert!(!session.is_done()); + } + + #[test] + fn session_http11_connection_close() { + let mut session = Session::new(HttpCodec::Http11( + crate::net::http::codec::v1_1::Http11Codec::new(), + )); + let buf = b"GET /test HTTP/1.1\r\nHost: test\r\nConnection: close\r\n\r\n"; + let result = session.try_decode_request(buf, 0).unwrap(); + assert!(result.is_some()); + session.begin_response(); + let keep_alive = session.finish_response(); + assert!(!keep_alive); + assert!(session.is_done()); + } + + #[test] + fn session_http10_keep_alive_header() { + let mut session = Session::new(HttpCodec::Http10( + crate::net::http::codec::v1_0::Http10Codec::new(), + )); + let buf = b"GET /test HTTP/1.0\r\nConnection: keep-alive\r\n\r\n"; + let result = session.try_decode_request(buf, 0).unwrap(); + assert!(result.is_some()); + session.begin_response(); + let keep_alive = session.finish_response(); + assert!(keep_alive); + assert!(!session.is_done()); + } + + #[test] + fn session_awaiting_next_to_awaiting_request() { + let mut session = Session::new(HttpCodec::Http11( + crate::net::http::codec::v1_1::Http11Codec::new(), + )); + let buf = b"GET /test HTTP/1.1\r\nHost: test\r\n\r\n"; + session.try_decode_request(buf, 0).unwrap(); + session.begin_response(); + let keep_alive = session.finish_response(); + assert!(keep_alive); + session.prepare_next_request(); + assert!(!session.is_done()); + } +} diff --git a/src/net/mod.rs b/src/net/mod.rs new file mode 100644 index 0000000..88b682a --- /dev/null +++ b/src/net/mod.rs @@ -0,0 +1,17 @@ +#[doc(hidden)] +pub mod checksum; +mod fragment; +mod neighbor; +mod pmtu; + +pub mod handler; +pub mod http; +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(crate) use neighbor::NeighborUpdate; +pub use pmtu::PmtuCache; diff --git a/src/net/neighbor/arp.rs b/src/net/neighbor/arp.rs new file mode 100644 index 0000000..67aaf16 --- /dev/null +++ b/src/net/neighbor/arp.rs @@ -0,0 +1,513 @@ +use coarsetime::{Duration, Instant}; +use rustc_hash::FxHashMap; + +use crate::{ + net::wire::{ + arp::{ARP_FRAME_LEN, ArpFrame, ArpHardwareTypes, ArpOperations}, + ethernet::{EtherTypes, MacAddress}, + ip::{IpAddress, Ipv4Address}, + }, + xdp::frame::{Frame, FrameBuffer}, +}; + +use super::NeighborState; +use super::handler::NeighborUpdate; + +#[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: &mut FxHashMap, + local_mac: MacAddress, + local_ipv4: &[Ipv4Address], + ttl: Duration, + mut frame: Frame<'umem>, + rx_return: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, +) -> Option { + // 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 None; + } + + // 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 None; + } + + // 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. + let ip = IpAddress::V4(spa); + table.insert(ip, NeighborState::reachable(sha, now + ttl)); + let update = NeighborUpdate { ip, mac: sha }; + + // 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 Some(update); + } + + // 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); + Some(update) +} + +#[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..0809e98 --- /dev/null +++ b/src/net/neighbor/entry.rs @@ -0,0 +1,421 @@ +use coarsetime::{Duration, Instant}; + +use crate::net::wire::ethernet::MacAddress; + +/// Time before an [`NeighborState::Incomplete`] entry is considered timed out. +const INCOMPLETE_TIMEOUT: Duration = Duration::from_secs(3); + +/// Time before a [`NeighborState::Stale`] entry is evicted from the cache. +const STALE_TIMEOUT: Duration = Duration::from_secs(30); + +/// Minimum gap between solicitations for the same entry. +const SOLICIT_GUARD: Duration = Duration::from_secs(1); + +/// Three-state model for a neighbor cache entry. +/// +/// The state machine mirrors the relevant subset of RFC 4861 §7.3: +/// +/// ```text +/// [new IP] [reply received] [TTL expires] +/// │ │ │ +/// ▼ ▼ ▼ +/// Incomplete ──────► Reachable ──────────► Stale +/// │ │ +/// └─ evicted after INCOMPLETE_TIMEOUT └─ evicted after STALE_TIMEOUT +/// ``` +#[derive(Debug, Clone)] +pub(super) enum NeighborState { + /// A solicitation has been sent but no reply has been received yet. + Incomplete { + /// When the solicitation was (last) sent. + solicited_at: Instant, + }, + /// The mapping is confirmed and the TTL has not yet expired. + Reachable { + /// The resolved hardware address. + mac: MacAddress, + /// Absolute time at which the entry expires and transitions to `Stale`. + expires_at: Instant, + }, + /// The TTL has expired but the MAC is probably still valid. + /// + /// A new solicitation will be sent lazily (on next use) unless one has + /// already been sent recently. + Stale { + /// The most-recently confirmed hardware address. + mac: MacAddress, + /// When the entry transitioned to `Stale`. + stale_since: Instant, + /// When a solicitation was last sent (if any) while in this state. + solicited_at: Option, + }, +} + +impl NeighborState { + /// Creates a new `Incomplete` entry representing a just-sent solicitation. + #[inline] + pub fn incomplete(now: Instant) -> Self { + Self::Incomplete { solicited_at: now } + } + + /// Creates a new `Reachable` entry with a known MAC and expiry time. + #[inline] + pub fn reachable(mac: MacAddress, expires_at: Instant) -> Self { + Self::Reachable { mac, expires_at } + } + + /// Creates a new `Stale` entry from a previously-known MAC. + #[inline] + pub fn stale(mac: MacAddress, stale_since: Instant) -> Self { + Self::Stale { + mac, + stale_since, + solicited_at: None, + } + } + + /// Returns the cached MAC address, if one is available. + /// + /// Returns `None` for [`NeighborState::Incomplete`] because no reply has + /// been received yet. + #[inline] + pub fn mac(&self) -> Option { + match self { + Self::Incomplete { .. } => None, + Self::Reachable { mac, .. } | Self::Stale { mac, .. } => Some(*mac), + } + } + + /// Returns `true` if a `Reachable` entry's TTL has expired. + /// + /// `Incomplete` and `Stale` entries are **never** considered expired by + /// this predicate; use [`should_evict`](Self::should_evict) to decide + /// whether to remove those states from the cache. + #[inline] + pub fn is_expired(&self, now: Instant) -> bool { + match self { + Self::Reachable { expires_at, .. } => now >= *expires_at, + _ => false, + } + } + + /// Returns `true` if the entry should be removed from the cache. + /// + /// | State | Evict when | + /// |-------------|---------------------------------------------------| + /// | Incomplete | `now ≥ solicited_at + INCOMPLETE_TIMEOUT` (3 s) | + /// | Reachable | never — use [`is_expired`](Self::is_expired) | + /// | Stale | `now ≥ stale_since + STALE_TIMEOUT` (30 s) | + #[inline] + pub fn should_evict(&self, now: Instant) -> bool { + match self { + Self::Incomplete { solicited_at } => now >= *solicited_at + INCOMPLETE_TIMEOUT, + Self::Reachable { .. } => false, + Self::Stale { stale_since, .. } => now >= *stale_since + STALE_TIMEOUT, + } + } + + /// Returns `true` if a new solicitation should be sent for this entry. + /// + /// | State | Solicit when | + /// |------------|-----------------------------------------------------------------| + /// | Incomplete | `now ≥ solicited_at + SOLICIT_GUARD` (rate-limit retransmits) | + /// | Reachable | never | + /// | Stale | never solicited **or** `now ≥ solicited_at + SOLICIT_GUARD` | + #[inline] + pub fn should_solicit(&self, now: Instant) -> bool { + match self { + Self::Incomplete { solicited_at } => now >= *solicited_at + SOLICIT_GUARD, + Self::Reachable { .. } => false, + Self::Stale { solicited_at, .. } => match solicited_at { + None => true, + Some(t) => now >= *t + SOLICIT_GUARD, + }, + } + } + + /// Records that a solicitation was sent at `now`. + /// + /// Updates `solicited_at` for `Incomplete` and `Stale` states. Has no + /// effect on `Reachable` entries. + #[inline] + pub fn mark_solicited(&mut self, now: Instant) { + match self { + Self::Incomplete { solicited_at } => *solicited_at = now, + Self::Stale { solicited_at, .. } => *solicited_at = Some(now), + Self::Reachable { .. } => {} + } + } +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + + // ----------------------------------------------------------------------- + // Helpers + // ----------------------------------------------------------------------- + + const TEST_MAC: MacAddress = MacAddress::new([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]); + const TEST_MAC2: MacAddress = MacAddress::new([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]); + + fn secs(n: u64) -> Duration { + Duration::from_secs(n) + } + + // ----------------------------------------------------------------------- + // NeighborState::incomplete + // ----------------------------------------------------------------------- + + #[test] + fn incomplete_mac_is_none() { + let now = Instant::now(); + let entry = NeighborState::incomplete(now); + assert!(entry.mac().is_none()); + } + + #[test] + fn incomplete_is_not_expired() { + let now = Instant::now(); + let entry = NeighborState::incomplete(now); + assert!(!entry.is_expired(now)); + assert!(!entry.is_expired(now + secs(100))); + } + + #[test] + fn incomplete_should_not_evict_immediately() { + let now = Instant::now(); + let entry = NeighborState::incomplete(now); + assert!(!entry.should_evict(now)); + // Just before timeout + assert!(!entry.should_evict(now + secs(2))); + } + + #[test] + fn incomplete_should_evict_after_timeout() { + let now = Instant::now(); + let entry = NeighborState::incomplete(now); + assert!(entry.should_evict(now + secs(3))); + assert!(entry.should_evict(now + secs(10))); + } + + #[test] + fn incomplete_should_not_solicit_within_guard() { + let now = Instant::now(); + let entry = NeighborState::incomplete(now); + assert!(!entry.should_solicit(now)); + assert!(!entry.should_solicit(now + Duration::from_millis(500))); + } + + #[test] + fn incomplete_should_solicit_after_guard() { + let now = Instant::now(); + let entry = NeighborState::incomplete(now); + assert!(entry.should_solicit(now + secs(1))); + assert!(entry.should_solicit(now + secs(5))); + } + + #[test] + fn incomplete_mark_solicited_updates_guard() { + let now = Instant::now(); + let mut entry = NeighborState::incomplete(now); + let t1 = now + secs(2); + entry.mark_solicited(t1); + // Guard restarts from t1 + assert!(!entry.should_solicit(t1)); + assert!(entry.should_solicit(t1 + secs(1))); + } + + // ----------------------------------------------------------------------- + // NeighborState::reachable + // ----------------------------------------------------------------------- + + #[test] + fn reachable_mac_is_some() { + let now = Instant::now(); + let entry = NeighborState::reachable(TEST_MAC, now + secs(60)); + assert_eq!(entry.mac(), Some(TEST_MAC)); + } + + #[test] + fn reachable_not_expired_before_ttl() { + let now = Instant::now(); + let entry = NeighborState::reachable(TEST_MAC, now + secs(60)); + assert!(!entry.is_expired(now)); + assert!(!entry.is_expired(now + secs(59))); + } + + #[test] + fn reachable_expired_at_and_after_ttl() { + let now = Instant::now(); + let entry = NeighborState::reachable(TEST_MAC, now + secs(60)); + assert!(entry.is_expired(now + secs(60))); + assert!(entry.is_expired(now + secs(120))); + } + + #[test] + fn reachable_never_evicted() { + let now = Instant::now(); + let entry = NeighborState::reachable(TEST_MAC, now + secs(60)); + assert!(!entry.should_evict(now)); + assert!(!entry.should_evict(now + secs(1000))); + } + + #[test] + fn reachable_never_solicited() { + let now = Instant::now(); + let entry = NeighborState::reachable(TEST_MAC, now + secs(60)); + assert!(!entry.should_solicit(now)); + assert!(!entry.should_solicit(now + secs(1000))); + } + + #[test] + fn reachable_mark_solicited_is_noop() { + let now = Instant::now(); + let mut entry = NeighborState::reachable(TEST_MAC, now + secs(60)); + entry.mark_solicited(now + secs(5)); + // Still reachable, still has the same mac + assert_eq!(entry.mac(), Some(TEST_MAC)); + assert!(!entry.should_solicit(now + secs(1000))); + } + + // ----------------------------------------------------------------------- + // NeighborState::stale + // ----------------------------------------------------------------------- + + #[test] + fn stale_mac_is_some() { + let now = Instant::now(); + let entry = NeighborState::stale(TEST_MAC2, now); + assert_eq!(entry.mac(), Some(TEST_MAC2)); + } + + #[test] + fn stale_is_not_expired() { + let now = Instant::now(); + let entry = NeighborState::stale(TEST_MAC, now); + assert!(!entry.is_expired(now)); + assert!(!entry.is_expired(now + secs(1000))); + } + + #[test] + fn stale_should_not_evict_immediately() { + let now = Instant::now(); + let entry = NeighborState::stale(TEST_MAC, now); + assert!(!entry.should_evict(now)); + assert!(!entry.should_evict(now + secs(29))); + } + + #[test] + fn stale_should_evict_after_timeout() { + let now = Instant::now(); + let entry = NeighborState::stale(TEST_MAC, now); + assert!(entry.should_evict(now + secs(30))); + assert!(entry.should_evict(now + secs(60))); + } + + #[test] + fn stale_should_solicit_immediately_when_never_solicited() { + let now = Instant::now(); + let entry = NeighborState::stale(TEST_MAC, now); + // No solicitation has been sent yet → should solicit right away + assert!(entry.should_solicit(now)); + } + + #[test] + fn stale_should_not_solicit_within_guard_after_solicitation() { + let now = Instant::now(); + let mut entry = NeighborState::stale(TEST_MAC, now); + entry.mark_solicited(now); + assert!(!entry.should_solicit(now)); + assert!(!entry.should_solicit(now + Duration::from_millis(500))); + } + + #[test] + fn stale_should_solicit_after_guard() { + let now = Instant::now(); + let mut entry = NeighborState::stale(TEST_MAC, now); + entry.mark_solicited(now); + assert!(entry.should_solicit(now + secs(1))); + assert!(entry.should_solicit(now + secs(10))); + } + + #[test] + fn stale_mark_solicited_updates_guard() { + let now = Instant::now(); + let mut entry = NeighborState::stale(TEST_MAC, now); + let t1 = now + secs(5); + entry.mark_solicited(t1); + // Guard restarts from t1 + assert!(!entry.should_solicit(t1)); + let t2 = t1 + secs(1); + assert!(entry.should_solicit(t2)); + // Update again + entry.mark_solicited(t2); + assert!(!entry.should_solicit(t2)); + assert!(entry.should_solicit(t2 + secs(1))); + } + + // ----------------------------------------------------------------------- + // State transitions + // ----------------------------------------------------------------------- + + #[test] + fn reachable_to_stale_transition() { + let now = Instant::now(); + let ttl = secs(60); + let reachable = NeighborState::reachable(TEST_MAC, now + ttl); + let mac = reachable.mac().expect("reachable must have a mac"); + + // When TTL expires, caller converts to Stale + let expired_at = now + ttl; + let stale = NeighborState::stale(mac, expired_at); + + assert_eq!(stale.mac(), Some(TEST_MAC)); + assert!(!stale.is_expired(expired_at)); + assert!(stale.should_solicit(expired_at)); // no solicitation yet + } + + #[test] + fn incomplete_to_reachable_transition() { + let now = Instant::now(); + let mut incomplete = NeighborState::incomplete(now); + // Record that we sent a solicitation + incomplete.mark_solicited(now); + + // Reply arrives → transition to Reachable + let reachable = NeighborState::reachable(TEST_MAC, now + secs(60)); + assert_eq!(reachable.mac(), Some(TEST_MAC)); + assert!(!reachable.is_expired(now)); + assert!(!reachable.should_evict(now)); + } + + #[test] + fn stale_to_reachable_on_reply() { + let now = Instant::now(); + let stale = NeighborState::stale(TEST_MAC, now); + + // Updated MAC arrives via NA → transition to Reachable with new MAC + let reachable = NeighborState::reachable(TEST_MAC2, now + secs(60)); + assert_eq!(reachable.mac(), Some(TEST_MAC2)); + assert!(!reachable.is_expired(now)); + + // Old stale entry's mac is still accessible before replacement + assert_eq!(stale.mac(), Some(TEST_MAC)); + } + + // ----------------------------------------------------------------------- + // Constants sanity checks + // ----------------------------------------------------------------------- + + #[test] + fn constants_have_expected_values() { + assert_eq!(INCOMPLETE_TIMEOUT, Duration::from_secs(3)); + assert_eq!(STALE_TIMEOUT, Duration::from_secs(30)); + assert_eq!(SOLICIT_GUARD, Duration::from_secs(1)); + } +} diff --git a/src/net/neighbor/handler.rs b/src/net/neighbor/handler.rs new file mode 100644 index 0000000..268dbc8 --- /dev/null +++ b/src/net/neighbor/handler.rs @@ -0,0 +1,954 @@ +use std::{cell::UnsafeCell, net::IpAddr, sync::mpsc::SyncSender}; + +use coarsetime::{Duration, Instant}; +use getifaddrs::InterfaceFilter; +use rustc_hash::FxHashMap; + +use crate::{ + net::wire::{ + ethernet::MacAddress, + ip::{IpAddress, Ipv4Address, Ipv6Address}, + }, + xdp::{ + error::Result, + frame::{Frame, FrameBuffer}, + }, +}; + +use super::{ + NeighborState, + arp::{handle_arp, resolve_v4}, + ndp::{handle_ndp, resolve_v6}, +}; + +/// A resolved neighbor (IP → MAC) to broadcast to peer queues. +#[derive(Debug, Clone, Copy)] +pub(crate) struct NeighborUpdate { + pub ip: IpAddress, + pub mac: MacAddress, +} + +/// 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: UnsafeCell>, + ttl: Duration, + rx_offload: bool, + tx_offload: bool, + broadcast: Vec>, +} + +impl NeighborHandler { + /// Returns a mutable reference to the inner neighbor table. + /// + /// # Safety + /// + /// Caller must ensure no other references to the table exist. This is + /// trivially satisfied because `NeighborHandler` is only used via + /// `Rc` within a single-threaded `LocalRuntime`. + #[inline(always)] + #[allow(clippy::mut_from_ref)] + fn table(&self) -> &mut FxHashMap { + // SAFETY: single-threaded access guaranteed by Rc + // within LocalRuntime. No concurrent or reentrant callers exist. + unsafe { &mut *self.table.get() } + } + + /// 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: UnsafeCell::new(FxHashMap::default()), + ttl, + rx_offload: false, + tx_offload: false, + broadcast: Vec::new(), + }) + } + + /// 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 broadcast senders for cross-queue neighbor replication. + pub(crate) fn set_broadcast(&mut self, senders: Vec>) { + self.broadcast = senders; + } + + /// 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) { + let table = self.table(); + // First pass: transition expired Reachable → Stale. + for entry in table.values_mut() { + if entry.is_expired(now) + && let NeighborState::Reachable { mac, .. } = *entry + { + *entry = NeighborState::stale(mac, now); + } + } + // Second pass: remove Incomplete and Stale entries past their timeouts. + table.retain(|_, entry| !entry.should_evict(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) { 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)) + } + + /// Directly seed the neighbor cache with an IP-to-MAC mapping. + /// Used in tests and benchmarks to populate the cache without requiring ARP/NDP exchange. + #[doc(hidden)] + pub fn seed_cache(&self, now: Instant, ip: IpAddress, mac: MacAddress) { + self.table() + .insert(ip, NeighborState::reachable(mac, now + self.ttl)); + } + + /// 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, + ); + } + + /// Broadcasts a neighbor update to all peer queues. Drops silently if full. + fn broadcast_update(&self, update: NeighborUpdate) { + for sender in &self.broadcast { + let _ = sender.try_send(update); + } + } + + /// Applies a neighbor update received from a peer queue. + pub(crate) fn apply_update(&self, update: NeighborUpdate, now: Instant) { + self.table().insert( + update.ip, + NeighborState::reachable(update.mac, now + self.ttl), + ); + } + + /// 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(crate) fn handle_arp<'umem>( + &self, + now: Instant, + frame: Frame<'umem>, + rx_return: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) -> Option { + let resolved = handle_arp( + now, + self.table(), + self.local_mac, + &self.local_ipv4, + self.ttl, + frame, + rx_return, + tx_return, + ); + if let Some(update) = resolved { + self.broadcast_update(update); + } + resolved + } + + /// 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(crate) 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>, + ) -> Option { + let resolved = 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, + ); + if let Some(update) = resolved { + self.broadcast_update(update); + } + resolved + } + + /// Look up a neighbor MAC. On cache miss or expired entry, send an + /// ARP request (IPv4) or NDP Neighbor Solicitation (IPv6) and return + /// `None` (caller should drop the packet; TCP retransmit recovers). + /// + /// On Stale hit, returns the MAC optimistically while re-validating. + pub fn lookup_or_resolve<'umem>( + &self, + now: Instant, + addr: &IpAddress, + src_addr: &IpAddress, + free_frames: &mut impl FrameBuffer<'umem>, + rx_return: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) -> Option { + let table = self.table(); + + // Fast path: entry exists. + if let Some(entry) = table.get_mut(addr) { + match *entry { + NeighborState::Reachable { mac, expires_at } => { + if now < expires_at { + return Some(mac); + } + // Transition to Stale, solicit, return MAC optimistically. + *entry = NeighborState::Stale { + mac, + stale_since: now, + solicited_at: Some(now), + }; + self.send_solicitation(now, addr, src_addr, free_frames, rx_return, tx_return); + return Some(mac); + } + NeighborState::Stale { mac, .. } => { + let needs_solicit = entry.should_solicit(now); + if needs_solicit { + entry.mark_solicited(now); + } + if needs_solicit { + self.send_solicitation( + now, + addr, + src_addr, + free_frames, + rx_return, + tx_return, + ); + } + return Some(mac); + } + NeighborState::Incomplete { .. } => { + let needs_solicit = entry.should_solicit(now); + if needs_solicit { + entry.mark_solicited(now); + } + if needs_solicit { + self.send_solicitation( + now, + addr, + src_addr, + free_frames, + rx_return, + tx_return, + ); + } + return None; + } + } + } + + // No entry — insert Incomplete and solicit. + table.insert(*addr, NeighborState::incomplete(now)); + self.send_solicitation(now, addr, src_addr, free_frames, rx_return, tx_return); + None + } + + /// Send an ARP request or NDP NS based on address family. + fn send_solicitation<'umem>( + &self, + _now: Instant, + addr: &IpAddress, + src_addr: &IpAddress, + free_frames: &mut impl FrameBuffer<'umem>, + rx_return: &mut impl FrameBuffer<'umem>, + tx_return: &mut impl FrameBuffer<'umem>, + ) { + let Some(frame) = free_frames.pop() else { + return; + }; + match (src_addr, addr) { + (IpAddress::V4(src), IpAddress::V4(dst)) => { + resolve_v4(self.local_mac, *src, *dst, frame, rx_return, tx_return); + } + (IpAddress::V6(src), IpAddress::V6(dst)) => { + resolve_v6( + self.local_mac, + *src, + *dst, + self.tx_offload, + frame, + rx_return, + tx_return, + ); + } + _ => { + // Mismatched address families — return the frame. + rx_return.push(frame); + } + } + } +} + +#[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()); + + // First evict transitions Reachable → Stale. + let after_ttl = now + TEST_TTL + Duration::from_secs(1); + handler.evict_stale(after_ttl); + + // Entry is now Stale — lookup still returns the MAC (optimistic). + assert!(handler.lookup_v4(after_ttl, &TEST_REMOTE_IP).is_some()); + + // Second evict after STALE_TIMEOUT removes it. + let after_stale = after_ttl + Duration::from_secs(31); + handler.evict_stale(after_stale); + assert!(handler.lookup_v4(after_stale, &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 (transitions to Stale), B is still Reachable. + let t11 = now + Duration::from_secs(11); + handler.evict_stale(t11); + + // A is now Stale (still returns MAC optimistically). + assert_eq!(handler.lookup_v4(t11, &ip_a), Some(mac_a)); + assert_eq!(handler.lookup_v4(t11, &ip_b), Some(mac_b)); + + // After STALE_TIMEOUT, A is fully evicted. + let t42 = t11 + Duration::from_secs(31); + handler.evict_stale(t42); + assert!(handler.lookup_v4(t42, &ip_a).is_none()); + // B is now also expired → Stale (but still returns MAC). + assert_eq!(handler.lookup_v4(t42, &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); + } + + // ----------------------------------------------------------------------- + // lookup_or_resolve + // ----------------------------------------------------------------------- + + #[test] + fn lookup_or_resolve_miss_inserts_incomplete_and_sends_arp() { + let now = Instant::now(); + let handler = new_handler(); + let mut free = BasicFrameBuffer::new(4); + free.push(Frame::new( + 0, + Box::leak(vec![0u8; 128].into_boxed_slice()), + 128, + false, + )); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + + let addr = IpAddress::V4(TEST_REMOTE_IP); + let src = IpAddress::V4(TEST_LOCAL_IP); + let result = handler.lookup_or_resolve(now, &addr, &src, &mut free, &mut rx, &mut tx); + + assert!(result.is_none(), "miss should return None"); + assert_eq!(tx.num_frames(), 1, "ARP request should be sent"); + // Entry should now be Incomplete. + assert!(handler.lookup(now, &addr).is_none()); + } + + #[test] + fn lookup_or_resolve_incomplete_suppresses_solicit_within_guard() { + let now = Instant::now(); + let handler = new_handler(); + let addr = IpAddress::V4(TEST_REMOTE_IP); + let src = IpAddress::V4(TEST_LOCAL_IP); + + // First call: inserts Incomplete + sends ARP. + let mut free = BasicFrameBuffer::new(4); + free.push(Frame::new( + 0, + Box::leak(vec![0u8; 128].into_boxed_slice()), + 128, + false, + )); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + handler.lookup_or_resolve(now, &addr, &src, &mut free, &mut rx, &mut tx); + assert_eq!(tx.num_frames(), 1); + + // Second call within guard: no additional solicitation. + free.push(Frame::new( + 1, + Box::leak(vec![0u8; 128].into_boxed_slice()), + 128, + false, + )); + let mut tx2 = BasicFrameBuffer::new(4); + let mut rx2 = BasicFrameBuffer::new(4); + handler.lookup_or_resolve(now, &addr, &src, &mut free, &mut rx2, &mut tx2); + assert_eq!(tx2.num_frames(), 0, "should not re-solicit within guard"); + } + + #[test] + fn lookup_or_resolve_reachable_returns_mac() { + let now = Instant::now(); + let handler = new_handler(); + let addr = IpAddress::V4(TEST_REMOTE_IP); + handler.seed_cache(now, addr, TEST_REMOTE_MAC); + + let mut free = BasicFrameBuffer::new(4); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + let result = handler.lookup_or_resolve( + now, + &addr, + &IpAddress::V4(TEST_LOCAL_IP), + &mut free, + &mut rx, + &mut tx, + ); + + assert_eq!(result, Some(TEST_REMOTE_MAC)); + assert_eq!(tx.num_frames(), 0, "no solicitation for reachable"); + } + + #[test] + fn lookup_or_resolve_expired_transitions_to_stale_and_solicits() { + let now = Instant::now(); + let handler = new_handler(); + let addr = IpAddress::V4(TEST_REMOTE_IP); + handler.seed_cache(now, addr, TEST_REMOTE_MAC); + + let expired = now + TEST_TTL + Duration::from_secs(1); + let mut free = BasicFrameBuffer::new(4); + free.push(Frame::new( + 0, + Box::leak(vec![0u8; 128].into_boxed_slice()), + 128, + false, + )); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + let result = handler.lookup_or_resolve( + expired, + &addr, + &IpAddress::V4(TEST_LOCAL_IP), + &mut free, + &mut rx, + &mut tx, + ); + + assert_eq!( + result, + Some(TEST_REMOTE_MAC), + "stale returns MAC optimistically" + ); + assert_eq!(tx.num_frames(), 1, "solicitation sent for re-validation"); + } + + #[test] + fn lookup_or_resolve_after_cache_seed_returns_mac() { + let now = Instant::now(); + let handler = new_handler(); + let addr = IpAddress::V4(TEST_REMOTE_IP); + let src = IpAddress::V4(TEST_LOCAL_IP); + + // Create Incomplete entry. + let mut free = BasicFrameBuffer::new(4); + free.push(Frame::new( + 0, + Box::leak(vec![0u8; 128].into_boxed_slice()), + 128, + false, + )); + let mut rx = BasicFrameBuffer::new(4); + let mut tx = BasicFrameBuffer::new(4); + assert!( + handler + .lookup_or_resolve(now, &addr, &src, &mut free, &mut rx, &mut tx) + .is_none() + ); + + // Cache seeded — transitions to Reachable. + handler.seed_cache(now, addr, TEST_REMOTE_MAC); + + // Now lookup_or_resolve should return the MAC. + let mut free2 = BasicFrameBuffer::new(4); + let mut rx2 = BasicFrameBuffer::new(4); + let mut tx2 = BasicFrameBuffer::new(4); + let result = handler.lookup_or_resolve(now, &addr, &src, &mut free2, &mut rx2, &mut tx2); + assert_eq!(result, Some(TEST_REMOTE_MAC)); + } + + #[test] + fn evict_stale_transitions_reachable_to_stale_then_evicts() { + let now = Instant::now(); + let handler = new_handler(); + let addr = IpAddress::V4(TEST_REMOTE_IP); + handler.seed_cache(now, addr, TEST_REMOTE_MAC); + + let after_ttl = now + TEST_TTL + Duration::from_secs(1); + + // First evict: Reachable → Stale (MAC still returned). + handler.evict_stale(after_ttl); + assert_eq!(handler.lookup(after_ttl, &addr), Some(TEST_REMOTE_MAC)); + + // Second evict after STALE_TIMEOUT: evicted. + let after_stale = after_ttl + Duration::from_secs(31); + handler.evict_stale(after_stale); + assert!(handler.lookup(after_stale, &addr).is_none()); + } + + #[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); + } + + #[test] + fn set_offload_changes_flag() { + let mut handler = new_handler(); + handler.set_offload(true, true); + handler.set_offload(false, false); + } + + #[test] + fn set_local_mac_updates_mac() { + let mut handler = new_handler(); + let new_mac = MacAddress { + octets: [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF], + }; + handler.set_local_mac(new_mac); + assert_eq!(handler.local_mac(), new_mac); + } + + #[test] + fn add_local_ipv6_dedup() { + let mut handler = new_handler(); + let addr = Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + handler.add_local_ipv6(addr); + handler.add_local_ipv6(addr); + } + + #[test] + fn lookup_v4_unknown_returns_none() { + let now = Instant::now(); + let handler = new_handler(); + let addr = Ipv4Address::new([192, 168, 99, 99]); + assert!(handler.lookup_v4(now, &addr).is_none()); + } + + #[test] + fn lookup_v6_unknown_returns_none() { + let now = Instant::now(); + let handler = new_handler(); + let addr = Ipv6Address::new([0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 99]); + assert!(handler.lookup_v6(now, &addr).is_none()); + } + + #[test] + fn apply_update_inserts_reachable_entry() { + let now = Instant::now(); + let handler = new_handler(); + let ip = IpAddress::V4(TEST_REMOTE_IP); + + handler.apply_update( + NeighborUpdate { + ip, + mac: TEST_REMOTE_MAC, + }, + now, + ); + + assert_eq!(handler.lookup(now, &ip), Some(TEST_REMOTE_MAC)); + } + + #[test] + fn apply_update_refreshes_existing_entry() { + let now = Instant::now(); + let handler = new_handler(); + let ip = IpAddress::V4(TEST_REMOTE_IP); + + handler.seed_cache(now, ip, TEST_REMOTE_MAC); + + let new_mac = MacAddress::new([0x99, 0x88, 0x77, 0x66, 0x55, 0x44]); + let later = now + Duration::from_secs(30); + handler.apply_update(NeighborUpdate { ip, mac: new_mac }, later); + + assert_eq!(handler.lookup(later, &ip), Some(new_mac)); + } + + #[test] + fn handle_arp_broadcasts_to_peers() { + use std::sync::mpsc; + + let now = Instant::now(); + let mut handler = new_handler(); + + let (tx, rx) = mpsc::sync_channel(16); + handler.set_broadcast(vec![tx]); + + let mut rx_buf = BasicFrameBuffer::new(4); + let mut tx_buf = 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_buf, &mut tx_buf); + + let update = rx.try_recv().expect("should have received broadcast"); + assert_eq!(update.ip, IpAddress::V4(TEST_REMOTE_IP)); + assert_eq!(update.mac, TEST_REMOTE_MAC); + } +} diff --git a/src/net/neighbor/mod.rs b/src/net/neighbor/mod.rs new file mode 100644 index 0000000..2744425 --- /dev/null +++ b/src/net/neighbor/mod.rs @@ -0,0 +1,8 @@ +mod arp; +mod entry; +mod handler; +mod ndp; + +use entry::NeighborState; +pub use handler::NeighborHandler; +pub(crate) use handler::NeighborUpdate; diff --git a/src/net/neighbor/ndp.rs b/src/net/neighbor/ndp.rs new file mode 100644 index 0000000..193b448 --- /dev/null +++ b/src/net/neighbor/ndp.rs @@ -0,0 +1,964 @@ +use coarsetime::{Duration, Instant}; +use rustc_hash::FxHashMap; + +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::NeighborState; +use super::handler::NeighborUpdate; + +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: &mut FxHashMap, + 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>, +) -> Option { + if icmpv6_len < 8 { + rx_return.push(frame); + return None; + } + + 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 None; + } + } + + 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); + None + } + } +} + +fn handle_neighbor_solicitation<'umem>( + now: Instant, + ttl: Duration, + table: &mut FxHashMap, + 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>, +) -> Option { + if icmpv6_len < NDP_MIN_NS_NA_LEN { + rx_return.push(frame); + return None; + } + + 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 ::). + let mut resolved = None; + if !src_addr.is_unspecified() + && let Some(mac) = sender_mac + { + let ip = IpAddress::V6(src_addr); + table.insert(ip, NeighborState::reachable(mac, now + ttl)); + resolved = Some(NeighborUpdate { ip, mac }); + } + + // Check if the target is one of our addresses. + if !local_ipv6.contains(&target_addr) { + rx_return.push(frame); + return resolved; + } + + // 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); + resolved +} + +fn handle_neighbor_advertisement<'umem>( + now: Instant, + ttl: Duration, + table: &mut FxHashMap, + frame: Frame<'umem>, + icmpv6_offset: usize, + icmpv6_len: usize, + rx_return: &mut impl FrameBuffer<'umem>, +) -> Option { + if icmpv6_len < NDP_MIN_NS_NA_LEN { + rx_return.push(frame); + return None; + } + + 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; + let resolved = + if let Some(mac) = parse_ndp_link_layer_option(&frame, options_start, icmpv6_end, 2) { + let ip = IpAddress::V6(target_addr); + table.insert(ip, NeighborState::reachable(mac, now + ttl)); + Some(NeighborUpdate { ip, mac }) + } else { + None + }; + + rx_return.push(frame); + resolved +} + +fn handle_router_advertisement<'umem>( + now: Instant, + ttl: Duration, + table: &mut FxHashMap, + frame: Frame<'umem>, + icmpv6_offset: usize, + icmpv6_len: usize, + rx_return: &mut impl FrameBuffer<'umem>, +) -> Option { + if icmpv6_len < NDP_MIN_RA_LEN { + rx_return.push(frame); + return None; + } + + 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 + let resolved = + if let Some(mac) = parse_ndp_link_layer_option(&frame, options_start, icmpv6_end, 1) { + let ip = IpAddress::V6(src_addr); + table.insert(ip, NeighborState::reachable(mac, now + ttl)); + Some(NeighborUpdate { ip, mac }) + } else { + None + }; + + rx_return.push(frame); + resolved +} + +/// 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..3d171b8 --- /dev/null +++ b/src/net/pmtu.rs @@ -0,0 +1,236 @@ +use coarsetime::{Duration, Instant}; +use rustc_hash::FxHashMap; + +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: FxHashMap, + /// 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: FxHashMap::default(), + 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: FxHashMap::default(), + 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: FxHashMap::default(), + 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(&mut 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(|&(mtu, inserted_at)| { + 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(&mut 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 mut 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 mut 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 mut 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 mut 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 mut 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 mut 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 mut 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 mut 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); + } + + #[test] + fn with_mtu_sets_custom_default() { + let now = Instant::now(); + let cache = PmtuCache::with_mtu(9000); + let addr = IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])); + assert_eq!(cache.get(now, &addr), 9000); + } + + #[test] + fn default_trait_uses_standard_mtu() { + let now = Instant::now(); + let cache = PmtuCache::default(); + let addr = IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])); + assert_eq!(cache.get(now, &addr), 1500); + } + + #[test] + fn evict_stale_mixed_v4_v6() { + let now = Instant::now(); + let mut cache = PmtuCache::with_mtu_and_ttl(1500, Duration::from_secs(60)); + let v4 = IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])); + let v6 = IpAddress::V6(Ipv6Address::new([ + 0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + ])); + + cache.update(now, v4, 1400); + let later = now.add(Duration::from_secs(30)); + cache.update(later, v6, 1280); + + let evict_time = now.add(Duration::from_secs(61)); + cache.evict_stale(evict_time); + assert_eq!(cache.table.len(), 1); + assert_eq!(cache.get(evict_time, &v6), 1280); + assert_eq!(cache.get(evict_time, &v4), 1500); + } +} 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..7f34e15 --- /dev/null +++ b/src/net/socket/queue.rs @@ -0,0 +1,422 @@ +use std::{ + cell::UnsafeCell, + collections::{VecDeque, vec_deque::Drain}, + fmt, + ops::RangeBounds, + rc::Rc, + sync::Arc, + task::Waker, +}; + +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>>, + waker: 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))), + waker: Rc::new(UnsafeCell::new(None)), + 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); + let waker_slot = unsafe { &mut *self.waker.get() }; + if let Some(waker) = waker_slot.take() { + waker.wake(); + } + evicted + } + + /// Register a waker to be called when data is pushed to this queue. + /// Only one waker is stored — re-registering replaces the previous. + #[inline(always)] + pub fn register_waker(&self, waker: &Waker) { + unsafe { *self.waker.get() = Some(waker.clone()) }; + } + + /// 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), + waker: Rc::clone(&self.waker), + 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_queue_wakes_on_push() { + use std::sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }; + use std::task::Wake; + + struct TestWaker { + woken: AtomicBool, + } + impl Wake for TestWaker { + fn wake(self: Arc) { + self.woken.store(true, Ordering::Relaxed); + } + } + + let q = LocalQueue::new(8); + let test_waker = Arc::new(TestWaker { + woken: AtomicBool::new(false), + }); + let waker = Waker::from(test_waker.clone()); + q.register_waker(&waker); + q.push(42); + assert!(test_waker.woken.load(Ordering::Relaxed)); + } + + #[test] + fn local_queue_waker_cleared_after_wake() { + use std::sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }; + use std::task::Wake; + + struct TestWaker { + woken: AtomicBool, + } + impl Wake for TestWaker { + fn wake(self: Arc) { + self.woken.store(true, Ordering::Relaxed); + } + } + + let q = LocalQueue::new(8); + let test_waker = Arc::new(TestWaker { + woken: AtomicBool::new(false), + }); + let waker = Waker::from(test_waker.clone()); + q.register_waker(&waker); + q.push(1); + assert!(test_waker.woken.load(Ordering::Relaxed)); + test_waker.woken.store(false, Ordering::Relaxed); + q.push(2); + assert!(!test_waker.woken.load(Ordering::Relaxed)); + } + + #[test] + fn local_queue_drain_range() { + let queue = LocalQueue::new(8); + for i in 0..5u32 { + queue.push(i); + } + let drained: Vec<_> = queue.drain(1..3).collect(); + assert_eq!(drained, vec![1, 2]); + assert_eq!(queue.len(), 3); + // Remaining items: 0, 3, 4 + assert_eq!(queue.pop(), Some(0)); + assert_eq!(queue.pop(), Some(3)); + assert_eq!(queue.pop(), Some(4)); + assert_eq!(queue.pop(), None); + } + + #[test] + fn local_queue_drain_full_range() { + let queue = LocalQueue::new(4); + for i in 0..4u32 { + queue.push(i); + } + let drained: Vec<_> = queue.drain(..).collect(); + assert_eq!(drained, vec![0, 1, 2, 3]); + assert!(queue.is_empty()); + } + + #[test] + fn local_queue_drain_empty_range() { + let queue = LocalQueue::new(8); + for i in 0..5u32 { + queue.push(i); + } + let drained: Vec<_> = queue.drain(2..2).collect(); + assert!(drained.is_empty()); + assert_eq!(queue.len(), 5); + } + + #[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..342f87b --- /dev/null +++ b/src/net/socket/tcp.rs @@ -0,0 +1,931 @@ +use std::{ + 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_key) => { + let handler = unsafe { &*this.handler.get() }; + let info = handler + .get_by_key(conn_key) + .map(|tcb| (tcb.id, tcb.event_queue.clone())); + + if let Some((conn_id, event_queue)) = info { + Poll::Ready(TcpStream::from_accepted( + conn_key, + 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. + this.accept_queue.register_waker(cx.waker()); + Poll::Pending + } + } + None => { + this.accept_queue.register_waker(cx.waker()); + Poll::Pending + } + } + } +} + +/// A connected TCP stream. +/// +/// Created either by [`TcpListener::accept()`] (passive open) or +/// [`TcpStream::connect()`] (active open). +pub struct TcpStream { + conn_key: usize, + conn_id: ConnectionId, + #[allow(dead_code)] // used in future data transfer phases + event_queue: LocalQueue, + handler: Rc>, + 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 (conn_key, 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_key, + 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 (conn_key, 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_key, + conn_id, + event_queue, + handler: ctx.tcp_handler.clone(), + }) + }) + } + + /// Internal constructor for connections created via accept. + fn from_accepted( + conn_key: usize, + conn_id: ConnectionId, + event_queue: LocalQueue, + handler: Rc>, + ) -> Self { + Self { + conn_key, + conn_id, + event_queue, + handler, + closed: false, + write_closed: false, + } + } + + #[cfg(test)] + pub(crate) fn from_accepted_for_test( + conn_key: usize, + conn_id: ConnectionId, + event_queue: LocalQueue, + handler: Rc>, + ) -> Self { + Self { + conn_key, + conn_id, + event_queue, + handler, + 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_key: self.conn_key, + event_queue: &self.event_queue, + 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_key: self.conn_key, + event_queue: &self.event_queue, + 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_key: self.conn_key, + event_queue: &self.event_queue, + 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_key); + } + + /// 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_key); + } + + /// 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_by_key_mut(self.conn_key) { + 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_by_key(self.conn_key) + .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_by_key_mut(self.conn_key) { + 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_by_key(self.conn_key) + .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_by_key_mut(self.conn_key) { + tcb.linger = linger; + } + } + + /// Returns the current SO_LINGER setting. + pub fn linger(&self) -> Option { + let handler = unsafe { &*self.handler.get() }; + handler.get_by_key(self.conn_key).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_key: usize, + 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) => Poll::Ready(Ok(TcpStream { + conn_key: this.conn_key, + conn_id: this.conn_id, + event_queue: this.event_queue.clone(), + handler: this.handler.clone(), + 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 => { + this.event_queue.register_waker(cx.waker()); + Poll::Pending + } + } + } +} + +/// Future returned by [`TcpStream::write()`]. +pub struct TcpWrite<'stream> { + handler: &'stream Rc>, + conn_key: usize, + event_queue: &'stream LocalQueue, + 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 remaining = &this.data[this.written..]; + if let Some(n) = handler.write_to_send_buffer(this.conn_key, remaining) { + this.written += n; + if this.written == this.data.len() { + Poll::Ready(Ok(this.written)) + } else { + // Need to register waker for when send buffer has space. + if let Some(tcb) = handler.get_by_key_mut(this.conn_key) { + tcb.send_buffer.register_write_waker(cx.waker()); + } + this.event_queue.register_waker(cx.waker()); + Poll::Pending + } + } else { + Poll::Ready(Err(TcpError::NotConnected)) + } + } +} + +/// Future returned by [`TcpStream::read()`]. +pub struct TcpRead<'stream> { + handler: &'stream Rc>, + conn_key: usize, + event_queue: &'stream LocalQueue, + 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() }; + if let Some(tcb) = handler.get_by_key_mut(this.conn_key) { + 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 { + tcb.recv_buffer.register_read_waker(cx.waker()); + this.event_queue.register_waker(cx.waker()); + Poll::Pending + } + } else { + Poll::Ready(Err(TcpError::NotConnected)) + } + } +} + +pub struct TcpSplice<'stream> { + handler: &'stream Rc>, + conn_key: usize, + 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 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() }; + if let Some((n, is_remote_closed)) = handler.splice_buffers(this.conn_key, this.max_len) { + if n > 0 { + Poll::Ready(Ok(n)) + } else if is_remote_closed { + Poll::Ready(Ok(0)) // EOF + } else { + if let Some(tcb) = handler.get_by_key_mut(this.conn_key) { + tcb.recv_buffer.register_read_waker(cx.waker()); + } + this.event_queue.register_waker(cx.waker()); + Poll::Pending + } + } else { + Poll::Ready(Err(TcpError::NotConnected)) + } + } +} + +/// Future returned by [`TcpStream::splice()`]. +#[cfg(test)] +mod tests { + use std::cell::UnsafeCell; + use std::collections::BTreeMap; + use std::rc::Rc; + + use coarsetime::Instant; + + use crate::net::handler::tcp::TcpHandler; + use crate::net::handler::tcp::congestion::CubicState; + use crate::net::handler::tcp::recovery::{FRtoState, PrrState, SackRecovery}; + use crate::net::handler::tcp::ring_buffer::RingBuffer; + use crate::net::handler::tcp::state::TcpState; + use crate::net::handler::tcp::tcb::{ + ConnectionId, DEFAULT_DELAYED_ACK_MS, DEFAULT_RCV_MSS, Tcb, + }; + use crate::net::socket::LocalQueue; + use crate::net::wire::ip::{IpAddress, Ipv4Address}; + + use super::TcpStream; + + /// Build a TcpStream backed by a real handler + TCB for accessor testing. + fn make_test_stream() -> TcpStream { + let local_addr = IpAddress::V4(Ipv4Address { + octets: [10, 0, 0, 1], + }); + let remote_addr = IpAddress::V4(Ipv4Address { + octets: [10, 0, 0, 2], + }); + let conn_id = ConnectionId { + local_addr, + local_port: 4000, + remote_addr, + remote_port: 80, + }; + let event_queue = LocalQueue::new(16); + + let tcb = Tcb { + id: conn_id, + state: TcpState::Established, + from_passive_open: true, + iss: 1000, + snd_una: 1000, + snd_nxt: 1000, + snd_wnd: 65535, + snd_wl1: 0, + snd_wl2: 0, + irs: 2000, + rcv_nxt: 2001, + rcv_wnd: 65535, + snd_mss: DEFAULT_RCV_MSS, + rcv_mss: DEFAULT_RCV_MSS, + eff_snd_mss: DEFAULT_RCV_MSS, + snd_wscale: 0, + rcv_wscale: 0, + wscale_enabled: false, + retransmit_deadline: None, + rto_backoff: 0, + event_queue: event_queue.clone(), + send_buffer: RingBuffer::new(1024), + recv_buffer: RingBuffer::new(1024), + 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, + }; + + let mut handler = TcpHandler::new(false, false); + let key = handler.insert_connection(tcb); + let handler_rc = Rc::new(UnsafeCell::new(handler)); + + TcpStream::from_accepted_for_test(key, conn_id, event_queue, handler_rc) + } + + #[test] + fn local_addr_returns_configured_address() { + let stream = make_test_stream(); + assert_eq!( + stream.local_addr(), + IpAddress::V4(Ipv4Address { + octets: [10, 0, 0, 1] + }) + ); + } + + #[test] + fn local_port_returns_configured_port() { + let stream = make_test_stream(); + assert_eq!(stream.local_port(), 4000); + } + + #[test] + fn remote_addr_returns_configured_address() { + let stream = make_test_stream(); + assert_eq!( + stream.remote_addr(), + IpAddress::V4(Ipv4Address { + octets: [10, 0, 0, 2] + }) + ); + } + + #[test] + fn remote_port_returns_configured_port() { + let stream = make_test_stream(); + assert_eq!(stream.remote_port(), 80); + } + + #[test] + fn conn_id_returns_full_4_tuple() { + let stream = make_test_stream(); + let id = stream.conn_id(); + assert_eq!( + id.local_addr, + IpAddress::V4(Ipv4Address { + octets: [10, 0, 0, 1] + }) + ); + assert_eq!(id.local_port, 4000); + assert_eq!( + id.remote_addr, + IpAddress::V4(Ipv4Address { + octets: [10, 0, 0, 2] + }) + ); + assert_eq!(id.remote_port, 80); + } + + #[test] + fn nodelay_default_is_false() { + let stream = make_test_stream(); + assert!( + !stream.nodelay(), + "default should be Nagle enabled (nodelay=false)" + ); + } + + #[test] + fn set_nodelay_true_round_trip() { + let stream = make_test_stream(); + stream.set_nodelay(true); + assert!(stream.nodelay()); + } + + #[test] + fn set_nodelay_false_after_true() { + let stream = make_test_stream(); + stream.set_nodelay(true); + assert!(stream.nodelay()); + stream.set_nodelay(false); + assert!(!stream.nodelay()); + } + + #[test] + fn keepalive_default_is_false() { + let stream = make_test_stream(); + assert!(!stream.keepalive()); + } + + #[test] + fn set_keepalive_true_round_trip() { + let stream = make_test_stream(); + stream.set_keepalive(true); + assert!(stream.keepalive()); + } + + #[test] + fn set_keepalive_false_after_true() { + let stream = make_test_stream(); + stream.set_keepalive(true); + assert!(stream.keepalive()); + stream.set_keepalive(false); + assert!(!stream.keepalive()); + } + + #[test] + fn linger_default_is_none() { + let stream = make_test_stream(); + assert_eq!(stream.linger(), None); + } + + #[test] + fn set_linger_some_round_trip() { + let stream = make_test_stream(); + stream.set_linger(Some(5000)); + assert_eq!(stream.linger(), Some(5000)); + } + + #[test] + fn set_linger_zero_for_rst_on_close() { + let stream = make_test_stream(); + stream.set_linger(Some(0)); + assert_eq!(stream.linger(), Some(0)); + } + + #[test] + fn close_sets_closed_flag() { + let mut stream = make_test_stream(); + assert!(!stream.closed); + stream.close(); + assert!(stream.closed); + } + + #[test] + fn close_is_idempotent() { + let mut stream = make_test_stream(); + stream.close(); + assert!(stream.closed); + // Second close should not panic. + stream.close(); + assert!(stream.closed); + } + + #[test] + fn shutdown_sets_write_closed_flag() { + let mut stream = make_test_stream(); + assert!(!stream.write_closed); + stream.shutdown(); + assert!(stream.write_closed); + } + + #[test] + fn shutdown_is_idempotent() { + let mut stream = make_test_stream(); + stream.shutdown(); + assert!(stream.write_closed); + // Second shutdown should not panic. + stream.shutdown(); + assert!(stream.write_closed); + } + + #[test] + fn shutdown_then_close() { + let mut stream = make_test_stream(); + stream.shutdown(); + assert!(stream.write_closed); + assert!(!stream.closed); + stream.close(); + assert!(stream.closed); + } + + #[test] + fn close_prevents_shutdown() { + let mut stream = make_test_stream(); + stream.close(); + // shutdown after close is a no-op (early return because closed=true) + stream.shutdown(); + assert!(stream.closed); + // write_closed should still be false since shutdown was a no-op + assert!(!stream.write_closed); + } + + #[test] + fn set_linger_none_clears_previous() { + let stream = make_test_stream(); + stream.set_linger(Some(3000)); + assert_eq!(stream.linger(), Some(3000)); + stream.set_linger(None); + assert_eq!(stream.linger(), None); + } +} diff --git a/src/net/socket/udp.rs b/src/net/socket/udp.rs new file mode 100644 index 0000000..3a3724c --- /dev/null +++ b/src/net/socket/udp.rs @@ -0,0 +1,767 @@ +use std::{ + cell::UnsafeCell, + pin::Pin, + rc::Rc, + task::{Context, Poll}, +}; + +use coarsetime::Instant; + +use crate::{ + net::{ + NeighborHandler, PmtuCache, + checksum::compute_udp_checksum_ip, + fragment::{FragmentWriter, Packet}, + handler::udp::{BindError, ReceivedUdpPacket, UdpHandler}, + wire::{ + ethernet::MacAddress, + ip::{IpAddress, Ipv4, Ipv4Address, Ipv6, 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: unsafe { &*self.pmtu.get() }, + 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: unsafe { &*self.pmtu.get() }, + 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 { + let this = self.get_mut(); + match this.rx_queue.pop() { + Some(packet) => Poll::Ready(packet), + None => { + this.rx_queue.register_waker(cx.waker()); + 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> { + let this = self.get_mut(); + match this.rx_queue.pop() { + Some(packet) => Poll::Ready(Some(packet)), + None => { + this.rx_queue.register_waker(cx.waker()); + 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_or_resolve( + now, + &self.dst_addr, + &self.src_addr, + &mut self.free_frames, + &mut self.rx_return, + &mut self.tx_return, + ) { + Some(mac) => mac, + None => 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_ip::( + &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_ip::( + &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(_) => { + crate::rt::context::register_capacity_waker(cx.waker()); + return Poll::Pending; + } + }, + pkt => pkt, + }; + + if this.tx_return.free_space() < pkt.num_frames() { + this.pkt = pkt; + crate::rt::context::register_capacity_waker(cx.waker()); + 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() { + crate::rt::context::register_capacity_waker(cx.waker()); + 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}, + task::TaskQueue, + }, + 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(UnsafeCell::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, + task_queue: UnsafeCell::new(TaskQueue::new()), + capacity_wakers: UnsafeCell::new(Vec::new()), + }; + { + 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] + fn new_with_ipv6_unspecified() { + with_test_context(|| { + let sock = UdpSocket::new(IpAddress::V6(Ipv6Address::unspecified()), 9000).unwrap(); + assert_eq!(sock.local_addr(), IpAddress::V6(Ipv6Address::unspecified())); + assert_eq!(sock.local_port(), 9000); + }); + } + + #[test] + fn new_with_specific_ipv4() { + with_test_context(|| { + let addr = IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])); + let sock = UdpSocket::new(addr, 12345).unwrap(); + assert_eq!(sock.local_addr(), addr); + assert_eq!(sock.local_port(), 12345); + }); + } + + #[test] + fn new_with_specific_ipv6() { + with_test_context(|| { + let addr = IpAddress::V6(Ipv6Address::new([ + 0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + ])); + let sock = UdpSocket::new(addr, 8080).unwrap(); + assert_eq!(sock.local_addr(), addr); + assert_eq!(sock.local_port(), 8080); + }); + } + + #[test] + fn close_then_rebind_same_port() { + with_test_context(|| { + let addr = IpAddress::V4(Ipv4Address::new([192, 168, 1, 1])); + + // First bind. + let mut sock = UdpSocket::new(addr, 5000).unwrap(); + assert_eq!(sock.local_port(), 5000); + + // Explicitly close — should unbind. + sock.close(); + + // Re-bind the same address:port should succeed after close. + // Need to drop the old socket first to avoid the Drop calling close + // on an already-unbound port (which is fine, unbind is idempotent). + std::mem::forget(sock); + + let sock2 = UdpSocket::new(addr, 5000).unwrap(); + assert_eq!(sock2.local_addr(), addr); + assert_eq!(sock2.local_port(), 5000); + }); + } + + #[test] + fn drop_unbinds_port() { + with_test_context(|| { + let addr = IpAddress::V4(Ipv4Address::new([192, 168, 1, 1])); + + // Bind then drop. + { + let _sock = UdpSocket::new(addr, 6000).unwrap(); + } + + // Should be able to re-bind after drop. + let sock2 = UdpSocket::new(addr, 6000).unwrap(); + assert_eq!(sock2.local_port(), 6000); + }); + } + + #[test] + fn close_is_idempotent() { + with_test_context(|| { + let addr = IpAddress::V4(Ipv4Address::new([192, 168, 1, 1])); + let mut sock = UdpSocket::new(addr, 7000).unwrap(); + + // Close twice should not panic. + sock.close(); + sock.close(); + + // Accessors still return the original values (they are not cleared). + assert_eq!(sock.local_addr(), addr); + assert_eq!(sock.local_port(), 7000); + }); + } + + #[test] + fn multiple_sockets_different_ports() { + with_test_context(|| { + let addr = IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])); + let s1 = UdpSocket::new(addr, 3000).unwrap(); + let s2 = UdpSocket::new(addr, 3001).unwrap(); + let s3 = UdpSocket::new(addr, 3002).unwrap(); + + assert_eq!(s1.local_port(), 3000); + assert_eq!(s2.local_port(), 3001); + assert_eq!(s3.local_port(), 3002); + }); + } + + #[test] + fn multiple_sockets_different_addrs_same_port() { + with_test_context(|| { + let a1 = IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])); + let a2 = IpAddress::V4(Ipv4Address::new([10, 0, 0, 2])); + + let s1 = UdpSocket::new(a1, 4000).unwrap(); + let s2 = UdpSocket::new(a2, 4000).unwrap(); + + assert_eq!(s1.local_addr(), a1); + assert_eq!(s2.local_addr(), a2); + }); + } + + #[test] + fn split_returns_halves() { + with_test_context(|| { + let addr = IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])); + let mut sock = UdpSocket::new(addr, 8000).unwrap(); + + let (_recv_half, _send_half) = sock.split(); + + assert_eq!(sock.local_addr(), addr); + assert_eq!(sock.local_port(), 8000); + }); + } + + #[test] + fn bind_port_zero_wildcard() { + with_test_context(|| { + // Port 0 with wildcard address should be bindable. + let sock = UdpSocket::new(IpAddress::V4(Ipv4Address::unspecified()), 0).unwrap(); + assert_eq!(sock.local_port(), 0); + assert_eq!(sock.local_addr(), IpAddress::V4(Ipv4Address::unspecified())); + }); + } + + #[test] + fn bind_loopback_ipv4() { + with_test_context(|| { + let addr = IpAddress::V4(Ipv4Address::loopback()); + let sock = UdpSocket::new(addr, 11000).unwrap(); + assert_eq!(sock.local_addr(), addr); + assert_eq!(sock.local_port(), 11000); + }); + } + + #[test] + fn bind_loopback_ipv6() { + with_test_context(|| { + let addr = IpAddress::V6(Ipv6Address::loopback()); + let sock = UdpSocket::new(addr, 11001).unwrap(); + assert_eq!(sock.local_addr(), addr); + assert_eq!(sock.local_port(), 11001); + }); + } + + #[test] + fn double_bind_ipv6_returns_address_in_use() { + with_test_context(|| { + let addr = IpAddress::V6(Ipv6Address::loopback()); + let _sock = UdpSocket::new(addr, 5555).unwrap(); + let err = UdpSocket::new(addr, 5555).unwrap_err(); + assert_eq!(err, BindError::AddressInUse); + }); + } + + #[test] + fn split_recv_half_is_separate_from_send_half() { + with_test_context(|| { + let addr = IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])); + let mut sock = UdpSocket::new(addr, 8001).unwrap(); + + // split() should succeed and both halves should be usable. + let (recv_half, _send_half) = sock.split(); + + // RecvHalf should be able to create a RecvFrom future (won't resolve + // since there's no data, but the construction must succeed). + let _recv_future = recv_half.recv_from(); + + // RecvHalf should be able to create a RecvStream. + let _recv_stream = recv_half.recv_stream(); + }); + } + + #[test] + fn multiple_splits_sequential() { + with_test_context(|| { + let addr = IpAddress::V4(Ipv4Address::new([10, 0, 0, 1])); + let mut sock = UdpSocket::new(addr, 8002).unwrap(); + + // First split. + { + let (_recv, _send) = sock.split(); + } + + // Second split after first halves are dropped. + { + let (_recv, _send) = sock.split(); + } + + // Socket should still be valid. + assert_eq!(sock.local_port(), 8002); + }); + } + + #[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..f54a046 --- /dev/null +++ b/src/net/wire/arp.rs @@ -0,0 +1,182 @@ +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 { + 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 { + 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 { + 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); + } + + #[test] + fn arp_frame_as_bytes_length() { + let mut buf = [0u8; ARP_FRAME_LEN]; + let frame = ArpFrame::from_bytes_mut(&mut buf); + assert_eq!(frame.as_bytes().len(), ARP_FRAME_LEN); + } + + #[test] + fn arp_packet_from_bytes_roundtrip() { + let mut buf = [0u8; ARP_FRAME_LEN]; + let frame = ArpFrame::from_bytes_mut(&mut buf); + frame.arp.oper = ArpOperations::Reply; + frame.arp.htype = ArpHardwareTypes::Ethernet; + + let pkt = ArpPacket::from_bytes(&buf); + let oper = pkt.oper; + let htype = pkt.htype; + assert_eq!(oper, ArpOperations::Reply); + assert_eq!(htype, ArpHardwareTypes::Ethernet); + } + + #[test] + fn arp_packet_from_bytes_mut_allows_mutation() { + let mut buf = [0u8; ARP_FRAME_LEN]; + let pkt = ArpPacket::from_bytes_mut(&mut buf); + pkt.oper = ArpOperations::Request; + pkt.spa = Ipv4Address::new([10, 0, 0, 1]); + let oper = pkt.oper; + let spa = pkt.spa; + assert_eq!(oper, ArpOperations::Request); + assert_eq!(spa.octets, [10, 0, 0, 1]); + } + + #[test] + #[should_panic] + fn arp_frame_from_bytes_mut_rejects_short_buffer() { + let mut buf = [0u8; ARP_FRAME_LEN - 1]; + let _ = ArpFrame::from_bytes_mut(&mut buf); + } +} diff --git a/src/net/wire/ethernet.rs b/src/net/wire/ethernet.rs new file mode 100644 index 0000000..b1e3f82 --- /dev/null +++ b/src/net/wire/ethernet.rs @@ -0,0 +1,265 @@ +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 { + 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 { + 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); + } + + #[test] + #[should_panic(expected = "assertion")] + fn from_bytes_rejects_truncated_frame() { + let short = [0u8; 13]; // EthernetFrame needs 14 bytes + let _ = EthernetFrame::from_bytes(&short); + } + + #[test] + fn from_bytes_accepts_minimum_frame() { + let exact = [0u8; 14]; + let frame = EthernetFrame::from_bytes(&exact); + let _ = frame.ether_type; + } + + #[test] + fn mac_address_display() { + let mac = MacAddress::new([0x01, 0x23, 0x45, 0x67, 0x89, 0xAB]); + assert_eq!(format!("{}", mac), "01:23:45:67:89:ab"); + + assert_eq!(format!("{}", MacAddress::zero()), "00:00:00:00:00:00"); + assert_eq!(format!("{}", MacAddress::broadcast()), "ff:ff:ff:ff:ff:ff"); + } + + #[test] + fn ether_type_display() { + assert_eq!(format!("{}", EtherTypes::IPv4), "IPv4"); + assert_eq!(format!("{}", EtherTypes::IPv6), "IPv6"); + assert_eq!(format!("{}", EtherTypes::Arp), "ARP"); + assert_eq!( + format!( + "{}", + EtherType { + octets: [0x00, 0x00] + } + ), + "Unknown" + ); + } + + #[test] + fn ethernet_frame_display() { + let mut data = [0u8; 14]; + let src = MacAddress::new([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]); + let dst = MacAddress::new([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]); + write_ethernet_header(&mut data, dst, src, EtherTypes::IPv4); + let frame = EthernetFrame::from_bytes(&data); + let display = format!("{}", frame); + assert!(display.contains("aa:bb:cc:dd:ee:ff")); + assert!(display.contains("11:22:33:44:55:66")); + assert!(display.contains("IPv4")); + } + + #[test] + fn write_ethernet_header_sets_fields() { + let mut data = [0u8; 14]; + let src = MacAddress::new([0x01, 0x02, 0x03, 0x04, 0x05, 0x06]); + let dst = MacAddress::broadcast(); + write_ethernet_header(&mut data, dst, src, EtherTypes::Arp); + let frame = EthernetFrame::from_bytes(&data); + assert_eq!(frame.dst_mac, MacAddress::broadcast()); + assert_eq!(frame.src_mac, src); + assert_eq!(frame.ether_type, EtherTypes::Arp); + } + + #[test] + fn from_bytes_mut_allows_mutation() { + let mut data = [0u8; 14]; + let frame = EthernetFrame::from_bytes_mut(&mut data); + frame.dst_mac = MacAddress::broadcast(); + frame.ether_type = EtherTypes::IPv6; + let frame = EthernetFrame::from_bytes(&data); + assert_eq!(frame.dst_mac, MacAddress::broadcast()); + assert_eq!(frame.ether_type, EtherTypes::IPv6); + } + + #[test] + #[should_panic(expected = "assertion")] + fn from_bytes_mut_rejects_truncated_frame() { + let mut short = [0u8; 13]; + let _ = EthernetFrame::from_bytes_mut(&mut short); + } +} diff --git a/src/net/wire/icmpv4.rs b/src/net/wire/icmpv4.rs new file mode 100644 index 0000000..7f6f77a --- /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 { + 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 { + 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 { + 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..aa92a7a --- /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 { + 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 { + 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 { + 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..6ab5e60 --- /dev/null +++ b/src/net/wire/ip/addr.rs @@ -0,0 +1,661 @@ +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())); + } + + #[test] + fn ipv4_display() { + assert_eq!(format!("{}", Ipv4Address::loopback()), "127.0.0.1"); + assert_eq!(format!("{}", Ipv4Address::unspecified()), "0.0.0.0"); + assert_eq!(format!("{}", Ipv4Address::broadcast()), "255.255.255.255"); + assert_eq!( + format!("{}", Ipv4Address::new([192, 168, 1, 1])), + "192.168.1.1" + ); + } + + #[test] + fn ipv4_from_str() { + let addr: Ipv4Address = "10.0.0.1".parse().unwrap(); + assert_eq!(addr.octets, [10, 0, 0, 1]); + + let addr: Ipv4Address = "255.255.255.255".parse().unwrap(); + assert_eq!(addr, Ipv4Address::broadcast()); + + assert!("not.an.ip".parse::().is_err()); + assert!("".parse::().is_err()); + } + + #[test] + fn ipv4_std_conversions() { + let std_addr = std::net::Ipv4Addr::new(192, 168, 0, 1); + let our_addr: Ipv4Address = std_addr.into(); + assert_eq!(our_addr.octets, [192, 168, 0, 1]); + + let back: std::net::Ipv4Addr = our_addr.into(); + assert_eq!(back, std_addr); + } + + #[test] + fn ipv6_display() { + assert_eq!(format!("{}", Ipv6Address::loopback()), "::1"); + assert_eq!(format!("{}", Ipv6Address::unspecified()), "::"); + } + + #[test] + fn ipv6_from_str() { + let addr: Ipv6Address = "::1".parse().unwrap(); + assert_eq!(addr, Ipv6Address::loopback()); + + let addr: Ipv6Address = "fe80::1".parse().unwrap(); + assert!(addr.is_link_local()); + + assert!("not-an-ipv6".parse::().is_err()); + } + + #[test] + fn ipv6_std_conversions() { + let std_addr = std::net::Ipv6Addr::LOCALHOST; + let our_addr: Ipv6Address = std_addr.into(); + assert_eq!(our_addr, Ipv6Address::loopback()); + + let back: std::net::Ipv6Addr = our_addr.into(); + assert_eq!(back, std_addr); + } + + #[test] + fn ip_address_display() { + assert_eq!( + format!("{}", IpAddress::V4(Ipv4Address::loopback())), + "127.0.0.1" + ); + assert_eq!(format!("{}", IpAddress::V6(Ipv6Address::loopback())), "::1"); + } + + #[test] + fn ip_address_from_str() { + let addr: IpAddress = "10.0.0.1".parse().unwrap(); + assert_eq!(addr, IpAddress::V4(Ipv4Address::new([10, 0, 0, 1]))); + + let addr: IpAddress = "::1".parse().unwrap(); + assert_eq!(addr, IpAddress::V6(Ipv6Address::loopback())); + + assert!("garbage".parse::().is_err()); + } + + #[test] + fn ip_address_std_conversions() { + let std_v4 = std::net::IpAddr::V4(std::net::Ipv4Addr::new(10, 0, 0, 1)); + let our: IpAddress = std_v4.into(); + assert_eq!(our, IpAddress::V4(Ipv4Address::new([10, 0, 0, 1]))); + let back: std::net::IpAddr = our.into(); + assert_eq!(back, std_v4); + + let std_v6 = std::net::IpAddr::V6(std::net::Ipv6Addr::LOCALHOST); + let our: IpAddress = std_v6.into(); + assert_eq!(our, IpAddress::V6(Ipv6Address::loopback())); + let back: std::net::IpAddr = our.into(); + assert_eq!(back, std_v6); + } + + #[test] + fn ip_address_from_std_ipv4() { + let std_addr = std::net::Ipv4Addr::new(1, 2, 3, 4); + let our: IpAddress = std_addr.into(); + assert_eq!(our, IpAddress::V4(Ipv4Address::new([1, 2, 3, 4]))); + } + + #[test] + fn ip_address_from_std_ipv6() { + let std_addr = std::net::Ipv6Addr::LOCALHOST; + let our: IpAddress = std_addr.into(); + assert_eq!(our, IpAddress::V6(Ipv6Address::loopback())); + } + + #[test] + fn ip_address_is_unspecified() { + assert!(IpAddress::V4(Ipv4Address::unspecified()).is_unspecified()); + assert!(IpAddress::V6(Ipv6Address::unspecified()).is_unspecified()); + assert!(!IpAddress::V4(Ipv4Address::loopback()).is_unspecified()); + assert!(!IpAddress::V6(Ipv6Address::loopback()).is_unspecified()); + } + + #[test] + fn socket_addr_new() { + let sa = SocketAddr::new(IpAddress::V4(Ipv4Address::loopback()), 8080); + assert_eq!(sa.ip, IpAddress::V4(Ipv4Address::loopback())); + assert_eq!(sa.port, 8080); + } + + #[test] + fn socket_addr_display() { + let sa = SocketAddr::new(IpAddress::V4(Ipv4Address::loopback()), 443); + assert_eq!(format!("{}", sa), "127.0.0.1:443"); + + let sa = SocketAddr::new(IpAddress::V6(Ipv6Address::loopback()), 80); + assert_eq!(format!("{}", sa), "::1:80"); + } + + #[test] + fn socket_addr_from_str() { + let sa: SocketAddr = "127.0.0.1:8080".parse().unwrap(); + assert_eq!(sa.ip, IpAddress::V4(Ipv4Address::loopback())); + assert_eq!(sa.port, 8080); + + let sa: SocketAddr = "[::1]:443".parse().unwrap(); + assert_eq!(sa.ip, IpAddress::V6(Ipv6Address::loopback())); + assert_eq!(sa.port, 443); + + assert!("not-a-socket-addr".parse::().is_err()); + } + + #[test] + fn socket_addr_std_conversions() { + let std_sa = std::net::SocketAddr::new( + std::net::IpAddr::V4(std::net::Ipv4Addr::new(10, 0, 0, 1)), + 3000, + ); + let our: SocketAddr = std_sa.into(); + assert_eq!(our.ip, IpAddress::V4(Ipv4Address::new([10, 0, 0, 1]))); + assert_eq!(our.port, 3000); + + let back: std::net::SocketAddr = our.into(); + assert_eq!(back, std_sa); + } +} diff --git a/src/net/wire/ip/mod.rs b/src/net/wire/ip/mod.rs new file mode 100644 index 0000000..758fb91 --- /dev/null +++ b/src/net/wire/ip/mod.rs @@ -0,0 +1,17 @@ +mod addr; +mod proto; +pub mod traits; +mod v4; +mod v6; + +use super::ethernet; + +pub use traits::{IpVersion, Ipv4, Ipv6}; + +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..e7d6377 --- /dev/null +++ b/src/net/wire/ip/proto.rs @@ -0,0 +1,53 @@ +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; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn display_known_protocols() { + assert_eq!(format!("{}", IpProtocol(IpProtocols::Icmp)), "ICMP"); + assert_eq!(format!("{}", IpProtocol(IpProtocols::IcmpV6)), "ICMPv6"); + assert_eq!(format!("{}", IpProtocol(IpProtocols::Udp)), "UDP"); + assert_eq!(format!("{}", IpProtocol(IpProtocols::Tcp)), "TCP"); + } + + #[test] + fn display_unknown_protocol() { + assert_eq!(format!("{}", IpProtocol(255)), "Unknown"); + assert_eq!(format!("{}", IpProtocol(0)), "Unknown"); + } + + #[test] + fn protocol_constants() { + assert_eq!(IpProtocols::Icmp, 1); + assert_eq!(IpProtocols::IcmpV6, 58); + assert_eq!(IpProtocols::Udp, 17); + assert_eq!(IpProtocols::Tcp, 6); + } +} diff --git a/src/net/wire/ip/traits.rs b/src/net/wire/ip/traits.rs new file mode 100644 index 0000000..3e00c22 --- /dev/null +++ b/src/net/wire/ip/traits.rs @@ -0,0 +1,345 @@ +use std::mem::size_of; + +use super::addr::{IpAddress, Ipv4Address, Ipv6Address}; +use super::proto::IpProtocols; +use super::v4::{IPV4_MIN_HEADER_LEN, Ipv4Header}; +use super::v6::IPV6_HEADER_LEN; +use crate::net::checksum::{pseudo_header_sum_v4, pseudo_header_sum_v6}; +use crate::net::wire::ethernet::{EtherType, EtherTypes, EthernetFrame}; + +const ETH_LEN: usize = size_of::(); + +/// Zero-cost abstraction over IPv4 and IPv6 for generic network code. +/// +/// Implementations are zero-sized types that monomorphize away, enabling +/// the compiler to generate specialized code for each IP version without +/// any runtime dispatch overhead. +pub trait IpVersion { + type Address: Copy + Eq + Into; + + const VERSION: u8; + const ETHER_TYPE: EtherType; + const IP_HEADER_LEN: usize; + const VERSION_BYTE: u8; + const NEXT_HEADER_OFFSET: usize; + const TTL_OFFSET: usize; + + fn write_ip_header( + frame: &mut [u8], + src: &Self::Address, + dst: &Self::Address, + payload_len: usize, + ); + fn get_ecn_bits(frame: &[u8], eth_len: usize) -> u8; + /// Set ECN ECT(0) bit in the IP header. + /// IPv4: sets ToS byte to 0x02, then recomputes IPv4 header checksum. + /// IPv6: sets Traffic Class ECT(0) via `ip[1] |= 0x20`. + fn set_ecn_ect(frame: &mut [u8], eth_len: usize); + fn pseudo_header_sum( + src: &Self::Address, + dst: &Self::Address, + protocol: u8, + transport_len: u32, + ) -> u64; +} + +/// Zero-sized type for IPv4 version-specific operations. +pub struct Ipv4; + +/// Zero-sized type for IPv6 version-specific operations. +pub struct Ipv6; + +impl IpVersion for Ipv4 { + type Address = Ipv4Address; + + const VERSION: u8 = 4; + const ETHER_TYPE: EtherType = EtherTypes::IPv4; + const IP_HEADER_LEN: usize = IPV4_MIN_HEADER_LEN; + const VERSION_BYTE: u8 = 0x45; + const NEXT_HEADER_OFFSET: usize = 9; + const TTL_OFFSET: usize = 8; + + #[inline] + fn write_ip_header(frame: &mut [u8], src: &Ipv4Address, dst: &Ipv4Address, payload_len: usize) { + let ip = &mut frame[ETH_LEN..ETH_LEN + IPV4_MIN_HEADER_LEN]; + ip.fill(0); + ip[0] = 0x45; + let total_ip_len = (IPV4_MIN_HEADER_LEN + payload_len) as u16; + 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).into(); + ip[12..16].copy_from_slice(&src_bytes); + let dst_bytes: [u8; 4] = (*dst).into(); + ip[16..20].copy_from_slice(&dst_bytes); + // Compute IPv4 header checksum + let ip_header = Ipv4Header::from_bytes_mut(frame); + ip_header.fill_checksum(); + } + + #[inline] + fn get_ecn_bits(frame: &[u8], eth_len: usize) -> u8 { + frame[eth_len + 1] & 0x03 + } + + #[inline] + fn set_ecn_ect(frame: &mut [u8], eth_len: usize) { + frame[eth_len + 1] = 0x02; + // Recompute IPv4 header checksum since we modified the ToS field. + let ip = Ipv4Header::from_bytes_mut(frame); + ip.fill_checksum(); + } + + #[inline] + fn pseudo_header_sum( + src: &Ipv4Address, + dst: &Ipv4Address, + protocol: u8, + transport_len: u32, + ) -> u64 { + pseudo_header_sum_v4(src, dst, protocol, transport_len as u16) + } +} + +impl IpVersion for Ipv6 { + type Address = Ipv6Address; + + const VERSION: u8 = 6; + const ETHER_TYPE: EtherType = EtherTypes::IPv6; + const IP_HEADER_LEN: usize = IPV6_HEADER_LEN; + const VERSION_BYTE: u8 = 0x60; + const NEXT_HEADER_OFFSET: usize = 6; + const TTL_OFFSET: usize = 7; + + #[inline] + fn write_ip_header(frame: &mut [u8], src: &Ipv6Address, dst: &Ipv6Address, payload_len: usize) { + let ip = &mut frame[ETH_LEN..ETH_LEN + IPV6_HEADER_LEN]; + ip.fill(0); + ip[0] = 0x60; // version=6 + let payload_len_u16 = payload_len as u16; + ip[4..6].copy_from_slice(&payload_len_u16.to_be_bytes()); + ip[6] = IpProtocols::Tcp; // Next Header + ip[7] = 64; // 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); + } + + #[inline] + fn get_ecn_bits(frame: &[u8], eth_len: usize) -> u8 { + (frame[eth_len + 1] >> 4) & 0x03 + } + + #[inline] + fn set_ecn_ect(frame: &mut [u8], eth_len: usize) { + frame[eth_len + 1] |= 0x20; + } + + #[inline] + fn pseudo_header_sum( + src: &Ipv6Address, + dst: &Ipv6Address, + protocol: u8, + transport_len: u32, + ) -> u64 { + pseudo_header_sum_v6(src, dst, protocol, transport_len) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::net::checksum::verify_ipv4_checksum; + + const ETH: usize = size_of::(); + + // --- IPv4 write_ip_header --- + + #[test] + fn ipv4_write_ip_header_fields() { + let src = Ipv4Address::new([192, 168, 1, 10]); + let dst = Ipv4Address::new([10, 0, 0, 1]); + let payload_len = 20; // e.g. TCP header only + let mut frame = vec![0u8; ETH + IPV4_MIN_HEADER_LEN + payload_len]; + + Ipv4::write_ip_header(&mut frame, &src, &dst, payload_len); + + let ip = &frame[ETH..ETH + IPV4_MIN_HEADER_LEN]; + // version=4, IHL=5 + assert_eq!(ip[0], 0x45); + // total length = 20 (header) + 20 (payload) = 40 + assert_eq!(u16::from_be_bytes([ip[2], ip[3]]), 40); + // Don't Fragment flag + assert_eq!(ip[6] & 0x40, 0x40); + // TTL = 64 + assert_eq!(ip[8], 64); + // Protocol = TCP (6) + assert_eq!(ip[9], IpProtocols::Tcp); + // Source address + assert_eq!(&ip[12..16], &[192, 168, 1, 10]); + // Destination address + assert_eq!(&ip[16..20], &[10, 0, 0, 1]); + } + + #[test] + fn ipv4_write_ip_header_valid_checksum() { + let src = Ipv4Address::new([172, 16, 0, 1]); + let dst = Ipv4Address::new([172, 16, 0, 2]); + let mut frame = vec![0u8; ETH + IPV4_MIN_HEADER_LEN + 32]; + + Ipv4::write_ip_header(&mut frame, &src, &dst, 32); + + let ip = &frame[ETH..ETH + IPV4_MIN_HEADER_LEN]; + assert!(verify_ipv4_checksum(ip)); + } + + // --- IPv6 write_ip_header --- + + #[test] + fn ipv6_write_ip_header_fields() { + 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_len = 20; + let mut frame = vec![0u8; ETH + IPV6_HEADER_LEN + payload_len]; + + Ipv6::write_ip_header(&mut frame, &src, &dst, payload_len); + + let ip = &frame[ETH..ETH + IPV6_HEADER_LEN]; + // Version = 6 (high nibble of byte 0) + assert_eq!(ip[0] >> 4, 6); + // Payload length + assert_eq!(u16::from_be_bytes([ip[4], ip[5]]), 20); + // Next Header = TCP (6) + assert_eq!(ip[6], IpProtocols::Tcp); + // Hop Limit = 64 + assert_eq!(ip[7], 64); + // Source address + assert_eq!( + &ip[8..24], + &[0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1] + ); + // Destination address + assert_eq!( + &ip[24..40], + &[0xFE, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2] + ); + } + + // --- IPv4 ECN --- + + #[test] + fn ipv4_get_ecn_bits_zero() { + let src = Ipv4Address::new([10, 0, 0, 1]); + let dst = Ipv4Address::new([10, 0, 0, 2]); + let mut frame = vec![0u8; ETH + IPV4_MIN_HEADER_LEN + 20]; + Ipv4::write_ip_header(&mut frame, &src, &dst, 20); + + // After write_ip_header, ToS/DSCP_ECN byte (ip[1]) is 0 => ECN bits = 0 + assert_eq!(Ipv4::get_ecn_bits(&frame, ETH), 0); + } + + #[test] + fn ipv4_set_ecn_ect_sets_ect0() { + let src = Ipv4Address::new([10, 0, 0, 1]); + let dst = Ipv4Address::new([10, 0, 0, 2]); + let mut frame = vec![0u8; ETH + IPV4_MIN_HEADER_LEN + 20]; + Ipv4::write_ip_header(&mut frame, &src, &dst, 20); + + Ipv4::set_ecn_ect(&mut frame, ETH); + + // ECN bits should now be 0x02 (ECT(0)) + assert_eq!(Ipv4::get_ecn_bits(&frame, ETH), 0x02); + // ToS byte should be exactly 0x02 + assert_eq!(frame[ETH + 1], 0x02); + // Checksum should still be valid after recomputation + let ip = &frame[ETH..ETH + IPV4_MIN_HEADER_LEN]; + assert!(verify_ipv4_checksum(ip)); + } + + // --- IPv6 ECN --- + + #[test] + fn ipv6_get_ecn_bits_zero() { + 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 frame = vec![0u8; ETH + IPV6_HEADER_LEN + 20]; + Ipv6::write_ip_header(&mut frame, &src, &dst, 20); + + // Traffic class is 0 after write => ECN bits = 0 + assert_eq!(Ipv6::get_ecn_bits(&frame, ETH), 0); + } + + #[test] + fn ipv6_set_ecn_ect_sets_ect0() { + 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 frame = vec![0u8; ETH + IPV6_HEADER_LEN + 20]; + Ipv6::write_ip_header(&mut frame, &src, &dst, 20); + + Ipv6::set_ecn_ect(&mut frame, ETH); + + // ECN ECT(0) = bit 5 of byte 1 set => get_ecn_bits extracts (byte1 >> 4) & 0x03 + // byte1 was 0x00, now 0x20 => (0x20 >> 4) & 0x03 = 0x02 + assert_eq!(Ipv6::get_ecn_bits(&frame, ETH), 0x02); + } + + // --- Pseudo-header sums --- + + #[test] + fn ipv4_pseudo_header_sum_nonzero() { + let src = Ipv4Address::new([192, 168, 1, 1]); + let dst = Ipv4Address::new([10, 0, 0, 1]); + let sum = Ipv4::pseudo_header_sum(&src, &dst, IpProtocols::Tcp, 20); + assert!(sum > 0); + } + + #[test] + fn ipv4_pseudo_header_sum_matches_direct() { + let src = Ipv4Address::new([192, 168, 1, 1]); + let dst = Ipv4Address::new([10, 0, 0, 1]); + let sum_trait = Ipv4::pseudo_header_sum(&src, &dst, IpProtocols::Tcp, 100); + let sum_direct = pseudo_header_sum_v4(&src, &dst, IpProtocols::Tcp, 100); + assert_eq!(sum_trait, sum_direct); + } + + #[test] + fn ipv6_pseudo_header_sum_nonzero() { + 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 sum = Ipv6::pseudo_header_sum(&src, &dst, IpProtocols::Tcp, 20); + assert!(sum > 0); + } + + #[test] + fn ipv6_pseudo_header_sum_matches_direct() { + 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 sum_trait = Ipv6::pseudo_header_sum(&src, &dst, IpProtocols::Tcp, 100); + let sum_direct = pseudo_header_sum_v6(&src, &dst, IpProtocols::Tcp, 100); + assert_eq!(sum_trait, sum_direct); + } + + // --- Constants --- + + #[test] + fn ipv4_constants() { + assert_eq!(Ipv4::VERSION, 4); + assert_eq!(Ipv4::ETHER_TYPE, EtherTypes::IPv4); + assert_eq!(Ipv4::IP_HEADER_LEN, 20); + assert_eq!(Ipv4::VERSION_BYTE, 0x45); + assert_eq!(Ipv4::NEXT_HEADER_OFFSET, 9); + assert_eq!(Ipv4::TTL_OFFSET, 8); + } + + #[test] + fn ipv6_constants() { + assert_eq!(Ipv6::VERSION, 6); + assert_eq!(Ipv6::ETHER_TYPE, EtherTypes::IPv6); + assert_eq!(Ipv6::IP_HEADER_LEN, 40); + assert_eq!(Ipv6::VERSION_BYTE, 0x60); + assert_eq!(Ipv6::NEXT_HEADER_OFFSET, 6); + assert_eq!(Ipv6::TTL_OFFSET, 7); + } +} diff --git a/src/net/wire/ip/v4.rs b/src/net/wire/ip/v4.rs new file mode 100644 index 0000000..bb3a9aa --- /dev/null +++ b/src/net/wire/ip/v4.rs @@ -0,0 +1,319 @@ +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 { + 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 { + 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); + } + + #[test] + #[should_panic(expected = "assertion")] + fn from_bytes_rejects_truncated_ipv4() { + let short = [0u8; 33]; // IPV4_MIN_FRAME_LEN is 34 + let _ = Ipv4Header::from_bytes(&short); + } +} diff --git a/src/net/wire/ip/v6.rs b/src/net/wire/ip/v6.rs new file mode 100644 index 0000000..29cc0a4 --- /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 { + 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 { + 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 { + 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 { + 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..8191d3d --- /dev/null +++ b/src/net/wire/ndp.rs @@ -0,0 +1,231 @@ +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 { + 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 { + 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 + ); + } + + #[test] + fn ndp_ns_frame_from_bytes_mut_roundtrip() { + let mut buf = [0u8; NDP_NS_FRAME_LEN]; + let frame = NdpNsFrame::from_bytes_mut(&mut buf); + frame.ns.icmp_type = 135; + frame.ns.code = 0; + let bytes = frame.as_bytes(); + assert_eq!(bytes.len(), NDP_NS_FRAME_LEN); + let ns_offset = size_of::() + IPV6_HEADER_LEN; + assert_eq!(bytes[ns_offset], 135); + } + + #[test] + fn ndp_na_frame_from_bytes_mut_roundtrip() { + let mut buf = [0u8; NDP_NA_FRAME_LEN]; + let frame = NdpNaFrame::from_bytes_mut(&mut buf); + frame.na.icmp_type = 136; + frame.na.flags = [0x60, 0x00, 0x00, 0x00]; + let bytes = frame.as_bytes(); + assert_eq!(bytes.len(), NDP_NA_FRAME_LEN); + let na_offset = size_of::() + IPV6_HEADER_LEN; + assert_eq!(bytes[na_offset], 136); + } + + #[test] + fn ndp_ns_message_as_bytes_length() { + let msg = NdpNsMessage { + icmp_type: 135, + code: 0, + checksum: [0; 2], + reserved: [0; 4], + target: Ipv6Address::new([0; 16]), + opt_type: 1, + opt_len: 1, + opt_mac: MacAddress { octets: [0; 6] }, + }; + assert_eq!(msg.as_bytes().len(), 32); + } + + #[test] + fn ndp_na_message_as_bytes_length() { + let msg = NdpNaMessage { + icmp_type: 136, + code: 0, + checksum: [0; 2], + flags: [0; 4], + target: Ipv6Address::new([0; 16]), + opt_type: 2, + opt_len: 1, + opt_mac: MacAddress { octets: [0; 6] }, + }; + assert_eq!(msg.as_bytes().len(), 32); + } + + #[test] + fn ndp_constants() { + assert_eq!(NDP_MIN_NS_NA_LEN, 24); + assert_eq!(NDP_MIN_RA_LEN, 16); + let octets = ALL_NODES_MULTICAST.octets; + assert_eq!( + octets, + [0xFF, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1] + ); + } + + #[test] + #[should_panic] + fn ndp_ns_frame_from_bytes_mut_rejects_short() { + let mut buf = [0u8; NDP_NS_FRAME_LEN - 1]; + let _ = NdpNsFrame::from_bytes_mut(&mut buf); + } + + #[test] + #[should_panic] + fn ndp_na_frame_from_bytes_mut_rejects_short() { + let mut buf = [0u8; NDP_NA_FRAME_LEN - 1]; + let _ = NdpNaFrame::from_bytes_mut(&mut buf); + } +} diff --git a/src/net/wire/tcp.rs b/src/net/wire/tcp.rs new file mode 100644 index 0000000..7794b4a --- /dev/null +++ b/src/net/wire/tcp.rs @@ -0,0 +1,786 @@ +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 { + 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 { + 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])); + } + + #[test] + #[should_panic(expected = "assertion")] + fn from_bytes_at_rejects_truncated_tcp() { + let short = [0u8; 19]; // TCP_HEADER_LEN is 20 + let _ = unsafe { TcpHeader::from_bytes_at(&short, 0) }; + } + + #[test] + #[should_panic(expected = "assertion")] + fn from_bytes_at_rejects_offset_overflow() { + let buf = [0u8; 40]; + // offset 25 + TCP_HEADER_LEN 20 = 45 > 40 + let _ = unsafe { TcpHeader::from_bytes_at(&buf, 25) }; + } + + #[test] + fn seq_le_equal() { + assert!(seq_le(100, 100)); + } + + #[test] + fn seq_lt_equal_is_false() { + assert!(!seq_lt(100, 100)); + } + + #[test] + fn parse_sack_blocks_empty_options() { + let opts: [u8; 0] = []; + let (blocks, count) = parse_sack_blocks(&opts); + assert_eq!(count, 0); + assert!(blocks[0].is_none()); + } + + #[test] + fn parse_sack_blocks_single_block() { + let mut opts = [0u8; 10]; + opts[0] = options::SACK; + opts[1] = 10; + opts[2..6].copy_from_slice(&500u32.to_be_bytes()); + opts[6..10].copy_from_slice(&600u32.to_be_bytes()); + let (blocks, count) = parse_sack_blocks(&opts); + assert_eq!(count, 1); + assert_eq!(blocks[0], Some((500, 600))); + assert!(blocks[1].is_none()); + } +} diff --git a/src/net/wire/udp.rs b/src/net/wire/udp.rs new file mode 100644 index 0000000..7e88a9c --- /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 { + 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 { + 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/netlink/ethtool.rs b/src/netlink/ethtool.rs new file mode 100644 index 0000000..7ca5ced --- /dev/null +++ b/src/netlink/ethtool.rs @@ -0,0 +1,379 @@ +use std::io::Cursor; + +use neli::{ + FromBytesWithInput, Size, + consts::{ + genl::{Cmd, NlAttrType}, + nl::NlmF, + socket::NlFamily, + }, + genl::{AttrTypeBuilder, Genlmsghdr, GenlmsghdrBuilder, NlattrBuilder}, + neli_enum, + nl::NlPayload, + router::synchronous::NlRouter, + types::{Buffer, GenlBuffer}, + utils::Groups, +}; + +use crate::xdp::error::{self, Error}; + +/// Ethtool generic netlink command IDs (from linux/ethtool_netlink.h). +#[neli_enum(serialized_type = "u8")] +pub enum EthtoolCmd { + FeaturesGet = 11, + ChannelsGet = 17, +} +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 {} + +/// Ethtool CHANNELS request/reply attributes (from linux/ethtool_netlink.h). +#[neli_enum(serialized_type = "u16")] +pub enum EthtoolAttrChannels { + Unspec = 0, + Header = 1, + RxMax = 2, + TxMax = 3, + OtherMax = 4, + CombinedMax = 5, + RxCount = 6, + TxCount = 7, + OtherCount = 8, + CombinedCount = 9, +} +impl NlAttrType for EthtoolAttrChannels {} + +/// 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. +pub fn get_checksum_offload(if_index: i32) -> error::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)) +} + +/// Queries the ethtool generic netlink interface to determine the number of +/// combined RX/TX queues for the given network interface. +/// +/// Returns the combined channel count. If the query fails (e.g., the driver +/// does not support it, or the interface is virtual), returns `Ok(1)` as a +/// safe default. +pub fn get_queue_count(if_index: i32) -> error::Result { + // Use neli only for family resolution, then raw socket I/O for the + // CHANNELS_GET request/response. neli's typed attribute deserialization + // misparses responses that mix nested and flat attributes at the same level. + let (router, _) = match NlRouter::connect(NlFamily::Generic, None, Groups::empty()) { + Ok(r) => r, + Err(_) => return Ok(1), + }; + + let family_id: u16 = match router.resolve_genl_family("ethtool") { + Ok(id) => id, + Err(_) => return Ok(1), + }; + + // Build the request manually as raw bytes. + // nlattr: DevIndex (type=1, nested=false, len=8, payload=if_index) + let dev_index_attr = { + let mut buf = Vec::new(); + buf.extend_from_slice(&8u16.to_ne_bytes()); // nla_len + buf.extend_from_slice(&1u16.to_ne_bytes()); // nla_type = ETHTOOL_A_HEADER_DEV_INDEX + buf.extend_from_slice(&(if_index as u32).to_ne_bytes()); + buf + }; + + // nlattr: Header (type=1|NLA_F_NESTED, len=4+dev_index_attr.len()) + let header_attr = { + let nla_len = (4 + dev_index_attr.len()) as u16; + let mut buf = Vec::new(); + buf.extend_from_slice(&nla_len.to_ne_bytes()); + buf.extend_from_slice(&(1u16 | 0x8000).to_ne_bytes()); // type=1, NLA_F_NESTED + buf.extend_from_slice(&dev_index_attr); + // NLA padding + while buf.len() % 4 != 0 { + buf.push(0); + } + buf + }; + + // genlmsghdr: cmd=17 (CHANNELS_GET), version=1, reserved=0 + let genlhdr = [17u8, 1, 0, 0]; + + // nlmsghdr + let total_len = 16 + genlhdr.len() + header_attr.len(); + let mut msg = Vec::with_capacity(total_len); + msg.extend_from_slice(&(total_len as u32).to_ne_bytes()); // nlmsg_len + msg.extend_from_slice(&family_id.to_ne_bytes()); // nlmsg_type + msg.extend_from_slice(&1u16.to_ne_bytes()); // nlmsg_flags = NLM_F_REQUEST + msg.extend_from_slice(&0u32.to_ne_bytes()); // nlmsg_seq + msg.extend_from_slice(&0u32.to_ne_bytes()); // nlmsg_pid + msg.extend_from_slice(&genlhdr); + msg.extend_from_slice(&header_attr); + + // Send via raw netlink socket. + let sock = { + let fd = unsafe { + libc::socket( + libc::AF_NETLINK, + libc::SOCK_RAW | libc::SOCK_CLOEXEC, + 16, // NETLINK_GENERIC + ) + }; + if fd < 0 { + return Ok(1); + } + let addr: libc::sockaddr_nl = unsafe { std::mem::zeroed() }; + let mut addr = addr; + addr.nl_family = libc::AF_NETLINK as u16; + let ret = unsafe { + libc::bind( + fd, + &addr as *const _ as *const libc::sockaddr, + std::mem::size_of::() as u32, + ) + }; + if ret < 0 { + unsafe { libc::close(fd) }; + return Ok(1); + } + fd + }; + + let sent = unsafe { libc::send(sock, msg.as_ptr() as *const _, msg.len(), 0) }; + if sent < 0 { + unsafe { libc::close(sock) }; + return Ok(1); + } + + let mut recv_buf = [0u8; 4096]; + let received = unsafe { libc::recv(sock, recv_buf.as_mut_ptr() as *mut _, recv_buf.len(), 0) }; + unsafe { libc::close(sock) }; + + if received < 20 { + return Ok(1); + } + let received = received as usize; + + // Parse the response: skip nlmsghdr (16 bytes) + genlmsghdr (4 bytes). + let raw = &recv_buf[20..received]; + + let mut combined_count: u32 = 0; + let mut rx_count: u32 = 0; + let mut tx_count: u32 = 0; + + let mut off = 0usize; + while off + 4 <= raw.len() { + let nla_len = u16::from_ne_bytes([raw[off], raw[off + 1]]) as usize; + let nla_type = u16::from_ne_bytes([raw[off + 2], raw[off + 3]]); + if nla_len < 4 { + break; + } + let attr_type = nla_type & 0x3FFF; + // Only read non-nested u32 attrs (nla_len == 8 = 4-byte header + 4-byte payload). + if nla_len == 8 && off + 8 <= raw.len() { + let val = u32::from_ne_bytes(raw[off + 4..off + 8].try_into().unwrap()); + // 0xFFFFFFFF means "not applicable" in ethtool (displayed as "n/a"). + if val != u32::MAX { + match attr_type { + 9 => combined_count = val, // CombinedCount + 6 => rx_count = val, // RxCount + 7 => tx_count = val, // TxCount + _ => {} + } + } + } + // Advance by NLA-aligned length. + off += (nla_len + 3) & !3; + } + // Prefer combined count; fall back to rx or tx count; default to 1. + let count = if combined_count > 0 { + combined_count + } else if rx_count > 0 { + rx_count + } else if tx_count > 0 { + tx_count + } else { + 1 + }; + + Ok(count) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn get_queue_count_on_loopback() { + let count = get_queue_count(1).expect("query should not error"); + assert!(count >= 1, "loopback should have at least 1 combined queue"); + } + + #[test] + fn get_queue_count_on_eth0() { + // eth0 on this machine has 1 combined queue per `ethtool -l eth0`. + let idx = unsafe { libc::if_nametoindex(c"eth0".as_ptr() as *const _) }; + if idx == 0 { + return; // eth0 doesn't exist, skip. + } + let count = get_queue_count(idx as i32).expect("query should not error"); + assert!(count >= 1, "eth0 should have at least 1 combined queue"); + } + + #[test] + fn get_queue_count_on_invalid_interface() { + let count = get_queue_count(999999).expect("query should not error"); + assert_eq!(count, 1, "should default to 1 for invalid interface"); + } +} diff --git a/src/netlink/ifinfo.rs b/src/netlink/ifinfo.rs new file mode 100644 index 0000000..7cae86b --- /dev/null +++ b/src/netlink/ifinfo.rs @@ -0,0 +1,47 @@ +use neli::{ + consts::{ + nl::NlmF, + rtnl::{Ifla, RtAddrFamily, Rtm}, + socket::NlFamily, + }, + nl::NlPayload, + router::synchronous::NlRouter, + rtnl::{Ifinfomsg, IfinfomsgBuilder}, + utils::Groups, +}; + +use crate::xdp::error::{self, Error}; + +pub fn get_mtu(if_index: i32) -> error::Result { + let (rtnl, _) = NlRouter::connect(NlFamily::Route, None, Groups::empty()) + .map_err(|e| Error::GetMtu(e.to_string()))?; + + let ifinfomsg = IfinfomsgBuilder::default() + .ifi_family(RtAddrFamily::Netlink) + .ifi_index(if_index) + .build() + .map_err(|e| Error::GetMtu(e.to_string()))?; + + let recv = rtnl + .send::<_, _, Rtm, Ifinfomsg>( + Rtm::Getlink, + NlmF::DUMP_FILTERED | NlmF::REQUEST, + NlPayload::Payload(ifinfomsg), + ) + .map_err(|e| Error::GetMtu(e.to_string()))?; + + for response in recv { + let mut response = response.map_err(|e| Error::GetMtu(e.to_string()))?; + if let Some(payload) = response.get_payload() { + return payload + .rtattrs() + .get_attr_handle() + .get_attr_payload_as::(Ifla::Mtu) + .map_err(|e| Error::GetMtu(e.to_string())); + } + if let Some(err) = response.get_err() { + return Err(Error::GetMtu(err.to_string())); + } + } + Err(Error::GetMtu(format!("Interface {} not found", if_index))) +} diff --git a/src/netlink/mod.rs b/src/netlink/mod.rs new file mode 100644 index 0000000..019f4df --- /dev/null +++ b/src/netlink/mod.rs @@ -0,0 +1,6 @@ +mod ethtool; +mod ifinfo; + +pub use ethtool::get_checksum_offload; +pub use ethtool::get_queue_count; +pub use ifinfo::*; diff --git a/src/rt/affinity.rs b/src/rt/affinity.rs new file mode 100644 index 0000000..0b792f7 --- /dev/null +++ b/src/rt/affinity.rs @@ -0,0 +1,25 @@ +pub fn pin_core(queue: u32) { + let core_ids = core_affinity::get_core_ids().unwrap(); + let index = queue as usize % core_ids.len(); + let core_id = core_ids[index]; + + core_affinity::set_for_current(core_id); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn pin_core_queue_zero() { + // Should not panic — queue 0 always exists. + pin_core(0); + } + + #[test] + fn pin_core_wraps_on_overflow() { + // Should not panic even if queue_id exceeds core count. + let num_cores = core_affinity::get_core_ids().unwrap().len() as u32; + pin_core(num_cores + 1); + } +} diff --git a/src/rt/context.rs b/src/rt/context.rs new file mode 100644 index 0000000..1035ee2 --- /dev/null +++ b/src/rt/context.rs @@ -0,0 +1,184 @@ +use std::{ + cell::{Cell, UnsafeCell}, + rc::Rc, + task::Waker, +}; + +use crate::{ + net::{ + handler::{tcp::TcpHandler, udp::UdpHandler}, + {NeighborHandler, PmtuCache}, + }, + rt::task::TaskQueue, + 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| { + let ptr = c.get(); + if !ptr.is_null() { + // SAFETY: We created this pointer via Box::into_raw in new(). + // The lifetime is erased but still valid — we drop before the + // UMEM/socket that the context references. + unsafe { drop(Box::from_raw(ptr as *mut RuntimeContext<'_>)) }; + } + 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, + /// Task queue for spawned tasks. + pub task_queue: UnsafeCell, + /// Wakers for futures blocked on frame/buffer capacity. + pub capacity_wakers: UnsafeCell>, +} + +/// 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) + }) +} + +/// Register a waker to be called when frame capacity is freed. +/// +/// Called by capacity-driven futures (SendTo, Echo, TcpWrite) when +/// they return Pending due to insufficient buffer space. +pub(crate) fn register_capacity_waker(waker: &Waker) { + with_runtime_context(|ctx| { + let wakers = unsafe { &mut *ctx.capacity_wakers.get() }; + wakers.push(waker.clone()); + }); +} + +#[cfg(test)] +mod tests { + use std::{cell::UnsafeCell, rc::Rc}; + + use coarsetime::Duration; + + use crate::{ + net::{ + NeighborHandler, PmtuCache, + handler::{tcp::TcpHandler, udp::UdpHandler}, + }, + rt::task::TaskQueue, + xdp::frame::BasicFrameBuffer, + }; + + use super::*; + + /// Build a minimal `RuntimeContext` suitable for testing. + fn make_test_context<'umem>() -> RuntimeContext<'umem> { + let free_frames = BasicFrameBuffer::new(128).into(); + let tx_return = BasicFrameBuffer::new(128).into(); + let rx_return = BasicFrameBuffer::new(128).into(); + RuntimeContext { + free_frames, + tx_return, + rx_return, + pmtu: Rc::new(UnsafeCell::new(PmtuCache::new())), + neighbor_handler: Rc::new( + NeighborHandler::new("test0", Duration::from_secs(60)).unwrap(), + ), + udp_handler: Rc::new(UnsafeCell::new(UdpHandler::new(256, false))), + tcp_handler: Rc::new(UnsafeCell::new(TcpHandler::new(false, false))), + tx_offload: false, + task_queue: UnsafeCell::new(TaskQueue::new()), + capacity_wakers: UnsafeCell::new(Vec::new()), + } + } + + #[test] + #[should_panic] + fn with_runtime_context_panics_outside_runtime() { + with_runtime_context(|_ctx| {}); + } + + #[test] + fn context_drop_guard_installs_and_clears_context() { + // Before the guard: accessing context must panic. + let result = std::panic::catch_unwind(|| { + with_runtime_context(|_ctx| {}); + }); + assert!(result.is_err(), "expected panic before guard is installed"); + + // Inside the guard: with_runtime_context succeeds. + { + let ctx = make_test_context(); + let _guard = ContextDropGuard::new(ctx); + // Should not panic. + with_runtime_context(|ctx| { + // Sanity check: tx_offload is the value we set. + assert!(!ctx.tx_offload); + }); + } + + // After the guard is dropped: context pointer is cleared again. + let result = std::panic::catch_unwind(|| { + with_runtime_context(|_ctx| {}); + }); + assert!(result.is_err(), "expected panic after guard is dropped"); + } + + #[test] + fn context_drop_guard_does_not_leak() { + let ctx = make_test_context(); + let guard = ContextDropGuard::new(ctx); + drop(guard); + + let result = std::panic::catch_unwind(|| { + with_runtime_context(|_ctx| {}); + }); + assert!(result.is_err(), "expected panic after guard is dropped"); + } +} diff --git a/src/rt/local.rs b/src/rt/local.rs new file mode 100644 index 0000000..9bc8ee9 --- /dev/null +++ b/src/rt/local.rs @@ -0,0 +1,569 @@ +use std::{ + cell::UnsafeCell, + rc::Rc, + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + mpsc::{Receiver, SyncSender}, + }, + task::Context, +}; + +use coarsetime::Duration; +use futures_util::pin_mut; + +use crate::{ + net::{ + NeighborHandler, NeighborUpdate, PmtuCache, + handler::{ + ethernet::EthernetHandler, ipv4::Ipv4Handler, ipv6::Ipv6Handler, tcp::TcpHandler, + udp::UdpHandler, + }, + }, + rt::task::TaskQueue, + xdp::{ + context::{XdpContext, XdpContextBuilder}, + error::Result, + frame::{BasicFrameBuffer, FrameBuffer, SharedFrameBuffer}, + program::AttachMode, + socket::{CopyMode, Socket, SocketBuilder}, + umem::{Umem, UmemBuilder}, + }, +}; + +use super::context::{ContextDropGuard, RuntimeContext}; + +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: Option, + // 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, + // Whether TX checksum offload is enabled on this interface. + tx_offload: bool, + /// Receiver for neighbor updates from peer queues (multi-queue only). + neighbor_rx: Option>, +} + +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(); + 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(UnsafeCell::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: Some(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, + tx_offload, + neighbor_rx: None, + }) + } + + /// Creates a `LocalRuntime` from pre-built components for use by the + /// multi-threaded `Runtime` orchestrator. Does not own an `XdpContext` — + /// the orchestrator retains that on the main thread. + pub(crate) fn new_worker( + if_name: &str, + umem: Umem<'umem>, + socket: Socket<'umem>, + mtu: u32, + rx_offload: bool, + tx_offload: bool, + arp_ttl: Duration, + neighbor_tx: Vec>, + neighbor_rx: Receiver, + ) -> Result { + let mut neighbor_handler = NeighborHandler::new(if_name, arp_ttl)?; + neighbor_handler.set_offload(rx_offload, tx_offload); + neighbor_handler.set_broadcast(neighbor_tx); + let neighbor_handler = Rc::new(neighbor_handler); + let pmtu = Rc::new(UnsafeCell::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: None, + _queue: 0, + 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, + tx_offload, + neighbor_rx: Some(neighbor_rx), + }) + } + + /// Drains `rx_return` completely: fill queue first (ring-limited), overflow + /// to `free_frames`. + /// + /// # Frame Accounting + /// + /// Every frame in `rx_return` moves to exactly one destination: + /// - `fill_queue` via `process_fill_queue`: frame leaves our accounting + /// (kernel RX path owns it). + /// - `free_frames`: frame stays in our accounting (available for TX). + /// + /// The fill queue ring size naturally caps how many frames enter the kernel + /// RX path. Overflow goes to `free_frames` to maintain TX capacity. + /// + /// After return: `rx_return.num_frames() == 0`. + #[inline(always)] + fn recycle_rx_return(&mut self) -> Result<()> { + // Feed fill queue — ring size prevents overfilling. + while self.rx_return.num_frames() > 0 { + if self.umem.process_fill_queue(&mut self.rx_return).is_err() { + break; // Fill ring full + } + } + // Overflow to free_frames — available for TX packet building. + while self.rx_return.num_frames() > 0 { + self.free_frames.push(self.rx_return.pop().unwrap()); + } + // Wake fill queue so kernel processes newly submitted addresses. + self.umem.maybe_wake_fill_queue(self.socket.fd()) + } + + /// Runs the event loop until `exit` is set or `fut` completes. + /// + /// Each iteration: receive frames, dispatch through the protocol stack, + /// poll `fut` (if woken), poll spawned tasks, drive TCP timers, evict + /// stale state, transmit, and wake capacity-blocked futures. + pub fn run(&mut self, exit: Arc, fut: F) -> Result<()> + where + F: Future, + { + use super::waker::MainWaker; + + // 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.tx_offload, + task_queue: UnsafeCell::new(TaskQueue::new()), + capacity_wakers: UnsafeCell::new(Vec::new()), + }); + + // 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_total = self.free_frames.num_frames() as u32; + let mut in_flight_tx: u32 = 0; + + // Main future gets a real waker (initialized to woken for first poll). + let main_waker = MainWaker::new(); + let main_std_waker = main_waker.waker(); + let mut main_cx = Context::from_waker(&main_std_waker); + pin_mut!(fut); + + // Task queue uses a no-op top-level waker — FuturesUnordered manages + // its own per-task wakers internally. We poll it every iteration. + let task_waker = super::waker::task_queue_waker(); + let mut task_cx = Context::from_waker(&task_waker); + + let mut buffer = BasicFrameBuffer::new(self.umem.num_frames()); + let mut now = coarsetime::Instant::now(); + while !exit.load(Ordering::Relaxed) { + // ---- Drain neighbor updates from peer queues ---- + if let Some(ref neighbor_rx) = self.neighbor_rx { + while let Ok(update) = neighbor_rx.try_recv() { + self.neighbor_handler.apply_update(update, now); + } + } + + // ---- Phase 1: Collect TX Completions (non-blocking) ---- + // Frames completed by the kernel since last iteration return to + // rx_return. Runs BEFORE recv to maximize frame availability. + // Note: in_flight_tx correctness depends on the socket/interface + // remaining operational. ENETDOWN would leave it permanently + // inflated, but that is a fatal condition for the event loop. + while let Ok(n) = self.umem.process_completion_queue(&mut self.rx_return) { + debug_assert!( + in_flight_tx >= n, + "completion underflow: in_flight={in_flight_tx} completed={n}" + ); + in_flight_tx -= n; + } + + // ---- Phase 2: Recycle rx_return → fill queue + free_frames ---- + // rx_return contains: TX completions from Phase 1, plus + // handler-returned frames from the PREVIOUS iteration. + self.recycle_rx_return()?; + + // ---- Receive & Protocol Dispatch ---- + match self.socket.recv(&mut buffer) { + Err(_) => {} + Ok(_) => { + // 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 pmtu = unsafe { &mut *self.pmtu.get() }; + let Self { + neighbor_handler, + 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, + ); + } + } + } + + // ---- Poll Main Future (only if woken) ---- + if main_waker.take_woken() && fut.as_mut().poll(&mut main_cx).is_ready() { + return Ok(()); + } + + // ---- Poll Spawned Tasks ---- + crate::rt::context::with_runtime_context(|ctx| { + let tq = unsafe { &mut *ctx.task_queue.get() }; + tq.poll(&mut task_cx); + }); + + // ---- TCP Timers & Send ---- + // + // 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.rx_return, + &mut self.tx_return, + ); + + self.evict_counter = self.evict_counter.wrapping_add(1); + if self.evict_counter & 65535 == 0 { + now = coarsetime::Instant::now(); + + // SAFETY: single-threaded, no reentrant handler calls. + unsafe { &mut *self.udp_handler.get() }.evict_stale( + now, + Duration::from_secs(30), + &mut self.rx_return, + ); + + // 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.rx_return, + &mut self.tx_return, + ); + + self.neighbor_handler.evict_stale(now); + // SAFETY: single-threaded, no reentrant access. + unsafe { &mut *self.pmtu.get() }.evict_stale(now); + } + + // ---- Transmit (non-blocking) ---- + // Note: free_before is captured HERE (before transmit/recycle), matching + // the current code's placement. Capacity wakes detect frames freed by + // the transmit cycle, not by Phase 1+2 completion recycling. + let free_before = self.free_frames.num_frames(); + + while self.tx_return.num_frames() > 0 { + match self.socket.send(&mut self.tx_return) { + Ok(n) => in_flight_tx += n, + Err(_) => { + // TX ring full — kick kernel and try to free completions. + self.socket.maybe_wake()?; + while let Ok(n) = self.umem.process_completion_queue(&mut self.rx_return) { + debug_assert!( + in_flight_tx >= n, + "completion underflow: in_flight={in_flight_tx} completed={n}" + ); + in_flight_tx -= n; + } + self.recycle_rx_return()?; + // Retry once after freeing ring slots. + match self.socket.send(&mut self.tx_return) { + Ok(n) => in_flight_tx += n, + Err(_) => break, // Still full — defer to next iteration. + } + } + } + } + + // Kick the kernel to process the TX ring. send() only writes + // descriptors — the kernel needs a sendto() to start DMA. + // Without this, frames sit in the TX ring and never transmit. + self.socket.maybe_wake()?; + + // ---- Recycle remaining rx_return ---- + // Drains whatever is left in rx_return. If all sends above succeeded, + // this contains handler-returned RX frames from protocol dispatch. + // If the send hit WouldBlock, mid-retry recycle already drained + // those, so this is a no-op in that path. + self.recycle_rx_return()?; + + // ---- Capacity-Driven Wakes ---- + // After frame recycling, wake any futures blocked on capacity. + // Only wake if free_frames actually grew (i.e., outbound capacity was freed). + if self.free_frames.num_frames() > free_before { + main_waker.set_woken(); + crate::rt::context::with_runtime_context(|ctx| { + let wakers = unsafe { &mut *ctx.capacity_wakers.get() }; + for waker in wakers.drain(..) { + waker.wake(); + } + }); + } + + // ---- Frame Accounting Invariant ---- + debug_assert_eq!( + self.rx_return.num_frames(), + 0, + "rx_return must be fully drained" + ); + // Use <= rather than == because frames held by UdpHandler's + // FragmentReader are outside our tracked variables. The deficit + // equals fragments currently awaiting reassembly. This also + // corrects a pre-existing gap in the old == assertion. + let tracked = self.free_frames.num_frames() as u32 + + in_flight_tx + + self.tx_return.num_frames() as u32; + debug_assert!( + tracked <= expected_total, + "Frame leak: tracked={} (free={} in_flight={} tx_pending={}) > expected={}", + tracked, + self.free_frames.num_frames(), + in_flight_tx, + self.tx_return.num_frames(), + expected_total, + ); + } + + Ok(()) + } +} diff --git a/src/rt/mod.rs b/src/rt/mod.rs new file mode 100644 index 0000000..b5b9bae --- /dev/null +++ b/src/rt/mod.rs @@ -0,0 +1,11 @@ +mod affinity; +pub(crate) mod context; +mod local; +mod runtime; +pub(crate) mod task; +pub(crate) mod waker; + +pub use affinity::*; +pub use local::*; +pub use runtime::*; +pub use task::{JoinHandle, spawn}; diff --git a/src/rt/runtime.rs b/src/rt/runtime.rs new file mode 100644 index 0000000..49942fb --- /dev/null +++ b/src/rt/runtime.rs @@ -0,0 +1,470 @@ +use std::{ + ffi::CString, + future::Future, + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + mpsc::{self, Receiver, SyncSender}, + }, +}; + +use coarsetime::Duration; + +use crate::{ + net::NeighborUpdate, + netlink, + rt::{affinity::pin_core, local::LocalRuntime}, + xdp::{ + context::XdpContext, + error::{Error, Result}, + program::AttachMode, + socket::{CopyMode, Socket}, + umem::{Umem, UmemBuilder}, + }, +}; + +const DEFAULT_ARP_TTL: Duration = Duration::from_secs(60); + +/// Determines which queues to bind. +enum QueueSelection { + /// Auto-discover all queues via ethtool. + Auto, + /// Bind exactly these queue IDs. + Explicit(Vec), + /// Auto-discover, but cap at this many (starting from queue 0). + Max(u32), +} + +/// Builder for configuring and constructing a multi-threaded [`Runtime`]. +/// +/// Mirrors the configuration surface of `LocalRuntimeBuilder` with additional +/// queue selection options. All threads receive identical configuration. +pub struct RuntimeBuilder<'name> { + if_name: &'name str, + queue_selection: QueueSelection, + attach_mode: AttachMode, + umem_builder: UmemBuilder, + rx_ring_size: u32, + tx_ring_size: u32, + busy_poll: bool, + busy_poll_batch_size: usize, + busy_poll_timeout_us: i32, + copy_mode: CopyMode, + enable_fragmentation: bool, + arp_ttl: Duration, +} + +impl<'name> RuntimeBuilder<'name> { + /// Creates a new [`RuntimeBuilder`] for the given interface name. + pub fn new(if_name: &'name str) -> Self { + Self { + if_name, + queue_selection: QueueSelection::Auto, + attach_mode: AttachMode::default(), + umem_builder: UmemBuilder::new(), + rx_ring_size: 0, // 0 means use SocketBuilder defaults + tx_ring_size: 0, + busy_poll: false, + busy_poll_batch_size: 32, + busy_poll_timeout_us: 20, + copy_mode: CopyMode::default(), + enable_fragmentation: false, + arp_ttl: DEFAULT_ARP_TTL, + } + } + + /// Explicitly select which queue IDs to bind. + /// Mutually exclusive with `max_queues`. + pub fn queues(mut self, queues: &[u32]) -> Self { + self.queue_selection = QueueSelection::Explicit(queues.to_vec()); + self + } + + /// Auto-discover queues but cap at this count. + /// Mutually exclusive with `queues`. + pub fn max_queues(mut self, max: u32) -> Self { + self.queue_selection = QueueSelection::Max(max); + self + } + + /// Sets the attach mode for the XDP program. + pub fn attach_mode(mut self, mode: AttachMode) -> Self { + self.attach_mode = mode; + self + } + + /// Enables or disables fragmentation support. + pub fn enable_fragmentation(mut self, enable: bool) -> Self { + self.enable_fragmentation = enable; + self + } + + /// Sets the ARP TTL for neighbor entries. + pub fn arp_ttl(mut self, ttl: Duration) -> Self { + self.arp_ttl = ttl; + self + } + + /// Sets the completion ring size for each UMEM. + pub fn completion_ring_size(mut self, size: u32) -> Self { + self.umem_builder = self.umem_builder.completion_ring_size(size); + self + } + + /// Sets the fill ring size for each UMEM. + pub fn fill_ring_size(mut self, size: u32) -> Self { + self.umem_builder = self.umem_builder.fill_ring_size(size); + self + } + + /// Sets the frame size for each UMEM. + pub fn frame_size(mut self, size: usize) -> Self { + self.umem_builder = self.umem_builder.frame_size(size); + self + } + + /// Enables or disables busy polling. + pub fn busy_poll(mut self, enable: bool) -> Self { + self.busy_poll = enable; + self + } + + /// Sets the busy poll batch size. + pub fn busy_poll_batch_size(mut self, size: usize) -> Self { + self.busy_poll_batch_size = size; + self + } + + /// Sets the busy poll timeout in microseconds. + pub fn busy_poll_timeout_us(mut self, timeout: i32) -> Self { + self.busy_poll_timeout_us = timeout; + self + } + + /// Enables or disables huge tables for UMEM allocation. + pub fn huge_tables(mut self, enable: bool) -> Self { + self.umem_builder = self.umem_builder.huge_tables(enable); + self + } + + /// Enables or disables unaligned frame mode. + pub fn unaligned(mut self, enable: bool) -> Self { + self.umem_builder = self.umem_builder.unaligned(enable); + self + } + + /// Sets the RX ring size for each socket. + pub fn rx_ring_size(mut self, size: u32) -> Self { + self.rx_ring_size = size; + self + } + + /// Sets the TX ring size for each socket. + pub fn tx_ring_size(mut self, size: u32) -> Self { + self.tx_ring_size = size; + self + } + + /// Sets the copy mode for each socket. + pub fn copy_mode(mut self, mode: CopyMode) -> Self { + self.copy_mode = mode; + self + } + + /// Resolve interface name to index using libc::if_nametoindex. + fn resolve_if_index(if_name: &str) -> Result { + let c_name = CString::new(if_name).map_err(Error::InterfaceNameToIndex)?; + let index = unsafe { libc::if_nametoindex(c_name.as_ptr()) }; + if index == 0 { + return Err(Error::InterfaceNotFound); + } + Ok(index as i32) + } + + /// Resolve which queue IDs to bind based on the queue selection strategy. + fn resolve_queues(&self) -> Result> { + match &self.queue_selection { + QueueSelection::Explicit(queues) => Ok(queues.clone()), + QueueSelection::Auto => { + let if_index = Self::resolve_if_index(self.if_name)?; + let count = netlink::get_queue_count(if_index)?; + Ok((0..count).collect()) + } + QueueSelection::Max(max) => { + let if_index = Self::resolve_if_index(self.if_name)?; + let count = netlink::get_queue_count(if_index)?; + let capped = count.min(*max); + Ok((0..capped).collect()) + } + } + } + + /// Build the multi-threaded Runtime. + pub fn build(self) -> Result { + let queues = self.resolve_queues()?; + + // Validate all queue IDs are within xsks_map bounds. + for &qid in &queues { + if qid >= 2048 { + return Err(Error::QueueIdOutOfRange(qid)); + } + } + + Ok(Runtime { + if_name: self.if_name.to_string(), + queues, + umem_builder: self.umem_builder, + rx_ring_size: self.rx_ring_size, + tx_ring_size: self.tx_ring_size, + busy_poll: self.busy_poll, + busy_poll_batch_size: self.busy_poll_batch_size, + busy_poll_timeout_us: self.busy_poll_timeout_us, + copy_mode: self.copy_mode, + enable_fragmentation: self.enable_fragmentation, + arp_ttl: self.arp_ttl, + attach_mode: self.attach_mode, + }) + } +} + +/// Multi-threaded runtime that spawns one worker thread per hardware queue. +/// +/// Created via `Runtime::builder("eth0").build()`. The `run` method drives +/// the two-phase lifecycle: setup on the main thread, then spawn workers. +pub struct Runtime { + if_name: String, + queues: Vec, + umem_builder: UmemBuilder, + rx_ring_size: u32, + tx_ring_size: u32, + busy_poll: bool, + busy_poll_batch_size: usize, + busy_poll_timeout_us: i32, + copy_mode: CopyMode, + enable_fragmentation: bool, + arp_ttl: Duration, + attach_mode: AttachMode, +} + +impl Runtime { + /// Returns a builder for constructing a multi-threaded Runtime. + pub fn builder<'name>(if_name: &'name str) -> RuntimeBuilder<'name> { + RuntimeBuilder::new(if_name) + } + + /// Run the multi-threaded event loop. + /// + /// Phase 1 (main thread): Create XdpContext, UMEMs, sockets; register all sockets. + /// Phase 2 (worker threads): Spawn one thread per queue, each running a LocalRuntime. + /// + /// The factory closure is called on each worker thread with the queue ID. + /// Returns when all threads exit, or on the first error (which triggers shutdown). + pub fn run(&self, exit: Arc, factory: F) -> Result<()> + where + F: Fn(u32) -> Fut + Send + Sync + 'static, + Fut: Future + 'static, + { + // ---- Phase 1: Setup (main thread) ---- + let mut ctx = XdpContext::builder(&self.if_name) + .attach_mode(self.attach_mode) + .enable_fragmentation(self.enable_fragmentation) + .build()?; + + let info = ctx.info(); + let mtu = info.mtu; + let rx_offload = info.rx_offload; + let tx_offload = info.tx_offload; + + // Create per-queue resources. + let mut workers: Vec<(u32, Umem<'static>, Socket<'static>)> = Vec::new(); + + for &queue_id in &self.queues { + let umem: Umem<'static> = self.umem_builder.clone().build()?; + + let mut socket_builder = Socket::builder(&self.if_name, queue_id); + if self.rx_ring_size > 0 { + socket_builder = socket_builder.rx_ring_size(self.rx_ring_size); + } + if self.tx_ring_size > 0 { + socket_builder = socket_builder.tx_ring_size(self.tx_ring_size); + } + socket_builder = socket_builder + .busy_poll(self.busy_poll) + .busy_poll_batch_size(self.busy_poll_batch_size) + .busy_poll_timeout_us(self.busy_poll_timeout_us) + .copy_mode(self.copy_mode) + .enable_fragmentation(self.enable_fragmentation); + + let socket = socket_builder.build(&mut ctx, umem.owner().clone())?; + + workers.push((queue_id, umem, socket)); + } + + // ---- Build neighbor broadcast channel mesh ---- + let num_queues = workers.len(); + let mut channels: Vec<(Vec>, Receiver)> = + Vec::with_capacity(num_queues); + + if num_queues > 1 { + let mut receivers: Vec> = Vec::with_capacity(num_queues); + let mut all_senders: Vec> = Vec::with_capacity(num_queues); + for _ in 0..num_queues { + let (tx, rx) = mpsc::sync_channel(256); + all_senders.push(tx); + receivers.push(rx); + } + + // For each queue i, collect senders to all queues j != i. + let mut senders: Vec>> = Vec::with_capacity(num_queues); + for i in 0..num_queues { + let mut queue_senders = Vec::with_capacity(num_queues - 1); + for (j, sender) in all_senders.iter().enumerate() { + if i != j { + queue_senders.push(sender.clone()); + } + } + senders.push(queue_senders); + } + + for (tx_vec, rx) in senders.into_iter().zip(receivers.into_iter()) { + channels.push((tx_vec, rx)); + } + } else { + // Single queue via Runtime — no broadcast needed. + let (_, rx) = mpsc::sync_channel(1); + channels.push((Vec::new(), rx)); + } + + // ---- Phase 2: Run (worker threads) ---- + let factory = Arc::new(factory); + let mut handles = Vec::new(); + + for ((queue_id, umem, socket), (neighbor_tx, neighbor_rx)) in + workers.into_iter().zip(channels.into_iter()) + { + let exit = exit.clone(); + let factory = factory.clone(); + let if_name = self.if_name.clone(); + let arp_ttl = self.arp_ttl; + + let handle = std::thread::Builder::new() + .name(format!("voidnet-q{queue_id}")) + .spawn(move || -> Result<()> { + pin_core(queue_id); + + println!("spawning worker thread for queue {queue_id}"); + let mut rt = LocalRuntime::new_worker( + &if_name, + umem, + socket, + mtu, + rx_offload, + tx_offload, + arp_ttl, + neighbor_tx, + neighbor_rx, + )?; + + let result = rt.run(exit.clone(), factory(queue_id)); + + // First-failure propagation: signal all other threads to exit. + if result.is_err() { + exit.store(true, Ordering::Relaxed); + } + + result + }) + .map_err(|e| Error::Other(format!("failed to spawn thread: {e}")))?; + + handles.push(handle); + } + + // ---- Phase 3: Join ---- + // Keep ctx alive (holds BPF program) until all workers are done. + let _ctx = ctx; + + let mut first_error: Option = None; + + for handle in handles { + match handle.join() { + Ok(Ok(())) => {} + Ok(Err(e)) => { + if first_error.is_none() { + first_error = Some(e); + } + } + Err(panic_payload) => { + if first_error.is_none() { + let msg = if let Some(s) = panic_payload.downcast_ref::<&str>() { + format!("worker thread panicked: {s}") + } else if let Some(s) = panic_payload.downcast_ref::() { + format!("worker thread panicked: {s}") + } else { + "worker thread panicked".to_string() + }; + first_error = Some(Error::Other(msg)); + } + } + } + } + + match first_error { + Some(e) => Err(e), + None => Ok(()), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::xdp::test_utils::TestVethPair; + + /// Test that Runtime can be constructed and run with a single queue on a veth pair. + #[test] + fn runtime_single_queue_veth() { + let veth = TestVethPair::new().expect("failed to create veth pair"); + + let exit = Arc::new(AtomicBool::new(false)); + let exit_clone = exit.clone(); + + // Signal exit after a brief moment so the test doesn't run forever. + std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_millis(100)); + exit_clone.store(true, Ordering::Relaxed); + }); + + let rt = Runtime::builder(veth.outer_name()) + .queues(&[0]) + .build() + .expect("failed to build runtime"); + + let result = rt.run(exit, |_queue_id| async { + // No-op: just confirm the event loop runs and exits cleanly. + }); + + assert!( + result.is_ok(), + "runtime should exit cleanly: {:?}", + result.err() + ); + } + + /// Test that explicit queue selection works at build time. + #[test] + fn runtime_builder_explicit_queues() { + let veth = TestVethPair::new().expect("failed to create veth pair"); + + let rt = Runtime::builder(veth.outer_name()).queues(&[0]).build(); + + assert!(rt.is_ok(), "should build with explicit queue 0"); + } + + /// Test that queue ID validation rejects out-of-range values. + #[test] + fn runtime_builder_rejects_out_of_range_queue() { + let result = Runtime::builder("lo").queues(&[3000]).build(); + + assert!(result.is_err(), "should reject queue ID >= 2048"); + } +} diff --git a/src/rt/task.rs b/src/rt/task.rs new file mode 100644 index 0000000..8d27a7b --- /dev/null +++ b/src/rt/task.rs @@ -0,0 +1,150 @@ +use std::{ + cell::UnsafeCell, + collections::VecDeque, + future::Future, + pin::Pin, + rc::Rc, + task::{Context, Poll, Waker}, +}; + +use futures_util::stream::{FuturesUnordered, StreamExt}; + +use crate::rt::context::with_runtime_context; + +/// Type-erased task future. +type Task = Pin>>; + +/// Collection of spawned tasks with a staging buffer for newly spawned tasks. +pub(crate) struct TaskQueue { + tasks: FuturesUnordered, + staging: VecDeque, +} + +impl TaskQueue { + pub fn new() -> Self { + Self { + tasks: FuturesUnordered::new(), + staging: VecDeque::new(), + } + } + + /// Add a task to the staging buffer. + pub fn push(&mut self, task: Task) { + self.staging.push_back(task); + } + + /// Drain staging into FuturesUnordered, then poll all woken tasks. + pub fn poll(&mut self, cx: &mut Context<'_>) { + while let Some(task) = self.staging.pop_front() { + self.tasks.push(task); + } + while let Poll::Ready(Some(())) = self.tasks.poll_next_unpin(cx) { + continue; + } + } +} + +/// Handle to a spawned task. Implements `Future`. +/// +/// Awaiting the handle returns the task's result when it completes. +/// Dropping the handle does NOT cancel the task — the task continues +/// running but its return value is discarded. +pub struct JoinHandle { + result: Rc>>, + waker: Rc>>, +} + +impl Future for JoinHandle { + type Output = T; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + // SAFETY: single-threaded, no concurrent access. + let slot = unsafe { &mut *self.result.get() }; + match slot.take() { + Some(value) => Poll::Ready(value), + None => { + unsafe { *self.waker.get() = Some(cx.waker().clone()) }; + Poll::Pending + } + } + } +} + +/// Spawn a future as a task on the local runtime. +/// +/// Returns a [`JoinHandle`] that can be awaited to get the task's return value. +/// +/// # Panics +/// +/// Panics if called outside of `LocalRuntime::run()`. +pub fn spawn(future: F) -> JoinHandle +where + F: Future + 'static, + T: 'static, +{ + let result_slot: Rc>> = Rc::new(UnsafeCell::new(None)); + let waker_slot: Rc>> = Rc::new(UnsafeCell::new(None)); + + let slot = result_slot.clone(); + let wake = waker_slot.clone(); + + let task: Task = Box::pin(async move { + let value = future.await; + // SAFETY: single-threaded, no concurrent access. + unsafe { *slot.get() = Some(value) }; + // Wake the parent task awaiting the JoinHandle. + let waker = unsafe { &mut *wake.get() }; + if let Some(w) = waker.take() { + w.wake(); + } + }); + + with_runtime_context(|ctx| { + // SAFETY: single-threaded, no concurrent access. + let tq = unsafe { &mut *ctx.task_queue.get() }; + tq.push(task); + }); + + JoinHandle { + result: result_slot, + waker: waker_slot, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn task_queue_new_does_not_panic() { + let _tq = TaskQueue::new(); + } + + #[test] + fn task_queue_push_stages_task() { + let mut tq = TaskQueue::new(); + // Staging is empty initially. + assert!(tq.staging.is_empty()); + + // Push a no-op future into the staging buffer. + let task: Task = Box::pin(async {}); + tq.push(task); + + // One item staged. + assert_eq!(tq.staging.len(), 1); + } + + #[test] + fn task_queue_push_multiple_stages_all() { + let mut tq = TaskQueue::new(); + for _ in 0..5 { + tq.push(Box::pin(async {})); + } + assert_eq!(tq.staging.len(), 5); + } + + // poll() is not tested here because it requires a std::task::Context (waker), + // which in turn requires either a real async executor or a manually constructed + // RawWaker. Building one safely is non-trivial and duplicates executor + // infrastructure that doesn't exist in this crate's test helpers. +} diff --git a/src/rt/waker.rs b/src/rt/waker.rs new file mode 100644 index 0000000..aab3780 --- /dev/null +++ b/src/rt/waker.rs @@ -0,0 +1,194 @@ +use std::{ + cell::Cell, + rc::Rc, + task::{RawWaker, RawWakerVTable, Waker}, +}; + +/// Flag-based waker for the main future. +/// +/// When woken, sets an internal flag that the event loop checks +/// before polling the main future. Initialized to `true` so the +/// main future gets its first poll immediately. +#[derive(Clone)] +pub(crate) struct MainWaker { + woken: Rc>, +} + +impl MainWaker { + pub fn new() -> Self { + Self { + woken: Rc::new(Cell::new(true)), + } + } + + /// Check whether the waker has been triggered and reset the flag. + #[inline(always)] + pub fn take_woken(&self) -> bool { + let was_woken = self.woken.get(); + self.woken.set(false); + was_woken + } + + /// Manually set the woken flag (used by event loop for capacity-driven wakes). + #[inline(always)] + pub fn set_woken(&self) { + self.woken.set(true); + } + + /// Build a `std::task::Waker` that sets this flag when woken. + pub fn waker(&self) -> Waker { + let data = Rc::into_raw(self.woken.clone()) as *const (); + let raw = RawWaker::new(data, &MAIN_WAKER_VTABLE); + unsafe { Waker::from_raw(raw) } + } +} + +unsafe fn clone_main_waker(data: *const ()) -> RawWaker { + let rc = unsafe { Rc::from_raw(data as *const Cell) }; + let cloned = rc.clone(); + std::mem::forget(rc); + let ptr = Rc::into_raw(cloned) as *const (); + RawWaker::new(ptr, &MAIN_WAKER_VTABLE) +} + +unsafe fn wake_main(data: *const ()) { + let rc = unsafe { Rc::from_raw(data as *const Cell) }; + rc.set(true); +} + +unsafe fn wake_main_by_ref(data: *const ()) { + let rc = unsafe { Rc::from_raw(data as *const Cell) }; + rc.set(true); + std::mem::forget(rc); +} + +unsafe fn drop_main_waker(data: *const ()) { + drop(unsafe { Rc::from_raw(data as *const Cell) }); +} + +const MAIN_WAKER_VTABLE: RawWakerVTable = RawWakerVTable::new( + clone_main_waker, + wake_main, + wake_main_by_ref, + drop_main_waker, +); + +// ---- No-op waker for TaskQueue's top-level FuturesUnordered poll ---- + +const fn noop_raw_waker() -> RawWaker { + RawWaker::new(std::ptr::null(), &NOOP_WAKER_VTABLE) +} + +unsafe fn noop_clone(_data: *const ()) -> RawWaker { + noop_raw_waker() +} + +unsafe fn noop(_data: *const ()) {} + +const NOOP_WAKER_VTABLE: RawWakerVTable = RawWakerVTable::new(noop_clone, noop, noop, noop); + +/// No-op waker used for the TaskQueue's top-level FuturesUnordered poll. +/// +/// FuturesUnordered manages its own per-task wakers internally. The top-level +/// waker would signal "at least one task is ready to poll" — but we poll the +/// task queue every iteration anyway, so we don't need this notification. +pub(crate) const fn task_queue_waker() -> Waker { + unsafe { Waker::from_raw(noop_raw_waker()) } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn initial_state_is_woken() { + let mw = MainWaker::new(); + assert!(mw.take_woken()); + assert!(!mw.take_woken()); + } + + #[test] + fn waker_sets_flag() { + let mw = MainWaker::new(); + mw.take_woken(); + let waker = mw.waker(); + waker.wake_by_ref(); + assert!(mw.take_woken()); + } + + #[test] + fn set_woken_manual() { + let mw = MainWaker::new(); + mw.take_woken(); + mw.set_woken(); + assert!(mw.take_woken()); + } + + #[test] + fn waker_clone_works() { + let mw = MainWaker::new(); + mw.take_woken(); + let waker = mw.waker(); + let cloned = waker.clone(); + drop(waker); + cloned.wake(); + assert!(mw.take_woken()); + } + + #[test] + fn wake_consumes_waker() { + let mw = MainWaker::new(); + mw.take_woken(); + let waker = mw.waker(); + waker.wake(); + assert!(mw.take_woken()); + } + + #[test] + fn wake_by_ref_does_not_consume() { + let mw = MainWaker::new(); + mw.take_woken(); + let waker = mw.waker(); + waker.wake_by_ref(); + assert!(mw.take_woken()); + drop(waker); + } + + #[test] + fn drop_waker_does_not_panic() { + let mw = MainWaker::new(); + let waker = mw.waker(); + drop(waker); + mw.set_woken(); + assert!(mw.take_woken()); + } + + #[test] + fn multiple_wakers_from_same_main() { + let mw = MainWaker::new(); + mw.take_woken(); + let w1 = mw.waker(); + let w2 = mw.waker(); + w1.wake_by_ref(); + assert!(mw.take_woken()); + w2.wake(); + assert!(mw.take_woken()); + } + + #[test] + fn noop_waker_does_not_panic() { + let waker = task_queue_waker(); + waker.wake_by_ref(); + let cloned = waker.clone(); + cloned.wake(); + } + + #[test] + fn task_queue_waker_clone_roundtrip() { + let waker = task_queue_waker(); + let cloned = waker.clone(); + drop(waker); + cloned.wake_by_ref(); + drop(cloned); + } +} diff --git a/src/xdp/context/ctx.rs b/src/xdp/context/ctx.rs index 8a4675e..69764cb 100644 --- a/src/xdp/context/ctx.rs +++ b/src/xdp/context/ctx.rs @@ -1,14 +1,11 @@ -#[cfg(feature = "tokio")] -use std::{os::fd::RawFd, sync::Arc}; - use crate::xdp::{ - error::Result, + error::{Error, Result}, program::{AttachMode, Map, XdpInfo, XdpProgram}, socket::SocketOwner, }; /// 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 +22,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 +52,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) } } @@ -58,21 +63,13 @@ impl<'name> XdpContextBuilder<'name> { /// # Socket Registration /// /// Sockets are registered via [`Socket::builder`](crate::xdp::socket::Socket::builder), -/// which updates the BPF maps so the kernel can route packets using round-robin -/// distribution across registered sockets. +/// which updates the BPF maps so the kernel can route packets using queue-based +/// routing to registered sockets. pub struct XdpContext { - /// BPF map containing the `num_sockets` counter in the program's `.bss` section. - data_map: Map, - /// BPF map array storing socket file descriptors indexed by socket number. + /// BPF map array storing socket file descriptors indexed by queue ID. xsks_map: Map, - /// Current count of registered sockets; used as the next index in `xsks_map`. - num_sockets: u32, /// The loaded and attached XDP program. program: XdpProgram, - #[cfg(feature = "tokio")] - tokio_fd_factory: crate::xdp::futures::TokioFdFactory, - #[cfg(feature = "smol")] - smol_fd_factory: crate::xdp::futures::SmolFdFactory, } impl XdpContext { @@ -84,32 +81,16 @@ impl XdpContext { fn new(if_name: &str, attach_mode: AttachMode, enable_fragmentation: bool) -> Result { let program = XdpProgram::new(XDP_PROG_DATA, if_name, attach_mode, enable_fragmentation)?; - let data_map = program.find_map(".bss")?; let xsks_map = program.find_map("xsks_map")?; - Ok(Self { - data_map, - xsks_map, - num_sockets: 0, - program, - #[cfg(feature = "tokio")] - tokio_fd_factory: crate::xdp::futures::TokioFdFactory::new(), - #[cfg(feature = "smol")] - smol_fd_factory: crate::xdp::futures::SmolFdFactory::new(), - }) + Ok(Self { xsks_map, program }) } #[cfg(test)] pub fn new_no_init() -> Result { Ok(Self { - data_map: Map::new(std::ptr::null_mut(), unsafe { std::mem::zeroed() }), xsks_map: Map::new(std::ptr::null_mut(), unsafe { std::mem::zeroed() }), - num_sockets: 0, program: XdpProgram::new_no_init()?, - #[cfg(feature = "tokio")] - tokio_fd_factory: crate::xdp::futures::TokioFdFactory::new(), - #[cfg(feature = "smol")] - smol_fd_factory: crate::xdp::futures::SmolFdFactory::new(), }) } @@ -123,47 +104,27 @@ impl XdpContext { self.program.info() } - pub(crate) fn register_socket(&mut self, socket: &mut SocketOwner<'_>) -> Result<()> { - let loc = self.num_sockets; - self.num_sockets += 1; - - // Update our xsks_map with the socket's file descriptor. + pub(crate) fn register_socket( + &mut self, + socket: &mut SocketOwner<'_>, + map_index: u32, + ) -> Result<()> { + if map_index >= 2048 { + return Err(Error::QueueIdOutOfRange(map_index)); + } + // Update xsks_map with the socket's file descriptor at the given map index. // SAFETY: The map was created with u32 keys and i32 (fd) values. - // The `loc` index is valid as it's derived from our internal counter. - unsafe { self.xsks_map.update_elem(&loc, &socket.fd())? }; - - const KEY: u32 = 0; - // Update our num_sockets counter in the XDP program's .bss map. - // SAFETY: The .bss map contains a u32 at key 0 by program design. - unsafe { self.data_map.update_elem(&KEY, &self.num_sockets)? }; - + unsafe { self.xsks_map.update_elem(&map_index, &socket.fd())? }; Ok(()) } - - #[cfg(feature = "tokio")] - pub(crate) fn get_tokio_fd(&self, fd: RawFd) -> Result>> { - self.tokio_fd_factory.get_tokio_fd(fd) - } - - #[cfg(feature = "smol")] - pub(crate) fn get_smol_fd( - &self, - fd: RawFd, - ) -> Result>> { - self.smol_fd_factory.get_smol_fd(fd) - } - - #[cfg(test)] - pub(crate) fn num_sockets(&self) -> u32 { - self.num_sockets - } } #[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() { @@ -176,7 +137,6 @@ mod tests { assert!(ctx.is_ok(), "failed to create XdpContext: {:?}", ctx.err()); let ctx = ctx.unwrap(); - assert_eq!(ctx.num_sockets(), 0); assert_eq!(ctx.attach_mode(), AttachMode::default()); let info = ctx.info(); @@ -224,10 +184,6 @@ mod tests { .build() .expect("context 2"); - // Both contexts should have their own state. - assert_eq!(ctx1.num_sockets(), 0); - assert_eq!(ctx2.num_sockets(), 0); - // Their program IDs should be different. let info1 = ctx1.info(); let info2 = ctx2.info(); @@ -257,11 +213,37 @@ mod tests { .build() .expect("inner context"); - // Both should be valid with separate state. - assert_eq!(ctx_outer.num_sockets(), 0); - assert_eq!(ctx_inner.num_sockets(), 0); - // Their MTUs should match since they're a pair. assert_eq!(ctx_outer.info().mtu, ctx_inner.info().mtu); } + + /// Tests that register_socket places the socket at the queue ID index in xsks_map. + #[test] + fn test_register_socket_uses_queue_id() { + use crate::xdp::socket::Socket; + + let veth = TestVethPair::new().expect("failed to create veth pair"); + let mut ctx = XdpContext::builder(veth.outer_name()) + .attach_mode(AttachMode::default()) + .enable_fragmentation(false) + .build() + .expect("failed to create context"); + + let (umem, _fq, _cq) = crate::xdp::umem::Umem::builder() + .num_frames(16) + .frame_size(4096) + .fill_ring_size(8) + .completion_ring_size(8) + .build() + .expect("failed to create umem") + .split(); + + // Build socket on queue 0 (the only queue veth supports). + let _socket = Socket::builder(veth.outer_name(), 0) + .rx_ring_size(8) + .tx_ring_size(8) + .build(&mut ctx, umem) + .expect("failed to create socket"); + // Verify: no panic, socket was registered at xsks_map[0]. + } } 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..9347e72 100644 --- a/src/xdp/error.rs +++ b/src/xdp/error.rs @@ -59,6 +59,12 @@ pub enum Error { QueryXdpFeatures(Errno), #[error("failed while getting MTU: {0}")] GetMtu(String), + #[error("failed to query checksum offload capabilities: {0}")] + GetChecksumOffload(String), + #[error("failed to query queue count: {0}")] + GetQueueCount(String), + #[error("queue ID {0} exceeds xsks_map max_entries (2048)")] + QueueIdOutOfRange(u32), #[error("fragmentation not supported by the network interface")] FragmentationNotSupported, #[error("zero copy not supported by the network interface")] @@ -71,6 +77,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. @@ -98,3 +108,70 @@ pub fn get_xdp_error_message(err: i32) -> String { let buf = buf[..nul_pos].iter().map(|c| *c as u8).collect::>(); String::from_utf8_lossy(&buf).to_string() } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn display_error_variants() { + let e = Error::InterfaceNotFound; + assert_eq!(e.to_string(), "failed to find specified interface"); + + let e = Error::FragmentationNotSupported; + assert_eq!( + e.to_string(), + "fragmentation not supported by the network interface" + ); + + let e = Error::ZeroCopyNotSupported; + assert_eq!( + e.to_string(), + "zero copy not supported by the network interface" + ); + + let e = Error::ExitRuntime; + assert_eq!(e.to_string(), "Exiting runtime"); + + let e = Error::InvalidFrameSize(3000); + assert!(e.to_string().contains("3000")); + + let e = Error::InvalidFillRingSize(7); + assert!(e.to_string().contains("7")); + + let e = Error::InvalidCompletionRingSize(5); + assert!(e.to_string().contains("5")); + + let e = Error::InvalidAttachMode("bad".to_string()); + assert!(e.to_string().contains("bad")); + + let e = Error::InvalidCopyMode("bad".to_string()); + assert!(e.to_string().contains("bad")); + + let e = Error::Other("custom error".to_string()); + assert_eq!(e.to_string(), "custom error"); + + let e = Error::GetMtu("eth0 failed".to_string()); + assert!(e.to_string().contains("eth0 failed")); + + let e = Error::GetChecksumOffload("not supported".to_string()); + assert!(e.to_string().contains("not supported")); + + let e = Error::GetQueueCount("not supported".to_string()); + assert!(e.to_string().contains("not supported")); + + let e = Error::QueueIdOutOfRange(3000); + assert!(e.to_string().contains("3000")); + } + + #[test] + fn display_would_block() { + let e = WouldBlock; + assert_eq!(e.to_string(), "network I/O error: would block"); + } + + #[test] + fn would_block_equality() { + assert_eq!(WouldBlock, WouldBlock); + } +} 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..5197026 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,91 +119,48 @@ 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) - } - - #[inline(always)] - fn take_frames(&mut self) -> Self::Drain<'_> { - B::take_frames(&mut *self) - } - - #[inline(always)] - fn iter_frames(&self) -> Self::Iter<'_> { - B::iter_frames(&*self) - } - - #[inline(always)] - fn iter_frames_mut(&mut self) -> Self::IterMut<'_> { - B::iter_frames_mut(&mut *self) - } -} - -#[cfg(feature = "async")] -impl<'umem, B: FrameBuffer<'umem>> FrameBuffer<'umem> for futures_util::lock::MutexGuard<'_, B> { - type Drain<'a> - = B::Drain<'a> - where - Self: 'a, - 'umem: 'a; - - type Iter<'a> - = B::Iter<'a> - where - Self: 'a, - 'umem: 'a; - - type IterMut<'a> - = B::IterMut<'a> - where - Self: 'a, - 'umem: 'a; - - #[inline(always)] - fn free_space(&self) -> usize { - B::free_space(&*self) - } - - #[inline(always)] - fn num_frames(&self) -> usize { - B::num_frames(&*self) + B::push(self, frame) } #[inline(always)] - fn push(&mut self, frame: Frame<'umem>) { - B::push(&mut *self, frame) + 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 +205,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(..) } @@ -292,13 +261,4 @@ mod tests { let mut data = vec![0u8; 10]; test_buffer_logic(guard, &mut data); } - - #[cfg(feature = "async")] - #[test] - fn test_futures_mutex_wrapper() { - let mock = futures_util::lock::Mutex::new(MockFrameBuffer::new(10)); - let guard = futures::executor::block_on(mock.lock()); - let mut data = vec![0u8; 10]; - test_buffer_logic(guard, &mut data); - } } diff --git a/src/xdp/frame/frame.rs b/src/xdp/frame/frame.rs index 6059753..9e33ec8 100644 --- a/src/xdp/frame/frame.rs +++ b/src/xdp/frame/frame.rs @@ -14,9 +14,10 @@ unsafe impl<'umem> Send for Frame<'umem> {} 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 { + #[doc(hidden)] + pub 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 +43,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 +70,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 +161,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 +174,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..b45773f --- /dev/null +++ b/src/xdp/frame/shared.rs @@ -0,0 +1,232 @@ +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() + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::xdp::frame::Frame; + + fn make_frame(buf: &mut [u8], len: usize) -> Frame<'_> { + Frame::new(0, buf, len, false) + } + + #[test] + fn from_basic_frame_buffer() { + let basic = BasicFrameBuffer::new(8); + let shared = SharedFrameBuffer::from(basic); + assert_eq!(shared.num_frames(), 0); + assert_eq!(shared.free_space(), 8); + } + + #[test] + fn push_and_pop() { + let basic = BasicFrameBuffer::new(8); + let mut shared = SharedFrameBuffer::from(basic); + let mut buf = [0u8; 64]; + buf[0] = 0xAA; + shared.push(make_frame(&mut buf, 10)); + assert_eq!(shared.num_frames(), 1); + let frame = shared.pop().unwrap(); + assert_eq!(frame[0], 0xAA); + assert_eq!(shared.num_frames(), 0); + } + + #[test] + fn pop_empty_returns_none() { + let basic = BasicFrameBuffer::new(8); + let mut shared = SharedFrameBuffer::from(basic); + assert!(shared.pop().is_none()); + } + + #[test] + fn clone_shares_inner() { + let basic = BasicFrameBuffer::new(8); + let mut shared = SharedFrameBuffer::from(basic); + let cloned = shared.clone(); + let mut buf = [0u8; 64]; + shared.push(make_frame(&mut buf, 10)); + assert_eq!(cloned.num_frames(), 1); + } + + #[test] + fn free_space_decreases_on_push() { + let basic = BasicFrameBuffer::new(4); + let mut shared = SharedFrameBuffer::from(basic); + assert_eq!(shared.free_space(), 4); + let mut buf = [0u8; 64]; + shared.push(make_frame(&mut buf, 10)); + assert_eq!(shared.free_space(), 3); + } + + #[test] + fn take_frames_drains_all() { + let basic = BasicFrameBuffer::new(8); + let mut shared = SharedFrameBuffer::from(basic); + let mut bufs = [[0u8; 64]; 3]; + for buf in bufs.iter_mut() { + shared.push(make_frame(buf, 10)); + } + assert_eq!(shared.num_frames(), 3); + let taken: Vec<_> = shared.take_frames().collect(); + assert_eq!(taken.len(), 3); + assert_eq!(shared.num_frames(), 0); + } + + #[test] + fn iter_frames_borrows() { + let basic = BasicFrameBuffer::new(8); + let mut shared = SharedFrameBuffer::from(basic); + let mut bufs = [[0u8; 64]; 2]; + bufs[0][0] = 0x01; + bufs[1][0] = 0x02; + for buf in bufs.iter_mut() { + shared.push(make_frame(buf, 10)); + } + let collected: Vec<_> = shared.iter_frames().collect(); + assert_eq!(collected.len(), 2); + assert_eq!(collected[0][0], 0x01); + assert_eq!(collected[1][0], 0x02); + } + + #[test] + fn iter_frames_mut_allows_mutation() { + let basic = BasicFrameBuffer::new(8); + let mut shared = SharedFrameBuffer::from(basic); + let mut buf = [0u8; 64]; + shared.push(make_frame(&mut buf, 10)); + for frame in shared.iter_frames_mut() { + frame[0] = 0xFF; + } + let frame = shared.iter_frames().next().unwrap(); + assert_eq!(frame[0], 0xFF); + } + + #[test] + fn drain_range() { + let basic = BasicFrameBuffer::new(8); + let mut shared = SharedFrameBuffer::from(basic); + let mut bufs = [[0u8; 64]; 3]; + for (i, buf) in bufs.iter_mut().enumerate() { + buf[0] = i as u8; + shared.push(make_frame(buf, 10)); + } + let drained: Vec<_> = shared.drain(0..2).collect(); + assert_eq!(drained.len(), 2); + assert_eq!(shared.num_frames(), 1); + } +} diff --git a/src/xdp/futures/local/comp.rs b/src/xdp/futures/local/comp.rs deleted file mode 100644 index 60f31ea..0000000 --- a/src/xdp/futures/local/comp.rs +++ /dev/null @@ -1,70 +0,0 @@ -use std::{ - ops::{Deref, DerefMut}, - pin::Pin, - task::{Context, Poll}, -}; - -use crate::xdp::{error::Result, frame::FrameBuffer, umem::CompletionQueue}; - -/// A completion queue designed to work on the [LocalExecutor] executor. -/// -/// [LocalExecutor]: crate::xdp::futures::local::LocalExecutor -pub struct LocalCompletionQueue<'umem> { - inner: CompletionQueue<'umem>, -} - -impl<'umem> LocalCompletionQueue<'umem> { - pub(crate) fn new(completion_queue: CompletionQueue<'umem>) -> Result { - Ok(Self { - inner: completion_queue, - }) - } - - /// Asynchronously processes the completion queue, pushing new frames from the frame stack into the completion ring up to the size of the completion ring. - #[inline(always)] - pub fn process_queue<'que, B: FrameBuffer<'umem>>( - &'que mut self, - batch: B, - ) -> LocalCompFuture<'que, 'umem, B> { - LocalCompFuture { - completion_queue: self, - batch, - } - } -} - -impl<'umem> Deref for LocalCompletionQueue<'umem> { - type Target = CompletionQueue<'umem>; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl<'umem> DerefMut for LocalCompletionQueue<'umem> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner - } -} - -/// A future that asynchronously processes the completion queue, pushing new frames from the frame stack into the completion ring up to the size of the completion ring. -pub struct LocalCompFuture<'que, 'umem, B: FrameBuffer<'umem>> { - pub(crate) completion_queue: &'que mut LocalCompletionQueue<'umem>, - pub(crate) batch: B, -} - -impl<'que, 'umem, B: FrameBuffer<'umem>> Future for LocalCompFuture<'que, 'umem, B> { - type Output = Result<()>; - - fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll { - // SAFETY: We guarantee not to move self, just access its fields. - // 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) { - return Poll::Ready(Ok(())); - } - - Poll::Pending - } -} diff --git a/src/xdp/futures/local/executor.rs b/src/xdp/futures/local/executor.rs deleted file mode 100644 index 5f7d098..0000000 --- a/src/xdp/futures/local/executor.rs +++ /dev/null @@ -1,58 +0,0 @@ -use std::task::{Context, Poll}; - -use futures_util::pin_mut; - -use crate::xdp::error::Result; - -use super::{Poller, waker}; - -/// A local executor designed to work with XDP sockets specifically, this is NOT a full async runtime, but a purpose built executor to allow for async ergonomics -/// when the throughput/latency requirements are loose enough. -/// -/// The idea here is that the executor does not use normal waker semanitcs as these semantics cause significant performance overhead when dealing with cross thread synchronization. -/// -/// So this polls the given future on the local thread, sleeping on a custom epoll implementation to await for new IO events. These then drive the future to completion. Removing a -/// significant amount of overhead from the future. -/// -/// When to use this? You want the lowest possible latency but want to use async/await semantics, and _critically_ you either are bringing your own full async runtime or you don't have other -/// async work to do. -pub struct LocalExecutor { - poller: Poller, -} - -impl LocalExecutor { - /// Creates a new local executor. - pub fn new() -> Result { - Ok(Self { - poller: Poller::new()?, - }) - } - - /// Registers a file descriptor with the local executor. - pub fn register(&self, fd: i32) -> Result<()> { - self.poller.register(fd) - } - - /// Deregisters a file descriptor from the local executor. - pub fn deregister(&self, fd: i32) -> Result<()> { - self.poller.deregister(fd) - } - - /// Runs a future on the local executor. - pub fn run(&mut self, fut: F) -> F::Output { - let waker = waker(); - let mut cx = Context::from_waker(&waker); - - pin_mut!(fut); - loop { - if let Poll::Ready(output) = fut.as_mut().poll(&mut cx) { - return output; - } - - match self.poller.poll(-1) { - Ok(events) => events, - Err(e) => panic!("Poller poll failed: {}", e), - }; - } - } -} diff --git a/src/xdp/futures/local/fill.rs b/src/xdp/futures/local/fill.rs deleted file mode 100644 index 332f359..0000000 --- a/src/xdp/futures/local/fill.rs +++ /dev/null @@ -1,79 +0,0 @@ -use std::{ - ops::{Deref, DerefMut}, - os::raw::c_int, - pin::Pin, - task::{Context, Poll}, -}; - -use crate::xdp::{error::Result, frame::FrameBuffer, umem::FillQueue}; - -/// A fill queue designed to work on the [LocalExecutor] executor. -/// -/// [LocalExecutor]: crate::xdp::futures::local::LocalExecutor -pub struct LocalFillQueue<'umem> { - inner: FillQueue<'umem>, -} - -impl<'umem> LocalFillQueue<'umem> { - pub(crate) fn new(fill_queue: FillQueue<'umem>) -> Result { - Ok(Self { inner: fill_queue }) - } - - /// 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<'fd, B: FrameBuffer<'umem>>( - &mut self, - batch: B, - fds: &'fd [c_int], - ) -> LocalFillFuture<'_, 'umem, 'fd, B> { - LocalFillFuture { - fill_queue: self, - batch, - fds, - } - } -} - -impl<'umem> Deref for LocalFillQueue<'umem> { - type Target = FillQueue<'umem>; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl<'umem> DerefMut for LocalFillQueue<'umem> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner - } -} - -/// A future that asynchronously 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. -pub struct LocalFillFuture<'que, 'umem, 'fd, B: FrameBuffer<'umem>> { - pub(crate) fill_queue: &'que mut LocalFillQueue<'umem>, - pub(crate) batch: B, - pub(crate) fds: &'fd [c_int], -} - -impl<'que, 'umem, 'fd, B: FrameBuffer<'umem>> Future for LocalFillFuture<'que, 'umem, 'fd, B> { - type Output = Result<()>; - - fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll { - // SAFETY: We guarantee not to move self, just access its fields. - // Those accesses are also guaranteed to not move self or the fields themselves. - let this = unsafe { self.get_unchecked_mut() }; - - for fd in this.fds.iter() { - if let Err(e) = this.fill_queue.maybe_wake(*fd) { - return Poll::Ready(Err(e)); - } - } - - this.fill_queue.inner.process_queue(&mut this.batch); - if this.batch.num_frames() == 0 { - return Poll::Ready(Ok(())); - } - - Poll::Pending - } -} diff --git a/src/xdp/futures/local/mod.rs b/src/xdp/futures/local/mod.rs deleted file mode 100644 index 9f1ae52..0000000 --- a/src/xdp/futures/local/mod.rs +++ /dev/null @@ -1,32 +0,0 @@ -//! Local executor futures for AF_XDP. -//! -//! This module provides futures for the [LocalExecutor] executor, and associated types for working with the XDP subsystem. -//! -//! So why a local executor? The idea here is that the executor does not use normal waker semanitcs as these semantics cause significant performance overhead when dealing with cross thread synchronization. -//! -//! So this polls the given future on the local thread, sleeping on a custom epoll implementation to await for new IO events. These then drive the future to completion. Removing a -//! significant amount of overhead from the future. -//! -//! The idea here is that it colocates the I/O poller and the actual logic, this removes all needed synchronization _and_ it ensures cache coherency for the duration of the poll loop. This is the fastest possible polling implementation -//! that also leverages the full async/await semantics. A slightly faster version with polling would pull the [Poller] out of the executor and handle manual polling and execution. - -mod comp; -mod executor; -mod fill; -mod poller; -mod recv; -mod send; -mod socket; -mod umem; -mod waker; - -use waker::waker; - -pub use comp::{LocalCompFuture, LocalCompletionQueue}; -pub use executor::LocalExecutor; -pub use fill::{LocalFillFuture, LocalFillQueue}; -pub use poller::Poller; -pub use recv::{LocalRecvFuture, LocalSocketRx}; -pub use send::{LocalSendFuture, LocalSocketTx}; -pub use socket::LocalSocket; -pub use umem::LocalUmem; diff --git a/src/xdp/futures/local/poller.rs b/src/xdp/futures/local/poller.rs deleted file mode 100644 index e50800e..0000000 --- a/src/xdp/futures/local/poller.rs +++ /dev/null @@ -1,94 +0,0 @@ -use std::ptr::null_mut; - -use errno::errno; -use libc::{ - EPOLL_CLOEXEC, EPOLL_CTL_ADD, EPOLL_CTL_DEL, EPOLLET, EPOLLIN, EPOLLOUT, epoll_create1, - epoll_ctl, epoll_event, epoll_wait, -}; - -use crate::xdp::error::{Error, Result}; - -const MAX_EVENTS: usize = 1024; - -/// A poller designed to work with the [LocalExecutor] executor. -/// -/// This poller wraps epoll on linux to provide a simple and efficient network I/O polling solution to drive I/O futures. -/// -/// [LocalExecutor]: crate::xdp::futures::local::LocalExecutor -pub struct Poller { - poll_fd: i32, - events: [epoll_event; MAX_EVENTS], -} - -impl Poller { - /// Creates a new poller. This will call `epoll_create1` with the `EPOLL_CLOEXEC` flag to create a new epoll file descriptor. - pub(crate) fn new() -> Result { - let poll_fd = unsafe { epoll_create1(EPOLL_CLOEXEC) }; - if poll_fd < 0 { - return Err(Error::EpollCreate(errno())); - } - - Ok(Self { - poll_fd, - events: [epoll_event { events: 0, u64: 0 }; MAX_EVENTS], - }) - } - - /// Registers a new file descriptor with the poller. This will automatically register for both EPOLLIN and EPOLLOUT events. - /// - /// The [Poller] operates under edge triggered semantics, so its important to ensure that resources are exhausted before polling again. - pub fn register(&self, fd: i32) -> Result<()> { - let mut event = epoll_event { - events: (EPOLLIN | EPOLLOUT | EPOLLET) as u32, - u64: fd as u64, - }; - - let ret = unsafe { epoll_ctl(self.poll_fd, EPOLL_CTL_ADD, fd, &mut event) }; - if ret < 0 { - return Err(Error::EpollCtl(errno())); - } - Ok(()) - } - - /// Deregisters a file descriptor from the poller. This will remove the file descriptor from the poller's event loop. - pub fn deregister(&self, fd: i32) -> Result<()> { - let ret = unsafe { epoll_ctl(self.poll_fd, EPOLL_CTL_DEL, fd, null_mut()) }; - if ret < 0 { - return Err(Error::EpollCtl(errno())); - } - Ok(()) - } - - /// Polls the poller for new events. This will block for up to the given timeout duration. - /// - /// If no events are ready before the timeout duration, this will return `false`. - /// If events are ready, this will return `true`. - /// - /// If an error occurs, this will return an error. - pub fn poll(&mut self, timeout_ms: i32) -> Result { - let n = unsafe { - epoll_wait( - self.poll_fd, - self.events.as_mut_ptr() as *mut epoll_event, - MAX_EVENTS as i32, - timeout_ms, - ) - }; - - if n == 0 { - return Ok(false); - } - if n < 0 { - return Err(Error::EpollWait(errno())); - } - - Ok(true) - } -} - -impl Drop for Poller { - fn drop(&mut self) { - // Close the poll fd to avoid leaking it. - unsafe { libc::close(self.poll_fd) }; - } -} diff --git a/src/xdp/futures/local/recv.rs b/src/xdp/futures/local/recv.rs deleted file mode 100644 index 3b5961b..0000000 --- a/src/xdp/futures/local/recv.rs +++ /dev/null @@ -1,65 +0,0 @@ -use std::{ - ops::{Deref, DerefMut}, - pin::Pin, - task::{Context, Poll}, -}; - -use crate::xdp::{error::Result, frame::FrameBuffer, socket::SocketRx}; - -/// A socket receiver designed to work on the [LocalExecutor] executor. -/// -/// [LocalExecutor]: crate::xdp::futures::local::LocalExecutor -pub struct LocalSocketRx<'umem> { - inner: SocketRx<'umem>, -} - -impl<'umem> LocalSocketRx<'umem> { - pub(crate) fn new(socket: SocketRx<'umem>) -> Result { - Ok(Self { inner: socket }) - } - - /// Asynchronously receives a batch of frames from the socket. - #[inline(always)] - pub fn recv>(&mut self, batch: B) -> LocalRecvFuture<'_, 'umem, B> { - LocalRecvFuture { - socket: self, - batch, - } - } -} - -impl<'umem> Deref for LocalSocketRx<'umem> { - type Target = SocketRx<'umem>; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl<'umem> DerefMut for LocalSocketRx<'umem> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner - } -} - -/// A future that asynchronously receives a batch of frames from the socket. -pub struct LocalRecvFuture<'sock, 'umem, B: FrameBuffer<'umem>> { - pub(crate) socket: &'sock mut LocalSocketRx<'umem>, - pub(crate) batch: B, -} - -impl<'sock, 'umem, B: FrameBuffer<'umem>> Future for LocalRecvFuture<'sock, 'umem, B> { - type Output = Result; - - fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll { - // SAFETY: We guarantee not to move self, just access its fields. - // Those accesses are also guaranteed to not move self or the fields themselves. - let this = unsafe { self.get_unchecked_mut() }; - - if let Ok(received) = this.socket.inner.recv(&mut this.batch) { - return Poll::Ready(Ok(received)); - } - - Poll::Pending - } -} diff --git a/src/xdp/futures/local/send.rs b/src/xdp/futures/local/send.rs deleted file mode 100644 index 0222b04..0000000 --- a/src/xdp/futures/local/send.rs +++ /dev/null @@ -1,66 +0,0 @@ -use std::{ - ops::{Deref, DerefMut}, - pin::Pin, - task::{Context, Poll}, -}; - -use crate::xdp::{error::Result, frame::FrameBuffer, socket::SocketTx}; - -/// A socket transmitter designed to work on the [LocalExecutor] executor. -/// -/// [LocalExecutor]: crate::xdp::futures::local::LocalExecutor -pub struct LocalSocketTx<'umem> { - inner: SocketTx<'umem>, -} - -impl<'umem> LocalSocketTx<'umem> { - pub(crate) fn new(socket: SocketTx<'umem>) -> Result { - Ok(Self { inner: socket }) - } - - /// Asynchronously sends a batch of frames to the socket. - #[inline(always)] - pub fn send>(&mut self, batch: B) -> LocalSendFuture<'_, 'umem, B> { - LocalSendFuture { - socket: self, - batch, - } - } -} - -impl<'umem> Deref for LocalSocketTx<'umem> { - type Target = SocketTx<'umem>; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl<'umem> DerefMut for LocalSocketTx<'umem> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner - } -} - -/// A future that asynchronously sends a batch of frames to the socket. -pub struct LocalSendFuture<'sock, 'umem, B: FrameBuffer<'umem>> { - pub(crate) socket: &'sock mut LocalSocketTx<'umem>, - pub(crate) batch: B, -} - -impl<'sock, 'umem, B: FrameBuffer<'umem>> Future for LocalSendFuture<'sock, 'umem, B> { - type Output = Result; - - fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll { - // SAFETY: We guarantee not to move self, just access its fields. - // Those accesses are also guaranteed to not move self or the fields themselves. - let this = unsafe { self.get_unchecked_mut() }; - - if let Ok(sent) = this.socket.inner.send(&mut this.batch) { - this.socket.maybe_wake()?; - return Poll::Ready(Ok(sent)); - } - - Poll::Pending - } -} diff --git a/src/xdp/futures/local/socket.rs b/src/xdp/futures/local/socket.rs deleted file mode 100644 index 11c3094..0000000 --- a/src/xdp/futures/local/socket.rs +++ /dev/null @@ -1,310 +0,0 @@ -use std::{os::fd::RawFd, sync::Arc}; - -use crate::xdp::{ - error::Result, - frame::FrameBuffer, - socket::{SocketOwner, SocketRx, SocketTx}, -}; - -use super::{LocalRecvFuture, LocalSendFuture, LocalSocketRx, LocalSocketTx}; - -/// A socket designed to work on the [LocalExecutor] executor. -/// -/// [LocalExecutor]: crate::xdp::futures::local::LocalExecutor -pub struct LocalSocket<'umem> { - owner: Arc>, - rx: LocalSocketRx<'umem>, - tx: LocalSocketTx<'umem>, -} - -impl<'umem> LocalSocket<'umem> { - pub(crate) fn new( - owner: Arc>, - rx: SocketRx<'umem>, - tx: SocketTx<'umem>, - ) -> Result { - Ok(Self { - owner, - rx: LocalSocketRx::new(rx)?, - tx: LocalSocketTx::new(tx)?, - }) - } - - /// Splits the socket into its owner, rx, and tx components. - #[inline(always)] - pub fn split( - self, - ) -> ( - Arc>, - LocalSocketRx<'umem>, - LocalSocketTx<'umem>, - ) { - (self.owner, self.rx, self.tx) - } - - /// Returns the file descriptor of the socket. - #[inline(always)] - pub fn fd(&self) -> RawFd { - self.owner.fd() - } - - /// Possibly wakes the tx queue, so the kernel continues to process outgoing packets. - #[inline(always)] - pub fn maybe_wake(&self) -> Result<()> { - self.tx.maybe_wake() - } - - /// Receives a batch of frames from the socket. - #[inline(always)] - pub fn recv>(&mut self, batch: B) -> LocalRecvFuture<'_, 'umem, B> { - self.rx.recv(batch) - } - - /// Sends a batch of frames to the socket. - #[inline(always)] - pub fn send>(&mut self, frames: B) -> LocalSendFuture<'_, 'umem, B> { - self.tx.send(frames) - } -} - -#[cfg(test)] -mod tests { - use std::sync::Arc; - use std::sync::atomic::{AtomicBool, Ordering}; - use std::thread; - use std::time::Duration; - - use crate::xdp::context::XdpContext; - use crate::xdp::frame::{BasicFrameBuffer, FrameBuffer}; - use crate::xdp::futures::LocalExecutor; - use crate::xdp::program::AttachMode; - use crate::xdp::socket::Socket; - use crate::xdp::test_utils::TestVethPair; - use crate::xdp::umem::Umem; - - /// Helper to build a minimal Ethernet frame with a payload. - /// Format: [dst MAC (6)] [src MAC (6)] [EtherType (2)] [payload] - fn build_ethernet_frame(dst_mac: &[u8; 6], src_mac: &[u8; 6], payload: &[u8]) -> Vec { - let mut frame = Vec::with_capacity(14 + payload.len()); - frame.extend_from_slice(dst_mac); - frame.extend_from_slice(src_mac); - // EtherType 0x88B5 = Local Experimental EtherType - frame.extend_from_slice(&[0x88, 0xB5]); - frame.extend_from_slice(payload); - frame - } - - /// Test that sends a packet from one side of a veth pair and receives it on the other using local async. - #[test] - fn test_local_socket_send_recv() { - // Use a flag to signal test completion - let completed = Arc::new(AtomicBool::new(false)); - let completed_clone = completed.clone(); - - // Run the test in a separate thread with a timeout - let test_handle = thread::spawn(move || { - // Create veth pair - let veth = TestVethPair::new().expect("failed to create veth pair"); - - // Create XDP contexts on both ends - let mut ctx_outer = XdpContext::builder(veth.outer_name()) - .attach_mode(AttachMode::default()) - .enable_fragmentation(false) - .build() - .expect("failed to create outer context"); - let mut ctx_inner = XdpContext::builder(veth.inner_name()) - .attach_mode(AttachMode::default()) - .enable_fragmentation(false) - .build() - .expect("failed to create inner context"); - - // Create UMEM for outer socket (sender) - let (umem_outer, _fq_outer, mut cq_outer) = Umem::builder() - .num_frames(64) - .frame_size(4096) - .fill_ring_size(32) - .completion_ring_size(32) - .build_local() - .expect("failed to create outer umem") - .split(); - - // Create UMEM for inner socket (receiver) - let (umem_inner, mut fq_inner, _cq_inner) = Umem::builder() - .num_frames(64) - .frame_size(4096) - .fill_ring_size(32) - .completion_ring_size(32) - .build_local() - .expect("failed to create inner umem") - .split(); - - // Initialize frame buffers - let mut tx_buffer: BasicFrameBuffer<'_> = umem_outer.init_buffer().unwrap(); - let mut rx_buffer: BasicFrameBuffer<'_> = umem_inner.init_buffer().unwrap(); - - // Create local sockets - let mut socket_outer = Socket::builder(veth.outer_name(), 0) - .build_local(&mut ctx_outer, umem_outer.clone()) - .expect("failed to create outer local socket"); - - let mut socket_inner = Socket::builder(veth.inner_name(), 0) - .build_local(&mut ctx_inner, umem_inner.clone()) - .expect("failed to create inner local socket"); - - // Create the local executor and register socket fds - let mut executor = LocalExecutor::new().expect("failed to create local executor"); - executor - .register(socket_outer.fd()) - .expect("failed to register outer socket"); - executor - .register(socket_inner.fd()) - .expect("failed to register inner socket"); - - executor.run(async { - // Prime the receiver's fill queue so it can receive packets - let rx_prime_count = rx_buffer.num_frames().min(32); - let mut prime_buffer = BasicFrameBuffer::new(rx_prime_count); - for frame in rx_buffer.drain(..rx_prime_count) { - prime_buffer.push(frame); - } - fq_inner - .process_queue(&mut prime_buffer, &[socket_inner.fd()]) - .await - .expect("failed to process fill queue"); - - // Build a test packet - let payload = b"Hello XDP Local Socket Test!"; - let packet_data = build_ethernet_frame( - veth.inner_mac().as_bytes(), - veth.outer_mac().as_bytes(), - payload, - ); - - // Prepare a frame for sending - let mut send_buffer = BasicFrameBuffer::new(1); - let mut frame = tx_buffer.drain(..1).next().expect("no frames available"); - frame.copy_from(&packet_data); - send_buffer.push(frame); - - // Send the packet (async) - let sent = socket_outer - .send(&mut send_buffer) - .await - .expect("send failed"); - assert_eq!(sent, 1, "expected to send 1 frame"); - assert_eq!(send_buffer.num_frames(), 0, "buffer should be drained"); - - // Receive the packet (async) - let mut recv_buffer = BasicFrameBuffer::new(16); - let received = socket_inner - .recv(&mut recv_buffer) - .await - .expect("recv failed"); - - assert!(received > 0, "expected to receive at least 1 frame"); - assert!( - recv_buffer.num_frames() >= 1, - "expected at least 1 received frame" - ); - - // Verify the received packet matches what we sent - let recv_frame = recv_buffer.iter_frames().next().unwrap(); - assert!( - recv_frame.len() >= packet_data.len(), - "received frame too short" - ); - assert_eq!( - &recv_frame[..packet_data.len()], - &packet_data[..], - "packet data mismatch" - ); - - // Clean up: process completion queue to reclaim TX frame - let mut reclaim_buffer = BasicFrameBuffer::new(1); - cq_outer - .process_queue(&mut reclaim_buffer) - .await - .expect("failed to process completion queue"); - }); - - completed_clone.store(true, Ordering::SeqCst); - }); - - // Wait with timeout - let timeout = Duration::from_secs(10); - let start = std::time::Instant::now(); - while !completed.load(Ordering::SeqCst) && start.elapsed() < timeout { - thread::sleep(Duration::from_millis(100)); - } - - if !completed.load(Ordering::SeqCst) { - panic!("test_local_socket_send_recv timed out"); - } - - test_handle.join().expect("test thread panicked"); - } - - /// Test local socket creation and basic properties. - #[test] - fn test_local_socket_creation() { - let veth = TestVethPair::new().expect("failed to create veth pair"); - - let mut ctx = XdpContext::builder(veth.outer_name()) - .attach_mode(AttachMode::default()) - .enable_fragmentation(false) - .build() - .expect("failed to create context"); - - let (umem, _fq, _cq) = Umem::builder() - .num_frames(16) - .frame_size(4096) - .fill_ring_size(8) - .completion_ring_size(8) - .build() - .expect("failed to create umem") - .split(); - - let socket = Socket::builder(veth.outer_name(), 0) - .rx_ring_size(8) - .tx_ring_size(8) - .build_local(&mut ctx, umem) - .expect("failed to create local socket"); - - // Socket should have valid fd - assert!(socket.fd() >= 0, "socket fd should be valid"); - - // Context should now have 1 socket registered - assert_eq!(ctx.num_sockets(), 1, "context should have 1 socket"); - } - - /// Test local socket split into owner, rx, tx components. - #[test] - fn test_local_socket_split() { - let veth = TestVethPair::new().expect("failed to create veth pair"); - - let mut ctx = XdpContext::builder(veth.outer_name()) - .attach_mode(AttachMode::default()) - .enable_fragmentation(false) - .build() - .expect("failed to create context"); - - let (umem, _fq, _cq) = Umem::builder() - .num_frames(16) - .frame_size(4096) - .fill_ring_size(8) - .completion_ring_size(8) - .build() - .expect("failed to create umem") - .split(); - - let socket = Socket::builder(veth.outer_name(), 0) - .build_local(&mut ctx, umem) - .expect("failed to create local socket"); - - let original_fd = socket.fd(); - let (owner, _rx, _tx) = socket.split(); - - // Owner should have the same fd - assert_eq!(owner.fd(), original_fd, "owner fd should match original"); - } -} diff --git a/src/xdp/futures/local/umem.rs b/src/xdp/futures/local/umem.rs deleted file mode 100644 index 57c3493..0000000 --- a/src/xdp/futures/local/umem.rs +++ /dev/null @@ -1,74 +0,0 @@ -use std::{os::raw::c_int, sync::Arc}; - -use crate::xdp::{ - error::Result, - frame::{Frame, FrameBuffer}, - futures::{LocalCompFuture, LocalCompletionQueue, LocalFillFuture, LocalFillQueue}, - umem::{CompletionQueue, FillQueue, UmemOwner}, -}; - -/// A UMEM designed to work on the [LocalExecutor] executor. -/// -/// [LocalExecutor]: crate::xdp::futures::local::LocalExecutor -pub struct LocalUmem<'umem> { - owner: Arc>, - fill_queue: LocalFillQueue<'umem>, - completion_queue: LocalCompletionQueue<'umem>, -} - -impl<'umem> LocalUmem<'umem> { - pub(crate) fn new( - owner: Arc>, - fill_queue: FillQueue<'umem>, - completion_queue: CompletionQueue<'umem>, - ) -> Result { - Ok(Self { - owner, - fill_queue: LocalFillQueue::new(fill_queue)?, - completion_queue: LocalCompletionQueue::new(completion_queue)?, - }) - } - - /// Splits the umem into its owner, fill queue, and completion queue components. - #[inline(always)] - pub fn split( - self, - ) -> ( - Arc>, - LocalFillQueue<'umem>, - LocalCompletionQueue<'umem>, - ) { - (self.owner, self.fill_queue, self.completion_queue) - } - - /// Returns the owner of the umem. - #[inline(always)] - pub fn owner(&self) -> &Arc> { - &self.owner - } - - /// 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 { - self.owner.init_buffer() - } - - /// 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<'fd, B: FrameBuffer<'umem>>( - &mut self, - batch: B, - fds: &'fd [c_int], - ) -> LocalFillFuture<'_, 'umem, 'fd, B> { - self.fill_queue.process_queue(batch, fds) - } - - /// Processes the completion queue, allocating new frames from the frame stack and submitting them to the completion ring up to the size of the completion ring. - #[inline(always)] - pub fn process_completion_queue<'que, B: FrameBuffer<'umem>>( - &'que mut self, - batch: B, - ) -> LocalCompFuture<'que, 'umem, B> { - self.completion_queue.process_queue(batch) - } -} diff --git a/src/xdp/futures/local/waker.rs b/src/xdp/futures/local/waker.rs deleted file mode 100644 index 9f8cd64..0000000 --- a/src/xdp/futures/local/waker.rs +++ /dev/null @@ -1,25 +0,0 @@ -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 new waker for the [LocalExecutor] executor. -/// -/// This waker is a no-op waker that does nothing. This is be design as we are driven purely by epoll events, so we don't need to wake up the task. -/// -/// [LocalExecutor]: crate::xdp::futures::local::LocalExecutor -pub(super) const fn waker() -> Waker { - unsafe { Waker::from_raw(raw_waker()) } -} diff --git a/src/xdp/futures/mod.rs b/src/xdp/futures/mod.rs deleted file mode 100644 index ee7c727..0000000 --- a/src/xdp/futures/mod.rs +++ /dev/null @@ -1,62 +0,0 @@ -//! Futures for AF_XDP. -//! -//! This module provides futures for the [Tokio] (feature: `tokio`), [Smol] (feature: `smol`), and [Local] (feature: `local`) runtimes, and associated types for working with the XDP subsystem. -//! -//! [Tokio]: tokio -//! [Smol]: smol -//! [Local]: local - -macro_rules! cfg_block { - (#[$meta:meta] { $($item:item)* }) => { - $( - #[$meta] - $item - )* - }; -} - -cfg_block! { - #[cfg(feature = "tokio")] - { - pub mod tokio; - - pub use tokio::{TokioCompFuture, TokioCompletionQueue}; - pub use tokio::{TokioFillFuture, TokioFillQueue}; - pub use tokio::{TokioRecvFuture, TokioSocketRx}; - pub use tokio::{TokioSendFuture, TokioSocketTx}; - pub use tokio::TokioSocket; - pub use tokio::TokioUmem; - pub use tokio::TokioFdFactory; - } -} - -cfg_block! { - #[cfg(feature = "smol")] - { - pub mod smol; - - pub use smol::{SmolFd, SmolFdFactory}; - pub use smol::{SmolCompFuture, SmolCompletionQueue}; - pub use smol::{SmolFillFuture, SmolFillQueue}; - pub use smol::{SmolRecvFuture, SmolSocketRx}; - pub use smol::{SmolSendFuture, SmolSocketTx}; - pub use smol::SmolSocket; - pub use smol::SmolUmem; - } -} - -cfg_block! { - #[cfg(feature = "local")] - { - pub mod local; - - pub use local::{LocalCompFuture, LocalCompletionQueue}; - pub use local::{LocalFillFuture, LocalFillQueue}; - pub use local::{LocalRecvFuture, LocalSocketRx}; - pub use local::{LocalSendFuture, LocalSocketTx}; - pub use local::LocalSocket; - pub use local::LocalUmem; - pub use local::LocalExecutor; - pub use local::Poller; - } -} diff --git a/src/xdp/futures/smol/comp.rs b/src/xdp/futures/smol/comp.rs deleted file mode 100644 index ace11cc..0000000 --- a/src/xdp/futures/smol/comp.rs +++ /dev/null @@ -1,86 +0,0 @@ -use std::{ - ops::{Deref, DerefMut}, - pin::Pin, - sync::Arc, - task::{Context, Poll}, -}; - -use async_io::Async; -use futures_core::ready; - -use crate::xdp::{error::Result, frame::FrameBuffer, umem::CompletionQueue}; - -use super::SmolFd; - -/// A completion queue designed to work on the [Smol](https://docs.rs/smol/latest/smol/) runtime. -pub struct SmolCompletionQueue<'umem> { - inner: CompletionQueue<'umem>, - async_fd: Arc>, -} - -impl<'umem> SmolCompletionQueue<'umem> { - pub(crate) fn new( - completion_queue: CompletionQueue<'umem>, - async_fd: Arc>, - ) -> Self { - Self { - inner: completion_queue, - async_fd, - } - } - - /// Asynchronously processes the completion queue, pushing new frames from the frame stack into the completion ring up to the size of the completion ring. - #[inline(always)] - pub fn process_queue<'que, B: FrameBuffer<'umem>>( - &'que mut self, - batch: B, - ) -> SmolCompFuture<'que, 'umem, B> { - SmolCompFuture { - completion_queue: self, - batch, - } - } -} - -impl<'umem> Deref for SmolCompletionQueue<'umem> { - type Target = CompletionQueue<'umem>; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl<'umem> DerefMut for SmolCompletionQueue<'umem> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner - } -} - -/// A future that asynchronously processes the completion queue, pushing new frames from the frame stack into the completion ring up to the size of the completion ring. -pub struct SmolCompFuture<'que, 'umem, B: FrameBuffer<'umem>> { - completion_queue: &'que mut SmolCompletionQueue<'umem>, - batch: B, -} - -impl<'que, 'umem, B: FrameBuffer<'umem>> Future for SmolCompFuture<'que, 'umem, B> { - type Output = Result<()>; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - // SAFETY: We guarantee not to move self, just access its fields. - // 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) { - return Poll::Ready(Ok(())); - } - - let SmolCompletionQueue { inner, async_fd } = this.completion_queue; - loop { - ready!(async_fd.poll_writable(cx))?; - - if let Ok(_) = inner.process_queue(&mut this.batch) { - return Poll::Ready(Ok(())); - } - } - } -} diff --git a/src/xdp/futures/smol/fd.rs b/src/xdp/futures/smol/fd.rs deleted file mode 100644 index 6710eac..0000000 --- a/src/xdp/futures/smol/fd.rs +++ /dev/null @@ -1,53 +0,0 @@ -use std::{ - os::fd::{AsFd, BorrowedFd, RawFd}, - sync::Arc, -}; - -use async_io::Async; -use dashmap::DashMap; - -use crate::xdp::error::Result; - -/// A SmolFd is a wrapper around a raw file descriptor that is used to create an [Async] file descriptor. -/// -/// We need this because [Async] requires an [AsFd] implementation which [RawFd] does not implement. -pub struct SmolFd(RawFd); - -impl AsFd for SmolFd { - fn as_fd(&self) -> BorrowedFd<'_> { - unsafe { BorrowedFd::borrow_raw(self.0) } - } -} - -/// A factory for creating [SmolFd] instances. -/// -/// The purpose of this is to cache [Async] file descriptors for the lifetime of the program, this is important -/// because [Async] file descriptors must be unique and we have multiple components that all need a copy. So we -/// store a [`Arc>`] in a [DashMap] for the lifetime of the program to share across components with t -/// he same fd. -pub struct SmolFdFactory { - inner: DashMap>>, -} - -impl SmolFdFactory { - pub(crate) fn new() -> Self { - Self { - inner: DashMap::new(), - } - } - - /// Creates a new [SmolFd] instance for the given file descriptor, or return a cached instance if it already exists. - /// - /// This returns an error in the event a registration occurs and fails. - pub fn get_smol_fd(&self, fd: RawFd) -> Result>> { - use dashmap::Entry::*; - match self.inner.entry(fd) { - Occupied(entry) => Ok(entry.get().clone()), - Vacant(entry) => { - let async_fd = Arc::new(Async::new(SmolFd(fd))?); - entry.insert(async_fd.clone()); - Ok(async_fd) - } - } - } -} diff --git a/src/xdp/futures/smol/fill.rs b/src/xdp/futures/smol/fill.rs deleted file mode 100644 index 2f752df..0000000 --- a/src/xdp/futures/smol/fill.rs +++ /dev/null @@ -1,95 +0,0 @@ -use std::{ - ops::{Deref, DerefMut}, - os::raw::c_int, - pin::Pin, - sync::Arc, - task::{Context, Poll}, -}; - -use async_io::Async; -use futures_core::ready; - -use crate::xdp::{error::Result, frame::FrameBuffer, umem::FillQueue}; - -use super::SmolFd; - -/// A fill queue designed to work on the [Smol](https://docs.rs/smol/latest/smol/) runtime. -pub struct SmolFillQueue<'umem> { - inner: FillQueue<'umem>, - async_fd: Arc>, -} - -impl<'umem> SmolFillQueue<'umem> { - pub(crate) fn new(fill_queue: FillQueue<'umem>, async_fd: Arc>) -> Self { - Self { - inner: fill_queue, - async_fd, - } - } - - /// Asynchronously 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<'fd, B: FrameBuffer<'umem>>( - &mut self, - batch: B, - fds: &'fd [c_int], - ) -> SmolFillFuture<'_, 'umem, 'fd, B> { - SmolFillFuture { - fill_queue: self, - batch, - fds, - } - } -} - -impl<'umem> Deref for SmolFillQueue<'umem> { - type Target = FillQueue<'umem>; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl<'umem> DerefMut for SmolFillQueue<'umem> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner - } -} - -/// A future that asynchronously 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. -pub struct SmolFillFuture<'que, 'umem, 'fd, B: FrameBuffer<'umem>> { - fill_queue: &'que mut SmolFillQueue<'umem>, - batch: B, - fds: &'fd [c_int], -} - -impl<'que, 'umem, 'fd, B: FrameBuffer<'umem>> Future for SmolFillFuture<'que, 'umem, 'fd, B> { - type Output = Result<()>; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - // SAFETY: We guarantee not to move self, just access its fields. - // Those accesses are also guaranteed to not move self or the fields themselves. - let this = unsafe { self.get_unchecked_mut() }; - - for fd in this.fds.iter() { - if let Err(e) = this.fill_queue.maybe_wake(*fd) { - return Poll::Ready(Err(e)); - } - } - - this.fill_queue.inner.process_queue(&mut this.batch); - if this.batch.num_frames() == 0 { - return Poll::Ready(Ok(())); - } - - let SmolFillQueue { inner, async_fd } = this.fill_queue; - loop { - ready!(async_fd.poll_writable(cx))?; - - inner.process_queue(&mut this.batch); - if this.batch.num_frames() == 0 { - return Poll::Ready(Ok(())); - } - } - } -} diff --git a/src/xdp/futures/smol/mod.rs b/src/xdp/futures/smol/mod.rs deleted file mode 100644 index 1ff61b4..0000000 --- a/src/xdp/futures/smol/mod.rs +++ /dev/null @@ -1,19 +0,0 @@ -//! Smol executor futures for AF_XDP. -//! -//! This module provides futures for the [Smol](https://docs.rs/smol/latest/smol/) runtime, and associated types for working with the XDP subsystem. - -mod comp; -mod fd; -mod fill; -mod recv; -mod send; -mod socket; -mod umem; - -pub use comp::{SmolCompFuture, SmolCompletionQueue}; -pub use fd::{SmolFd, SmolFdFactory}; -pub use fill::{SmolFillFuture, SmolFillQueue}; -pub use recv::{SmolRecvFuture, SmolSocketRx}; -pub use send::{SmolSendFuture, SmolSocketTx}; -pub use socket::SmolSocket; -pub use umem::SmolUmem; diff --git a/src/xdp/futures/smol/recv.rs b/src/xdp/futures/smol/recv.rs deleted file mode 100644 index f905236..0000000 --- a/src/xdp/futures/smol/recv.rs +++ /dev/null @@ -1,80 +0,0 @@ -use std::{ - ops::{Deref, DerefMut}, - pin::Pin, - sync::Arc, - task::{Context, Poll}, -}; - -use async_io::Async; -use futures_core::ready; - -use crate::xdp::{error::Result, frame::FrameBuffer, socket::SocketRx}; - -use super::SmolFd; - -/// A socket receiver designed to work on the [Smol](https://docs.rs/smol/latest/smol/) runtime. -pub struct SmolSocketRx<'umem> { - inner: SocketRx<'umem>, - async_fd: Arc>, -} - -impl<'umem> SmolSocketRx<'umem> { - pub(crate) fn new(socket: SocketRx<'umem>, async_fd: Arc>) -> Self { - Self { - inner: socket, - async_fd, - } - } - - /// Asynchronously receives a batch of frames from the socket. - #[inline(always)] - pub fn recv>(&mut self, batch: B) -> SmolRecvFuture<'_, 'umem, B> { - SmolRecvFuture { - socket: self, - batch, - } - } -} - -impl<'umem> Deref for SmolSocketRx<'umem> { - type Target = SocketRx<'umem>; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl<'umem> DerefMut for SmolSocketRx<'umem> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner - } -} - -/// A future that asynchronously receives a batch of frames from the socket. -pub struct SmolRecvFuture<'sock, 'umem, B: FrameBuffer<'umem>> { - socket: &'sock mut SmolSocketRx<'umem>, - batch: B, -} - -impl<'sock, 'umem, B: FrameBuffer<'umem>> Future for SmolRecvFuture<'sock, 'umem, B> { - type Output = Result; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - // SAFETY: We guarantee not to move self, just access its fields. - // Those accesses are also guaranteed to not move self or the fields themselves. - let this = unsafe { self.get_unchecked_mut() }; - - if let Ok(received) = this.socket.inner.recv(&mut this.batch) { - return Poll::Ready(Ok(received)); - } - - let SmolSocketRx { inner, async_fd } = this.socket; - loop { - ready!(async_fd.poll_readable(cx))?; - - if let Ok(received) = inner.recv(&mut this.batch) { - return Poll::Ready(Ok(received)); - } - } - } -} diff --git a/src/xdp/futures/smol/send.rs b/src/xdp/futures/smol/send.rs deleted file mode 100644 index 7611361..0000000 --- a/src/xdp/futures/smol/send.rs +++ /dev/null @@ -1,82 +0,0 @@ -use std::{ - ops::{Deref, DerefMut}, - pin::Pin, - sync::Arc, - task::{Context, Poll}, -}; - -use async_io::Async; -use futures_core::ready; - -use crate::xdp::{error::Result, frame::FrameBuffer, socket::SocketTx}; - -use super::SmolFd; - -/// A socket transmitter designed to work on the [Smol](https://docs.rs/smol/latest/smol/) runtime. -pub struct SmolSocketTx<'umem> { - inner: SocketTx<'umem>, - async_fd: Arc>, -} - -impl<'umem> SmolSocketTx<'umem> { - pub(crate) fn new(socket: SocketTx<'umem>, async_fd: Arc>) -> Self { - Self { - inner: socket, - async_fd, - } - } - - /// Asynchronously sends a batch of frames to the socket. - #[inline(always)] - pub fn send>(&mut self, batch: B) -> SmolSendFuture<'_, 'umem, B> { - SmolSendFuture { - socket: self, - batch, - } - } -} - -impl<'umem> Deref for SmolSocketTx<'umem> { - type Target = SocketTx<'umem>; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl<'umem> DerefMut for SmolSocketTx<'umem> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner - } -} - -/// A future that asynchronously sends a batch of frames to the socket. -pub struct SmolSendFuture<'sock, 'umem, B: FrameBuffer<'umem>> { - socket: &'sock mut SmolSocketTx<'umem>, - batch: B, -} - -impl<'sock, 'umem, B: FrameBuffer<'umem>> Future for SmolSendFuture<'sock, 'umem, B> { - type Output = Result; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - // SAFETY: We guarantee not to move self, just access its fields. - // Those accesses are also guaranteed to not move self or the fields themselves. - let this = unsafe { self.get_unchecked_mut() }; - - if let Ok(sent) = this.socket.inner.send(&mut this.batch) { - this.socket.maybe_wake()?; - return Poll::Ready(Ok(sent)); - } - - let SmolSocketTx { inner, async_fd } = this.socket; - loop { - ready!(async_fd.poll_writable(cx))?; - - if let Ok(sent) = inner.send(&mut this.batch) { - inner.maybe_wake()?; - return Poll::Ready(Ok(sent)); - } - } - } -} diff --git a/src/xdp/futures/smol/socket.rs b/src/xdp/futures/smol/socket.rs deleted file mode 100644 index 9f6d77c..0000000 --- a/src/xdp/futures/smol/socket.rs +++ /dev/null @@ -1,295 +0,0 @@ -use std::{os::fd::RawFd, sync::Arc}; - -use async_io::Async; - -use crate::xdp::{ - error::Result, - frame::FrameBuffer, - socket::{SocketOwner, SocketRx, SocketTx}, -}; - -use super::{SmolFd, SmolRecvFuture, SmolSendFuture, SmolSocketRx, SmolSocketTx}; - -/// A socket designed to work on the [Smol](https://docs.rs/smol/latest/smol/) runtime. -pub struct SmolSocket<'umem> { - owner: Arc>, - rx: SmolSocketRx<'umem>, - tx: SmolSocketTx<'umem>, -} - -unsafe impl<'umem> Send for SmolSocket<'umem> {} - -impl<'umem> SmolSocket<'umem> { - pub(crate) fn new( - owner: Arc>, - rx: SocketRx<'umem>, - tx: SocketTx<'umem>, - async_fd: Arc>, - ) -> Self { - Self { - owner, - rx: SmolSocketRx::new(rx, async_fd.clone()), - tx: SmolSocketTx::new(tx, async_fd), - } - } - - /// Splits the socket into its owner, rx, and tx components. - #[inline(always)] - pub fn split( - self, - ) -> ( - Arc>, - SmolSocketRx<'umem>, - SmolSocketTx<'umem>, - ) { - (self.owner, self.rx, self.tx) - } - - /// Returns the file descriptor of the socket. - #[inline(always)] - pub fn fd(&self) -> RawFd { - self.owner.fd() - } - - /// Possibly wakes the tx queue, so the kernel continues to process outgoing packets. - #[inline(always)] - pub fn maybe_wake(&self) -> Result<()> { - self.tx.maybe_wake() - } - - /// Receives a batch of frames from the socket. - #[inline(always)] - pub fn recv>(&mut self, batch: B) -> SmolRecvFuture<'_, 'umem, B> { - self.rx.recv(batch) - } - - /// Sends a batch of frames to the socket. - #[inline(always)] - pub fn send>(&mut self, frames: B) -> SmolSendFuture<'_, 'umem, B> { - self.tx.send(frames) - } -} - -#[cfg(test)] -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::socket::Socket; - use crate::xdp::test_utils::TestVethPair; - use crate::xdp::umem::Umem; - - /// Helper to build a minimal Ethernet frame with a payload. - /// Format: [dst MAC (6)] [src MAC (6)] [EtherType (2)] [payload] - fn build_ethernet_frame(dst_mac: &[u8; 6], src_mac: &[u8; 6], payload: &[u8]) -> Vec { - let mut frame = Vec::with_capacity(14 + payload.len()); - frame.extend_from_slice(dst_mac); - frame.extend_from_slice(src_mac); - // EtherType 0x88B5 = Local Experimental EtherType - frame.extend_from_slice(&[0x88, 0xB5]); - frame.extend_from_slice(payload); - frame - } - - /// Test that sends a packet from one side of a veth pair and receives it on the other using smol async. - #[test] - fn test_smol_socket_send_recv() { - smol::block_on(async { - // Create veth pair - let veth = TestVethPair::new().expect("failed to create veth pair"); - - // Create XDP contexts on both ends - let mut ctx_outer = XdpContext::builder(veth.outer_name()) - .attach_mode(AttachMode::default()) - .enable_fragmentation(false) - .build() - .expect("failed to create outer context"); - let mut ctx_inner = XdpContext::builder(veth.inner_name()) - .attach_mode(AttachMode::default()) - .enable_fragmentation(false) - .build() - .expect("failed to create inner context"); - - // Create UMEM for outer socket (sender) - let (umem_outer, _fq_outer, mut cq_outer) = Umem::builder() - .num_frames(64) - .frame_size(4096) - .fill_ring_size(32) - .completion_ring_size(32) - .build_smol(&mut ctx_outer) - .expect("failed to create outer umem") - .split(); - - // Create UMEM for inner socket (receiver) - let (umem_inner, mut fq_inner, _cq_inner) = Umem::builder() - .num_frames(64) - .frame_size(4096) - .fill_ring_size(32) - .completion_ring_size(32) - .build_smol(&mut ctx_inner) - .expect("failed to create inner umem") - .split(); - - // Initialize frame buffers - let mut tx_buffer: BasicFrameBuffer<'_> = umem_outer.init_buffer().unwrap(); - let mut rx_buffer: BasicFrameBuffer<'_> = umem_inner.init_buffer().unwrap(); - - // Create smol sockets - let mut socket_outer = Socket::builder(veth.outer_name(), 0) - .build_smol(&mut ctx_outer, umem_outer.clone()) - .expect("failed to create outer smol socket"); - - let mut socket_inner = Socket::builder(veth.inner_name(), 0) - .build_smol(&mut ctx_inner, umem_inner.clone()) - .expect("failed to create inner smol socket"); - - // Prime the receiver's fill queue so it can receive packets - let rx_prime_count = rx_buffer.num_frames().min(32); - let mut prime_buffer = BasicFrameBuffer::new(rx_prime_count); - for frame in rx_buffer.drain(..rx_prime_count) { - prime_buffer.push(frame); - } - fq_inner - .process_queue(&mut prime_buffer, &[socket_inner.fd()]) - .await - .expect("failed to process fill queue"); - - // Build a test packet - let payload = b"Hello XDP Smol Socket Test!"; - let packet_data = build_ethernet_frame( - veth.inner_mac().as_bytes(), - veth.outer_mac().as_bytes(), - payload, - ); - - // Prepare a frame for sending - let mut send_buffer = BasicFrameBuffer::new(1); - let mut frame = tx_buffer.drain(..1).next().expect("no frames available"); - frame.copy_from(&packet_data); - send_buffer.push(frame); - - // Send the packet (async) - let sent = socket_outer - .send(&mut send_buffer) - .await - .expect("send failed"); - assert_eq!(sent, 1, "expected to send 1 frame"); - assert_eq!(send_buffer.num_frames(), 0, "buffer should be drained"); - - // Receive the packet with timeout using smol::Timer - let mut recv_buffer = BasicFrameBuffer::new(16); - let start = Instant::now(); - let timeout = Duration::from_secs(5); - - let recv_result = smol::future::race( - async { - let r = socket_inner.recv(&mut recv_buffer).await; - Ok(r) - }, - async { - smol::Timer::after(timeout).await; - Err("recv timed out") - }, - ) - .await; - - let received = recv_result.expect("recv timed out").expect("recv failed"); - - assert!(start.elapsed() < timeout, "should receive before timeout"); - assert!(received > 0, "expected to receive at least 1 frame"); - assert!( - recv_buffer.num_frames() >= 1, - "expected at least 1 received frame" - ); - - // Verify the received packet matches what we sent - let recv_frame = recv_buffer.iter_frames().next().unwrap(); - assert!( - recv_frame.len() >= packet_data.len(), - "received frame too short" - ); - assert_eq!( - &recv_frame[..packet_data.len()], - &packet_data[..], - "packet data mismatch" - ); - - // Clean up: process completion queue to reclaim TX frame - let mut reclaim_buffer = BasicFrameBuffer::new(1); - cq_outer - .process_queue(&mut reclaim_buffer) - .await - .expect("failed to process completion queue"); - }); - } - - /// Test smol socket creation and basic properties. - #[test] - fn test_smol_socket_creation() { - smol::block_on(async { - let veth = TestVethPair::new().expect("failed to create veth pair"); - - let mut ctx = XdpContext::builder(veth.outer_name()) - .attach_mode(AttachMode::default()) - .enable_fragmentation(false) - .build() - .expect("failed to create context"); - - let (umem, _fq, _cq) = Umem::builder() - .num_frames(16) - .frame_size(4096) - .fill_ring_size(8) - .completion_ring_size(8) - .build_smol(&mut ctx) - .expect("failed to create umem") - .split(); - - let socket = Socket::builder(veth.outer_name(), 0) - .rx_ring_size(8) - .tx_ring_size(8) - .build_smol(&mut ctx, umem) - .expect("failed to create smol socket"); - - // Socket should have valid fd - assert!(socket.fd() >= 0, "socket fd should be valid"); - - // Context should now have 1 socket registered - assert_eq!(ctx.num_sockets(), 1, "context should have 1 socket"); - }); - } - - /// Test smol socket split into owner, rx, tx components. - #[test] - fn test_smol_socket_split() { - smol::block_on(async { - let veth = TestVethPair::new().expect("failed to create veth pair"); - - let mut ctx = XdpContext::builder(veth.outer_name()) - .attach_mode(AttachMode::default()) - .enable_fragmentation(false) - .build() - .expect("failed to create context"); - - let (umem, _fq, _cq) = Umem::builder() - .num_frames(16) - .frame_size(4096) - .fill_ring_size(8) - .completion_ring_size(8) - .build_smol(&mut ctx) - .expect("failed to create umem") - .split(); - - let socket = Socket::builder(veth.outer_name(), 0) - .build_smol(&mut ctx, umem) - .expect("failed to create smol socket"); - - let original_fd = socket.fd(); - let (owner, _rx, _tx) = socket.split(); - - // Owner should have the same fd - assert_eq!(owner.fd(), original_fd, "owner fd should match original"); - }); - } -} diff --git a/src/xdp/futures/smol/umem.rs b/src/xdp/futures/smol/umem.rs deleted file mode 100644 index 12bb19d..0000000 --- a/src/xdp/futures/smol/umem.rs +++ /dev/null @@ -1,75 +0,0 @@ -use std::{os::raw::c_int, sync::Arc}; - -use async_io::Async; - -use crate::xdp::{ - frame::{Frame, FrameBuffer}, - umem::{CompletionQueue, FillQueue, UmemOwner}, -}; - -use super::{SmolCompFuture, SmolCompletionQueue, SmolFd, SmolFillFuture, SmolFillQueue}; - -/// A UMEM designed to work on the [Smol](https://docs.rs/smol/latest/smol/) runtime. -pub struct SmolUmem<'umem> { - owner: Arc>, - fill_queue: SmolFillQueue<'umem>, - completion_queue: SmolCompletionQueue<'umem>, -} - -impl<'umem> SmolUmem<'umem> { - pub(crate) fn new( - owner: Arc>, - fill_queue: FillQueue<'umem>, - completion_queue: CompletionQueue<'umem>, - async_fd: Arc>, - ) -> Self { - Self { - owner, - fill_queue: SmolFillQueue::new(fill_queue, async_fd.clone()), - completion_queue: SmolCompletionQueue::new(completion_queue, async_fd), - } - } - - /// Splits the umem into its owner, fill queue, and completion queue components. - #[inline(always)] - pub fn split( - self, - ) -> ( - Arc>, - SmolFillQueue<'umem>, - SmolCompletionQueue<'umem>, - ) { - (self.owner, self.fill_queue, self.completion_queue) - } - - /// Returns the owner of the umem. - #[inline(always)] - pub fn owner(&self) -> &Arc> { - &self.owner - } - - /// 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 { - self.owner.init_buffer() - } - - /// 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<'fd, B: FrameBuffer<'umem>>( - &mut self, - batch: B, - fds: &'fd [c_int], - ) -> SmolFillFuture<'_, 'umem, 'fd, B> { - self.fill_queue.process_queue(batch, fds) - } - - /// Processes the completion queue, allocating new frames from the frame stack and submitting them to the completion ring up to the size of the completion ring. - #[inline(always)] - pub fn process_completion_queue<'que, B: FrameBuffer<'umem>>( - &'que mut self, - batch: B, - ) -> SmolCompFuture<'que, 'umem, B> { - self.completion_queue.process_queue(batch) - } -} diff --git a/src/xdp/futures/tokio/comp.rs b/src/xdp/futures/tokio/comp.rs deleted file mode 100644 index d9badd2..0000000 --- a/src/xdp/futures/tokio/comp.rs +++ /dev/null @@ -1,90 +0,0 @@ -use std::{ - ops::{Deref, DerefMut}, - os::fd::RawFd, - pin::Pin, - sync::Arc, - task::{Context, Poll}, -}; - -use futures_core::ready; -use tokio::io::{Ready, unix::AsyncFd}; - -use crate::xdp::{error::Result, frame::FrameBuffer, umem::CompletionQueue}; - -/// A completion queue designed to work on the [Tokio] runtime. -/// -/// [Tokio]: tokio -pub struct TokioCompletionQueue<'umem> { - inner: CompletionQueue<'umem>, - async_fd: Arc>, -} - -impl<'umem> TokioCompletionQueue<'umem> { - pub(crate) fn new( - completion_queue: CompletionQueue<'umem>, - async_fd: Arc>, - ) -> Self { - Self { - inner: completion_queue, - async_fd, - } - } - - /// Asynchronously processes the completion queue, pushing new frames from the frame stack into the completion ring up to the size of the completion ring. - #[inline(always)] - pub fn process_queue<'que, B: FrameBuffer<'umem>>( - &'que mut self, - batch: B, - ) -> TokioCompFuture<'que, 'umem, B> { - TokioCompFuture { - completion_queue: self, - batch, - } - } -} - -impl<'umem> Deref for TokioCompletionQueue<'umem> { - type Target = CompletionQueue<'umem>; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl<'umem> DerefMut for TokioCompletionQueue<'umem> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner - } -} - -/// A future that asynchronously processes the completion queue, pushing new frames from the frame stack into the completion ring up to the size of the completion ring. -pub struct TokioCompFuture<'que, 'umem, B: FrameBuffer<'umem>> { - completion_queue: &'que mut TokioCompletionQueue<'umem>, - batch: B, -} - -impl<'que, 'umem, B: FrameBuffer<'umem>> Future for TokioCompFuture<'que, 'umem, B> { - type Output = Result<()>; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - // SAFETY: We guarantee not to move self, just access its fields. - // 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) { - return Poll::Ready(Ok(())); - } - - let TokioCompletionQueue { inner, async_fd } = this.completion_queue; - loop { - let mut guard = ready!(async_fd.poll_write_ready(cx))?; - - if let Ok(_) = inner.process_queue(&mut this.batch) { - return Poll::Ready(Ok(())); - } - - // We aren't actually ready clear our status and loop again. - guard.clear_ready_matching(Ready::WRITABLE); - } - } -} diff --git a/src/xdp/futures/tokio/fd.rs b/src/xdp/futures/tokio/fd.rs deleted file mode 100644 index 72c7d3c..0000000 --- a/src/xdp/futures/tokio/fd.rs +++ /dev/null @@ -1,39 +0,0 @@ -use std::{os::fd::RawFd, sync::Arc}; - -use dashmap::DashMap; -use tokio::io::unix::AsyncFd; - -use crate::xdp::error::Result; - -/// A factory for creating [AsyncFd] instances. -/// -/// The purpose of this is to cache [AsyncFd] file descriptors for the lifetime of the program, this is important -/// because [AsyncFd] file descriptors must be unique and we have multiple components that all need a copy. So we -/// store a [`Arc>`] in a [DashMap] for the lifetime of the program to share across components with t -/// he same fd. -pub struct TokioFdFactory { - inner: DashMap>>, -} - -impl TokioFdFactory { - pub(crate) fn new() -> Self { - Self { - inner: DashMap::new(), - } - } - - /// Creates a new [AsyncFd] instance for the given file descriptor, or return a cached instance if it already exists. - /// - /// This returns an error in the event a registration occurs and fails. - pub fn get_tokio_fd(&self, fd: RawFd) -> Result>> { - use dashmap::Entry::*; - match self.inner.entry(fd) { - Occupied(entry) => Ok(entry.get().clone()), - Vacant(entry) => { - let async_fd = Arc::new(AsyncFd::new(fd)?); - entry.insert(async_fd.clone()); - Ok(async_fd) - } - } - } -} diff --git a/src/xdp/futures/tokio/fill.rs b/src/xdp/futures/tokio/fill.rs deleted file mode 100644 index 54d32af..0000000 --- a/src/xdp/futures/tokio/fill.rs +++ /dev/null @@ -1,97 +0,0 @@ -use std::{ - ops::{Deref, DerefMut}, - os::{fd::RawFd, raw::c_int}, - pin::Pin, - sync::Arc, - task::{Context, Poll}, -}; - -use futures_core::ready; -use tokio::io::{Ready, unix::AsyncFd}; - -use crate::xdp::{error::Result, frame::FrameBuffer, umem::FillQueue}; - -/// A fill queue designed to work on the [Tokio] runtime. -/// -/// [Tokio]: tokio -pub struct TokioFillQueue<'umem> { - inner: FillQueue<'umem>, - async_fd: Arc>, -} - -impl<'umem> TokioFillQueue<'umem> { - pub(crate) fn new(fill_queue: FillQueue<'umem>, async_fd: Arc>) -> Self { - Self { - inner: fill_queue, - async_fd, - } - } - - /// Asynchronously 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<'fd, B: FrameBuffer<'umem>>( - &mut self, - batch: B, - fds: &'fd [c_int], - ) -> TokioFillFuture<'_, 'umem, 'fd, B> { - TokioFillFuture { - fill_queue: self, - batch, - fds, - } - } -} - -impl<'umem> Deref for TokioFillQueue<'umem> { - type Target = FillQueue<'umem>; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl<'umem> DerefMut for TokioFillQueue<'umem> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner - } -} - -/// A future that asynchronously 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. -pub struct TokioFillFuture<'que, 'umem, 'fd, B: FrameBuffer<'umem>> { - fill_queue: &'que mut TokioFillQueue<'umem>, - batch: B, - fds: &'fd [c_int], -} - -impl<'que, 'umem, 'fd, B: FrameBuffer<'umem>> Future for TokioFillFuture<'que, 'umem, 'fd, B> { - type Output = Result<()>; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - // SAFETY: We guarantee not to move self, just access its fields. - // Those accesses are also guaranteed to not move self or the fields themselves. - let this = unsafe { self.get_unchecked_mut() }; - - for fd in this.fds.iter() { - if let Err(e) = this.fill_queue.maybe_wake(*fd) { - return Poll::Ready(Err(e)); - } - } - - this.fill_queue.inner.process_queue(&mut this.batch); - if this.batch.num_frames() == 0 { - return Poll::Ready(Ok(())); - } - - let TokioFillQueue { inner, async_fd } = this.fill_queue; - loop { - let mut guard = ready!(async_fd.poll_write_ready(cx))?; - - inner.process_queue(&mut this.batch); - if this.batch.num_frames() == 0 { - return Poll::Ready(Ok(())); - } - - guard.clear_ready_matching(Ready::WRITABLE); - } - } -} diff --git a/src/xdp/futures/tokio/mod.rs b/src/xdp/futures/tokio/mod.rs deleted file mode 100644 index 81e3f24..0000000 --- a/src/xdp/futures/tokio/mod.rs +++ /dev/null @@ -1,21 +0,0 @@ -//! Tokio executor futures for AF_XDP. -//! -//! This module provides futures for the [Tokio] runtime, and associated types for working with the XDP subsystem. -//! -//! [Tokio]: tokio - -mod comp; -mod fd; -mod fill; -mod recv; -mod send; -mod socket; -mod umem; - -pub use comp::{TokioCompFuture, TokioCompletionQueue}; -pub use fd::TokioFdFactory; -pub use fill::{TokioFillFuture, TokioFillQueue}; -pub use recv::{TokioRecvFuture, TokioSocketRx}; -pub use send::{TokioSendFuture, TokioSocketTx}; -pub use socket::TokioSocket; -pub use umem::TokioUmem; diff --git a/src/xdp/futures/tokio/recv.rs b/src/xdp/futures/tokio/recv.rs deleted file mode 100644 index b29f49c..0000000 --- a/src/xdp/futures/tokio/recv.rs +++ /dev/null @@ -1,84 +0,0 @@ -use std::{ - ops::{Deref, DerefMut}, - os::fd::RawFd, - pin::Pin, - sync::Arc, - task::{Context, Poll}, -}; - -use futures_core::ready; -use tokio::io::{Ready, unix::AsyncFd}; - -use crate::xdp::{error::Result, frame::FrameBuffer, socket::SocketRx}; - -/// A socket receiver designed to work on the [Tokio] runtime. -/// -/// [Tokio]: tokio -pub struct TokioSocketRx<'umem> { - inner: SocketRx<'umem>, - async_fd: Arc>, -} - -impl<'umem> TokioSocketRx<'umem> { - pub(crate) fn new(socket: SocketRx<'umem>, async_fd: Arc>) -> Self { - Self { - inner: socket, - async_fd, - } - } - - /// Asynchronously receives a batch of frames from the socket. - #[inline(always)] - pub fn recv>(&mut self, batch: B) -> TokioRecvFuture<'_, 'umem, B> { - TokioRecvFuture { - socket: self, - batch, - } - } -} - -impl<'umem> Deref for TokioSocketRx<'umem> { - type Target = SocketRx<'umem>; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl<'umem> DerefMut for TokioSocketRx<'umem> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner - } -} - -/// A future that asynchronously receives a batch of frames from the socket. -pub struct TokioRecvFuture<'sock, 'umem, B: FrameBuffer<'umem>> { - socket: &'sock mut TokioSocketRx<'umem>, - batch: B, -} - -impl<'sock, 'umem, B: FrameBuffer<'umem>> Future for TokioRecvFuture<'sock, 'umem, B> { - type Output = Result; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - // SAFETY: We guarantee not to move self, just access its fields. - // Those accesses are also guaranteed to not move self or the fields themselves. - let this = unsafe { self.get_unchecked_mut() }; - - if let Ok(received) = this.socket.inner.recv(&mut this.batch) { - return Poll::Ready(Ok(received)); - } - - let TokioSocketRx { inner, async_fd } = this.socket; - loop { - let mut guard = ready!(async_fd.poll_read_ready(cx))?; - - match inner.recv(&mut this.batch) { - Ok(received) => { - return Poll::Ready(Ok(received)); - } - Err(_) => guard.clear_ready_matching(Ready::READABLE), - } - } - } -} diff --git a/src/xdp/futures/tokio/send.rs b/src/xdp/futures/tokio/send.rs deleted file mode 100644 index ab1c31d..0000000 --- a/src/xdp/futures/tokio/send.rs +++ /dev/null @@ -1,86 +0,0 @@ -use std::{ - ops::{Deref, DerefMut}, - os::fd::RawFd, - pin::Pin, - sync::Arc, - task::{Context, Poll}, -}; - -use futures_core::ready; -use tokio::io::{Ready, unix::AsyncFd}; - -use crate::xdp::{error::Result, frame::FrameBuffer, socket::SocketTx}; - -/// A socket transmitter designed to work on the [Tokio] runtime. -/// -/// [Tokio]: tokio -pub struct TokioSocketTx<'umem> { - inner: SocketTx<'umem>, - async_fd: Arc>, -} - -impl<'umem> TokioSocketTx<'umem> { - pub(crate) fn new(socket: SocketTx<'umem>, async_fd: Arc>) -> Self { - Self { - inner: socket, - async_fd, - } - } - - /// Asynchronously sends a batch of frames to the socket. - #[inline(always)] - pub fn send>(&mut self, batch: B) -> TokioSendFuture<'_, 'umem, B> { - TokioSendFuture { - socket: self, - batch, - } - } -} - -impl<'umem> Deref for TokioSocketTx<'umem> { - type Target = SocketTx<'umem>; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl<'umem> DerefMut for TokioSocketTx<'umem> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner - } -} - -/// A future that asynchronously sends a batch of frames to the socket. -pub struct TokioSendFuture<'sock, 'umem, B: FrameBuffer<'umem>> { - socket: &'sock mut TokioSocketTx<'umem>, - batch: B, -} - -impl<'sock, 'umem, B: FrameBuffer<'umem>> Future for TokioSendFuture<'sock, 'umem, B> { - type Output = Result; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - // SAFETY: We guarantee not to move self, just access its fields. - // Those accesses are also guaranteed to not move self or the fields themselves. - let this = unsafe { self.get_unchecked_mut() }; - - if let Ok(sent) = this.socket.inner.send(&mut this.batch) { - this.socket.maybe_wake()?; - return Poll::Ready(Ok(sent)); - } - - let TokioSocketTx { inner, async_fd } = this.socket; - loop { - let mut guard = ready!(async_fd.poll_write_ready(cx))?; - - match inner.send(&mut this.batch) { - Ok(sent) => { - inner.maybe_wake()?; - return Poll::Ready(Ok(sent)); - } - Err(_) => guard.clear_ready_matching(Ready::WRITABLE), - } - } - } -} diff --git a/src/xdp/futures/tokio/socket.rs b/src/xdp/futures/tokio/socket.rs deleted file mode 100644 index 4be2449..0000000 --- a/src/xdp/futures/tokio/socket.rs +++ /dev/null @@ -1,278 +0,0 @@ -use std::{os::fd::RawFd, sync::Arc}; - -use tokio::io::unix::AsyncFd; - -use crate::xdp::{ - error::Result, - frame::FrameBuffer, - socket::{SocketOwner, SocketRx, SocketTx}, -}; - -use super::{TokioRecvFuture, TokioSendFuture, TokioSocketRx, TokioSocketTx}; - -/// A socket designed to work on the [Tokio] runtime. -/// -/// [Tokio]: tokio -pub struct TokioSocket<'umem> { - owner: Arc>, - rx: TokioSocketRx<'umem>, - tx: TokioSocketTx<'umem>, -} - -unsafe impl<'umem> Send for TokioSocket<'umem> {} - -impl<'umem> TokioSocket<'umem> { - pub(crate) fn new( - owner: Arc>, - rx: SocketRx<'umem>, - tx: SocketTx<'umem>, - async_fd: Arc>, - ) -> Self { - Self { - owner, - rx: TokioSocketRx::new(rx, async_fd.clone()), - tx: TokioSocketTx::new(tx, async_fd), - } - } - - /// Splits the socket into its owner, rx, and tx components. - #[inline(always)] - pub fn split( - self, - ) -> ( - Arc>, - TokioSocketRx<'umem>, - TokioSocketTx<'umem>, - ) { - (self.owner, self.rx, self.tx) - } - - /// Returns the file descriptor of the socket. - #[inline(always)] - pub fn fd(&self) -> RawFd { - self.owner.fd() - } - - /// Possibly wakes the tx queue, so the kernel continues to process outgoing packets. - #[inline(always)] - pub fn maybe_wake(&self) -> Result<()> { - self.tx.maybe_wake() - } - - /// Receives a batch of frames from the socket. - #[inline(always)] - pub fn recv>(&mut self, batch: B) -> TokioRecvFuture<'_, 'umem, B> { - self.rx.recv(batch) - } - - /// Sends a batch of frames to the socket. - #[inline(always)] - pub fn send>(&mut self, frames: B) -> TokioSendFuture<'_, 'umem, B> { - self.tx.send(frames) - } -} - -#[cfg(test)] -mod tests { - use std::time::Duration; - - use crate::xdp::context::XdpContext; - use crate::xdp::frame::{BasicFrameBuffer, FrameBuffer}; - use crate::xdp::program::AttachMode; - use crate::xdp::socket::Socket; - use crate::xdp::test_utils::TestVethPair; - use crate::xdp::umem::Umem; - - /// Helper to build a minimal Ethernet frame with a payload. - /// Format: [dst MAC (6)] [src MAC (6)] [EtherType (2)] [payload] - fn build_ethernet_frame(dst_mac: &[u8; 6], src_mac: &[u8; 6], payload: &[u8]) -> Vec { - let mut frame = Vec::with_capacity(14 + payload.len()); - frame.extend_from_slice(dst_mac); - frame.extend_from_slice(src_mac); - // EtherType 0x88B5 = Local Experimental EtherType - frame.extend_from_slice(&[0x88, 0xB5]); - frame.extend_from_slice(payload); - frame - } - - /// Test that sends a packet from one side of a veth pair and receives it on the other using tokio async. - #[tokio::test] - async fn test_tokio_socket_send_recv() { - // Create veth pair - let veth = TestVethPair::new().expect("failed to create veth pair"); - - // Create XDP contexts on both ends - let mut ctx_outer = XdpContext::builder(veth.outer_name()) - .attach_mode(AttachMode::default()) - .enable_fragmentation(false) - .build() - .expect("failed to create outer context"); - let mut ctx_inner = XdpContext::builder(veth.inner_name()) - .attach_mode(AttachMode::default()) - .enable_fragmentation(false) - .build() - .expect("failed to create inner context"); - - // Create UMEM for outer socket (sender) - let (umem_outer, _fq_outer, mut cq_outer) = Umem::builder() - .num_frames(64) - .frame_size(4096) - .fill_ring_size(32) - .completion_ring_size(32) - .build_tokio(&mut ctx_outer) - .expect("failed to create outer umem") - .split(); - - // Create UMEM for inner socket (receiver) - let (umem_inner, mut fq_inner, _cq_inner) = Umem::builder() - .num_frames(64) - .frame_size(4096) - .fill_ring_size(32) - .completion_ring_size(32) - .build_tokio(&mut ctx_inner) - .expect("failed to create inner umem") - .split(); - - // Initialize frame buffers - let mut tx_buffer: BasicFrameBuffer<'_> = umem_outer.init_buffer().unwrap(); - let mut rx_buffer: BasicFrameBuffer<'_> = umem_inner.init_buffer().unwrap(); - - // Create tokio sockets - let mut socket_outer = Socket::builder(veth.outer_name(), 0) - .build_tokio(&mut ctx_outer, umem_outer.clone()) - .expect("failed to create outer tokio socket"); - - let mut socket_inner = Socket::builder(veth.inner_name(), 0) - .build_tokio(&mut ctx_inner, umem_inner.clone()) - .expect("failed to create inner tokio socket"); - - // Prime the receiver's fill queue so it can receive packets - let rx_prime_count = rx_buffer.num_frames().min(32); - let mut prime_buffer = BasicFrameBuffer::new(rx_prime_count); - for frame in rx_buffer.drain(..rx_prime_count) { - prime_buffer.push(frame); - } - fq_inner - .process_queue(&mut prime_buffer, &[socket_inner.fd()]) - .await - .expect("failed to process fill queue"); - - // Build a test packet - let payload = b"Hello XDP Tokio Socket Test!"; - let packet_data = build_ethernet_frame( - veth.inner_mac().as_bytes(), - veth.outer_mac().as_bytes(), - payload, - ); - - // Prepare a frame for sending - let mut send_buffer = BasicFrameBuffer::new(1); - let mut frame = tx_buffer.drain(..1).next().expect("no frames available"); - frame.copy_from(&packet_data); - send_buffer.push(frame); - - // Send the packet (async) - let sent = socket_outer - .send(&mut send_buffer) - .await - .expect("send failed"); - assert_eq!(sent, 1, "expected to send 1 frame"); - assert_eq!(send_buffer.num_frames(), 0, "buffer should be drained"); - - // Receive the packet with timeout (async) - let mut recv_buffer = BasicFrameBuffer::new(16); - let recv_result = - tokio::time::timeout(Duration::from_secs(5), socket_inner.recv(&mut recv_buffer)).await; - - let received = recv_result.expect("recv timed out").expect("recv failed"); - - assert!(received > 0, "expected to receive at least 1 frame"); - assert!( - recv_buffer.num_frames() >= 1, - "expected at least 1 received frame" - ); - - // Verify the received packet matches what we sent - let recv_frame = recv_buffer.iter_frames().next().unwrap(); - assert!( - recv_frame.len() >= packet_data.len(), - "received frame too short" - ); - assert_eq!( - &recv_frame[..packet_data.len()], - &packet_data[..], - "packet data mismatch" - ); - - // Clean up: process completion queue to reclaim TX frame - let mut reclaim_buffer = BasicFrameBuffer::new(1); - cq_outer - .process_queue(&mut reclaim_buffer) - .await - .expect("failed to process completion queue"); - } - - /// Test tokio socket creation and basic properties. - #[tokio::test] - async fn test_tokio_socket_creation() { - let veth = TestVethPair::new().expect("failed to create veth pair"); - - let mut ctx = XdpContext::builder(veth.outer_name()) - .attach_mode(AttachMode::default()) - .enable_fragmentation(false) - .build() - .expect("failed to create context"); - - let (umem, _fq, _cq) = Umem::builder() - .num_frames(16) - .frame_size(4096) - .fill_ring_size(8) - .completion_ring_size(8) - .build_tokio(&mut ctx) - .expect("failed to create umem") - .split(); - - let socket = Socket::builder(veth.outer_name(), 0) - .rx_ring_size(8) - .tx_ring_size(8) - .build_tokio(&mut ctx, umem) - .expect("failed to create tokio socket"); - - // Socket should have valid fd - assert!(socket.fd() >= 0, "socket fd should be valid"); - - // Context should now have 1 socket registered - assert_eq!(ctx.num_sockets(), 1, "context should have 1 socket"); - } - - /// Test tokio socket split into owner, rx, tx components. - #[tokio::test] - async fn test_tokio_socket_split() { - let veth = TestVethPair::new().expect("failed to create veth pair"); - - let mut ctx = XdpContext::builder(veth.outer_name()) - .attach_mode(AttachMode::default()) - .enable_fragmentation(false) - .build() - .expect("failed to create context"); - - let (umem, _fq, _cq) = Umem::builder() - .num_frames(16) - .frame_size(4096) - .fill_ring_size(8) - .completion_ring_size(8) - .build_tokio(&mut ctx) - .expect("failed to create umem") - .split(); - - let socket = Socket::builder(veth.outer_name(), 0) - .build_tokio(&mut ctx, umem) - .expect("failed to create tokio socket"); - - let original_fd = socket.fd(); - let (owner, _rx, _tx) = socket.split(); - - // Owner should have the same fd - assert_eq!(owner.fd(), original_fd, "owner fd should match original"); - } -} diff --git a/src/xdp/futures/tokio/umem.rs b/src/xdp/futures/tokio/umem.rs deleted file mode 100644 index 84d445e..0000000 --- a/src/xdp/futures/tokio/umem.rs +++ /dev/null @@ -1,79 +0,0 @@ -use std::{ - os::{fd::RawFd, raw::c_int}, - sync::Arc, -}; - -use tokio::io::unix::AsyncFd; - -use crate::xdp::{ - frame::{Frame, FrameBuffer}, - futures::{TokioCompFuture, TokioCompletionQueue, TokioFillFuture, TokioFillQueue}, - umem::{CompletionQueue, FillQueue, UmemOwner}, -}; - -/// A UMEM designed to work on the [Tokio] runtime. -/// -/// [Tokio]: tokio -pub struct TokioUmem<'umem> { - owner: Arc>, - fill_queue: TokioFillQueue<'umem>, - completion_queue: TokioCompletionQueue<'umem>, -} - -impl<'umem> TokioUmem<'umem> { - pub(crate) fn new( - owner: Arc>, - fill_queue: FillQueue<'umem>, - completion_queue: CompletionQueue<'umem>, - async_fd: Arc>, - ) -> Self { - Self { - owner, - fill_queue: TokioFillQueue::new(fill_queue, async_fd.clone()), - completion_queue: TokioCompletionQueue::new(completion_queue, async_fd), - } - } - - /// Splits the umem into its owner, fill queue, and completion queue components. - #[inline(always)] - pub fn split( - self, - ) -> ( - Arc>, - TokioFillQueue<'umem>, - TokioCompletionQueue<'umem>, - ) { - (self.owner, self.fill_queue, self.completion_queue) - } - - /// Returns the owner of the umem. - #[inline(always)] - pub fn owner(&self) -> &Arc> { - &self.owner - } - - /// 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 { - self.owner.init_buffer() - } - - /// 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<'fd, B: FrameBuffer<'umem>>( - &mut self, - batch: B, - fds: &'fd [c_int], - ) -> TokioFillFuture<'_, 'umem, 'fd, B> { - self.fill_queue.process_queue(batch, fds) - } - - /// Processes the completion queue, allocating new frames from the frame stack and submitting them to the completion ring up to the size of the completion ring. - #[inline(always)] - pub fn process_completion_queue<'que, B: FrameBuffer<'umem>>( - &'que mut self, - batch: B, - ) -> TokioCompFuture<'que, 'umem, B> { - self.completion_queue.process_queue(batch) - } -} diff --git a/src/xdp/mod.rs b/src/xdp/mod.rs index f3ecba5..ed91e92 100644 --- a/src/xdp/mod.rs +++ b/src/xdp/mod.rs @@ -12,8 +12,6 @@ pub mod context; pub mod error; mod flags; pub mod frame; -#[cfg(any(feature = "tokio", feature = "smol", feature = "local"))] -pub mod futures; pub mod program; pub mod ring; pub mod socket; 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/map.rs b/src/xdp/program/map.rs index 9f29bf8..391ac5f 100644 --- a/src/xdp/program/map.rs +++ b/src/xdp/program/map.rs @@ -85,3 +85,49 @@ impl Map { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + + fn make_test_map() -> Map { + let mut info: bpf_map_info = unsafe { std::mem::zeroed() }; + let bytes = b"test_map"; + for (i, &b) in bytes.iter().enumerate() { + #[cfg(target_arch = "aarch64")] + { + info.name[i] = b; + } + #[cfg(target_arch = "x86_64")] + { + info.name[i] = b as i8; + } + } + Map::new(std::ptr::null_mut(), info) + } + + #[test] + fn name_returns_map_name() { + let map = make_test_map(); + assert!(map.name().starts_with("test_map")); + } + + #[test] + fn as_ptr_returns_inner() { + let map = make_test_map(); + assert!(map.as_ptr().is_null()); + } + + #[test] + fn as_mut_ptr_returns_inner() { + let mut map = make_test_map(); + assert!(map.as_mut_ptr().is_null()); + } + + #[test] + fn info_returns_info() { + let map = make_test_map(); + let info = map.info(); + assert_eq!(info.type_, 0); + } +} diff --git a/src/xdp/program/prog.rs b/src/xdp/program/prog.rs index 1654a54..ec4801f 100644 --- a/src/xdp/program/prog.rs +++ b/src/xdp/program/prog.rs @@ -8,19 +8,11 @@ use libxdp_sys::{ xdp_program__attach, xdp_program__bpf_obj, xdp_program__close, xdp_program__detach, xdp_program__from_bpf_obj, xdp_program__set_xdp_frags_support, }; -use neli::{ - consts::{ - nl::NlmF, - rtnl::{Ifla, RtAddrFamily, Rtm}, - socket::NlFamily, - }, - nl::NlPayload, - router::synchronous::NlRouter, - rtnl::{Ifinfomsg, IfinfomsgBuilder}, - utils::Groups, -}; -use crate::xdp::error::{Error, Result, get_xdp_error_message}; +use crate::{ + netlink::{get_checksum_offload, get_mtu}, + xdp::error::{Error, Result, get_xdp_error_message}, +}; use super::{AttachMode, Map, XdpInfo}; @@ -93,6 +85,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, @@ -171,36 +167,37 @@ impl Drop for XdpProgram { } } -fn get_mtu(if_index: i32) -> Result { - let (rtnl, _) = NlRouter::connect(NlFamily::Route, None, Groups::empty()) - .map_err(|e| Error::GetMtu(e.to_string()))?; - - let ifinfomsg = IfinfomsgBuilder::default() - .ifi_family(RtAddrFamily::Netlink) - .ifi_index(if_index) - .build() - .map_err(|e| Error::GetMtu(e.to_string()))?; - - let recv = rtnl - .send::<_, _, Rtm, Ifinfomsg>( - Rtm::Getlink, - NlmF::DUMP_FILTERED | NlmF::REQUEST, - NlPayload::Payload(ifinfomsg), - ) - .map_err(|e| Error::GetMtu(e.to_string()))?; - - for response in recv { - let mut response = response.map_err(|e| Error::GetMtu(e.to_string()))?; - if let Some(payload) = response.get_payload() { - return payload - .rtattrs() - .get_attr_handle() - .get_attr_payload_as::(Ifla::Mtu) - .map_err(|e| Error::GetMtu(e.to_string())); - } - if let Some(err) = response.get_err() { - return Err(Error::GetMtu(err.to_string())); - } +#[cfg(test)] +mod tests { + use super::*; + + // `XdpProgram::new()` is the only constructor for real programs. The two + // error paths that can be triggered without real BPF/XDP infrastructure are: + // + // 1. An interface name containing a NUL byte — `CString::new` fails and + // `Error::InterfaceNameToIndex` is returned before any kernel call. + // 2. A syntactically valid but nonexistent interface name — `if_nametoindex` + // returns 0 and `Error::InterfaceNotFound` is returned before any BPF + // object is opened. + // + // All paths beyond those two require a real BPF object and a real network + // interface, so they are skipped here. + + #[test] + fn new_rejects_null_byte_in_interface_name() { + let result = XdpProgram::new(b"", "eth\x000", AttachMode::Skb, false); + assert!( + matches!(result, Err(Error::InterfaceNameToIndex(_))), + "expected InterfaceNameToIndex error" + ); + } + + #[test] + fn new_rejects_nonexistent_interface() { + let result = XdpProgram::new(b"", "voidnet_no_such_iface_xyz", AttachMode::Skb, false); + assert!( + matches!(result, Err(Error::InterfaceNotFound)), + "expected InterfaceNotFound error" + ); } - Err(Error::GetMtu(format!("Interface {} not found", if_index))) } diff --git a/src/xdp/ring/consumer.rs b/src/xdp/ring/consumer.rs index eb28d7a..8d3b857 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)] diff --git a/src/xdp/ring/producer.rs b/src/xdp/ring/producer.rs index 684cdbc..741c700 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. @@ -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,9 +182,9 @@ 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)] @@ -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,7 +222,7 @@ 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)] diff --git a/src/xdp/socket/socket.rs b/src/xdp/socket/socket.rs index 89fe3f3..50a9dfe 100644 --- a/src/xdp/socket/socket.rs +++ b/src/xdp/socket/socket.rs @@ -18,13 +18,6 @@ use crate::xdp::{ umem::UmemOwner, }; -#[cfg(feature = "local")] -use crate::xdp::futures::LocalSocket; -#[cfg(feature = "smol")] -use crate::xdp::futures::SmolSocket; -#[cfg(feature = "tokio")] -use crate::xdp::futures::TokioSocket; - use super::{SocketOwner, SocketRx, SocketTx}; /// Builder for creating a new socket. @@ -143,44 +136,6 @@ impl<'if_name> SocketBuilder<'if_name> { ) -> Result> { self.build_internal(ctx, umem) } - - /// Builds the socket as a [TokioSocket]. - #[cfg(feature = "tokio")] - pub fn build_tokio<'umem>( - mut self, - ctx: &mut XdpContext, - umem: Arc>, - ) -> Result> { - let (owner, rx, tx) = self.build_internal(ctx, umem)?.split(); - let async_fd = ctx.get_tokio_fd(owner.fd())?; - - Ok(TokioSocket::new(owner, rx, tx, async_fd)) - } - - /// Builds the socket as a [LocalSocket]. - #[cfg(feature = "local")] - pub fn build_local<'umem>( - mut self, - ctx: &mut XdpContext, - umem: Arc>, - ) -> Result> { - let (owner, rx, tx) = self.build_internal(ctx, umem)?.split(); - - Ok(LocalSocket::new(owner, rx, tx)?) - } - - /// Builds the socket as a [SmolSocket]. - #[cfg(feature = "smol")] - pub fn build_smol<'umem>( - mut self, - ctx: &mut XdpContext, - umem: Arc>, - ) -> Result> { - let (owner, rx, tx) = self.build_internal(ctx, umem)?.split(); - let async_fd = ctx.get_smol_fd(owner.fd())?; - - Ok(SmolSocket::new(owner, rx, tx, async_fd)) - } } /// A socket for reading and writing packets to an XDP network interface. @@ -241,7 +196,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(), @@ -258,7 +213,7 @@ impl<'umem> Socket<'umem> { let tx = unsafe { tx.assume_init() }; let mut owner = SocketOwner::new(umem.clone(), xsk); - xdp_ctx.register_socket(&mut owner)?; + xdp_ctx.register_socket(&mut owner, queue)?; let owner = Arc::new(owner); let rx = SocketRx::new(owner.clone(), rx); let tx = SocketTx::new(owner.clone(), tx, busy_poll); @@ -362,11 +317,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 +397,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"); @@ -538,9 +497,6 @@ mod tests { // Socket should have valid fd assert!(socket.fd() >= 0, "socket fd should be valid"); - - // Context should now have 1 socket registered - assert_eq!(ctx.num_sockets(), 1, "context should have 1 socket"); } /// Test socket split into owner, rx, tx components. @@ -605,7 +561,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 +607,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 +621,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..9295cdc 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}, }; @@ -56,19 +56,22 @@ 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); + 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..8155233 100644 --- a/src/xdp/umem/owner.rs +++ b/src/xdp/umem/owner.rs @@ -1,5 +1,3 @@ -#[cfg(feature = "async")] -use std::os::fd::RawFd; use std::{ marker::PhantomData, sync::{ @@ -8,8 +6,6 @@ use std::{ }, }; -#[cfg(feature = "async")] -use libxdp_sys::xsk_umem__fd; use libxdp_sys::{xsk_umem, xsk_umem__delete}; use memmap2::MmapMut; @@ -27,8 +23,6 @@ pub struct UmemOwner<'umem> { frame_size: usize, num_frames: usize, init: AtomicBool, - #[cfg(feature = "async")] - fd: RawFd, // Ok so some explanation here, to make sure our Frame's can't outlive the actual memory that is backing them we need some lifetime to use. That said // we are going to end up being wrapped in an Arc which will lose all concept of lifetimes for its references as it should. So to get around this @@ -56,8 +50,6 @@ impl<'umem> UmemOwner<'umem> { frame_size, num_frames, init: AtomicBool::new(false), - #[cfg(feature = "async")] - fd: unsafe { xsk_umem__fd(umem) }, _lifetime: PhantomData, } } @@ -88,13 +80,6 @@ impl<'umem> UmemOwner<'umem> { ) } - /// Returns the file descriptor of the umem. - #[cfg(feature = "async")] - #[inline(always)] - pub(crate) fn fd(&self) -> RawFd { - self.fd - } - /// Returns the pointer to the umem. #[inline(always)] pub(crate) fn as_ptr(&self) -> *mut xsk_umem { @@ -120,6 +105,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 +130,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 +192,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..8099011 100644 --- a/src/xdp/umem/umem.rs +++ b/src/xdp/umem/umem.rs @@ -11,24 +11,16 @@ use libxdp_sys::{ }; use memmap2::MmapOptions; -#[cfg(any(feature = "tokio", feature = "smol"))] -use crate::xdp::context::XdpContext; use crate::xdp::{ error::{Error, NonBlocking, Result}, frame::{Frame, FrameBuffer}, ring::{Consumer, Producer}, }; -#[cfg(feature = "local")] -use crate::xdp::futures::LocalUmem; -#[cfg(feature = "smol")] -use crate::xdp::futures::SmolUmem; -#[cfg(feature = "tokio")] -use crate::xdp::futures::TokioUmem; - use super::{CompletionQueue, FillQueue, UmemOwner}; /// A builder for creating a new [Umem] instance. +#[derive(Clone)] pub struct UmemBuilder { completion_ring_size: u32, fill_ring_size: u32, @@ -135,30 +127,6 @@ impl UmemBuilder { pub fn build<'umem>(mut self) -> Result> { self.build_internal() } - - /// Builds the Umem as a Tokio Umem. - #[cfg(feature = "tokio")] - pub fn build_tokio<'umem>(mut self, ctx: &mut XdpContext) -> Result> { - let (owner, fq, cq) = self.build_internal()?.split(); - let async_fd = ctx.get_tokio_fd(owner.fd())?; - - Ok(TokioUmem::new(owner, fq, cq, async_fd)) - } - - #[cfg(feature = "local")] - pub fn build_local<'umem>(mut self) -> Result> { - let (owner, fq, cq) = self.build_internal()?.split(); - - Ok(LocalUmem::new(owner, fq, cq)?) - } - - #[cfg(feature = "smol")] - pub fn build_smol<'umem>(mut self, ctx: &mut XdpContext) -> Result> { - let (owner, fq, cq) = self.build_internal()?.split(); - let async_fd = ctx.get_smol_fd(owner.fd())?; - - Ok(SmolUmem::new(owner, fq, cq, async_fd)) - } } /// A high level wrapper around a kernel UMEM object. @@ -190,7 +158,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 +179,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 +226,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 +254,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 +270,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/local.rs b/tests/local.rs deleted file mode 100644 index 52a3ea0..0000000 --- a/tests/local.rs +++ /dev/null @@ -1,156 +0,0 @@ -use std::{ - sync::{ - Arc, - atomic::{AtomicBool, Ordering}, - }, - thread, - time::Duration, -}; - -use libvoid::xdp::{ - context::XdpContext, - frame::{BasicFrameBuffer, FrameBuffer}, - futures::LocalExecutor, - socket::Socket, - test_utils::TestVethPair, - umem::Umem, -}; - -fn build_ethernet_frame(dst_mac: &[u8; 6], src_mac: &[u8; 6], payload: &[u8]) -> Vec { - let mut frame = Vec::with_capacity(14 + payload.len()); - frame.extend_from_slice(dst_mac); - frame.extend_from_slice(src_mac); - // EtherType 0x0800 = IPv4, but we'll use a custom one for testing - frame.extend_from_slice(&[0x88, 0xB5]); // Local Experimental EtherType - frame.extend_from_slice(payload); - frame -} - -#[test] -fn test_local() { - // Create veth pair - let veth = TestVethPair::new().expect("failed to create veth pair"); - - let done_sending = Arc::new(AtomicBool::new(false)); - let target_count = 1_000_000; - - let mut inner_exec = LocalExecutor::new().expect("failed to create inner executor"); - let mut outer_exec = LocalExecutor::new().expect("failed to create outer executor"); - - let inner_done_sending = done_sending.clone(); - let inner_name = veth.inner_name().to_string(); - let mut ctx_inner = XdpContext::builder(&inner_name) - .build() - .expect("failed to create inner context"); - - // Create UMEM for inner socket (receiver) - let mut umem_inner = Umem::builder() - .num_frames(2048) - .build_local() - .expect("failed to create inner umem"); - - // Create inner socket and initialize frame buffer - let mut socket_inner = Socket::builder(&inner_name, 0) - .build_local(&mut ctx_inner, umem_inner.owner().clone()) - .expect("failed to create inner socket"); - let mut rx_buffer: BasicFrameBuffer<'_> = umem_inner.init_buffer().unwrap(); - let inner_fd = socket_inner.fd(); - let inner_task = async move { - // Wake up the fill queue and process the fill queue - umem_inner - .process_fill_queue(&mut rx_buffer, &[socket_inner.fd()]) - .await - .unwrap(); - - let mut count = 0; - while count < target_count { - match socket_inner.recv(&mut rx_buffer).await { - Ok(n) => { - count += n; - } - Err(_) if inner_done_sending.load(Ordering::Relaxed) => break, - Err(_) => { - continue; - } - } - - umem_inner - .process_fill_queue(&mut rx_buffer, &[socket_inner.fd()]) - .await - .unwrap(); - } - assert!(count >= target_count, "did not receive enough packets"); - }; - - let outer_name = veth.outer_name().to_string(); - let outer_mac = veth.outer_mac(); - let inner_mac = veth.inner_mac(); - let mut ctx_outer = XdpContext::builder(&outer_name) - .build() - .expect("failed to create outer context"); - - // Create UMEM for outer socket (sender) - let mut umem_outer = Umem::builder() - .num_frames(10) - .build_local() - .expect("failed to create outer umem"); - - // Initialize frame buffers - let mut tx_buffer: BasicFrameBuffer<'_> = umem_outer.init_buffer().unwrap(); - - // Create sockets - let mut socket_outer = Socket::builder(&outer_name, 0) - .build_local(&mut ctx_outer, umem_outer.owner().clone()) - .expect("failed to create outer socket"); - let outer_fd = socket_outer.fd(); - let outer_task = async move { - let mut count = 0; - - let packet_data = - build_ethernet_frame(inner_mac.as_bytes(), outer_mac.as_bytes(), b"Hello, world!"); - for frame in tx_buffer.iter_frames_mut() { - frame.copy_from(&packet_data); - } - - while count < target_count { - match socket_outer.send(&mut tx_buffer).await { - Ok(sent) => { - count += sent; - } - Err(_) => { - continue; - } - }; - - umem_outer - .process_completion_queue(&mut tx_buffer) - .await - .unwrap(); - - for frame in tx_buffer.iter_frames_mut() { - frame.copy_from(&packet_data); - } - } - assert!(count >= target_count, "did not send enough packets"); - done_sending.store(true, Ordering::Relaxed); - }; - - let inner_handle = thread::spawn(move || { - inner_exec - .register(inner_fd) - .expect("failed to register inner socket"); - inner_exec.run(inner_task); - }); - - thread::sleep(Duration::from_millis(100)); - - let outer_handle = thread::spawn(move || { - outer_exec - .register(outer_fd) - .expect("failed to register outer socket"); - outer_exec.run(outer_task); - }); - - inner_handle.join().unwrap(); - outer_handle.join().unwrap(); -} diff --git a/tests/smol.rs b/tests/smol.rs deleted file mode 100644 index a308192..0000000 --- a/tests/smol.rs +++ /dev/null @@ -1,140 +0,0 @@ -use std::{ - sync::{ - Arc, - atomic::{AtomicBool, Ordering}, - }, - thread, - time::Duration, -}; - -use libvoid::xdp::{ - context::XdpContext, - frame::{BasicFrameBuffer, FrameBuffer}, - socket::Socket, - test_utils::TestVethPair, - umem::Umem, -}; -use macro_rules_attribute::apply; -use smol::Executor; -use smol_macros::test; - -fn build_ethernet_frame(dst_mac: &[u8; 6], src_mac: &[u8; 6], payload: &[u8]) -> Vec { - let mut frame = Vec::with_capacity(14 + payload.len()); - frame.extend_from_slice(dst_mac); - frame.extend_from_slice(src_mac); - // EtherType 0x0800 = IPv4, but we'll use a custom one for testing - frame.extend_from_slice(&[0x88, 0xB5]); // Local Experimental EtherType - frame.extend_from_slice(payload); - frame -} - -#[apply(test!)] -async fn test_smol(exec: &Executor<'_>) { - // Create veth pair - let veth = TestVethPair::new().expect("failed to create veth pair"); - - let done_sending = Arc::new(AtomicBool::new(false)); - let target_count = 1_000_000; - - let inner_name = veth.inner_name().to_string(); - let inner_done_sending = done_sending.clone(); - let inner_task = exec.spawn(async move { - let mut ctx_inner = XdpContext::builder(&inner_name) - .build() - .expect("failed to create inner context"); - - // Create UMEM for inner socket (receiver) - let mut umem_inner = Umem::builder() - .num_frames(2048) - .build_smol(&mut ctx_inner) - .expect("failed to create inner umem"); - - // Create inner socket and initialize frame buffer - let mut socket_inner = Socket::builder(&inner_name, 0) - .build_smol(&mut ctx_inner, umem_inner.owner().clone()) - .expect("failed to create inner socket"); - let mut rx_buffer: BasicFrameBuffer<'_> = umem_inner.init_buffer().unwrap(); - - // Wake up the fill queue and process the fill queue - umem_inner - .process_fill_queue(&mut rx_buffer, &[socket_inner.fd()]) - .await - .unwrap(); - - let mut count = 0; - while count < target_count { - match socket_inner.recv(&mut rx_buffer).await { - Ok(n) => { - count += n; - } - Err(_) if inner_done_sending.load(Ordering::Relaxed) => break, - Err(_) => { - continue; - } - } - - umem_inner - .process_fill_queue(&mut rx_buffer, &[socket_inner.fd()]) - .await - .unwrap(); - } - assert!(count >= target_count, "did not receive enough packets"); - }); - - thread::sleep(Duration::from_millis(100)); - - let outer_name = veth.outer_name().to_string(); - let outer_mac = veth.outer_mac(); - let inner_mac = veth.inner_mac(); - let outer_task = exec.spawn(async move { - let mut ctx_outer = XdpContext::builder(&outer_name) - .build() - .expect("failed to create outer context"); - - // Create UMEM for outer socket (sender) - let mut umem_outer = Umem::builder() - .num_frames(10) - .build_smol(&mut ctx_outer) - .expect("failed to create outer umem"); - - // Initialize frame buffers - let mut tx_buffer: BasicFrameBuffer<'_> = umem_outer.init_buffer().unwrap(); - - // Create sockets - let mut socket_outer = Socket::builder(&outer_name, 0) - .build_smol(&mut ctx_outer, umem_outer.owner().clone()) - .expect("failed to create outer socket"); - - let mut count = 0; - - let packet_data = - build_ethernet_frame(inner_mac.as_bytes(), outer_mac.as_bytes(), b"Hello, world!"); - for frame in tx_buffer.iter_frames_mut() { - frame.copy_from(&packet_data); - } - - while count < target_count { - match socket_outer.send(&mut tx_buffer).await { - Ok(sent) => { - count += sent; - } - Err(_) => { - continue; - } - }; - - umem_outer - .process_completion_queue(&mut tx_buffer) - .await - .unwrap(); - - for frame in tx_buffer.iter_frames_mut() { - frame.copy_from(&packet_data); - } - } - assert!(count >= target_count, "did not send enough packets"); - done_sending.store(true, Ordering::Relaxed); - }); - - smol::future::zip(inner_task, outer_task).await; -} 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(); } diff --git a/tests/tokio.rs b/tests/tokio.rs deleted file mode 100644 index 8e6c1a8..0000000 --- a/tests/tokio.rs +++ /dev/null @@ -1,155 +0,0 @@ -use std::{ - sync::{ - Arc, - atomic::{AtomicBool, Ordering}, - }, - thread, - time::Duration, -}; - -use libvoid::xdp::{ - context::XdpContext, - frame::{BasicFrameBuffer, FrameBuffer}, - socket::Socket, - test_utils::TestVethPair, - umem::Umem, -}; - -fn build_ethernet_frame(dst_mac: &[u8; 6], src_mac: &[u8; 6], payload: &[u8]) -> Vec { - let mut frame = Vec::with_capacity(14 + payload.len()); - frame.extend_from_slice(dst_mac); - frame.extend_from_slice(src_mac); - // EtherType 0x0800 = IPv4, but we'll use a custom one for testing - frame.extend_from_slice(&[0x88, 0xB5]); // Local Experimental EtherType - frame.extend_from_slice(payload); - frame -} - -#[tokio::test(flavor = "multi_thread")] -async fn test_tokio() { - // Create veth pair - let veth = TestVethPair::new().expect("failed to create veth pair"); - - let done_sending = Arc::new(AtomicBool::new(false)); - let target_count = 1_000_000; - - let inner_name = veth.inner_name().to_string(); - let inner_done_sending = done_sending.clone(); - let inner_handle = thread::spawn(move || { - tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .unwrap() - .block_on(async { - let mut ctx_inner = XdpContext::builder(&inner_name) - .build() - .expect("failed to create inner context"); - - // Create UMEM for inner socket (receiver) - let mut umem_inner = Umem::builder() - .num_frames(2048) - .build_tokio(&mut ctx_inner) - .expect("failed to create inner umem"); - - // Create inner socket and initialize frame buffer - let mut socket_inner = Socket::builder(&inner_name, 0) - .build_tokio(&mut ctx_inner, umem_inner.owner().clone()) - .expect("failed to create inner socket"); - let mut rx_buffer: BasicFrameBuffer<'_> = umem_inner.init_buffer().unwrap(); - - // Wake up the fill queue and process the fill queue - umem_inner - .process_fill_queue(&mut rx_buffer, &[socket_inner.fd()]) - .await - .unwrap(); - - let mut count = 0; - while count < target_count { - match socket_inner.recv(&mut rx_buffer).await { - Ok(n) => { - count += n; - } - Err(_) if inner_done_sending.load(Ordering::Relaxed) => break, - Err(_) => { - continue; - } - } - - umem_inner - .process_fill_queue(&mut rx_buffer, &[socket_inner.fd()]) - .await - .unwrap(); - } - assert!(count >= target_count, "did not receive enough packets"); - }); - }); - - thread::sleep(Duration::from_millis(500)); - - let outer_name = veth.outer_name().to_string(); - let outer_mac = veth.outer_mac(); - let inner_mac = veth.inner_mac(); - let outer_handle = thread::spawn(move || { - tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .unwrap() - .block_on(async { - let mut ctx_outer = XdpContext::builder(&outer_name) - .build() - .expect("failed to create outer context"); - - // Create UMEM for outer socket (sender) - let mut umem_outer = Umem::builder() - .num_frames(10) - .build_tokio(&mut ctx_outer) - .expect("failed to create outer umem"); - - // Initialize frame buffers - let mut tx_buffer: BasicFrameBuffer<'_> = umem_outer.init_buffer().unwrap(); - - // Create sockets - let mut socket_outer = Socket::builder(&outer_name, 0) - .build_tokio(&mut ctx_outer, umem_outer.owner().clone()) - .expect("failed to create outer socket"); - - let mut count = 0; - - let packet_data = build_ethernet_frame( - inner_mac.as_bytes(), - outer_mac.as_bytes(), - b"Hello, world!", - ); - for frame in tx_buffer.iter_frames_mut() { - frame.copy_from(&packet_data); - } - - while count < target_count { - match socket_outer.send(&mut tx_buffer).await { - Ok(sent) => { - count += sent; - } - Err(_) => { - continue; - } - }; - - umem_outer - .process_completion_queue(&mut tx_buffer) - .await - .unwrap(); - - for frame in tx_buffer.iter_frames_mut() { - frame.copy_from(&packet_data); - } - // CI is flaky so lets punt writes for tokio. - tokio::task::yield_now().await; - } - assert!(count >= target_count, "did not send enough packets"); - done_sending.store(true, Ordering::Relaxed); - }); - }); - - inner_handle.join().expect("inner thread panicked"); - outer_handle.join().expect("outer thread panicked"); -}