diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..5987348 --- /dev/null +++ b/default.nix @@ -0,0 +1,6 @@ +{ + imports = [ + ./hosts.nix + ./peering.nix + ]; +} \ No newline at end of file diff --git a/hosts.nix b/hosts.nix new file mode 100644 index 0000000..47b45cf --- /dev/null +++ b/hosts.nix @@ -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..subnet` is set to `internet`, + the host must be reachable at this address from the public + internet. + + If {option}`birdsong.hosts..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..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="; + }; + }; +} diff --git a/peering.nix b/peering.nix new file mode 100644 index 0000000..9832e4f --- /dev/null +++ b/peering.nix @@ -0,0 +1,91 @@ +{ 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 = "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; + }; + privateKeyFile = mkOption { + description = "Path to the private key for this peer, as generated by `wg genkey`."; + type = types.path; + }; + persistentKeepalive = mkOption { + default = null; + 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. + ''; + type = with types; nullOr int; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { + assertion = cfg ? privateKeyFile; + message = "birdsong.peering.privateKeyFile must be set"; + } + { + assertion = hostName != null; + message = "birdsong.peering.hostName or networking.hostName must be set"; + } + ]; + + networking = { + firewall.allowedUDPPorts = mkIf cfg.openPorts [ host.port ]; + + wireguard.interfaces.${cfg.interface} = { + ips = [ "${host.ipv4}/16" "${host.ipv6}/48" ] + ++ optionals host.isRouter [ "10.127.0.0/16" "fd70:81ca:0f8f::/48" ]; + privateKeyFile = cfg.privateKeyFile; + listenPort = host.port; + + peers = + let + canDirectPeer = host: peer: peer.subnet == "internet" || (host.subnet != "roaming" && peer.subnet == host.subnet); + 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); + }; + }; + }; +}