Cross-platform DPI bypass proxy — written in Rust, works on Windows, Linux, and rooted Android/Termux.
ZeroDPI sits between your upstream VPN app (xray-core, sing-box, v2ray, Hysteria, etc.) and the internet, transparently evading Deep Packet Inspection (DPI) that would otherwise block or throttle your VPN traffic.
It is not a replacement VPN client. It is a local TCP relay that your existing VPN client connects to. Your VPN client still owns the VPN protocol, credentials, TLS settings, authentication, multiplexing, and routing rules; ZeroDPI only handles target selection, local relaying, and the DPI-bypass behavior applied at connection startup.
- Features
- What ZeroDPI Does
- Screenshots
- Quick Start
- First-Run Checklist
- Requirements
- Project Layout
- Release Package Contents
- Choosing a Mode
- Operating Modes
- Bypass Methods
- Choosing a Bypass Method
- Configuration Recipes
- Configuration Reference
- Unified Probe Scoring
- How Scanning Works
- Scan Result JSON
- Interactive TUI
- CLI Reference
- Integrating with Upstream VPN Apps
- Choosing Decoy SNIs
- IP List
- Running
- Building from Source
- Testing
- Known Limitations
- Troubleshooting
- Security & Privacy Checklist
- Extending
- Credits
- License
| Feature | Description |
|---|---|
| 🧩 11 bypass methods | wrong_seq, wrong_checksum, wrong_md5, wrong_seq_wrong_md5, wrong_ack, wrong_timestamp, tls_record_frag, wrong_seq_tls_frag, wrong_md5_tls_frag, wrong_seq_tls_record_frag, tls_frag |
| 🎯 6 operating modes | sni_spoof, ip_bypass, ip_bypass_plus, sni_scan, ip_scan, proxy_scan |
| 🖥️ TUI dashboard | Ratatui-powered live progress, selection tables, and connection monitoring |
| 🔄 Auto-rescan | Background re-scanning hot-swaps the best target without restart |
| 🧪 Smart scoring | Unified 0–100 composite score across TCP, TLS, TTFB, speed, and cert validity |
| ⚡ Concurrent scanning | Configurable concurrency per phase for fast results |
| 🔌 Protocol agnostic | Raw TCP relay — works with any TLS-based VPN protocol |
| 🪟 Windows | WinDivert packet interception |
| 🐧 Linux / Android | NFQUEUE packet interception, with selectable iptables/nftables rule setup on Linux |
ZeroDPI creates a local TCP listener, scans candidate targets, chooses a reachable target, then relays your VPN client's TCP stream to port 443 on the selected upstream IP. Depending on BYPASS_METHOD, it may also inject or rewrite the first connection packets so DPI devices see a harmless or fragmented TLS ClientHello instead of the VPN ClientHello they would normally block.
The normal connection path is:
Your apps -> VPN client -> ZeroDPI local listener -> selected edge IP:443 -> real VPN service
ZeroDPI is useful when:
- Your VPN protocol already works when the network does not inspect or block the TLS handshake.
- Your upstream VPN profile is TCP + TLS based and can be configured to connect to
127.0.0.1:44444. - A CDN edge, relay IP, or public SNI candidate can reach the same service path you need.
- You want to scan many candidates and keep using the best one without manually editing the VPN profile each time.
ZeroDPI does not:
- Provide VPN accounts, proxy credentials, routing rules, DNS rules, or encryption by itself.
- Change the real TLS server name configured inside your VPN profile.
- Bypass every DPI implementation. Different networks require different methods and candidate lists.
- Support UDP-based VPN handshakes. The relay is TCP-focused and the current interceptor paths inspect IPv4 TCP packets.
Keep the upstream VPN profile's real server name/SNI in the VPN app. Change only the address and port that the VPN app dials so it connects to ZeroDPI's local listener.
After an SNI scan, ZeroDPI shows a ranked table of candidates. Use it to compare score, latency, certificate validity, response speed, and HTTP behavior before selecting the target that new proxy connections should use.
The running dashboard confirms the active SNI/IP pair, current bypass method, local listener, uptime, connection state, byte counters, and recent relay activity. This is the main view for interactive desktop runs.
For systemd or other headless deployments, run with --no-tui and inspect logs instead of the terminal UI. The log stream shows accepted local proxy connections, bypass attempts, interceptor decisions, and successful handoff to the relay.
- Build or download ZeroDPI for your platform.
- Edit
config.tomland choose a mode. Start withMODE = "sni_spoof"unless you know you needip_bypass,ip_bypass_plus, or a scan-only mode. - Fill the input list:
sni_list.txtfor SNI-based modes.ip_list.txtfor IP-based modes.
- Run ZeroDPI with the required privileges:
# Linux / rooted Android
sudo ./zerodpi --config ./config.toml# Windows Administrator terminal
.\zerodpi.exe --config .\config.toml- Point your VPN client at ZeroDPI, not directly at the remote VPN server. The default local endpoint is
127.0.0.1:44444. - Select a candidate in the TUI, or set
AUTO_SELECT = true/ pass--auto-selectfor unattended startup.
For service deployments, combine AUTO_SELECT = true with --no-tui so the process can run without an interactive terminal.
Use this checklist when ZeroDPI starts but the VPN app still does not connect:
- Confirm the VPN profile is TCP + TLS based. UDP-only profiles are outside ZeroDPI's relay path.
- Keep the VPN profile's real TLS
serverName/ SNI unchanged. - Change the VPN profile's dial address to
127.0.0.1and dial port to44444unless you changedLISTEN_HOSTorLISTEN_PORT. - Put candidate public hostnames in
sni_list.txtwhen usingsni_spoof,sni_scan, orproxy_scan. - Put plain IPs or CIDR ranges in
ip_list.txtwhen usingip_bypass,ip_bypass_plus, orip_scan. - Start ZeroDPI before starting or reconnecting the VPN client.
- Run as Administrator/root for all interceptor methods except standalone
tls_frag, plainip_bypass, andip_bypass_pluswhen it usestls_frag. - If the TUI is unavailable, pass
--auto-select --no-tuiand read logs instead.
For the first test, keep the candidate list small. A short list makes failures easier to understand and avoids creating unnecessary outbound probes while you are still checking the VPN profile wiring.
| Platform | Runtime Requirements | Notes |
|---|---|---|
| Windows | Administrator terminal, WinDivert.dll, WinDivert64.sys next to zerodpi.exe |
Required for interceptor methods. Standalone tls_frag does not open WinDivert, but Administrator is still the safest first-run environment. |
| Linux | root or CAP_NET_ADMIN, NFQUEUE kernel support, iptables or nft depending on LINUX_FIREWALL_BACKEND |
Interceptor methods install temporary firewall rules and remove them on shutdown. |
| Rooted Android / Termux | root, compatible kernel, iptables or nft for NFQUEUE methods |
Try tls_frag first if NFQUEUE support is uncertain. |
| All platforms | A TCP + TLS upstream VPN profile, reachable candidate SNIs or IPs, and permission to bind LISTEN_HOST:LISTEN_PORT |
Default listener is 127.0.0.1:44444. |
Build-time requirements are separate from runtime requirements. See Building from Source when compiling locally, and see Release Package Contents when using packaged artifacts.
📦 zerodpi/
├── 📁 .cargo/ # Cargo environment (WINDIVERT_PATH)
├── 📁 .github/
│ └── 📁 workflows/ # GitHub Actions release pipeline
├── 📁 crates/
│ ├── 📁 zerodpi-core/ # Platform-independent: config, TLS templates,
│ │ # flow tracking, bypass methods, scanners
│ ├── 📁 zerodpi-platform/ # Packet interception: WinDivert (win), NFQUEUE (nix)
│ └── 📁 zerodpi/ # CLI binary + ratatui TUI
├── 📄 AGENTS.md # Contributor/AI agent guidelines
├── 📄 config.toml # Configuration file
├── 📄 sni_list.txt # Decoy CDN hostnames (sni_spoof mode)
├── 📄 ip_list.txt # Relay IPs / CIDR ranges (IP modes)
├── 📄 install-systemd.sh # Linux systemd service installer
├── 📁 images/ # README screenshots
├── 📁 windivert/ # Windows: WinDivert.dll, .lib, .sys
└── 🐍 build.py # Cross-platform packaging script
Packaged builds are designed to be run from the extracted directory. Keep the runtime files next to the executable unless you pass absolute paths in config.toml.
| Package | Expected Files |
|---|---|
| Windows | zerodpi.exe, WinDivert.dll, WinDivert64.sys, config.toml, sni_list.txt, ip_list.txt, README.md |
| Linux | zerodpi, config.toml, sni_list.txt, ip_list.txt, install-systemd.sh, README.md |
| Termux | zerodpi, config.toml, sni_list.txt, ip_list.txt, README.md |
Relative paths in config.toml are resolved from the directory containing the config file. This matters for service installs: if SNI_LIST = "sni_list.txt", the service expects sni_list.txt beside the same config.toml that was passed with --config.
When building with python build.py, outputs are staged under:
dist/windows/
dist/linux/
dist/linux/<target>/
dist/termux/<arch>/
dist/android-app/<runtime>/
Copy or deploy the whole generated directory, not only the binary.
| Goal | Recommended Mode | Notes |
|---|---|---|
| Bypass DPI for a TLS VPN behind a CDN | sni_spoof |
Best default. Scans SNI candidates, selects an SNI/IP pair, then relays VPN traffic. |
| Use a scanned relay IP without SNI spoofing | ip_bypass |
No packet interception. Useful when you have IPs or CIDR ranges to test directly. |
| Use a scanned IPv4 plus real-SNI fragmentation | ip_bypass_plus |
Preserves the VPN client's real SNI; supports only tls_record_frag or tls_frag. |
| Audit SNI candidates only | sni_scan |
Runs the SNI scanner, displays or saves results, then exits. |
| Audit IP/CIDR candidates only | ip_scan |
Runs the IP scanner, displays or saves results, then exits. |
| Measure real VPN performance through an existing SOCKS5 client | proxy_scan |
Tests candidates through V2RayN/sing-box and blends scanner score with end-to-end proxy results. |
Choose a bypass method separately with BYPASS_METHOD. If you cannot or do not want to use WinDivert/NFQUEUE packet interception, try BYPASS_METHOD = "tls_frag" with MODE = "sni_spoof" or MODE = "ip_bypass_plus".
Mode-specific inputs:
| Mode | Reads SNI_LIST |
Reads IP_LIST |
Starts Proxy | Uses BYPASS_METHOD |
|---|---|---|---|---|
sni_spoof |
Yes, unless SELECTED_SNI is set |
No | Yes | Yes |
ip_bypass |
No | Yes, unless SELECTED_IP is set |
Yes | No |
ip_bypass_plus |
No | Yes, unless SELECTED_IP is set |
Yes | Yes, only tls_record_frag or tls_frag |
sni_scan |
Yes | No | No | No relay; scan only |
ip_scan |
No | Yes | No | No |
proxy_scan |
Yes | No | Temporary per-candidate tests | Yes, except standalone proxy scoring still depends on your SOCKS5 proxy |
SELECTED_SNI and SELECTED_IP are operational shortcuts. They skip scanning and are useful after you have already identified a stable candidate. They are not a replacement for periodic scan-only testing, because CDN routing and IP reachability can change.
Injects a decoy ClientHello with a harmless CDN-hosted SNI (e.g. auth.vercel.com) that the DPI classifies as benign. The decoy uses a deliberately broken TCP sequence number, TCP acknowledgment number, TCP timestamp, or checksum so the real upstream server discards it — but the DPI has already passed the flow. Your real ClientHello then passes through unchallenged.
🖥️ Local apps → 🌐 VPN App → 🔄 ZeroDPI (sni_spoof) → 🌍 CDN Edge → 🖥️ VPN Server
TCP :44444 TCP :443
Use when: Your VPN server sits behind a CDN and you have CDN-hosted hostnames.
No packet interception, no SNI manipulation. Scans a list of IPs (or CIDR ranges), picks the best one via a 4-phase quality test, and relays all connections through it.
🖥️ Local apps → 🌐 VPN App → 🔄 ZeroDPI (ip_bypass) → 🌍 Selected IP :443
TCP :44444 Raw TCP (SNI untouched)
Use when: No CDN hostname is available, or you just need a reliable relay point.
Scans an IPv4 list, selects a target, then relays the VPN client's real TLS stream while applying a bypass method that does not inject or replace SNI. Use BYPASS_METHOD = "tls_record_frag" for TLS-record fragmentation with WinDivert/NFQUEUE, or BYPASS_METHOD = "tls_frag" for socket-only TCP segmentation.
🖥️ Local apps → 🌐 VPN App → 🔄 ZeroDPI (ip_bypass_plus) → 🌍 Selected IPv4 :443
TCP :44444 Real SNI + fragmentation
Use when: You need IP scanning like ip_bypass, but plain relay still exposes a blockable ClientHello.
Runs the full SNI scan pipeline (same as sni_spoof), displays ranked results, optionally saves to JSON, then exits. No proxy is started.
Use for: Auditing sni_list.txt before deployment.
Runs the full IP scan pipeline (same as the IP relay modes), displays ranked results, optionally saves to JSON, then exits. No proxy is started.
Use for: Auditing ip_list.txt before deployment.
A two-phase hybrid scan:
- Phase 1 — Standard SNI scan (
sni_list.txt) - Phase 2 — For each passing candidate, opens a SOCKS5 connection through your running V2RayN/sing-box instance and measures real-world TCP latency, TTFB, and download speed
Results are blended using a configurable weight and displayed in the TUI.
Use for: Evaluating how each SNI candidate performs end-to-end through your actual proxy setup.
| Method | Mechanism | Requires Packet Interception? | Best For |
|---|---|---|---|
wrong_seq |
Injects fake ClientHello with deliberately old TCP sequence number | ✅ Yes (WinDivert/NFQUEUE) | Most DPI systems |
wrong_checksum |
Injects fake ClientHello with corrupted TCP checksum | ✅ Yes | DPI that doesn't verify checksums |
wrong_md5 |
Injects fake ClientHello with a TCP-MD5 Signature option | ✅ Yes | DPI that accepts spoofed data but servers reject TCP-MD5 |
wrong_seq_wrong_md5 |
Injects fake ClientHello with both an old TCP sequence number and TCP-MD5 option | ✅ Yes | DPI paths where one fake-packet rejection trick is not enough |
wrong_ack |
Injects fake ClientHello with deliberately old TCP ACK number | ✅ Yes | DPI that accepts forged data but servers reject old ACKs |
wrong_timestamp |
Injects fake ClientHello with backdated TCP Timestamp TSval | ✅ Yes | DPI that accepts forged data but servers enforce PAWS |
tls_record_frag |
TLS Record Fragment: splits the real ClientHello record body into multiple tiny TLS records | ✅ Yes | DPI that can't reassemble TLS records |
wrong_seq_tls_frag |
Sends a wrong-sequence fake ClientHello, then fragments selected real client data with TLS_FRAG_* settings |
✅ Yes | Layered TCP-segment DPI paths |
wrong_md5_tls_frag |
Sends a TCP-MD5 fake ClientHello, then fragments selected real client data with TLS_FRAG_* settings |
✅ Yes | Layered TCP-MD5 plus TCP-segment DPI paths |
wrong_seq_tls_record_frag |
Sends a wrong-sequence fake ClientHello, then splits the real ClientHello body into tiny TLS records | ✅ Yes | Layered TLS-record DPI paths |
tls_frag |
TLS Fragment: writes selected client data in small TCP chunks without changing TLS bytes | ❌ No | DPI that inspects individual TCP segments |
Start with the least complex method that can run on your platform, then move to stronger or more specific methods only when needed.
| Situation | Try |
|---|---|
| Windows or Linux desktop with Administrator/root access | wrong_seq first |
| Rooted Android where NFQUEUE support is uncertain | tls_frag first |
| You cannot run packet interception but can point the VPN client at ZeroDPI | tls_frag |
| DPI appears to ignore invalid sequence tricks | wrong_seq_wrong_md5, wrong_ack, wrong_timestamp, wrong_checksum, wrong_md5, or tls_record_frag |
| DPI sees through fake packets but fails with fragmented real handshakes | tls_record_frag |
| You need a scanned IPv4 target but must preserve the VPN client's real SNI | MODE = "ip_bypass_plus" with tls_record_frag or tls_frag |
| A first firewall layer is fooled, but another layer still blocks the real ClientHello | wrong_seq_tls_frag, wrong_md5_tls_frag, or wrong_seq_tls_record_frag |
| You only need the fastest reachable IP and not SNI spoofing | MODE = "ip_bypass" |
Method behavior in more detail:
wrong_seq,wrong_ack,wrong_timestamp,wrong_checksum,wrong_md5, andwrong_seq_wrong_md5send a fake decoy ClientHello during the TCP handshake path. DPI may inspect it, but the real upstream server should discard it.wrong_md5is ZeroDPI's snake_case name for sing-box'swrong-md5spoof behavior. It adds a TCP-MD5 Signature option to the forged segment without negotiating a TCP-MD5 key.wrong_seq_wrong_md5sends one fake ClientHello with both thewrong_seqsequence rewrite and thewrong_md5TCP-MD5 option.wrong_timestampis ZeroDPI's snake_case name for sing-box'swrong-timestampspoof behavior. It requires TCP timestamps on the intercepted flow and backdatesTSvalso PAWS rejects the forged segment.tls_record_fragrewrites the real first TLS record into many smaller TLS records. The server should reassemble the TLS handshake normally.tls_fragkeeps the TLS bytes unchanged and writes selected client data in small TCP chunks from the proxy. It can fragment the first TLS ClientHello (TLS_FRAG_PACKETS = "tlshello") or a 1-based range of client writes such as"1-3".- The combo methods such as
wrong_seq_tls_fragandwrong_md5_tls_fragfirst send a decoy ClientHello, then also fragment the real ClientHello path.
If a method works but connection setup is slow, increase fragment sizes gradually (TLS_FRAG_LENGTH, TLS_RECORD_FRAG_SIZE) or try a higher-scoring SNI/IP. Very small fragments are aggressive and can add connection-start overhead.
Use this when your VPN server is reachable through a CDN edge and you have candidate hostnames in sni_list.txt.
MODE = "sni_spoof"
LISTEN_HOST = "127.0.0.1"
LISTEN_PORT = 44444
SNI_LIST = "sni_list.txt"
BYPASS_METHOD = "wrong_seq"
AUTO_SELECT = falseRun ZeroDPI, select a high-scoring SNI, then configure your VPN client to connect to 127.0.0.1:44444.
Use this for systemd, scheduled startup, or remote machines where no terminal UI is available.
MODE = "sni_spoof"
AUTO_SELECT = true
RESCAN_INTERVAL_SECS = 300
SNI_SWITCH_MIN_SCORE = 40
RELAY_MAX_LIFETIME_SECS = 0Start the process with:
./zerodpi --config ./config.toml --auto-select --no-tuiUse this when WinDivert/NFQUEUE is unavailable or you want TCP-level TLS Fragment behavior that operates entirely inside the proxy.
MODE = "sni_spoof"
BYPASS_METHOD = "tls_frag"
TLS_FRAG_PACKETS = "tlshello"
TLS_FRAG_LENGTH = "1"
TLS_FRAG_INTERVAL_MS = "0"
TCP_SEG_NODELAY = trueThis still requires your VPN client to connect to ZeroDPI's local listener. The TLS layer stays intact; ZeroDPI only controls how the ClientHello bytes are written into TCP segments. Use TLS_FRAG_PACKETS = "1-3" to fragment the first through third client-to-upstream writes instead of only the first TLS record.
Use scan-only modes to prepare candidate lists before a production run.
MODE = "sni_scan"
SNI_LIST = "sni_list.txt"
SCAN_OUTPUT = "sni-results.json"MODE = "ip_scan"
IP_LIST = "ip_list.txt"
SCAN_OUTPUT = "ip-results.json"Use this when you want ZeroDPI to pick a working IP from ip_list.txt and relay raw TCP without SNI spoofing.
MODE = "ip_bypass"
IP_LIST = "ip_list.txt"
IP_SCAN_SNI = "cloudflare.com"
AUTO_SELECT = trueUse this when you want IP scanning, but also need a bypass method that preserves the VPN client's real SNI. ip_bypass_plus is IPv4-only.
MODE = "ip_bypass_plus"
IP_LIST = "ip_list.txt"
BYPASS_METHOD = "tls_frag"
IP_SCAN_SNI = "cloudflare.com"
AUTO_SELECT = trueFor TLS-record fragmentation instead, set BYPASS_METHOD = "tls_record_frag" and run with packet interception privileges.
Use this after you have already run sni_scan and want deterministic startup without scanning every time.
MODE = "sni_spoof"
SELECTED_SNI = "auth.vercel.com"
BYPASS_METHOD = "wrong_seq"
AUTO_SELECT = trueZeroDPI resolves SELECTED_SNI at startup and creates synthetic score-0 entries because it intentionally skips the probe phases. If the hostname stops resolving or the selected edge stops working, clear SELECTED_SNI and run the scanner again.
For ip_bypass, use a fixed IP instead:
MODE = "ip_bypass"
SELECTED_IP = "104.16.132.229"Use this when V2RayN, sing-box, or another local client already exposes a SOCKS5/mixed port and you want to measure candidate performance through the full VPN stack.
MODE = "proxy_scan"
SNI_LIST = "sni_list.txt"
PROXY_TEST_SOCKS5_HOST = "127.0.0.1"
PROXY_TEST_SOCKS5_PORT = 10808
PROXY_TEST_MIN_SNI_SCORE = 20
PROXY_TEST_TOP_N = 20
PROXY_TEST_SNI_WEIGHT = 0.5Start the SOCKS5 client first, then run ZeroDPI in proxy_scan mode. This mode exits after displaying or saving results.
Use this only on trusted networks. It allows another device on your LAN to point its VPN client at the machine running ZeroDPI.
LISTEN_HOST = "0.0.0.0"
LISTEN_PORT = 44444Open the local firewall for LISTEN_PORT, then configure the other device's VPN client to dial the ZeroDPI machine's LAN IP. Keep this private; exposing the listener publicly can create an unintended open relay.
All fields go in config.toml (loaded from the binary's directory, or via --config <path>). Every field has a sensible default — start minimal and override as needed.
| Field | Type | Default | Description |
|---|---|---|---|
LISTEN_HOST |
string |
"127.0.0.1" |
IP address to bind the local TCP proxy |
LISTEN_PORT |
u16 |
44444 |
TCP port for the local proxy |
| Field | Type | Default | Description |
|---|---|---|---|
MODE |
string |
"sni_spoof" |
One of: sni_spoof, ip_bypass, ip_bypass_plus, sni_scan, ip_scan, proxy_scan |
AUTO_SELECT |
bool |
false |
Auto-pick rank-1 after scan (skip manual selection table) |
SELECTED_SNI |
string |
— | Skip SNI scan; use this hostname directly |
SELECTED_IP |
string |
— | Skip IP scan; use this IP directly |
| Field | Type | Default | Description |
|---|---|---|---|
SNI_LIST |
string |
"sni_list.txt" |
Path to decoy SNI hostname file (one per line) |
IP_LIST |
string |
"ip_list.txt" |
Path to IP list file (plain IPs or CIDR ranges) |
| Field | Type | Default | Description |
|---|---|---|---|
SCAN_TIMEOUT_SECS |
u64 |
5 |
Per-probe timeout (seconds) |
RESCAN_INTERVAL_SECS |
u64 |
0 |
Background rescan interval (0 = disabled) |
SNI_SWITCH_MIN_SCORE |
u8 |
1 |
Minimum score to auto-switch target on rescan (0–100) |
SCAN_OUTPUT |
string |
— | Path to save scan results as JSON (scan-only modes) |
| Field | Type | Default | Description |
|---|---|---|---|
SNI_MAX_CONCURRENT |
usize |
64 |
Max concurrent SNI probes |
IP_MAX_P1_CONCURRENT |
usize |
128 |
Max concurrent TCP connections in IP phase 1 |
IP_MAX_P2_CONCURRENT |
usize |
32 |
Max concurrent TLS probes in IP phase 2 |
SCAN_DOWNLOAD_CAP |
usize |
10240 |
Max bytes downloaded for speed tests |
SCAN_UPLOAD_CAP |
usize |
10240 |
Max bytes uploaded for upload speed tests |
SCAN_UPLOAD_PATH |
string |
"/" |
Candidate-relative HTTP path used for upload speed tests |
IP_SCAN_SNI |
string |
"cloudflare.com" |
SNI used during IP scan TLS phase only |
IPV6_MAX_HOSTS |
u64 |
65536 |
Max hosts expanded from a single IPv6 CIDR |
| Field | Type | Default | Description |
|---|---|---|---|
TCP_LATENCY_CAP_MS |
f64 |
500.0 |
TCP latency cap for scoring (ms) |
TLS_LATENCY_CAP_MS |
f64 |
1000.0 |
TLS handshake latency cap (ms) |
TTFB_CAP_MS |
f64 |
2000.0 |
Time-to-first-byte cap (ms) |
SPEED_CAP_BPS |
f64 |
2048000 |
Download speed cap for scoring (bytes/sec) |
UPLOAD_SPEED_CAP_BPS |
f64 |
2048000 |
Upload speed cap for scoring (bytes/sec) |
| Field | Type | Default | Description |
|---|---|---|---|
BYPASS_METHOD |
string |
"wrong_seq" |
wrong_seq, wrong_checksum, wrong_md5, wrong_seq_wrong_md5, wrong_ack, wrong_timestamp, tls_record_frag, wrong_seq_tls_frag, wrong_md5_tls_frag, wrong_seq_tls_record_frag, or tls_frag; ip_bypass_plus allows only tls_record_frag or tls_frag |
BYPASS_TIMEOUT_SECS |
u64 |
2 |
Time to wait for bypass setup before giving up |
RELAY_MAX_LIFETIME_SECS |
u64 |
0 |
Rotate established relays after this many seconds (0 = disabled/default) |
NFQUEUE_NUM |
u16 |
1 |
(Linux) NFQUEUE queue number |
LINUX_FIREWALL_BACKEND |
string |
"iptables" |
(Linux) Rule backend: iptables or nftables |
| Field | Type | Default | Description |
|---|---|---|---|
WRONG_SEQ_EXTRA_OFFSET |
u32 |
0 |
Extra bytes subtracted from injected TCP seq number |
WRONG_SEQ_SET_PSH |
bool |
true |
Set PSH flag on the spoofed packet |
WRONG_SEQ_BUMP_IP_IDENT |
bool |
true |
Bump IPv4 Identification field |
wrong_seq_wrong_md5 uses these wrong_seq parameters for the sequence rewrite, PSH flag, and IPv4 Identification behavior.
| Field | Type | Default | Description |
|---|---|---|---|
WRONG_CHECKSUM_DELTA |
u16 |
1 |
Value added to corrupt TCP checksum (≥ 1) |
WRONG_CHECKSUM_SET_PSH |
bool |
true |
Set PSH flag on the spoofed packet |
WRONG_CHECKSUM_BUMP_IP_IDENT |
bool |
true |
Bump IPv4 Identification field |
WRONG_CHECKSUM_COMPLETE_IMMEDIATELY |
bool |
true |
Signal bypass complete immediately after emission |
| Field | Type | Default | Description |
|---|---|---|---|
WRONG_MD5_SET_PSH |
bool |
true |
Set PSH flag on the TCP-MD5 spoofed packet |
WRONG_MD5_BUMP_IP_IDENT |
bool |
true |
Bump IPv4 Identification field |
WRONG_MD5_COMPLETE_IMMEDIATELY |
bool |
true |
Signal bypass complete immediately after emission |
wrong_seq_wrong_md5 uses the TCP-MD5 option and WRONG_MD5_COMPLETE_IMMEDIATELY from this group, while its PSH flag and IPv4 Identification behavior come from the wrong_seq parameter group. wrong_md5_tls_frag uses WRONG_MD5_SET_PSH and WRONG_MD5_BUMP_IP_IDENT from this group, then uses the tls_frag parameter group for the real client-data fragmentation stage. WRONG_MD5_COMPLETE_IMMEDIATELY does not affect wrong_md5_tls_frag; that combo waits for the segmented real-data stage.
| Field | Type | Default | Description |
|---|---|---|---|
WRONG_ACK_OFFSET |
u32 |
1 |
Bytes subtracted from syn_ack_seq + 1 for the spoofed TCP ACK (>= 1) |
WRONG_ACK_SET_PSH |
bool |
true |
Set PSH flag on the spoofed packet |
WRONG_ACK_BUMP_IP_IDENT |
bool |
true |
Bump IPv4 Identification field |
WRONG_ACK_COMPLETE_IMMEDIATELY |
bool |
true |
Signal bypass complete immediately after emission |
| Field | Type | Default | Description |
|---|---|---|---|
WRONG_TIMESTAMP_OFFSET |
u32 |
1 |
Value subtracted from captured TCP Timestamp TSval (>= 1) |
WRONG_TIMESTAMP_SET_PSH |
bool |
true |
Set PSH flag on the spoofed packet |
WRONG_TIMESTAMP_BUMP_IP_IDENT |
bool |
true |
Bump IPv4 Identification field |
WRONG_TIMESTAMP_COMPLETE_IMMEDIATELY |
bool |
true |
Signal bypass complete immediately after emission |
| Field | Type | Default | Description |
|---|---|---|---|
TLS_RECORD_FRAG_SIZE |
usize |
1 |
Max TLS record body bytes per TLS record fragment (≥ 1) |
TLS_RECORD_FRAG_SET_PSH |
bool |
true |
Set PSH flag on the fragmented packet |
TLS_RECORD_FRAG_BUMP_IP_IDENT |
bool |
true |
Bump IPv4 Identification field |
wrong_seq_tls_record_frag uses both the wrong_seq and tls_record_frag parameter groups.
| Field | Type | Default | Description |
|---|---|---|---|
TLS_FRAG_PACKETS |
string |
"tlshello" |
"tlshello" for first TLS record, or a 1-based client-write range like "1-3" |
TLS_FRAG_LENGTH |
Int32Range |
uses TCP_SEG_SIZE |
Fragment chunk length in bytes; accepts N or "A-B" (≥ 1) |
TLS_FRAG_INTERVAL_MS |
Int32Range |
0 |
Delay between chunks in ms; accepts N or "A-B" (≥ 0) |
TCP_SEG_SIZE |
usize |
1 |
Legacy fixed-length fallback used when TLS_FRAG_LENGTH is omitted |
TCP_SEG_NODELAY |
bool |
true |
Enable TCP_NODELAY to prevent Nagle coalescing |
TLS_FRAG_LENGTH and TLS_FRAG_INTERVAL_MS use Xray-style range syntax: 5 means a fixed value, while "1-5" chooses a fresh random value in that inclusive range for each fragment chunk. With TLS_FRAG_INTERVAL_MS = "0", ZeroDPI writes chunks back-to-back; actual TCP packet coalescing still depends on TCP_SEG_NODELAY, MSS/MTU, and the host TCP stack.
wrong_seq_tls_frag uses both the wrong_seq and tls_frag parameter groups. wrong_md5_tls_frag uses the wrong_md5 and tls_frag parameter groups.
| Field | Type | Default | Description |
|---|---|---|---|
PROXY_TEST_MIN_SNI_SCORE |
u8 |
1 |
Min Phase-1 score to enter Phase 2 |
PROXY_TEST_TOP_N |
usize |
0 |
Max candidates to carry into Phase 2 (0 = all) |
PROXY_TEST_SOCKS5_HOST |
string |
"127.0.0.1" |
SOCKS5 proxy host |
PROXY_TEST_SOCKS5_PORT |
u16 |
10808 |
SOCKS5 proxy port |
PROXY_TEST_URL |
string |
"https://speed.cloudflare.com/__down?bytes=524288" |
HTTPS URL for speed test |
PROXY_TEST_TIMEOUT_SECS |
u64 |
30 |
Per-proxy-test probe timeout |
PROXY_TEST_SNI_WEIGHT |
f64 |
0.5 |
SNI-score blend weight (0.0–1.0) |
PROXY_TEST_LATENCY_CAP_MS |
f64 |
500.0 |
Proxy TCP latency cap (ms) |
PROXY_TEST_TTFB_CAP_MS |
f64 |
3000.0 |
Proxy TTFB cap (ms) |
PROXY_TEST_SPEED_CAP_BPS |
f64 |
2048000 |
Proxy speed cap (bytes/sec) |
Both the SNI and IP scanners use the same scoring formula. Each (SNI, IP) pair or plain IP is evaluated across phases:
| Component | Max Pts | Formula |
|---|---|---|
| ✅ TCP latency | 25 | Linear: 0 ms → 25 pts, ≥ TCP_LATENCY_CAP_MS → 0 pts |
| 🔒 TLS success | 10 | Flat bonus for a successful TLS handshake |
| ⏱️ TLS latency | 15 | Linear: 0 ms → 15 pts, ≥ TLS_LATENCY_CAP_MS → 0 pts |
| 🏷️ Cert valid | 5 | Flat bonus for valid certificate (Mozilla roots via webpki-roots) |
| 🚀 TTFB | 20 | Linear: 0 ms → 20 pts, ≥ TTFB_CAP_MS → 0 pts |
| ⚡ Download speed | 7.5 | Linear: 0 B/s → 0 pts, ≥ SPEED_CAP_BPS → 7.5 pts |
| ⬆️ Upload speed | 7.5 | Linear: 0 B/s → 0 pts, ≥ UPLOAD_SPEED_CAP_BPS → 7.5 pts |
| 🏆 All phases bonus | 10 | TCP, TLS, cert, TTFB, download, and upload signals present |
Tiebreaker: Score (desc) → TCP latency (asc).
- SNI probe endpoint:
GET /on each resolved IPv4 address, thenPOST SCAN_UPLOAD_PATHfor upload timing. - IP probe endpoint:
GET /cdn-cgi/tracewithIP_SCAN_SNIin theHostheader, thenPOST SCAN_UPLOAD_PATHfor upload timing.
The scanners are quality filters, not bypass methods. They help ZeroDPI choose a target before the relay starts.
sni_scan, sni_spoof, and Phase 1 of proxy_scan read sni_list.txt, ignore blank lines and # comments, resolve each hostname, and probe every resolved IPv4 address. For each (SNI, IP) pair, ZeroDPI measures:
- DNS resolution to IPv4 addresses.
- TCP connect latency to
ip:443. - TLS handshake success and TLS latency using the candidate hostname as SNI.
- Certificate validity through the bundled Mozilla root store from
webpki-roots. - HTTP
GET /time-to-first-byte. - Download speed up to
SCAN_DOWNLOAD_CAPbytes. - Upload speed up to
SCAN_UPLOAD_CAPbytes usingPOST SCAN_UPLOAD_PATH. - HTTP status code from the first response line.
The result list is sorted by score descending, then TCP latency ascending. A high score usually means the candidate is reachable, fast, and able to complete normal TLS/HTTP checks from your network.
ip_scan and ip_bypass read ip_list.txt, ignore blank lines and # comments, accept plain IPv4/IPv6 addresses, and expand CIDR ranges. IPv4 CIDRs are expanded in full; IPv6 CIDRs are capped by IPV6_MAX_HOSTS. ip_bypass_plus uses the same IP scanner but is IPv4-only and rejects IPv6 entries.
The IP scanner runs a pipelined flow:
- Phase 1: TCP connect to each IP on port
443. - Phase 2: TLS handshake using
IP_SCAN_SNI. - Phase 3: HTTP
GET /cdn-cgi/trace. - Phase 4: small download sample up to
SCAN_DOWNLOAD_CAP. - Phase 5: separate upload sample up to
SCAN_UPLOAD_CAPusingPOST SCAN_UPLOAD_PATH.
IP_SCAN_SNI is only used for the scan's TLS/HTTP probe. It is not inserted into real proxied VPN traffic in ip_bypass or ip_bypass_plus; the upstream VPN client's own TLS handshake passes through unchanged.
proxy_scan first runs the SNI scanner, filters candidates using PROXY_TEST_MIN_SNI_SCORE and PROXY_TEST_TOP_N, then tests each survivor through PROXY_TEST_SOCKS5_HOST:PROXY_TEST_SOCKS5_PORT. This is useful when raw SNI reachability is not enough and you want to measure behavior through your actual local VPN/proxy client.
The final proxy_scan score blends:
final_score = SNI scan score * PROXY_TEST_SNI_WEIGHT
+ proxy test score * (1.0 - PROXY_TEST_SNI_WEIGHT)
Set SCAN_OUTPUT in scan-only modes to save results:
MODE = "sni_scan"
SCAN_OUTPUT = "sni-results.json"SNI scan results are an array of objects like:
[
{
"sni": "auth.vercel.com",
"ip": "76.76.21.21",
"tcp_latency_ms": 42,
"tls_ok": true,
"tls_latency_ms": 88,
"cert_valid": true,
"ttfb_ms": 140,
"download_bps": 1048576.0,
"upload_bps": 786432.0,
"speed_bps": 1048576.0,
"http_status": 200,
"score": 91
}
]IP scan results are similar, but the object starts with ip and has no sni field:
[
{
"ip": "104.16.132.229",
"tcp_latency_ms": 35,
"tls_ok": true,
"tls_latency_ms": 70,
"cert_valid": true,
"ttfb_ms": 120,
"download_bps": 2048000.0,
"upload_bps": 1048576.0,
"speed_bps": 2048000.0,
"http_status": 200,
"score": 96
}
]Failed phases are stored as null for optional numeric fields and false for boolean success flags. A low score is still useful: it tells you whether the candidate failed at TCP, TLS, HTTP, or speed measurement.
speed_bps is retained as a legacy alias for download_bps in scan-result JSON.
ZeroDPI uses ratatui for a live terminal UI in every mode:
| Mode | View 1 | View 2 | View 3 |
|---|---|---|---|
sni_spoof |
📊 Scan progress (Score · SNI · IP · TCP · TLS · TTFB · Down · Up · HTTP) | 🎯 Selection table | 📈 Dashboard |
ip_bypass |
📊 IP scan progress | 🎯 Selection table | 📈 Dashboard |
ip_bypass_plus |
📊 IP scan progress | 🎯 Selection table | 📈 Dashboard |
sni_scan |
📊 Scan progress | 📋 Results table (view-only) | — |
ip_scan |
📊 IP scan progress | 📋 Results table (view-only) | — |
proxy_scan |
📊 Phase 1 + Phase 2 progress | 📋 Blended results table | — |
Navigation: ↑/↓ or j/k to move, Enter to confirm, q/Esc to skip to rank-1.
zerodpi [OPTIONS]
Options:
-c, --config <PATH> Path to config.toml
--listen-host <HOST> Override LISTEN_HOST
--listen-port <PORT> Override LISTEN_PORT
--auto-select Auto-select top-ranked candidate
--no-tui Disable ratatui screens for headless/service runs
--json-events Emit newline-delimited JSON runtime events to stdout; implies --no-tui
--sni <SNI> Override SELECTED_SNI (skip scan)
--method <METHOD> Override BYPASS_METHOD (e.g. wrong_seq, wrong_timestamp, tls_frag)
--queue-num <N> Override NFQUEUE_NUM (Linux)
--scan-timeout <SECS> Override SCAN_TIMEOUT_SECS
--rescan-interval <SECS> Override RESCAN_INTERVAL_SECS
--sni-switch-min-score <SCORE> Override SNI_SWITCH_MIN_SCORE
--wrong-seq-extra-offset <N> Override WRONG_SEQ_EXTRA_OFFSET
--wrong-seq-no-psh Clear PSH flag (sets WRONG_SEQ_SET_PSH=false)
--wrong-seq-no-bump-ident Skip IPv4 ID bump (sets WRONG_SEQ_BUMP_IP_IDENT=false)
--bypass-timeout <SECS> Override BYPASS_TIMEOUT_SECS
--relay-max-lifetime <SECS> Override RELAY_MAX_LIFETIME_SECS
-h, --help Print help
-V, --version Print version
Configure your VPN app to point to LISTEN_HOST:LISTEN_PORT (default: 127.0.0.1:44444) instead of your actual VPN server. ZeroDPI handles the DPI bypass and relays the raw TCP stream.
In most clients this means:
| VPN Profile Field | Set To |
|---|---|
| Server address / host / endpoint | 127.0.0.1 or your configured LISTEN_HOST |
| Server port | 44444 or your configured LISTEN_PORT |
| TLS server name / SNI / peer name | The real VPN server name from your provider/profile |
| UUID/password/private key/path/header settings | Keep unchanged |
| Transport protocol | TCP + TLS compatible transport |
ZeroDPI's local listener is a raw TCP relay, not a SOCKS5 server. Do not configure your VPN client to use ZeroDPI as an HTTP/SOCKS proxy unless that client mode still opens the actual VPN TCP stream to LISTEN_HOST:LISTEN_PORT.
xray-core (click to expand)
{
"outbounds": [
{
"tag": "proxy",
"protocol": "vless",
"settings": {
"vnext": [
{
"address": "127.0.0.1",
"port": 44444,
"users": [{ "id": "<uuid>", "encryption": "none" }]
}
]
},
"streamSettings": {
"network": "tcp",
"security": "tls",
"tlsSettings": {
"serverName": "your.vpn.domain.com"
}
}
}
]
}sing-box (click to expand)
{
"outbounds": [
{
"type": "vless",
"tag": "proxy",
"server": "127.0.0.1",
"server_port": 44444,
"uuid": "<uuid>",
"tls": {
"enabled": true,
"server_name": "your.vpn.domain.com"
}
}
]
}Protocol agnostic — ZeroDPI relays raw TCP bytes. Any TLS-based VPN protocol works.
- Same CDN — Decoy hostnames must resolve to CDN edge IPs that also terminate your VPN server domain.
- Low latency — ZeroDPI ranks candidates automatically; pick from the top.
- Public, harmless hostnames — Use hostnames that are normal to access from your network and do not expose your private services.
- Keep it current — CDN routing changes. Re-run
sni_scanperiodically and remove candidates that stop completing TCP/TLS/HTTP probes. - Avoid secrets — Do not put private VPN domains, credentials, customer domains, or internal hostnames in a list you plan to publish.
# Example sni_list.txt
cloudflare.com
auth.vercel.com
www.fastly.com
For a first pass, keep the list small enough to understand the results. After you know which CDN family works on your network, expand the list and use SNI_MAX_CONCURRENT to control scan speed.
Interpreting SNI results:
- High TCP score but failed TLS usually means the IP is reachable but the hostname/IP pair is not a valid TLS target for that SNI.
- TLS succeeds but TTFB is missing can mean the edge accepts TLS but does not serve HTTP for the probe path.
- Good score but VPN still fails usually points to VPN-profile wiring, a mismatch between CDN/service routing, or a bypass method that does not work on that network.
- Many candidates fail at DNS means the list contains stale hostnames, blocked hostnames, or names unavailable from the current resolver.
Comments are allowed:
# Cloudflare-family candidates
cloudflare.com
# Vercel-family candidates
auth.vercel.com
# Plain IPv4
104.16.132.229
# Plain IPv6
2606:4700::6810:84e5
# IPv4 CIDR
104.16.0.0/24
# IPv6 CIDR (capped at IPV6_MAX_HOSTS)
2606:4700::/32
Hostnames are silently skipped — IPs and CIDRs only.
ip_bypass_plus accepts only IPv4 entries. IPv6 examples are valid for ip_scan and plain ip_bypass, but are rejected by ip_bypass_plus.
Large CIDR ranges can take time and create many outbound probes. Start with narrow ranges, keep IP_MAX_P1_CONCURRENT conservative on slow networks, and use IPV6_MAX_HOSTS to cap IPv6 expansion.
CIDR expansion can create far more probes than expected:
| Entry | Approximate Probes |
|---|---|
104.16.0.0/30 |
2 IPv4 host addresses |
104.16.0.0/24 |
254 IPv4 host addresses |
104.16.0.0/16 |
65,534 IPv4 host addresses |
2606:4700::/64 |
Capped by IPV6_MAX_HOSTS |
Use scan-only mode first when testing a new range:
MODE = "ip_scan"
IP_LIST = "ip_list.txt"
SCAN_OUTPUT = "ip-results.json"Before starting ZeroDPI:
- Make sure your VPN client is configured to connect to
LISTEN_HOST:LISTEN_PORT. - Make sure the real VPN server name is still configured inside your VPN profile's TLS settings.
- Use an Administrator/root shell for interceptor-based methods.
- Use
--no-tuifor services, SSH sessions without a proper terminal, and log-only operation.
Runtime behavior to know:
- ZeroDPI currently relays to upstream port
443. - Interceptor-based methods inspect IPv4 TCP packets in the current backends.
tls_fragdoes not open WinDivert/NFQUEUE because it operates by controlling socket writes inside the proxy.- Scan-only modes do not start the local proxy and do not need your VPN client to be running.
proxy_scanrequires the configured SOCKS5 proxy to be running before ZeroDPI starts Phase 2.
Android process-wrapper runners should start ZeroDPI without the terminal UI:
zerodpi --config <path> --no-tui --auto-select --json-events--json-events emits newline-delimited JSON to stdout and leaves human logs on stderr. It implies --no-tui, so the stream remains parseable for app controllers. Event names include startup, config_loaded, scan_started, scan_progress, scan_completed, selected_target, listener_started, connection_accepted, bypass_finished, relay_bytes, active_target_changed, root_required, fatal_error, and graceful_shutdown.
To stop a headless run, send SIGTERM and wait for ZeroDPI to exit. On Linux/Android NFQUEUE paths, ZeroDPI requests interceptor shutdown before returning so firewall guards can clean up. A controller may kill the process only after its own timeout. Exit code 0 means a scan completed or a headless proxy stopped cleanly; non-zero means the controller should show the error and retain stderr/stdout logs. If root is required but unavailable, the JSON stream includes root_required with rootless alternatives.
sudo ./zerodpi --config ./config.tomlRequires CAP_NET_ADMIN (or root), NFQUEUE kernel support, and the selected firewall command. By default ZeroDPI uses iptables; set LINUX_FIREWALL_BACKEND = "nftables" to use the nft command instead. Rules are installed on startup and automatically removed on shutdown for interceptor-based methods.
LINUX_FIREWALL_BACKEND = "nftables"install-systemd.sh exists for Linux servers and headless machines where ZeroDPI should start at boot and keep running without an interactive terminal. It installs ZeroDPI as a native systemd service instead of requiring you to keep a root shell open. It is not needed for interactive desktop runs, Windows, or Android/Termux.
Run it from the same directory as the ZeroDPI release files:
sudo ./install-systemd.sh
systemctl status zerodpi.service
journalctl -u zerodpi.service -fBefore running the installer, edit config.toml, sni_list.txt, and ip_list.txt in that directory. The installer requires root, systemctl, a running systemd instance, a ZeroDPI executable, and config.toml next to the script.
The installer:
- Finds the ZeroDPI executable in the script directory (
zerodpiorzerodpi-*). - Uses that directory as the service
WorkingDirectory, so relative config/list paths resolve there. - Verifies the generated unit with
systemd-analyze verifywhen that command is available. - Warns if
sni_list.txtorip_list.txtis missing, and makes the binary executable. - Writes
/etc/systemd/system/zerodpi.service. - Runs the service as
root, which is required for NFQUEUE-based bypass methods. - Starts ZeroDPI with the resolved binary and config paths plus
--auto-select --no-tui. - Sets
RUST_LOG=info, sends output to journald, restarts on failure, reloads systemd, enables the service at boot, and starts it immediately.
The generated unit deliberately runs with --auto-select --no-tui because services cannot wait for keyboard selection or render the TUI. Use journalctl -u zerodpi.service -f to watch scan results, selected candidates, bypass attempts, and relay activity.
Useful service commands:
sudo systemctl restart zerodpi.service
sudo systemctl stop zerodpi.service
sudo systemctl disable --now zerodpi.service
sudo systemctl daemon-reloadIf you move the release directory, binary, or config file after installation, rerun sudo ./install-systemd.sh from the new directory so the unit points at the correct paths. The installer rejects paths containing whitespace, quotes, backslashes, or % characters because those are unsafe in the generated systemd unit.
.\zerodpi.exe --config .\config.tomlRun from an Administrator prompt. Requires WinDivert.dll and WinDivert64.sys next to the EXE.
If Windows blocks the driver or DLL, unblock the downloaded archive before extracting it, then run the terminal as Administrator. Keep the windivert/ runtime files next to the executable when packaging manually.
./zerodpi --config ./config.tomlRequires root, a supported firewall backend command (iptables by default, or nft with LINUX_FIREWALL_BACKEND = "nftables"), and a kernel with NFQUEUE support.
On Android, tls_frag is the simplest method to try first because it does not require NFQUEUE interception. Interceptor-based methods still need root and a compatible kernel.
Requires Rust 1.75+ (MSRV). The workspace targets the 2021 edition.
cargo build --releaseThe plain Cargo build writes binaries under target/release/. The packaging helper copies the binary plus runtime files into dist/ so the result is easier to deploy.
# Auto-detect Linux/Windows host where supported
python build.py
# Explicit platform
python build.py --platform linux
python build.py --platform windows
python build.py --platform termux
# Build all supported package families from one host where toolchains exist
python build.py --platform allLinux (click to expand)
sudo apt-get install libnetfilter-queue-dev
cargo build --releaseLinux packaging:
python build.py --platform linuxWindows hosts can cross-compile Linux packages through cargo-zigbuild when Zig and the Rust targets are installed:
python build.py --platform linux --linux-target x86_64
python build.py --platform linux --linux-target aarch64
python build.py --platform linux --linux-target allWindows (click to expand)
Requires the MSVC toolchain. When using build.py, WinDivert is downloaded into the repo-local windivert/ folder automatically.
cargo +stable-x86_64-pc-windows-msvc build --releaseOr use the build script:
python build.py --platform windowsUseful Windows build options:
python build.py --platform windows --windivert-version 2.2.2
python build.py --platform windows --toolchain stable-x86_64-pc-windows-msvcAndroid / Termux (click to expand)
python build.py --platform termux --termux-arch all --android-ndk /path/to/android-ndkUse --termux-arch armv7 or --termux-arch armv8 to build one Android ARM package. Output is staged under dist/termux/<arch>/.
Additional Termux options:
python build.py --platform termux --termux-arch armv8 --android-api 23
python build.py --platform termux --termux-arch x86_64 --android-ndk /path/to/android-ndkAndroid app APK packaging stages the native runtime and then runs the Android Gradle project:
python build.py --platform android
python build.py --platform android --android-app-runtime both
python build.py --platform android --android-app-abi debug --android-ndk /path/to/android-ndk
python build.py --platform android --android-app-build-type releaseThe default Android app build creates the first-release public ABIs
arm64-v8a and armeabi-v7a under dist/android-app/rootless/. Use
--android-app-abi debug to also build x86_64 for emulator work.
Runtime variants:
rootless: buildszerodpiwith packet interception disabled. Scan-only,ip_bypass, and supportedtls_fragworkflows still run; NFQUEUE modes fail with a clear unsupported-artifact/rootless-alternative error.full: keeps the default packet-interception feature for rooted NFQUEUE testing. Device support still depends on root, firewall commands, and kernel NFQUEUE support.
Output layout:
dist/android-app/rootless/
assets/zerodpi/config.toml
assets/zerodpi/sni_list.txt
assets/zerodpi/ip_list.txt
bin/<abi>/zerodpi
jniLibs/<abi>/libzerodpi_exec.so
zerodpi-runtime-manifest.json
zerodpi-android-rootless-debug.apk
build.py passes the staged runtime directory to Gradle, so the APK packages
jniLibs/ and the default config/list assets without a manual copy into the app
module. The app runs the ABI-matched libzerodpi_exec.so from
ApplicationInfo.nativeLibraryDir. bin/<abi>/zerodpi is still provided for
adb smoke tests such as zerodpi --version and rootless listener checks.
cargo test --workspaceUnit tests cover:
- 🔄 TLS ClientHello byte-exact round-trip
- 🏗️ Handshake state machine
- 📦 IPv4/TCP packet rewrite and checksum recomputation
- ⚙️ Config parsing (all fields, defaults, validation modes)
- 📊 SNI & IP scanner unified scoring
- 🌐 CIDR expansion, IPv6 cap, hostname skipping
- Upstream relay port is fixed to
443in the current proxy path. - Interceptor-based methods currently parse and rewrite IPv4 TCP packets. IPv6 scan entries can be tested in IP scanning, but packet-interceptor bypass behavior is IPv4-oriented.
- UDP VPN transports are not supported by the relay. Use TCP + TLS profiles.
- ZeroDPI does not create candidate lists for you. Good results depend heavily on SNI/IP candidates that make sense for your network and upstream service.
SELECTED_SNIskips probing. It can start faster, but it will not tell you whether the resolved edge is currently healthy.ip_bypassdoes not spoof SNI. It relays the upstream VPN client's original TLS bytes to the selected IP.ip_bypass_plusalso preserves the upstream VPN client's original SNI, but can fragment the first real ClientHello withtls_record_fragortls_frag.wrong_timestamprequires TCP timestamps to be negotiated by the host OS on the upstream connection. If the intercepted ACK has no Timestamp option, ZeroDPI aborts that bypass attempt.- Very aggressive fragmentation (
TLS_FRAG_LENGTH = "1"orTLS_RECORD_FRAG_SIZE = 1) can add overhead during connection setup. - Firewall, antivirus, endpoint security, or kernel driver policy can block WinDivert/NFQUEUE even when ZeroDPI is configured correctly.
| Symptom | What to Check |
|---|---|
| No traffic reaches ZeroDPI | Your VPN app must connect to 127.0.0.1:44444 or your configured LISTEN_HOST:LISTEN_PORT. Keep the real server/SNI inside the VPN TLS settings. |
| Permission or interceptor errors | Use Administrator on Windows or root/CAP_NET_ADMIN on Linux. For Linux, install NFQUEUE support and make sure the selected firewall backend is available (iptables or nft). |
| Windows starts but interception fails | Confirm WinDivert.dll and WinDivert64.sys are next to zerodpi.exe and that the terminal is elevated. |
| Linux service starts then exits | Run journalctl -u zerodpi.service -f, check config.toml, and confirm sni_list.txt / ip_list.txt paths are valid relative to the service working directory. |
| Scan returns no useful candidates | Increase SCAN_TIMEOUT_SECS, lower concurrency on weak networks, refresh the candidate list, and verify the CDN or IP range is reachable without ZeroDPI. |
| TUI is garbled over SSH or systemd | Run with --no-tui and rely on logs. |
wrong_seq works on simple paths but fails on layered firewalls |
Try wrong_seq_tls_frag for TCP-level fragmentation or wrong_seq_tls_record_frag for TLS-record fragmentation. Both keep the fake wrong-sequence stage for the first DPI layer. |
wrong_md5 works partly but the real ClientHello is still blocked |
Try wrong_md5_tls_frag so the fake TCP-MD5 stage is followed by TCP-level fragmentation of the real ClientHello. |
wrong_seq, wrong_ack, wrong_timestamp, wrong_checksum, wrong_md5, or wrong_seq_wrong_md5 does not work |
Try tls_record_frag (TLS-record layer), then tls_frag (TCP layer). Different DPI devices fail on different TCP/TLS behaviors. |
| Connections start but stall | Raise BYPASS_TIMEOUT_SECS, reduce SNI_MAX_CONCURRENT, and check whether the selected candidate has high TTFB or low speed. |
| gRPC works after restart but fails after hours | Enable RESCAN_INTERVAL_SECS and set RELAY_MAX_LIFETIME_SECS to a positive value so long-lived relays reconnect through the latest working target. |
| Scan-only mode works but relay mode fails | Confirm the VPN profile dials ZeroDPI, not the real server directly, and confirm the selected BYPASS_METHOD is supported on your platform. |
proxy_scan exits before Phase 2 |
Start the configured SOCKS5/mixed proxy first and verify PROXY_TEST_SOCKS5_HOST:PROXY_TEST_SOCKS5_PORT. |
A fixed SELECTED_SNI stopped working |
Clear SELECTED_SNI, run sni_scan, and select a fresh candidate. DNS and CDN edge routing can change. |
| Linux rules remain after a forced kill | Restart ZeroDPI cleanly if possible, or inspect/remove matching iptables/nft rules manually. Normal shutdown removes temporary rules. |
| Another device cannot connect to ZeroDPI | Use LISTEN_HOST = "0.0.0.0", open the host firewall for LISTEN_PORT, and make sure the other device dials the ZeroDPI machine's LAN IP. |
Use RUST_LOG=debug when collecting detailed diagnostics:
RUST_LOG=debug ./zerodpi --config ./config.toml --no-tuiOn Windows PowerShell:
$env:RUST_LOG = "debug"
.\zerodpi.exe --config .\config.toml --no-tuiOn systemd:
sudo systemctl status zerodpi.service
sudo journalctl -u zerodpi.service -f
sudo journalctl -u zerodpi.service --since "10 minutes ago"- Do not publish real VPN endpoints, private SNI lists, proxy credentials, or machine-specific paths.
- Treat screenshots as publishable artifacts only after removing visible private details and embedded metadata.
- Keep
config.toml,sni_list.txt, andip_list.txtout of public commits if they contain operational infrastructure. - Prefer
LISTEN_HOST = "127.0.0.1"unless another device must connect to ZeroDPI. - Review logs before sharing them. Logs can include local ports, selected candidates, timing, and failure reasons.
- Use scan-only modes before production changes so you can validate candidates without running the relay.
- Do not expose
LISTEN_HOST = "0.0.0.0"on public interfaces unless you have a separate access-control layer. - Treat
SCAN_OUTPUTfiles as operational data. They can reveal which CDNs, IP ranges, and hostnames work from your network. - Follow the laws and acceptable-use rules that apply to your network, service provider, and jurisdiction.
| Task | Interface / Location |
|---|---|
| New bypass method | Implement [zerodpi_core::methods::BypassMethod] → register in methods::build_method |
| New OS backend | Implement [zerodpi_core::interceptor::PacketInterceptor] in zerodpi-platform |
| New operating mode | Add branch in zerodpi/src/main.rs guarded by cfg.MODE + implement proxy logic in zerodpi-core::proxy |
- Original Python project:
patterniha/SNI-Spoofing - WinDivert: https://reqrypt.org/windivert.html
MIT — see LICENSE.


