birdsong/peering.nix

127 lines
4.4 KiB
Nix

{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.birdsong.peering;
hostName = if null != cfg.hostName then cfg.hostName else config.networking.hostName;
hosts = config.birdsong.hosts;
host = hosts.${hostName};
in
{
options.birdsong.peering = {
enable = mkEnableOption "WireGuard peering with the birdsong network";
hostName = mkOption {
default = null;
description = ''
The hostname of this peer within the network. Must be listed in
{option}`birdsong.hosts`. If not set, defaults to
{option}`networking.hostName`.
'';
type = with types; nullOr str;
};
interface = mkOption {
default = "wg-birdsong";
example = "wg0";
description = "The name of the network interface to use for WireGuard.";
type = types.str;
};
openPorts = mkOption {
default = true;
description = "Whether to automatically open firewall ports.";
type = types.bool;
};
privateKey = mkOption {
description = ''
Private key for this peer, as generated by `wg genkey`. For security,
it is recommended to use {option}`birdsong.peering.privateKeyFile`
instead, or use `@`-syntax to read a systemd credential; see
systemd.netdev(5) for more details.
'';
type = types.str;
};
privateKeyFile = mkOption {
description = ''
Path to the private key for this peer, as generated by `wg genkey`. Must
be readable by the user "systemd-network"; see systemd.netdev(5) for
more details.
'';
type = types.path;
};
persistentKeepalive = mkOption {
default = 0;
example = 23;
description = ''
Constantly ping each peer outside the LAN this often, in seconds, in
order to keep the WireGuard tunnel open. Set this if you are behind NAT
to keep the NAT session active, or if you have a dynamic IP to keep the
other peers aware when your IP changes. To avoid syncing, this should
ideally be a prime number that is not shared by another peer in the same
LAN. 0 (the default) disables this.
'';
type = types.int;
};
};
config = mkIf cfg.enable {
assertions = [
{
assertion = config.systemd.network.enable == true;
message = "birdsong depends on networkd. systemd.network.enable must be true";
}
{
assertion = cfg ? privateKey || cfg ? privateKeyFile;
message = "birdsong.peering.privateKey or birdsong.peering.privateKeyFile must be set";
}
{
assertion = hostName != null;
message = "birdsong.peering.hostName or networking.hostName must be set";
}
];
boot.kernel.sysctl = mkIf host.isRouter {
"net.ipv4.conf.${cfg.interface}.forwarding" = true;
"net.ipv6.conf.${cfg.interface}.forwarding" = true;
};
networking.firewall.allowedUDPPorts = mkIf cfg.openPorts [ host.port ];
systemd.network = {
enable = true;
netdevs."30-birdsong" = {
netdevConfig = {
Name = cfg.interface;
Kind = "wireguard";
Description = "wireguard tunnel to the birdsong network";
};
wireguardConfig = {
PrivateKey = mkIf (cfg ? privateKey) cfg.privateKey;
PrivateKeyFile = mkIf (cfg ? privateKeyFile) cfg.privateKeyFile;
ListenPort = host.port;
};
wireguardPeers =
let
canDirectPeer = host: peer: peer.subnet == "internet" || (host.subnet != "roaming" && peer.subnet == host.subnet);
in
mapAttrsToList
(name: peer: {
wireguardPeerConfig = {
PublicKey = peer.wireguardKey;
AllowedIPs = [ peer.ipv4 peer.ipv6 ]
++ optionals peer.isRouter [ "10.127.0.0/16" "fd70:81ca:0f8f::/48" ];
Endpoint = mkIf (canDirectPeer host peer) "${peer.endpoint}:${toString peer.port}";
PersistentKeepalive = mkIf (peer.subnet != host.subnet) cfg.persistentKeepalive;
};
})
(filterAttrs (name: peer: peer != host && (host.subnet == "internet" || canDirectPeer host peer)) hosts);
};
networks."30-birdsong" = {
matchConfig.Name = cfg.interface;
networkConfig.Address = [ "${host.ipv4}/16" "${host.ipv6}/48" ]
++ optionals host.isRouter [ "10.127.0.0/16" "fd70:81ca:0f8f::/48" ];
};
};
};
}