A few days ago I needed to access a Cloudflare R2 bucket from home and the TLS handshake kept breaking with:
| |
I walked through every workaround I could think of on my OpenWrt router, and most of what the internet recommends turned out to be useless. Here’s what happened and what the actual fix is.
The setup
- Router: OpenWrt on a Raspberry Pi 5, PPPoE WAN on
eth0.35 - ISP: Türk Telekom
- WAN address from PPPoE:
100.112.3.164 - Public IP from ipify:
78.190.232.58 - Zapret installed and running in autohostlist mode
The first thing that matters: 100.112.3.164 is not your public IP. It’s an address inside Türk Telekom’s carrier-grade NAT pool. When client-side attacks didn’t work, I checked this and realized most advice about “getting a new IP” doesn’t apply here.
What didn’t work and why
MAC spoofing. PPPoE works by authenticated session, not by device MAC. I swapped a random MAC on the WAN interface, re-authenticated, and the RADIUS server handed me back the exact same 100.112.3.164. mac is ignored.
Reconnecting. I dropped the PPPoE session and re-dialed several times. Zapret logs showed Authentication Successful,Welcome! every time and assigned the same CGNAT address. The address comes from Türk Telekom’s account-pinned CGNAT pool, not from a fresh DHCP lease.
Long waits. I kept the interface down for a minute or two before reconnecting. Same result. TT’s RADIUS doesn’t rotate the pool.
IPv6. The router actually has working IPv6 from TT, and R2 serves it. But getting clients on the LAN behind routing means enabling DHCPv6 and RA, which changes the local network. More importantly, the problem was shared across v4 and v6, so a protocol switch wouldn’t fix it.
I ran all these tests myself because most posts on Technopat and similar forums say none of them work against CGNAT. My router confirmed it.
The real problem
With no way to get a different IP, I looked at zapret’s logs and the actual network path.
The R2 URL failed on both IPv4 and IPv6 with certificate/record errors. A Cloudflare block would not do this identically on both protocols. An ISP DPI device sitting between my WAN interface and the internet would: injecting RST or garbage during the TLS handshake to specific endpoints.
Zapret’s logs from my router show this happening:
That’s Türk Telekom’s DPI interrupting TLS handshakes to endpoints it doesn’t like. R2.dev wasn’t in zapret’s hostlist, so TLS traffic to it wasn’t going through DPI desync.
The fix: add r2.dev to zapret
Zapret (the flowchart-fake-packet kind) works by forging unusual packet characteristics around the real TLS Client Hello so the DPI device skips it. On OpenWrt, there are two hostlists:
zapret-hosts-auto.txt— domains the autohostlist learns dynamically when connections failzapret-hosts-user.txt— domains you force, manually
Autohostlist won’t always catch something like r2.dev because the failures are DPI-based, not DNS or site-based. The manual fix is one line:
When zapret restarted and the new hostlist loaded, I tested the URL that was failing:
The Cloudflare R2 response shows up cleanly because zapret is now intercepting r2.dev and running desync on the TLS handshake.
Conclusion
CGNAT combined with Türk Telekom’s DPI makes some endpoints look broken in a way that client-side tricks can never fix. MAC spoofing, PPPoE reconnects, and long disconnects don’t move the CGNAT exit IP 78.190.232.58. The only paths that work are:
- Pay Türk Telekom for static IP (230 TL/month) — gets you off CGNAT
- Ask Türk Telekom support to remove you from CGNAT — possible, though hard to get
- Accept CGNAT and route broken traffic through zapret with a manual hostlist entry
The third option solved the R2 access problem for me. The r2.dev entry in /opt/zapret/ipset/zapret-hosts-user.txt persists across reboots. If other endpoints break the same way later, add them to the same file and restart zapret.