The joys of carrier-grade NAT
Neuland
Living in the capital of Neuland doesn't guarantee to have decent internet connectivity. Some 20 years ago I lived in a much smaller city and it really mattered in which street you lived in: either you'd only get ISDN or you were lucky enough to move into a building where DSL is available. And apparently nothing really changed in all these years, except now you can choose between DSL and cable, and if you're lucky FTTC. But at my address no DSL and no fiber connections are available, only cable. One can choose from two (2) companies, one is called Pÿur and o2. Rolled a dice and went with the former and was pretty satisfied at first: stable enough service, a real flat rate, almost as fast as advertised, and the hotline (needed to exchange a modem once) was easy to reach and even appeared to be somewhat competent.
But all that changed a few weeks ago when Pyur decided to switch me over to CGN, or Carrier-grade NAT, one of these abominations some providers feel necessary when trying to deal with the impending IPv4 address exhaustion. Yes, I've called up their hotline, but every time I call them, the less the person on the other end understands the topic, and they don't really know what they are doing. The case is open since October now with no solution in sight.
So, how do we deal with that? IPv6 of course, as the modem still receives a valid, reachable IPv6 address so as long as I have IPv6 connectivity I can reach my little home network just fine, even via DNS. But I may not have IPv6 connectivity everywhere I go and thus may not be able to reach my home network. Also, there are a couple remote machines that used to access my network but not have an IPv6 uplink and are limited to IPv4.
Solutions
Luckily I have this little hosted VM somewhere on the internet equipped with a static IPv4 (and IPv6) address. That will be my little helper host to forward traffic. Let's use some ASCII art to sketch out the scenario:
|-- IPv4, CGN ------- NOT accessible from the IPv4 world | | |-- VPN -- Hosted VM --- accessible from the IPv4 world | with IPv4 | connectivity _______ | |machine| | | at .|----- CPE -----| | home .| (FritzBox) | |_______| |-- IPv6, public -------- accessible from the IPv6 world
DNS
To bring DNS into the game: we will update our dynamic DNS record separately for IPv4 and for IPv6. For example, with DuckDNS:
curl -s "https://www.duckdns.org/update?domains=${DOMAIN}&token=${TOKEN}&ip4=${IP4}" curl -s "https://www.duckdns.org/update?domains=${DOMAIN}&token=${TOKEN}&ipv6=${IP6}"
That results in:
$ dig +short -t A example.duckdns.org 93.184.216.34 # The address of our hosted VM $ dig +short -t AAAA example.duckdns.org 2606:2800:220:1:248:1893:25c8:1946 # The address of our CPE at home
All requests over IPv6 will terminate directly on the CPE (that does the port-forwarding to the actual machine serving the request) and all requests over IPv4 will terminate at the hosted VM. Here, we'll need to use some tricks to get these packets forwarded to our home network after all.
VPN
The machine in the home network will be connected via VPN to the hosted VM, so packets can flow back and forth via that tunnel connection. I went with a small VPN daemon named fastd, as the setup was easy enough and worked quite well, and was already in place for other applications. There are some serious performance issues here, but these will be discussed below and in a following post. A switch to WireGuard (the obvious choice these days) may or may not make a difference here, this is yet to be seen.
HTTPS
The most obvious problem to be solved was the web server running on my home network. To make it accessible via IPv4 again we need to use some kind of reverse proxy on our hosted VM.
-
A lighttpd instance was already running here. However, we needed to proxy to a HTTPS server but apparently that's not possible with mod_proxy.
-
The next contender was Nginx of course, since this was running on the other end as well. But all the same, Nginx is not able to proxy to HTTPS servers just so. An out-of-tree http_proxy_connect_module would to the trick, but this might be an exercise for another day.
-
Finally we settled for Apache because it can proxy to HTTPS servers out-of-the box (with both mod_proxy_connect and mod_proxy_http enabled):
<VirtualHost *:80> RewriteEngine On RewriteCond %{HTTPS} off RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} CustomLog /var/www/logs/access_notls.log combined Protocols h2 http/1.1 </VirtualHost> <VirtualHost *:443> ServerName example.duckdns.org ServerAlias sub.example.duckdns.org Protocols h2 http/1.1 ErrorLog /var/www/logs/error_example.log CustomLog /var/www/logs/access_example.log combined SSLEngine on SSLCertificateFile /etc/ssl/private/example.pem SSLCertificateKeyFile /etc/ssl/private/example.key SSLProxyEngine on SSLProxyVerify none SSLProxyCheckPeerName off # SSLProxyCheckPeerExpire off ProxyPass / https://example.fastd/ ProxyPassReverse / https://example.fastd/
Here, example.fastd
is basically just an /etc/hosts
entry pointing to the
other end of that VPN connection, i.e. the server in my home network. Notice the
SSLProxyCheckPeerName
option set since the TLS certificate on the server will only be valid for
example.duckdns.org
of course, not for example.fastd
.
This works quite well and is fast enough for our little web server instance. It
even does the Right Thing™ for ACME clients
most of the time. For dehydrated we need to add
the --ipv6
switch so that the home network server will handle the
verification steps (i.e. placing a file into the .well-known/acme-challenge/
directory).
Loghost
Several external machines are configured to use our machine at home as a loghost. That is, they are sending syslog messages to a Syslog-ng instance over syslog-tls. But some of these machines can only use IPv4. Let's reconfigure these machines to use our hosted VM as a loghost instead, and then forward these packets through the VPN to the machine at home. Most of these external machines have Rsyslog installed, i.e. they are acting as the syslog client:
$ cat /etc/rsyslog.d/local.conf $ActionWriteAllMarkMessages on $PreserveFQDN on $DefaultNetstreamDriverCAFile /etc/ssl/certs/ISRG_Root_X1.pem $DefaultNetstreamDriver gtls $ActionSendStreamDriverMode 1 $ActionSendStreamDriverAuthMode 509/certvalid # Or use "anon" kern,mark.*;*.warning @@hosted-vm.example.net:6514
Similarly to the SSLProxyCheckPeerName
option above, we need to set
ActionSendStreamDriverAuthMode
to allow the TLS connection because
the subject name cannot be validated.
On the hosted VM we will use iptables nftables
to mangle our packets accordingly:
# Enable IP forwarding $ sysctl net.ipv4.ip_forward=1 # Setup netfilter rules nft add table ip nat nft add chain ip nat prerouting '{ type nat hook prerouting priority -100; }' nft add chain ip nat postrouting '{ type nat hook postrouting priority 100; }' nft add rule ip nat prerouting tcp dport 6514 dnat to example.fastd nft add rule ip nat postrouting ip daddr example.fastd masquerade
That wasn't quite enough and we still had to adjust the policy of our
forward
chain to accept
:
$ cat /etc/nftables.conf [...] chain forward { type filter hook forward priority 0; policy accept; }
With all that in place, packets to our hosted VM on 6514/tcp
will be sent to
our machine at home.
SSH
While our machine at home can be reached via IPv6 just fine, we may want to setup a backup route via IPv4, that is our hosted VM. For that we'll add another Netfilter rule and our configuration should now look something like this:
table ip nat { chain prerouting { type nat hook prerouting priority dstnat; policy accept; tcp dport 2022 dnat to example.fastd:22 tcp dport 6514 dnat to example.fastd } chain postrouting { type nat hook postrouting priority srcnat; policy accept; ip daddr example.fastd masquerade } }
Performance
The setup above works quite well most of the time, but when trying to use a more network-intensive application, for example Nextcloud, the poor network performance really shows.
This will be covered in detail in the next post, but let's dump some benchmarks here:
# Running speedtest on the hosted VM, which located in a well # connected data center in Europe: $ speedtest RTT: 1.685 ms Testing download speed......................................................... Download: 4224.57 Mbit/s Testing upload speed........................................................... Upload: 3174.84 Mbit/s # Running speedtest on my machine at home, with an advertised 500 Mbps uplink: $ speedtest RTT: 30.911 ms Testing download speed......................................................... Download: 432.97 Mbit/s Testing upload speed........................................................... Upload: 26.90 Mbit/s
So, we are severely limited by our uplink at home of course. Perhaps not as much by its upload speed, but maybe more but the huge round round-trip time.
Use iperf3 to see how fast we can go between our hosts:
$ iperf3 -fM -c hosted-vm.example.net [ ID] Interval Transfer Bitrate Retr Cwnd [ 5] 0.00-1.00 sec 4.37 MBytes 4.37 MBytes/sec 0 499 KBytes [ 5] 1.00-2.00 sec 3.63 MBytes 3.63 MBytes/sec 0 638 KBytes [ 5] 2.00-3.00 sec 2.50 MBytes 2.50 MBytes/sec 0 775 KBytes [ 5] 3.00-4.00 sec 2.50 MBytes 2.50 MBytes/sec 0 898 KBytes [ 5] 4.00-5.00 sec 2.50 MBytes 2.50 MBytes/sec 0 988 KBytes [ 5] 5.00-6.00 sec 0.00 Bytes 0.00 MBytes/sec 6 2.83 KBytes [ 5] 6.00-7.00 sec 0.00 Bytes 0.00 MBytes/sec 1 1.41 KBytes [ 5] 7.00-8.00 sec 0.00 Bytes 0.00 MBytes/sec 1 1.41 KBytes [ 5] 8.00-9.00 sec 0.00 Bytes 0.00 MBytes/sec 0 1.41 KBytes [ 5] 9.00-10.00 sec 0.00 Bytes 0.00 MBytes/sec 1 1.41 KBytes - - - - - - - - - - - - - - - - - - - - - - - - - [ ID] Interval Transfer Bitrate Retr [ 5] 0.00-10.00 sec 15.5 MBytes 1.55 MBytes/sec 9 sender [ 5] 0.00-10.06 sec 12.5 MBytes 1.24 MBytes/sec receiver
Sadly, this is repeatable: 5 transfers are completed just fine, then the transfer rate drops to zero and comes out at ~1.5 MB/s overall. Phew...
Let's try again over our VPN connection:
$ iperf3 -fM -c example.fastd [ ID] Interval Transfer Bitrate Retr Cwnd [ 5] 0.00-1.00 sec 801 KBytes 0.78 MBytes/sec 0 59.3 KBytes [ 5] 1.00-2.00 sec 1.61 MBytes 1.61 MBytes/sec 0 121 KBytes [ 5] 2.00-3.00 sec 3.22 MBytes 3.22 MBytes/sec 0 248 KBytes [ 5] 3.00-4.00 sec 571 KBytes 0.56 MBytes/sec 12 193 KBytes [ 5] 4.00-5.00 sec 0.00 Bytes 0.00 MBytes/sec 15 193 KBytes [ 5] 5.00-6.00 sec 0.00 Bytes 0.00 MBytes/sec 16 193 KBytes [ 5] 6.00-7.00 sec 0.00 Bytes 0.00 MBytes/sec 15 193 KBytes [ 5] 7.00-8.00 sec 1.98 MBytes 1.98 MBytes/sec 8 214 KBytes [ 5] 8.00-9.00 sec 2.79 MBytes 2.79 MBytes/sec 0 245 KBytes [ 5] 9.00-10.00 sec 571 KBytes 0.56 MBytes/sec 12 174 KBytes - - - - - - - - - - - - - - - - - - - - - - - - - [ ID] Interval Transfer Bitrate Retr [ 5] 0.00-10.00 sec 11.5 MBytes 1.15 MBytes/sec 78 sender [ 5] 0.00-10.06 sec 10.0 MBytes 0.99 MBytes/sec receiver
Again, the transfer rate drops to zero after 5 transfers, then picks up speed again, but we are still slightly slower than without the VPN. Maybe it's time to employ some kind of traffic shaping again :-\