← BackJan 4, 2026

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

Click to rate this post! [Total: 11 Average: 3.9]I am continuing to set up my home server on FreeBSD 14.3, which is intended to serv...

Click to rate this post! [Total: 11 Average: 3.9]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. All posts in this blog series: FreeBSD: Home NAS, part 1 – configuring ZFS mirror (RAID1) FreeBSD: Home NAS, part 2 – introduction to Packet Filter (PF) firewall (current) FreeBSD: Home NAS, part 3 – WireGuard VPN, Linux peer, and routing FreeBSD: Home NAS, part 4 – Local DNS with Unbound FreeBSD: Home NAS, part 5 – ZFS pool, datasets, snapshots, and ZFS monitoring FreeBSD: Home NAS, part 6 – Samba server and client connections 
 to be continued Contents WireGuard vs OpenVPNNetwork ArchitectureRunning WireGuard on FreeBSDNetwork configurationIP forwarding configurationPacket Filter ConfigurationWireGuard ConfigurationCreating KeysBasic WireGuard configTP-Link Dynamic DNS and NAT port-forwardingRunning WireGuard on Arch LinuxCross-LAN access configurationRouting Table SetupPacket Filter Configuration 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 metric 0 mtu 1420 options=80000 inet 10.8.0.1 netmask 0xffffff00 groups: wg nd6 options=109 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 Interface – PrivateKey: this is /usr/local/etc/wireguard/server.key on the FreeBSD host In Peer – PublicKey: this is /etc/wireguard/client1.pub on the Arch Linux laptop on the client: In Interface – PrivateKey: this is /etc/wireguard/client1.key In Peer – PublicKey: 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: 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