diff --git a/hive.nix b/hive.nix index 54c4f66..a4114a7 100644 --- a/hive.nix +++ b/hive.nix @@ -23,7 +23,6 @@ in { imports = [ (import "${sources.home-manager}/nixos") (import "${sources.agenix}/modules/age.nix") - (import sources.birdsong) ./pinning.nix ./common ./services diff --git a/npins/sources.json b/npins/sources.json index 1adc343..4c161a4 100644 --- a/npins/sources.json +++ b/npins/sources.json @@ -15,17 +15,6 @@ "url": "https://api.github.com/repos/ryantm/agenix/tarball/0.15.0", "hash": "01dhrghwa7zw93cybvx4gnrskqk97b004nfxgsys0736823956la" }, - "birdsong": { - "type": "Git", - "repository": { - "type": "Git", - "url": "https://git.qenya.tel/qenya/birdsong.git" - }, - "branch": "main", - "revision": "04e5519bf363388debfafc31285851c7816d087a", - "url": null, - "hash": "04xzplpbqy5lsild4amy58x0d9dbvf988d3r65grg41vy08d3ym4" - }, "home-manager": { "type": "Git", "repository": { @@ -46,15 +35,15 @@ "repo": "nix-vscode-extensions" }, "branch": "master", - "revision": "500be2a1404429cfccdb4bf71e515cc38f206a25", - "url": "https://github.com/nix-community/nix-vscode-extensions/archive/500be2a1404429cfccdb4bf71e515cc38f206a25.tar.gz", - "hash": "0w01kcnjpwb9zfsw066lnq0l84w28nbryfrdbddnl768l30rbz63" + "revision": "829828eddd52363236a53d55c40e1d4aa7af5a56", + "url": "https://github.com/nix-community/nix-vscode-extensions/archive/829828eddd52363236a53d55c40e1d4aa7af5a56.tar.gz", + "hash": "0ahiqmj36ib0fc98isgpqs9adafdgfvll60ccmryx6d6ziga0w5d" }, "nixpkgs": { "type": "Channel", "name": "nixos-24.05", - "url": "https://releases.nixos.org/nixos/24.05/nixos-24.05.3214.575f3027caa1/nixexprs.tar.xz", - "hash": "0w5kza4qrnlhsp1ls385zmf6cbkfwcxiriz69bi29zjhn2rl9gh5" + "url": "https://releases.nixos.org/nixos/24.05/nixos-24.05.2933.c716603a63ac/nixexprs.tar.xz", + "hash": "0gy2wvfwwi2jss5prhxq5c1rw321mi82c0mnki5m404j2zzzas6f" }, "nur": { "type": "Git", @@ -64,9 +53,9 @@ "repo": "NUR" }, "branch": "master", - "revision": "1002ee1f90ca51d8891642094d3a1e840d82b616", - "url": "https://github.com/nix-community/NUR/archive/1002ee1f90ca51d8891642094d3a1e840d82b616.tar.gz", - "hash": "1b1b4mdhdznbz6rz2hvwfg79x7s6ln44gpn968gyl5kc02wmaia3" + "revision": "6e46867fdecc920a1de55dc1e553a16f54e2d2ee", + "url": "https://github.com/nix-community/NUR/archive/6e46867fdecc920a1de55dc1e553a16f54e2d2ee.tar.gz", + "hash": "0vwl9svpc51x2byzn844z7q9v4hsa3hhqi8m40fj401hqdivrg3n" } }, "version": 3 diff --git a/services/birdsong/default.nix b/services/birdsong/default.nix new file mode 100644 index 0000000..5987348 --- /dev/null +++ b/services/birdsong/default.nix @@ -0,0 +1,6 @@ +{ + imports = [ + ./hosts.nix + ./peering.nix + ]; +} \ No newline at end of file diff --git a/services/birdsong/hosts.nix b/services/birdsong/hosts.nix new file mode 100644 index 0000000..47b45cf --- /dev/null +++ b/services/birdsong/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/services/birdsong/peering.nix b/services/birdsong/peering.nix new file mode 100644 index 0000000..9832e4f --- /dev/null +++ b/services/birdsong/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); + }; + }; + }; +} diff --git a/services/default.nix b/services/default.nix index 7c73723..304281d 100644 --- a/services/default.nix +++ b/services/default.nix @@ -1,5 +1,6 @@ { imports = [ + ./birdsong ./fonts.nix ./forgejo.nix ./steam.nix