summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md8
-rw-r--r--README.md14
-rw-r--r--dpibreak.1.in36
-rw-r--r--dpibreak.1.md28
-rw-r--r--src/opt.rs118
-rw-r--r--src/pkt.rs35
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
diff --git a/README.md b/README.md
index e2113c9..35711fd 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/src/opt.rs b/src/opt.rs
index f0ff59c..b4592cd 100644
--- a/src/opt.rs
+++ b/src/opt.rs
@@ -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");
}
diff --git a/src/pkt.rs b/src/pkt.rs
index aecd437..f7f2c52 100644
--- a/src/pkt.rs
+++ b/src/pkt.rs
@@ -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)
}