Add WireGuard tunnels as firewall zone members
router.wireguard.<name> configures tunnels through networkd: road warrior (listening, dual-stack), site-to-site (no transfer network) and provider dial-out with masquerade. A tunnel has a zone like a LAN, so inter-zone rules apply; management, DNS and internet are opt-in. Peers' allowedIPs become routes (destination-based routing; default-route tunnels are rejected). Tunnels have a kill switch (default on): forwarded packets for their allowedIPs are dropped if they would leave via an uplink, before the established-accept so flows that used the tunnel are cut too. Private keys stay outside the Nix store and are passed to the unprivileged systemd-networkd with LoadCredential, so key files can be root-only. Assertions cover CIDR/key/endpoint formats, duplicate allowedIPs, overlaps with LAN subnets, port and device clashes, and rule destinations inside tunnel networks. A five-VM `wireguard` check covers road warrior, site-to-site, the provider tunnel with an IPv6 transfer net, zone rules and the kill switch. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
+114
-11
@@ -60,24 +60,52 @@ let
|
||||
]
|
||||
);
|
||||
|
||||
zoneNames = lib.unique (map (l: l.zone) lans);
|
||||
wgs = lib.attrValues cfg.wireguard;
|
||||
zoneNames = lib.unique (map (l: l.zone) lans ++ map (w: w.zone) wgs);
|
||||
|
||||
isV6 = lib.hasInfix ":";
|
||||
v4Cidr = s: lib.hasInfix "/" s && ipv4.parseNet s != null;
|
||||
v6Cidr =
|
||||
s:
|
||||
let
|
||||
m = builtins.match "([0-9a-fA-F:.]+)/([0-9]{1,3})" s;
|
||||
in
|
||||
isV6 s && m != null && lib.toInt (builtins.elemAt m 1) <= 128;
|
||||
validCidr = s: v4Cidr s || v6Cidr s;
|
||||
|
||||
lanNets = map (l: {
|
||||
address = ipv4.parse l.ipv4.address;
|
||||
inherit (l.ipv4) prefixLength;
|
||||
}) (lib.filter (l: validIp l.ipv4.address) lans);
|
||||
|
||||
# IPv4 networks a tunnel brings into its zone (own addresses and what peers may send).
|
||||
wgV4Nets =
|
||||
w:
|
||||
lib.filter (n: n != null) (
|
||||
map ipv4.parseNet (
|
||||
lib.filter (n: !isV6 n && v4Cidr n) (
|
||||
w.addresses ++ lib.concatMap (p: p.allowedIPs) (lib.attrValues w.peers)
|
||||
)
|
||||
)
|
||||
);
|
||||
zoneV4Nets =
|
||||
zone:
|
||||
map (l: {
|
||||
address = ipv4.parse l.ipv4.address;
|
||||
inherit (l.ipv4) prefixLength;
|
||||
}) (lib.filter (l: l.zone == zone && validIp l.ipv4.address) lans)
|
||||
++ lib.concatMap wgV4Nets (lib.filter (w: w.zone == zone) wgs);
|
||||
|
||||
ruleAssertions =
|
||||
r:
|
||||
let
|
||||
where = "router.firewall.rules (${r.from} -> ${r.to})";
|
||||
dest = if r.destination == null then null else ipv4.parseNet r.destination;
|
||||
destLans = lib.filter (l: l.zone == r.to && validIp l.ipv4.address) lans;
|
||||
inZone =
|
||||
dest != null
|
||||
&& lib.any (
|
||||
l:
|
||||
dest.prefixLength >= l.ipv4.prefixLength
|
||||
&& ipv4.inSubnet {
|
||||
address = ipv4.parse l.ipv4.address;
|
||||
inherit (l.ipv4) prefixLength;
|
||||
} dest.address
|
||||
) destLans;
|
||||
&& lib.any (n: dest.prefixLength >= n.prefixLength && ipv4.inSubnet n dest.address) (
|
||||
zoneV4Nets r.to
|
||||
);
|
||||
in
|
||||
[
|
||||
{
|
||||
@@ -104,7 +132,7 @@ let
|
||||
}
|
||||
{
|
||||
assertion = dest == null || !(lib.elem r.to zoneNames) || inZone;
|
||||
message = "router: ${where}: destination `${r.destination}` is not inside any LAN of zone `${r.to}`.";
|
||||
message = "router: ${where}: destination `${r.destination}` is not inside any network of zone `${r.to}`.";
|
||||
}
|
||||
{
|
||||
assertion = dest == null || ipv4.networkOf dest.address dest.prefixLength == dest.address;
|
||||
@@ -112,6 +140,61 @@ let
|
||||
}
|
||||
];
|
||||
|
||||
overlapsLan = n: lib.any (l: ipv4.overlaps n l) lanNets;
|
||||
|
||||
wgAssertions =
|
||||
name: w:
|
||||
let
|
||||
where = "router.wireguard.${name}";
|
||||
peers = lib.attrValues w.peers;
|
||||
allowed = lib.concatMap (p: p.allowedIPs) peers;
|
||||
v4Allowed = lib.filter (n: v4Cidr n) allowed;
|
||||
v4Own = lib.filter v4Cidr w.addresses;
|
||||
asNet = n: ipv4.parseNet n;
|
||||
in
|
||||
[
|
||||
{
|
||||
assertion = w.peers != { };
|
||||
message = "router: ${where} has no peers.";
|
||||
}
|
||||
{
|
||||
assertion = lib.stringLength w.interface <= 15;
|
||||
message = "router: ${where}: device name `${w.interface}` is longer than 15 characters.";
|
||||
}
|
||||
{
|
||||
assertion = lib.all validCidr w.addresses;
|
||||
message = "router: ${where}.addresses must be IPv4/IPv6 addresses in CIDR notation.";
|
||||
}
|
||||
{
|
||||
assertion = lib.all validCidr allowed;
|
||||
message = "router: ${where}: every peer allowedIPs entry must be an IPv4/IPv6 CIDR.";
|
||||
}
|
||||
{
|
||||
assertion = !(lib.any (n: lib.hasSuffix "/0" n) allowed);
|
||||
message = "router: ${where}: default-route tunnels (allowedIPs ending in /0) are not supported; list the destinations instead.";
|
||||
}
|
||||
{
|
||||
assertion = lib.length allowed == lib.length (lib.unique allowed);
|
||||
message = "router: ${where}: an allowedIPs entry appears more than once (WireGuard would give it to one peer only).";
|
||||
}
|
||||
{
|
||||
assertion = lib.all (n: !(overlapsLan (asNet n))) (
|
||||
lib.filter (n: !lib.hasSuffix "/0" n) (v4Own ++ v4Allowed)
|
||||
);
|
||||
message = "router: ${where}: a tunnel address or allowedIPs entry overlaps one of the LAN subnets.";
|
||||
}
|
||||
{
|
||||
assertion = lib.all (p: builtins.match "[A-Za-z0-9+/]{43}=" p.publicKey != null) peers;
|
||||
message = "router: ${where}: a peer publicKey is not a valid WireGuard public key.";
|
||||
}
|
||||
{
|
||||
assertion = lib.all (
|
||||
p: p.endpoint == null || builtins.match ".+:[0-9]{1,5}" p.endpoint != null
|
||||
) peers;
|
||||
message = "router: ${where}: a peer endpoint must look like host:port or [ipv6]:port.";
|
||||
}
|
||||
];
|
||||
|
||||
overlapPairs = lib.concatLists (
|
||||
lib.imap0 (
|
||||
i: a:
|
||||
@@ -163,6 +246,26 @@ in
|
||||
]
|
||||
++ lib.concatLists (lib.mapAttrsToList perLan lansByName)
|
||||
++ lib.concatMap ruleAssertions cfg.firewall.rules
|
||||
++ lib.concatLists (lib.mapAttrsToList wgAssertions cfg.wireguard)
|
||||
++ [
|
||||
{
|
||||
assertion =
|
||||
let
|
||||
ports = lib.filter (p: p != null) (map (w: w.listenPort) wgs);
|
||||
in
|
||||
lib.length ports == lib.length (lib.unique ports);
|
||||
message = "router: two WireGuard tunnels listen on the same UDP port.";
|
||||
}
|
||||
{
|
||||
assertion =
|
||||
let
|
||||
own = map (w: w.interface) wgs;
|
||||
others = map (l: l.device) lans ++ [ cfg.wan.interface ];
|
||||
in
|
||||
lib.length own == lib.length (lib.unique own) && !(lib.any (d: lib.elem d others) own);
|
||||
message = "router: a WireGuard device name clashes with a LAN device or the WAN interface.";
|
||||
}
|
||||
]
|
||||
++ map (
|
||||
{ a, b }:
|
||||
let
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
./assertions.nix
|
||||
./networking.nix
|
||||
./pppoe.nix
|
||||
./wireguard.nix
|
||||
./firewall.nix
|
||||
./dhcp.nix
|
||||
./dns.nix
|
||||
|
||||
+18
-2
@@ -2,6 +2,21 @@
|
||||
let
|
||||
cfg = config.router;
|
||||
lans = lib.attrValues cfg.lans;
|
||||
ipv4 = (import ./lib.nix { inherit lib; }).ipv4;
|
||||
|
||||
# Tunnels that opted into DNS: listen on each of their addresses.
|
||||
dnsAddrs = lib.concatMap (w: w.addresses) (lib.filter (w: w.dns) (lib.attrValues cfg.wireguard));
|
||||
addrOf = a: builtins.head (lib.splitString "/" a);
|
||||
# IPv4 gets the proper network address; IPv6 netblocks are masked by Unbound.
|
||||
netOf =
|
||||
a:
|
||||
let
|
||||
parts = lib.splitString "/" a;
|
||||
in
|
||||
if lib.hasInfix ":" a then
|
||||
a
|
||||
else
|
||||
ipv4.cidr (builtins.head parts) (lib.toInt (builtins.elemAt parts 1));
|
||||
in
|
||||
{
|
||||
config = lib.mkIf cfg.enable {
|
||||
@@ -9,13 +24,14 @@ in
|
||||
enable = true;
|
||||
settings = {
|
||||
server = {
|
||||
interface = [ "127.0.0.1" ] ++ map (l: l.ipv4.address) lans;
|
||||
interface = [ "127.0.0.1" ] ++ map (l: l.ipv4.address) lans ++ map addrOf dnsAddrs;
|
||||
# LAN addresses are assigned by networkd, possibly after Unbound starts.
|
||||
ip-freebind = true;
|
||||
access-control = [
|
||||
"127.0.0.0/8 allow"
|
||||
]
|
||||
++ map (l: "${l.ipv4.subnet} allow") lans;
|
||||
++ map (l: "${l.ipv4.subnet} allow") lans
|
||||
++ map (a: "${netOf a} allow") dnsAddrs;
|
||||
};
|
||||
}
|
||||
// lib.optionalAttrs (cfg.dns.forwarders != [ ]) {
|
||||
|
||||
+61
-11
@@ -4,19 +4,59 @@ let
|
||||
lans = lib.attrValues cfg.lans;
|
||||
|
||||
set = ifaces: "{ ${lib.concatMapStringsSep ", " (i: ''"${i}"'') ifaces} }";
|
||||
lanIfaces = map (l: l.device) lans;
|
||||
mgmtIfaces = map (l: l.device) (lib.filter (l: l.allowManagement) lans);
|
||||
internetIfaces = map (l: l.device) (lib.filter (l: l.internet) lans);
|
||||
wgs = lib.attrValues cfg.wireguard;
|
||||
isV6 = lib.hasInfix ":";
|
||||
|
||||
# zone name -> devices of its member LANs
|
||||
zones = lib.mapAttrs (_: ls: map (l: l.device) ls) (lib.groupBy (l: l.zone) lans);
|
||||
# Everything that is a firewall zone member: LANs and tunnels.
|
||||
members =
|
||||
map (l: {
|
||||
inherit (l)
|
||||
device
|
||||
zone
|
||||
internet
|
||||
allowManagement
|
||||
;
|
||||
}) lans
|
||||
++ map (w: {
|
||||
device = w.interface;
|
||||
inherit (w) zone internet allowManagement;
|
||||
}) wgs;
|
||||
devicesOf = pred: map (m: m.device) (lib.filter pred members);
|
||||
|
||||
lanIfaces = map (l: l.device) lans;
|
||||
memberIfaces = devicesOf (_: true);
|
||||
dnsIfaces = lanIfaces ++ map (w: w.interface) (lib.filter (w: w.dns) wgs);
|
||||
mgmtIfaces = devicesOf (m: m.allowManagement);
|
||||
internetIfaces = devicesOf (m: m.internet);
|
||||
listenPorts = lib.unique (map (w: w.listenPort) (lib.filter (w: w.listenPort != null) wgs));
|
||||
masqueradeIfaces = map (w: w.interface) (lib.filter (w: w.masquerade) wgs);
|
||||
|
||||
# Destinations a kill-switched tunnel must never leak to an uplink.
|
||||
killSwitched = lib.filter (w: w.killSwitch) wgs;
|
||||
killNets = lib.concatMap (w: lib.concatMap (p: p.allowedIPs) (lib.attrValues w.peers)) killSwitched;
|
||||
killRules =
|
||||
let
|
||||
v4 = lib.filter (n: !isV6 n) killNets;
|
||||
v6 = lib.filter isV6 killNets;
|
||||
list = ns: "{ ${lib.concatStringsSep ", " ns} }";
|
||||
in
|
||||
lib.optional (v4 != [ ]) "oifname ${set uplinks} ip daddr ${list v4} drop"
|
||||
++ lib.optional (v6 != [ ]) "oifname ${set uplinks} ip6 daddr ${list v6} drop";
|
||||
|
||||
# zone name -> devices of its members
|
||||
zones = lib.mapAttrs (_: ms: map (m: m.device) ms) (lib.groupBy (m: m.zone) members);
|
||||
zoneDevices = zone: zones.${zone} or [ ];
|
||||
|
||||
# LANs sharing a zone may forward to each other (a single LAN is plain L2).
|
||||
# Members of one zone may forward to each other, but never back out of the
|
||||
# device they came in on (a tunnel's peers do not see each other).
|
||||
intraZone = lib.concatLists (
|
||||
lib.mapAttrsToList (
|
||||
zone: devs:
|
||||
lib.optional (lib.length devs > 1) "iifname ${set devs} oifname ${set devs} accept # zone ${zone}"
|
||||
lib.optionals (lib.length devs > 1) (
|
||||
map (
|
||||
d: "iifname ${set [ d ]} oifname ${set (lib.filter (x: x != d) devs)} accept # zone ${zone}"
|
||||
) devs
|
||||
)
|
||||
) zones
|
||||
);
|
||||
|
||||
@@ -69,14 +109,19 @@ in
|
||||
destination-unreachable, packet-too-big, time-exceeded, parameter-problem,
|
||||
nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert
|
||||
} accept
|
||||
iifname ${set lanIfaces} icmp type echo-request accept
|
||||
iifname ${set lanIfaces} icmpv6 type echo-request accept
|
||||
iifname ${set memberIfaces} icmp type echo-request accept
|
||||
iifname ${set memberIfaces} icmpv6 type echo-request accept
|
||||
|
||||
${lib.optionalString (!pppoe) ''iifname "${cfg.wan.interface}" udp dport 68 accept''}
|
||||
${lib.optionalString cfg.wan.ipv6.enable ''iifname "${cfg.wan.interface}" udp dport 546 accept''}
|
||||
|
||||
iifname ${set lanIfaces} udp dport 53 accept
|
||||
iifname ${set lanIfaces} tcp dport 53 accept
|
||||
iifname ${set dnsIfaces} udp dport 53 accept
|
||||
iifname ${set dnsIfaces} tcp dport 53 accept
|
||||
${lib.optionalString (listenPorts != [ ])
|
||||
"iifname ${set uplinks} udp dport { ${
|
||||
lib.concatMapStringsSep ", " toString listenPorts
|
||||
} } accept # wireguard"
|
||||
}
|
||||
${lib.optionalString (dhcpIfaces != [ ]) "iifname ${set dhcpIfaces} udp dport 67 accept"}
|
||||
${lib.optionalString (mgmtIfaces != [ ]) "iifname ${set mgmtIfaces} tcp dport 22 accept"}
|
||||
}
|
||||
@@ -84,6 +129,8 @@ in
|
||||
chain forward {
|
||||
type filter hook forward priority filter; policy drop;
|
||||
|
||||
${lib.concatStringsSep "\n " killRules}
|
||||
|
||||
ct state established,related accept
|
||||
ct state invalid drop
|
||||
|
||||
@@ -110,6 +157,9 @@ in
|
||||
chain postrouting {
|
||||
type nat hook postrouting priority srcnat; policy accept;
|
||||
oifname "${uplink4}" masquerade
|
||||
${lib.optionalString (
|
||||
masqueradeIfaces != [ ]
|
||||
) "oifname ${set masqueradeIfaces} masquerade # tunnels"}
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
@@ -186,6 +186,132 @@ in
|
||||
);
|
||||
};
|
||||
|
||||
wireguard = mkOption {
|
||||
default = { };
|
||||
description = ''
|
||||
WireGuard tunnels, keyed by name. A tunnel is a firewall member like a
|
||||
LAN (it has a `zone`, `internet`, `allowManagement`), so
|
||||
`router.firewall.rules` also cover traffic to and from it. Peers'
|
||||
`allowedIPs` are the destinations routed into the tunnel; routing is
|
||||
by destination only (no default-route tunnels).
|
||||
'';
|
||||
type = types.attrsOf (
|
||||
types.submodule (
|
||||
{ name, ... }:
|
||||
{
|
||||
options = {
|
||||
interface = mkOption {
|
||||
type = types.str;
|
||||
default = "wg-${name}";
|
||||
description = "Name of the WireGuard device.";
|
||||
};
|
||||
privateKeyFile = mkOption {
|
||||
type = types.pathWith {
|
||||
absolute = true;
|
||||
inStore = false;
|
||||
};
|
||||
example = "/var/lib/secrets/wg-rw.key";
|
||||
description = ''
|
||||
File with the private key, outside the Nix store. It is
|
||||
handed to systemd-networkd as a systemd credential, so it
|
||||
can be root-only (mode 0600) and is read when networkd starts.
|
||||
'';
|
||||
};
|
||||
listenPort = mkOption {
|
||||
type = types.nullOr types.port;
|
||||
default = null;
|
||||
description = "UDP port to listen on (opened on the uplinks); null for a dial-out-only tunnel.";
|
||||
};
|
||||
mtu = mkOption {
|
||||
type = types.nullOr (types.ints.between 576 9000);
|
||||
default = null;
|
||||
description = "Tunnel MTU; null keeps the kernel default (1420).";
|
||||
};
|
||||
addresses = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
example = [
|
||||
"10.100.0.1/24"
|
||||
"fd42:100::1/64"
|
||||
];
|
||||
description = ''
|
||||
Addresses (CIDR, IPv4 and/or IPv6) on the tunnel interface;
|
||||
empty means no transfer network (fine for site-to-site).
|
||||
'';
|
||||
};
|
||||
zone = mkOption {
|
||||
type = types.str;
|
||||
default = name;
|
||||
description = "Firewall zone; shared with LANs or other tunnels if you want a common policy.";
|
||||
};
|
||||
internet = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Whether traffic from this tunnel may reach the internet via the uplink.";
|
||||
};
|
||||
allowManagement = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Whether SSH on the router is reachable through this tunnel.";
|
||||
};
|
||||
dns = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Whether the router's resolver answers on the tunnel's IPv4 address.";
|
||||
};
|
||||
masquerade = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Source-NAT IPv4 traffic leaving through this tunnel to the tunnel address.";
|
||||
};
|
||||
killSwitch = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Drop forwarded traffic for this tunnel's peers' `allowedIPs`
|
||||
when it would leave through an uplink (for example because
|
||||
the tunnel device is down), instead of letting it leak.
|
||||
'';
|
||||
};
|
||||
peers = mkOption {
|
||||
default = { };
|
||||
type = types.attrsOf (
|
||||
types.submodule {
|
||||
options = {
|
||||
publicKey = mkOption { type = types.str; };
|
||||
presharedKeyFile = mkOption {
|
||||
type = types.nullOr (
|
||||
types.pathWith {
|
||||
absolute = true;
|
||||
inStore = false;
|
||||
}
|
||||
);
|
||||
default = null;
|
||||
};
|
||||
endpoint = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
example = "vpn.example.org:51820";
|
||||
};
|
||||
allowedIPs = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
example = [ "192.168.50.0/24" ];
|
||||
};
|
||||
persistentKeepalive = mkOption {
|
||||
type = types.nullOr (types.ints.between 1 65535);
|
||||
default = null;
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
firewall.rules = mkOption {
|
||||
default = [ ];
|
||||
description = ''
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
{ config, lib, ... }:
|
||||
let
|
||||
cfg = config.router;
|
||||
|
||||
# systemd-networkd runs unprivileged and could not read root-only key files.
|
||||
# LoadCredential= makes systemd read them as root and hand networkd a copy
|
||||
# in its credentials directory, so the files can stay 0600 root.
|
||||
credDir = "/run/credentials/systemd-networkd.service";
|
||||
keyCred = name: "wg-${name}-key";
|
||||
pskCred = name: peer: "wg-${name}-${peer}-psk";
|
||||
credentials = lib.concatLists (
|
||||
lib.mapAttrsToList (
|
||||
name: wg:
|
||||
[ "${keyCred name}:${toString wg.privateKeyFile}" ]
|
||||
++ lib.concatLists (
|
||||
lib.mapAttrsToList (
|
||||
peer: p:
|
||||
lib.optional (p.presharedKeyFile != null) "${pskCred name peer}:${toString p.presharedKeyFile}"
|
||||
) wg.peers
|
||||
)
|
||||
) cfg.wireguard
|
||||
);
|
||||
in
|
||||
{
|
||||
config = lib.mkIf (cfg.enable && cfg.wireguard != { }) {
|
||||
systemd.network.netdevs = lib.mapAttrs' (
|
||||
name: wg:
|
||||
lib.nameValuePair "30-wg-${name}" {
|
||||
netdevConfig = {
|
||||
Kind = "wireguard";
|
||||
Name = wg.interface;
|
||||
}
|
||||
// lib.optionalAttrs (wg.mtu != null) { MTUBytes = wg.mtu; };
|
||||
wireguardConfig = {
|
||||
PrivateKeyFile = "${credDir}/${keyCred name}";
|
||||
}
|
||||
// lib.optionalAttrs (wg.listenPort != null) { ListenPort = wg.listenPort; };
|
||||
wireguardPeers = lib.mapAttrsToList (
|
||||
peer: p:
|
||||
{
|
||||
PublicKey = p.publicKey;
|
||||
AllowedIPs = p.allowedIPs;
|
||||
}
|
||||
// lib.optionalAttrs (p.endpoint != null) { Endpoint = p.endpoint; }
|
||||
// lib.optionalAttrs (p.persistentKeepalive != null) {
|
||||
PersistentKeepalive = p.persistentKeepalive;
|
||||
}
|
||||
// lib.optionalAttrs (p.presharedKeyFile != null) {
|
||||
PresharedKeyFile = "${credDir}/${pskCred name peer}";
|
||||
}
|
||||
) wg.peers;
|
||||
}
|
||||
) cfg.wireguard;
|
||||
|
||||
systemd.services.systemd-networkd.serviceConfig.LoadCredential = credentials;
|
||||
|
||||
systemd.network.networks = lib.mapAttrs' (
|
||||
name: wg:
|
||||
lib.nameValuePair "30-wg-${name}" {
|
||||
matchConfig.Name = wg.interface;
|
||||
address = wg.addresses;
|
||||
networkConfig = {
|
||||
ConfigureWithoutCarrier = true;
|
||||
IPv6AcceptRA = false;
|
||||
};
|
||||
# Cryptokey routing: a peer's allowedIPs are the destinations sent into the tunnel.
|
||||
routes = lib.concatMap (p: map (ip: { Destination = ip; }) p.allowedIPs) (lib.attrValues wg.peers);
|
||||
}
|
||||
) cfg.wireguard;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user