diff options
| -rw-r--r-- | Cargo.lock | 433 | ||||
| -rw-r--r-- | Cargo.toml | 11 | ||||
| -rw-r--r-- | benches/hoptab_bench.rs | 99 | ||||
| -rw-r--r-- | src/opt.rs | 25 | ||||
| -rw-r--r-- | src/pkt.rs | 124 | ||||
| -rw-r--r-- | src/pkt/fake.rs | 143 | ||||
| -rw-r--r-- | src/pkt/hoptab.rs | 551 | ||||
| -rw-r--r-- | src/platform/linux/iptables.rs | 19 | ||||
| -rw-r--r-- | src/platform/linux/nftables.rs | 59 | ||||
| -rw-r--r-- | src/platform/windows.rs | 24 |
10 files changed, 1388 insertions, 100 deletions
@@ -3,6 +3,36 @@ version = 4 [[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloca" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" +dependencies = [ + "cc", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] name = "anyhow" version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -15,6 +45,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -30,12 +66,24 @@ dependencies = [ ] [[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] name = "bytes" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] name = "cc" version = "1.2.53" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -58,6 +106,124 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" + +[[package]] +name = "criterion" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d883447757bb0ee46f233e9dc22eb84d93a9508c9b868687b274fc431d886bf" +dependencies = [ + "alloca", + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "itertools", + "num-traits", + "oorandom", + "page_size", + "plotters", + "rayon", + "regex", + "serde", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed943f81ea2faa8dcecbbfa50164acf95d555afec96a27871663b300e387b2e4" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] name = "ctrlc" version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -85,6 +251,7 @@ name = "dpibreak" version = "0.2.2" dependencies = [ "anyhow", + "criterion", "ctrlc", "etherparse 0.18.2", "nfq-updated", @@ -96,6 +263,12 @@ dependencies = [ ] [[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] name = "etherparse" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -120,12 +293,42 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" [[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] name = "itoa" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] name = "libc" version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -172,6 +375,15 @@ dependencies = [ ] [[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] name = "objc2" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -187,6 +399,56 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" [[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + +[[package]] name = "proc-macro2" version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -205,12 +467,77 @@ dependencies = [ ] [[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] name = "serde" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", + "serde_derive", ] [[package]] @@ -294,6 +621,16 @@ dependencies = [ ] [[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] name = "toml" version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -309,6 +646,102 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] name = "windivert" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -48,3 +48,14 @@ winres = "0.1" [profile.release] lto = true panic = "abort" + +[dev-dependencies] +criterion = { version = "0.8", features = ["html_reports"] } + +[features] +bench = [] + +[[bench]] +name = "hoptab_bench" +harness = false +required-features = ["bench"]
\ No newline at end of file diff --git a/benches/hoptab_bench.rs b/benches/hoptab_bench.rs new file mode 100644 index 0000000..fd9a863 --- /dev/null +++ b/benches/hoptab_bench.rs @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: 2026 Dilluti0n <[email protected]> +// SPDX-License-Identifier: GPL-3.0-or-later + +#![cfg(feature = "bench")] + +use std::net::{IpAddr, Ipv4Addr}; +use criterion::{criterion_group, criterion_main, Criterion, BenchmarkId, BatchSize, Throughput}; +use std::hint::black_box; + +#[macro_use] +#[path = "../src/log.rs"] +mod log; + +#[path = "../src/opt.rs"] +mod opt; + +#[path = "../src/pkt/hoptab.rs"] +pub mod hoptab; + +use hoptab::{put_0, find_0, reset_0}; + +fn prepare_data(count: usize) -> Vec<(IpAddr, u8)> { + (0..count as u32) + .map(|i| (IpAddr::V4(Ipv4Addr::from(i)), (i % 255) as u8)) + .collect() +} + +fn bench_hoptab_operations(c: &mut Criterion) { + let dataset = prepare_data(1024); + let mut group = c.benchmark_group("HopTab_Core"); + + let (test_ip, test_hop) = dataset[0]; + reset_0(); + put_0(test_ip, test_hop); + + group.bench_function("find_hop_hit", |b| { + b.iter(|| { + _ = find_0(black_box(test_ip)); + }) + }); + reset_0(); + + let mut i = 0; + group.bench_function("put_and_evict", |b| { + b.iter(|| { + let (ip, hop) = dataset[i % dataset.len()]; + put_0(black_box(ip), black_box(hop)); + i += 1; + }) + }); + + group.finish(); +} + +#[inline] +fn xorshift64(s: &mut u64) -> u64 { + let mut x = *s; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + *s = x; + x +} + +pub fn bench_hoptab_usecase(c: &mut Criterion) { + let dataset = prepare_data(1024); + + let mut group = c.benchmark_group("HopTab_Usecase"); + let mut seed = 0xC0FFEE_u64; + + for &noise in &[0usize, 1, 4, 16, 64, 128] { + reset_0(); + + // put origin + let i = (xorshift64(&mut seed) as usize) % dataset.len(); + let (ip, hop) = dataset[i]; + put_0(black_box(ip), black_box(hop)); + + // noise: other SYN/ACK + for _ in 0..noise { + let j = (xorshift64(&mut seed) as usize) % dataset.len(); + let (ip2, hop2) = dataset[j]; + put_0(black_box(ip2), black_box(hop2)); + } + + group.bench_function( + BenchmarkId::new("find_between_noise", noise), + |b| { + b.iter(|| { + _ = black_box(find_0(black_box(ip))); + }) + }); + } + + group.finish(); +} + +criterion_group!(benches, bench_hoptab_operations, bench_hoptab_usecase); +criterion_main!(benches); @@ -15,6 +15,7 @@ static OPT_NO_SPLASH: OnceLock<bool> = OnceLock::new(); static OPT_FAKE: OnceLock<bool> = OnceLock::new(); static OPT_FAKE_TTL: OnceLock<u8> = OnceLock::new(); +static OPT_FAKE_AUTOTTL: OnceLock<bool> = OnceLock::new(); static OPT_FAKE_BADSUM: OnceLock<bool> = OnceLock::new(); static OPT_DELAY_MS: OnceLock<u64> = OnceLock::new(); @@ -28,6 +29,7 @@ const DEFAULT_NO_SPLASH: bool = false; const DEFAULT_FAKE: bool = false; const DEFAULT_FAKE_TTL: u8 = 8; +const DEFAULT_FAKE_AUTOTTL: bool = false; const DEFAULT_FAKE_BADSUM: bool = false; const DEFAULT_DELAY_MS: u64 = 0; @@ -51,6 +53,10 @@ pub fn fake_ttl() -> u8 { *OPT_FAKE_TTL.get().unwrap_or(&DEFAULT_FAKE_TTL) } +pub fn fake_autottl() -> bool { + *OPT_FAKE_AUTOTTL.get().unwrap_or(&DEFAULT_FAKE_AUTOTTL) +} + pub fn fake_badsum() -> bool { *OPT_FAKE_BADSUM.get().unwrap_or(&DEFAULT_FAKE_BADSUM) } @@ -96,7 +102,9 @@ fn usage() { println!(" --fake Enable fake clienthello injection"); println!(" --fake-ttl <u8> Override ttl of fake clienthello (default: {DEFAULT_FAKE_TTL})"); - println!(" --fake-badsum Modifies the TCP checksum of the fake packet to an invalid value.\n"); + println!(" --fake-autottl Override ttl of fake clienthello automatically"); + println!(" --fake-badsum Modifies the TCP checksum of the fake packet to an invalid value."); + println!(""); println!(" -h, --help Show this help"); } @@ -115,12 +123,13 @@ fn set_opt<T: std::fmt::Display>( } fn parse_args_1() -> Result<()> { - let mut log_level = DEFAULT_LOG_LEVEL; - let mut delay_ms = DEFAULT_DELAY_MS; - let mut no_splash = DEFAULT_NO_SPLASH; - let mut fake = DEFAULT_FAKE; - let mut fake_ttl = DEFAULT_FAKE_TTL; - let mut fake_badsum = DEFAULT_FAKE_BADSUM; + let mut log_level = DEFAULT_LOG_LEVEL; + let mut delay_ms = DEFAULT_DELAY_MS; + let mut no_splash = DEFAULT_NO_SPLASH; + let mut fake = DEFAULT_FAKE; + let mut fake_ttl = DEFAULT_FAKE_TTL; + let mut fake_autottl = DEFAULT_FAKE_AUTOTTL; + let mut fake_badsum = DEFAULT_FAKE_BADSUM; #[cfg(target_os = "linux")] let mut queue_num: u16 = DEFAULT_QUEUE_NUM; @@ -150,6 +159,7 @@ Use `--log-level' instead."); "--fake" => { fake = true; } "--fake-ttl" => { fake_ttl = take_value(&mut args, argv)?; } + "--fake-autottl" => { fake_autottl = true } "--fake-badsum" => { fake_badsum = true } #[cfg(target_os = "linux")] @@ -168,6 +178,7 @@ Use `--log-level' instead."); set_opt("OPT_DELAY_MS", &OPT_DELAY_MS, delay_ms)?; set_opt("OPT_FAKE", &OPT_FAKE, fake)?; set_opt("OPT_FAKE_TTL", &OPT_FAKE_TTL, fake_ttl)?; + set_opt("OPT_FAKE_AUTOTTL", &OPT_FAKE_AUTOTTL, fake_autottl)?; set_opt("OPT_FAKE_BADSUM", &OPT_FAKE_BADSUM, fake_badsum)?; #[cfg(target_os = "linux")] set_opt("OPT_QUEUE_NUM", &OPT_QUEUE_NUM, queue_num)?; @@ -18,6 +18,7 @@ use anyhow::Result; use etherparse::{IpSlice, TcpSlice}; use anyhow::anyhow; +#[cfg(target_os = "linux")] use std::sync::atomic::Ordering; #[cfg(debug_assertions)] @@ -28,91 +29,49 @@ use crate::opt; use crate::platform; use crate::tls; +mod fake; +mod hoptab; + #[cfg(debug_assertions)] use log::LogLevel; -/// www.microsoft.com -/// Stolen from github.com/bol-van/zapret/blob/master/nfq/desync.c -const DEFAULT_FAKE_TLS_CLIENTHELLO: &'static [u8] = &[ - 0x16, 0x03, 0x01, 0x02, 0xa3, 0x01, 0x00, 0x02, 0x9f, 0x03, 0x03, 0x41, - 0x88, 0x82, 0x2d, 0x4f, 0xfd, 0x81, 0x48, 0x9e, 0xe7, 0x90, 0x65, 0x1f, - 0xba, 0x05, 0x7b, 0xff, 0xa7, 0x5a, 0xf9, 0x5b, 0x8a, 0x8f, 0x45, 0x8b, - 0x41, 0xf0, 0x3d, 0x1b, 0xdd, 0xe3, 0xf8, 0x20, 0x9b, 0x23, 0xa5, 0xd2, - 0x21, 0x1e, 0x9f, 0xe7, 0x85, 0x6c, 0xfc, 0x61, 0x80, 0x3a, 0x3f, 0xba, - 0xb9, 0x60, 0xba, 0xb3, 0x0e, 0x98, 0x27, 0x6c, 0xf7, 0x38, 0x28, 0x65, - 0x80, 0x5d, 0x40, 0x38, 0x00, 0x22, 0x13, 0x01, 0x13, 0x03, 0x13, 0x02, - 0xc0, 0x2b, 0xc0, 0x2f, 0xcc, 0xa9, 0xcc, 0xa8, 0xc0, 0x2c, 0xc0, 0x30, - 0xc0, 0x0a, 0xc0, 0x09, 0xc0, 0x13, 0xc0, 0x14, 0x00, 0x9c, 0x00, 0x9d, - 0x00, 0x2f, 0x00, 0x35, 0x01, 0x00, 0x02, 0x34, 0x00, 0x00, 0x00, 0x16, - 0x00, 0x14, 0x00, 0x00, 0x11, 0x77, 0x77, 0x77, 0x2e, 0x6d, 0x69, 0x63, - 0x72, 0x6f, 0x73, 0x6f, 0x66, 0x74, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x17, - 0x00, 0x00, 0xff, 0x01, 0x00, 0x01, 0x00, 0x00, 0x0a, 0x00, 0x0e, 0x00, - 0x0c, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, 0x00, 0x19, 0x01, 0x00, 0x01, - 0x01, 0x00, 0x0b, 0x00, 0x02, 0x01, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, - 0x10, 0x00, 0x0e, 0x00, 0x0c, 0x02, 0x68, 0x32, 0x08, 0x68, 0x74, 0x74, - 0x70, 0x2f, 0x31, 0x2e, 0x31, 0x00, 0x05, 0x00, 0x05, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x22, 0x00, 0x0a, 0x00, 0x08, 0x04, 0x03, 0x05, 0x03, - 0x06, 0x03, 0x02, 0x03, 0x00, 0x12, 0x00, 0x00, 0x00, 0x33, 0x00, 0x6b, - 0x00, 0x69, 0x00, 0x1d, 0x00, 0x20, 0x69, 0x15, 0x16, 0x29, 0x6d, 0xad, - 0xd5, 0x68, 0x88, 0x27, 0x2f, 0xde, 0xaf, 0xac, 0x3c, 0x4c, 0xa4, 0xe4, - 0xd8, 0xc8, 0xfb, 0x41, 0x87, 0xf4, 0x76, 0x4e, 0x0e, 0xfa, 0x64, 0xc4, - 0xe9, 0x29, 0x00, 0x17, 0x00, 0x41, 0x04, 0xfe, 0x62, 0xb9, 0x08, 0xc8, - 0xc3, 0x2a, 0xb9, 0x87, 0x37, 0x84, 0x42, 0x6b, 0x5c, 0xcd, 0xc9, 0xca, - 0x62, 0x38, 0xd3, 0xd9, 0x99, 0x8a, 0xc4, 0x2d, 0xc6, 0xd0, 0xa3, 0x60, - 0xb2, 0x12, 0x54, 0x41, 0x8e, 0x52, 0x5e, 0xe3, 0xab, 0xf9, 0xc2, 0x07, - 0x81, 0xdc, 0xf8, 0xf2, 0x6a, 0x91, 0x40, 0x2f, 0xcb, 0xa4, 0xff, 0x6f, - 0x24, 0xc7, 0x4d, 0x77, 0x77, 0x2d, 0x6f, 0xe0, 0x77, 0xaa, 0x92, 0x00, - 0x2b, 0x00, 0x05, 0x04, 0x03, 0x04, 0x03, 0x03, 0x00, 0x0d, 0x00, 0x18, - 0x00, 0x16, 0x04, 0x03, 0x05, 0x03, 0x06, 0x03, 0x08, 0x04, 0x08, 0x05, - 0x08, 0x06, 0x04, 0x01, 0x05, 0x01, 0x06, 0x01, 0x02, 0x03, 0x02, 0x01, - 0x00, 0x2d, 0x00, 0x02, 0x01, 0x01, 0x00, 0x1c, 0x00, 0x02, 0x40, 0x01, - 0x00, 0x1b, 0x00, 0x07, 0x06, 0x00, 0x01, 0x00, 0x02, 0x00, 0x03, 0xfe, - 0x0d, 0x01, 0x19, 0x00, 0x00, 0x01, 0x00, 0x03, 0x21, 0x00, 0x20, 0x62, - 0xe8, 0x83, 0xd8, 0x97, 0x05, 0x8a, 0xbe, 0xa1, 0xf2, 0x63, 0x4e, 0xce, - 0x93, 0x84, 0x8e, 0xcf, 0xe7, 0xdd, 0xb2, 0xe4, 0x87, 0x06, 0xac, 0x11, - 0x19, 0xbe, 0x0e, 0x71, 0x87, 0xf1, 0xa6, 0x00, 0xef, 0xd8, 0x6b, 0x27, - 0x5e, 0xc0, 0xa7, 0x5d, 0x42, 0x4e, 0x8c, 0xdc, 0xf3, 0x9f, 0x1c, 0x51, - 0x62, 0xef, 0xff, 0x5b, 0xed, 0xc8, 0xfd, 0xee, 0x6f, 0xbb, 0x88, 0x9b, - 0xb1, 0x30, 0x9c, 0x66, 0x42, 0xab, 0x0f, 0x66, 0x89, 0x18, 0x8b, 0x11, - 0xc1, 0x6d, 0xe7, 0x2a, 0xeb, 0x96, 0x3b, 0x7f, 0x52, 0x78, 0xdb, 0xf8, - 0x6d, 0x04, 0xf7, 0x95, 0x1a, 0xa8, 0xf0, 0x64, 0x52, 0x07, 0x39, 0xf0, - 0xa8, 0x1d, 0x0d, 0x16, 0x36, 0xb7, 0x18, 0x0e, 0xc8, 0x44, 0x27, 0xfe, - 0xf3, 0x31, 0xf0, 0xde, 0x8c, 0x74, 0xf5, 0xa1, 0xd8, 0x8f, 0x6f, 0x45, - 0x97, 0x69, 0x79, 0x5e, 0x2e, 0xd4, 0xb0, 0x2c, 0x0c, 0x1a, 0x6f, 0xcc, - 0xce, 0x90, 0xc7, 0xdd, 0xc6, 0x60, 0x95, 0xf3, 0xc2, 0x19, 0xde, 0x50, - 0x80, 0xbf, 0xde, 0xf2, 0x25, 0x63, 0x15, 0x26, 0x63, 0x09, 0x1f, 0xc5, - 0xdf, 0x32, 0xf5, 0xea, 0x9c, 0xd2, 0xff, 0x99, 0x4e, 0x67, 0xa2, 0xe5, - 0x1a, 0x94, 0x85, 0xe3, 0xdf, 0x36, 0xa5, 0x83, 0x4b, 0x0a, 0x1c, 0xaf, - 0xd7, 0x48, 0xc9, 0x4b, 0x8a, 0x27, 0xdd, 0x58, 0x7f, 0x95, 0xf2, 0x6b, - 0xde, 0x2b, 0x12, 0xd3, 0xec, 0x4d, 0x69, 0x37, 0x9c, 0x13, 0x9b, 0x16, - 0xb0, 0x45, 0x52, 0x38, 0x77, 0x69, 0xef, 0xaa, 0x65, 0x19, 0xbc, 0xc2, - 0x93, 0x4d, 0xb0, 0x1b, 0x7f, 0x5b, 0x41, 0xff, 0xaf, 0xba, 0x50, 0x51, - 0xc3, 0xf1, 0x27, 0x09, 0x25, 0xf5, 0x60, 0x90, 0x09, 0xb1, 0xe5, 0xc0, - 0xc7, 0x42, 0x78, 0x54, 0x3b, 0x23, 0x19, 0x7d, 0x8e, 0x72, 0x13, 0xb4, - 0xd3, 0xcd, 0x63, 0xb6, 0xc4, 0x4a, 0x28, 0x3d, 0x45, 0x3e, 0x8b, 0xdb, - 0x84, 0x4f, 0x78, 0x64, 0x30, 0x69, 0xe2, 0x1b -]; - -pub struct PktView<'a> { - pub ip: IpSlice<'a>, - pub tcp: TcpSlice<'a> +struct PktView<'a> { + ip: IpSlice<'a>, + tcp: TcpSlice<'a> } impl<'a> PktView<'a> { #[inline] - pub fn from_raw(raw: &'a [u8]) -> Result<Self> { + fn from_raw(raw: &'a [u8]) -> Result<Self> { let ip = IpSlice::from_slice(raw)?; let tcp = TcpSlice::from_slice(ip.payload().payload)?; Ok(Self { ip, tcp }) } + + fn ttl(&self) -> u8 { + use etherparse::IpSlice; + + match &self.ip { + IpSlice::Ipv4(v4) => v4.header().ttl(), + IpSlice::Ipv6(v6) => v6.header().hop_limit() + } + } + + fn saddr(&self) -> std::net::IpAddr { + self.ip.source_addr() + } + + fn daddr(&self) -> std::net::IpAddr { + self.ip.destination_addr() + } } /// Write TCP/IP packet (payload = view.tcp.payload[start..Some(end)]) /// to out_buf, explicitly clearing before. /// /// If payload, ttl or tcp_checksum is given, override view's one. -pub fn split_packet_0( +fn split_packet_0( view: &PktView, start: u32, end: Option<u32>, @@ -183,25 +142,6 @@ pub fn split_packet_0( Ok(()) } -fn fake_clienthello( - view: &PktView, - start: u32, - end: Option<u32>, - out_buf: &mut Vec<u8> -) -> Result<()> { - - let tcp_checksum = if opt::fake_badsum() { - Some(0) - } else { - None - }; - - split_packet_0(view, start, end, out_buf, - Some(DEFAULT_FAKE_TLS_CLIENTHELLO), - Some(opt::fake_ttl()), - tcp_checksum) -} - fn split_packet( view: &PktView, start: u32, @@ -220,7 +160,7 @@ fn send_segment( use platform::send_to_raw; if opt::fake() { - fake_clienthello(view, start, end, buf)?; + fake::fake_clienthello(view, start, end, buf)?; send_to_raw(buf)?; } split_packet(view, start, end, buf)?; @@ -247,6 +187,11 @@ fn split_packet_1(view: &PktView, order: &[u32], buf: &mut Vec<u8>) -> Result<() Ok(()) } +fn is_synack_from_443(view: &PktView) -> bool { + // sport == 443 and flags SYN+ACK + view.tcp.source_port() == 443 && view.tcp.syn() && view.tcp.ack() +} + /// Return Ok(true) if packet is handled pub fn handle_packet(pkt: &[u8], buf: &mut Vec::<u8>) -> Result<bool> { #[cfg(target_os = "linux")] @@ -257,6 +202,11 @@ pub fn handle_packet(pkt: &[u8], buf: &mut Vec::<u8>) -> Result<bool> { let view = PktView::from_raw(pkt)?; + if opt::fake_autottl() && is_synack_from_443(&view) { + fake::saddr_hop_put(&view); + return Ok(false); + } + if !is_filtered && !tls::is_client_hello(view.tcp.payload()) { return Ok(false); } diff --git a/src/pkt/fake.rs b/src/pkt/fake.rs new file mode 100644 index 0000000..774642f --- /dev/null +++ b/src/pkt/fake.rs @@ -0,0 +1,143 @@ +// SPDX-FileCopyrightText: 2026 Dilluti0n <[email protected]> +// SPDX-License-Identifier: GPL-3.0-or-later + +use anyhow::Result; +use log::LogLevel; + +use crate::log; +use crate::opt; +use crate::log_println; +use crate::pkt::hoptab; + +use super::PktView; + +/// www.microsoft.com +/// Stolen from github.com/bol-van/zapret/blob/master/nfq/desync.c +const DEFAULT_FAKE_TLS_CLIENTHELLO: &'static [u8] = &[ + 0x16, 0x03, 0x01, 0x02, 0xa3, 0x01, 0x00, 0x02, 0x9f, 0x03, 0x03, 0x41, + 0x88, 0x82, 0x2d, 0x4f, 0xfd, 0x81, 0x48, 0x9e, 0xe7, 0x90, 0x65, 0x1f, + 0xba, 0x05, 0x7b, 0xff, 0xa7, 0x5a, 0xf9, 0x5b, 0x8a, 0x8f, 0x45, 0x8b, + 0x41, 0xf0, 0x3d, 0x1b, 0xdd, 0xe3, 0xf8, 0x20, 0x9b, 0x23, 0xa5, 0xd2, + 0x21, 0x1e, 0x9f, 0xe7, 0x85, 0x6c, 0xfc, 0x61, 0x80, 0x3a, 0x3f, 0xba, + 0xb9, 0x60, 0xba, 0xb3, 0x0e, 0x98, 0x27, 0x6c, 0xf7, 0x38, 0x28, 0x65, + 0x80, 0x5d, 0x40, 0x38, 0x00, 0x22, 0x13, 0x01, 0x13, 0x03, 0x13, 0x02, + 0xc0, 0x2b, 0xc0, 0x2f, 0xcc, 0xa9, 0xcc, 0xa8, 0xc0, 0x2c, 0xc0, 0x30, + 0xc0, 0x0a, 0xc0, 0x09, 0xc0, 0x13, 0xc0, 0x14, 0x00, 0x9c, 0x00, 0x9d, + 0x00, 0x2f, 0x00, 0x35, 0x01, 0x00, 0x02, 0x34, 0x00, 0x00, 0x00, 0x16, + 0x00, 0x14, 0x00, 0x00, 0x11, 0x77, 0x77, 0x77, 0x2e, 0x6d, 0x69, 0x63, + 0x72, 0x6f, 0x73, 0x6f, 0x66, 0x74, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x17, + 0x00, 0x00, 0xff, 0x01, 0x00, 0x01, 0x00, 0x00, 0x0a, 0x00, 0x0e, 0x00, + 0x0c, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, 0x00, 0x19, 0x01, 0x00, 0x01, + 0x01, 0x00, 0x0b, 0x00, 0x02, 0x01, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, + 0x10, 0x00, 0x0e, 0x00, 0x0c, 0x02, 0x68, 0x32, 0x08, 0x68, 0x74, 0x74, + 0x70, 0x2f, 0x31, 0x2e, 0x31, 0x00, 0x05, 0x00, 0x05, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x22, 0x00, 0x0a, 0x00, 0x08, 0x04, 0x03, 0x05, 0x03, + 0x06, 0x03, 0x02, 0x03, 0x00, 0x12, 0x00, 0x00, 0x00, 0x33, 0x00, 0x6b, + 0x00, 0x69, 0x00, 0x1d, 0x00, 0x20, 0x69, 0x15, 0x16, 0x29, 0x6d, 0xad, + 0xd5, 0x68, 0x88, 0x27, 0x2f, 0xde, 0xaf, 0xac, 0x3c, 0x4c, 0xa4, 0xe4, + 0xd8, 0xc8, 0xfb, 0x41, 0x87, 0xf4, 0x76, 0x4e, 0x0e, 0xfa, 0x64, 0xc4, + 0xe9, 0x29, 0x00, 0x17, 0x00, 0x41, 0x04, 0xfe, 0x62, 0xb9, 0x08, 0xc8, + 0xc3, 0x2a, 0xb9, 0x87, 0x37, 0x84, 0x42, 0x6b, 0x5c, 0xcd, 0xc9, 0xca, + 0x62, 0x38, 0xd3, 0xd9, 0x99, 0x8a, 0xc4, 0x2d, 0xc6, 0xd0, 0xa3, 0x60, + 0xb2, 0x12, 0x54, 0x41, 0x8e, 0x52, 0x5e, 0xe3, 0xab, 0xf9, 0xc2, 0x07, + 0x81, 0xdc, 0xf8, 0xf2, 0x6a, 0x91, 0x40, 0x2f, 0xcb, 0xa4, 0xff, 0x6f, + 0x24, 0xc7, 0x4d, 0x77, 0x77, 0x2d, 0x6f, 0xe0, 0x77, 0xaa, 0x92, 0x00, + 0x2b, 0x00, 0x05, 0x04, 0x03, 0x04, 0x03, 0x03, 0x00, 0x0d, 0x00, 0x18, + 0x00, 0x16, 0x04, 0x03, 0x05, 0x03, 0x06, 0x03, 0x08, 0x04, 0x08, 0x05, + 0x08, 0x06, 0x04, 0x01, 0x05, 0x01, 0x06, 0x01, 0x02, 0x03, 0x02, 0x01, + 0x00, 0x2d, 0x00, 0x02, 0x01, 0x01, 0x00, 0x1c, 0x00, 0x02, 0x40, 0x01, + 0x00, 0x1b, 0x00, 0x07, 0x06, 0x00, 0x01, 0x00, 0x02, 0x00, 0x03, 0xfe, + 0x0d, 0x01, 0x19, 0x00, 0x00, 0x01, 0x00, 0x03, 0x21, 0x00, 0x20, 0x62, + 0xe8, 0x83, 0xd8, 0x97, 0x05, 0x8a, 0xbe, 0xa1, 0xf2, 0x63, 0x4e, 0xce, + 0x93, 0x84, 0x8e, 0xcf, 0xe7, 0xdd, 0xb2, 0xe4, 0x87, 0x06, 0xac, 0x11, + 0x19, 0xbe, 0x0e, 0x71, 0x87, 0xf1, 0xa6, 0x00, 0xef, 0xd8, 0x6b, 0x27, + 0x5e, 0xc0, 0xa7, 0x5d, 0x42, 0x4e, 0x8c, 0xdc, 0xf3, 0x9f, 0x1c, 0x51, + 0x62, 0xef, 0xff, 0x5b, 0xed, 0xc8, 0xfd, 0xee, 0x6f, 0xbb, 0x88, 0x9b, + 0xb1, 0x30, 0x9c, 0x66, 0x42, 0xab, 0x0f, 0x66, 0x89, 0x18, 0x8b, 0x11, + 0xc1, 0x6d, 0xe7, 0x2a, 0xeb, 0x96, 0x3b, 0x7f, 0x52, 0x78, 0xdb, 0xf8, + 0x6d, 0x04, 0xf7, 0x95, 0x1a, 0xa8, 0xf0, 0x64, 0x52, 0x07, 0x39, 0xf0, + 0xa8, 0x1d, 0x0d, 0x16, 0x36, 0xb7, 0x18, 0x0e, 0xc8, 0x44, 0x27, 0xfe, + 0xf3, 0x31, 0xf0, 0xde, 0x8c, 0x74, 0xf5, 0xa1, 0xd8, 0x8f, 0x6f, 0x45, + 0x97, 0x69, 0x79, 0x5e, 0x2e, 0xd4, 0xb0, 0x2c, 0x0c, 0x1a, 0x6f, 0xcc, + 0xce, 0x90, 0xc7, 0xdd, 0xc6, 0x60, 0x95, 0xf3, 0xc2, 0x19, 0xde, 0x50, + 0x80, 0xbf, 0xde, 0xf2, 0x25, 0x63, 0x15, 0x26, 0x63, 0x09, 0x1f, 0xc5, + 0xdf, 0x32, 0xf5, 0xea, 0x9c, 0xd2, 0xff, 0x99, 0x4e, 0x67, 0xa2, 0xe5, + 0x1a, 0x94, 0x85, 0xe3, 0xdf, 0x36, 0xa5, 0x83, 0x4b, 0x0a, 0x1c, 0xaf, + 0xd7, 0x48, 0xc9, 0x4b, 0x8a, 0x27, 0xdd, 0x58, 0x7f, 0x95, 0xf2, 0x6b, + 0xde, 0x2b, 0x12, 0xd3, 0xec, 0x4d, 0x69, 0x37, 0x9c, 0x13, 0x9b, 0x16, + 0xb0, 0x45, 0x52, 0x38, 0x77, 0x69, 0xef, 0xaa, 0x65, 0x19, 0xbc, 0xc2, + 0x93, 0x4d, 0xb0, 0x1b, 0x7f, 0x5b, 0x41, 0xff, 0xaf, 0xba, 0x50, 0x51, + 0xc3, 0xf1, 0x27, 0x09, 0x25, 0xf5, 0x60, 0x90, 0x09, 0xb1, 0xe5, 0xc0, + 0xc7, 0x42, 0x78, 0x54, 0x3b, 0x23, 0x19, 0x7d, 0x8e, 0x72, 0x13, 0xb4, + 0xd3, 0xcd, 0x63, 0xb6, 0xc4, 0x4a, 0x28, 0x3d, 0x45, 0x3e, 0x8b, 0xdb, + 0x84, 0x4f, 0x78, 0x64, 0x30, 0x69, 0xe2, 0x1b +]; + +const AUTOTTL_DELTA: u8 = 1; + +/// Crudely infer hop from ttl +/// +/// Assume server initial TTL is one of: 64, 126, 255. +/// Pick the smallest origin that can produce the observed TTL (origin >= ttl), +/// then hops = origin - ttl. +fn infer_hops(ttl: u8) -> u8 { + let origin = if ttl <= 64 { + 64u8 + } else if ttl <= 126 { + 126u8 + } else { + 255u8 + }; + + origin - ttl +} + + +pub fn saddr_hop_put(view: &PktView) { + hoptab::put_0(view.saddr(), infer_hops(view.ttl())) +} + +fn daddr_hop(view: &PktView) -> hoptab::HopResult<u8> { + hoptab::find_0(view.daddr()) +} + +pub fn fake_clienthello( + view: &PktView, + start: u32, + end: Option<u32>, + out_buf: &mut Vec<u8> +) -> Result<()> { + + let tcp_checksum = if opt::fake_badsum() { + Some(0) + } else { + None + }; + + let ttl: u8 = if opt::fake_autottl() { + match daddr_hop(&view) { + Ok(hop) => { + let fake_ttl = hop.saturating_sub(AUTOTTL_DELTA); + log_println!(LogLevel::Debug, + "autottl: set ttl to {fake_ttl}" + ); + fake_ttl + }, + Err(e) => { + let fake_ttl = opt::fake_ttl(); + log_println!(LogLevel::Warning, + "autottl: sv_hop_find: {e}; fallback to {fake_ttl}"); + fake_ttl + } + } + } else { + opt::fake_ttl() + }; + + super::split_packet_0( + view, start, end, out_buf, + Some(DEFAULT_FAKE_TLS_CLIENTHELLO), + Some(ttl), + tcp_checksum + ) +} diff --git a/src/pkt/hoptab.rs b/src/pkt/hoptab.rs new file mode 100644 index 0000000..2ad1beb --- /dev/null +++ b/src/pkt/hoptab.rs @@ -0,0 +1,551 @@ +// SPDX-FileCopyrightText: 2026 Dilluti0n <[email protected]> +// SPDX-License-Identifier: GPL-3.0-or-later + +//! Linear probing hash table for ip-hop cache +//! +//! On inbound SYN/ACK (src port 443) from a server, infer the hop +//! count from the observed TTL via `crate::pkt::fake::infer_hops` and +//! store it with [`HopTab::put`]. Later, when sending a fake +//! ClientHello, look up the hop count by destination IP +//! [`HopTab::find_hop`] to automatically choose an appropriate TTL. +//! +//! One could consider keeping a global variable (like `current_hop`), +//! loading it when a SYN/ACK arrives, and reading it when needed. The +//! problem is that, before the ClientHello is sent after that SYN/ACK +//! (i.e., in between), a SYN/ACK from a different server may +//! arrive. To handle this, the (ip, hop) pair must be stored in an +//! appropriate data structure and looked up later. +//! +//! [`HopTab`] has capacity [`CAP`], which means that if at least +//! [`CAP`] SYN/ACKs arrive from distinct servers before the first +//! ClientHello is sent, [`HopLookupError::NotFound`] will inevitably +//! occur. (Given the size of [`CAP`], this is extremely unlikely.) +//! Also, after an entry is inserted, if [`HopTab::update`] runs at +//! least [`HopTab::STALE_AGE`] times, the entry is marked stale and +//! becomes evictable, so once the number of updates reaches +//! [`HopTab::STALE_AGE`] or more, there is a chance that +//! [`HopLookupError::NotFound`] occurs. Other than these cases, it +//! will not occur. + + +use std::fmt; +use std::net::IpAddr; +use std::sync::{Mutex, OnceLock}; +use std::net::{Ipv4Addr, Ipv6Addr}; + +use crate::log_println; +use crate::log::LogLevel; + +/// Size of [`HopTab`] +const CAP: usize = 1 << 7; // 128 + +/// 128-bit (IPv6-shaped) unified IP key for [`HopTab`] lookups (IPv4 +/// stored as ::ffff:a.b.c.d). +#[repr(C)] +#[derive(Clone, Copy, PartialEq, Eq)] +struct HopKey { + hi: u64, + lo: u64 +} + +impl HopKey { + const ZERO: Self = Self { hi: 0, lo: 0 }; + + #[inline] + fn from_ipaddr(ip: IpAddr) -> Self { + match ip { + IpAddr::V4(v4) => { + // ::ffff:a.b.c.d (IPv4-mapped IPv6) + let v4u = u32::from(v4) as u64; + Self { hi: 0, lo: (0xFFFFu64 << 32) | v4u } + } + IpAddr::V6(v6) => { + let b = v6.octets(); + let hi = u64::from_be_bytes(b[0..8].try_into().unwrap()); + let lo = u64::from_be_bytes(b[8..16].try_into().unwrap()); + Self { hi, lo } + } + } + } + + #[inline] + fn to_ipaddr(self) -> IpAddr { + // ::ffff:a.b.c.d (IPv4-mapped IPv6) + if self.hi == 0 && (self.lo >> 32) == 0x0000_FFFF { + let v4 = (self.lo & 0xFFFF_FFFF) as u32; + return IpAddr::V4(Ipv4Addr::from(v4)); + } + + let mut b = [0u8; 16]; + b[0..8].copy_from_slice(&self.hi.to_be_bytes()); + b[8..16].copy_from_slice(&self.lo.to_be_bytes()); + IpAddr::V6(Ipv6Addr::from(b)) + } +} + +#[derive(Clone, Copy)] +struct HopTabEntry { + key: HopKey, + + /// [RESERVED(32) | TS(16) | HOP(8) | STATE(8)] + /// * TS: timestamp snapshot (see [`HopTab::now`]) + /// * HOP: stored hop count + /// * STATE: [`Self::ST_OCCUPIED`], [`Self::ST_TOUCHED`] + meta: u64, +} + +impl HopTabEntry { + /// For internal use; Do not use it globally + const ST_EMPTY: u8 = 0; + const ST_OCCUPIED: u8 = 1 << 0; + + /// Entry has been touched (i.e., consumed by [`HopTab::find_hop`] + /// at least once). + /// + /// Note: "touched" does not mean "recently used, keep it". It + /// means the entry already served its purpose (consumed for Fake + /// ClientHello TTL), so it is more eligible for eviction under + /// pressure than a fresh, untouched entry. + const ST_TOUCHED: u8 = 1 << 1; + + const EMPTY: Self = Self { key: HopKey::ZERO, meta: Self::ST_EMPTY as u64}; + + const S_STATE: usize = 0; + const S_HOP: usize = 8; + const S_TS: usize = 16; + + #[inline] + fn key(&self) -> HopKey { + self.key + } + + #[inline] + fn new(key: HopKey, ts: u16, hop: u8) -> Self { + Self { + key: key, + meta: ((ts as u64) << Self::S_TS) + | ((hop as u64) << Self::S_HOP) + | ((Self::ST_OCCUPIED as u64) << Self::S_STATE) + } + } + + #[inline] + fn hop(&self) -> u8 { + (self.meta >> Self::S_HOP) as u8 + } + + #[inline] + fn state(&self) -> u8 { + (self.meta >> Self::S_STATE) as u8 + } + + #[inline] + fn has(&self, mask: u8) -> bool { + (self.state() & mask) == mask + } + + #[inline] + fn touch(&mut self) { + self.meta |= Self::ST_TOUCHED as u64; + } + + #[inline] + fn ts(&self) -> u16 { + (self.meta >> Self::S_TS) as u16 + } +} + +impl fmt::Debug for HopTabEntry { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let ip = self.key.to_ipaddr(); + let state = self.state(); + let hop = self.hop(); + let ts = self.ts(); + + write!( + f, + "HopTabEntry{{ ip={}, state=0x{:02x}, hop={}, ts={}, meta=0x{:016x} }}", + ip, state, hop, ts, self.meta + ) + } +} + +#[derive(Debug, Clone)] +pub enum HopLookupError { + NotFound { ip: IpAddr }, +} + +impl fmt::Display for HopLookupError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + HopLookupError::NotFound { ip } => write!(f, "hop not found for {ip}"), + } + } +} + +impl std::error::Error for HopLookupError {} + +pub type HopResult<T> = std::result::Result<T, HopLookupError>; + +struct HopTab<const CAP: usize> { + entries: Box<[HopTabEntry; CAP]>, + + /// Logical tick counter. + /// + /// Increments on every successful [`Self::update`] + /// (put/overwrite) and wraps via [`u16::wrapping_add`]. It is + /// ensured that at least [`CAP`] - [`Self::STALE_AGE`] entries + /// evictable. (i.e. [`HopTab`] not become corrupted.) + now: u16, +} + +/// Non-cryptographic hash using SplitMix64-style finalizer +#[inline] +fn hash(key: HopKey) -> usize { + let mut x = key.hi ^ key.lo.rotate_left(13); + x ^= x >> 30; + x = x.wrapping_mul(0xbf58476d1ce4e5b9); + x ^= x >> 27; + x = x.wrapping_mul(0x94d049bb133111eb); + x ^= x >> 31; + x as usize +} + +trait HashIdx { + /// `CAP` must be a power of two (we index via `hash & (CAP-1)`). + fn to_idx<const CAP: usize>(self) -> usize; +} + +impl HashIdx for usize { + #[inline] + fn to_idx<const CAP: usize>(self) -> usize { + self & (CAP - 1) + } +} + +#[derive(PartialEq, PartialOrd, Clone, Copy)] +enum EvictPriority { + /// Occupied && fresh && untouched + None = 0, + + /// Already consumed + Touched = 1, + Stale = 2, + Empty = 3, + + /// Same key occers + MustUpdate = 4, +} + +impl<const CAP: usize> HopTab<CAP> { + const ASSERT_CAP_POW2: () = { + assert!(CAP.is_power_of_two()); + }; + + /// To avoid the edge case where all entries become non-stale, + /// [`Self::STALE_AGE`] must be smaller than [`CAP`]. + const STALE_AGE: usize = CAP >> 1; // 64 + + fn new() -> Self { + _ = Self::ASSERT_CAP_POW2; + + Self { + entries: Box::new([HopTabEntry::EMPTY; CAP]), + now: 0, + } + } + + #[inline] + fn age(&self, entry: &HopTabEntry) -> u16 { + // Since we use u16 with wrapping_sub, the age calculation remains + // correct even when `self.now` overflows and wraps around to zero. + // This holds true as long as the temporal distance between + // `entry.ts()` and `self.now` does not exceed 2^15 (32,768). + // Given that STALE_AGE (64) << 2^15, the "stale" judgment is + // always mathematically sound. + self.now.wrapping_sub(entry.ts()) + } + + #[inline] + fn is_stale(&self, entry: &HopTabEntry) -> bool { + self.age(entry) >= Self::STALE_AGE as u16 + } + + #[inline] + fn update(&mut self, idx: usize, new: HopTabEntry) { + self.entries[idx] = new; + self.now = self.now.wrapping_add(1); + } + + #[inline] + fn evict_priority(&self, entry: &HopTabEntry) -> EvictPriority { + if !entry.has(HopTabEntry::ST_OCCUPIED) { + EvictPriority::Empty + } else if self.is_stale(entry) { + EvictPriority::Stale + } else if entry.has(HopTabEntry::ST_TOUCHED) { + EvictPriority::Touched + } else { + EvictPriority::None + } + } + + fn put(&mut self, ip: IpAddr, hop: u8) { + let key = HopKey::from_ipaddr(ip); + let entry = HopTabEntry::new(key, self.now, hop); + let start = hash(key).to_idx::<CAP>(); + + let mut victim = (0, EvictPriority::None); // (idx, priority) + + // Key must be unique in the table + for step in 0..CAP { + let idx = (start + step).to_idx::<CAP>(); + let e = self.entries[idx]; + + // Hit; must update same key (hop could be changed) + if e.key() == key && e.has(HopTabEntry::ST_OCCUPIED) { + victim = (idx, EvictPriority::MustUpdate); + #[cfg(debug_assertions)] + log_println!(LogLevel::Debug, "HopTab::put: hit {}; {:#?}", victim.0, entry); + break; + } + + let prio = self.evict_priority(&e); + + if prio > victim.1 { + victim = (idx, prio); + + if prio == EvictPriority::Empty { + #[cfg(debug_assertions)] + log_println!(LogLevel::Debug, + "HopTab::put: hit empty {}; {:#?}", victim.0, entry); + break; // linear probing; there is no key here + } + } + } + + if victim.1 > EvictPriority::None { + self.update(victim.0, entry); + #[cfg(debug_assertions)] + log_println!(LogLevel::Debug, "HopTab::put: update {} to {:#?}", victim.0, entry); + } else { + log_println!(LogLevel::Error, "HopTab::put: update fail: corrupted; {:#?}", entry); + } + } + + fn find_hop(&mut self, ip: IpAddr) -> HopResult<u8> { + let key = HopKey::from_ipaddr(ip); + let start = hash(key).to_idx::<CAP>(); + + for step in 0..CAP { + let idx = (start + step).to_idx::<CAP>(); + let e = self.entries[idx]; + + if !e.has(HopTabEntry::ST_OCCUPIED) { + break; // linear probing; there is no key here + } + + if e.key() == key { + self.entries[idx].touch(); + + #[cfg(debug_assertions)] + log_println!(LogLevel::Debug, "HopTab::find_hop: found {idx}; {:#?}", e); + return Ok(e.hop()); + } + } + + Err(HopLookupError::NotFound { ip }) + } +} + +static H_TAB: OnceLock<Mutex<HopTab<CAP>>> = OnceLock::new(); + +#[inline] +fn htab() -> std::sync::MutexGuard<'static, HopTab<CAP>> { + H_TAB.get_or_init(|| Mutex::new(HopTab::new())) + .lock() + .unwrap() +} + +pub fn put_0(ip: IpAddr, hop: u8) { + htab().put(ip, hop) +} + +pub fn find_0(ip: IpAddr) -> HopResult<u8> { + htab().find_hop(ip) +} + +// +// below are test/bench codes +// + +#[cfg(test)] +mod tests { + use super::*; + use std::fs::File; + use std::io::Read; + use std::net::{IpAddr, Ipv4Addr}; + + #[test] + fn test_hop_key_conversion() { + let ip: IpAddr = "1.2.3.4".parse().unwrap(); + let key = HopKey::from_ipaddr(ip); + assert_eq!(key.to_ipaddr(), ip); + } + + #[test] + fn test_basic_flow() { + let ip: IpAddr = "1.1.1.1".parse().unwrap(); + put_0(ip, 12); + + let result = find_0(ip).expect("cannot find {ip}"); + assert_eq!(result, 12); + } + + #[test] + fn test_not_found() { + let ip: IpAddr = "8.8.8.8".parse().unwrap(); + let result = find_0(ip); + assert!(result.is_err()); + } + + fn u32_to_ipaddr(i: u32) -> IpAddr { + IpAddr::V4(Ipv4Addr::from(i)) + } + + #[test] + fn test_full_table() { + let mut tab = HopTab::<CAP>::new(); + for i in 0..CAP { + tab.put(u32_to_ipaddr(i as u32), i as u8); + } + for i in 0..CAP { + assert_eq!(tab.find_hop(u32_to_ipaddr(i as u32)).unwrap(), i as u8); + } + } + + fn get_random(size: usize) -> Vec<u8> { + const RAND: &'static str = "/dev/urandom"; + let mut f = File::open(RAND).unwrap(); + let mut buf = vec![0u8; size]; + + f.read_exact(&mut buf).unwrap(); + + buf + } + + const ITERATIONS: usize = 1 << 19; + + fn get_random_bulk() -> Vec<u8> { + get_random(ITERATIONS * 5) + } + + fn get_iphop(raw: &Vec<u8>, idx: usize) -> (IpAddr, u8) { + let off = idx * 5; + let ip_num = u32::from_ne_bytes(raw[off..off+4].try_into().unwrap()); + + (u32_to_ipaddr(ip_num), raw[off+4]) + } + + #[test] + fn test_hoptab_stress() { + let mut tab = HopTab::<CAP>::new(); + let rand = get_random_bulk(); + + for i in 0..ITERATIONS { + let (ip, hop) = get_iphop(&rand, i); + + tab.put(ip, hop); + + assert_eq!(tab.find_hop(ip).unwrap(), hop); + } + } + + #[test] + fn test_random_cache_integrity() { + let mut tab = HopTab::<CAP>::new(); + + const SAFE_RANGE: usize = CAP - (CAP >> 1); + let rand = get_random_bulk(); + + let mut recent_data = std::collections::VecDeque::with_capacity(SAFE_RANGE); + + for i in 0..ITERATIONS { + let (ip, hop) = get_iphop(&rand, i); + + tab.put(ip, hop); + + if recent_data.len() == SAFE_RANGE { + recent_data.pop_front(); + } + recent_data.push_back((ip, hop)); + } + + for (ip, expected_hop) in recent_data { + let actual_hop = tab.find_hop(ip).expect("In-flight data should not be evicted"); + assert_eq!(actual_hop, expected_hop); + } + } + + #[test] + fn test_age_overflow_handling() { + let mut tab = HopTab::<CAP>::new(); + let ip1 = u32_to_ipaddr(1); + let ip2 = u32_to_ipaddr(2); + + tab.now = u16::MAX; + + tab.put(ip1, 10); + assert_eq!(tab.now, 0); + + tab.put(ip2, 20); + assert_eq!(tab.now, 1); + + let entry1 = tab.entries[hash(HopKey::from_ipaddr(ip1)).to_idx::<CAP>()]; + assert_eq!(tab.age(&entry1), 2); + assert!(!tab.is_stale(&entry1)); + + const DISTINCT: u32 = 3; + let ip3 = u32_to_ipaddr(DISTINCT); + + // Since there is no lookup for ip1, it is not become + // evictable by putting ip3. Use STALE_AGE - 1 because ip2 has + // been putted already. + for _ in 0..HopTab::<CAP>::STALE_AGE-1 { + tab.put(ip3, 3); + } + + assert!(tab.is_stale(&entry1)); + } +} + +#[cfg(feature = "bench")] +#[allow(unused_imports)] +pub use bench_support::reset_0; + +#[cfg(feature = "bench")] +#[allow(dead_code)] +mod bench_support { + use super::*; + + impl HopTabEntry { + #[inline] + fn clear(&mut self) { + self.meta &= (!Self::ST_OCCUPIED << Self::S_STATE) as u64; + } + } + + impl<const CAP: usize> HopTab<CAP> { + fn reset(&mut self) { + for i in 0..CAP { + self.entries[i].clear(); + } + self.now = 0; + } + } + + #[inline] + pub fn reset_0() { + htab().reset(); + } +} diff --git a/src/platform/linux/iptables.rs b/src/platform/linux/iptables.rs index 91fa1da..4264967 100644 --- a/src/platform/linux/iptables.rs +++ b/src/platform/linux/iptables.rs @@ -138,6 +138,21 @@ pub fn install_iptables_rules(ipt: &IPTables) -> Result<()> { 1 ).map_err(iptables_err)?; + if opt::fake_autottl() { + let synack_rule = vec![ + "-p", "tcp", + "--sport", "443", + "-m", "tcp", "--tcp-flags", "SYN,ACK", "SYN,ACK", + "-j", "NFQUEUE", "--queue-num", &q_num, "--queue-bypass", + ]; + + ipt.append("mangle", DPIBREAK_CHAIN, &synack_rule).map_err(iptables_err)?; + log_println!(LogLevel::Info, "{}: add SYN/ACK learning rule on mangle/{}", ipt.cmd, DPIBREAK_CHAIN); + + ipt.insert("mangle", "INPUT", &["-j", DPIBREAK_CHAIN], 1).map_err(iptables_err)?; + log_println!(LogLevel::Info, "{}: add jump to {} chain on INPUT", ipt.cmd, DPIBREAK_CHAIN); + } + ipt.append("mangle", DPIBREAK_CHAIN, &rule).map_err(iptables_err)?; log_println!(LogLevel::Info, "{}: new chain {} on table mangle", ipt.cmd, DPIBREAK_CHAIN); @@ -152,6 +167,10 @@ pub fn cleanup_iptables_rules(ipt: &IPTables) -> Result<()> { log_println!(LogLevel::Info, "{}: delete jump to {} from mangle/POSTROUTING", ipt.cmd, DPIBREAK_CHAIN); } + if opt::fake_autottl() && ipt.delete("mangle", "INPUT", &["-j", DPIBREAK_CHAIN]).is_ok() { + log_println!(LogLevel::Info, "{}: delete jump to {} from mangle/INPUT", ipt.cmd, DPIBREAK_CHAIN); + } + if ipt.flush_chain("mangle", DPIBREAK_CHAIN).is_ok() { log_println!(LogLevel::Info, "{}: flush chain {}", ipt.cmd, DPIBREAK_CHAIN); } diff --git a/src/platform/linux/nftables.rs b/src/platform/linux/nftables.rs index ccb918e..27e1830 100644 --- a/src/platform/linux/nftables.rs +++ b/src/platform/linux/nftables.rs @@ -103,6 +103,65 @@ pub fn install_nft_rules() -> Result<()> { opt::queue_num()); log_println!(LogLevel::Debug, "nftables: rule json={}", rule); + if opt::fake_autottl() { + let synack_rule = serde_json::json!( + { + "nftables": [ + // SYN,ACK (for --fake-autottl) + { + "add": { + "chain": { + "family": "inet", + "table": DPIBREAK_TABLE, + "name": "INPUT", + "type": "filter", + "hook": "input", + "prio": 0, + "policy": "accept", + } + } + }, + { + "add" : { + "rule": { + "family": "inet", + "table": DPIBREAK_TABLE, + "chain": "INPUT", + "expr": [ + { + "match": { + "left": { "payload": { "protocol": "tcp", "field": "sport" }}, + "op": "==", "right": 443 + } + }, + { + "match": { + "left": { "payload": { "protocol": "tcp", "field": "flags" }}, + "op": "==", "right": 18 // 18 = SYN(2) | ACK(16) + } + }, + { + "queue": { + "num": crate::opt::queue_num(), + "flags": [ "bypass" ] + } + } + ] + }, + } + } + ] + } + ); + + apply_nft_rules(&serde_json::to_string(&synack_rule)?)?; + + log_println!(LogLevel::Info, + "nftables: add chain INPUT, match tcp sport 443 & SYN|ACK -> queue {})", + opt::queue_num()); + log_println!(LogLevel::Debug, "nftables: synack rule json={}", synack_rule); + } + // clienthello filtered by nft IS_U32_SUPPORTED.store(true, Ordering::Relaxed); log_println!(LogLevel::Info, "nftables: create table inet {DPIBREAK_TABLE}"); diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 4303a65..37a6654 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -23,17 +23,29 @@ use windivert::{ use std::sync::{atomic::Ordering, LazyLock, Mutex, MutexGuard}; use crate::{log::LogLevel, log_println, splash}; +fn windivert_filter() -> String { + let base = "(outbound and tcp and tcp.DstPort == 443 \ + and tcp.Payload[0] == 22 \ + and tcp.Payload[5] == 1)"; + + if crate::opt::fake_autottl() { + let synack = "(!outbound and tcp and tcp.SrcPort == 443 \ + and tcp.Syn and tcp.Ack)"; + format!("({base} or {synack}) and !impostor") + } else { + format!("{base} and !impostor") + } +} + + pub static WINDIVERT_HANDLE: LazyLock<Mutex<WinDivert<NetworkLayer>>> = LazyLock::new(|| { use windivert::*; - const FILTER: &str = "outbound and tcp and tcp.DstPort == 443 \ - and tcp.Payload[0] == 22 \ - and tcp.Payload[5] == 1 \ - and !impostor"; // to prevent inf loop + let filter = windivert_filter(); - let h = match WinDivert::network(FILTER, 0, prelude::WinDivertFlags::new()) { + let h = match WinDivert::network(&filter, 0, prelude::WinDivertFlags::new()) { Ok(h) => { - log_println!(LogLevel::Info, "windivert: HANDLE constructed for {}", FILTER); + log_println!(LogLevel::Info, "windivert: HANDLE constructed for {}", filter); h }, Err(e) => { |
