A tunnel can end at a gateway while the TCP connection inside it continues to the client. That distinction changes what an origin's handshake timer measures.
Our previous experiment used a relay that opened a second TCP connection. Its origin-facing handshake completed before the relay forwarded application data. Here we keep the client, gateway and origin fixed, then compare that relay with a gateway that forwards an encapsulated TCP connection.
We test whether adding delay between client and gateway also delays the handshake observed next to the origin.
Two paths through the same gateway
The fixture runs in an isolated Linux container. Client and origin occupy separate network namespaces; the gateway connects their virtual Ethernet links. An IP-in-IP tunnel carries traffic between the client and gateway:
Forwarding:
client TCP == IPIP tunnel ==> gateway -- forwarded TCP --> origin
Termination:
client TCP == IPIP tunnel ==> gateway TCP relay -- new TCP --> originBoth paths use the tunnel. In the forwarding case, the gateway removes the outer IP header and routes the inner packet. In the relay case, an application on the gateway accepts the client's connection, opens a connection to the origin and copies a fixed payload and its reply.
IP-in-IP makes the packet layers easy to inspect. An outer IPv4 header with protocol number 4 carries the original IP datagram. The outer addresses identify tunnel endpoints; the inner addresses identify the original sender and destination. Forwarding after decapsulation can decrement the inner TTL. Those are the relevant rules in RFC 2003.
We capture the encapsulated packets on the client–gateway link and the TCP packets on the gateway–origin link. There is no NAT or encryption in this fixture. It tests encapsulation and termination, not a particular VPN service.
Follow one SYN across the gateway
For a forwarded connection, compare the TCP header inside the encapsulated SYN with the SYN sent toward the origin. The checker compares these fields directly:
fields = ["src", "dst", "sport", "dport", "seq", "window", "options_hex"]
assert all(encapsulated_syn[k] == origin_syn[k] for k in fields)
assert encapsulated_syn["ttl"] == 64
assert origin_syn["ttl"] == 63All six forwarding trials satisfy those checks. The sequence number, window and raw option bytes survive the gateway, along with the inner addresses and ports. The origin receives the client's TCP SYN after an ordinary forwarding hop.
The relay supplies the counterexample. Its incoming connection targets the gateway's tunnel address. Its outgoing connection comes from the gateway's origin-side address, starts with a different sequence number and arrives with TTL 64. Those are two connections, even though an application copies the same payload between them.
A TCP signature therefore needs an observation point. In this fixture, a forwarded SYN describes the client's TCP stack; a relay's outgoing SYN describes the gateway's stack. Neither statement requires the client to own the publicly visible address. This experiment uses private fixture addresses and does not test address translation.
Put delay on the link
Each topology runs three times with no imposed delay, then three times with 30 ms added to each direction of the client–gateway link. Linux netem supplies the delay on the two egress interfaces:
# Gateway toward client
tc qdisc add dev gc root netem delay 30ms
# Client toward gateway, inside the client namespace
ip netns exec client tc qdisc add dev c0 root netem delay 30msThe gateway–origin link has no imposed delay. For each origin-facing connection, the decoder measures from the captured SYN to the empty ACK that acknowledges the observed SYN-ACK. Separately, the client measures the time from connection completion through sending and receiving a known payload. The packet timestamps and application clock are not subtracted from one another.
| Path | Delay | SYN→ACK | Echo |
|---|---|---|---|
| Forwarded IPIP | 0 | 0.009 | 0.067 |
| Forwarded IPIP | 30 ms | 65.203 | 65.376 |
| Relay through IPIP | 0 | 0.007 | 0.145 |
| Relay through IPIP | 30 ms | 0.028 | 68.084 |
Delay is added in each direction. SYN→ACK is measured at the origin-facing interface; Echo is measured by the client. Both result columns show milliseconds, using medians of three trials rounded to three decimals. In the delayed forwarding case, origin SYN→ACK intervals range from 63.105 to 69.541 ms. In the delayed relay case, they range from 0.022 to 0.212 ms. Client echoes cross the delayed link in both cases.
For forwarding, the origin's SYN-ACK must travel back through the tunnel to the client, and the client's ACK must return. For termination, the gateway can finish its own origin-facing handshake without that trip. The application reply still has to reach the client.
These are configured local queues on one host. They are not Internet-distance measurements or a performance benchmark. Scheduler behavior and timer granularity contribute to the observed durations; a 30 ms queue setting does not promise an exact 60 ms round trip.
What the measurement establishes
Heretic's current TCP snapshot computes its handshake interval from stored SYN and client-ACK timestamps. An endpoint interpretation belongs alongside that number: whose TCP exchange produced it, and which path did that exchange cross? The additional ACK-number matching in this experiment is an evidence check, not a claim about Heretic's capture implementation.
A tunnel does not necessarily move the TCP endpoint to its exit. Conversely, a gateway that terminates TCP can originate a new handshake close to the origin while application traffic still crosses a longer path. The packet pairs and delayed-link controls demonstrate both cases on the same fixture.
This does not make a short handshake proof of concealment, or a preserved TCP signature proof of a direct path. The earlier experiment's direct client with delayed application processing remains a counterexample to identifying a proxy from a large application/handshake timing gap alone.
All twelve fixture echoes matched, and all eight capture processes reported zero kernel drops. The test used Linux network namespaces sharing one kernel, plain IPIP and a small TCP relay. It did not run Apostate, CloakBrowser, a commercial VPN, QUIC or a production Heretic assessment. It supplies the transport distinction those later comparisons need.
The reproduction package contains the scripts, Dockerfile, all captures, tcpdump decodes, independent checker and complete per-trial results.