Introduction

This article is a supplement to the previous post: WireGuard: Cross-City LAN Communication. It focuses on how remote developers – those outside the office network – can access services within the office LAN across cities using WireGuard(R).

Scenario

The team embraces remote work, with developers located across the country. The company has an office in a certain city, where virtualization is self-hosted on the office LAN, running a series of development services (GitLab, CI, K8s).

  • Developers in different cities need to access multiple services within the office LAN.
  • Developers in different cities need to access each other.

Challenges

  • How can developers reliably access all devices on the office LAN across the public internet (as if they were on the office LAN).
  • The WireGuard node on the office LAN is behind an ISP NAT device, with no NAT traversal support and no public IP.
  • Only one WireGuard node runs on the office LAN (rather than adding all services to the WireGuard network).
  • How to solve the problem when the LAN gateway does not support static routing rules.
  • How to avoid routing conflicts.

Architecture

wireguard-remote-development-architecture

Approach

Assume there are two LANs: LAN1 and LAN2, representing the company’s office networks in Shenzhen (LAN1) and Beijing (LAN2).

There are three servers. z (the letter before the IP) is a public server (with a public IP), while a and c are private servers (without public IPs). The public server (z) and private servers (a and c) form a WireGuard network, and all three servers have IP forwarding enabled.

e and f are two remote developers, where e is in Guangzhou and f is in Shanghai. Each developer is a WireGuard client, joining the WireGuard network through their respective client configurations.

Developer Accessing Services

Assume developer e in Guangzhou (10.70.0.4) wants to access device d (192.168.200.244) on the Beijing office network (LAN2).

e connects to the WireGuard network through the WireGuard client. The client establishes a connection with the WireGuard Server and writes relevant routing rules to the system routing table (192.168.200.0/24 is handled by wg0).

e’s kernel matches 192.168.200.0/24 via the system routing table to wg0, and the kernel sends the packet to wg0. After receiving the packet, WireGuard matches the destination address against the internal routing table1, determines that the target Peer for 192.168.200.0/24 is z, and forwards the packet to z.

After the public server (z) receives the request, z’s kernel matches 192.168.200.0/24 via the system routing table to wg0, and the kernel sends the packet to wg0. After receiving the packet, WireGuard matches the destination against the internal routing table, determines that the target Peer for 192.168.200.0/24 is c, and forwards the packet to c.

After the private server (c) receives the request, c’s kernel finds via the system routing table that 192.168.200.244/32 is not itself (192.168.200.248) but device d on the LAN. Since IP forwarding is enabled, c forwards the packet to device d.

Device d receives the packet, processes it, and generates a response packet for e (10.70.0.4). Device d determines the next hop through the default gateway (192.168.200.1), which has a static routing rule configured (specifying that the next hop for 10.70.0.0/16 is c), so the default gateway sends the packet to c.

After the private server (c) receives the request, c’s kernel matches 10.70.0.0/16 via the system routing table to wg0, and the kernel sends the packet to wg0. After receiving the packet, WireGuard matches the destination against the internal routing table, determines that the target Peer for 10.70.0.0/16 is z, and forwards the packet to z.

After the public server (z) receives the request, z’s kernel matches 10.70.0.0/16 via the system routing table to wg0, and the kernel sends the packet to wg0. After receiving the packet, WireGuard matches the destination against the internal routing table, determines that the target Peer for 10.70.0.4/32 is e, and forwards the packet to e.

After device e receives the request, it finds the destination IP is itself, and e accepts and processes the packet from d.

Developer Accessing Developer

Assume developer e’s device in Guangzhou (10.70.0.4) wants to access developer f’s device in Shanghai (10.70.0.5).

Device e’s kernel matches 10.70.0.0/16 via the system routing table to interface wg0, and the kernel sends the packet to wg0. After receiving the packet, WireGuard matches the destination against the internal routing table, determines that the target Peer for 10.70.0.0/16 is z, and forwards the packet to z.

After the public server (z) receives the request, z’s kernel matches 10.70.0.0/16 via the system routing table to wg0, and the kernel sends the packet to wg0. After receiving the packet, WireGuard matches the destination against the internal routing table, determines that the target Peer for 10.70.0.5/32 is f, and forwards the packet to f.

After device f receives the request, it finds the destination IP is itself, and f accepts and processes the packet from e.

Operations

Deploying the WireGuard Network

Since e and f are developer devices (macOS/Windows), the configurations for e and f are wg-quick conf files, which can be imported through the WireGuard GUI client.

The command to generate key pairs is as follows (run the following command locally to create a private key, public key, and preshared key):

prikey="$(wg genkey)"; pubkey="$(wg pubkey <<< ${prikey})"; pskkey="$(wg genpsk)"
echo -e "PrivateKey: ${prikey}\nPublicKey: ${pubkey}\nPresharedKey: ${pskkey}"

Note that the public server only uses the public key and private key. The PresharedKey must match the client’s. For example, if client e’s PresharedKey is KK...GE=, then the PresharedKey in e’s corresponding Peer on the public server must also be KK...GE=.

Configuration for e:

[Interface]
PrivateKey = WMWozvjIgpA2h75juoku2btWxbJ54i4Yt0A0RhpW7V8=
ListenPort = 51820
Address = 10.70.0.4/16
MTU = 1280

[Peer]
PublicKey = CmmeC0yqofMMZhEGHuK5dd2Mxyxe7tA8wSniDWiI5V0=
PresharedKey = KKQGN01IkZ1kJD2fAxtDZ6k5VFAI2fMca2q+SV7OrGE=
Endpoint = 39.101.166.124:51820
AllowedIPs = 10.70.0.0/16
AllowedIPs = 192.168.200.0/24
AllowedIPs = 192.168.201.0/24
PersistentKeepalive = 15

Key information for e:

PrivateKey: WMWozvjIgpA2h75juoku2btWxbJ54i4Yt0A0RhpW7V8=
PublicKey: 0VooFEp8ZdpZp3P7BPa2W3piw+eu/6cyUGpSUWZp4HA=
PresharedKey: KKQGN01IkZ1kJD2fAxtDZ6k5VFAI2fMca2q+SV7OrGE=

Configuration for f:

[Interface]
PrivateKey = mOUA5x3hfa4D3zwC1HpivQYH2f/z6Ltdpm4YeEQSFXg=
ListenPort = 51820
Address = 10.70.0.5/16
MTU = 1280

[Peer]
PublicKey = CmmeC0yqofMMZhEGHuK5dd2Mxyxe7tA8wSniDWiI5V0=
PresharedKey = pXt2C8EeBPF+y7izRv40GwYpEtaF625PRlp9kcTSzIM=
Endpoint = 39.101.166.124:51820
AllowedIPs = 10.70.0.0/16
AllowedIPs = 192.168.200.0/24
AllowedIPs = 192.168.201.0/24
PersistentKeepalive = 15

Key information for f:

PrivateKey: mOUA5x3hfa4D3zwC1HpivQYH2f/z6Ltdpm4YeEQSFXg=
PublicKey: MyeHCD9sVXMXPgyEY79Yif8QskCjqm+SoJqmLXHN1RM=
PresharedKey: pXt2C8EeBPF+y7izRv40GwYpEtaF625PRlp9kcTSzIM=

Public server (z) configuration:

File: /etc/systemd/network/wg0.netdev (WireGuard internal routing table rules are specified via AllowedIPs in this file)

[NetDev]
Name=wg0
Kind=wireguard
MTUBytes=1280

[WireGuard]
ListenPort=51820
PrivateKey=qJwN1zu5alr89c6k18U4SUwZXnUgVhr/tvSgLM7rmHQ=

# LAN1 WireGuard Client
[WireGuardPeer]
PublicKey=u4vkZywF9hZcy5QUhcbOCjD2EZ7GL2yXgoAH+IoIbHE=
PresharedKey=IBAsO/DwbautQjq/Gksv3lFJZwnUXvkMR3GyxeelS4o=
AllowedIPs=10.70.0.2/32
AllowedIPs=192.168.201.0/24
PersistentKeepalive=15

# LAN2 WireGuard Client
[WireGuardPeer]
PublicKey=/d8UR69JrdjxC2Fo9hvfZaoSC35LSGNLJ5mqjEyWjRo=
PresharedKey=YdpxayZdKMeoHzo9fVvBSWoDTn1c++r5awhm28Is9Ic=
AllowedIPs=10.70.0.3/32
AllowedIPs=192.168.200.0/24
PersistentKeepalive=15

# Remote Developer e
[WireGuardPeer]
PublicKey=0VooFEp8ZdpZp3P7BPa2W3piw+eu/6cyUGpSUWZp4HA=
# PresharedKey on the public server must match the client's
PresharedKey=KKQGN01IkZ1kJD2fAxtDZ6k5VFAI2fMca2q+SV7OrGE=
AllowedIPs=10.70.0.4/32
PersistentKeepalive=15

# Remote Developer f
[WireGuardPeer]
PublicKey=MyeHCD9sVXMXPgyEY79Yif8QskCjqm+SoJqmLXHN1RM=
# PresharedKey on the public server must match the client's
PresharedKey=pXt2C8EeBPF+y7izRv40GwYpEtaF625PRlp9kcTSzIM=
AllowedIPs=10.70.0.5/32
PersistentKeepalive=15

File: /etc/systemd/network/wg0.network (system-level routing table rules are specified in this file)

[Match]
Name=wg0

[Network]
Address=10.70.0.1/16

[Route]
Destination=192.168.200.0/24

[Route]
Destination=192.168.201.0/24

After adding the configuration, run: systemctl restart systemd-networkd to restart systemd-networkd.

Key information for z:

PrivateKey: qJwN1zu5alr89c6k18U4SUwZXnUgVhr/tvSgLM7rmHQ=
PublicKey: CmmeC0yqofMMZhEGHuK5dd2Mxyxe7tA8wSniDWiI5V0=
PresharedKey: <not needed>

Private server (a) configuration:

File: /etc/systemd/network/wg0.netdev

[NetDev]
Name=wg0
Kind=wireguard
MTUBytes=1280

[WireGuard]
ListenPort=51820
PrivateKey=gNCkDUdQ3yBynzEST9XOd31NsPluF3Q7aeFu82QsV04=

[WireGuardPeer]
PublicKey=CmmeC0yqofMMZhEGHuK5dd2Mxyxe7tA8wSniDWiI5V0=
PresharedKey=IBAsO/DwbautQjq/Gksv3lFJZwnUXvkMR3GyxeelS4o=
Endpoint=39.101.166.124:51820
AllowedIPs=10.70.0.0/16
PersistentKeepalive=15

File: /etc/systemd/network/wg0.network

[Match]
Name=wg0

[Network]
Address=10.70.0.2/16

After adding the configuration, run: systemctl restart systemd-networkd to restart systemd-networkd.

Key information for a:

PrivateKey: gNCkDUdQ3yBynzEST9XOd31NsPluF3Q7aeFu82QsV04=
PublicKey: u4vkZywF9hZcy5QUhcbOCjD2EZ7GL2yXgoAH+IoIbHE=
PresharedKey: IBAsO/DwbautQjq/Gksv3lFJZwnUXvkMR3GyxeelS4o=

Private server (c) configuration:

File: /etc/systemd/network/wg0.netdev

[NetDev]
Name=wg0
Kind=wireguard
MTUBytes=1280

[WireGuard]
ListenPort=51820
PrivateKey=SF6RqNRo1c9vwFVteFonRwpbU1Ee1zsKp6v9c8pXwnA=

[WireGuardPeer]
PublicKey=CmmeC0yqofMMZhEGHuK5dd2Mxyxe7tA8wSniDWiI5V0=
PresharedKey=YdpxayZdKMeoHzo9fVvBSWoDTn1c++r5awhm28Is9Ic=
Endpoint=39.101.166.124:51820
AllowedIPs=10.70.0.0/16
PersistentKeepalive=15

File: /etc/systemd/network/wg0.network

[Match]
Name=wg0

[Network]
Address=10.70.0.3/16

After adding the configuration, run: systemctl restart systemd-networkd to restart systemd-networkd.

Key information for c:

PrivateKey: SF6RqNRo1c9vwFVteFonRwpbU1Ee1zsKp6v9c8pXwnA=
PublicKey: /d8UR69JrdjxC2Fo9hvfZaoSC35LSGNLJ5mqjEyWjRo=
PresharedKey: YdpxayZdKMeoHzo9fVvBSWoDTn1c++r5awhm28Is9Ic=

IP Forwarding

To achieve LAN-level communication, we need to enable IP forwarding on the three servers a/c/z (WireGuard nodes). Run the following command on each server:

# The leading number in the sysctl config filename affects priority
# To verify the configuration is effective, run: systemd-analyze cat-config sysctl.d
echo -e 'net.ipv4.ip_forward = 1' > /etc/sysctl.d/10-ip-forward.conf
sysctl -p /etc/sysctl.d/10-ip-forward.conf

Adding Static Routing Rules

To allow e to access d, we need to add the following static routing rule on the default gateway of d’s LAN (LAN2). (My test environment’s gateway does not support static routing rules, so I added the routing rule directly on d):

ip route add 10.70.0.0/16 via 192.168.200.248

Similarly, add the following static routing rule on LAN1’s default gateway:

ip route add 10.70.0.0/16 via 192.168.201.247

Now e and f can communicate with all devices in LAN1/LAN2.

Testing

d’s IP address: 192.168.200.244. Test from e and f whether they can access d:

ping 192.168.200.244

Result:

Security Hardening

Since connecting to the WireGuard network grants access to all devices in 192.168.200.0/24 (192.168.201.0/24), this poses a risk to the LAN. We can add firewall rules on the private servers (a and c) to restrict the public server from initiating access to the LAN. (Choose one of the following two approaches.)

After adding these firewall rules, the WireGuard Server node (and other nodes not on the allowlist) will be prevented from initiating access to the LAN.

iptables Security Hardening

# Only allow 10.70.0.3 and 10.70.0.4 to access the LAN. You can add more WireGuard Client VPN IPs as needed.
iptables -t filter -I FORWARD 1 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -t filter -I FORWARD 2 -s 10.70.0.3/32 -m conntrack --ctstate NEW -j ACCEPT
iptables -t filter -I FORWARD 2 -s 10.70.0.4/32 -m conntrack --ctstate NEW -j ACCEPT
iptables -t filter -I FORWARD 10 -j DROP

firewalld Security Hardening

Save to: /etc/firewalld/direct.xml, then run: firewall-cmd --reload.

<?xml version="1.0" encoding="utf-8"?>
<direct>
  <rule ipv="ipv4" table="filter" chain="FORWARD" priority="1">-m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT</rule>
  <rule ipv="ipv4" table="filter" chain="FORWARD" priority="2">-s 10.70.0.3/32 -m conntrack --ctstate NEW -j ACCEPT</rule>
  <rule ipv="ipv4" table="filter" chain="FORWARD" priority="2">-s 10.70.0.4/32 -m conntrack --ctstate NEW -j ACCEPT</rule>
  <rule ipv="ipv4" table="filter" chain="FORWARD" priority="10">-j DROP</rule>
</direct>

FAQ

Gateway Does Not Support Static Routing Rules

When the LAN gateway does not support static routing rules, you can enable MASQUERADE on the LAN WireGuard node (a and c in the architecture diagram) to modify the source address of packets during forwarding, achieving the same effect as static routing rules. (Choose one of the following two approaches.)

The following configuration uses the private server (c) in LAN2 as an example. You need to adjust it according to your actual setup (e.g., LAN subnet, physical NIC name, etc.).

Configuring MASQUERADE with iptables

iptables -t nat -A POSTROUTING -s 10.70.0.0/16 -d 192.168.200.0/24 -o eth2 -j MASQUERADE

Configuring MASQUERADE with firewalld

Enable the Forward feature for both the public Zone and the wireguard Zone, then place the physical NIC (eth2) and wg0 in the same Zone (let’s call it wireguard):

wireguard (active)
  ...
  interfaces: eth2 wg0
  forward: yes
  masquerade: no
  ...

Add a rich rule in the public Zone:

public (default, active)
  ...
  sources: 192.168.200.0/24
  services: dhcpv6-client ssh wireguard
  forward: yes
  masquerade: no
  ...
  rich rules:
        rule family="ipv4" source address="10.66.0.0/16" destination address="192.168.200.0/24" masquerade

This achieves MASQUERADE through firewalld.

Testing MASQUERADE

Test on remote developer e’s device to verify the WireGuard network is working. Ensure e can access the public server z and private server c via VPN IPs:

# Access public server z's VPN IP:
ping 10.70.0.1 -c 2
# PING 10.70.0.1 (10.70.0.1) 56(84) bytes of data.
# 64 bytes from 10.70.0.1: icmp_seq=1 ttl=64 time=48.9 ms
# 64 bytes from 10.70.0.1: icmp_seq=2 ttl=64 time=48.2 ms

# Access private server c's VPN IP:
ping 10.70.0.3 -c 2
# PING 10.70.0.3 (10.70.0.3) 56(84) bytes of data.
# 64 bytes from 10.70.0.3: icmp_seq=1 ttl=63 time=96.8 ms
# 64 bytes from 10.70.0.3: icmp_seq=2 ttl=63 time=96.4 ms

Verify that the private server c has MASQUERADE enabled (assuming MASQUERADE was configured via iptables):

iptables-save -t nat
# *nat
# :PREROUTING ACCEPT [1189:501372]
# :INPUT ACCEPT [5:1627]
# :OUTPUT ACCEPT [4:732]
# :POSTROUTING ACCEPT [5:816]
# -A POSTROUTING -s 10.70.0.0/16 -d 192.168.200.0/24 -o eth2 -j MASQUERADE
# COMMIT

While running the test command on e, capture packets on d:

# Test on e:
ping 192.168.200.244 -c 5
# PING 192.168.200.244 (192.168.200.244) 56(84) bytes of data.
# 64 bytes from 192.168.200.244: icmp_seq=1 ttl=62 time=98.3 ms
# 64 bytes from 192.168.200.244: icmp_seq=2 ttl=62 time=97.1 ms
# Capture packets on d:
tcpdump -i any -n icmp
# 04:38:02.622391 eth2  In  IP 192.168.200.248 > 192.168.200.244: ICMP echo request, id 16, seq 1, length 64
# 04:38:02.622428 eth2  Out IP 192.168.200.244 > 192.168.200.248: ICMP echo reply, id 16, seq 1, length 64
# 04:38:03.621300 eth2  In  IP 192.168.200.248 > 192.168.200.244: ICMP echo request, id 16, seq 2, length 64
# 04:38:03.621325 eth2  Out IP 192.168.200.244 > 192.168.200.248: ICMP echo reply, id 16, seq 2, length 64

From the packets captured on d, we can see that the source address is the private server c’s LAN IP (192.168.200.248) rather than remote developer e’s VPN IP (10.70.0.4). This confirms that MASQUERADE is working.

Routing Conflicts

Routing conflicts occur when a remote developer arrives at the office. At that point, the remote developer’s device will have duplicate routing rules, causing LAN traffic to be routed through the public internet.

First, let’s simulate the scenario where remote developer e accesses office device d via WireGuard. Here we test the link speed with iperf3:

# On e:
iperf3 -c 192.168.200.244 -i 5 -n 1G
# [  5] local 10.70.0.4 port 46172 connected to 192.168.200.244 port 5201
# [ ID] Interval           Transfer     Bitrate         Retr  Cwnd
# [  5]   0.00-5.01   sec  21.0 MBytes  35.2 Mbits/sec  7561    333 KBytes
# [  5]   5.01-10.01  sec  15.4 MBytes  25.8 Mbits/sec   17    277 KBytes
# [  5]  10.01-15.00  sec  13.8 MBytes  23.1 Mbits/sec    0    319 KBytes
# [  5]  15.00-20.00  sec  19.1 MBytes  32.1 Mbits/sec  229    289 KBytes
# [  5]  20.00-25.00  sec  15.2 MBytes  25.6 Mbits/sec    0    323 KBytes
# [  5]  25.00-30.01  sec  15.5 MBytes  26.0 Mbits/sec    9    323 KBytes
# On d:
iperf3 -s -i 5
# Accepted connection from 192.168.200.248, port 46160
# [  5] local 192.168.200.244 port 5201 connected to 192.168.200.248 port 46172
# [ ID] Interval           Transfer     Bitrate
# [  5]   0.00-5.01   sec  18.4 MBytes  30.8 Mbits/sec
# [  5]   5.01-10.01  sec  14.2 MBytes  23.9 Mbits/sec
# [  5]  10.01-15.01  sec  14.6 MBytes  24.5 Mbits/sec
# [  5]  15.01-20.01  sec  18.5 MBytes  31.0 Mbits/sec
# [  5]  20.01-25.01  sec  16.0 MBytes  26.8 Mbits/sec
# [  5]  25.01-30.01  sec  15.1 MBytes  25.4 Mbits/sec

We can see that the WireGuard network throughput in my test environment is about 25 Mbits/sec.

Now let’s simulate the scenario where the remote developer arrives at the office. At this point, e connects to the office network and receives a 192.168.200.0/24 IP via DHCP:

# On e:
ip a s wlan0
# wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
#   link/ether 52:54:00:0d:b4:90 brd ff:ff:ff:ff:ff:ff
#   altname enp10s0
#   inet 192.168.200.27/24 metric 1024 brd 192.168.200.255 scope global dynamic wlan0
#      valid_lft 3590sec preferred_lft 3590sec

A routing conflict occurs. The routing table has two 192.168.200.0/24 routing rules:

ip r | grep 200
# 192.168.200.0/24 dev wg0 scope link
# 192.168.200.0/24 dev eth2 proto kernel scope link src 192.168.200.27 metric 1024
# 192.168.200.1 dev eth2 proto dhcp scope link src 192.168.200.27 metric 1024

Running iperf3 to test d on the same LAN, due to the routing conflict, 192.168.200.0/24 LAN traffic is forwarded through the public internet, yielding only 25 Mbits/sec:

# On e:
iperf3 -c 192.168.200.244 -i 5 -n 1G
# Connecting to host 192.168.200.244, port 5201
# [  5] local 10.70.0.4 port 43220 connected to 192.168.200.244 port 5201
# [ ID] Interval           Transfer     Bitrate         Retr  Cwnd
# [  5]   0.00-5.01   sec  22.8 MBytes  38.1 Mbits/sec  6558    329 KBytes
# [  5]   5.01-10.01  sec  13.6 MBytes  22.9 Mbits/sec   10    279 KBytes
# [  5]  10.01-15.00  sec  13.9 MBytes  23.3 Mbits/sec    0    342 KBytes
# [  5]  15.00-20.01  sec  18.1 MBytes  30.4 Mbits/sec  231    302 KBytes
# [  5]  20.01-25.01  sec  16.4 MBytes  27.5 Mbits/sec    0    323 KBytes
# [  5]  25.01-30.01  sec  15.2 MBytes  25.6 Mbits/sec   10    324 KBytes
# On d:
iperf3 -s -i 5
# Accepted connection from 192.168.200.248, port 46160
# [  5] local 192.168.200.244 port 5201 connected to 192.168.200.248 port 43220
# [ ID] Interval           Transfer     Bitrate
# [  5]   0.00-5.00   sec  18.9 MBytes  31.7 Mbits/sec
# [  5]   5.00-10.01  sec  13.8 MBytes  23.0 Mbits/sec
# [  5]  10.01-15.01  sec  14.9 MBytes  25.0 Mbits/sec
# [  5]  15.01-20.00  sec  18.2 MBytes  30.6 Mbits/sec
# [  5]  20.00-25.01  sec  16.2 MBytes  27.3 Mbits/sec
# [  5]  25.01-30.01  sec  14.9 MBytes  25.0 Mbits/sec

Resolving Routing Conflicts

The routing table is implemented as a binary trie. We can add a routing rule with lower priority as a parent node of the target routing rule (in binary trie terms), thereby lowering the priority of the WireGuard-added routing rule.

wireguard-remote-development-trie.png

The WireGuard GUI client adds corresponding system-level routing rules based on the AllowedIPs configuration. Here we simply change 192.168.200.0/24 to 192.168.0.0/16 in remote developer e’s client configuration.

--- /etc/wireguard/wg0.conf.old 2024-01-22 13:30:54.793166541 +0000
+++ /etc/wireguard/wg0.conf     2024-01-28 05:15:34.232343033 +0000
@@ -9,6 +9,5 @@
 PresharedKey = KKQGN01IkZ1kJD2fAxtDZ6k5VFAI2fMca2q+SV7OrGE=
 Endpoint = 39.101.166.124:51820
 AllowedIPs = 10.70.0.0/16
-AllowedIPs = 192.168.201.0/24
-AllowedIPs = 192.168.200.0/24
+AllowedIPs = 192.168.0.0/16
 PersistentKeepalive = 15

Then we re-test with iperf3:

# On e:
iperf3 -c 192.168.200.244 -i 5 -n 1G
# Connecting to host 192.168.200.244, port 5201
# [  5] local 10.70.0.4 port 43220 connected to 192.168.200.244 port 5201
# [ ID] Interval           Transfer     Bitrate
# [  5]   0.00-5.01   sec  8.39 GBytes  14.4 Gbits/sec    0   2.49 MBytes
# [  5]   5.01-10.01  sec  8.11 GBytes  13.9 Gbits/sec    0   2.62 MBytes
# [  5]  10.01-15.00  sec  8.34 GBytes  14.3 Gbits/sec    0   2.75 MBytes
# [  5]  15.00-20.01  sec  8.24 GBytes  14.1 Gbits/sec    0   2.75 MBytes
# [  5]  20.01-25.01  sec  7.99 GBytes  13.7 Gbits/sec    0   4.28 MBytes
# [  5]  25.01-30.00  sec  7.63 GBytes  13.1 Gbits/sec    0   4.28 MBytes
# On d:
iperf3 -s -i 5
# Accepted connection from 192.168.200.248, port 46160
# [  5] local 192.168.200.244 port 5201 connected to 192.168.200.248 port 43220
# [ ID] Interval           Transfer     Bitrate
# [  5]   0.00-5.00   sec  8.39 GBytes  14.4 Gbits/sec
# [  5]   5.00-10.00  sec  8.12 GBytes  14.0 Gbits/sec
# [  5]  10.00-15.00  sec  8.36 GBytes  14.4 Gbits/sec
# [  5]  15.00-20.00  sec  8.25 GBytes  14.2 Gbits/sec
# [  5]  20.00-25.00  sec  7.99 GBytes  13.7 Gbits/sec
# [  5]  25.00-30.00  sec  7.73 GBytes  13.3 Gbits/sec

After resolving the conflict, the LAN speed reached 14 Gbits/sec.

This approach works on Linux/macOS/iOS. It is unknown on Android (no Android device available for testing) and does not work on Windows (Windows routing table is not purely based on a binary trie; routing policy is also affected by interface priority and routing rule priority).

Why systemd Instead of /etc/wireguard/

WireGuard is actually part of the kernel (a kernel module). Linux servers can configure WireGuard networking directly through systemd-networkd without installing the wireguard-tools package (which is just a management utility and does not contain core logic).

All my servers’ network configurations are managed through systemd-networkd, so I manage WireGuard configuration through systemd-networkd as well. This is meaningful for minimizing changes and improving operational consistency across the cluster.

Configurations added to /etc/wireguard/* are actually another approach to managing WireGuard configuration provided by wireguard-tools. It essentially uses Bash scripts to invoke corresponding command-line tools like ip and wg to manage system routes and WireGuard configuration.

The wireguard-tools package provides the wg command-line tool, similar to the ip tool (in the future, wg’s functionality may be merged directly into ip), allowing users to view and manage WireGuard devices from the command line.

The wg command is also used to create key pairs. Key pairs are essentially a pair of 32-byte arrays. We can create key pairs using the wg command in a non-production environment, prepare the WireGuard configuration, and then configure WireGuard devices through systemd-networkd. Installing the wireguard-tools package on production servers is not necessary.

Auto-Start on Boot

My server cluster primarily runs Arch Linux. If your network configuration is not managed through systemd-networkd (e.g., Debian and Ubuntu use netplan), you may need to start and enable systemd-networkd on boot:

systemctl enable --now systemd-networkd

Summary

LAN interconnection solves the problem of how users in two office networks can access each other. This article supplements that scenario by addressing how developers outside the office network can access any office network device.

References


  1. WireGuard internal routing table. WireGuard has its own routing table implementation (distinct from the Linux system routing table), based on a binary trie. Nodes in the trie are specified via the AllowedIPs configuration, with tree nodes mapped to Peers. ↩︎