<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="../assets/xml/rss.xsl" media="all"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>TinyComputers.io (Posts about Networking)</title><link>https://tinycomputers.io/</link><description></description><atom:link href="https://tinycomputers.io/categories/cat_networking.xml" rel="self" type="application/rss+xml"></atom:link><language>en</language><copyright>Contents © 2026 A.C. Jokela 
&lt;!-- div style="width: 100%" --&gt;
&lt;a rel="license" href="http://creativecommons.org/licenses/by-sa/4.0/"&gt;&lt;img alt="" style="border-width:0" src="https://i.creativecommons.org/l/by-sa/4.0/80x15.png" /&gt; Creative Commons Attribution-ShareAlike&lt;/a&gt;&amp;nbsp;|&amp;nbsp;
&lt;!-- /div --&gt;
</copyright><lastBuildDate>Thu, 18 Jun 2026 20:20:47 GMT</lastBuildDate><generator>Nikola (getnikola.com)</generator><docs>http://blogs.law.harvard.edu/tech/rss</docs><item><title>The Tunnel Was the Easy Part: Site-to-Site WireGuard Between My House and a VPC</title><link>https://tinycomputers.io/posts/the-tunnel-was-the-easy-part-site-to-site-wireguard-home-to-vpc.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/the-tunnel-was-the-easy-part-site-to-site-wireguard-home-to-vpc_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;25 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;h2&gt;The Tunnel Was the Easy Part: Site-to-Site WireGuard Between My House and a VPC&lt;/h2&gt;
&lt;p&gt;For about three years, a PostgreSQL database that runs dozens of projects — portfolio data, home-automation history, a dozen scraped feeds — had its port 5432 listening on the public internet. Not wide open: it sat behind a long password and TLS, the rest of the host was hardened, and nothing had ever gone wrong. But "open to the internet behind a password" is the kind of sentence that sounds fine right up until it doesn't, and I'd been meaning to fix it for months.&lt;/p&gt;
&lt;p&gt;The fix I wanted wasn't a bastion host, and it wasn't a clutch of &lt;code&gt;/32&lt;/code&gt; firewall exceptions I'd have to update every time my home IP changed. I wanted the database to simply &lt;em&gt;not be reachable&lt;/em&gt; from the public internet — and to reach it anyway, from my house, as if the AWS VPC it lives in were just another room in the building.&lt;/p&gt;
&lt;p&gt;That's a site-to-site VPN. And the moment you say "VPN" in 2026 you mean &lt;a href="https://www.wireguard.com/"&gt;WireGuard&lt;/a&gt;, because the alternatives — IPsec, OpenVPN — are heavier, slower, and far more painful to reason about. WireGuard is a few thousand lines of kernel code, a dozen lines of config per side, and a security model you can hold in your head: every peer is a public key, and a packet is either from a key you trust or it's dropped.&lt;/p&gt;
&lt;p&gt;So I expected the WireGuard part to be the work. It wasn't. The WireGuard part took about fifteen minutes and came up on the first handshake. The other day and a half went to an IP-address collision I'd created years earlier, a load-balancing router that kept quietly breaking the tunnel's return path, and the slow realization that &lt;strong&gt;a VPN doesn't connect two networks so much as force you to confront every assumption each of them made while pretending the other didn't exist.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;As an aside, my last dabbling with VPNs and such was during my FreeBSD phase in the early 2000s.  To be more precise, February of 2005.  I had an article published in &lt;a href="https://web.archive.org/web/20050718081300/http://ezine.daemonnews.org/200502/ipsec.html"&gt;Daemon News&lt;/a&gt; on using Racoon and X.509 certificates for IPSEC.&lt;/p&gt;
&lt;p&gt;This is the story of the parts that weren't WireGuard.&lt;/p&gt;
&lt;h3&gt;The shape of the thing&lt;/h3&gt;
&lt;p&gt;Two networks. At home, a flat &lt;code&gt;10.1.1.0/24&lt;/code&gt; LAN behind a MikroTik router — laptops, a large handful of Linux hosts, a few BSD hosts, the usual home-lab sprawl. In the cloud, an AWS VPC on &lt;code&gt;10.0.0.0/16&lt;/code&gt; with the database sitting on a private address, &lt;code&gt;10.0.0.76&lt;/code&gt;, that I'd very much like to be the &lt;em&gt;only&lt;/em&gt; way to reach it.&lt;/p&gt;
&lt;p&gt;The naive instinct is to put WireGuard directly on the database host. Don't. The endpoint that terminates a site-to-site tunnel becomes a router — it forwards packets for an entire subnet, runs &lt;code&gt;ip_forward&lt;/code&gt;, owns firewall and NAT rules — and you do not want that responsibility tangled up with your most important stateful service. Give it its own box.&lt;/p&gt;
&lt;p&gt;So the tunnel terminates on a dedicated &lt;strong&gt;gateway instance&lt;/strong&gt;: a &lt;code&gt;t4g.nano&lt;/code&gt; (the smallest Graviton box AWS sells, a few dollars a month, comfortably inside the free tier) with an Elastic IP so the home side always has a fixed address to dial, and — critically — with its EC2 &lt;strong&gt;source/destination check disabled&lt;/strong&gt;, because by default AWS drops any packet whose source or destination isn't the instance itself. A router's entire job is to handle packets addressed to &lt;em&gt;other&lt;/em&gt; machines; leave that check on and the gateway silently swallows everything it's supposed to forward.&lt;/p&gt;
&lt;p&gt;The home end of the tunnel lives on one of my always-on Linux boxes. It dials out — the house is behind NAT and a dynamic-ish IP, so it can't be dialed &lt;em&gt;into&lt;/em&gt; — and the AWS gateway, with its stable Elastic IP, waits to be reached.&lt;/p&gt;
&lt;p&gt;Between them I gave the tunnel its own little address space, a &lt;code&gt;/30&lt;/code&gt; carved out of a range that exists nowhere else in either network: &lt;code&gt;10.99.99.0/30&lt;/code&gt;. The gateway is &lt;code&gt;10.99.99.1&lt;/code&gt;, the house is &lt;code&gt;10.99.99.2&lt;/code&gt;. That deliberate, unused &lt;code&gt;/30&lt;/code&gt; turns out to matter enormously, for reasons that won't be obvious until the routing breaks. Hold onto it.&lt;/p&gt;
&lt;h3&gt;WireGuard, the easy twenty percent&lt;/h3&gt;
&lt;p&gt;Here is essentially the entire VPN. On the AWS gateway:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;[Interface]&lt;/span&gt;
&lt;span class="na"&gt;Address&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;10.99.99.1/30&lt;/span&gt;
&lt;span class="na"&gt;ListenPort&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;51820&lt;/span&gt;
&lt;span class="na"&gt;PrivateKey&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;lt;gateway private key&amp;gt;&lt;/span&gt;
&lt;span class="na"&gt;PostUp&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;iptables -A FORWARD -i wg0 -j ACCEPT&lt;/span&gt;&lt;span class="c1"&gt;; iptables -A FORWARD -o wg0 -j ACCEPT&lt;/span&gt;
&lt;span class="na"&gt;PostDown&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;iptables -D FORWARD -i wg0 -j ACCEPT&lt;/span&gt;&lt;span class="c1"&gt;; iptables -D FORWARD -o wg0 -j ACCEPT&lt;/span&gt;

&lt;span class="k"&gt;[Peer]&lt;/span&gt;
&lt;span class="c1"&gt;# the house&lt;/span&gt;
&lt;span class="na"&gt;PublicKey&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;lt;home public key&amp;gt;&lt;/span&gt;
&lt;span class="na"&gt;AllowedIPs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;10.99.99.2/32&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And on the home box:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;[Interface]&lt;/span&gt;
&lt;span class="na"&gt;Address&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;10.99.99.2/30&lt;/span&gt;
&lt;span class="na"&gt;PrivateKey&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;lt;home private key&amp;gt;&lt;/span&gt;
&lt;span class="na"&gt;PostUp&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;iptables -t nat -A POSTROUTING -o wg0 -j MASQUERADE&lt;/span&gt;
&lt;span class="na"&gt;PostDown&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;iptables -t nat -D POSTROUTING -o wg0 -j MASQUERADE&lt;/span&gt;

&lt;span class="k"&gt;[Peer]&lt;/span&gt;
&lt;span class="c1"&gt;# the AWS gateway&lt;/span&gt;
&lt;span class="na"&gt;PublicKey&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;lt;gateway public key&amp;gt;&lt;/span&gt;
&lt;span class="na"&gt;Endpoint&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;203.0.113.10:51820&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="c1"&gt;# the gateway's Elastic IP&lt;/span&gt;
&lt;span class="na"&gt;AllowedIPs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;10.0.0.0/16, 10.99.99.1/32&lt;/span&gt;
&lt;span class="na"&gt;PersistentKeepalive&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;25&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;wg-quick up wg0&lt;/code&gt; on each end — or &lt;code&gt;systemctl enable --now wg-quick@wg0&lt;/code&gt; to make it survive reboots — and within a second or two &lt;code&gt;wg show&lt;/code&gt; reports a handshake. Ping &lt;code&gt;10.99.99.1&lt;/code&gt; from the house; it answers. The cryptographic part, the part everyone is nervous about, is done.&lt;/p&gt;
&lt;p&gt;Two fields carry almost all the meaning. &lt;strong&gt;&lt;code&gt;AllowedIPs&lt;/code&gt; is doing double duty&lt;/strong&gt; — it's both an access-control list (packets arriving from this peer are only accepted if their source falls inside it) and a routing table (packets you send to those destinations get encrypted and shipped to this peer). On the home side I list &lt;code&gt;10.0.0.0/16&lt;/code&gt; because that's the VPC I want to reach; that single line installs a route sending all VPC-bound traffic into the tunnel. &lt;strong&gt;&lt;code&gt;PersistentKeepalive = 25&lt;/code&gt;&lt;/strong&gt; is on the home side only, and it's there because the house initiates from behind NAT: without a packet every 25 seconds, the NAT mapping that lets return traffic find its way home expires, and the tunnel goes deaf until the next outbound packet wakes it. The side with the stable public address doesn't need it; the side hiding behind NAT does.&lt;/p&gt;
&lt;p&gt;That's the whole VPN. If your two networks were sensibly addressed and your path were clean, you would be done, and this would be a very short post.&lt;/p&gt;
&lt;h3&gt;The first wall: you can't route to your own house&lt;/h3&gt;
&lt;p&gt;The tunnel was up, but a tunnel is just a wire. For a machine &lt;em&gt;inside&lt;/em&gt; the VPC to send a reply back to my house, the VPC has to know that house-bound packets go to the gateway instance. That's a route-table entry: destination &lt;code&gt;10.1.1.0/24&lt;/code&gt; (my home LAN), target the gateway's network interface. I added it.&lt;/p&gt;
&lt;p&gt;AWS rejected it:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;Route destination doesn't match any subnet CIDR blocks
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;It took me an embarrassing minute to understand, and then a much longer one to forgive my past self. Years ago, for reasons that made sense at the time and that I have completely forgotten, I had given this VPC a &lt;em&gt;secondary&lt;/em&gt; CIDR block of &lt;code&gt;10.1.0.0/16&lt;/code&gt;. My home LAN, &lt;code&gt;10.1.1.0/24&lt;/code&gt;, lives entirely inside that &lt;code&gt;/16&lt;/code&gt;. As far as the VPC is concerned, &lt;code&gt;10.1.1.0/24&lt;/code&gt; is &lt;strong&gt;local&lt;/strong&gt; — it's part of the VPC's own address space — and AWS will not let you install a route that's more specific than a local one. You cannot route to a place the network already believes it &lt;em&gt;is&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;This is the oldest mistake in site-to-site networking and I'd walked straight into it: &lt;strong&gt;the two address spaces overlapped.&lt;/strong&gt; RFC 1918 hands you &lt;code&gt;10.0.0.0/8&lt;/code&gt; — sixteen million addresses — and the one discipline that makes joining networks painless is to carve home and cloud out of ranges that can never collide. I hadn't. And re-addressing a production VPC, or renumbering my entire house, to fix a self-inflicted overlap was not a Tuesday-afternoon project.&lt;/p&gt;
&lt;p&gt;So I didn't fix the overlap. I routed &lt;em&gt;around&lt;/em&gt; it — and this is where that deliberate, unused &lt;code&gt;/30&lt;/code&gt; earns its place.&lt;/p&gt;
&lt;h3&gt;One address to stand for a whole house&lt;/h3&gt;
&lt;p&gt;Look again at the home config: &lt;code&gt;PostUp = iptables -t nat -A POSTROUTING -o wg0 -j MASQUERADE&lt;/code&gt;. That one line is doing the heavy lifting. It tells the home box to &lt;strong&gt;source-NAT every packet leaving the tunnel&lt;/strong&gt; so it appears to originate from the tunnel interface's own address, &lt;code&gt;10.99.99.2&lt;/code&gt;. From the VPC's point of view, my entire house — every laptop, every Linux box, the whole &lt;code&gt;10.1.1.0/24&lt;/code&gt; — collapses into a single client at &lt;code&gt;10.99.99.2&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;And &lt;code&gt;10.99.99.0/30&lt;/code&gt; lives &lt;em&gt;outside&lt;/em&gt; every CIDR the VPC claims. So the route AWS refused to accept for &lt;code&gt;10.1.1.0/24&lt;/code&gt; is perfectly legal for &lt;code&gt;10.99.99.0/30&lt;/code&gt;: destination &lt;code&gt;10.99.99.0/30&lt;/code&gt;, target the gateway interface. Accepted instantly. The VPC never has to route to the contested &lt;code&gt;10.1.1.0/24&lt;/code&gt; at all, because it never &lt;em&gt;sees&lt;/em&gt; &lt;code&gt;10.1.1.0/24&lt;/code&gt; — every reply goes to &lt;code&gt;10.99.99.2&lt;/code&gt;, an address it's allowed to know about, and the gateway tunnels it home.&lt;/p&gt;
&lt;p&gt;The cost of this trick is honesty about its asymmetry. &lt;strong&gt;Masquerade collapses a network into a point, and points don't have interior structure.&lt;/strong&gt; Traffic the &lt;em&gt;house&lt;/em&gt; initiates to the cloud works beautifully; the VPC sees one client and answers it. Traffic the &lt;em&gt;cloud&lt;/em&gt; initiates toward an individual home machine has nowhere to land — to AWS, &lt;code&gt;10.99.99.2&lt;/code&gt; is the whole house, with no way to address the laptop behind it. For my purpose — reaching cloud services from home — that's exactly the direction I needed, and the asymmetry is free. If you need the VPC to open connections &lt;em&gt;into&lt;/em&gt; specific home hosts, you're back to real routes and real subnets, which means back to not overlapping your address spaces in the first place. Pick your ranges so that future-you never has to make this choice.&lt;/p&gt;
&lt;h3&gt;The second wall: a router doing exactly what I told it&lt;/h3&gt;
&lt;p&gt;With routing sorted, the tunnel worked — and then, intermittently, didn't. The handshake was solid. Small things — a ping, a quick &lt;code&gt;psql&lt;/code&gt; connection — went through. But sessions would stall, and throughput was a coin flip. The classic shape of a working control path and a broken data path, which usually screams MTU (more on that later). It wasn't MTU. It was my own router being obedient.&lt;/p&gt;
&lt;p&gt;My house has two internet connections, load-balanced on the MikroTik with per-connection-classifier (PCC) mangle rules — the standard dual-WAN recipe that pins each new connection to one uplink or the other so a single flow doesn't get split across two public IPs. Which is the entire problem. WireGuard is one long-lived UDP flow to a fixed &lt;code&gt;Endpoint&lt;/code&gt;. The PCC rules looked at the tunnel's packets, classified them like any other connection, and cheerfully load-balanced them — sending some out WAN 1 and some out WAN 2. But the gateway had completed its handshake with whichever public IP came first, and a reply arriving from the &lt;em&gt;other&lt;/em&gt; WAN's address didn't match the cryptographic session. WireGuard, correctly, dropped it.&lt;/p&gt;
&lt;p&gt;The fix is a single exemption at the top of the mangle chain: traffic to or from the WireGuard endpoint is &lt;strong&gt;accepted before the PCC rules ever see it&lt;/strong&gt;, pinning the entire tunnel to one WAN. In MikroTik terms, a mangle rule matching the WireGuard port and destination with an &lt;code&gt;action=accept&lt;/code&gt; ahead of the connection-marking rules. The moment that landed, the flakiness vanished.&lt;/p&gt;
&lt;p&gt;The lesson generalizes past MikroTik: &lt;strong&gt;a VPN tunnel over a multi-WAN uplink must be pinned to one path.&lt;/strong&gt; And the deeper lesson is the one I keep relearning — this bug didn't come from the new thing. The router was doing precisely what I had configured it to do, years ago, for a completely unrelated reason. The tunnel just walked into a decision that had been sitting there, correct and forgotten, waiting for a single long-lived UDP flow to make it wrong.&lt;/p&gt;
&lt;h3&gt;Making it useful: split-horizon DNS&lt;/h3&gt;
&lt;p&gt;A tunnel that routes private addresses is only half a win. My home cron jobs and scripts didn't connect to &lt;code&gt;10.0.0.76&lt;/code&gt;; they connected to a hostname, and that hostname resolved — on the public internet, as it must — to the database's &lt;em&gt;public&lt;/em&gt; address. Routing it privately is pointless if the name still points at the front door.&lt;/p&gt;
&lt;p&gt;The answer is split-horizon DNS: the same name resolving differently depending on where you're standing. On the LAN I want &lt;code&gt;db.example.net&lt;/code&gt; to mean &lt;code&gt;10.0.0.76&lt;/code&gt;; everywhere else it should keep meaning the public address. Two small overrides do it. A static DNS entry on the MikroTik (&lt;code&gt;db.example.net → 10.0.0.76&lt;/code&gt;) catches anything using the router as its resolver, and a matching &lt;code&gt;local-data&lt;/code&gt; line in the Unbound instance that actually serves the &lt;code&gt;10.1.1.0/24&lt;/code&gt; segment catches the rest:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;local-zone: "db.example.net." transparent
local-data: "db.example.net. IN A 10.0.0.76"
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now every home machine reaches the database over the tunnel, by name, with &lt;strong&gt;zero application changes&lt;/strong&gt; — the code doesn't know or care that the address it gets back is private. The backup job that used to copy dumps across the public internet now runs entirely inside the tunnel.&lt;/p&gt;
&lt;p&gt;One caveat worth stating because it bit me for ten confused minutes: split-horizon is &lt;em&gt;per resolver&lt;/em&gt;. My main workstation sits on a different subnet — &lt;code&gt;192.168.0.x&lt;/code&gt;, behind a different router with a different upstream resolver — and it never sees the LAN's Unbound, so it kept resolving the public address and connecting the old way. A DNS horizon only covers the clients that actually ask the resolver you edited. Map your resolvers before you trust the trick.&lt;/p&gt;
&lt;h3&gt;The gotcha everyone eventually hits: MTU&lt;/h3&gt;
&lt;p&gt;I promised to come back to MTU, because it is the single most common way a WireGuard tunnel "works but doesn't," and the symptom is exactly the one I &lt;em&gt;thought&lt;/em&gt; the dual-WAN bug was: the handshake succeeds, small requests fly, and then a large transfer hangs forever with no error anywhere.&lt;/p&gt;
&lt;p&gt;WireGuard wraps every packet in roughly 60–80 bytes of encapsulation, which drops the usable MTU inside the tunnel to about 1420 bytes. On a clean path this is invisible. On a path that drops oversized packets without sending the ICMP "fragmentation needed" message back — and plenty of the internet does, because plenty of the internet firewalls ICMP into oblivion — full-size data packets vanish silently while the small control packets sail through. The cure is &lt;strong&gt;MSS clamping&lt;/strong&gt;: an &lt;code&gt;iptables&lt;/code&gt; rule on the forwarding chain that rewrites the TCP maximum-segment-size of passing connections down to fit the tunnel.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN \
  -j TCPMSS --clamp-mss-to-pmtu
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;In the interest of honesty: my home-to-AWS tunnel didn't need this. A 330 MB database dump rsync'd across it on the first attempt, full speed, no clamp. But a &lt;em&gt;second&lt;/em&gt; tunnel I stood up later — a cloud-to-cloud hop over a different path, joining a second provider into the same private fabric — reproduced the "connects fine, transfers hang" symptom perfectly, and the clamp on both ends fixed it instantly. It's path-dependent, which is exactly why it's so maddening to diagnose. Reach for it the instant authentication succeeds but bulk data stalls; it is cheap insurance and an even cheaper first guess.&lt;/p&gt;
&lt;h3&gt;What it bought&lt;/h3&gt;
&lt;p&gt;With the tunnel carrying traffic and the name resolving privately, the actual goal became a one-line change. The database's security group dropped its &lt;code&gt;0.0.0.0/0&lt;/code&gt; rule on 5432 and replaced it with the gateway and the VPC. The port that had been answering the entire internet for a year now answers exactly one place: the wire to my house. The home backup runs over the private path. My laptop talks to the database as if AWS were a closet down the hall.&lt;/p&gt;
&lt;p&gt;The bill for all of this is a &lt;code&gt;t4g.nano&lt;/code&gt; and an Elastic IP — call it a few dollars a month, much of it inside the free tier — plus a &lt;code&gt;/30&lt;/code&gt; of address space and a handful of config lines that now live in version control next to everything else I'd need to rebuild the setup from scratch. And the VPC stopped being a remote, walled-off place I reach through public endpoints. It became a routable extension of my own network, which changes what feels reasonable to build there next.&lt;/p&gt;
&lt;h3&gt;The tunnel was the easy part&lt;/h3&gt;
&lt;p&gt;Add it up and the proportions are absurd. The WireGuard configuration — the cryptography, the key exchange, the part that is genuinely hard to get right and that brilliant people spent years making bulletproof — was a dozen lines per side and worked on the first handshake. Every hour after that went to problems that had nothing to do with WireGuard: an address-space overlap I'd authored years earlier and forgotten, a load-balancing router executing old instructions faithfully into a new failure, a DNS horizon that stopped at a subnet boundary, an MTU gremlin lying in wait on the next path over.&lt;/p&gt;
&lt;p&gt;None of those are VPN problems. They're the problems a VPN &lt;em&gt;reveals&lt;/em&gt;. Two networks each grew up as the center of their own universe — chose their address ranges, their routing policies, their resolvers — with no expectation of ever meeting. The tunnel is just the introduction. Everything afterward is the negotiation between two designs that were each, locally, perfectly correct, and that turn out to disagree the moment you ask them to share a routing table.&lt;/p&gt;
&lt;p&gt;So the practical advice compresses to almost nothing. Give the tunnel its own box and its own unused &lt;code&gt;/30&lt;/code&gt;. Keep a keepalive on the side behind NAT. Pin the flow to one WAN if you have more than one. Clamp the MSS the moment large transfers stall. And, above all the rest: &lt;strong&gt;choose your private address ranges, on day one, so that home and cloud can never collide&lt;/strong&gt; — because the fifteen minutes WireGuard costs you is real, and so is the day and a half that an overlapping &lt;code&gt;/16&lt;/code&gt; will cost you somewhere down the line, long after you've forgotten you created it.&lt;/p&gt;
&lt;p&gt;The encryption is solved. The hard part was never the secret. The hard part was the addresses.&lt;/p&gt;</description><category>aws</category><category>home lab</category><category>infrastructure</category><category>iptables</category><category>mikrotik</category><category>mtu</category><category>nat</category><category>networking</category><category>routing</category><category>self-hosting</category><category>site-to-site</category><category>split-horizon dns</category><category>vpc</category><category>vpn</category><category>wireguard</category><guid>https://tinycomputers.io/posts/the-tunnel-was-the-easy-part-site-to-site-wireguard-home-to-vpc.html</guid><pubDate>Mon, 15 Jun 2026 21:30:00 GMT</pubDate></item></channel></rss>