An open-source way to connect a home or small office network to an internal one.
Introduction ๐
Whether it is a defense-in-depth1 security strategy or lack of IP space, there are many reasons for organizations to have internal networks. End-users typically connect to these networks either by using a virtual private network (VPN) client from the internet, or by connecting to an office’s network on site, which is typically “on the internal network” for convenience2.
There are a couple of well-known open-source VPN clients, be it WireGuard or OpenVPN, but to my knowledge there is no open source system which will connect client devices on a small office network to an internal network. The turn-key solutions that are known to me include only proprietary ones, such as Cisco’s Meraki box, which is quite expensive and sold with a subscription, and until recently did not even support IPv6.
Hence, in this post, we’ll look into how a small office network can be built, that automatically gives all devices on it access to the internal network.
Note: we’ll only cover IPv6.
Overview ๐
-
We have an internal network to which we can connect via WireGuard.
-
We have a home or small office network which we would also like to connect to the internal network as a whole, so that any client on it automatically gets access to the internal network.
-
Any client on the internal network should still have regular internet access.
The solution, as illustrated in the figure below, is based on adding a secondary router to the office network, which advertises routes to the internal network, and tunnels traffic to the internal network over WireGuard.
Prerequisites ๐
-
A private (ULA) IP prefix for the internal network. E.g.
fd00::/32
3. -
A private domain for the internal network, and a DNS server running on the internal network for said domain. E.g.
.internal
. -
A wireguard VPN endpoint which can expose the internal network.
-
A “main” router, which:
- acts as the default gateway (“connects to the internet”)
- can hand out DNS configuration to clients, via SLAAC with RDNSS, or DHCPv6, and is configurable.
This is typically a regular home/office router.
-
A server which will act as a secondary “tunnel” router. Running the following software:
- radvd
- dnsmasq
- wireguard
This can be anything from a raspberry pi to a standard rack-mounted server.
It is important that the two routers be on the same layer-2 (ethernet) network. This means that the server should be directly plugged into the main router’s ethernet port, or that there only be switches in between it and the main router (no other routers).
Setup ๐
As noted in the overview, the secondary router will be configured to advertise routes to the internal network and tunnel any traffic over wireguard.
This setup essentially consists of three independent parts:
-
the tunneling
-
the advertising of the router and configuration of client IP addresses
-
DNS resolution, so that only queries for services on the internal network will be routed over it.
Tunneling ๐
Configure wireguard to route all traffic to the internal network.
[Interface]
Address=fd00:0:0:1::1/64
PrivateKey = ...
ListenPort = 51822
PostUp=sysctl -w net.ipv6.conf.all.forwarding=1
# allow wireguard traffic to reach host
PostUp=iptables -A INPUT -p udp -m udp --dport 51822 -j ACCEPT
PostDown=iptables -D INPUT -p udp -m udp --dport 51822 -j ACCEPT
PostUp=ip6tables -A INPUT -p udp -m udp --dport 51822 -j ACCEPT
PostDown=ip6tables -D INPUT -p udp -m udp --dport 51822 -j ACCEPT
# allow packet forwarding from VPN interface (VPN is IPv6-only)
PostUp=ip6tables -A FORWARD -o %i -j ACCEPT
PostDown=ip6tables -D FORWARD -o %i -j ACCEPT
PostUp=ip6tables -A FORWARD -i %i -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
PostDown=ip6tables -A FORWARD -i %i -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
PostUp=ip6tables -A INPUT -i %i -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
PostDown=ip6tables -D INPUT -i %i -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
[Peer]
PublicKey = ...
AllowedIPs = fd00:0:1::/48
Endpoint=vpn.example.org:51822
In this example, we assume that
-
fd00:0:0::/48
is the local internal network, and specifically that the tunnel router will hand out addresses from fd00:0:0:1::/64 -
fd00:0:1::/48
is the remote internal network, which can be accessed overvpn.example.org:51822
Router advertising ๐
In IPv6, devices configure their network settings via a protocol called “Neighbor Discovery”, defined in RFC4861. Part of this protocol is the “router advertisement”, in which routers on the same network send messages to client devices which tell them about their existence and routing information. A neat feature in IPv6 is that a local network is not limited to having one router.
In a very simplified way, we’d like the following router advertisements to happen as follows, when a new device connects to the network (and periodically afterwards):
The tunnel router will run an instance of radvd to send router advertisements, and allow client devices to configure internal addresses.
interface eth0
{
AdvSendAdvert on;
# lifetime 0 => not a default router
AdvDefaultLifetime 0;
prefix fd00:0:0:1::/64 {};
route fd00:0::/32 {};
};
/etc/radvd.conf
Several things to note:
-
AdvDefaultLifetime 0;
is required to make sure that the tunnel router doesn’t clash with the main router. As defined in section 4.2 of RFC4861, Router Lifetime “A Lifetime of 0 indicates that the router is not a default router and SHOULD NOT appear on the default router list”. If this were not set, then client devices would receive conflicting information for which router to use as default route. -
The prefix must be a
/64
, and must be within the local internal network as configured by the wireguard tunnel. -
The route tells client devices that any packets to addressed beneath this prefix must be sent to the tunnel router.
-
Clients will continue getting IP addresses from the main router as well as the tunnel router. They will essentially have two different IP addresses on the same interface, and select the correct source address depending on the destination.
DNS ๐
While DNS is not strictly required to access the internal network, it can be very convenient, especially in an IPv6 network where addresses can be very long.
In this setup, we’ll configure the tunnel router to act as a proxy DNS server for the internal domain. All non-internal domains will be forwarded to a public DNS server. This way, we still maintain site-wide internet connectivity if the internal DNS server should go down for some reason.
dnsmasq is used as a proxy DNS server. The configuration is as follows:
# Configuration file for dnsmasq.
#
# Format is one option per line, legal options are the same
# as the long options legal on the command line. See
# "/usr/sbin/dnsmasq --help" or "man 8 dnsmasq" for details.
# Add other name servers here, with domain specs if they are for
# non-public domains.
server=/*.internal/fd00:0:1::1
# fallback to cloudflare
server=2606:4700:4700::1111
server=2606:4700:4700::1001
server=1.1.1.1
server=1.0.0.1
/etc/dnsmasq.d/internal.conf
A couple of things to note:
- this example will forward DNS queries to
*.internal
to a DNS server which is assumed to be running atfd00:0:1::1
- other DNS queries are forwarded to cloudflare’s resolvers
Finally, the last step to get DNS working is to tell clients to use the tunnel router’s proxy DNS. While we could include this in the router advertisement itself, there is no way to have per-domain DNS servers configured directly on clients. Hence, if the tunnel router and main router were to both advertise DNS configuration, it would lead to conflicts on the clients. Therefore, as a workaround, we only advertise DNS on the main router, but change it to point to the tunnel router.
Conclusion ๐
Putting together all these parts will lead to the desired outcome:
- clients will receive internal IP addresses
- internal domains will resolve to internal IP addresses
- internal traffic is forwarded to the tunnel router, which will then forward it to the rest of the internal network over wireguard.
Note: the dnsmasq docs lead me to believe that it is possible to use dnsmasq for the router advertisement as well, instead of radvd. This would simplify the setup, as it would remove the need for a separate service. However, I have not been able to come up with a working example.
-
Not treating the network as the sole security perimeter is good practice and a necessary step for a zero-trust architecture. ↩︎
-
In a high-security environment this may not be the case, but that is out of scope for this article. ↩︎
-
The ULA space is divided into
/48
blocks. However, in order to illustrate the networking setup we’ll split prefixes only on hextet-boundaries, so we’ll use a/32
. ↩︎