summaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/opt.rs25
-rw-r--r--src/pkt.rs124
-rw-r--r--src/pkt/fake.rs143
-rw-r--r--src/pkt/hoptab.rs551
-rw-r--r--src/platform/linux/iptables.rs19
-rw-r--r--src/platform/linux/nftables.rs59
-rw-r--r--src/platform/windows.rs24
7 files changed, 845 insertions, 100 deletions
diff --git a/src/opt.rs b/src/opt.rs
index 78d6b0a..255ae8f 100644
--- a/src/opt.rs
+++ b/src/opt.rs
@@ -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)?;
diff --git a/src/pkt.rs b/src/pkt.rs
index 158ad4b..34190a8 100644
--- a/src/pkt.rs
+++ b/src/pkt.rs
@@ -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) => {