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

2026-01-0411:5517212rtfm.co.ua

Setting up WireGuard VPN on FreeBSD 14.3: PF firewall, routing between networks, and peer-to-peer VPN between FreeBSD and Arch Linux

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.

All posts in this blog series:

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…

Loading


Read the original article

Comments

  • By age123456gpg 2026-01-0415:242 reply

    You can get yourself a vanity key using https://github.com/AlexanderYastrebov/wireguard-vanity-key tool:

       % wireguard-vanity-key -prefix=NAS/
       private                                      public                                       attempts   duration   attempts/s
       EiBsDB8zt/G4+VWGvxW2ZznNXYmcslcIyJimNR2PpF4= NAS/aex8+IFzLePBYVNGMsSo/1/XeUZcam+Hn8wbNB4= 22619537   0s         112587360

    • By hnarn 2026-01-0512:45

      While it's cool, something about vanity keys in general stroke me the wrong way. I feel like in principle you should never use a very short part of a public key for ocular identification, and it attempts to solve something that should be solved outside of wireguard, i.e. the "friendly naming" of public keys.

    • By bschmidt25002 2026-01-0421:02

      [dead]

  • By rpcope1 2026-01-0420:063 reply

    Wireguard is cool, but there's some reasons it's worth considering OpenVPN (why I still use OpenVPN anyways). First, OpenVPN has kernel mode now (called DCO, which I think Netgate maybe has upstreamed to FreeBSD); I've found it's performance on hardware with AES-NI on Linux is actually often better than wireguard. Second, there's a lot of quality of life things that just work on OpenVPN that you've got to use a ton of duct tape to make work with Wireguard, a major one being handling DNS record change (think especially dynamic DNS, which is likely if this is IPv4 and a residential connection). This is a huge pain with Wireguard, but just works on OpenVPN. Similarly if you have multiple WAN links, like I do, for OpenVPN it's just two connection stanzas and it largely just works. Again for Wireguard you're adding lots of duct tape to make it work right. I know Wireguard is the new hot thing, but it leaves a lot to be desired in the resiliency and features department.

    • By paranoidrobot 2026-01-051:561 reply

      One of the major advantages for Wireguard over OpenVPN (for me) is that it's quite difficult for random port scans to detect it.

      With OpenVPN it's hanging out there responding to everyone that asks nicely that yes, it's OpenVPN.

      So anyone with a new exploit for OpenVPN just has to pull up Shodan and now they've got a nice list of targets that likely have access to more private networks.

      Wireguard doesn't respond at all unless you've got the right keys.

      Also, fwiw - we're approaching 11 years since it was announced, and 5 years since it was accepted into the Linux/BSD kernels.

      • By rsyring 2026-01-056:23

        > With OpenVPN it's hanging out there responding to everyone that asks nicely that yes, it's OpenVPN.

        I believe asing UDP mode and a ta.key go a long way towards making OpenVPN invisible to port scans. Double check docs for details.

    • By ZeWaren 2026-01-0420:331 reply

      I use wireguard as my main VPN to connect to my homelab from my phone and my laptops.

      I also have an OpenVPN as a backup option, running behind sslh. My same port on my router (443) serves both a webserver hosting photos, and that OpenVPN instance. This allows me to VPN into my home in most firewalled office networks.

      • By bayindirh 2026-01-0420:421 reply

        Why not using tailscale/headscale, which removes the requirement to expose home network to internet at all?

        • By lurking_swe 2026-01-0423:291 reply

          i’m assuming because of the “web server hosting photos”. Probably Immich if i had to guess?

          tailscale is fine if you’re somewhat tech savvy, but it’s annoying to show all your friends and family how to “correctly” access your web server. Too much friction. First download the tailscale app, sign in, blah blah. Then you also are unnecessarily bogging down everyone’s smartphone with a wire guard VPN profile which is…undesirable.

          I like tailscale and use it for some stuff. But for web servers that i want my whole family (and some friends) to easily access, a traditional setup makes much more sense. The tradeoff is (obviously) a higher security burden. I protect the web apps in my homelab with SSO (OIDC), among other things.

          • By bayindirh 2026-01-050:001 reply

            I prefer to gatekeep "entry points" with Tailscale. A server can have HTTP/S exposed to the world, but its SSH can stay behind Tailscale to enable defense in depth.

            Keeping Tailscale as the only security layer will be foolish of course, but keeping the entry points hidden from general internet is a useful additional layer, if you ask me.

            As a matter of principle, I like keep the number of open ports to a minimum. Let it be SSH or VPN, it doesn't matter. I have been burned enough times.

            • By waynesonfire 2026-01-050:341 reply

              I've applied the same principal to my network. Though, I do have plans to re-open some additional ports beyond just SSH / VPN.

              Thinking through how I would achieve this introduced me to the concept of a DMZ-zone. The DMZ places publicly accessible services in a highly locked down environment.

              • By bayindirh 2026-01-068:55

                DMZ is a very old concept, and applying it is easy when everything is in a single room, connected to a single network, and everything can be isolated there.

                When the network is distributed on multiple sites, things get exponentially harder if you don't own a dark fiber from site to site and have essentially a single network.

                I personally manage enough servers to scratch that itch, so I yearn for simplicity. If Tailscale gives me that isolation for free (which it does), I'd rather use that for my toy network rather than an elaborate multi-site DMZ setup.

    • By justsomehnguy 2026-01-060:57

      Wireguard is cool transport protocol.

      OpenVPN is a proper VPN protocol with a serious performance troubles if you misstep even once.

      Wireguard fanboys just never use it more than on a couple of devices where they could manually tinker everything what is needed, they never provided a VPN solutions for even dozens of users.

  • By bschmidt25017 2026-01-0421:26

    [dead]

HackerNews