From 252abe9443a4263b62469434138fe6e240e525cf Mon Sep 17 00:00:00 2001 From: SebastianStork Date: Sun, 11 Jan 2026 19:13:30 +0100 Subject: [PATCH] Create networking abstraction on top of nebula --- flake-parts/hosts.nix | 2 +- hosts/desktop/default.nix | 13 +- hosts/laptop/default.nix | 13 +- hosts/vps-monitor/default.nix | 24 ++-- hosts/vps-private/default.nix | 24 ++-- hosts/vps-public/default.nix | 20 ++- modules/system/networking.nix | 90 +++++++++++++ modules/system/services/caddy.nix | 7 +- modules/system/services/{nebula => }/dns.nix | 24 ++-- modules/system/services/nebula/default.nix | 123 +++++------------- modules/system/services/{nebula => }/sshd.nix | 27 ++-- modules/system/services/syncthing.nix | 9 +- modules/system/web-services/alloy.nix | 4 +- modules/system/web-services/gatus.nix | 2 +- modules/system/web-services/grafana.nix | 6 +- 15 files changed, 223 insertions(+), 165 deletions(-) create mode 100644 modules/system/networking.nix rename modules/system/services/{nebula => }/dns.nix (63%) rename modules/system/services/{nebula => }/sshd.nix (55%) diff --git a/flake-parts/hosts.nix b/flake-parts/hosts.nix index 55592fb..675f99f 100644 --- a/flake-parts/hosts.nix +++ b/flake-parts/hosts.nix @@ -22,7 +22,7 @@ let mkDeployNode = hostname: { hostname = "${hostname}.${ - self.nixosConfigurations.${hostname}.config.custom.services.nebula.network.domain + self.nixosConfigurations.${hostname}.config.custom.networking.overlay.domain }"; user = "root"; interactiveSudo = true; diff --git a/hosts/desktop/default.nix b/hosts/desktop/default.nix index a06baa7..8a69df1 100644 --- a/hosts/desktop/default.nix +++ b/hosts/desktop/default.nix @@ -23,14 +23,17 @@ }; de.hyprland.enable = true; + networking = { + overlay.address = "10.254.250.1"; + underlay.interface = "enp6s0"; + isClient = true; + }; + services = { gc.enable = true; sound.enable = true; - nebula.node = { - enable = true; - address = "10.254.250.1"; - isClient = true; - }; + nebula.enable = true; + sshd.enable = true; syncthing = { enable = true; deviceId = "FAJS5WM-UAWGW2U-FXCGPSP-VAUOTGM-XUKSEES-D66PMCJ-WBODJLV-XTNCRA7"; diff --git a/hosts/laptop/default.nix b/hosts/laptop/default.nix index 0f13386..8f4acea 100644 --- a/hosts/laptop/default.nix +++ b/hosts/laptop/default.nix @@ -23,17 +23,20 @@ }; de.hyprland.enable = true; + networking = { + overlay.address = "10.254.250.3"; + underlay.interface = "wlan0"; + isClient = true; + }; + services = { resolved.enable = true; gc.enable = true; wlan.enable = true; bluetooth.enable = true; sound.enable = true; - nebula.node = { - enable = true; - address = "10.254.250.3"; - isClient = true; - }; + nebula.enable = true; + sshd.enable = true; syncthing = { enable = true; deviceId = "Q4YPD3V-GXZPHSN-PT5X4PU-FBG4GX2-IASBX75-7NYMG75-4EJHBMZ-4WGDDAP"; diff --git a/hosts/vps-monitor/default.nix b/hosts/vps-monitor/default.nix index 554782f..3b580c8 100644 --- a/hosts/vps-monitor/default.nix +++ b/hosts/vps-monitor/default.nix @@ -20,25 +20,31 @@ boot.loader.grub.enable = true; + networking = { + overlay.address = "10.254.250.5"; + underlay = { + interface = "enp1s0"; + address = "188.245.223.145"; + isPublic = true; + }; + isLighthouse = true; + isServer = true; + }; + services = { gc = { enable = true; onlyCleanRoots = true; }; - nebula.node = { - enable = true; - address = "10.254.250.5"; - routableAddress = "188.245.223.145"; - isLighthouse = true; - isServer = true; - dns.enable = true; - }; + nebula.node.enable = true; + sshd.enable = true; + dns.enable = true; }; web-services = let - privateDomain = config.custom.services.nebula.network.domain; + privateDomain = config.custom.networking.overlay.domain; in { gatus = { diff --git a/hosts/vps-private/default.nix b/hosts/vps-private/default.nix index 0850461..5888e01 100644 --- a/hosts/vps-private/default.nix +++ b/hosts/vps-private/default.nix @@ -15,7 +15,7 @@ custom = let - privateDomain = config.custom.services.nebula.network.domain; + privateDomain = config.custom.networking.overlay.domain; in { persistence.enable = true; @@ -24,20 +24,26 @@ boot.loader.systemd-boot.enable = true; + networking = { + overlay.address = "10.254.250.2"; + underlay = { + interface = "enp1s0"; + address = "49.13.231.235"; + isPublic = true; + }; + isLighthouse = true; + isServer = true; + }; + services = { gc = { enable = true; onlyCleanRoots = true; }; - nebula.node = { - enable = true; - address = "10.254.250.2"; - routableAddress = "49.13.231.235"; - isLighthouse = true; - isServer = true; - dns.enable = true; - }; + nebula.node.enable = true; + sshd.enable = true; + dns.enable = true; syncthing = { enable = true; diff --git a/hosts/vps-public/default.nix b/hosts/vps-public/default.nix index 7d897d5..c35d0f5 100644 --- a/hosts/vps-public/default.nix +++ b/hosts/vps-public/default.nix @@ -20,18 +20,24 @@ boot.loader.systemd-boot.enable = true; + networking = { + overlay.address = "10.254.250.4"; + underlay = { + interface = "enp1s0"; + address = "167.235.73.246"; + isPublic = true; + }; + isServer = true; + }; + services = { gc = { enable = true; onlyCleanRoots = true; }; - nebula.node = { - enable = true; - address = "10.254.250.4"; - routableAddress = "167.235.73.246"; - isServer = true; - }; + nebula.node.enable = true; + sshd.enable = true; crowdsec = { enable = true; @@ -76,7 +82,7 @@ alloy = { enable = true; - domain = "alloy.${config.networking.hostName}.${config.custom.services.nebula.network.domain}"; + domain = "alloy.${config.networking.hostName}.${config.custom.networking.overlay.domain}"; }; }; }; diff --git a/modules/system/networking.nix b/modules/system/networking.nix new file mode 100644 index 0000000..db3d7d6 --- /dev/null +++ b/modules/system/networking.nix @@ -0,0 +1,90 @@ +{ + config, + self, + lib, + ... +}: +let + cfg = config.custom.networking; +in +{ + options.custom.networking = { + hostname = lib.mkOption { + type = lib.types.nonEmptyStr; + default = config.networking.hostName; + readOnly = true; + }; + isLighthouse = lib.mkEnableOption ""; + isServer = lib.mkEnableOption ""; + isClient = lib.mkEnableOption ""; + + overlay = { + networkAddress = lib.mkOption { + type = lib.types.nonEmptyStr; + default = "10.254.250.0"; + readOnly = true; + }; + prefixLength = lib.mkOption { + type = lib.types.ints.between 0 32; + default = 24; + readOnly = true; + }; + domain = lib.mkOption { + type = lib.types.nonEmptyStr; + default = "splitleaf.de"; + readOnly = true; + }; + + address = lib.mkOption { + type = lib.types.nonEmptyStr; + default = ""; + }; + interface = lib.mkOption { + type = lib.types.nonEmptyStr; + default = "nebula.mesh"; + }; + systemdUnit = lib.mkOption { + type = lib.types.nonEmptyStr; + default = "nebula@mesh.service"; + }; + }; + + underlay = { + interface = lib.mkOption { + type = lib.types.nonEmptyStr; + default = ""; + }; + useDhcp = lib.mkEnableOption ""; + isPublic = lib.mkEnableOption ""; + address = lib.mkOption { + type = lib.types.nullOr lib.types.nonEmptyStr; + default = null; + }; + gateway = lib.mkOption { + type = lib.types.nullOr lib.types.nonEmptyStr; + default = null; + }; + }; + + nodes = lib.mkOption { + type = lib.types.anything; + default = + self.nixosConfigurations + |> lib.attrValues + |> lib.map (host: host.config.custom.networking) + |> lib.map ( + node: + lib.removeAttrs node [ + "nodes" + "peers" + ] + ); + readOnly = true; + }; + peers = lib.mkOption { + type = lib.types.anything; + default = cfg.nodes |> lib.filter (node: node.hostname != cfg.hostname); + readOnly = true; + }; + }; +} diff --git a/modules/system/services/caddy.nix b/modules/system/services/caddy.nix index 90d242d..1bb4deb 100644 --- a/modules/system/services/caddy.nix +++ b/modules/system/services/caddy.nix @@ -6,6 +6,7 @@ }: let cfg = config.custom.services.caddy; + netCfg = config.custom.networking; virtualHosts = cfg.virtualHosts |> lib.attrValues |> lib.filter (value: value.enable); @@ -33,7 +34,7 @@ let in '' tls ${certDir}/fullchain.pem ${certDir}/key.pem - bind ${config.custom.services.nebula.node.address} + bind ${config.custom.networking.overlay.address} '' )) (lib.optionalString (port != null) "reverse_proxy localhost:${toString port}") @@ -150,8 +151,8 @@ in ]; systemd.services.caddy = { - requires = [ "nebula@mesh.service" ]; - after = [ "nebula@mesh.service" ]; + requires = [ netCfg.overlay.systemdUnit ]; + after = [ netCfg.overlay.systemdUnit ]; }; }) ] diff --git a/modules/system/services/nebula/dns.nix b/modules/system/services/dns.nix similarity index 63% rename from modules/system/services/nebula/dns.nix rename to modules/system/services/dns.nix index b2c77e6..13e6c8f 100644 --- a/modules/system/services/nebula/dns.nix +++ b/modules/system/services/dns.nix @@ -6,13 +6,13 @@ ... }: let - nebulaCfg = config.custom.services.nebula; - cfg = nebulaCfg.node; + cfg = config.custom.services.dns; + netCfg = config.custom.networking; in { - options.custom.services.nebula.node.dns.enable = lib.mkEnableOption ""; + options.custom.services.dns.enable = lib.mkEnableOption ""; - config = lib.mkIf (cfg.enable && cfg.dns.enable) { + config = lib.mkIf cfg.enable { # meta.ports = { # tcp = [ 53 ]; # udp = [ 53 ]; @@ -24,17 +24,17 @@ in settings = { server = { - interface = [ cfg.interface ]; + interface = [ netCfg.overlay.interface ]; access-control = [ - "${nebulaCfg.network.address}/${toString nebulaCfg.network.prefixLength} allow" + "${netCfg.overlay.networkAddress}/${toString netCfg.overlay.prefixLength} allow" ]; - local-zone = "\"${nebulaCfg.network.domain}.\" static"; + local-zone = "\"${netCfg.overlay.domain}.\" static"; local-data = let nodeRecords = - nebulaCfg.nodes - |> lib.map (node: "\"${node.name}.${nebulaCfg.network.domain}. A ${node.address}\""); + netCfg.nodes + |> lib.map (node: "\"${node.hostname}.${node.overlay.domain}. A ${node.overlay.address}\""); serviceRecords = self.nixosConfigurations |> lib.attrValues @@ -42,7 +42,7 @@ in host: host.config.meta.domains.local |> lib.filter (domain: lib'.isPrivateDomain domain) - |> lib.map (domain: "\"${domain}. A ${host.config.custom.services.nebula.node.address}\"") + |> lib.map (domain: "\"${domain}. A ${host.config.custom.networking.overlay.address}\"") ); in nodeRecords ++ serviceRecords; @@ -66,8 +66,8 @@ in }; systemd.services.unbound = { - requires = [ "nebula@mesh.service" ]; - after = [ "nebula@mesh.service" ]; + requires = [ netCfg.overlay.systemdUnit ]; + after = [ netCfg.overlay.systemdUnit ]; }; }; } diff --git a/modules/system/services/nebula/default.nix b/modules/system/services/nebula/default.nix index e6cb193..af31b6e 100644 --- a/modules/system/services/nebula/default.nix +++ b/modules/system/services/nebula/default.nix @@ -5,92 +5,33 @@ ... }: let - nebulaCfg = config.custom.services.nebula; - cfg = nebulaCfg.node; + cfg = config.custom.services.nebula; + netCfg = config.custom.networking; - hostname = config.networking.hostName; + publicPort = 47141; in { options.custom.services.nebula = { - network = { - address = lib.mkOption { - type = lib.types.nonEmptyStr; - default = "10.254.250.0"; - readOnly = true; - }; - prefixLength = lib.mkOption { - type = lib.types.ints.between 0 32; - default = 24; - readOnly = true; - }; - domain = lib.mkOption { - type = lib.types.nonEmptyStr; - default = "splitleaf.de"; - readOnly = true; - }; + enable = lib.mkEnableOption ""; + + publicKeyPath = lib.mkOption { + type = lib.types.path; + default = "${self}/hosts/${netCfg.hostname}/keys/nebula.pub"; }; - - node = { - enable = lib.mkEnableOption ""; - name = lib.mkOption { - type = lib.types.nonEmptyStr; - default = hostname; - }; - interface = lib.mkOption { - type = lib.types.nonEmptyStr; - default = "nebula.mesh"; - }; - address = lib.mkOption { - type = lib.types.nonEmptyStr; - default = ""; - }; - isLighthouse = lib.mkEnableOption ""; - isServer = lib.mkEnableOption ""; - isClient = lib.mkEnableOption ""; - - routableAddress = lib.mkOption { - type = lib.types.nullOr lib.types.nonEmptyStr; - default = null; - }; - routablePort = lib.mkOption { - type = lib.types.nullOr lib.types.port; - default = if cfg.routableAddress != null then 47141 else null; - }; - - publicKeyPath = lib.mkOption { - type = lib.types.path; - default = "${self}/hosts/${hostname}/keys/nebula.pub"; - }; - certificatePath = lib.mkOption { - type = lib.types.path; - default = "${self}/hosts/${hostname}/keys/nebula.crt"; - }; - }; - - nodes = lib.mkOption { - type = lib.types.anything; - default = - self.nixosConfigurations - |> lib.attrValues - |> lib.map (value: value.config.custom.services.nebula.node) - |> lib.filter (node: node.enable); - readOnly = true; - }; - peers = lib.mkOption { - type = lib.types.anything; - default = nebulaCfg.nodes |> lib.filter (node: node.name != hostname); - readOnly = true; + certificatePath = lib.mkOption { + type = lib.types.path; + default = "${self}/hosts/${netCfg.hostname}/keys/nebula.crt"; }; }; config = lib.mkIf cfg.enable { - meta.ports.udp = lib.optional (cfg.routablePort != null) cfg.routablePort; - assertions = lib.singleton { - assertion = cfg.isLighthouse -> cfg.routableAddress != null; - message = "'${hostname}' is a Nebula lighthouse, but routableAddress is not set. Lighthouses must be publicly reachable."; + assertion = netCfg.isLighthouse -> netCfg.underlay.isPublic; + message = "'${netCfg.hostname}' is a Nebula lighthouse, but underlay.isPublic is not set. Lighthouses must be publicly reachable."; }; + meta.ports.udp = lib.optional (netCfg.underlay.isPublic) publicPort; + sops.secrets."nebula/host-key" = { owner = config.users.users.nebula-mesh.name; restartUnits = [ "nebula@mesh.service" ]; @@ -103,19 +44,21 @@ in cert = cfg.certificatePath; key = config.sops.secrets."nebula/host-key".path; - listen.port = cfg.routablePort; + listen.port = lib.mkIf netCfg.underlay.isPublic publicPort; - inherit (cfg) isLighthouse; - lighthouses = lib.mkIf (!cfg.isLighthouse) ( - nebulaCfg.peers |> lib.filter (node: node.isLighthouse) |> lib.map (lighthouse: lighthouse.address) + inherit (netCfg) isLighthouse; + lighthouses = lib.mkIf (!netCfg.isLighthouse) ( + netCfg.peers + |> lib.filter (peer: peer.isLighthouse) + |> lib.map (lighthouse: lighthouse.overlay.address) ); staticHostMap = - nebulaCfg.peers - |> lib.filter (node: node.routableAddress != null) - |> lib.map (lighthouse: { - name = lighthouse.address; - value = lib.singleton "${lighthouse.routableAddress}:${toString lighthouse.routablePort}"; + netCfg.peers + |> lib.filter (peer: peer.underlay.isPublic) + |> lib.map (publicPeer: { + name = publicPeer.overlay.address; + value = lib.singleton "${publicPeer.underlay.address}:${toString publicPort}"; }) |> lib.listToAttrs; @@ -138,13 +81,17 @@ in }; }; - networking.firewall.trustedInterfaces = [ cfg.interface ]; + networking.firewall.trustedInterfaces = [ netCfg.overlay.interface ]; systemd.network.networks."40-nebula" = { - matchConfig.Name = cfg.interface; - address = [ "${cfg.address}/${toString nebulaCfg.network.prefixLength}" ]; - dns = nebulaCfg.peers |> lib.filter (node: node.dns.enable) |> lib.map (node: node.address); - domains = [ nebulaCfg.network.domain ]; + matchConfig.Name = netCfg.overlay.interface; + address = [ "${netCfg.overlay.address}/${toString netCfg.overlay.prefixLength}" ]; + dns = + self.nixosConfigurations + |> lib.attrValues + |> lib.filter (host: host.config.custom.services.dns.enable) + |> lib.map (host: host.config.custom.networking.overlay.address); + domains = [ netCfg.overlay.domain ]; }; }; } diff --git a/modules/system/services/nebula/sshd.nix b/modules/system/services/sshd.nix similarity index 55% rename from modules/system/services/nebula/sshd.nix rename to modules/system/services/sshd.nix index 8ce7ed0..527b944 100644 --- a/modules/system/services/nebula/sshd.nix +++ b/modules/system/services/sshd.nix @@ -5,14 +5,13 @@ ... }: let - cfg = config.custom.services.nebula.node; + cfg = config.custom.services.sshd; + netCfg = config.custom.networking; in { - options.custom.services.nebula.node.sshd.enable = lib.mkEnableOption "" // { - default = true; - }; + options.custom.services.sshd.enable = lib.mkEnableOption ""; - config = lib.mkIf (cfg.enable && cfg.sshd.enable) { + config = lib.mkIf cfg.enable { meta.ports.tcp = [ 22 ]; services = { @@ -21,7 +20,7 @@ in openFirewall = false; ports = [ ]; listenAddresses = lib.singleton { - addr = cfg.address; + addr = netCfg.overlay.address; port = 22; }; settings = { @@ -32,26 +31,26 @@ in }; nebula.networks.mesh.firewall.inbound = - config.custom.services.nebula.peers + netCfg.peers |> lib.filter (node: node.isClient) - |> lib.map (nebula: { + |> lib.map (client: { port = 22; proto = "tcp"; - host = nebula.name; + host = client.hostname; }); }; systemd.services.sshd = { - requires = [ "nebula@mesh.service" ]; - after = [ "nebula@mesh.service" ]; + requires = [ netCfg.overlay.systemdUnit ]; + after = [ netCfg.overlay.systemdUnit ]; }; users.users.seb.openssh.authorizedKeys.keyFiles = self.nixosConfigurations - |> lib.filterAttrs (name: _: name != config.networking.hostName) |> lib.attrValues - |> lib.filter (value: value.config |> lib.hasAttr "home-manager") - |> lib.map (value: value.config.home-manager.users.seb.custom.programs.ssh) + |> lib.filter (host: host.config.custom.networking.hostname != netCfg.hostname) + |> lib.filter (host: host.config |> lib.hasAttr "home-manager") + |> lib.map (host: host.config.home-manager.users.seb.custom.programs.ssh) |> lib.filter (ssh: ssh.enable) |> lib.map (ssh: ssh.publicKeyPath); }; diff --git a/modules/system/services/syncthing.nix b/modules/system/services/syncthing.nix index abfd81b..819726f 100644 --- a/modules/system/services/syncthing.nix +++ b/modules/system/services/syncthing.nix @@ -7,6 +7,7 @@ }: let cfg = config.custom.services.syncthing; + netCfg = config.custom.networking; inherit (config.services.syncthing) dataDir; @@ -50,10 +51,6 @@ in config = lib.mkIf cfg.enable { assertions = [ - { - assertion = config.custom.services.nebula.node.enable; - message = "Syncthing requires nebula"; - } { assertion = cfg.isServer -> (cfg.gui.domain != null); message = "Running syncthing on a server requires `gui.domain` to be set"; @@ -109,7 +106,7 @@ in _: value: { id = value.config.custom.services.syncthing.deviceId; addresses = [ - "tcp://${value.config.custom.services.nebula.node.address}:${toString cfg.syncPort}" + "tcp://${value.config.custom.networking.overlay.address}:${toString cfg.syncPort}" ]; } ); @@ -122,7 +119,7 @@ in }); options = { - listenAddress = "tcp://${config.custom.services.nebula.node.address}:${toString cfg.syncPort}"; + listenAddress = "tcp://${netCfg.overlay.address}:${toString cfg.syncPort}"; globalAnnounceEnabled = false; localAnnounceEnabled = false; relaysEnabled = false; diff --git a/modules/system/web-services/alloy.nix b/modules/system/web-services/alloy.nix index e0bc827..3fea35b 100644 --- a/modules/system/web-services/alloy.nix +++ b/modules/system/web-services/alloy.nix @@ -15,11 +15,11 @@ in }; metricsEndpoint = lib.mkOption { type = lib.types.nonEmptyStr; - default = "https://metrics.${config.custom.services.nebula.network.domain}/prometheus/api/v1/write"; + default = "https://metrics.${config.custom.networking.overlay.domain}/prometheus/api/v1/write"; }; logsEndpoint = lib.mkOption { type = lib.types.nonEmptyStr; - default = "https://logs.${config.custom.services.nebula.network.domain}/insert/loki/api/v1/push"; + default = "https://logs.${config.custom.networking.overlay.domain}/insert/loki/api/v1/push"; }; collect = { metrics = { diff --git a/modules/system/web-services/gatus.nix b/modules/system/web-services/gatus.nix index 0cda068..163b882 100644 --- a/modules/system/web-services/gatus.nix +++ b/modules/system/web-services/gatus.nix @@ -113,7 +113,7 @@ in connectivity.checker.target = "1.1.1.1:53"; # Cloudflare DNS alerting.ntfy = { topic = "uptime"; - url = "https://alerts.${config.custom.services.nebula.network.domain}"; + url = "https://alerts.${config.custom.networking.overlay.domain}"; click = "https://${cfg.domain}"; default-alert = { enable = true; diff --git a/modules/system/web-services/grafana.nix b/modules/system/web-services/grafana.nix index 155674c..a0c64b7 100644 --- a/modules/system/web-services/grafana.nix +++ b/modules/system/web-services/grafana.nix @@ -23,21 +23,21 @@ in enable = lib.mkEnableOption ""; url = lib.mkOption { type = lib.types.nonEmptyStr; - default = "https://metrics.${config.custom.services.nebula.network.domain}"; + default = "https://metrics.${config.custom.networking.overlay.domain}"; }; }; victoriametrics = { enable = lib.mkEnableOption ""; url = lib.mkOption { type = lib.types.nonEmptyStr; - default = "https://metrics.${config.custom.services.nebula.network.domain}"; + default = "https://metrics.${config.custom.networking.overlay.domain}"; }; }; victorialogs = { enable = lib.mkEnableOption ""; url = lib.mkOption { type = lib.types.nonEmptyStr; - default = "https://logs.${config.custom.services.nebula.network.domain}"; + default = "https://logs.${config.custom.networking.overlay.domain}"; }; }; };