Skip to content

Router (PfSense)

Tunables

Note: most of these are defaults but I can't remember whats default anymore so here's all of them.

System->Advanced->System Tunables !

pfsense_tunables

net.inet.tcp.recvspace  2097152
net.inet.tcp.sendspace  2097152
net.raw.recvspace   262144
net.inet.raw.recvspace  262144
net.inet.raw.maxdgram   262144
net.raw.sendspace   262144
net.inet.ip.portrange.first 1024
net.inet.tcp.blackhole  2
net.inet.udp.blackhole  1
net.inet.ip.random_id   1
net.inet.tcp.drop_synfin    1
net.inet.ip.redirect    1
net.inet6.ip6.redirect  1
net.inet6.ip6.use_tempaddr  0
net.inet6.ip6.prefer_tempaddr   0
net.inet.tcp.syncookies 1
net.inet.tcp.delayed_ack    0
net.inet.udp.maxdgram   57344
net.link.bridge.pfil_onlyip 0
net.link.bridge.pfil_member 1
net.link.bridge.pfil_bridge 0
net.link.tap.user_open  1
net.link.vlan.mtag_pcp  1
kern.randompid  347
net.inet.ip.intr_queue_maxlen   1000
hw.syscons.kbd_reboot   0
net.inet.tcp.log_debug  0
net.inet.tcp.tso    1
net.inet.icmp.icmplim   0
vfs.read_max    32
kern.ipc.maxsockbuf 4262144
net.inet.ip.process_options 0
kern.random.harvest.mask    351
net.route.netisr_maxqlen    1024
net.inet.udp.checksum   1
net.inet.icmp.reply_from_interface  1
net.inet6.ip6.rfc6204w3 1
net.key.preferred_oldsa 0
net.inet.carp.senderr_demotion_factor   0
net.pfsync.carp_demotion_factor 0
kern.corefile   /root/%N.core   

General Setup

I know its a firewall not a router.

All interfaces have an MTU of 9000. All physical or virtualized adapters need to have this too. Right now I have every network port on the server running through the hypervisor on its own vswitch before it gets to pfsense. This is because I have the horsepower to do it without thinking and it doesn't impact my 10G networking performance appreciably. What I gain is flexibility with my physical port utilization and a degree of laziness. I also have the option to more directly connect a VM to a physical machine by wiring that VM directly to that port group.

  • General setup:
    • DNS 1.1.1.1 and 1.0.0.1
    • theme pfsense-dark
  • Interfaces:
    • set MTU to 9000 on all.
    • disable DHCP on all
    • disable ipv6 on all
  • add wireguard package
  • add pfblockerng package
    • IP->CIDR aggregation
    • maybe check the latest optional lists to add restart pfsense

Wireguard

  • Mullvad:
    • add tunnel
    • add peer
    • allowed IPs should be 0.0.0.0/0
    • go to system->routing
    • apply to the mullvad interface
    • add gateway (10.64.0.1 for some reason)
    • check "Use non-local gateway through interface specific route."
    • set ipv4 upstream gateway in mullvad interface
    • restart wireguard service
    • verify connection in wireguard->status
  • Port Fowarding:
    • no port 80
    • port 443 TCP for Caddy HTTPS. dest "wan address". specific IP and add host like normal.
    • port for the personal wireguard vpn, UDP, wan address
  • Outbound NAT:
    • manual mode, NEVER change it.
    • my typical setup WAS a /26 to split a /24 given to the compute esxi box so some stuff is vpn and others not.
      • now everything that needs isolation etc is just on an appropriate subnet/virtual adapter.
    • clone the two rules for this /24.
    • the /24 rules should go first and should have "interface" adjusted to mullvad's (wireguard tunnel) interface.
    • /26 rules are the original rules, just edit the subnet to be /26
    • remember to save and apply changes
  • Firewall Rules:
    • LAN: IPv4, source lan, destination *, default allow lan to any
    • NAS: allow nas (source) to talk to stuff (destination)
    • MEDIA: (media servers and servarrs): for each VM, allow (source) access to NAS (dest)
      • allow /24 * (all) access to mullvad gateway, dest *
    • personal WG: ipv4 * allow all.
  • LAN (Nomicon)
    • My primary LAN interface is used to connect me via my "main terminal" i.e. desktop, to pfsense and the internet.
  • NAS
    • NAS gets its own interface for security, reliability, and performance.
  • MEDIA
    • Everything
  • VPN
    • The interface we have connecting to a VPN provider. i.e. Mullvad
    • We give this interface to virtual machines we want to only ever be able to see the internet through our VPN.
  • WIREGUARD_CLIENTS
    • Same for any new wireguard server instance because each new tunnel creates a new interface.

PfBlockerNG (Adblock)

Wireguard Server

Make a new tunnel, new interface. set interface to static ipv4 and give it a /24 subnet to control. Once you have your new tunnel, theres a download button in pfsense to download it, then run my little tool here and you should have a client config all ready to go. !

pfsensewireguardgenerator.py

#!/usr/bin/env python3
"""
Generate WireGuard client configs from pfSense server config.
Generates new keypairs for each peer and outputs the public keys for pfSense update.
"""
import subprocess
import sys

def generate_keypair():
    """Generate a new WireGuard private/public keypair."""
    try:
        # Generate private key
        privkey = subprocess.check_output(['wg', 'genkey'], text=True).strip()
        # Derive public key
        pubkey = subprocess.check_output(
            ['wg', 'pubkey'],
            input=privkey,
            text=True
        ).strip()
        return privkey, pubkey
    except FileNotFoundError:
        print("Error: 'wg' command not found. Install wireguard-tools.", file=sys.stderr)
        sys.exit(1)


def parse_server_config(config_path):
    """Parse pfSense WireGuard server config file."""
    config = {}
    peers = []
    current_peer = None

    with open(config_path, 'r') as f:
        for line in f:
            line = line.strip()

            if line.startswith('[Interface]'):
                current_peer = None
            elif line.startswith('[Peer]'):
                current_peer = {}
                peers.append(current_peer)
            elif '=' in line:
                key, value = [x.strip() for x in line.split('=', 1)]

                if current_peer is None:
                    # Server interface section
                    config[key] = value
                else:
                    # Peer section
                    current_peer[key] = value

    return config, peers


def generate_client_config(server_config, peer, server_endpoint, new_privkey):
    """Generate client config for a peer."""
    # Extract server's public key
    server_pubkey = server_config.get('PrivateKey', '')
    if server_pubkey:
        # Derive public key from server's private key
        try:
            server_pubkey = subprocess.check_output(
                ['wg', 'pubkey'],
                input=server_pubkey,
                text=True
            ).strip()
        except:
            server_pubkey = ''

    # Get server's listen port
    listen_port = server_config.get('ListenPort', '51820')

    # Get peer's allowed IPs (this becomes the client's address)
    allowed_ips = peer.get('AllowedIPs', '')

    # Parse client IP from AllowedIPs (e.g., "10.0.0.2/32" -> "10.0.0.2/32")
    client_address = allowed_ips if allowed_ips else '10.0.0.2/24'

    # Build client config
    config = f"""[Interface]
PrivateKey = {new_privkey}
Address = {client_address}
DNS = 1.1.1.1

[Peer]
PublicKey = {server_pubkey}
Endpoint = {server_endpoint}:{listen_port}
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25"""

    return config


def main():
    if len(sys.argv) < 3:
        print("Usage: python script.py <server_config_file> <server_endpoint_ip>")
        print("Example: python script.py wg0.conf 49.253.81.77")
        sys.exit(1)
    conf_file = sys.argv[1]
    server_endpoint = sys.argv[2]

    # for when I'm extra lazy
    # server_endpoint = "ChangeMeToYourWANIP"
    # for root, dirs, files in os.walk(os.getcwd()):
    #     for f in files:
    #         if f.endswith('.conf'):
    #             conf_files.append(os.path.join(root, f))

    try:
        with open(conf_file, "r"):
            pass
    except FileNotFoundError:
        print(f"Error: Config file not found.", file=sys.stderr)
        sys.exit(1)

    # Parse server config
    server_config, peers = parse_server_config(conf_file)

    if not peers:
        print("No peers found in config file.", file=sys.stderr)
        sys.exit(1)

    print(f"Found {len(peers)} peer(s) in config.\n")

    # Generate configs for each peer
    pfsense_updates = []

    for i, peer in enumerate(peers, 1):
        # Generate new keypair
        new_privkey, new_pubkey = generate_keypair()

        # Generate client config
        client_config = generate_client_config(
            server_config,
            peer,
            server_endpoint,
            new_privkey
        )

        # Save client config
        peer_id = peer.get('AllowedIPs', f'peer{i}').replace('/', '_')
        output_file = f"client_{peer_id}.conf"

        with open(output_file, 'w') as f:
            f.write(client_config)

        print(f"Generated: {output_file}")
        print("=" * 70)
        print(client_config)
        print("=" * 70)
        print()

        # Store for pfSense update instructions
        pfsense_updates.append({
            'old_pubkey': peer.get('PublicKey', 'N/A'),
            'new_pubkey': new_pubkey,
            'allowed_ips': peer.get('AllowedIPs', 'N/A')
        })

    # Print pfSense update instructions
    print("\n" + "=" * 70)
    print("PFSENSE UPDATE INSTRUCTIONS")
    print("=" * 70)
    print("\nIn pfSense, go to VPN > WireGuard > Peers and update each peer:\n")

    for i, update in enumerate(pfsense_updates, 1):
        print(f"Peer {i} (Allowed IPs: {update['allowed_ips']}):")
        print(f"  Old Public Key: {update['old_pubkey']}")
        print(f"  New Public Key: {update['new_pubkey']}")
        print()


if __name__ == '__main__':
    main()

Mullvad VPN Interface (Wireguard Client)

Outbound NAT

Firewall Rules