diff options
| -rw-r--r-- | CHANGELOG.md | 8 | ||||
| -rw-r--r-- | README.md | 14 | ||||
| -rw-r--r-- | dpibreak.1.in | 36 | ||||
| -rw-r--r-- | dpibreak.1.md | 28 | ||||
| -rw-r--r-- | src/opt.rs | 118 | ||||
| -rw-r--r-- | src/pkt.rs | 35 |
6 files changed, 203 insertions, 36 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 5841c4c..e1fcd8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## Unreleased +### Added +- Option `-o, --segment-order`: configure segment boundaries and + transmission order of the TLS ClientHello. (#20) + +### Changed +- Renamed "fragment" to "segmentation" in `README.md`. + ## [DPIBreak v0.5.1] - 2026-03-10 ### Fixed - Linux: fix BPF filter to match IPv6 SYN/ACK also (previously only @@ -39,12 +39,20 @@ the same speed as an unrestricted connection, with minimal setup. ## Features For more information, please refer to -[dpibreak(1)](./dpibreak.1.md). (Though you probably won't need it. :) +[dpibreak](./dpibreak.1.md)(1). (Though you probably won't need it. :) -### fragment (default) +### segmentation (default) Split the TLS ClientHello into smaller pieces so that DPI equipment cannot read the SNI from a single packet. The server reassembles them -normally. +normally. It can be configured via `--segment-order`. See +[#20](https://github.com/Dilluti0n/DPIBreak/issues/20) for examples +that help illustrate the rules. + +> [!NOTE] +> Some servers may return a connection error with the default `0,1` +> split (first byte sent seperately). If this happens, try +> `--segment-order 0,5`. See +> [#23](https://github.com/Dilluti0n/DPIBreak/issues/23) for details. ### fake Enable fake ClientHello packet (with SNI `www.microsoft.com`) diff --git a/dpibreak.1.in b/dpibreak.1.in index 93fbcc8..8fbb1b6 100644 --- a/dpibreak.1.in +++ b/dpibreak.1.in @@ -100,6 +100,20 @@ expected. It can be useful if your router/firewall provides an option to disable TCP checksum verification. Implicitly enables .BR \-\-fake . .TP +.BR \-o ", " \-\-segment-order " \fI<u32,u32,...>\fR" +Specify the order in which TCP segments of the TLS ClientHello are +transmitted. The argument is a comma-separated list of byte offsets, +which must include +.BR 0 . +Offsets are sorted to define segment +boundaries: for example, +.B 0,1,3,5 +defines segments +.BR [0,1) ", " [1,3) ", " [3,5) ", and " [5,end) . +The transmission order follows the order given on the command +line. Segments whose start offset exceeds the payload length are +silently skipped. (Default: {{DEFAULT_SEGMENT_ORDER}}) +.TP .B \-\-queue\-num \fI<u16>\fR .Linux only. NFQUEUE number to attach to. The same queue number is @@ -115,9 +129,11 @@ Aliases: .BR warn " \-> " warning , .BR err " \-> " error . .TP + .B \-\-no\-splash Disable splash messages at startup. .TP + .BR \-h ", " \-\-help Show usage information and exit. @@ -144,7 +160,25 @@ Run as daemon with fake ClientHello injection: .B dpibreak \-D \-\-fake\-autottl .RE .PP -Run with a 10 ms delay between fragmanted packets and verbose logging: +Send the TLS ClientHello with the second byte first, followed by the +remainder, then the first byte: +.PP +.RS +.B dpibreak \-\-segment\-order 1,2,0 +.RE +.PP +This causes DPI equipment to receive an incomplete record header in +the first observed segment, preventing SNI extraction, while the +destination server reassembles the segments normally via TCP sequence +numbers. +.PP +Send segments in a custom out-of-order sequence with a delay: +.PP +.RS +.B dpibreak \-\-segment\-order 5,1,3,0 \-\-delay\-ms 10 +.PP +.RE +Run with a 10 ms delay between fragmented packets and verbose logging: .PP .RS .B dpibreak \-\-delay\-ms 10 \-\-log\-level debug diff --git a/dpibreak.1.md b/dpibreak.1.md index 207772d..2728b3b 100644 --- a/dpibreak.1.md +++ b/dpibreak.1.md @@ -79,6 +79,15 @@ packets cannot pass through most routers and will not behave as expected. It can be useful if your router/firewall provides an option to disable TCP checksum verification. Implicitly enables **--fake**. +**-o**, **--segment-order** *\<u32,u32,...\>* +Specify the order in which TCP segments of the TLS ClientHello are +transmitted. The argument is a comma-separated list of byte offsets, +which must include **0**. Offsets are sorted to define segment +boundaries: for example, **0,1,3,5** defines segments **\[0,1)**, +**\[1,3)**, **\[3,5)**, and **\[5,end)**. The transmission order follows +the order given on the command line. Segments whose start offset exceeds +the payload length are silently skipped. (Default: 0,1) + **--queue-num *\<u16\>*** NFQUEUE number to attach to. The same queue number is used for IPv4 and @@ -117,7 +126,20 @@ Run as daemon with fake ClientHello injection: > **dpibreak -D --fake-autottl** -Run with a 10 ms delay between fragmanted packets and verbose logging: +Send the TLS ClientHello with the second byte first, followed by the +remainder, then the first byte: + +> **dpibreak --segment-order 1,2,0** + +This causes DPI equipment to receive an incomplete record header in the +first observed segment, preventing SNI extraction, while the destination +server reassembles the segments normally via TCP sequence numbers. + +Send segments in a custom out-of-order sequence with a delay: + +> **dpibreak --segment-order 5,1,3,0 --delay-ms 10** + +Run with a 10 ms delay between fragmented packets and verbose logging: > **dpibreak --delay-ms 10 --log-level debug** @@ -138,7 +160,7 @@ Only for daemon. log goes here. There are two types of bugs: -> a\. DPIBreak does not work as described in the manual. +> a\. DPIBreak does not work as described in the manual.\ > b. It works as described but fails to bypass the DPI. Reporting both cases to the bug tracker helps improve the program. For @@ -147,7 +169,7 @@ your region and ISP. Any other minor improvements or suggestions are also welcome. -You can view the known bugs list and submit reports at: +You can view the known bugs list and submit reports at:\ *https://github.com/dilluti0n/dpibreak/issues* ## SEE ALSO @@ -8,34 +8,108 @@ use crate::log; use log::LogLevel; +#[derive(Copy, Clone)] +pub struct Segment(pub u32, pub u32); + +impl std::fmt::Display for Segment { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let end = if self.1 == u32::MAX { "end".to_string() } else { self.1.to_string() }; + write!(f, "[{},{})", self.0, end) + } +} + +impl std::fmt::Debug for Segment { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{self}") + } +} + +pub struct SegmentOrder { + raw: String, + segments: Vec<Segment> +} + +impl SegmentOrder { + /// Parse 5,1,0,3 to (5, u32::MAX), (1, 3), (0, 1), (3, 5). + pub fn new(s: &str) -> Result<Self> { + let mut points: Vec<u32> = s + .split(',') + .map(|x| x.trim().parse::<u32>()) + .collect::<std::result::Result<_, _>>() + .with_context(|| format!("--segment-order: invalid value '{s}'"))?; + + if points.is_empty() { + return Err(anyhow!("--segment-order: empty")); + } + + let order = points.clone(); + points.sort_unstable(); + points.dedup(); + + if !points.contains(&0) { + return Err(anyhow!("--segment-order: must contain 0")); + } + + let sorted_ranges: Vec<Segment> = points.windows(2) + .map(|w| Segment(w[0], w[1])) + .chain(std::iter::once(Segment(*points.last().unwrap(), u32::MAX))) + .collect(); + + let segments = order.iter() + .map(|&p| { + sorted_ranges.iter() + .find(|&&Segment(start, _)| start == p) + .copied() + .ok_or_else(|| anyhow!("--segment-order: internal error")) + }) + .collect::<Result<Vec<_>>>()?; + + Ok(Self { + raw: s.to_string(), + segments, + }) + } + + pub fn segments(&self) -> &[Segment] { + &self.segments + } +} + +impl std::fmt::Display for SegmentOrder { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{} (", self.raw)?; + for (i, seg) in self.segments.iter().enumerate() { + if i > 0 { write!(f, ", ")?; } + write!(f, "{seg}")?; + } + write!(f, ")") + } +} + static OPT_DAEMON: OnceLock<bool> = OnceLock::new(); static OPT_LOG_LEVEL: OnceLock<LogLevel> = OnceLock::new(); 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(); - #[cfg(target_os = "linux")] static OPT_QUEUE_NUM: OnceLock<u16> = OnceLock::new(); #[cfg(target_os = "linux")] static OPT_NFT_COMMAND: OnceLock<String> = OnceLock::new(); +static OPT_SEGMENT_ORDER: OnceLock<SegmentOrder> = OnceLock::new(); const DEFAULT_DAEMON: bool = false; #[cfg(debug_assertions)] const DEFAULT_LOG_LEVEL: LogLevel = LogLevel::Debug; #[cfg(not(debug_assertions))] const DEFAULT_LOG_LEVEL: LogLevel = LogLevel::Warning; 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; - #[cfg(target_os = "linux")] const DEFAULT_QUEUE_NUM: u16 = 1; #[cfg(target_os = "linux")] const DEFAULT_NFT_COMMAND: &str = "nft"; +const DEFAULT_SEGMENT_ORDER: &str = "0,1"; pub struct Opt { daemon: bool, @@ -48,18 +122,20 @@ pub struct Opt { delay_ms: u64, #[cfg(target_os = "linux")] queue_num: u16, #[cfg(target_os = "linux")] nft_command: String, + segment_order: SegmentOrder, } impl Opt { pub fn from_args() -> Result<Self> { let mut daemon = DEFAULT_DAEMON; - 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; + 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; + let mut segment_order = SegmentOrder::new(DEFAULT_SEGMENT_ORDER)?; #[cfg(target_os = "linux")] let mut queue_num: u16 = DEFAULT_QUEUE_NUM; @@ -94,6 +170,11 @@ impl Opt { } "--no-splash" => { no_splash = true; } + "-o" | "--segment-order" => { + let s: String = take_value(&mut args, argv)?; + segment_order = SegmentOrder::new(&s)?; + } + "--fake" => { fake = true; } "--fake-ttl" => { fake = true; fake_ttl = take_value(&mut args, argv)?; } "--fake-autottl" => { fake = true; fake_autottl = true } @@ -113,6 +194,7 @@ impl Opt { daemon: daemon, log_level: log_level, no_splash: no_splash, + segment_order: segment_order, fake: fake, fake_ttl: fake_ttl, fake_autottl: fake_autottl, @@ -128,6 +210,8 @@ impl Opt { set_opt("OPT_LOG_LEVEL", &OPT_LOG_LEVEL, self.log_level)?; set_opt("OPT_NO_SPLASH", &OPT_NO_SPLASH, self.no_splash)?; + set_opt("OPT_SEGMENT_ORDER", &OPT_SEGMENT_ORDER, self.segment_order)?; + set_opt("OPT_DELAY_MS", &OPT_DELAY_MS, self.delay_ms)?; set_opt("OPT_FAKE", &OPT_FAKE, self.fake)?; set_opt("OPT_FAKE_TTL", &OPT_FAKE_TTL, self.fake_ttl)?; @@ -157,6 +241,7 @@ impl InitializedOpts { crate::info!("OPT_QUEUE_NUM: {}", queue_num()); #[cfg(target_os = "linux")] crate::info!("OPT_NFT_COMMAND: {}", nft_command()); + crate::info!("OPT_SEGMENT_ORDER: {}", segment_order()); } } @@ -168,6 +253,10 @@ pub fn no_splash() -> bool { *OPT_NO_SPLASH.get().unwrap_or(&DEFAULT_NO_SPLASH) } +pub fn segment_order() -> &'static SegmentOrder { + OPT_SEGMENT_ORDER.get().unwrap() +} + pub fn log_level() -> LogLevel { *OPT_LOG_LEVEL.get().unwrap_or(&DEFAULT_LOG_LEVEL) } @@ -233,6 +322,9 @@ fn usage() { 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!(" -o, --segment-order <u32,u32,...> Byte offsets defining segment boundaries and transmission order."); + println!(" Must include 0. (default: {DEFAULT_SEGMENT_ORDER})"); + println!(""); println!(" -h, --help Show this help"); } @@ -165,26 +165,29 @@ fn send_segment( Ok(()) } -fn send_split(view: &PktView, order: &[u32], buf: &mut Vec<u8>) -> Result<()> { - let mut it = order.iter().copied(); - - let Some(mut first) = it.next() else { - return Err(anyhow!("send_split: invalid order array")); - }; - - for next in it { - send_segment(view, first, Some(next), buf)?; - std::thread::sleep(std::time::Duration::from_millis(opt::delay_ms())); - first = next; +fn send_split(view: &PktView, order: &[opt::Segment], buf: &mut Vec<u8>) -> Result<()> { + let payload_len = view.tcp.payload().len() as u32; + + for &opt::Segment(start, end) in order { + if start >= payload_len { + crate::warn!( + "send_split: segment {} exceeds payload len {payload_len}, skipping", + Segment(start, end) + ); + continue; + } + let end = if end == u32::MAX || end > payload_len { None } else { Some(end) }; + send_segment(view, start, end, buf)?; + if end.is_some() { + std::thread::sleep(std::time::Duration::from_millis(opt::delay_ms())); + } } - send_segment(view, first, None, buf)?; - crate::debug!( - "send_split: dst={} segments={:?} tcp_payload_len={}", + "send_split: dst={} order={:?} tcp_payload_len={}", view.daddr(), order, - view.tcp.payload().len() + payload_len ); Ok(()) @@ -247,7 +250,7 @@ pub fn handle_packet(pkt: &[u8], buf: &mut Vec::<u8>) -> Result<bool> { // TODO: if clienthello packet has been (unlikely) fragmented, // we should find the second part and drop, reassemble it here. - send_split(&view, &[0, 1], buf)?; + send_split(&view, opt::segment_order().segments(), buf)?; Ok(true) } |
