Pivoting and Tunneling Part 4: What Tunneling Does to Your Packets — MTU, Fragmentation, and PMTUD Inside a Tunnel

Lab-only note: continuing the same simulated CONTOSO.LOCAL lab. Authorised testing only.

This is the post that’s easiest to skip on a real engagement and the one that costs the most time when you do. Every pivot in this series — SSH forwarding, Chisel, ligolo-ng — encapsulates your traffic inside another protocol, and every layer of encapsulation eats into the payload space available before a packet has to fragment. Get this wrong and the symptom isn’t an error message, it’s a transfer that silently stalls at a suspiciously consistent size.

Stacking the overhead

A standard Ethernet path assumes a 1500-byte MTU end to end. Every encapsulation layer in this series subtracts from that budget before your actual data gets a look-in:

LayerTypical overheadRunning total eaten from 1500
Outer IP + TCP (the physical path)20 + 20 = 40 bytes40
SSH (Part 1) — packet framing, MAC, padding~50–70 bytes depending on cipher/MAC~90–110
Chisel (Part 2) — inner SSH-protocol handshake and an outer TLS record, both riding inside HTTPTLS record overhead (~40–100 bytes) + HTTP/WebSocket framing + the inner SSH framing again~150–250+
ligolo-ng (Part 3) — TUN interface MTU plus the outer TCP/TLS connection to the agentTUN interface commonly defaults to 1500 itself, but the effective payload has to fit inside the outer TLS/TCP connection’s own budgetEffectively the tightest constraint in the stack, because two separate MTUs (tun ↔ outer transport) both have to hold

The number that matters isn’t any single row — it’s that your actual application data (an RDP session, a large secretsdump transfer, a chunky HTTP response) now has to fit inside a payload space that’s meaningfully smaller than the 1500 bytes every intermediate device on the real path still assumes is fine. A packet that would have sailed through unfragmented on a direct connection can be too large the moment it’s wrapped in a tunnel — and that’s before accounting for anything the legitimate network path is already doing, like an IPsec-terminated WAN edge or an MPLS core adding its own overhead on top.

Why PMTUD specifically breaks down inside a pivot

Path MTU Discovery exists to solve exactly this class of problem on a normal network: send a packet with the Don’t Fragment bit set, and if some router along the path has a smaller onward MTU, it drops the packet and sends back an ICMP “Fragmentation Needed” message telling the sender to shrink. The sender adjusts and retries. This works fine when the path is a chain of routers all aware of the same IP header.

Inside a tunnel, three things break that assumption at once:

  1. The DF-set packet the application sends never actually touches the real network path as itself. It gets encapsulated first — wrapped in TLS, HTTP, SSH framing, or all of the above depending which tool from this series you’re using. The router that would have sent the ICMP Fragmentation-Needed message sees an ordinary-looking encapsulated packet at its MTU, not the application’s original DF-set packet, so the signal that would have triggered PMTUD never gets generated for the thing that actually needs to shrink.
  2. Even when an ICMP message does get generated, it often can’t get back to where it’s useful. A tunnel endpoint (say, the ligolo-ng proxy or the Chisel server) sees ICMP addressed to the outer connection’s IP, not to the original application inside the tunnel — the message either gets silently dropped or, at best, tells the wrong layer to shrink.
  3. A lot of the infrastructure in between — corporate firewalls, cloud NAT gateways, the egress proxy that’s forcing your Chisel traffic to look like ordinary HTTPS — filters ICMP outright, which is the same classic PMTU black-hole problem the whole industry has fought since the early days of ubiquitous firewalling, just now happening one layer deeper where you have far less visibility.

The result: no error, no explicit failure — packets above the effective size just vanish. TCP’s own retransmission logic eventually notices the connection isn’t making progress and, depending on the OS and the specific stack, may or may not converge on a smaller effective segment size on its own. In practice, over a fresh pivot, it frequently doesn’t converge cleanly, and the symptom looks exactly like a stalled connection: an RDP session that connects and then freezes, a secretsdump run that hangs partway through a large hive, an interactive shell that works fine for typed commands (small packets) and falls over the moment you cat a large file (large packets).

Diagnosing it — and why this looks like an RST problem if you’re not careful

This is the trap worth calling out explicitly: a tunnel silently black-holing large packets and a mid-path firewall injecting RSTs produce superficially similar symptoms — a connection that was working, then isn’t. The rst-forensics six-scorer approach exists precisely to tell these apart, and the tell here is decisive: a PMTU black hole shows no RST at all. The connection just stops sending data past a certain point and sits there, retransmitting the same segment, with no reset from either side. If you see an actual RST, go back to rst-forensics’ scoring model — this isn’t that problem.

To actually measure the effective MTU through a live pivot rather than guessing, pmtud-sweeper’s binary-search approach works unmodified — point it through the tunnel the same way you’d point any other tool at a pivoted destination:

# through a chisel SOCKS tunnel, for example
proxychains pmtud-sweeper --mode tcp-mss 10.10.40.10 --port 1433

It’ll binary-search the largest DF-set segment that actually gets an ACK back, which — because it works from observed acknowledgment behaviour rather than depending on ICMP — sidesteps the exact black-hole problem described above. Whatever value it converges on is your real, tunnel-inclusive effective MTU, not the 1500 everything upstream still assumes.

The practical fix: clamp the MSS, don’t chase ICMP

Once you know the effective ceiling, the fix is the same one production network engineers reach for on any tunnel interface — clamp the TCP MSS down to something that fits inside the tunnel’s real budget, rather than hoping PMTUD recovers on its own:

# on the pivot or proxy host, for traffic riding the tunnel interface
iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN \
  -j TCPMSS --clamp-mss-to-pmtu

--clamp-mss-to-pmtu rewrites the MSS option in the SYN itself to match the outgoing interface’s real MTU, forcing both ends to agree on a smaller segment size from the very first packet of the connection — no ICMP round-trip required, no black hole to fall into. If you’d rather set an explicit ceiling instead of relying on the interface’s reported MTU (useful when the tunnel interface itself misreports, which ligolo-ng’s TUN interface can do since it doesn’t know about the outer TLS connection’s own overhead):

iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN \
  -j TCPMSS --set-mss 1200

1200 isn’t a magic number — it’s a conservative starting point that comfortably clears the worst-case overhead stack from the table above; measure with pmtud-sweeper and tighten or loosen from there.

Where this fits

This post is the deliberate crossover point between this series and the packet-level tooling already on the site — pmtud-sweeper for measurement and rst-forensics for telling a black hole apart from an actual reset. Part 5 picks the pace back up with multi-hop pivot chains — and the MTU stacking problem here only compounds once you’re tunneling a tunnel.