简体中文 / [English]


IPv6 Firewall TCP Hole Punching

 

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.

One day, while using Syncthing for file synchronization, I noticed that a machine on the public internet (called B) successfully connected to my home machine sitting behind a firewall (called A).

I checked the firewall settings on my OpenWRT router, which showed: Input reject, Output accept, Forward reject.

In theory, the router should not allow direct access from external host B to internal host A — yet the connection went through…

As every programmer knows, the two hardest problems in computing are: “Why does it work?” and “Why doesn’t it work?”

So I decided to investigate — why does it work?

Syncthing ConnectionSyncthing Connection

Checking for Configuration Issues

Let me double-check all the configurations to rule out any obvious mistakes.

The official OpenWRT firmware doesn’t support UPnP by default, and I’ve never manually enabled or configured it.

The firewall settings are shown in the image below. No special rules were added to allow incoming connections.

Firewall RulesFirewall Rules

Packet Capture with Wireshark

I used Wireshark to confirm that host B indeed initiated a TCP connection from the outside and successfully reached host A inside my network.

The following image shows the packet capture from host B:

Packet CapturePacket Capture

And here’s the capture from host A:

Packet CapturePacket Capture

One unusual detail stood out: the same port (22000) was used on host A for both initiating and accepting TCP connections.

Researching Relevant Information

https://github.com/syncthing/syncthing/issues/4259 (This link provides a simple PoC of this hole-punching technique)

https://github.com/syncthing/syncthing/commit/f619a7f4ccd32ae853b3b84c0171fb5698b1370e

I found related discussions on GitHub, which revealed the underlying mechanism.

In short, most consumer-grade routers — including OpenWRT — do not fully track the state of TCP state machines.

Here’s how the trick works:

  1. Host A first attempts to connect to host B from port 22000, sending a TCP SYN packet. This outbound flow is recorded by the router’s firewall.
  2. However, the SYN packet gets dropped by the network on B’s side due to its firewall rules.
  3. Meanwhile, host A starts listening on port 22000 for incoming connections.
  4. Host B then initiates a TCP connection to A’s port 22000, sending a TCP SYN.
  5. When this SYN reaches A’s router, the firewall fails to distinguish between a SYN (initial handshake) and a SYN+ACK (reply to an outgoing connection). Since there’s already an outbound connection record from A’s 22000 to B, the router mistakenly treats B’s incoming SYN as part of the reply to that original outbound attempt — essentially classifying it as part of an outbound stream — and allows it through.
  6. Host A responds with SYN+ACK, B replies with ACK, and a full TCP connection is established. TLS handshake follows, along with keep-alive messages — hole punching succeeds.

For this to work, both A and B must have globally routable IP addresses (i.e., no IPv4 NAT). Additionally, at least one side (in this case, A’s OpenWRT router) must use a stateless or loosely stateful firewall that doesn’t properly validate TCP flags or connection states. On B’s side, the router performs normal TCP connection handling.

(In my actual setup: A is my home lab machine; B is my laptop on a university network. The university firewall is quite strict — attempts to punch through from B’s side fail.)

Explaining the Mechanism

Putting it all together, here’s how Syncthing achieves direct connectivity:

  1. After manually exchanging device IDs, the two machines discover each other’s IP addresses via public relay servers.
  2. Host A reuses the same port (e.g., 22000) to simultaneously listen and attempt outgoing connections to B.
  3. Host B initiates a connection to A — and thanks to the firewall’s weak state tracking, it gets through. Connection established.

Hole punching complete.

This article is licensed under the CC BY-NC-SA 4.0 license.

Author: lyc8503, Article link: https://blog.lyc8503.net/en/post/ipv6-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/