Skip to content

Instantly share code, notes, and snippets.

@WhiteHusky
Last active January 1, 2026 02:22
Show Gist options
  • Select an option

  • Save WhiteHusky/680c1f80c370c87703888a79e64cfa29 to your computer and use it in GitHub Desktop.

Select an option

Save WhiteHusky/680c1f80c370c87703888a79e64cfa29 to your computer and use it in GitHub Desktop.
Private WireGuard VPN for home access, including DNS and internet routing via peer
{ config, lib, pkgs, ... }:
{
networking.firewall = {
enable = true;
trustedInterfaces = [
"wg-vpn"
];
allowedUDPPorts = [
# 51820
51821
];
};
# DNS Caching
services.bind = {
enable = true;
cacheNetworks = [
"127.0.0.0/24"
"::1/128"
"10.10.10.0/24"
];
forward = "first";
forwarders = [
"9.9.9.9 tls quad9-tls"
"149.112.112.112 tls quad9-tls"
"2620:fe::fe tls quad9-tls"
"2620:fe::9 tls quad9-tls"
];
extraOptions = ''
validate-except { my.home.internal; };
'';
extraConfig = ''
tls quad9-tls { remote-hostname "dns.quad9.net"; };
zone "my.home.internal" {
type forward;
forward only;
forwarders { 10.10.10.0.101; };
};
'';
# Allows VPN devices to resolve each other for convenience.
# And for example, pfSense allows setting a specific DNS server for
# domains, allowing LAN devices to also find VPN devices if possible.
zones."vpn.my.home.internal" = {
master = true;
file = pkgs.writeText "vpn.my.home.internal.zone"
''
$TTL 30M
@ IN SOA dns.vpn.my.home.internal. hostmaster.vpn.my.home.internal. (
20251226
1d
15M
2d
30M
)
IN NS dns.vpn.my.home.internal.
@ IN A 10.10.10.100
dns IN A 10.10.10.100
some-peer IN A 10.10.10.1
'';
};
};
networking.wireguard.interfaces."wg-vpn" =
let
sysctl = lib.getExe pkgs.sysctl;
ns = "vpn";
ips = [ "10.10.10.100/32" ];
additional-routes = [ "10.10.10.0/24" ];
mac-address = "86:f1:b7:0f:cc:c3";
in
{
interfaceNamespace = ns;
preSetup = ''
ip netns add ${ns}
'';
postSetup = lib.strings.concatStringsSep "\n" ([''
ip link add ${ns} type veth peer host netns ${ns}
ip link set ${ns} address ${mac-address}
ip link set ${ns} arp off
# Without ARP, Ethernet frames send with the source and destination
# set to the sending interface's, but IP Forwarding does not act on
# frames that don't match its side. So use the same one.
ip -n ${ns} link set host address ${mac-address}
ip -n ${ns} link set host arp off
ip -n ${ns} link set host up
# There's a delay, so set up again here waits.
ip link set ${ns} up
''] ++
(map (ip: ''ip address add "${ip}" dev "${ns}"'') ips) ++
(map (ip: ''ip -n ${ns} route add "${ip}" dev host'') ips) ++
(map (route: ''ip route add "${route}" dev ${ns}'') additional-routes) ++
[''
ip netns exec ${ns} ${sysctl} -w net.ipv4.conf.$DEVICE.forwarding=1
ip netns exec ${ns} ${sysctl} -w net.ipv4.conf.host.forwarding=1
'']
);
postShutdown = ''
# This deconstructs everything made within (or part of) the namespace
ip netns delete ${ns}
'';
# The initial namespace holds the IP instead, so a route is used
#ips = [ XXX ]
listenPort = 51821;
privateKeyFile = "/etc/nixos/wg-vpn.key";
peers = [
# NAT translation could be done at this peer, unless downstream routers
# are aware (and the peer allows forwarding) of the VPN subnet and routes
# back up the peer via static routing.
{
name = "HomeFirewall";
publicKey = "AAAA";
allowedIPs = [
"10.10.10.101/32"
# Home subnet
"10.20.10.0/24"
# Allow routing out to the internet
"0.0.0.0/0"
];
}
# Other devices...
{
name = "SomePeer";
publicKey = "BBBB";
allowedIPs = ["10.10.10.1/32"];
}
];
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment