That was somehow more work than I had thought.
I solved my problem now after a long evening.
First of, I tried to build a PF firewall which works with one wire-guard interface.
Creating the interface, creating a new firewall ruleset, storing and loading it, I succeeded.
Quickly I noticed that I want to connect either randomly to one created wire-guard interface on the fly, or choose from a list, but I don't want to connect to the same wire-guard interface every time I boot/reboot the PC.
Then I noticed that I need options in case my connection goes down.
So, I thought about a rebuild option which destroys my current interface and creates a new one, and recreates the firewall ruleset.
All this lead me to write two scripts to perform the actions.
During boot, I ensure that PF loads my default deny all ruleset, until I encounter the login screen.
After login my script creates a new PF firewall ruleset, wg interface, and ensures that this new ruleset is loaded and applied.
Maybe it is my paranoia, but at least I am happy now that my real IP is blocked if the wire-guard interface crumbles.
Here are my scripts in case someone have a similar problem, and wants to have a solution for that problem.
Script1:
Code:
#!/usr/bin/env bash
# Functions defined in this file are going to be shared across different manager scripts
function isPath ()
{
[ ! -d "${1}" ] && echo "0"
[ -d "${1}" ] && echo "1"
}
function printHeader ()
{
# Print upper part
headerLength="${#3}"
echo -n "#"
for ((i=1; i<headerLength-1; ++i))
do
echo -n "$2"
done
echo "#"
# Print title
echo "$3"
# Print lower part
echo -n "#"
for ((i=1; i<headerLength-1; ++i))
do
echo -n "$2"
done
echo "#"
echo
}
function printFooter ()
{
argsNum="$#"
for ((i=1; i<=argsNum; ++i))
do
echo "$i -> $1"
shift 1 # Shift the next argument to the first index
done
echo
}
function getCharInput ()
{
while true
do
read -p "Enter [y|n]: " charInput
[[ "$charInput" == "y" ]] && echo "y" && break
[[ "$charInput" == "n" ]] && echo "n" && break
done
}
function getIntInput ()
{
while true
do
read -p "Enter a number between $1 and $2: " intInput
if (( "$intInput" >= "$1" && "$intInput" <= "$2" )); then
echo "$intInput"
break
fi
done
}
Script2:
Code:
#!/usr/bin/env bash
# This script manages the building/rebuilding process of a PF based firewall
# This firewall only allows local traffic through a local interface, and internet traffic through a wire-guard interface
# Include shared functions
source shared-manager-functions.sh .
# Generate a wire-guard config list from available wire-guard configs
ls -l $HOME/.config/wireguard-servers/*/* | awk '{print $9}' > "$HOME/.cache/wire-guard-config-list"
wgConfigList="$HOME/.cache/wire-guard-config-list"
function getRandomWireGuardConfig ()
{
# Choose a wire-guard config randomly from the wire-guard config list
local randomNumAmount="1"
local startNum="3" # I want to exclude my first two double-vpn connections
local endNum="$(cat "$wgConfigList" | wc -l)"
local randomNum="$(jot -r $randomNumAmount $startNum $endNum)"
wgConfig="$(sed -n "${randomNum}"p "$wgConfigList")"
}
function getChoosenWireGuardConfig ()
{
# Let the user choose a wire-guard config from the wire-guard config list
local wgConfigAmount="$(cat "$wgConfigList" | wc -l)"
basename $(cat "$wgConfigList") | less -N
local intInput="$(getIntInput 1 $wgConfigAmount)"
wgConfig="$(sed -n "${intInput}"p "$wgConfigList")"
}
function setWireGuardInterface ()
{
while true
do
doas -- wg-quick $1 "$2"
local wgInterface="$(ifconfig | grep wg- | awk '{printf $1}' | cut -d: -f1)"
[[ "$1" == "up" ]] && [ ! -z "$wgInterface" ] && break
[[ "$1" == "down" ]] && [ -z "$wgInterface" ] && break
done
}
function buildFirewall ()
{
# Get the wire-guard interface and wire-guard port number
local wgInterface="$(basename "$1" | cut -d. -f1)"
local wgPort="51820"
# Set path to PF firewall config
local pfConfig="$HOME/.cache/pf-vpn-firewall.conf"
# Define firewall ruleset
cat << EOF > "$pfConfig"
# Block all connections by default
block all
# Allow local traffic
pass quick on lo0 all
# Allow DHCP connections
pass quick proto udp from port 67 to port 68
pass quick proto udp from port 68 to port 67
# Allow DNS queries
pass quick proto udp to any port 53
pass quick proto tcp to any port 53
# Allow wire-guard connection
pass out quick proto udp to any port $wgPort
# Allow traffic through wire-guard interface
pass quick on $wgInterface all
EOF
# Clear current available PF firewall ruleset and load the new generated ruleset
doas -- pfctl -F all -f "$pfConfig" 2> /dev/null
}
function buildVPNFirewall ()
{
# Get a wire-guard config based on user choice
echo "Do you want to build a firewall with a random wire-guard interface ?"
charInput="$(getCharInput)"
[[ "$charInput" == "y" ]] && getRandomWireGuardConfig
[[ "$charInput" == "n" ]] && getChoosenWireGuardConfig
# Enable a wire-guard interface
setWireGuardInterface up "$wgConfig"
# Build the firewall based on the wire-guard config
buildFirewall "$wgConfig"
}
function rebuildVPNFirewall ()
{
# Disable current wire-guard interface
local wgInterface="$(ifconfig | grep wg- | cut -d: -f1)"
local wgConfig="$(cat "$wgConfigList" | grep "$wgInterface")"
setWireGuardInterface down "$wgConfig"
# Build a new firewall
buildVPNFirewall
}
# Display main menu with options to choose from
while true
do
clear
printHeader "#" "-" "# VPN Firewall Build Manager #"
printFooter "Build a VPN firewall" "Rebuild a VPN firewall" "Exit the manager"
intInput="$(getIntInput "1" "3")"
case "$intInput" in
1) buildVPNFirewall
break;;
2) rebuildVPNFirewall
break;;
3) break;;
esac
done