Site-to-Internal Network

ยท 1504 words ยท 8 minute read

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.

A secondary router on the network advertises and tunnels internal traffic.

A secondary router on the network advertises and tunnels internal traffic.

Prerequisites ๐Ÿ”—

  • A private (ULA) IP prefix for the internal network. E.g. fd00::/323.

  • 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 over vpn.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):

sequenceDiagram participant main router participant tunnel router participant device main router->>device: Hello device, I am a router. Use me to connect to the internet. tunnel router->>device: Hello device, I am a router. Use me to connect to the internal network.

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.

sequenceDiagram participant main router participant tunnel router participant device main router->>device: use address of tunnel router for DNS device->>tunnel router: please resolve `example.org` tunnel router->>device: <public internet IP> device->>tunnel router: please resolve `service.internal` tunnel router->>device: fd00:0:1::1234

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 at fd00: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.

The main router is configured to instruct clients to use the tunnel router as a DNS server.

The main router is configured to instruct clients to use the tunnel router as a DNS server.

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.


  1. Not treating the network as the sole security perimeter is good practice and a necessary step for a zero-trust architecture. ↩︎

  2. In a high-security environment this may not be the case, but that is out of scope for this article. ↩︎

  3. 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↩︎

comments powered by Disqus