FreeBSD: Home NAS, part 3 – WireGuard VPN, Linux peer, and routing

By | 12/25/2025
 

I am continuing to set up my home server on FreeBSD 14.3, which is intended to serve as a NAS.

In the previous post, FreeBSD: introduction to Packet Filter (PF) firewall, we got acquainted with firewalls; the next step is to configure a VPN for access.

The main idea is to (finally!) connect my “office” and my apartment, and later, perhaps, also connect the server where rtfm.co.ua is currently running so that blog files and database backups can be stored directly on the ZFS mirror pool of the home server.

WireGuard vs OpenVPN

When it came to choosing which specific VPN server to use, I initially thought about OpenVPN – since I’ve worked with it for years, and there are even some blog posts about it on RTFM.

However, after giving it some thought, I decided that for a home VPN, solutions like OpenVPN or Pritunl would be a bit of overkill, and I could give WireGuard a try.

The systems are very different, but in short:

  • WireGuard has a much smaller codebase – for example, the Linux implementation is about 4,000 lines in the kernel, while OpenVPN is about 100,000 lines in user space
  • WireGuard works as a kernel module – packet processing and cryptography are performed directly in kernel space, whereas OpenVPN is a user space service that operates through a TCP or UDP socket and interacts with the kernel via the standard kernel network stack
  • The same applies to encryption, as WireGuard has built-in cryptography that is part of the protocol itself and runs in kernel space, while OpenVPN uses the standard SSL/TLS stack (OpenSSL, LibreSSL, etc.) in user space, which adds complexity and CPU/RAM overhead
  • WireGuard’s operational model is peer-to-peer – meaning the protocol has no built-in “server” or “client” roles, only Peers with keys and allowed IPs, whereas OpenVPN is built around a classic client-server architecture

As a result, WireGuard can be perceived not as a separate service, but as an encrypted network interface, while OpenVPN remains a classic application-based VPN service.

Even the official WireGuard whitepaper is titled “Next Generation Kernel Network Tunnel“.

Network Architecture

So, here is what I have:

  • “office”: a separate local network 192.168.0.0/24, with a TP-LINK Archer AX12 router at the entry
    • this network contains a work laptop with Arch Linux and a Lenovo ThinkCentre with FreeBSD
    • the FreeBSD machine will host the NAS, NFS, and WireGuard itself
      • although the Archer AX12 has its own built-in OpenVPN and WireGuard – I want to do it myself, manually, for more control
  • home: a 192.168.100.0/24 network with the exact same Archer AX12 router
    • the only client there is a home laptop with Arch Linux

And here is what I want to achieve:

  • FreeBSD will act as the WireGuard VPN server
  • The Archer AX12 router will have NAT port-forwarding to connect to WireGuard on FreeBSD
  • VPN network – 10.8.0.1/24
  • Packet Filter firewall on FreeBSD to control traffic
  • Both laptops should have access to each other and to the future NAS on FreeBSD

Here is how it looks schematically:

Running WireGuard on FreeBSD

In FreeBSD (just like in Linux), WireGuard consists of a kernel module + userspace tools: the main “working” part is loaded as a kernel module, and a separate package is installed to interact with it.

Install wireguard-tools:

root@setevoy-nas:/home/setevoy # pkg install wireguard-tools

Load the module:

root@setevoy-nas:/home/setevoy # kldload if_wg

Verify:

root@setevoy-nas:/home/setevoy # kldstat | grep wg
 8    1 0xffffffff82a47000    2f5c0 if_wg.ko

Enable WireGuard in /etc/rc.conf:

root@setevoy-nas:/home/setevoy # sysrc wireguard_enable=YES
wireguard_enable:  -> YES
root@setevoy-nas:/home/setevoy # sysrc wireguard_interfaces=wg0
wireguard_interfaces:  -> wg0

Don’t start it yet – let’s move on to network configuration.

Network configuration

Next, the system needs to be configured to route packets between the physical interface and the WireGuard interface, and the firewall config needs an update.

IP forwarding configuration

Enable IP forwarding from the wg0 interface (which doesn’t exist yet, it will appear when WireGuard starts) to the LAN interface, em0.

Update the autostart in /etc/rc.conf:

root@setevoy-nas:/usr/local/etc/wireguard # sysrc gateway_enable="YES"
gateway_enable: NO -> YES

To enable forwarding immediately without a reboot – turn it on using sysctl:

root@setevoy-nas:/usr/local/etc/wireguard # sysctl net.inet.ip.forwarding=1
net.inet.ip.forwarding: 0 -> 1

Verify:

root@setevoy-nas:/usr/local/etc/wireguard # sysctl net.inet.ip.forwarding
net.inet.ip.forwarding: 1

The next step is configuring Packet Filter.

Packet Filter Configuration

So, here is what we have:

  • VPN network: 10.8.0.0/24
  • Office network where FreeBSD/VPN is located: 192.168.0.0/24
    • FreeBSD LAN IP: 192.168.0.2
  • Routing internet through VPN is not required – only traffic between the home and office networks

The current pf config is minimalist, from the previous post:

allowed_tcp_ports = "{ 22 }"

allowed_clients = "{ 192.168.0.0/24, 192.168.1.0/24 }"

set skip on lo

block all

# allow ssh only from specific hosts
pass in proto tcp from $allowed_clients to any port $allowed_tcp_ports keep state

# allow all outgoing traffic
pass out all keep state

What needs to be added:

  • allow inbound UDP connections to the WireGuard port (51820) for the handshake
  • allow traffic from the VPN network 10.8.0.0/24 to the FreeBSD host itself (ping, SSH)
  • allow transit traffic from the VPN network 10.8.0.0/24 to the local office and home networks (192.168.0.0/24 and 192.168.100.0/24)
  • allow ICMP and SSH from the VPN network and the home network to the FreeBSD host
  • allow outbound traffic from FreeBSD

I’ve added macros to the config, but while writing and testing – I specify all ports and addresses explicitly in the config for better readability.

Now /etc/pf.conf will look like this:

##################
### Interfaces ###
##################
# lan_if = "em0"
# wg_if  = "wg0"

################
### Networks ###
################
# lan_net      = "192.168.0.0/24"
# home_net     = "192.168.100.0/24"
# wg_net       = "10.8.0.0/24"
# vpn_nets     = "{ 10.8.0.0/24, 192.168.100.0/24 }"

################
### Services ###
################
# ssh_ports = "{ 22 }"
# wg_port   = "51820"

######################
### Basic settings ###
######################

# do not filter loopback traffic
set skip on lo

######################
### Default policy ###
######################

# block everything by default
block all

#######################
### Inbound traffic ###
#######################

### SSH

# allow SSH from Office LAN (192.168.0.0/24) to FreeBSD host
pass in log on em0 proto tcp from 192.168.0.0/24 to (em0) port 22 keep state

# allow SSH from Home network (192.168.100.0/24) to FreeBSD host
pass in log on em0 proto tcp from 192.168.100.0/24 to (em0) port 22 keep state

# allow SSH from VPN clients to FreeBSD host
pass in on wg0 proto tcp from 10.8.0.0/24 to (wg0) port 22 keep state

### VPN 

# allow WireGuard handshake (UDP/51820) on LAN interface
pass in on em0 proto udp to (em0) port 51820 keep state

# allow VPN clients (10.8.0.0/24) to access FreeBSD host itself
# this allows ping, ssh, etc. to the wg0 address
pass in on wg0 from 10.8.0.0/24 to (wg0) keep state

# allow VPN clients to access Office LAN (192.168.0.0/24)
pass in on wg0 from 10.8.0.0/24 to 192.168.0.0/24 keep state

# allow VPN clients to access Home network (192.168.100.0/24)
pass in on wg0 from 10.8.0.0/24 to 192.168.100.0/24 keep state

# allow ICMP (ping) from VPN clients to FreeBSD host
pass in on wg0 proto icmp from 10.8.0.0/24 to (wg0) keep state

# allow ICMP (ping) from Home network to FreeBSD host
pass in on em0 proto icmp from 192.168.100.0/24 to (em0) keep state

############################
### outbound traffic ###
############################

# allow all outbound traffic from FreeBSD
pass out keep state

Verify the syntax:

root@setevoy-nas:/home/setevoy # pfctl -vnf /etc/pf.conf
set skip on { lo }
block drop all
pass in log on em0 inet proto tcp from 192.168.0.0/24 to (em0) port = ssh flags S/SA keep state
pass in log on em0 inet proto tcp from 192.168.100.0/24 to (em0) port = ssh flags S/SA keep state
pass in on wg0 inet from 10.8.0.0/24 to (wg0) flags S/SA keep state
pass in on wg0 inet proto icmp from 10.8.0.0/24 to (wg0) keep state
pass in on wg0 inet from 10.8.0.0/24 to 192.168.0.0/24 flags S/SA keep state
pass in on wg0 inet from 10.8.0.0/24 to 192.168.100.0/24 flags S/SA keep state
pass in on em0 inet proto icmp from 192.168.100.0/24 to (em0) keep state
pass in on em0 proto udp from any to (em0) port = 51820 keep state
pass out all flags S/SA keep state

Reload the rules:

root@setevoy-nas:/home/setevoy # service pf reload
Reloading pf rules.

Now we can prepare to start WireGuard.

WireGuard Configuration

Everything is very simple here – create the keys, write the config file.

Creating Keys

Communication and cryptography in WireGuard are built on a standard asymmetric key scheme:

  • The private key is stored on the “server”
  • The public key is specified on the client
  • During the handshake, the client verifies it is connecting to the correct server whose public key it knows
  • Afterward, data encryption is performed using these keys

See Key Exchange and Data Packets.

I’m putting the word “server” in quotes because, as mentioned earlier, WireGuard is P2P, not client-server.

After installing wireguard-tools, the /usr/local/etc/wireguard directory is created – navigate there and create the private and public keys using wg genkey:

root@setevoy-nas:/home/setevoy # cd /usr/local/etc/wireguard
root@setevoy-nas:/usr/local/etc/wireguard # wg genkey | tee server.key | wg pubkey > server.pub

Change the permissions for the private key:

root@setevoy-nas:/usr/local/etc/wireguard # chmod 600 server.key

Verify:

root@setevoy-nas:/usr/local/etc/wireguard # ll
total 12
-rw-------  1 root wheel  45 Dec 17 15:58 server.key
-rw-r--r--  1 root wheel  45 Dec 17 15:58 server.pub

Basic WireGuard config

You can create several different configurations in /usr/local/etc/wireguard/, each on its own port and/or IP and with its own key, to have multiple distinct VPN connections, managing them by filename – wg0, wg1, etc.

There are even config generators available – https://www.wireguardconfig.com.

Syntax documentation – Wireguard Configuration File Format.

Retrieve the private key:

root@setevoy-nas:/usr/local/etc/wireguard # cat server.key 
cLS***GQ=

Create the file /usr/local/etc/wireguard/wg0.conf – just the “server” for now:

[Interface]
Address = 10.8.0.1/24
ListenPort = 51820
PrivateKey = cLS***sGQ=

The Interface block defines the parameters for the WireGuard interface wg0 – its IP address, UDP port, and the private key used for traffic encryption.

You can also specify which DNS to use, whether to update the routing tables on clients (default is true), and script execution with PreUp, PostUp, PreDown, and PostDown.

Start WireGuard itself:

root@setevoy-nas:/home/setevoy # wg-quick up wg0
[#] ifconfig wg create name wg0
[#] wg setconf wg0 /dev/stdin
[#] ifconfig wg0 inet 10.8.0.1/24 alias
[#] ifconfig wg0 mtu 1420
[#] ifconfig wg0 up
[+] Backgrounding route monitor

Verify the interface:

root@setevoy-nas:/home/setevoy # ifconfig wg0
wg0: flags=10080c1<UP,RUNNING,NOARP,MULTICAST,LOWER_UP> metric 0 mtu 1420
        options=80000<LINKSTATE>
        inet 10.8.0.1 netmask 0xffffff00
        groups: wg
        nd6 options=109<PERFORMNUD,IFDISABLED,NO_DAD>

And the WireGuard status:

root@setevoy-nas:/home/setevoy # wg show
interface: wg0
  public key: xLWA/FgF3LBswHD5Z1uZZMOiCbtSvDaUOOFjH4IF6W8=
  private key: (hidden)
  listening port: 51820

Since we don’t have any clients yet – let’s move on to setting them up.

TP-Link Dynamic DNS and NAT port-forwarding

To connect from home to the FreeBSD host with WireGuard – add port forwarding on the office router:

  • protocol: UDP
  • external port on the router: 51830 (to mask it slightly from bots)
  • forward to: 192.168.0.2 (the FreeBSD host)
  • forward to port: 51830 (WireGuard on em0 on FreeBSD)

On the TP-Link Archer AX12, it looks like this:

If the internet IP in the office is dynamic – the Archer AX12 has a Dynamic DNS setting:

Although mine is static, I set up DDNS out of interest using https://www.noip.com.

Running WireGuard on Arch Linux

On Linux, the process is identical – the modules are in the kernel, so we just need to install the package with the tools.

Check the modules:

root@setevoy-home:/home/setevoy # lsmod | grep wireguard
wireguard             122880  0
curve25519_x86_64      36864  1 wireguard
libcurve25519_generic    45056  2 curve25519_x86_64,wireguard
ip6_udp_tunnel         16384  1 wireguard
udp_tunnel             32768  1 wireguard

Install the package:

root@setevoy-home:/home/setevoy # pacman -S wireguard-tools

Navigate to /etc/wireguard/ and create the keys:

root@setevoy-home:/home/setevoy # cd /etc/wireguard/
root@setevoy-home:/etc/wireguard # wg genkey | tee client1.key | wg pubkey > client1.pub

Change the permissions for the private key:

root@setevoy-home:/etc/wireguard # chmod 600 client1.key

Now we can add Peers – clients.

To do this, we need to add keys on the client and the server:

  • on the server:
    • In InterfacePrivateKey: this is /usr/local/etc/wireguard/server.key on the FreeBSD host
    • In PeerPublicKey: this is /etc/wireguard/client1.pub on the Arch Linux laptop
  • on the client:
    • In InterfacePrivateKey: this is /etc/wireguard/client1.key
    • In PeerPublicKey: this is /usr/local/etc/wireguard/server.pub

Define the config **on the client**, file /etc/wireguard/wg0.conf:

[Interface]
PrivateKey = 0Cu***UWU=
Address = 10.8.0.3/24

[Peer]
PublicKey = xLWA/FgF3LBswHD5Z1uZZMOiCbtSvDaUOOFjH4IF6W8=
Endpoint = setevoy-***.ddns.me:51830

AllowedIPs = 10.8.0.1/32, 192.168.0.0/24
PersistentKeepalive = 25

In AllowedIPs, we specify the networks that will be accessible and added to the routing table (“Acts as a routing table and access control list“).

Start it on the client:

[root@setevoy-wg-test setevoy]# wg-quick up wg0
[#] ip link add dev wg0 type wireguard
[#] wg setconf wg0 /dev/fd/63
[#] ip -4 address add 10.8.0.3/24 dev wg0
[#] ip link set mtu 1420 up dev wg0
[#] ip -4 route add 10.8.0.3/32 dev wg0
[#] ip -4 route add 192.168.0.0/24 dev wg0

Here:

  • ip -4 address add: the Interface - Address set for wg0
  • ip -4 route add 10.8.0.3/32 and 192.168.0.0/24: new routes added via the wg0 interface for the VPN and office local networks

Verify:

root@setevoy-home:/etc/wireguard # ip r s 10.8.0.0/24
10.8.0.0/24 dev wg0 proto kernel scope link src 10.8.0.3 
root@setevoy-home:/etc/wireguard # ip r s 192.168.0.0/24
192.168.0.0/24 dev wg0 scope link

Add the Peer **on the server**, the /usr/local/etc/wireguard/wg0.conf file will now look like this:

[Interface]
Address = 10.8.0.1/24
ListenPort = 51820
PrivateKey = cLS***sGQ=

[Peer]
PublicKey = d7yqxOky4qOI/NTl/qbUnijfICwmbe/e/ulSVuQKLhk=
AllowedIPs = 10.8.0.3/32, 192.168.100.0/24

Restart:

root@setevoy-nas:/usr/local/etc/wireguard # wg-quick down wg0
[#] ifconfig wg0 destroy

root@setevoy-nas:/usr/local/etc/wireguard # wg-quick up wg0
[#] ifconfig wg create name wg0
[#] wg setconf wg0 /dev/stdin
[#] ifconfig wg0 inet 10.8.0.1/24 alias
[#] ifconfig wg0 mtu 1420
[#] ifconfig wg0 up
[#] route -q -n add -inet 10.8.0.2/32 -interface wg0
[+] Backgrounding route monitor

Check the status on the client:

root@setevoy-home:/etc/wireguard # wg show
interface: wg0
  public key: d7yqxOky4qOI/NTl/qbUnijfICwmbe/e/ulSVuQKLhk=
  private key: (hidden)
  listening port: 36864

peer: xLWA/FgF3LBswHD5Z1uZZMOiCbtSvDaUOOFjH4IF6W8=
  endpoint: 178.***.***.184:51830
  allowed ips: 10.8.0.1/32, 192.168.0.0/24
  latest handshake: 1 minute, 44 seconds ago
  transfer: 4.35 KiB received, 5.84 KiB sent
  persistent keepalive: every 25 seconds

The most important thing to look for is “latest handshake” – it means the client has connected to the server.

Check on the server:

root@setevoy-nas:/home/setevoy # wg show
interface: wg0
  public key: xLWA/FgF3LBswHD5Z1uZZMOiCbtSvDaUOOFjH4IF6W8=
  private key: (hidden)
  listening port: 51820

peer: d7yqxOky4qOI/NTl/qbUnijfICwmbe/e/ulSVuQKLhk=
  endpoint: 178.***.***.236:56432
  allowed ips: 192.168.100.0/24, 10.8.0.3/32
  latest handshake: 15 seconds ago
  transfer: 1.69 KiB received, 3.87 KiB sent

Verify SSH from the client to the server:

root@setevoy-home:/etc/wireguard # ssh [email protected]
([email protected]) Password for setevoy@setevoy-nas:
...
FreeBSD 14.3-RELEASE (GENERIC) releng/14.3-n271432-8c9ce319fef7

Welcome to FreeBSD!
...

setevoy@setevoy-nas:~ $

Or:

[setevoy@setevoy-home ~]$ ssh 192.168.0.2
([email protected]) Password for setevoy@setevoy-nas:

Enable the wg0 profile on autostart:

[setevoy@setevoy-home ~]$ sudo systemctl enable wg-quick@wg0
Created symlink '/etc/systemd/system/multi-user.target.wants/[email protected]' → '/usr/lib/systemd/system/[email protected]'.

At this point, almost everything is ready – access is established, and everything is working.

However, I also want to have direct access from the home laptop to the work laptop and vice versa, as the work laptop doesn’t have a VPN – it doesn’t need one because FreeBSD/NAS is in the same local network.

Cross-LAN access configuration

So what needs to be done is to set up direct access between the laptops in the home network (192.168.100.0/24) and the office network (192.168.0.0/24), because currently access between the work laptop and the home laptop doesn’t work.

The situation is currently as follows:

  • Office laptop IP: 192.168.0.165
  • Home laptop IP: 192.168.100.205
  • No WireGuard on the work laptop
  • No connection from the office to the home laptop
  • No connection from home to the work laptop
  • Connection from home to FreeBSD exists

Routing Table Setup

While setting this up – comment out block all in /etc/pf.conf; we’ll return to it later.

The result of what we’re about to do will look like this: the key is the routes. I’ve specifically made this as a diagram to make the following steps easier to understand:

Check the routes from the home laptop to FreeBSD:

root@setevoy-home:/etc/wireguard # ip route get 192.168.0.2
192.168.0.2 dev wg0 src 10.8.0.3 uid 0

And to the work laptop:

root@setevoy-home:/etc/wireguard # ip route get 192.168.0.165
192.168.0.165 dev wg0 src 10.8.0.3 uid 0

Traffic goes through wg0, and the Source Address for the packet is set as 10.8.0.3.

However, on the work laptop, the route to the home laptop goes through 192.168.0.1:

[setevoy@setevoy-work ~] $ ip route get 192.168.100.205
192.168.100.205 via 192.168.0.1 dev wlan0 src 192.168.0.165 uid 1000

Here, 192.168.0.1 is the default gateway, the office router, which knows nothing about the home network 192.168.100.0/24.

So first – add a route to the home network via the FreeBSD host:

[setevoy@setevoy-work ~] $ sudo ip route add 192.168.100.0/24 via 192.168.0.2

Check again:

[setevoy@setevoy-work ~] $ ip route get 192.168.100.205
192.168.100.205 via 192.168.0.2 dev wlan0 src 192.168.0.165 uid 1000

Now there is contact from the office to home:

[setevoy@setevoy-work ~] $ ping 192.168.100.205 -c 1
PING 192.168.100.205 (192.168.100.205) 56(84) bytes of data.
64 bytes from 192.168.100.205: icmp_seq=1 ttl=63 time=62.0 ms

--- 192.168.100.205 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms

But from home, it still doesn’t work, because from home we send:

  • from the home laptop with IP 192.168.100.205
    • through FreeBSD with IP 192.168.0.2
      • to the work laptop with IP 192.168.0.165

But from the home laptop, the Source IP is set as 10.8.0.3:

root@setevoy-home:/etc/wireguard # ip route get 192.168.0.165
192.168.0.165 dev wg0 src 10.8.0.3 uid 0

Because the route to 192.168.0.0/24 is specified via the VPN interface wg0:

root@setevoy-home:/etc/wireguard # ip r s 192.168.0.0/24
192.168.0.0/24 dev wg0 scope link 

And wg0 has the IP 10.8.0.3:

root@setevoy-home:/etc/wireguard # ip a s wg0
20: wg0: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000
    link/none 
    inet 10.8.0.3/24 scope global wg0

The work laptop knows nothing about the 10.8.0.0/24 network and cannot return a response.

So, add another route on the work laptop:

[setevoy@setevoy-work ~] $ sudo ip route add 10.8.0.0/24 via 192.168.0.2 dev wlan0

Verify:

[setevoy@setevoy-work ~]  $ ip r s 10.8.0.0/24
10.8.0.0/24 via 192.168.0.2 dev wlan0

And now there is also access from the home laptop to the work laptop:

root@setevoy-home:/etc/wireguard # ping -c1 192.168.0.165
PING 192.168.0.165 (192.168.0.165) 56(84) bytes of data.
64 bytes from 192.168.0.165: icmp_seq=1 ttl=63 time=6.19 ms

--- 192.168.0.165 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms

To make these routes permanent – you can do it via NetworkManager CLI.

Delete the ones added manually:

[setevoy@setevoy-work ~] $ sudo ip route del 10.8.0.0/24 via 192.168.0.2
[setevoy@setevoy-work ~] $ sudo ip route del 192.168.100.0/24 via 192.168.0.2

Find the connection name:

[setevoy@setevoy-work ~] $ nmcli connection show
NAME                  UUID                                   TYPE      DEVICE          
setevoy-tp-link-21-5  3a12a60d-7b37-4c20-b573-d27c47a94ae5  wifi       wlan0 
...

Add the routes:

[setevoy@setevoy-work ~] $ nmcli connection modify setevoy-tp-link-21-5 +ipv4.routes "10.8.0.0/24 192.168.0.2,192.168.100.0/24 192.168.0.2"

Verify:

[setevoy@setevoy-work ~] $ nmcli connection show setevoy-tp-link-21-5 | grep ipv4.routes
ipv4.routes:                            { ip = 10.8.0.0/24, nh = 192.168.0.2 }; { ip = 192.168.100.0/24, nh = 192.168.0.2 }

Restart the connection:

[setevoy@setevoy-work ~] $ sudo nmcli connection down setevoy-tp-link-21-5 && sudo nmcli connection up setevoy-tp-link-21-5
Connection 'setevoy-tp-link-21-5' successfully deactivated (D-Bus active path: /org/freedesktop/NetworkManager/ActiveConnection/15)
Connection successfully activated (D-Bus active path: /org/freedesktop/NetworkManager/ActiveConnection/16)

Check the routes now:

[setevoy@setevoy-work ~] $ ip route get 10.8.0.3
10.8.0.3 via 192.168.0.2 dev wlan0 src 192.168.0.165 uid 1000

[setevoy@setevoy-work ~] $ ip route get 192.168.100.205
192.168.100.205 via 192.168.0.2 dev wlan0 src 192.168.0.165 uid 1000

Now we have ping from the office laptop to the home laptop:

[setevoy@setevoy-work ~] $ ping -c1 192.168.100.205
PING 192.168.100.205 (192.168.100.205) 56(84) bytes of data.
64 bytes from 192.168.100.205: icmp_seq=1 ttl=63 time=5.95 ms

--- 192.168.100.205 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms

And from home to the work laptop:

root@setevoy-home:/etc/wireguard # ping -c1 192.168.0.165
PING 192.168.0.165 (192.168.0.165) 56(84) bytes of data.
64 bytes from 192.168.0.165: icmp_seq=1 ttl=63 time=5.67 ms

--- 192.168.0.165 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms

Packet Filter Configuration

However, if we enable block all in pf, the connection from the office to the home laptop will break, because we currently only have rules for the FreeBSD host:

...
# allow SSH from Office LAN (192.168.0.0/24) to FreeBSD host
pass in log on em0 proto tcp from 192.168.0.0/24 to (em0) port 22 keep state

...
# allow ICMP (ping) from Home network to FreeBSD host
pass in on em0 proto icmp from 192.168.100.0/24 to (em0) keep state

...

Here:

  • The first rule – allows SSH from the office network to the IP of the em0 interface on the FreeBSD host
  • The second rule – allows ping from the home network to the IP of the em0 interface on the FreeBSD host

So, let’s add two more rules – for SSH and ping from the office to the home network:

...
# allow SSH from Office network to Home network
pass in on em0 proto tcp from 192.168.0.0/24 to 192.168.100.0/24 port 22 keep state

...

# allow ICMP from Home network to Office network
pass in on em0 proto icmp from 192.168.0.0/24 to 192.168.100.0/24 keep state
...

Verify and reload the pf config:

root@setevoy-nas:/usr/local/etc/wireguard # pfctl -vnf /etc/pf.conf && service pf reload
set skip on { lo }
block drop log all
pass in log on em0 inet proto tcp from 192.168.0.0/24 to (em0) port = ssh flags S/SA keep state
pass in log on em0 inet proto tcp from 192.168.100.0/24 to (em0) port = ssh flags S/SA keep state
pass in on wg0 inet from 10.8.0.0/24 to (wg0) flags S/SA keep state
pass in on wg0 inet proto icmp from 10.8.0.0/24 to (wg0) keep state
pass in on wg0 inet from 10.8.0.0/24 to 192.168.0.0/24 flags S/SA keep state
pass in on wg0 inet from 10.8.0.0/24 to 192.168.100.0/24 flags S/SA keep state
pass in on em0 inet proto tcp from 192.168.0.0/24 to 192.168.100.0/24 port = ssh flags S/SA keep state
pass in on em0 inet proto icmp from 192.168.0.0/24 to 192.168.100.0/24 keep state
pass in on em0 proto udp from any to (em0) port = 51820 keep state
pass out all flags S/SA keep state
Reloading pf rules.

And now we have ping from home to the office:

root@setevoy-home:/etc/wireguard # ping -c1 192.168.0.165
PING 192.168.0.165 (192.168.0.165) 56(84) bytes of data.
64 bytes from 192.168.0.165: icmp_seq=1 ttl=63 time=8.09 ms

--- 192.168.0.165 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms

SSH from home to the office:

root@setevoy-home:/etc/wireguard # ssh 192.168.0.165
[email protected]'s password:

Ping from the office to home:

[setevoy@setevoy-work ~]  $ ping -c1 192.168.100.205
PING 192.168.100.205 (192.168.100.205) 56(84) bytes of data.
64 bytes from 192.168.100.205: icmp_seq=1 ttl=63 time=60.5 ms

--- 192.168.100.205 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms

And SSH from the office to home:

[setevoy@setevoy-work ~]  $ ssh 192.168.100.205
[email protected]'s password: 

Everything is working.

The full /etc/pf.conf is now as follows:

##################
### Interfaces ###
##################
# lan_if = "em0"
# wg_if  = "wg0"

################
### Networks ###
################
# lan_net      = "192.168.0.0/24"
# home_net     = "192.168.100.0/24"
# wg_net       = "10.8.0.0/24"
# vpn_nets     = "{ 10.8.0.0/24, 192.168.100.0/24 }"

################
### Services ###
################
# ssh_ports = "{ 22 }"
# wg_port   = "51820"

######################
### Basic settings ###
######################

# do not filter loopback traffic
set skip on lo

######################
### Default policy ###
######################

# block everything by default
block log all

#######################
### Inbound traffic ###
#######################

### SSH

# allow SSH from Office LAN (192.168.0.0/24) to FreeBSD host
pass in log on em0 proto tcp from 192.168.0.0/24 to (em0) port 22 keep state

# allow SSH from Home network (192.168.100.0/24) to FreeBSD host
pass in log on em0 proto tcp from 192.168.100.0/24 to (em0) port 22 keep state

# allow SSH from VPN clients to FreeBSD host
pass in on wg0 proto tcp from 10.8.0.0/24 to (wg0) port 22 keep state

### NEW
# allow SSH from Office netwrok to Home network 
pass in on em0 proto tcp from 192.168.0.0/24 to 192.168.100.0/24 port 22 keep state

### TEST

# allow Office LAN to reach Home LAN via WireGuard
#pass in  on em0 from 192.168.0.0/24 to 192.168.100.0/24 keep state
#pass out on wg0 from 192.168.0.0/24 to 192.168.100.0/24 keep state

# allow Home LAN to reach Office LAN via WireGuard
#pass in  on wg0 from 192.168.100.0/24 to 192.168.0.0/24 keep state
#pass out on em0 from 192.168.100.0/24 to 192.168.0.0/24 keep state

### VPN 

# allow WireGuard handshake (UDP/51820) on LAN interface
pass in on em0 proto udp to (em0) port 51820 keep state

# allow VPN clients (10.8.0.0/24) to access FreeBSD host itself
# this allows ping, ssh, etc. to the wg0 address
pass in on wg0 from 10.8.0.0/24 to (wg0) keep state

# allow VPN clients to access Office LAN (192.168.0.0/24)
pass in on wg0 from 10.8.0.0/24 to 192.168.0.0/24 keep state

# allow VPN clients to access Home network (192.168.100.0/24)
pass in on wg0 from 10.8.0.0/24 to 192.168.100.0/24 keep state

# 
#pass in on em0 from 192.168.0.0/24 to 192.168.100.0/24 keep state

#pass in on wg0 from 192.168.100.0/24 to 192.168.0.0/24 keep state

### ICMP

# allow ICMP from VPN clients to FreeBSD host
pass in on wg0 proto icmp from 10.8.0.0/24 to (wg0) keep state

# allow ICMP from Home network to FreeBSD host
#pass in on em0 proto icmp from 192.168.100.0/24 to (em0) keep state

# allow ICMP from Home network to Office network
pass in on em0 proto icmp from 192.168.0.0/24 to 192.168.100.0/24 keep state

############################
### outbound traffic ###
############################

# allow all outbound traffic from FreeBSD
pass out keep state

Active connections in pftop:

Where:

  • In 192.168.0.165:50286 => 192.168.0.2:22: SSH from work laptop to FreeBSD
  • In 178.***.***.236:56432 => 192.168.0.2:51820: connection from home via NAT Port-forwarding on the office router to VPN on FreeBSD
  • In 10.8.0.3:39442 => 192.168.0.165:22: SSH from home to the work laptop
  • Out 10.8.0.1:50589 => 10.8.0.3:22: SSH from FreeBSD to the home laptop

P.S. What an absolute blast – this “traditional networking” instead of all those AWS VPCs and their subnets…