This article is currently an experimental machine translation and may contain errors. If anything is unclear, please refer to the original Chinese version. I am continuously working to improve the translation.
Yes, you read that right—and no, I didn’t make a typo. TCP over IPv4 on residential broadband can actually be hole-punched… If this is old news to you, forgive me for being out of touch. Anyway, here’s my story.
To set up a fast and secure tunnel from my laptop → HomeLab across the stormy, unpredictable public internet, I’ve tried countless approaches. Here’s a quick rundown:
Apply for a public IPv4
Abandoned: I’m on China Mobile, no IPv4 for me.
Use IPv6 + DDNS
Currently kept as a backup. But IPv6 support and connectivity remain spotty in many places.
Use a self-hosted or public relay server
Abandoned: Bandwidth is limited, domestic bandwidth is expensive, and overseas relays require bypassing the firewall.
Free-ride on Synology’s QuickConnect relay
Currently used as a fallback. High latency, and bandwidth is still capped (~30Mbps).
Zerotier or Tailscale
Abandoned: UDP hole punching works with relatively low latency, but it’s often throttled by ISPs. Hard to sustain full bandwidth, and the Tailscale client feels clunky on Windows.
…and various other questionable hacks (like piggybacking on my university’s VPN), which aren’t worth mentioning.
So what am I using now? As the title suggests: IPv4 TCP Hole Punching.
But unlike my previous article on IPv6 TCP Hole Punching, where IPv6 addresses are already public, that “hole punching” was really just tricking OpenWRT’s firewall. This time, with IPv4, we’re actually punching through CGNAT and exposing a publicly accessible port.
The tool I used is natmap, which allows us to directly expose a LAN port onto the public internet.
The principle? Some ISP routers, after creating a TCP NAT mapping, don’t actually track TCP state or block incoming SYN packets. So we can follow this process to expose a public port:
- Initiate a long-lived outbound connection from a router port—say,
100.64.1.1:1234connecting towww.qq.com:443. - Keep the connection alive, so the NAT mapping persists. Suppose
100.64.1.1:1234is mapped to public1.2.3.4:40001. - Crucially, the mapping must depend only on the source port. So as a client, we can query a stunserver to discover our publicly mapped IP and port.
- Any incoming packet to
1.2.3.4:40001will now be forwarded to100.64.1.1:1234. This behavior resembles TCP Fullcone NAT (from my simple testing, many ISPs exhibit this—use Natter to test). - We’ve now successfully exposed the internal port
100.64.1.1:1234to the public internet at1.2.3.4:40001, and we know the public endpoint via the STUN server.
Once we have an exposed port, accessing the internal network becomes possible. For example, run an SSH server with dynamic forwarding, or even set up a TCP-based VPN—both are viable.
However, two minor issues remain:
- The port mapping is uncontrollable: both the public IP and the NAT-assigned port are unpredictable.
- Only the router holds the
100.64.1.1IP (the one we obtained post-NAT). All devices behind it go through a second layer of NAT. This method can’t punch through multi-layer NAT. While doing the punching on the router works, it’s cumbersome—especially on dumb routers, where direct access is limited and risks bricking the device.
To solve Problem 1, the natmap author proposed a non-standard custom IP4P address format. I’m not a fan. Instead, I use DDNS with SRV record updates. Just write a script to update your domain’s SRV record with the current public IP and port from natmap. While SSH clients don’t natively support SRV records, you can wrap SSH in a simple shell script or use existing tools like sshsrv.
For Problem 2, use the DMZ host feature: forward all TCP ports on the router to a specific internal machine, then run natmap on that machine. This way, the router’s secondary NAT doesn’t interfere. Works on both OpenWRT and consumer-grade routers.
After two months of daily use, I can confirm: TCP connections can achieve full bidirectional bandwidth, the mapping remains stable (changes only on reconnection, often lasting a month unchanged), and the connection is rock-solid. In terms of user experience, this is probably the best possible solution without a public IP, nearly matching the feel of having a real public IP—except for the lack of control over the exposed port.
This article is licensed under the CC BY-NC-SA 4.0 license.
Author: lyc8503, Article link: https://blog.lyc8503.net/en/post/ipv4-tcp-hole-punching/
If this article was helpful or interesting to you, consider buy me a coffee¬_¬
Feel free to comment in English below o/