systemd-networkd resolves an endpoint name once, when it creates the device. A peer behind a dynamic DNS name that changes its address was never found again until the next restart. A timer (router.wireguardEndpointRefresh, default 1min, null for off) runs wg set for the peers with a name whose last handshake is older than 135 seconds, as wireguard-tools' reresolve-dns does. Working tunnels are left alone. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
882 lines
33 KiB
Nix
882 lines
33 KiB
Nix
{ config, lib, ... }:
|
|
let
|
|
inherit (lib) mkOption mkEnableOption types;
|
|
rlib = import ./lib.nix { inherit lib; };
|
|
in
|
|
{
|
|
options.router = {
|
|
enable = mkEnableOption "the router role";
|
|
|
|
interfaces = mkOption {
|
|
default = { };
|
|
example = {
|
|
wan.mac = "52:54:00:12:34:56";
|
|
};
|
|
description = ''
|
|
Names for physical interfaces, keyed by the name to give: the network
|
|
card with the given MAC address is called that from the next boot on
|
|
(`wan.interface` and `lans.*.interface` use these names). Interfaces
|
|
that are not listed keep the kernel's name. Names cannot change while
|
|
the system runs, so a change only takes effect after a reboot.
|
|
'';
|
|
type = types.attrsOf (
|
|
types.submodule {
|
|
options.mac = mkOption {
|
|
type = types.strMatching "([0-9a-f]{2}:){5}[0-9a-f]{2}";
|
|
example = "52:54:00:12:34:56";
|
|
description = "Permanent MAC address of the network card, in lower case.";
|
|
};
|
|
}
|
|
);
|
|
};
|
|
|
|
wan = {
|
|
interface = mkOption {
|
|
type = types.str;
|
|
example = "eth0";
|
|
description = ''
|
|
Physical uplink interface. Native IPv6 (DHCPv6) runs on it untagged;
|
|
with PPPoE it also carries the (optionally tagged) PPPoE session.
|
|
|
|
The link may also carry LANs ("router on a stick") when the uplink
|
|
is PPPoE on a VLAN and IPv6 is disabled: the untagged link is then
|
|
free for a LAN and only the PPP device counts as an uplink.
|
|
'';
|
|
};
|
|
|
|
ipv4 = {
|
|
type = mkOption {
|
|
type = types.enum [
|
|
"dhcp"
|
|
"pppoe"
|
|
];
|
|
default = "dhcp";
|
|
description = "How the router gets its IPv4 uplink.";
|
|
};
|
|
|
|
pppoe = {
|
|
vlan = mkOption {
|
|
type = types.nullOr (types.ints.between 1 4094);
|
|
default = null;
|
|
description = "VLAN ID the ISP requires for PPPoE, or null for untagged.";
|
|
};
|
|
vlanName = mkOption {
|
|
type = types.nullOr types.str;
|
|
default = null;
|
|
example = "isp";
|
|
description = "Name of the VLAN device of the PPPoE session; null: `<interface>.<vlan>`.";
|
|
};
|
|
username = mkOption {
|
|
type = types.str;
|
|
default = "";
|
|
description = "PPP authentication username.";
|
|
};
|
|
passwordFile = mkOption {
|
|
type = types.nullOr (
|
|
types.pathWith {
|
|
absolute = true;
|
|
inStore = false;
|
|
}
|
|
);
|
|
default = null;
|
|
example = "/var/lib/secrets/pppoe-password";
|
|
description = ''
|
|
File containing the PPP password, read at service start. Must not
|
|
be in the Nix store (it would be world-readable).
|
|
'';
|
|
};
|
|
mtu = mkOption {
|
|
type = types.ints.between 576 1500;
|
|
default = 1492;
|
|
description = "PPP MTU/MRU; TCP MSS is clamped to match on forwarded traffic.";
|
|
};
|
|
};
|
|
|
|
interface = mkOption {
|
|
type = types.str;
|
|
readOnly = true;
|
|
internal = true;
|
|
description = "Device IPv4 traffic leaves through (derived).";
|
|
};
|
|
};
|
|
|
|
ipv6 = {
|
|
enable = mkEnableOption ''
|
|
IPv6 on the uplink: DHCPv6 (soliciting even without RA flags) plus
|
|
prefix delegation. The ISP is expected to delegate a single /64
|
|
'';
|
|
|
|
transport = mkOption {
|
|
type = types.enum [
|
|
"native"
|
|
"ppp"
|
|
];
|
|
default = "native";
|
|
description = ''
|
|
Where IPv6 runs. `native`: untagged on the uplink link, next to a
|
|
DHCPv4 or PPPoE (tagged) IPv4 uplink. `ppp`: inside the PPPoE
|
|
session (IPv6CP), so DHCPv6 and router advertisements arrive on
|
|
the PPP device; needs a PPPoE uplink, and leaves the physical link
|
|
free for a LAN.
|
|
'';
|
|
};
|
|
};
|
|
};
|
|
|
|
lans = mkOption {
|
|
default = { };
|
|
description = "LAN segments served by this router, keyed by name.";
|
|
type = types.attrsOf (
|
|
types.submodule (
|
|
{ name, config, ... }:
|
|
{
|
|
options = {
|
|
interface = mkOption {
|
|
type = types.str;
|
|
example = "eth1";
|
|
description = ''
|
|
Interface carrying this LAN. With `vlan` set this is the
|
|
parent (trunk) interface on which the VLAN device is created.
|
|
'';
|
|
};
|
|
|
|
vlan = mkOption {
|
|
type = types.nullOr (types.ints.between 1 4094);
|
|
default = null;
|
|
description = "802.1Q VLAN ID, or null for an untagged LAN on `interface`.";
|
|
};
|
|
|
|
deviceName = mkOption {
|
|
type = types.nullOr types.str;
|
|
default = null;
|
|
example = "dmz";
|
|
description = ''
|
|
Name of the VLAN device (only with `vlan`); null: `<interface>.<vlan>`.
|
|
Like `interfaces`, a changed name is safe only after a reboot.
|
|
'';
|
|
};
|
|
|
|
device = mkOption {
|
|
type = types.str;
|
|
readOnly = true;
|
|
internal = true;
|
|
description = "Device the LAN's addresses live on (derived).";
|
|
};
|
|
|
|
zone = mkOption {
|
|
type = types.str;
|
|
default = name;
|
|
description = ''
|
|
Firewall zone this LAN belongs to. LANs sharing a zone may
|
|
talk to each other freely; traffic between zones is denied
|
|
unless a rule in `router.firewall.rules` allows it. Defaults
|
|
to a zone named after the LAN.
|
|
'';
|
|
};
|
|
|
|
internet = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
description = "Whether this LAN may reach the internet (opt-in).";
|
|
};
|
|
|
|
ipv4 = {
|
|
address = mkOption {
|
|
type = types.str;
|
|
example = "192.168.10.1";
|
|
description = "Router address on this LAN.";
|
|
};
|
|
prefixLength = mkOption {
|
|
type = types.ints.between 8 30;
|
|
default = 24;
|
|
};
|
|
subnet = mkOption {
|
|
type = types.str;
|
|
readOnly = true;
|
|
description = "Network in CIDR notation, derived from `address` and `prefixLength`.";
|
|
};
|
|
};
|
|
|
|
dhcp = {
|
|
enable = mkOption {
|
|
type = types.bool;
|
|
default = true;
|
|
};
|
|
pool = {
|
|
start = mkOption {
|
|
type = types.str;
|
|
example = "192.168.10.100";
|
|
};
|
|
end = mkOption {
|
|
type = types.str;
|
|
example = "192.168.10.199";
|
|
};
|
|
};
|
|
reservations = mkOption {
|
|
type = types.attrsOf (
|
|
types.submodule {
|
|
options = {
|
|
mac = mkOption {
|
|
type = types.str;
|
|
example = "a8:a1:59:0b:4b:97";
|
|
description = "Hardware address of the client.";
|
|
};
|
|
address = mkOption {
|
|
type = types.str;
|
|
example = "192.168.10.20";
|
|
description = "Address handed to the client; must lie inside the LAN.";
|
|
};
|
|
};
|
|
}
|
|
);
|
|
default = { };
|
|
description = ''
|
|
Fixed leases, keyed by hostname. The name is also sent to the
|
|
client and published in DNS (under `router.dns.domain` if set).
|
|
'';
|
|
};
|
|
};
|
|
|
|
ipv6.pd = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
description = ''
|
|
Whether this LAN receives the /64 delegated on the uplink
|
|
(assigned to the interface and announced via SLAAC). A /64
|
|
cannot be split, so at most one LAN may set this; the others
|
|
stay IPv4-only.
|
|
'';
|
|
};
|
|
|
|
ipv6.address = mkOption {
|
|
type = types.nullOr types.str;
|
|
default = null;
|
|
example = "fd2c:cdef:cde9::1/64";
|
|
description = ''
|
|
Static unique-local (ULA) address of the router on this LAN,
|
|
as a /64. It is announced via SLAAC, so hosts get a stable
|
|
internal IPv6 address even without a delegated prefix, and
|
|
the router's resolver answers on it (also advertised as
|
|
RDNSS). Independent of `ipv6.pd`.
|
|
'';
|
|
};
|
|
|
|
allowManagement = mkOption {
|
|
type = types.bool;
|
|
default = true;
|
|
description = "Whether clients on this LAN may reach SSH on the router.";
|
|
};
|
|
};
|
|
|
|
config.device =
|
|
if config.vlan == null then
|
|
config.interface
|
|
else if config.deviceName != null then
|
|
config.deviceName
|
|
else
|
|
"${config.interface}.${toString config.vlan}";
|
|
|
|
# "invalid" is only ever seen when assertions.nix is already failing.
|
|
config.ipv4.subnet =
|
|
if rlib.ipv4.parse config.ipv4.address == null then
|
|
"invalid"
|
|
else
|
|
rlib.ipv4.cidr config.ipv4.address config.ipv4.prefixLength;
|
|
}
|
|
)
|
|
);
|
|
};
|
|
|
|
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).
|
|
'';
|
|
};
|
|
sourceAddresses = mkOption {
|
|
type = types.listOf types.str;
|
|
default = [ ];
|
|
example = [ "192.168.10.1" ];
|
|
description = ''
|
|
Source address (at most one per family) for traffic the router
|
|
itself sends into this tunnel, e.g. DNS queries to the other
|
|
site. Needed when the tunnel has no address of its own, so the
|
|
other side sees a source it routes back instead of whatever the
|
|
kernel picks. IPv4 ones must be an address the router holds on
|
|
a LAN or a tunnel.
|
|
'';
|
|
};
|
|
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;
|
|
};
|
|
};
|
|
}
|
|
);
|
|
};
|
|
};
|
|
}
|
|
)
|
|
);
|
|
};
|
|
|
|
wireguardEndpointRefresh = mkOption {
|
|
type = types.nullOr (types.strMatching "[0-9]+(s|min|h)");
|
|
default = "1min";
|
|
example = "5min";
|
|
description = ''
|
|
How often the names in WireGuard peers' `endpoint` are looked up
|
|
again, for peers that have not completed a handshake in the last two
|
|
minutes. systemd-networkd resolves a name only when it creates the
|
|
tunnel, so without this a tunnel whose peer got a new address (dynamic
|
|
DNS) stays down until the peer contacts the router, or forever if both
|
|
ends changed. A tunnel that works is not touched. null turns it off.
|
|
'';
|
|
};
|
|
|
|
firewall.zones = mkOption {
|
|
type = types.listOf types.str;
|
|
default = [ ];
|
|
example = [ "guests" ];
|
|
description = ''
|
|
Zones to define besides those of the LANs and tunnels (`zone` there
|
|
assigns them), so rules can name them before anything is assigned. A
|
|
zone without members matches nothing.
|
|
'';
|
|
};
|
|
|
|
firewall.nat = mkOption {
|
|
default = [ ];
|
|
description = ''
|
|
Address translation, separate from the allow rules. `dnat` sends
|
|
traffic that arrives in zone `from` (`wan` for the uplink) and matches
|
|
the rule to `toAddress` (a host in zone `to`); `snat` and `masquerade`
|
|
rewrite the source of traffic leaving through zone `to` (`wan` for the
|
|
uplink) to `toAddress`, or to the address of the outgoing interface.
|
|
LANs with `internet = true` and tunnels with `masquerade` are already
|
|
masqueraded; these rules come first. A translation does not allow
|
|
anything: a forward needs a matching entry in `firewall.rules`.
|
|
'';
|
|
type = types.listOf (
|
|
types.submodule {
|
|
options = {
|
|
type = mkOption {
|
|
type = types.enum [
|
|
"dnat"
|
|
"snat"
|
|
"masquerade"
|
|
];
|
|
description = "Kind of translation.";
|
|
};
|
|
from = mkOption {
|
|
type = types.nullOr types.str;
|
|
default = null;
|
|
description = "Zone the traffic comes from, or `wan`. For `snat` and `masquerade` null means any.";
|
|
};
|
|
to = mkOption {
|
|
type = types.str;
|
|
description = ''
|
|
`dnat`: the zone of the host the traffic is sent to. `snat` and
|
|
`masquerade`: the zone (or `wan`) the traffic leaves through.
|
|
'';
|
|
};
|
|
protocol = mkOption {
|
|
type = types.enum [
|
|
"any"
|
|
"tcp"
|
|
"udp"
|
|
"icmp"
|
|
];
|
|
default = "any";
|
|
};
|
|
ports = mkOption {
|
|
type = types.listOf (types.either types.port (types.strMatching "[0-9]+-[0-9]+"));
|
|
default = [ ];
|
|
description = "Destination ports or ranges to match (tcp/udp only); empty means all.";
|
|
};
|
|
sourcePorts = mkOption {
|
|
type = types.listOf (types.either types.port (types.strMatching "[0-9]+-[0-9]+"));
|
|
default = [ ];
|
|
description = "Source ports or ranges to match (tcp/udp only).";
|
|
};
|
|
sources = mkOption {
|
|
type = types.listOf types.str;
|
|
default = [ ];
|
|
description = "Only match traffic from these IPv4 addresses or CIDRs; empty means any.";
|
|
};
|
|
destination = mkOption {
|
|
type = types.nullOr types.str;
|
|
default = null;
|
|
example = "203.0.113.7";
|
|
description = ''
|
|
Only match traffic addressed to this IPv4 address or CIDR.
|
|
For `dnat` null means the router's own addresses (from `wan`:
|
|
anything that arrives on the uplink).
|
|
'';
|
|
};
|
|
toAddress = mkOption {
|
|
type = types.nullOr types.str;
|
|
default = null;
|
|
example = "192.168.10.5";
|
|
description = "`dnat`: the host to send the traffic to. `snat`: the source address to use. Not for `masquerade`.";
|
|
};
|
|
toPort = mkOption {
|
|
type = types.nullOr types.port;
|
|
default = null;
|
|
description = "`dnat`: the port on the host when it differs (needs exactly one `ports` entry).";
|
|
};
|
|
hairpin = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
description = ''
|
|
`dnat` from `wan` only: let LANs with `internet = true` reach
|
|
the forward through the router's public address (NAT
|
|
loopback, the source is masqueraded) and allow it to the host.
|
|
'';
|
|
};
|
|
};
|
|
}
|
|
);
|
|
};
|
|
|
|
firewall.rules = mkOption {
|
|
default = [ ];
|
|
description = ''
|
|
Allow rules between zones, on top of the default (deny between zones).
|
|
Rules are one-directional; return traffic is allowed by connection
|
|
tracking. `to` must be the zone of a LAN or tunnel, or the reserved
|
|
zone `local`, the router itself (SSH, the web UI, ...); `from` may also
|
|
be the reserved zone `wan` (traffic arriving on the uplinks), which
|
|
publishes services to the outside: such rules must name a
|
|
`destination` (not for `local`) and protocol (and ports for tcp/udp),
|
|
so a typo cannot expose a whole zone. A port forward on the same port
|
|
takes the traffic first, so it never reaches `local`. Address translation is separate (`firewall.nat`);
|
|
a port forward needs an allow rule as well, usually with `natOnly`.
|
|
'';
|
|
type = types.listOf (
|
|
types.submodule {
|
|
options = {
|
|
from = mkOption {
|
|
type = types.str;
|
|
description = "Source zone, or `wan`.";
|
|
};
|
|
to = mkOption {
|
|
type = types.str;
|
|
description = "Destination zone, or `local` (the router itself).";
|
|
};
|
|
protocol = mkOption {
|
|
type = types.enum [
|
|
"any"
|
|
"tcp"
|
|
"udp"
|
|
"icmp"
|
|
];
|
|
default = "any";
|
|
};
|
|
ports = mkOption {
|
|
type = types.listOf (types.either types.port (types.strMatching "[0-9]+-[0-9]+"));
|
|
default = [ ];
|
|
example = [
|
|
443
|
|
"50000-50100"
|
|
];
|
|
description = ''
|
|
Destination ports or ranges (tcp/udp only); empty means all
|
|
ports. After a port forward these are the translated ports
|
|
(its `toPort`, if it has one).
|
|
'';
|
|
};
|
|
sourcePorts = mkOption {
|
|
type = types.listOf (types.either types.port (types.strMatching "[0-9]+-[0-9]+"));
|
|
default = [ ];
|
|
example = [ 53 ];
|
|
description = "Source ports or ranges (tcp/udp only); empty means any.";
|
|
};
|
|
destination = mkOption {
|
|
type = types.nullOr types.str;
|
|
default = null;
|
|
example = "192.168.10.5";
|
|
description = ''
|
|
Restrict the rule to an IPv4 address or CIDR inside the
|
|
destination zone; null means the whole zone. IPv6 addresses
|
|
are accepted too (for pinholes) but cannot be checked against
|
|
the zone, because delegated prefixes are not known at build time.
|
|
'';
|
|
};
|
|
sources = mkOption {
|
|
type = types.listOf types.str;
|
|
default = [ ];
|
|
example = [ "198.51.100.0/24" ];
|
|
description = "Only match traffic from these IPv4/IPv6 addresses or CIDRs; empty means any source.";
|
|
};
|
|
natOnly = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
description = ''
|
|
Only match connections that a `firewall.nat` port forward
|
|
translated (the allow rule that goes with a forward), not
|
|
traffic addressed directly to the destination.
|
|
'';
|
|
};
|
|
rateLimit = mkOption {
|
|
type = types.nullOr (types.strMatching "[0-9]+/(second|minute|hour)");
|
|
default = null;
|
|
example = "10/minute";
|
|
description = "Limit new connections per source address; the excess is dropped.";
|
|
};
|
|
log = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
description = "Log new connections accepted by this rule, and connections dropped by `rateLimit` (kernel log).";
|
|
};
|
|
};
|
|
}
|
|
);
|
|
};
|
|
|
|
dnsProviders = mkOption {
|
|
default = { };
|
|
description = ''
|
|
Accounts at DNS providers, by name. Dynamic DNS (`ddns.provider`) and
|
|
certificates (`acme.provider`) use them, so a token is kept once.
|
|
'';
|
|
type = types.attrsOf (
|
|
types.submodule {
|
|
options = {
|
|
type = mkOption {
|
|
type = types.str;
|
|
example = "cloudflare";
|
|
description = ''
|
|
The provider. For dynamic DNS the ddclient protocol
|
|
(<https://ddclient.net/protocols.html>); for certificates the
|
|
provider code of lego (<https://go-acme.github.io/lego/dns>).
|
|
The names are the same where both know the provider.
|
|
The lists are in `pkgs/router-webui/providers.json`.
|
|
'';
|
|
};
|
|
username = mkOption {
|
|
type = types.str;
|
|
default = "token";
|
|
description = "Login for dynamic DNS; `token` for an API token (Cloudflare).";
|
|
};
|
|
server = mkOption {
|
|
type = types.nullOr types.str;
|
|
default = null;
|
|
example = "dynupdate.no-ip.com";
|
|
description = "The provider's update server, for protocols that need one (`dyndns2`).";
|
|
};
|
|
secretFile = mkOption {
|
|
type = types.nullOr types.str;
|
|
default = null;
|
|
example = "/var/lib/router/dns-secrets/cloudflare";
|
|
description = ''
|
|
File with the password or API token (dynamic DNS; for
|
|
certificates through cloudflare, desec, digitalocean, duckdns
|
|
and hetzner the same token). Never a path in the Nix store.
|
|
'';
|
|
};
|
|
environmentFile = mkOption {
|
|
type = types.nullOr types.str;
|
|
default = null;
|
|
example = "/var/lib/router/dns-secrets/route53.env";
|
|
description = ''
|
|
File with `VARIABLE=value` lines for lego, for certificates
|
|
through providers other than those above; the variables are
|
|
listed at <https://go-acme.github.io/lego/dns>. Never a path in the Nix store.
|
|
'';
|
|
};
|
|
};
|
|
}
|
|
);
|
|
};
|
|
|
|
acme = {
|
|
enable = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
description = ''
|
|
A certificate from Let's Encrypt for the web UI, proven by a DNS
|
|
record (DNS-01, no port open to the internet), renewed automatically.
|
|
'';
|
|
};
|
|
provider = mkOption {
|
|
type = types.nullOr types.str;
|
|
default = null;
|
|
example = "cloudflare";
|
|
description = "The name of a connection in `dnsProviders` that can change the records of the domains.";
|
|
};
|
|
domains = mkOption {
|
|
type = types.listOf types.str;
|
|
default = [ ];
|
|
example = [ "router.example.org" ];
|
|
description = "Names in the certificate; the first is its name. A wildcard like `*.home.example.org` is possible.";
|
|
};
|
|
email = mkOption {
|
|
type = types.nullOr types.str;
|
|
default = null;
|
|
description = "Where Let's Encrypt writes about expiring certificates.";
|
|
};
|
|
dnsResolver = mkOption {
|
|
type = types.nullOr types.str;
|
|
default = "1.1.1.1:53";
|
|
example = "9.9.9.9:53";
|
|
description = ''
|
|
Resolver (`address:port`) that lego asks to find the zone's name
|
|
servers and to check that the challenge record has spread. Without
|
|
one it would use the router's own Unbound, which answers the local
|
|
domain itself and caches. `null`: the router's resolver.
|
|
'';
|
|
};
|
|
staging = mkOption {
|
|
type = types.bool;
|
|
default = true;
|
|
description = "Use the staging service: certificates that browsers do not trust, for trying it out.";
|
|
};
|
|
acceptTerms = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
description = "Accept the Let's Encrypt subscriber agreement.";
|
|
};
|
|
};
|
|
|
|
ddns = {
|
|
enable = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
description = ''
|
|
Keep DNS records at a provider pointing at the router's uplink
|
|
addresses (ddclient). It checks every `interval` and as soon as an
|
|
uplink address changes.
|
|
'';
|
|
};
|
|
provider = mkOption {
|
|
type = types.nullOr types.str;
|
|
default = null;
|
|
example = "cloudflare";
|
|
description = "The name of a connection in `dnsProviders` to update the records through.";
|
|
};
|
|
zone = mkOption {
|
|
type = types.nullOr types.str;
|
|
default = null;
|
|
example = "example.org";
|
|
description = "The zone the names are in, for providers that need it (Cloudflare).";
|
|
};
|
|
domains = mkOption {
|
|
type = types.listOf types.str;
|
|
default = [ ];
|
|
example = [ "home.example.org" ];
|
|
description = "Names to keep up to date.";
|
|
};
|
|
ipv4 = mkOption {
|
|
type = types.bool;
|
|
default = true;
|
|
description = "Update A records with the IPv4 address of the uplink.";
|
|
};
|
|
ipv6 = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
description = "Update AAAA records with the IPv6 address of the uplink.";
|
|
};
|
|
ssl = mkOption {
|
|
type = types.bool;
|
|
default = true;
|
|
description = "Talk to the provider over HTTPS.";
|
|
};
|
|
interval = mkOption {
|
|
type = types.str;
|
|
default = "5min";
|
|
description = "How often to check, besides when an address changes (systemd time span).";
|
|
};
|
|
};
|
|
|
|
managementPorts = mkOption {
|
|
type = types.listOf types.port;
|
|
default = [ ];
|
|
example = [ 443 ];
|
|
description = ''
|
|
TCP ports on the router, besides SSH, reachable from LANs and tunnels
|
|
with `allowManagement` (the web UI adds its own).
|
|
'';
|
|
};
|
|
|
|
dns.domain = mkOption {
|
|
type = types.nullOr types.str;
|
|
default = null;
|
|
example = "home.example.org";
|
|
description = ''
|
|
Local domain. It is authoritative on the router: names below it that
|
|
have no record answer NXDOMAIN and are never forwarded. Short names in
|
|
`dns.hosts` and DHCP reservation names live in it.
|
|
'';
|
|
};
|
|
|
|
dns.hosts = mkOption {
|
|
type = types.attrsOf (types.listOf types.str);
|
|
default = { };
|
|
example = {
|
|
nas = [ "192.168.10.20" ];
|
|
"git.example.org" = [
|
|
"10.43.0.50"
|
|
"fd00::50"
|
|
];
|
|
};
|
|
description = ''
|
|
Local records: name to IPv4/IPv6 addresses. A name without a dot is
|
|
placed in `dns.domain`; a full name overrides public DNS for that
|
|
name only (split horizon). Reverse records are created as well.
|
|
'';
|
|
};
|
|
|
|
dns.overrides = mkOption {
|
|
type = types.attrsOf (types.listOf types.str);
|
|
default = { };
|
|
example = {
|
|
"k8s.example.org" = [ "10.43.0.50" ];
|
|
};
|
|
description = "Like `dns.hosts`, but the name and everything below it answer with these addresses (wildcard).";
|
|
};
|
|
|
|
dns.dynamicLeases = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
description = ''
|
|
Publish the host names of DHCP clients as `<name>.<dns.domain>` (with
|
|
reverse records), updated as leases come and go. Needs `dns.domain`.
|
|
|
|
A client picks its own name, so only its first label is used, only
|
|
under the local domain, and a name that is configured here (`dns.hosts`,
|
|
a reservation, the router itself, a wildcard or a forward zone it falls
|
|
under) is never taken over. Of two clients claiming one name the newer
|
|
lease wins. Run by `router-webui leases-dns` (the settings module).
|
|
'';
|
|
};
|
|
|
|
dns.forwardZones = mkOption {
|
|
type = types.attrsOf (types.listOf types.str);
|
|
default = { };
|
|
example = {
|
|
"branch.example.org" = [ "10.1.0.254" ];
|
|
};
|
|
description = ''
|
|
Zones answered by other resolvers (e.g. across a tunnel). DNSSEC
|
|
validation is skipped for them: they are private zones.
|
|
'';
|
|
};
|
|
|
|
dns.forwarders = mkOption {
|
|
type = types.listOf types.str;
|
|
default = [ ];
|
|
description = "Upstream resolvers. When empty, Unbound resolves recursively.";
|
|
};
|
|
};
|
|
|
|
config.router.wan.ipv4.interface =
|
|
if config.router.wan.ipv4.type == "pppoe" then "pppoe-wan" else config.router.wan.interface;
|
|
}
|