[2018.06.06]
Like many Linux nerds, I host a bunch of personal services, both at home and on my favorite ISP’s remote server.
Problem you may have with services hosted at home is accessing them remotely.
In Switzerland, public static IP addresses for private customers are expensive enough not to be a very appealing solution. Dynamic DNS is a possible alternative but I personally find it unsatisfactory since: 1. not being 100% reliable; 2. depending on some 3rd-party’s service (e.g. DynDNS).
On the other hand, my favorite ISP’s remote server does have both IPv4 and IPv6 public static addresses readily available.
By using a VPN and some clever routing, one can use those remote public static IP addresses to access home services reliably and efficiently.
From the perspective of an incoming request packet, the scenario is nothing out of the ordinary:
[1] a packet’s original destination IP address is the remote server’s public IP address (IPv4 or IPv6)
[2] Destination Network Address Translation (DNAT) is performed on the remote server, replacing the destination IP address by the home server’s (internal) IP address
[3] which is then routed to a remotely hosted VPN server
[4] which tunnels it further to its home VPN client counterpart
[5] from where the packet can finally reach the home service
The catch occurs when the response packet must be routed back to the source IP address! In order to reach that source and not being dropped as a martian, this packet MUST be routed via the remote server. But how to do so when the home default gateway is readily available to route that packet directly (but erroneously) to the source?
One could performed Source Network Address Translation (SNAT) on the incoming request packet at the same time DNAT is performed on the remote server. But that would have the major disadvantage of hiding the true source IP address to the home server.
This is where tagged routing comes into the picture:
[4bis] before leaving the home VPN client, tag the incoming request packet, using the ToS/DSCP IP header field
[6] when leaving the home service, make sure to tag the response packet identically
[7] and use this tag to identify the reponse packet and use an alternate routing table - via the home VPN client and the remote VPN server - for its way back to the remote server and, eventually, the source IP
To make things simpler, we’ll stick to IPv4 addresses. But everything that is said below can be translated to IPv6 straight-forwardly.
The remote network segment is made of:
a class C 192.168.200.0/24 private network, along
a public 192.0.2.200/32 IP address
a public server/router at 192.0.2.200 (on eth0) / 192.168.200.1 (on br0)
a virtual VPN server at 192.168.200.2 (bridged on br0)
While the home network segment is made of:
a class C 192.168.100.0/24 private network
a private server/router at 192.168.100.1 (on br0)
a virtual VPN client at 192.168.100.2 (bridged on br0)
a virtual web service at 192.168.100.3 (bridged on br0), on port TCP/443 (HTTPS)
The first step is to perform Destination Network Address Translation (DNAT) for packets reaching the public remote server:
# DNAT
# ... VPN server
iptables -t nat -A PREROUTING -i eth0 -d 192.0.2.200 -p udp --dport 1194 -j DNAT --to 192.168.200.2
# ... home service
iptables -t nat -A PREROUTING -i eth0 -d 192.0.2.200 -p tcp --dport 443 -j DNAT --to 192.168.100.3
# Firewall ACL
# ... VPN server
iptables -t filter -A FORWARD -i eth0 -o br0 -d 192.168.200.2 -p udp --dport 1194 -j ACCEPT
# ... home service
iptables -t filter -A FORWARD -i eth0 -o br0 -d 192.168.100.3 -p tcp --dport 443 -j ACCEPT
We must then provide a route for the DNAT-ed packet to reach its home destination:
#!/bin/sh
# Route home network via the VPN server
ip -4 route show | fgrep -q '192.168.100.0/24' || ip -4 route add 192.168.100.0/24 via 192.168.200.2 dev br0
# Enable routing
net.ipv4.ip_forward = 1
Nothing here but a very standard OpenVPN server setup.
## OpenVPN Server Settings
# Enable/disable with 'systemctl {enable|disable} openvpn@server.vpn.example.net.service'
# Start/stop with 'service openvpn {start|stop}'
# Server
mode server
dev tun0
proto udp
port 1194
# TLS
tls-server
# ... PKI (*.vpn.example.net)
ca /etc/openvpn/pki/ca.vpn.example.net-cert.pem
cert /etc/openvpn/pki/server.vpn.example.net-cert.pem
key /etc/openvpn/pki/server.vpn.example.net-key.pem
# Encryption
dh /etc/openvpn/server.vpn.example.net-dh.pem
tls-auth /etc/openvpn/server.vpn.example.net-auth.key
cipher AES-256-CBC
auth SHA256
reneg-sec 43200
# Connection
keepalive 5 30
comp-lzo yes
persist-key
persist-tun
# IPv4
ifconfig 10.0.0.1 10.0.0.2
push "route 192.168.200.0 255.255.255.0"
# Clients
client-config-dir /etc/openvpn/client/
# ... home network
route 192.168.100.0 255.255.255.0
# Logging
verb 3
status /var/log/openvpn-server-status.log
## OpenVPN Server Client-specific Settings
# home.vpn.example.net
# IPv4
ifconfig-push 10.0.0.4 10.0.0.3
iroute 192.168.100.0 255.255.255.0
# Firewall ACL
iptables -t filter -A FORWARD -i tun0 -o eth0 -j ACCEPT
iptables -t filter -A FORWARD -i eth0 -o tun0 -j ACCEPT
# Enable routing
net.ipv4.ip_forward = 1
This is where the first elements of the tagged routing are set in place:
a separate/dedicated routing table (42)
which default gateway is the VPN tunnel (to the remote VPN server)
along matching firewall (conn)marks (42)
## OpenVPN Server Settings
# Enable/disable with 'systemctl {enable|disable} openvpn@home.vpn.example.net.service'
# Start/stop with 'service openvpn {start|stop}'
# Client
client
nobind
dev tun
proto udp
remote 192.0.2.200 1194
tls-auth /etc/openvpn/server.vpn.example.net-auth.key
explicit-exit-notify
# TLS
remote-cert-tls server
# ... PKI (*.vpn.example.net)
ca /etc/openvpn/pki/ca.vpn.example.net-cert.pem
cert /etc/openvpn/pki/home.vpn.example.net-cert.pem
key /etc/openvpn/pki/home.vpn.example.net-key.pem
# Encryption
cipher AES-256-CBC
auth SHA256
reneg-sec 43200
# Connection
comp-lzo yes
persist-key
persist-tun
route-delay 2
# Additional setup
script-security 2
up /etc/openvpn/scripts/server.vpn.example.net-up.sh
down /etc/openvpn/scripts/server.vpn.example.net-down.sh
# Logging
verb 3
status /var/log/openvpn-client-status.log
#!/bin/sh
# Tagged routing
ip -4 route add default dev tun0 table 42
ip -4 rule add fwmark 42 table 42
# Allow "martians" packets from VPN tunnel
echo 0 > /proc/sys/net/ipv4/conf/tun0/rp_filter
# Done
exit 0
#!/bin/sh
# Tagged routing
ip -4 route flush table 42
ip -4 rule del fwmark 42 table 42
# Done
exit 0
# Packets tagging (using the ToS/DSCP IP header field)
# ... remote-originated (set tag)
iptables -t mangle -A PREROUTING -i tun0 -j DSCP --set-dscp 42
# ... return-path (use and clear tag)
iptables -t mangle -A PREROUTING -i eth0 -m dscp --dscp 42 -j MARK --set-mark 42
iptables -t mangle -A POSTROUTING -o tun0 -j DSCP --set-dscp 0
# Firewall ACL
# ... home service
iptables -A FORWARD -i tun0 -o eth0 -d 192.168.100.3 -p tcp --dport 443 -j ACCEPT
# Enable routing
net.ipv4.ip_forward = 1
This is where tagged routing is finalized:
a separate/dedicated routing table (42)
which default gateway is the VPN client
along matching firewall (conn)marks (42)
#!/bin/sh
# Tagged routing (via the VPN client)
case "${ADDRFAM}-${IFACE}" in
'inet-eth0')
ip -4 route flush table 42
ip -4 rule del fwmark 42 table 42 2>/dev/null
ip -4 route add default via 192.168.100.2 table 42
ip -4 rule add fwmark 42 table 42
;;
esac
#!/bin/sh
# Track tagged packets (tag responses identically to requests)
iptables -t mangle -A INPUT -m dscp --dscp 42 -j CONNMARK --set-mark 42
iptables -t mangle -A OUTPUT -j CONNMARK --restore-mark
iptables -t mangle -A OUTPUT -m connmark --mark 42 -j DSCP --set-dscp 42
The home service can now be seamlessly accessed via the remote server’s public IP address.
What is also very neat with this setup is that the home network can even be roaming while maintaining connectivity, thanks to the resiliency of OpenVPN.
Enjoy!