Solved Building a firewall to route all incoming and outgoing connections through a selected wire-guard interface

I have the feeling that I am close to finally get a firewall build, which only allows traffic through VPN connections.
What I have done so far...

1) I searched up the internet and found some articles about how I could implement kill switch behavior in my firewall.
I have attempted to build this kind of firewall:
Code:
INPUT:
cat ~/.config/vpn-firewall/pf.conf

OUTPUT:
WIREGUARD_PORT="33212"
VPN_INTERFACE="ch-zrh-wg-505"

# Default: block all traffic
block all

# Allow local traffic
pass quick on lo0 all

# Allow DHCP
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 WireGuard connection
pass out quick proto udp to any port $WIREGUARD_PORT

# Allow traffic through VPN
pass quick on $VPN_INTERFACE all

I got the wire-guard port through issuing wg and the interface through issuing ifconfig.
The problem I have is, I cannot get connected to the internet, although it should be possible.
My guess is that the tcp, and udp port number is wrong.

I changed the VPN_INTERFACE to re1, and I can send ping requests, but I get now the following error: ping: sendto: Permission denied.
 
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
 
Back
Top