Bypassing ISP Deep Packet Inspection on Linux

My ISP does deep packet inspection. Not the basic DNS-level blocking that you can get around by switching to a different resolver. Actual packet inspection that looks at TLS handshakes and blocks connections to certain domains based on the SNI field in the ClientHello.

This means changing your DNS server does nothing. The ISP sees the domain name in the TLS handshake before the connection is encrypted and drops the packet. You need a different approach.

How DPI Blocking Works

When you connect to a website over HTTPS, the first thing your browser sends is a TLS ClientHello message. This message is not encrypted. It contains the Server Name Indication field, which tells the server which domain you are trying to reach. This is necessary because multiple domains can share the same IP address, and the server needs to know which certificate to present.

Your ISP inspects this field. If the domain matches their blocklist, they inject a TCP RST packet or simply drop the connection. From your perspective, the site just times out or you get a connection reset.

The Zapret Approach

There is a tool called zapret that manipulates outgoing packets to confuse DPI systems. It does not encrypt anything or route traffic through a proxy. Instead, it exploits the fact that DPI systems have to make real-time decisions about packets and often take shortcuts.

The basic techniques include fragmenting the ClientHello across multiple TCP segments, adding padding to push the SNI field into a second packet, or modifying TCP flags in ways that confuse stateful inspection engines but are tolerated by the destination server.

I run zapret as a systemd service. It hooks into the outgoing packet flow using nfqueue and modifies specific packets before they leave my machine. The destination server receives the same data, just split up differently, and handles it fine. The ISP DPI system sees fragments that it cannot reassemble quickly enough and lets them through.

DNS-over-TLS Complement

Zapret handles the TLS handshake problem, but DNS queries can still leak information. Even if the ISP cannot block the connection, they can see which domains you are resolving and log that data.

I force all DNS through DNS-over-TLS using systemd-resolved. The configuration tells resolved to only use TLS-encrypted DNS and to require valid certificates from the upstream resolver. I use Cloudflare and Google as upstream servers since both support DNS-over-TLS.

The resolved config looks straightforward. You set the DNS servers and tell it to require encrypted transport. After restarting the service, all DNS queries from the machine go through TLS on port 853 instead of plaintext on port 53.

The VPN Conflict

This setup conflicts with split-tunnel VPN configurations. When the VPN is active, DNS queries for private services need to go to the VPN DNS server over the tunnel. But the DPI bypass forces all DNS through TLS, which means queries to the VPN DNS server also try to use TLS.

The VPN DNS server is just dnsmasq. It does not speak TLS. So the queries fail and private domain names do not resolve.

The fix is a PostUp hook in the WireGuard configuration that disables DNS-over-TLS specifically on the VPN interface. The system still uses DoT for all other DNS queries, but queries routed through the VPN go as plain DNS over the encrypted tunnel. Since the tunnel itself is encrypted, this is fine from a privacy perspective.

Getting this right took some trial and error. The symptoms were confusing: public websites worked, VPN connected successfully, but private subdomains just would not resolve. Once I traced it to the DoT setting, the fix was one line.

Performance Impact

Zapret adds essentially zero latency. The packet manipulation happens in kernel space and takes microseconds. DNS-over-TLS adds a small amount of latency to the first query for each domain, maybe 10 to 20 milliseconds. Subsequent queries are cached by resolved so you do not notice it.

I have been running this setup for months and have never noticed any performance impact in normal usage. Streaming, gaming, and large downloads all work at full speed. The only time I notice it is when I forget to check if the service is running and a blocked site fails to load.

Reliability

The DPI bypass works about 99 percent of the time. Occasionally the ISP updates their inspection rules and a specific technique stops working. Zapret has multiple strategies and you can configure fallbacks. Usually updating to the latest version fixes any regressions within a day or two.

The DNS-over-TLS setup is rock solid. I have never had it fail. Systemd-resolved handles reconnections and failovers between upstream servers automatically.

This is not about accessing illegal content. My ISP blocks legitimate websites, often because a previous user of the domain or IP address did something that triggered a broad block. The blocking is opaque, there is no notification, and there is no process for getting a site unblocked.

Bypassing DPI to access legal content that your ISP has incorrectly or broadly blocked is a reasonable thing to do. The tools involved are all open source and widely used. Nobody has ever gotten in trouble for running zapret on their own machine.

If you are in a situation where your ISP does DPI blocking, this setup gives you back control over your own internet connection without the overhead and privacy implications of routing all your traffic through a third-party VPN.

← all articles wleeaf.dev →