The symptom was maddening because it was so nearly fine. I'd set up a VPN between my homelab and a small VPS, and most things worked. SSH connected. ping was happy. I could log in, run commands, list directories. Then I tried to pull a git repo of any size over it, and it hung. Not failed, hung. Sat there forever, transferring nothing, until I gave up and killed it.
Everything small worked. Everything large died. If you've done any networking, the hairs on the back of your neck just went up, because that pattern has exactly one usual cause, and its name is MTU.
Here's the mechanism. A normal Ethernet link carries packets up to 1500 bytes. The moment you wrap traffic in a tunnel, VPN or otherwise, you bolt extra headers onto every packet, and those headers eat into the payload you've got left. So the effective MTU inside the tunnel is smaller than 1500, maybe 1420, maybe less depending on the encapsulation. Small packets fit under the new limit and sail through, which is why SSH login and ping were fine. Big packets, a full 1500-byte git transfer, don't fit, and something somewhere has to deal with that.
The "deal with it" is supposed to be Path MTU Discovery. The sender marks packets "don't fragment", a router that can't pass an oversized one sends back an ICMP "fragmentation needed" message, and the sender shrinks accordingly. Elegant, when it works. It does not work when some overzealous firewall along the path is dropping ICMP, because somebody once read that "ICMP is dangerous" and blocked the lot. Now the sender never hears that its packets are too big. It just keeps sending them, they keep getting silently dropped, and the connection hangs. No error. No log. Nothing. The silent killer.
Confirming it is the satisfying bit. You can probe the real path MTU with ping, forcing don't-fragment and shrinking the payload until packets start getting through:
ping -M do -s 1472 10.8.0.1
That 1472 plus 28 bytes of IP and ICMP header is your 1500. It failed with "message too long" or just no reply. I walked it down: 1472, 1400, 1392. Somewhere around 1392 the replies came back, which told me my real usable MTU through the tunnel was roughly 1420 once you account for the headers, well under the 1500 both ends were cheerfully assuming.
The fix has two parts. First, set the tunnel interface's MTU to something honest, below the limit I'd just measured, so the VPN stops handing the kernel packets the path can't carry:
ip link set dev tun0 mtu 1400
That alone fixed the direct case. But TCP connections also negotiate a Maximum Segment Size at handshake, and if the endpoints still think the path is 1500-clean, you can hit the same wall for forwarded traffic. The pragmatic belt-and-braces fix is MSS clamping, where the router rewrites the MSS in passing SYN packets to match the real MTU:
iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN \
-j TCPMSS --clamp-mss-to-pmtu
--clamp-mss-to-pmtu lets it follow the route's MTU automatically, which is tidier than hard-coding a number that'll be wrong the next time I change the tunnel.
With the interface MTU set sensibly and MSS clamping on the forwarding path, the git pull that had hung for a week completed in seconds. No drama, no further tuning.
The reason I'm calling MTU the silent killer is that it gives you none of the things you rely on when debugging. No error message. No failed connection you can point at. No log line. Just a transfer that works perfectly for small things and dies for large ones, and a sender that never gets told why. If you ever meet that exact shape, "small fine, large hangs", reach for ping with don't-fragment before you touch anything else. It'll have you to the answer in five minutes, and save you the day I didn't.