birdsong: fully modularise existing wireguard config

This commit is contained in:
Katherina Walshe-Grey 2024-07-25 07:52:43 +01:00
parent 5b79e6826b
commit e90afae7ac
6 changed files with 197 additions and 52 deletions

View file

@ -3,31 +3,8 @@
{ {
age.secrets.wireguard-hub.file = ../../secrets/wireguard-hub.age; age.secrets.wireguard-hub.file = ../../secrets/wireguard-hub.age;
networking = { birdsong.peer = {
nat = { enable = true;
enable = true; privateKeyFile = config.age.secrets.wireguard-hub.path;
externalInterface = "ens3";
internalInterfaces = [ "wg0" ];
};
firewall.allowedUDPPorts = [ config.networking.wireguard.interfaces.wg0.listenPort ];
wireguard.interfaces.wg0 = {
ips = [ "10.127.1.1/24" "fd70:81ca:0f8f:1::1/64" ];
listenPort = 51820;
privateKeyFile = config.age.secrets.wireguard-hub.path;
peers = [
{
name = "shaw";
publicKey = "eD79pROC2zjhKz4tGRS43O95gcFRqO+SFb2XDnTr0zc=";
allowedIPs = [ "10.127.1.2" "fd70:81ca:0f8f:1::2" ];
}
{
name = "tohru";
publicKey = "lk3PCQM1jmZoI8sM/rWSyKNuZOUnjox3n9L9geJD+18=";
allowedIPs = [ "10.127.1.3" "fd70:81ca:0f8f:1::3" ];
}
];
};
}; };
} }

View file

@ -6,10 +6,6 @@
birdsong.peer = { birdsong.peer = {
enable = true; enable = true;
privateKeyFile = config.age.secrets.wireguard-peer-tohru.path; privateKeyFile = config.age.secrets.wireguard-peer-tohru.path;
listenPort = 51821;
persistentKeepalive = 23; persistentKeepalive = 23;
}; };
# TODO: get this from a list of peers, keyed on hostname
networking.wireguard.interfaces.birdsong.ips = [ "10.127.1.3/24" "fd70:81ca:0f8f:1::3/64" ];
} }

View file

@ -9,6 +9,13 @@
boot.loader.systemd-boot.enable = true; boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true; boot.loader.efi.canTouchEfiVariables = true;
age.secrets.wireguard-peer-yevaud.file = ../../secrets/wireguard-peer-yevaud.age;
birdsong.peer = {
enable = true;
privateKeyFile = config.age.secrets.wireguard-peer-yevaud.path;
};
qenya.services.forgejo = { qenya.services.forgejo = {
enable = true; enable = true;
domain = "git.qenya.tel"; domain = "git.qenya.tel";

View file

@ -1,5 +1,6 @@
{ {
imports = [ imports = [
./peer.nix ./peer.nix
./hosts.nix
]; ];
} }

134
services/birdsong/hosts.nix Normal file
View file

@ -0,0 +1,134 @@
{ config, lib, pkgs, ... }:
with lib;
{
options.birdsong.hosts = mkOption {
description = "List of hosts in the birdsong network";
type = types.attrsOf
(types.submodule {
options = {
hostKey = mkOption {
default = null;
description = "SSH public key of the host, for use in known_hosts files";
type = with types; nullOr str;
};
subnet = mkOption {
default = "internet";
example = "roaming";
description = ''
Identifier representing a LAN the host belongs to. Hosts in the
same LAN will peer with each other.
The special value `internet` (the default) will accept peering
from all other hosts. This is to be used for servers that are
accessible from the public internet.
The special value `roaming` will not peer with other `roaming`
hosts, but will still peer with `internet` hosts. This is to be
used for portable devices like laptops that regularly move between
networks.
'';
type = types.str;
};
endpoint = mkOption {
default = null;
example = "example.com";
description = ''
Address (e.g. IP or domain name) by which the host is reachable
within its LAN.
If {option}`birdsong.hosts.<name>.subnet` is set to `internet`,
the host must be reachable at this address from the public
internet.
If {option}`birdsong.hosts.<name>.subnet` is set to `roaming`,
this option is not used.
'';
type = with types; nullOr str;
};
ipv4 = mkOption {
example = "10.127.1.1";
description = "IPv4 address of this peer within the network";
type = types.str;
};
ipv6 = mkOption {
example = "fd70:81ca:0f8f:1::1";
description = "IPv6 address of this peer within the network";
type = types.str;
};
port = mkOption {
default = 51820;
example = 51821;
description = ''
Which port to expose WireGuard on. Change this for peers behind
NAT, to a port not used by another peer in the same LAN.
'';
type = types.port;
};
wireguardKey = mkOption {
example = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=";
description = "WireGuard public key for this peer, as generated by `wg pubkey`";
type = types.str;
};
isRouter = mkOption {
default = false;
description = ''
The host with this flag set is the subnet router. It forwards
packets between WireGuard peers that can't connect directly to
each other. WireGuard's scope doesn't (yet) include full mesh
networking with load-balancing between routers, so only one peer
can hold this status. It should be peered with all other hosts
(i.e., {option}`birdsong.hosts.<name>.subnet` set to `internet`).
'';
type = types.bool;
};
};
});
};
config.birdsong.hosts = {
yevaud = {
hostKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICHUAgyQhl390yUObLUI+jEbuNrZ2U6+8px628DolD+T root@yevaud";
endpoint = "yevaud.birdsong.network";
ipv4 = "10.127.1.1";
ipv6 = "fd70:81ca:0f8f:1::1";
wireguardKey = "YPJsIs9x4wuWdFi/QRWSJbWvKE0GQAfVL4MNMqHygDw=";
isRouter = true;
};
orm = {
hostKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGc9rkcdOVWozBFj3kLVnSyUQQbyyH+UG+bLawanQkRQ root@orm";
endpoint = "orm.birdsong.network";
ipv4 = "10.127.1.2";
ipv6 = "fd70:81ca:0f8f:1::2";
wireguardKey = "birdLVh8roeZpcVo308Ums4l/aibhAxbi7MBsglkJyA=";
};
tohru = {
hostKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOk8wuGzF0Y7SaH9aimo3SmCz99MTQwL+rEVhx0jsueU root@tohru";
subnet = "roaming";
ipv4 = "10.127.2.1";
ipv6 = "fd70:81ca:0f8f:2::1";
port = 51821;
wireguardKey = "lk3PCQM1jmZoI8sM/rWSyKNuZOUnjox3n9L9geJD+18=";
};
# kilgharrah = {
# # hostKey = "";
# subnet = "weyrhold";
# endpoint = "192.168.2.1";
# ipv4 = "10.127.3.1";
# ipv6 = "fd70:81ca:0f8f:3::1";
# # wireguardKey = "";
# };
shaw = {
hostKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMC0AomCZZiUV/BCpImiV4p/vGvFaz5QNc+fJLXmS5p root@shaw";
subnet = "library";
# endpoint = "";
ipv4 = "10.127.4.1";
ipv6 = "fd70:81ca:0f8f:4::1";
wireguardKey = "eD79pROC2zjhKz4tGRS43O95gcFRqO+SFb2XDnTr0zc=";
};
};
}

View file

@ -3,12 +3,25 @@
with lib; with lib;
let let
cfg = config.birdsong.peer; cfg = config.birdsong.peer;
hostName = if null != cfg.hostName then cfg.hostName else config.networking.hostName;
hosts = config.birdsong.hosts;
host = hosts.${hostName};
in in
{ {
options.birdsong.peer = { options.birdsong.peer = {
enable = mkEnableOption "WireGuard peering with the birdsong network"; 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 { interface = mkOption {
default = "birdsong"; default = "birdsong";
example = "wg0";
description = "The name of the network interface to use for WireGuard."; description = "The name of the network interface to use for WireGuard.";
type = types.str; type = types.str;
}; };
@ -21,40 +34,57 @@ in
description = "Path to the private key for this peer, as generated by `wg genkey`."; description = "Path to the private key for this peer, as generated by `wg genkey`.";
type = types.path; type = types.path;
}; };
listenPort = mkOption {
default = 51820;
example = 51821;
description = "Which port to expose WireGuard on. Change this if you are behind NAT, to a port not used by another peer in the same LAN.";
type = types.port;
};
persistentKeepalive = mkOption { persistentKeepalive = mkOption {
default = null; default = null;
example = 23; example = 23;
description = "Constantly ping the hub 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. To avoid syncing, this should ideally be a prime number that is not shared by another peer in the same LAN."; description = ''
type = types.nullOr types.int; 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.
'';
type = with types; nullOr int;
}; };
}; };
config = mkIf cfg.enable { config = mkIf cfg.enable {
assertions = [{ assertions = [
assertion = cfg.privateKeyFile != null; {
message = "birdsong.peer.privateKeyFile must be set"; assertion = cfg ? privateKeyFile;
}]; message = "birdsong.peer.privateKeyFile must be set";
}
{
assertion = hostName != null;
message = "birdsong.peer.hostName or networking.hostName must be set";
}
];
networking = { networking = {
firewall.allowedUDPPorts = mkIf cfg.openPorts [ cfg.listenPort ]; firewall.allowedUDPPorts = mkIf cfg.openPorts [ host.port ];
wireguard.interfaces.${cfg.interface} = { wireguard.interfaces.${cfg.interface} = {
listenPort = cfg.listenPort; ips = [ "${host.ipv4}/16" "${host.ipv6}/48" ]
++ optionals host.isRouter [ "10.127.0.0/16" "fd70:81ca:0f8f::/48" ];
privateKeyFile = cfg.privateKeyFile; privateKeyFile = cfg.privateKeyFile;
peers = [ listenPort = host.port;
{
publicKey = "birdLVh8roeZpcVo308Ums4l/aibhAxbi7MBsglkJyA="; peers =
allowedIPs = [ "10.127.1.0/24" "fd70:81ca:0f8f:1::/64" ]; let
endpoint = "birdsong.network:51820"; canDirectPeer = host: peer: peer.subnet == "internet" || (host.subnet != "roaming" && peer.subnet == host.subnet);
persistentKeepalive = cfg.persistentKeepalive; in
} mapAttrsToList
]; (name: peer: {
name = name;
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}";
dynamicEndpointRefreshSeconds = mkIf (canDirectPeer host peer) 5;
persistentKeepalive = mkIf (peer.subnet != host.subnet) cfg.persistentKeepalive;
})
(filterAttrs (name: peer: peer != host && (host.subnet == "internet" || canDirectPeer host peer)) hosts);
}; };
}; };
}; };