about summary refs log tree commit diff
path: root/nixos/modules/services/networking
diff options
context:
space:
mode:
Diffstat (limited to 'nixos/modules/services/networking')
-rw-r--r--nixos/modules/services/networking/adguardhome.nix11
-rw-r--r--nixos/modules/services/networking/antennas.nix5
-rw-r--r--nixos/modules/services/networking/aria2.nix165
-rw-r--r--nixos/modules/services/networking/clatd.nix81
-rw-r--r--nixos/modules/services/networking/ddclient.nix22
-rw-r--r--nixos/modules/services/networking/frr.nix27
-rw-r--r--nixos/modules/services/networking/git-daemon.nix4
-rw-r--r--nixos/modules/services/networking/hostapd.nix17
-rw-r--r--nixos/modules/services/networking/inadyn.nix2
-rw-r--r--nixos/modules/services/networking/jotta-cli.md27
-rw-r--r--nixos/modules/services/networking/jotta-cli.nix43
-rw-r--r--nixos/modules/services/networking/kea.nix3
-rw-r--r--nixos/modules/services/networking/mihomo.nix1
-rw-r--r--nixos/modules/services/networking/mycelium.nix2
-rw-r--r--nixos/modules/services/networking/netbird.nix1
-rw-r--r--nixos/modules/services/networking/netbird/coturn.nix7
-rw-r--r--nixos/modules/services/networking/netbird/server.nix33
-rw-r--r--nixos/modules/services/networking/networkmanager.nix2
-rw-r--r--nixos/modules/services/networking/oink.nix84
-rw-r--r--nixos/modules/services/networking/pixiecore.nix4
-rw-r--r--nixos/modules/services/networking/radvd.nix13
-rw-r--r--nixos/modules/services/networking/rosenpass.nix10
-rw-r--r--nixos/modules/services/networking/smokeping.nix63
-rw-r--r--nixos/modules/services/networking/ssh/sshd.nix73
-rw-r--r--nixos/modules/services/networking/sunshine.nix6
-rw-r--r--nixos/modules/services/networking/tailscale-auth.nix2
-rw-r--r--nixos/modules/services/networking/tailscale.nix23
-rw-r--r--nixos/modules/services/networking/tayga.nix26
-rw-r--r--nixos/modules/services/networking/trust-dns.nix6
-rw-r--r--nixos/modules/services/networking/vsftpd.nix2
-rw-r--r--nixos/modules/services/networking/wireguard.nix10
-rw-r--r--nixos/modules/services/networking/wstunnel.nix563
-rw-r--r--nixos/modules/services/networking/zerotierone.nix8
33 files changed, 911 insertions, 435 deletions
diff --git a/nixos/modules/services/networking/adguardhome.nix b/nixos/modules/services/networking/adguardhome.nix
index df9927351edc3..5be3e0bea224a 100644
--- a/nixos/modules/services/networking/adguardhome.nix
+++ b/nixos/modules/services/networking/adguardhome.nix
@@ -140,7 +140,7 @@ in {
       {
         assertion = cfg.settings != null
           -> !(hasAttrByPath [ "bind_port" ] cfg.settings);
-        message = "AdGuard option `settings.bind_host' has been superseded by `services.adguardhome.port'";
+        message = "AdGuard option `settings.bind_port' has been superseded by `services.adguardhome.port'";
       }
       {
         assertion = settings != null -> cfg.mutableSettings
@@ -167,8 +167,13 @@ in {
       preStart = optionalString (settings != null) ''
         if    [ -e "$STATE_DIRECTORY/AdGuardHome.yaml" ] \
            && [ "${toString cfg.mutableSettings}" = "1" ]; then
+          # First run a schema_version update on the existing configuration
+          # This ensures that both the new config and the existing one have the same schema_version
+          # Note: --check-config has the side effect of modifying the file at rest!
+          ${lib.getExe cfg.package} -c "$STATE_DIRECTORY/AdGuardHome.yaml" --check-config
+
           # Writing directly to AdGuardHome.yaml results in empty file
-          ${pkgs.yaml-merge}/bin/yaml-merge "$STATE_DIRECTORY/AdGuardHome.yaml" "${configFile}" > "$STATE_DIRECTORY/AdGuardHome.yaml.tmp"
+          ${lib.getExe pkgs.yaml-merge} "$STATE_DIRECTORY/AdGuardHome.yaml" "${configFile}" > "$STATE_DIRECTORY/AdGuardHome.yaml.tmp"
           mv "$STATE_DIRECTORY/AdGuardHome.yaml.tmp" "$STATE_DIRECTORY/AdGuardHome.yaml"
         else
           cp --force "${configFile}" "$STATE_DIRECTORY/AdGuardHome.yaml"
@@ -178,7 +183,7 @@ in {
 
       serviceConfig = {
         DynamicUser = true;
-        ExecStart = "${cfg.package}/bin/adguardhome ${args}";
+        ExecStart = "${lib.getExe cfg.package} ${args}";
         AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]
           ++ optionals cfg.allowDHCP [ "CAP_NET_RAW" ];
         Restart = "always";
diff --git a/nixos/modules/services/networking/antennas.nix b/nixos/modules/services/networking/antennas.nix
index ef98af22f20f2..a37df953fc923 100644
--- a/nixos/modules/services/networking/antennas.nix
+++ b/nixos/modules/services/networking/antennas.nix
@@ -50,10 +50,7 @@ in
       };
 
       serviceConfig = {
-         ExecStart = "${pkgs.antennas}/bin/antennas";
-
-        # Antennas expects all resources like html and config to be relative to it's working directory
-        WorkingDirectory = "${pkgs.antennas}/libexec/antennas/deps/antennas/";
+        ExecStart = "${pkgs.antennas}/bin/antennas";
 
         # Hardening
         CapabilityBoundingSet = [ "" ];
diff --git a/nixos/modules/services/networking/aria2.nix b/nixos/modules/services/networking/aria2.nix
index f32f5682c9801..f0d5c5c8a21e3 100644
--- a/nixos/modules/services/networking/aria2.nix
+++ b/nixos/modules/services/networking/aria2.nix
@@ -1,98 +1,132 @@
 { config, lib, pkgs, ... }:
 
-with lib;
-
 let
   cfg = config.services.aria2;
 
   homeDir = "/var/lib/aria2";
-
-  settingsDir = "${homeDir}";
-  sessionFile = "${homeDir}/aria2.session";
-  downloadDir = "${homeDir}/Downloads";
-
-  rangesToStringList = map (x: builtins.toString x.from +"-"+ builtins.toString x.to);
-
-  settingsFile = pkgs.writeText "aria2.conf"
-  ''
-    dir=${cfg.downloadDir}
-    listen-port=${concatStringsSep "," (rangesToStringList cfg.listenPortRange)}
-    rpc-listen-port=${toString cfg.rpcListenPort}
-  '';
-
+  defaultRpcListenPort = 6800;
+  defaultDir = "${homeDir}/Downloads";
+
+  portRangesToString = ranges: lib.concatStringsSep "," (map
+    (x:
+      if x.from == x.to
+      then builtins.toString x.from
+      else builtins.toString x.from + "-" + builtins.toString x.to
+    )
+    ranges);
+
+  customToKeyValue = lib.generators.toKeyValue {
+    mkKeyValue = lib.generators.mkKeyValueDefault
+      {
+        mkValueString = v:
+          if builtins.isList v then portRangesToString v
+          else lib.generators.mkValueStringDefault { } v;
+      } "=";
+  };
 in
 {
   imports = [
-    (mkRemovedOptionModule [ "services" "aria2" "rpcSecret" ] "Use services.aria2.rpcSecretFile instead")
+    (lib.mkRemovedOptionModule [ "services" "aria2" "rpcSecret" ] "Use services.aria2.rpcSecretFile instead")
+    (lib.mkRemovedOptionModule [ "services" "aria2" "extraArguments" ] "Use services.aria2.settings instead")
+    (lib.mkRenamedOptionModule [ "services" "aria2" "downloadDir" ] [ "services" "aria2" "settings" "dir" ])
+    (lib.mkRenamedOptionModule [ "services" "aria2" "listenPortRange" ] [ "services" "aria2" "settings" "listen-port" ])
+    (lib.mkRenamedOptionModule [ "services" "aria2" "rpcListenPort" ] [ "services" "aria2" "settings" "rpc-listen-port" ])
   ];
 
   options = {
     services.aria2 = {
-      enable = mkOption {
-        type = types.bool;
+      enable = lib.mkOption {
+        type = lib.types.bool;
         default = false;
         description = ''
           Whether or not to enable the headless Aria2 daemon service.
 
-          Aria2 daemon can be controlled via the RPC interface using
-          one of many WebUI (http://localhost:6800/ by default).
+          Aria2 daemon can be controlled via the RPC interface using one of many
+          WebUIs (http://localhost:${toString defaultRpcListenPort}/ by default).
 
-          Targets are downloaded to ${downloadDir} by default and are
-          accessible to users in the "aria2" group.
+          Targets are downloaded to `${defaultDir}` by default and are
+          accessible to users in the `aria2` group.
         '';
       };
-      openPorts = mkOption {
-        type = types.bool;
+      openPorts = lib.mkOption {
+        type = lib.types.bool;
         default = false;
         description = ''
-          Open listen and RPC ports found in listenPortRange and rpcListenPort
-          options in the firewall.
-        '';
-      };
-      downloadDir = mkOption {
-        type = types.path;
-        default = downloadDir;
-        description = ''
-          Directory to store downloaded files.
-        '';
-      };
-      listenPortRange = mkOption {
-        type = types.listOf types.attrs;
-        default = [ { from = 6881; to = 6999; } ];
-        description = ''
-          Set UDP listening port range used by DHT(IPv4, IPv6) and UDP tracker.
+          Open listen and RPC ports found in `settings.listen-port` and
+          `settings.rpc-listen-port` options in the firewall.
         '';
       };
-      rpcListenPort = mkOption {
-        type = types.int;
-        default = 6800;
-        description = "Specify a port number for JSON-RPC/XML-RPC server to listen to. Possible Values: 1024-65535";
-      };
-      rpcSecretFile = mkOption {
-        type = types.path;
+      rpcSecretFile = lib.mkOption {
+        type = lib.types.path;
         example = "/run/secrets/aria2-rpc-token.txt";
         description = ''
           A file containing the RPC secret authorization token.
           Read https://aria2.github.io/manual/en/html/aria2c.html#rpc-auth to know how this option value is used.
         '';
       };
-      extraArguments = mkOption {
-        type = types.separatedString " ";
-        example = "--rpc-listen-all --remote-time=true";
-        default = "";
+      settings = lib.mkOption {
         description = ''
-          Additional arguments to be passed to Aria2.
+          Generates the `aria2.conf` file. Refer to [the documentation][0] for
+          all possible settings.
+
+          [0]: https://aria2.github.io/manual/en/html/aria2c.html#synopsis
         '';
+        default = { };
+        type = lib.types.submodule {
+          freeformType = with lib.types; attrsOf (oneOf [ bool int float singleLineStr ]);
+          options = {
+            save-session = lib.mkOption {
+              type = lib.types.singleLineStr;
+              default = "${homeDir}/aria2.session";
+              description = "Save error/unfinished downloads to FILE on exit.";
+            };
+            dir = lib.mkOption {
+              type = lib.types.singleLineStr;
+              default = defaultDir;
+              description = "Directory to store downloaded files.";
+            };
+            conf-path = lib.mkOption {
+              type = lib.types.singleLineStr;
+              default = "${homeDir}/aria2.conf";
+              description = "Configuration file path.";
+            };
+            enable-rpc = lib.mkOption {
+              type = lib.types.bool;
+              default = true;
+              description = "Enable JSON-RPC/XML-RPC server.";
+            };
+            listen-port = lib.mkOption {
+              type = with lib.types; listOf (attrsOf port);
+              default = [{ from = 6881; to = 6999; }];
+              description = "Set UDP listening port range used by DHT(IPv4, IPv6) and UDP tracker.";
+            };
+            rpc-listen-port = lib.mkOption {
+              type = lib.types.port;
+              default = defaultRpcListenPort;
+              description = "Specify a port number for JSON-RPC/XML-RPC server to listen to. Possible Values: 1024-65535";
+            };
+          };
+        };
       };
     };
   };
 
-  config = mkIf cfg.enable {
+  config = lib.mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = cfg.settings.enable-rpc;
+        message = "RPC has to be enabled, the default module option takes care of that.";
+      }
+      {
+        assertion = !(cfg.settings ? rpc-secret);
+        message = "Set the RPC secret through services.aria2.rpcSecretFile so it will not end up in the world-readable nix store.";
+      }
+    ];
 
     # Need to open ports for proper functioning
-    networking.firewall = mkIf cfg.openPorts {
-      allowedUDPPortRanges = config.services.aria2.listenPortRange;
-      allowedTCPPorts = [ config.services.aria2.rpcListenPort ];
+    networking.firewall = lib.mkIf cfg.openPorts {
+      allowedUDPPortRanges = config.services.aria2.settings.listen-port;
+      allowedTCPPorts = [ config.services.aria2.settings.rpc-listen-port ];
     };
 
     users.users.aria2 = {
@@ -107,7 +141,7 @@ in
 
     systemd.tmpfiles.rules = [
       "d '${homeDir}' 0770 aria2 aria2 - -"
-      "d '${config.services.aria2.downloadDir}' 0770 aria2 aria2 - -"
+      "d '${config.services.aria2.settings.dir}' 0770 aria2 aria2 - -"
     ];
 
     systemd.services.aria2 = {
@@ -115,22 +149,25 @@ in
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
       preStart = ''
-        if [[ ! -e "${sessionFile}" ]]
+        if [[ ! -e "${cfg.settings.save-session}" ]]
         then
-          touch "${sessionFile}"
+          touch "${cfg.settings.save-session}"
         fi
-        cp -f "${settingsFile}" "${settingsDir}/aria2.conf"
-        echo "rpc-secret=$(cat "$CREDENTIALS_DIRECTORY/rpcSecretFile")" >> "${settingsDir}/aria2.conf"
+        cp -f "${pkgs.writeText "aria2.conf" (customToKeyValue cfg.settings)}" "${cfg.settings.conf-path}"
+        chmod +w "${cfg.settings.conf-path}"
+        echo "rpc-secret=$(cat "$CREDENTIALS_DIRECTORY/rpcSecretFile")" >> "${cfg.settings.conf-path}"
       '';
 
       serviceConfig = {
         Restart = "on-abort";
-        ExecStart = "${pkgs.aria2}/bin/aria2c --enable-rpc --conf-path=${settingsDir}/aria2.conf ${config.services.aria2.extraArguments} --save-session=${sessionFile}";
+        ExecStart = "${pkgs.aria2}/bin/aria2c --conf-path=${cfg.settings.conf-path}";
         ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
         User = "aria2";
         Group = "aria2";
-        LoadCredential="rpcSecretFile:${cfg.rpcSecretFile}";
+        LoadCredential = "rpcSecretFile:${cfg.rpcSecretFile}";
       };
     };
   };
+
+  meta.maintainers = [ lib.maintainers.timhae ];
 }
diff --git a/nixos/modules/services/networking/clatd.nix b/nixos/modules/services/networking/clatd.nix
new file mode 100644
index 0000000000000..de6cde4e979c0
--- /dev/null
+++ b/nixos/modules/services/networking/clatd.nix
@@ -0,0 +1,81 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.clatd;
+
+  settingsFormat = pkgs.formats.keyValue {};
+
+  configFile = settingsFormat.generate "clatd.conf" cfg.settings;
+in
+{
+  options = {
+    services.clatd = {
+      enable = mkEnableOption "clatd";
+
+      package = mkPackageOption pkgs "clatd" { };
+
+      settings = mkOption {
+        type = types.submodule ({ name, ... }: {
+          freeformType = settingsFormat.type;
+        });
+        default = { };
+        example = literalExpression ''
+          {
+            plat-prefix = "64:ff9b::/96";
+          }
+        '';
+        description = ''
+          Configuration of clatd. See [clatd Documentation](https://github.com/toreanderson/clatd/blob/master/README.pod#configuration).
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.clatd = {
+      description = "464XLAT CLAT daemon";
+      documentation = [ "man:clatd(8)" ];
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network-online.target" ];
+      wants = [ "network-online.target" ];
+      startLimitIntervalSec = 0;
+
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/clatd -c ${configFile}";
+
+        # Hardening
+        CapabilityBoundingSet = [
+          "CAP_NET_ADMIN"
+        ];
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateTmp = true;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectProc = "invisible";
+        ProtectSystem = true;
+        RestrictAddressFamilies = [
+          "AF_INET"
+          "AF_INET6"
+          "AF_NETLINK"
+        ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [
+          "@network-io"
+          "@system-service"
+          "~@privileged"
+          "~@resources"
+        ];
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/ddclient.nix b/nixos/modules/services/networking/ddclient.nix
index b912550e1155e..272a50eb92de8 100644
--- a/nixos/modules/services/networking/ddclient.nix
+++ b/nixos/modules/services/networking/ddclient.nix
@@ -11,7 +11,9 @@ let
     # This file can be used as a template for configFile or is automatically generated by Nix options.
     cache=${dataDir}/ddclient.cache
     foreground=YES
-    use=${cfg.use}
+    ${lib.optionalString (cfg.use != "") "use=${cfg.use}"}
+    ${lib.optionalString (cfg.use == "" && cfg.usev4 != "") "usev4=${cfg.usev4}"}
+    ${lib.optionalString (cfg.use == "" && cfg.usev6 != "") "usev6=${cfg.usev6}"}
     login=${cfg.username}
     password=${if cfg.protocol == "nsupdate" then "/run/${RuntimeDirectory}/ddclient.key" else "@password_placeholder@"}
     protocol=${cfg.protocol}
@@ -163,12 +165,26 @@ with lib;
       };
 
       use = mkOption {
-        default = "web, web=checkip.dyndns.com/, web-skip='Current IP Address: '";
+        default = "";
         type = str;
         description = ''
           Method to determine the IP address to send to the dynamic DNS provider.
         '';
       };
+      usev4 = mkOption {
+        default = "webv4, webv4=checkip.dyndns.com/, webv4-skip='Current IP Address: '";
+        type = str;
+        description = ''
+          Method to determine the IPv4 address to send to the dynamic DNS provider. Only used if `use` is not set.
+        '';
+      };
+      usev6 = mkOption {
+        default = "webv6, webv6=checkipv6.dyndns.com/, webv6-skip='Current IP Address: '";
+        type = str;
+        description = ''
+          Method to determine the IPv6 address to send to the dynamic DNS provider. Only used if `use` is not set.
+        '';
+      };
 
       verbose = mkOption {
         default = false;
@@ -204,6 +220,8 @@ with lib;
   ###### implementation
 
   config = mkIf config.services.ddclient.enable {
+    warnings = lib.optional (cfg.use != "") "Setting `use` is deprecated, ddclient now supports `usev4` and `usev6` for separate IPv4/IPv6 configuration.";
+
     systemd.services.ddclient = {
       description = "Dynamic DNS Client";
       wantedBy = [ "multi-user.target" ];
diff --git a/nixos/modules/services/networking/frr.nix b/nixos/modules/services/networking/frr.nix
index 7f611ce7b1c7d..df2b4035d2f07 100644
--- a/nixos/modules/services/networking/frr.nix
+++ b/nixos/modules/services/networking/frr.nix
@@ -23,10 +23,9 @@ let
     "pbr"
     "bfd"
     "fabric"
-    "mgmt"
   ];
 
-  allServices = services ++ [ "zebra" ];
+  allServices = services ++ [ "zebra" "mgmt" ];
 
   isEnabled = service: cfg.${service}.enable;
 
@@ -137,6 +136,20 @@ in
             '';
           };
         };
+        mgmt = (serviceOptions "mgmt") // {
+          enable = mkOption {
+            type = types.bool;
+            default = isEnabled "static";
+            defaultText = lib.literalExpression "config.services.frr.static.enable";
+            description = ''
+              Whether to enable the Configuration management daemon.
+
+              The Configuration management daemon is automatically
+              enabled if needed, at the moment this is when staticd
+              is enabled.
+            '';
+          };
+        };
       };
     }
     { options.services.frr = (genAttrs services serviceOptions); }
@@ -164,7 +177,7 @@ in
 
     environment.etc = let
       mkEtcLink = service: {
-        name = "frr/${service}.conf";
+        name = "frr/${daemonName service}.conf";
         value.source = configFile service;
       };
     in
@@ -196,18 +209,18 @@ in
               unitConfig.Documentation = if service == "zebra" then "man:zebra(8)"
                 else "man:${daemon}(8) man:zebra(8)";
 
-              restartTriggers = [
+              restartTriggers = mkIf (service != "mgmt") [
                 (configFile service)
               ];
-              reloadIfChanged = true;
+              reloadIfChanged = (service != "mgmt");
 
               serviceConfig = {
                 PIDFile = "frr/${daemon}.pid";
-                ExecStart = "${pkgs.frr}/libexec/frr/${daemon} -f /etc/frr/${service}.conf"
+                ExecStart = "${pkgs.frr}/libexec/frr/${daemon}"
                   + optionalString (scfg.vtyListenAddress != "") " -A ${scfg.vtyListenAddress}"
                   + optionalString (scfg.vtyListenPort != null) " -P ${toString scfg.vtyListenPort}"
                   + " " + (concatStringsSep " " scfg.extraOptions);
-                ExecReload = "${pkgs.python3.interpreter} ${pkgs.frr}/libexec/frr/frr-reload.py --reload --daemon ${daemonName service} --bindir ${pkgs.frr}/bin --rundir /run/frr /etc/frr/${service}.conf";
+                ExecReload = mkIf (service != "mgmt") "${pkgs.python3.interpreter} ${pkgs.frr}/libexec/frr/frr-reload.py --reload --daemon ${daemon} --bindir ${pkgs.frr}/bin --rundir /run/frr /etc/frr/${daemon}.conf";
                 Restart = "on-abnormal";
               };
             });
diff --git a/nixos/modules/services/networking/git-daemon.nix b/nixos/modules/services/networking/git-daemon.nix
index 6be72505c216e..522e6b14f868f 100644
--- a/nixos/modules/services/networking/git-daemon.nix
+++ b/nixos/modules/services/networking/git-daemon.nix
@@ -27,6 +27,8 @@ in
         '';
       };
 
+      package = mkPackageOption pkgs "git" { };
+
       basePath = mkOption {
         type = types.str;
         default = "";
@@ -119,7 +121,7 @@ in
     systemd.services.git-daemon = {
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
-      script = "${pkgs.git}/bin/git daemon --reuseaddr "
+      script = "${getExe cfg.package} daemon --reuseaddr "
         + (optionalString (cfg.basePath != "") "--base-path=${cfg.basePath} ")
         + (optionalString (cfg.listenAddress != "") "--listen=${cfg.listenAddress} ")
         + "--port=${toString cfg.port} --user=${cfg.user} --group=${cfg.group} ${cfg.options} "
diff --git a/nixos/modules/services/networking/hostapd.nix b/nixos/modules/services/networking/hostapd.nix
index 1bef5a1f0a9e8..b678656f2e046 100644
--- a/nixos/modules/services/networking/hostapd.nix
+++ b/nixos/modules/services/networking/hostapd.nix
@@ -687,7 +687,7 @@ in {
                   authentication = {
                     mode = mkOption {
                       default = "wpa3-sae";
-                      type = types.enum ["none" "wpa2-sha256" "wpa3-sae-transition" "wpa3-sae"];
+                      type = types.enum ["none" "wpa2-sha1" "wpa2-sha256" "wpa3-sae-transition" "wpa3-sae"];
                       description = ''
                         Selects the authentication mode for this AP.
 
@@ -695,7 +695,9 @@ in {
                           and create an open AP. Use {option}`settings` together with this option if you
                           want to configure the authentication manually. Any password options will still be
                           effective, if set.
-                        - {var}`"wpa2-sha256"`: WPA2-Personal using SHA256 (IEEE 802.11i/RSN). Passwords are set
+                        - {var}`"wpa2-sha1"`: Not recommended. WPA2-Personal using HMAC-SHA1. Passwords are set
+                          using {option}`wpaPassword` or preferably by {option}`wpaPasswordFile` or {option}`wpaPskFile`.
+                        - {var}`"wpa2-sha256"`: WPA2-Personal using HMAC-SHA256 (IEEE 802.11i/RSN). Passwords are set
                           using {option}`wpaPassword` or preferably by {option}`wpaPasswordFile` or {option}`wpaPskFile`.
                         - {var}`"wpa3-sae-transition"`: Use WPA3-Personal (SAE) if possible, otherwise fallback
                           to WPA2-SHA256. Only use if necessary and switch to the newer WPA3-SAE when possible.
@@ -812,7 +814,7 @@ in {
                         Warning: These entries will get put into a world-readable file in
                         the Nix store! Using {option}`saePasswordFile` instead is recommended.
 
-                        Not used when {option}`mode` is {var}`"wpa2-sha256"`.
+                        Not used when {option}`mode` is {var}`"wpa2-sha1"` or {var}`"wpa2-sha256"`.
                       '';
                       type = types.listOf (types.submodule {
                         options = {
@@ -884,7 +886,7 @@ in {
                         parameters doesn't matter:
                         `<password>[|mac=<peer mac>][|vlanid=<VLAN ID>][|pk=<m:ECPrivateKey-base64>][|id=<identifier>]`
 
-                        Not used when {option}`mode` is {var}`"wpa2-sha256"`.
+                        Not used when {option}`mode` is {var}`"wpa2-sha1"` or {var}`"wpa2-sha256"`.
                       '';
                     };
 
@@ -959,6 +961,9 @@ in {
                   } // optionalAttrs (bssCfg.authentication.mode == "wpa3-sae-transition") {
                     wpa = 2;
                     wpa_key_mgmt = "WPA-PSK-SHA256 SAE";
+                  } // optionalAttrs (bssCfg.authentication.mode == "wpa2-sha1") {
+                    wpa = 2;
+                    wpa_key_mgmt = "WPA-PSK";
                   } // optionalAttrs (bssCfg.authentication.mode == "wpa2-sha256") {
                     wpa = 2;
                     wpa_key_mgmt = "WPA-PSK-SHA256";
@@ -1186,8 +1191,8 @@ in {
                   message = ''hostapd radio ${radio} bss ${bss}: uses WPA3-SAE in transition mode requires defining both a wpa password option and a sae password option'';
                 }
                 {
-                  assertion = auth.mode == "wpa2-sha256" -> countWpaPasswordDefinitions == 1;
-                  message = ''hostapd radio ${radio} bss ${bss}: uses WPA2-SHA256 which requires defining a wpa password option'';
+                  assertion = (auth.mode == "wpa2-sha1" || auth.mode == "wpa2-sha256") -> countWpaPasswordDefinitions == 1;
+                  message = ''hostapd radio ${radio} bss ${bss}: uses WPA2-PSK which requires defining a wpa password option'';
                 }
               ])
               radioCfg.networks))
diff --git a/nixos/modules/services/networking/inadyn.nix b/nixos/modules/services/networking/inadyn.nix
index baa4302096c2c..7022673538c8a 100644
--- a/nixos/modules/services/networking/inadyn.nix
+++ b/nixos/modules/services/networking/inadyn.nix
@@ -202,7 +202,7 @@ in
         startAt = cfg.interval;
         serviceConfig = {
           Type = "oneshot";
-          ExecStart = ''${lib.getExe pkgs.inadyn} -f ${configFile} --cache-dir ''${CACHE_DIRECTORY}/inadyn -1 --foreground -l ${cfg.logLevel}'';
+          ExecStart = ''${lib.getExe pkgs.inadyn} -f ${configFile} --cache-dir ''${CACHE_DIRECTORY} -1 --foreground -l ${cfg.logLevel}'';
           LoadCredential = "config:${configFile}";
           CacheDirectory = "inadyn";
 
diff --git a/nixos/modules/services/networking/jotta-cli.md b/nixos/modules/services/networking/jotta-cli.md
new file mode 100644
index 0000000000000..335e5c8e38563
--- /dev/null
+++ b/nixos/modules/services/networking/jotta-cli.md
@@ -0,0 +1,27 @@
+# Jottacloud Command-line Tool {#module-services-jotta-cli}
+
+The [Jottacloud Command-line Tool](https://docs.jottacloud.com/en/articles/1436834-jottacloud-command-line-tool) is a headless [Jottacloud](https://jottacloud.com) client.
+
+## Quick Start {#module-services-jotta-cli-quick-start}
+
+```nix
+{
+  services.jotta-cli.enable = true;
+}
+```
+
+This adds `jotta-cli` to `environment.systemPackages` and starts a user service that runs `jottad` with the default options.
+
+## Example Configuration {#module-services-jotta-cli-example-configuration}
+
+```nix
+services.jotta-cli = {
+  enable = true;
+  options = [ "slow" ];
+  package = pkgs.jotta-cli;
+};
+```
+
+This uses `jotta-cli` and `jottad` from the `pkgs.jotta-cli` package and starts `jottad` in low memory mode.
+
+`jottad` is also added to `environment.systemPackages`, so `jottad --help` can be used to explore options.
diff --git a/nixos/modules/services/networking/jotta-cli.nix b/nixos/modules/services/networking/jotta-cli.nix
new file mode 100644
index 0000000000000..e0fa1ef332fe6
--- /dev/null
+++ b/nixos/modules/services/networking/jotta-cli.nix
@@ -0,0 +1,43 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let cfg = config.services.jotta-cli;
+in {
+  options = {
+    services.jotta-cli = {
+
+      enable = mkEnableOption "Jottacloud Command-line Tool";
+
+      options = mkOption {
+        default = [ "stdoutlog" "datadir" "%h/.jottad/" ];
+        example = [ ];
+        type = with types; listOf str;
+        description = "Command-line options passed to jottad.";
+      };
+
+      package = lib.mkPackageOption pkgs "jotta-cli" { };
+    };
+  };
+  config = mkIf cfg.enable {
+    systemd.user.services.jottad = {
+
+      description = "Jottacloud Command-line Tool daemon";
+
+      serviceConfig = {
+        Type = "notify";
+        EnvironmentFile = "-%h/.config/jotta-cli/jotta-cli.env";
+        ExecStart = "${lib.getExe' cfg.package "jottad"} ${concatStringsSep " " cfg.options}";
+        Restart = "on-failure";
+      };
+
+      wantedBy = [ "default.target" ];
+      wants = [ "network-online.target" ];
+      after = [ "network-online.target" ];
+    };
+    environment.systemPackages = [ pkgs.jotta-cli ];
+  };
+
+  meta.maintainers = with lib.maintainers; [ evenbrenden ];
+  meta.doc = ./jotta-cli.md;
+}
diff --git a/nixos/modules/services/networking/kea.nix b/nixos/modules/services/networking/kea.nix
index 66173c145d16a..11add600b66fb 100644
--- a/nixos/modules/services/networking/kea.nix
+++ b/nixos/modules/services/networking/kea.nix
@@ -278,6 +278,9 @@ in
         "https://kea.readthedocs.io/en/kea-${package.version}/arm/agent.html"
       ];
 
+      wants = [
+        "network-online.target"
+      ];
       after = [
         "network-online.target"
         "time-sync.target"
diff --git a/nixos/modules/services/networking/mihomo.nix b/nixos/modules/services/networking/mihomo.nix
index 312530caeaade..d4bb10496279d 100644
--- a/nixos/modules/services/networking/mihomo.nix
+++ b/nixos/modules/services/networking/mihomo.nix
@@ -25,6 +25,7 @@ in
     webui = lib.mkOption {
       default = null;
       type = lib.types.nullOr lib.types.path;
+      example = lib.literalExpression "pkgs.metacubexd";
       description = ''
         Local web interface to use.
 
diff --git a/nixos/modules/services/networking/mycelium.nix b/nixos/modules/services/networking/mycelium.nix
index 9487a5daafee0..0d0b2945af4c1 100644
--- a/nixos/modules/services/networking/mycelium.nix
+++ b/nixos/modules/services/networking/mycelium.nix
@@ -60,6 +60,8 @@ in
     networking.firewall.allowedTCPPorts = lib.optionals cfg.openFirewall [ 9651 ];
     networking.firewall.allowedUDPPorts = lib.optionals cfg.openFirewall [ 9650 9651 ];
 
+    environment.systemPackages = [ cfg.package ];
+
     systemd.services.mycelium = {
       description = "Mycelium network";
       after = [ "network.target" ];
diff --git a/nixos/modules/services/networking/netbird.nix b/nixos/modules/services/networking/netbird.nix
index 7add377896cab..e68c39946fe3b 100644
--- a/nixos/modules/services/networking/netbird.nix
+++ b/nixos/modules/services/networking/netbird.nix
@@ -37,7 +37,6 @@ in
 {
   meta.maintainers = with maintainers; [
     misuzu
-    thubrecht
   ];
   meta.doc = ./netbird.md;
 
diff --git a/nixos/modules/services/networking/netbird/coturn.nix b/nixos/modules/services/networking/netbird/coturn.nix
index 746d70a07250d..29ff1e8fc15ee 100644
--- a/nixos/modules/services/networking/netbird/coturn.nix
+++ b/nixos/modules/services/networking/netbird/coturn.nix
@@ -60,6 +60,7 @@ in
       default = null;
       description = ''
         The password of the user used by netbird to connect to the coturn server.
+        Be advised this will be world readable in the nix store.
       '';
     };
 
@@ -142,7 +143,11 @@ in
           ];
         });
 
-      security.acme.certs.${cfg.domain}.postRun = optionalString cfg.useAcmeCertificates "systemctl restart coturn.service";
+      security.acme.certs = mkIf cfg.useAcmeCertificates {
+        ${cfg.domain}.postRun = ''
+          systemctl restart coturn.service
+        '';
+      };
 
       networking.firewall = {
         allowedUDPPorts = cfg.openPorts;
diff --git a/nixos/modules/services/networking/netbird/server.nix b/nixos/modules/services/networking/netbird/server.nix
index a4de0fda6a134..e3de286a04fa4 100644
--- a/nixos/modules/services/networking/netbird/server.nix
+++ b/nixos/modules/services/networking/netbird/server.nix
@@ -2,6 +2,7 @@
 
 let
   inherit (lib)
+    mkDefault
     mkEnableOption
     mkIf
     mkOption
@@ -15,7 +16,7 @@ in
 
 {
   meta = {
-    maintainers = with lib.maintainers; [ thubrecht ];
+    maintainers = with lib.maintainers; [patrickdag];
     doc = ./server.md;
   };
 
@@ -41,26 +42,46 @@ in
   config = mkIf cfg.enable {
     services.netbird.server = {
       dashboard = {
-        inherit (cfg) enable domain enableNginx;
+        domain = mkDefault cfg.domain;
+        enable = mkDefault cfg.enable;
+        enableNginx = mkDefault cfg.enableNginx;
 
         managementServer = "https://${cfg.domain}";
       };
 
       management =
         {
-          inherit (cfg) enable domain enableNginx;
+          domain = mkDefault cfg.domain;
+          enable = mkDefault cfg.enable;
+          enableNginx = mkDefault cfg.enableNginx;
         }
-        // (optionalAttrs cfg.coturn.enable {
+        // (optionalAttrs cfg.coturn.enable rec {
           turnDomain = cfg.domain;
           turnPort = config.services.coturn.tls-listening-port;
+          # We cannot merge a list of attrsets so we have to redefine the whole list
+          settings = {
+            TURNConfig.Turns = mkDefault [
+              {
+                Proto = "udp";
+                URI = "turn:${turnDomain}:${builtins.toString turnPort}";
+                Username = "netbird";
+                Password =
+                  if (cfg.coturn.password != null)
+                  then cfg.coturn.password
+                  else {_secret = cfg.coturn.passwordFile;};
+              }
+            ];
+          };
         });
 
       signal = {
-        inherit (cfg) enable domain enableNginx;
+        domain = mkDefault cfg.domain;
+        enable = mkDefault cfg.enable;
+        enableNginx = mkDefault cfg.enableNginx;
       };
 
       coturn = {
-        inherit (cfg) domain;
+        domain = mkDefault cfg.domain;
       };
     };
   };
diff --git a/nixos/modules/services/networking/networkmanager.nix b/nixos/modules/services/networking/networkmanager.nix
index e33bbb2af178f..b7143cf520f96 100644
--- a/nixos/modules/services/networking/networkmanager.nix
+++ b/nixos/modules/services/networking/networkmanager.nix
@@ -470,7 +470,7 @@ in
       -      [main]
       -      no-auto-default=*
       -    '''
-      +    extraConfig.main.no-auto-default = "*";
+      +    settings.main.no-auto-default = "*";
          };
       ```
     ''
diff --git a/nixos/modules/services/networking/oink.nix b/nixos/modules/services/networking/oink.nix
new file mode 100644
index 0000000000000..cd0fdf172331d
--- /dev/null
+++ b/nixos/modules/services/networking/oink.nix
@@ -0,0 +1,84 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.oink;
+  makeOinkConfig = attrs: (pkgs.formats.json { }).generate
+    "oink.json" (mapAttrs' (k: v: nameValuePair (toLower k) v) attrs);
+  oinkConfig = makeOinkConfig {
+    global = cfg.settings;
+    domains = cfg.domains;
+  };
+in
+{
+  options.services.oink = {
+    enable = mkEnableOption "Oink, a dynamic DNS client for Porkbun";
+    package = mkPackageOption pkgs "oink" { };
+    settings = {
+      apiKey = mkOption {
+        type = types.str;
+        description = "API key to use when modifying DNS records.";
+      };
+      secretApiKey = mkOption {
+        type = types.str;
+        description = "Secret API key to use when modifying DNS records.";
+      };
+      interval = mkOption {
+        # https://github.com/rlado/oink/blob/v1.1.1/src/main.go#L364
+        type = types.ints.between 60 172800; # 48 hours
+        default = 900;
+        description = "Seconds to wait before sending another request.";
+      };
+      ttl = mkOption {
+        type = types.ints.between 600 172800;
+        default = 600;
+        description = ''
+          The TTL ("Time to Live") value to set for your DNS records.
+
+          The TTL controls how long in seconds your records will be cached
+          for. A smaller value will allow the record to update quicker.
+        '';
+      };
+    };
+    domains = mkOption {
+      type = with types; listOf (attrsOf anything);
+      default = [];
+      example = [
+        {
+          domain = "nixos.org";
+          subdomain = "";
+          ttl = 1200;
+        }
+        {
+          domain = "nixos.org";
+          subdomain = "hydra";
+        }
+      ];
+      description = ''
+        List of attribute sets containing configuration for each domain.
+
+        Each attribute set must have two attributes, one named *domain*
+        and another named *subdomain*. The domain attribute must specify
+        the root domain that you want to configure, and the subdomain
+        attribute must specify its subdomain if any. If you want to
+        configure the root domain rather than a subdomain, leave the
+        subdomain attribute as an empty string.
+
+        Additionally, you can use attributes from *services.oink.settings*
+        to override settings per-domain.
+
+        Every domain listed here *must* have API access enabled in
+        Porkbun's control panel.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.oink = {
+      description = "Dynamic DNS client for Porkbun";
+      wantedBy = [ "multi-user.target" ];
+      script = "${cfg.package}/bin/oink -c ${oinkConfig}";
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/pixiecore.nix b/nixos/modules/services/networking/pixiecore.nix
index cfdb8014136ed..111cb7e355040 100644
--- a/nixos/modules/services/networking/pixiecore.nix
+++ b/nixos/modules/services/networking/pixiecore.nix
@@ -82,8 +82,8 @@ in
 
       apiServer = mkOption {
         type = types.str;
-        example = "localhost:8080";
-        description = "host:port to connect to the API. Ignored unless mode is set to 'api'";
+        example = "http://localhost:8080";
+        description = "URI to connect to the API. Ignored unless mode is set to 'api'";
       };
 
       extraArguments = mkOption {
diff --git a/nixos/modules/services/networking/radvd.nix b/nixos/modules/services/networking/radvd.nix
index 4e3e501d2f593..0143324a78155 100644
--- a/nixos/modules/services/networking/radvd.nix
+++ b/nixos/modules/services/networking/radvd.nix
@@ -33,6 +33,17 @@ in
 
     package = mkPackageOption pkgs "radvd" { };
 
+    debugLevel = mkOption {
+      type = types.int;
+      default = 0;
+      example = 5;
+      description = ''
+          The debugging level is an integer in the range from 1 to 5,
+          from quiet to very verbose. A debugging level of 0 completely
+          turns off debugging.
+        '';
+    };
+
     config = mkOption {
       type = types.lines;
       example =
@@ -67,7 +78,7 @@ in
         wantedBy = [ "multi-user.target" ];
         after = [ "network.target" ];
         serviceConfig =
-          { ExecStart = "@${cfg.package}/bin/radvd radvd -n -u radvd -C ${confFile}";
+          { ExecStart = "@${cfg.package}/bin/radvd radvd -n -u radvd -d ${toString cfg.debugLevel} -C ${confFile}";
             Restart = "always";
           };
       };
diff --git a/nixos/modules/services/networking/rosenpass.nix b/nixos/modules/services/networking/rosenpass.nix
index 373a6c7690799..92ecc1cb31a36 100644
--- a/nixos/modules/services/networking/rosenpass.nix
+++ b/nixos/modules/services/networking/rosenpass.nix
@@ -130,8 +130,8 @@ in
             relevant = config.systemd.network.enable;
             root = config.systemd.network.netdevs;
             peer = (x: x.wireguardPeers);
-            key = (x: if x.wireguardPeerConfig ? PublicKey then x.wireguardPeerConfig.PublicKey else null);
-            description = "${options.systemd.network.netdevs}.\"<name>\".wireguardPeers.*.wireguardPeerConfig.PublicKey";
+            key = x: x.PublicKey or null;
+            description = "${options.systemd.network.netdevs}.\"<name>\".wireguardPeers.*.PublicKey";
           }
           {
             relevant = config.networking.wireguard.enable;
@@ -225,8 +225,10 @@ in
         # See <https://www.freedesktop.org/software/systemd/man/systemd.unit.html#Specifiers>
         environment.CONFIG = "%t/${serviceConfig.RuntimeDirectory}/config.toml";
 
-        preStart = "${getExe pkgs.envsubst} -i ${config} -o \"$CONFIG\"";
-        script = "rosenpass exchange-config \"$CONFIG\"";
+        script = ''
+          ${getExe pkgs.envsubst} -i ${config} -o "$CONFIG"
+          rosenpass exchange-config "$CONFIG"
+        '';
       };
   };
 }
diff --git a/nixos/modules/services/networking/smokeping.nix b/nixos/modules/services/networking/smokeping.nix
index 38d6e4452c97b..3fb3eac45cc82 100644
--- a/nixos/modules/services/networking/smokeping.nix
+++ b/nixos/modules/services/networking/smokeping.nix
@@ -47,6 +47,13 @@ let
 in
 
 {
+  imports = [
+    (mkRemovedOptionModule [ "services" "smokeping" "port" ] ''
+      The smokeping web service is now served by nginx.
+      In order to change the port, you need to change the nginx configuration under `services.nginx.virtualHosts.smokeping.listen.*.port`.
+    '')
+  ];
+
   options = {
     services.smokeping = {
       enable = mkEnableOption "smokeping service";
@@ -71,8 +78,8 @@ in
       };
       cgiUrl = mkOption {
         type = types.str;
-        default = "http://${cfg.hostName}:${toString cfg.port}/smokeping.cgi";
-        defaultText = literalExpression ''"http://''${hostName}:''${toString port}/smokeping.cgi"'';
+        default = "http://${cfg.hostName}/smokeping.cgi";
+        defaultText = literalExpression ''"http://''${hostName}/smokeping.cgi"'';
         example = "https://somewhere.example.com/smokeping.cgi";
         description = "URL to the smokeping cgi.";
       };
@@ -177,11 +184,6 @@ in
           which makes it bind to all interfaces.
         '';
       };
-      port = mkOption {
-        type = types.port;
-        default = 8081;
-        description = "TCP port to use for the web server.";
-      };
       presentationConfig = mkOption {
         type = types.lines;
         default = ''
@@ -312,17 +314,8 @@ in
       description = "smokeping daemon user";
       home = smokepingHome;
       createHome = true;
-      # When `cfg.webService` is enabled, `thttpd` makes SmokePing available
-      # under `${cfg.host}:${cfg.port}/smokeping.fcgi` as per the `ln -s` below.
-      # We also want that going to `${cfg.host}:${cfg.port}` without `smokeping.fcgi`
-      # makes it easy for the user to find SmokePing.
-      # However `thttpd` does not seem to support easy redirections from `/` to `smokeping.fcgi`
-      # and only allows directory listings or `/` -> `index.html` resolution if the directory
-      # has `chmod 755` (see https://acme.com/software/thttpd/thttpd_man.html#PERMISSIONS,
-      # " directories should be 755 if you want to allow indexing").
-      # Otherwise it shows `403 Forbidden` on `/`.
-      # Thus, we need to make `smokepingHome` (which is given to `thttpd -d` below) `755`.
-      homeMode = "755";
+      # When `cfg.webService` is enabled, `nginx` requires read permissions on the home directory.
+      homeMode = "711";
     };
     users.groups.${cfg.user} = { };
     systemd.services.smokeping = {
@@ -342,21 +335,25 @@ in
         ${cfg.package}/bin/smokeping --static --config=${configPath}
       '';
     };
-    systemd.services.thttpd = mkIf cfg.webService {
-      requiredBy = [ "multi-user.target" ];
-      requires = [ "smokeping.service" ];
-      path = with pkgs; [ bash rrdtool smokeping thttpd ];
-      serviceConfig = {
-        Restart = "always";
-        ExecStart = lib.concatStringsSep " " (lib.concatLists [
-          [ "${pkgs.thttpd}/bin/thttpd" ]
-          [ "-u ${cfg.user}" ]
-          [ ''-c "**.fcgi"'' ]
-          [ "-d ${smokepingHome}" ]
-          (lib.optional (cfg.host != null) "-h ${cfg.host}")
-          [ "-p ${builtins.toString cfg.port}" ]
-          [ "-D -nos" ]
-        ]);
+
+    # use nginx to serve the smokeping web service
+    services.fcgiwrap.enable = mkIf cfg.webService true;
+    services.nginx = mkIf cfg.webService {
+      enable = true;
+      virtualHosts."smokeping" = {
+        serverName = mkDefault cfg.host;
+        locations."/" = {
+          root = smokepingHome;
+          index = "smokeping.fcgi";
+        };
+        locations."/smokeping.fcgi" = {
+          extraConfig = ''
+            include ${config.services.nginx.package}/conf/fastcgi_params;
+            fastcgi_pass unix:${config.services.fcgiwrap.socketAddress};
+            fastcgi_param SCRIPT_FILENAME ${smokepingHome}/smokeping.fcgi;
+            fastcgi_param DOCUMENT_ROOT ${smokepingHome};
+          '';
+        };
       };
     };
   };
diff --git a/nixos/modules/services/networking/ssh/sshd.nix b/nixos/modules/services/networking/ssh/sshd.nix
index c62bccd462d33..1e4e34a4f1675 100644
--- a/nixos/modules/services/networking/ssh/sshd.nix
+++ b/nixos/modules/services/networking/ssh/sshd.nix
@@ -5,11 +5,11 @@ with lib;
 let
 
   # The splicing information needed for nativeBuildInputs isn't available
-  # on the derivations likely to be used as `cfgc.package`.
+  # on the derivations likely to be used as `cfg.package`.
   # This middle-ground solution ensures *an* sshd can do their basic validation
   # on the configuration.
   validationPackage = if pkgs.stdenv.buildPlatform == pkgs.stdenv.hostPlatform
-    then cfgc.package
+    then cfg.package
     else pkgs.buildPackages.openssh;
 
   # dont use the "=" operator
@@ -169,6 +169,13 @@ in
         '';
       };
 
+      package = mkOption {
+        type = types.package;
+        default = config.programs.ssh.package;
+        defaultText = literalExpression "programs.ssh.package";
+        description = "OpenSSH package to use for sshd.";
+      };
+
       startWhenNeeded = mkOption {
         type = types.bool;
         default = false;
@@ -296,6 +303,17 @@ in
         '';
       };
 
+      authorizedKeysInHomedir = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Enables the use of the `~/.ssh/authorized_keys` file.
+
+          Otherwise, the only files trusted by default are those in `/etc/ssh/authorized_keys.d`,
+          *i.e.* SSH keys from [](#opt-users.users._name_.openssh.authorizedKeys.keys).
+        '';
+      };
+
       authorizedKeysCommand = mkOption {
         type = types.str;
         default = "none";
@@ -331,7 +349,7 @@ in
           freeformType = settingsFormat.type;
           options = {
             AuthorizedPrincipalsFile = mkOption {
-              type = types.str;
+              type = types.nullOr types.str;
               default = "none"; # upstream default
               description = ''
                 Specifies a file that lists principal names that are accepted for certificate authentication. The default
@@ -339,16 +357,18 @@ in
               '';
             };
             LogLevel = mkOption {
-              type = types.enum [ "QUIET" "FATAL" "ERROR" "INFO" "VERBOSE" "DEBUG" "DEBUG1" "DEBUG2" "DEBUG3" ];
+              type = types.nullOr (types.enum [ "QUIET" "FATAL" "ERROR" "INFO" "VERBOSE" "DEBUG" "DEBUG1" "DEBUG2" "DEBUG3" ]);
               default = "INFO"; # upstream default
               description = ''
                 Gives the verbosity level that is used when logging messages from sshd(8). Logging with a DEBUG level
                 violates the privacy of users and is not recommended.
               '';
             };
-            UsePAM = mkEnableOption "PAM authentication" // { default = true; };
+            UsePAM =
+              mkEnableOption "PAM authentication"
+              // { default = true; type = types.nullOr types.bool; };
             UseDns = mkOption {
-              type = types.bool;
+              type = types.nullOr types.bool;
               # apply if cfg.useDns then "yes" else "no"
               default = false;
               description = ''
@@ -359,14 +379,14 @@ in
               '';
             };
             X11Forwarding = mkOption {
-              type = types.bool;
+              type = types.nullOr types.bool;
               default = false;
               description = ''
                 Whether to allow X11 connections to be forwarded.
               '';
             };
             PasswordAuthentication = mkOption {
-              type = types.bool;
+              type = types.nullOr types.bool;
               default = true;
               description = ''
                 Specifies whether password authentication is allowed.
@@ -374,20 +394,20 @@ in
             };
             PermitRootLogin = mkOption {
               default = "prohibit-password";
-              type = types.enum ["yes" "without-password" "prohibit-password" "forced-commands-only" "no"];
+              type = types.nullOr (types.enum ["yes" "without-password" "prohibit-password" "forced-commands-only" "no"]);
               description = ''
                 Whether the root user can login using ssh.
               '';
             };
             KbdInteractiveAuthentication = mkOption {
-              type = types.bool;
+              type = types.nullOr types.bool;
               default = true;
               description = ''
                 Specifies whether keyboard-interactive authentication is allowed.
               '';
             };
             GatewayPorts = mkOption {
-              type = types.str;
+              type = types.nullOr types.str;
               default = "no";
               description = ''
                 Specifies whether remote hosts are allowed to connect to
@@ -396,7 +416,7 @@ in
               '';
             };
             KexAlgorithms = mkOption {
-              type = types.listOf types.str;
+              type = types.nullOr (types.listOf types.str);
               default = [
                 "sntrup761x25519-sha512@openssh.com"
                 "curve25519-sha256"
@@ -413,7 +433,7 @@ in
               '';
             };
             Macs = mkOption {
-              type = types.listOf types.str;
+              type = types.nullOr (types.listOf types.str);
               default = [
                 "hmac-sha2-512-etm@openssh.com"
                 "hmac-sha2-256-etm@openssh.com"
@@ -429,14 +449,14 @@ in
               '';
             };
             StrictModes = mkOption {
-              type = types.bool;
+              type = types.nullOr (types.bool);
               default = true;
               description = ''
                 Whether sshd should check file modes and ownership of directories
               '';
             };
             Ciphers = mkOption {
-              type = types.listOf types.str;
+              type = types.nullOr (types.listOf types.str);
               default = [
                 "chacha20-poly1305@openssh.com"
                 "aes256-gcm@openssh.com"
@@ -491,7 +511,9 @@ in
               '';
             };
             # Disabled by default, since pam_motd handles this.
-            PrintMotd = mkEnableOption "printing /etc/motd when a user logs in interactively";
+            PrintMotd =
+              mkEnableOption "printing /etc/motd when a user logs in interactively"
+              // { type = types.nullOr types.bool; };
           };
         });
       };
@@ -533,8 +555,8 @@ in
       };
     users.groups.sshd = {};
 
-    services.openssh.moduliFile = mkDefault "${cfgc.package}/etc/ssh/moduli";
-    services.openssh.sftpServerExecutable = mkDefault "${cfgc.package}/libexec/sftp-server";
+    services.openssh.moduliFile = mkDefault "${cfg.package}/etc/ssh/moduli";
+    services.openssh.sftpServerExecutable = mkDefault "${cfg.package}/libexec/sftp-server";
 
     environment.etc = authKeysFiles // authPrincipalsFiles //
       { "ssh/moduli".source = cfg.moduliFile;
@@ -548,7 +570,7 @@ in
             wantedBy = optional (!cfg.startWhenNeeded) "multi-user.target";
             after = [ "network.target" ];
             stopIfChanged = false;
-            path = [ cfgc.package pkgs.gawk ];
+            path = [ cfg.package pkgs.gawk ];
             environment.LD_LIBRARY_PATH = nssModulesPath;
 
             restartTriggers = optionals (!cfg.startWhenNeeded) [
@@ -582,7 +604,7 @@ in
             serviceConfig =
               { ExecStart =
                   (optionalString cfg.startWhenNeeded "-") +
-                  "${cfgc.package}/bin/sshd " + (optionalString cfg.startWhenNeeded "-i ") +
+                  "${cfg.package}/bin/sshd " + (optionalString cfg.startWhenNeeded "-i ") +
                   "-D " +  # don't detach into a daemon process
                   "-f /etc/ssh/sshd_config";
                 KillMode = "process";
@@ -628,14 +650,17 @@ in
     security.pam.services.sshd = lib.mkIf cfg.settings.UsePAM
       { startSession = true;
         showMotd = true;
-        unixAuth = cfg.settings.PasswordAuthentication;
+        unixAuth =
+          if cfg.settings.PasswordAuthentication == true
+          then true
+          else false;
       };
 
     # These values are merged with the ones defined externally, see:
     # https://github.com/NixOS/nixpkgs/pull/10155
     # https://github.com/NixOS/nixpkgs/pull/41745
     services.openssh.authorizedKeysFiles =
-      [ "%h/.ssh/authorized_keys" "/etc/ssh/authorized_keys.d/%u" ];
+      lib.optional cfg.authorizedKeysInHomedir "%h/.ssh/authorized_keys" ++ [ "/etc/ssh/authorized_keys.d/%u" ];
 
     services.openssh.settings.AuthorizedPrincipalsFile = mkIf (authPrincipalsFiles != {}) "/etc/ssh/authorized_principals.d/%u";
 
@@ -690,6 +715,10 @@ in
 
     assertions = [{ assertion = if cfg.settings.X11Forwarding then cfgc.setXAuthLocation else true;
                     message = "cannot enable X11 forwarding without setting xauth location";}
+                  { assertion = (builtins.match "(.*\n)?(\t )*[Kk][Ee][Rr][Bb][Ee][Rr][Oo][Ss][Aa][Uu][Tt][Hh][Ee][Nn][Tt][Ii][Cc][Aa][Tt][Ii][Oo][Nn][ |\t|=|\"]+yes.*" "${configFile}\n${cfg.extraConfig}") != null -> cfgc.package.withKerberos;
+                    message = "cannot enable Kerberos authentication without using a package with Kerberos support";}
+                  { assertion = (builtins.match "(.*\n)?(\t )*[Gg][Ss][Ss][Aa][Pp][Ii][Aa][Uu][Tt][Hh][Ee][Nn][Tt][Ii][Cc][Aa][Tt][Ii][Oo][Nn][ |\t|=|\"]+yes.*" "${configFile}\n${cfg.extraConfig}") != null -> cfgc.package.withKerberos;
+                    message = "cannot enable GSSAPI authentication without using a package with Kerberos support";}
                   (let
                     duplicates =
                       # Filter out the groups with more than 1 element
diff --git a/nixos/modules/services/networking/sunshine.nix b/nixos/modules/services/networking/sunshine.nix
index 0749eaee95d8a..ec78db1f3f8e9 100644
--- a/nixos/modules/services/networking/sunshine.nix
+++ b/nixos/modules/services/networking/sunshine.nix
@@ -1,6 +1,6 @@
 { config, lib, pkgs, utils, ... }:
 let
-  inherit (lib) mkEnableOption mkPackageOption mkOption mkIf mkDefault types optionals getExe;
+  inherit (lib) mkEnableOption mkPackageOption mkOption literalExpression mkIf mkDefault types optionals getExe;
   inherit (utils) escapeSystemdExecArgs;
   cfg = config.services.sunshine;
 
@@ -46,7 +46,7 @@ in
 
         See https://docs.lizardbyte.dev/projects/sunshine/en/latest/about/advanced_usage.html#configuration for syntax.
       '';
-      example = ''
+      example = literalExpression ''
         {
           sunshine_name = "nixos";
         }
@@ -67,7 +67,7 @@ in
       description = ''
         Configuration for applications to be exposed to Moonlight. If this is set, no configuration is possible from the web UI, and must be by the `settings` option.
       '';
-      example = ''
+      example = literalExpression ''
         {
           env = {
             PATH = "$(PATH):$(HOME)/.local/bin";
diff --git a/nixos/modules/services/networking/tailscale-auth.nix b/nixos/modules/services/networking/tailscale-auth.nix
index c3a515212e782..f21d1f108911c 100644
--- a/nixos/modules/services/networking/tailscale-auth.nix
+++ b/nixos/modules/services/networking/tailscale-auth.nix
@@ -14,7 +14,7 @@ let
 in
 {
   options.services.tailscaleAuth = {
-    enable = mkEnableOption "Enable tailscale.nginx-auth, to authenticate users via tailscale.";
+    enable = mkEnableOption "tailscale.nginx-auth, to authenticate users via tailscale";
 
     package = mkPackageOption pkgs "tailscale-nginx-auth" {};
 
diff --git a/nixos/modules/services/networking/tailscale.nix b/nixos/modules/services/networking/tailscale.nix
index a79e47d8491b8..a690dc610e825 100644
--- a/nixos/modules/services/networking/tailscale.nix
+++ b/nixos/modules/services/networking/tailscale.nix
@@ -61,12 +61,21 @@ in {
     };
 
     extraUpFlags = mkOption {
-      description = "Extra flags to pass to {command}`tailscale up`.";
+      description = ''
+        Extra flags to pass to {command}`tailscale up`. Only applied if `authKeyFile` is specified.";
+      '';
       type = types.listOf types.str;
       default = [];
       example = ["--ssh"];
     };
 
+    extraSetFlags = mkOption {
+      description = "Extra flags to pass to {command}`tailscale set`.";
+      type = types.listOf types.str;
+      default = [];
+      example = ["--advertise-exit-node"];
+    };
+
     extraDaemonFlags = mkOption {
       description = "Extra flags to pass to {command}`tailscaled`.";
       type = types.listOf types.str;
@@ -120,6 +129,18 @@ in {
       '';
     };
 
+    systemd.services.tailscaled-set = mkIf (cfg.extraSetFlags != []) {
+      after = ["tailscaled.service"];
+      wants = ["tailscaled.service"];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        Type = "oneshot";
+      };
+      script = ''
+        ${cfg.package}/bin/tailscale set ${escapeShellArgs cfg.extraSetFlags}
+      '';
+    };
+
     boot.kernel.sysctl = mkIf (cfg.useRoutingFeatures == "server" || cfg.useRoutingFeatures == "both") {
       "net.ipv4.conf.all.forwarding" = mkOverride 97 true;
       "net.ipv6.conf.all.forwarding" = mkOverride 97 true;
diff --git a/nixos/modules/services/networking/tayga.nix b/nixos/modules/services/networking/tayga.nix
index 1a0df33fe883d..9f118b243e90c 100644
--- a/nixos/modules/services/networking/tayga.nix
+++ b/nixos/modules/services/networking/tayga.nix
@@ -16,6 +16,8 @@ let
     prefix ${strAddr cfg.ipv6.pool}
     dynamic-pool ${strAddr cfg.ipv4.pool}
     data-dir ${cfg.dataDir}
+
+    ${concatStringsSep "\n" (mapAttrsToList (ipv4: ipv6: "map " + ipv4 + " " + ipv6) cfg.mappings)}
   '';
 
   addrOpts = v:
@@ -103,18 +105,38 @@ in
       dataDir = mkOption {
         type = types.path;
         default = "/var/lib/tayga";
-        description = "Directory for persistent data";
+        description = "Directory for persistent data.";
       };
 
       tunDevice = mkOption {
         type = types.str;
         default = "nat64";
-        description = "Name of the nat64 tun device";
+        description = "Name of the nat64 tun device.";
+      };
+
+      mappings = mkOption {
+        type = types.attrsOf types.str;
+        default = {};
+        description = "Static IPv4 -> IPv6 host mappings.";
+        example = literalExpression ''
+          {
+            "192.168.5.42" = "2001:db8:1:4444::1";
+            "192.168.5.43" = "2001:db8:1:4444::2";
+            "192.168.255.2" = "2001:db8:1:569::143";
+          }
+        '';
       };
     };
   };
 
   config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = allUnique (attrValues cfg.mappings);
+        message = "Neither the IPv4 nor the IPv6 addresses must be entered twice in the mappings.";
+      }
+    ];
+
     networking.interfaces."${cfg.tunDevice}" = {
       virtual = true;
       virtualType = "tun";
diff --git a/nixos/modules/services/networking/trust-dns.nix b/nixos/modules/services/networking/trust-dns.nix
index e6f8cc15819f6..039b7de263504 100644
--- a/nixos/modules/services/networking/trust-dns.nix
+++ b/nixos/modules/services/networking/trust-dns.nix
@@ -51,7 +51,7 @@ in
       package = mkPackageOption pkgs "trust-dns" {
         extraDescription = ''
           ::: {.note}
-          The package must provide `meta.mainProgram` which names the server binayr; any other utilities (client, resolver) are not needed.
+          The package must provide `meta.mainProgram` which names the server binary; any other utilities (client, resolver) are not needed.
           :::
         '';
       };
@@ -86,7 +86,7 @@ in
               type = types.listOf types.str;
               default = [ "0.0.0.0" ];
               description = ''
-              List of ipv4 addresses on which to listen for DNS queries.
+                List of ipv4 addresses on which to listen for DNS queries.
               '';
             };
             listen_addrs_ipv6 = mkOption {
@@ -114,7 +114,7 @@ in
             };
             zones = mkOption {
               description = "List of zones to serve.";
-              default = {};
+              default = [];
               type = types.listOf (types.coercedTo types.str (zone: { inherit zone; }) zoneType);
             };
           };
diff --git a/nixos/modules/services/networking/vsftpd.nix b/nixos/modules/services/networking/vsftpd.nix
index 25f950600b91c..07b93e92a7509 100644
--- a/nixos/modules/services/networking/vsftpd.nix
+++ b/nixos/modules/services/networking/vsftpd.nix
@@ -278,7 +278,7 @@ in
       }
       {
         assertion = (cfg.enableVirtualUsers -> cfg.userDbPath != null)
-                 && (cfg.enableVirtualUsers -> cfg.localUsers != null);
+                 && (cfg.enableVirtualUsers -> cfg.localUsers);
         message = "vsftpd: If enableVirtualUsers is true, you need to setup both the userDbPath and localUsers options.";
       }];
 
diff --git a/nixos/modules/services/networking/wireguard.nix b/nixos/modules/services/networking/wireguard.nix
index 3f68af3a86c96..81abae2c9303d 100644
--- a/nixos/modules/services/networking/wireguard.nix
+++ b/nixos/modules/services/networking/wireguard.nix
@@ -80,6 +80,15 @@ let
         description = "Commands called at the end of the interface setup.";
       };
 
+      preShutdown = mkOption {
+        example = literalExpression ''"''${pkgs.iproute2}/bin/ip netns del foo"'';
+        default = "";
+        type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines;
+        description = ''
+          Commands called before shutting down the interface.
+        '';
+      };
+
       postShutdown = mkOption {
         example = literalExpression ''"''${pkgs.openresolv}/bin/resolvconf -d wg0"'';
         default = "";
@@ -497,6 +506,7 @@ let
         '';
 
         postStop = ''
+          ${values.preShutdown}
           ${ipPostMove} link del dev "${name}"
           ${values.postShutdown}
         '';
diff --git a/nixos/modules/services/networking/wstunnel.nix b/nixos/modules/services/networking/wstunnel.nix
index efb65aead116a..bd7536351955a 100644
--- a/nixos/modules/services/networking/wstunnel.nix
+++ b/nixos/modules/services/networking/wstunnel.nix
@@ -1,95 +1,94 @@
-{ config, lib, options, pkgs, utils, ... }:
-with lib;
+{ config
+, lib
+, pkgs
+, ...
+}:
+
 let
   cfg = config.services.wstunnel;
-  attrsToArgs = attrs: utils.escapeSystemdExecArgs (
-    mapAttrsToList
-    (name: value: if value == true then "--${name}" else "--${name}=${value}")
-    attrs
-  );
+
+  hostPortToString = { host, port }: "${host}:${toString port}";
+
   hostPortSubmodule = {
     options = {
-      host = mkOption {
+      host = lib.mkOption {
         description = "The hostname.";
-        type = types.str;
+        type = lib.types.str;
       };
-      port = mkOption {
+      port = lib.mkOption {
         description = "The port.";
-        type = types.port;
-      };
-    };
-  };
-  localRemoteSubmodule = {
-    options = {
-      local = mkOption {
-        description = "Local address and port to listen on.";
-        type = types.submodule hostPortSubmodule;
-        example = {
-          host = "127.0.0.1";
-          port = 51820;
-        };
-      };
-      remote = mkOption {
-        description = "Address and port on remote to forward traffic to.";
-        type = types.submodule hostPortSubmodule;
-        example = {
-          host = "127.0.0.1";
-          port = 51820;
-        };
+        type = lib.types.port;
       };
     };
   };
-  hostPortToString = { host, port }: "${host}:${builtins.toString port}";
-  localRemoteToString = { local, remote }: utils.escapeSystemdExecArg "${hostPortToString local}:${hostPortToString remote}";
+
   commonOptions = {
-    enable = mkOption {
-      description = "Whether to enable this `wstunnel` instance.";
-      type = types.bool;
+    enable = lib.mkEnableOption "this `wstunnel` instance." // {
       default = true;
     };
 
-    package = mkPackageOption pkgs "wstunnel" {};
+    package = lib.mkPackageOption pkgs "wstunnel" { };
 
-    autoStart = mkOption {
-      description = "Whether this tunnel server should be started automatically.";
-      type = types.bool;
-      default = true;
-    };
+    autoStart =
+      lib.mkEnableOption "starting this wstunnel instance automatically." // {
+        default = true;
+      };
 
-    extraArgs = mkOption {
-      description = "Extra command line arguments to pass to `wstunnel`. Attributes of the form `argName = true;` will be translated to `--argName`, and `argName = \"value\"` to `--argName=value`.";
-      type = with types; attrsOf (either str bool);
-      default = {};
+    extraArgs = lib.mkOption {
+      description = ''
+        Extra command line arguments to pass to `wstunnel`.
+        Attributes of the form `argName = true;` will be translated to `--argName`,
+        and `argName = \"value\"` to `--argName value`.
+      '';
+      type = with lib.types; attrsOf (either str bool);
+      default = { };
       example = {
         "someNewOption" = true;
         "someNewOptionWithValue" = "someValue";
       };
     };
 
-    verboseLogging = mkOption {
-      description = "Enable verbose logging.";
-      type = types.bool;
-      default = false;
+    loggingLevel = lib.mkOption {
+      description = ''
+        Passed to --log-lvl
+
+        Control the log verbosity. i.e: TRACE, DEBUG, INFO, WARN, ERROR, OFF
+        For more details, checkout [EnvFilter](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#example-syntax)
+      '';
+      type = lib.types.nullOr lib.types.str;
+      example = "INFO";
+      default = null;
     };
 
-    environmentFile = mkOption {
-      description = "Environment file to be passed to the systemd service. Useful for passing secrets to the service to prevent them from being world-readable in the Nix store. Note however that the secrets are passed to `wstunnel` through the command line, which makes them locally readable for all users of the system at runtime.";
-      type = types.nullOr types.path;
+    environmentFile = lib.mkOption {
+      description = ''
+        Environment file to be passed to the systemd service.
+        Useful for passing secrets to the service to prevent them from being
+        world-readable in the Nix store.
+        Note however that the secrets are passed to `wstunnel` through
+        the command line, which makes them locally readable for all users of
+        the system at runtime.
+      '';
+      type = lib.types.nullOr lib.types.path;
       default = null;
       example = "/var/lib/secrets/wstunnelSecrets";
     };
   };
 
-  serverSubmodule = { config, ...}: {
+  serverSubmodule = { config, ... }: {
     options = commonOptions // {
-      listen = mkOption {
-        description = "Address and port to listen on. Setting the port to a value below 1024 will also give the process the required `CAP_NET_BIND_SERVICE` capability.";
-        type = types.submodule hostPortSubmodule;
+      listen = lib.mkOption {
+        description = ''
+          Address and port to listen on.
+          Setting the port to a value below 1024 will also give the process
+          the required `CAP_NET_BIND_SERVICE` capability.
+        '';
+        type = lib.types.submodule hostPortSubmodule;
         default = {
           host = "0.0.0.0";
           port = if config.enableHTTPS then 443 else 80;
         };
-        defaultText = literalExpression ''
+        defaultText = lib.literalExpression ''
           {
             host = "0.0.0.0";
             port = if enableHTTPS then 443 else 80;
@@ -97,250 +96,252 @@ let
         '';
       };
 
-      restrictTo = mkOption {
-        description = "Accepted traffic will be forwarded only to this service. Set to `null` to allow forwarding to arbitrary addresses.";
-        type = types.nullOr (types.submodule hostPortSubmodule);
-        example = {
+      restrictTo = lib.mkOption {
+        description = ''
+          Accepted traffic will be forwarded only to this service.
+        '';
+        type = lib.types.listOf (lib.types.submodule hostPortSubmodule);
+        default = [ ];
+        example = [{
           host = "127.0.0.1";
           port = 51820;
-        };
+        }];
       };
 
-      enableHTTPS = mkOption {
+      enableHTTPS = lib.mkOption {
         description = "Use HTTPS for the tunnel server.";
-        type = types.bool;
+        type = lib.types.bool;
         default = true;
       };
 
-      tlsCertificate = mkOption {
-        description = "TLS certificate to use instead of the hardcoded one in case of HTTPS connections. Use together with `tlsKey`.";
-        type = types.nullOr types.path;
+      tlsCertificate = lib.mkOption {
+        description = ''
+          TLS certificate to use instead of the hardcoded one in case of HTTPS connections.
+          Use together with `tlsKey`.
+        '';
+        type = lib.types.nullOr lib.types.path;
         default = null;
         example = "/var/lib/secrets/cert.pem";
       };
 
-      tlsKey = mkOption {
-        description = "TLS key to use instead of the hardcoded on in case of HTTPS connections. Use together with `tlsCertificate`.";
-        type = types.nullOr types.path;
+      tlsKey = lib.mkOption {
+        description = ''
+          TLS key to use instead of the hardcoded on in case of HTTPS connections.
+          Use together with `tlsCertificate`.
+        '';
+        type = lib.types.nullOr lib.types.path;
         default = null;
         example = "/var/lib/secrets/key.pem";
       };
 
-      useACMEHost = mkOption {
-        description = "Use a certificate generated by the NixOS ACME module for the given host. Note that this will not generate a new certificate - you will need to do so with `security.acme.certs`.";
-        type = types.nullOr types.str;
+      useACMEHost = lib.mkOption {
+        description = ''
+          Use a certificate generated by the NixOS ACME module for the given host.
+          Note that this will not generate a new certificate - you will need to do so with `security.acme.certs`.
+        '';
+        type = lib.types.nullOr lib.types.str;
         default = null;
         example = "example.com";
       };
     };
   };
+
   clientSubmodule = { config, ... }: {
     options = commonOptions // {
-      connectTo = mkOption {
+      connectTo = lib.mkOption {
         description = "Server address and port to connect to.";
-        type = types.submodule hostPortSubmodule;
-        example = {
-          host = "example.com";
-        };
-      };
-
-      enableHTTPS = mkOption {
-        description = "Enable HTTPS when connecting to the server.";
-        type = types.bool;
-        default = true;
-      };
-
-      localToRemote = mkOption {
-        description = "Local hosts and ports to listen on, plus the hosts and ports on remote to forward traffic to. Setting a local port to a value less than 1024 will additionally give the process the required CAP_NET_BIND_SERVICE capability.";
-        type = types.listOf (types.submodule localRemoteSubmodule);
-        default = [];
-        example = [ {
-          local = {
-            host = "127.0.0.1";
-            port = 8080;
-          };
-          remote = {
-            host = "127.0.0.1";
-            port = 8080;
-          };
-        } ];
+        type = lib.types.str;
+        example = "https://wstunnel.server.com:8443";
       };
 
-      dynamicToRemote = mkOption {
-        description = "Host and port for the SOCKS5 proxy to dynamically forward traffic to. Leave this at `null` to disable the SOCKS5 proxy. Setting the port to a value less than 1024 will additionally give the service the required CAP_NET_BIND_SERVICE capability.";
-        type = types.nullOr (types.submodule hostPortSubmodule);
-        default = null;
-        example = {
-          host = "127.0.0.1";
-          port = 1080;
-        };
+      localToRemote = lib.mkOption {
+        description = ''Listen on local and forwards traffic from remote.'';
+        type = lib.types.listOf (lib.types.str);
+        default = [ ];
+        example = [
+          "tcp://1212:google.com:443"
+          "unix:///tmp/wstunnel.sock:g.com:443"
+        ];
       };
 
-      udp = mkOption {
-        description = "Whether to forward UDP instead of TCP traffic.";
-        type = types.bool;
-        default = false;
+      remoteToLocal = lib.mkOption {
+        description = "Listen on remote and forwards traffic from local. Only tcp is supported";
+        type = lib.types.listOf lib.types.str;
+        default = [ ];
+        example = [
+          "tcp://1212:google.com:443"
+          "unix://wstunnel.sock:g.com:443"
+        ];
       };
 
-      udpTimeout = mkOption {
-        description = "When using UDP forwarding, timeout in seconds after which the tunnel connection is closed. `-1` means no timeout.";
-        type = types.int;
-        default = 30;
-      };
+      addNetBind = lib.mkEnableOption "Whether add CAP_NET_BIND_SERVICE to the tunnel service, this should be enabled if you want to bind port < 1024";
 
-      httpProxy = mkOption {
+      httpProxy = lib.mkOption {
         description = ''
           Proxy to use to connect to the wstunnel server (`USER:PASS@HOST:PORT`).
 
           ::: {.warning}
-          Passwords specified here will be world-readable in the Nix store! To pass a password to the service, point the `environmentFile` option to a file containing `PROXY_PASSWORD=<your-password-here>` and set this option to `<user>:$PROXY_PASSWORD@<host>:<port>`. Note however that this will also locally leak the passwords at runtime via e.g. /proc/<pid>/cmdline.
-
+          Passwords specified here will be world-readable in the Nix store!
+          To pass a password to the service, point the `environmentFile` option
+          to a file containing `PROXY_PASSWORD=<your-password-here>` and set
+          this option to `<user>:$PROXY_PASSWORD@<host>:<port>`.
+          Note however that this will also locally leak the passwords at
+          runtime via e.g. /proc/<pid>/cmdline.
           :::
         '';
-        type = types.nullOr types.str;
+        type = lib.types.nullOr lib.types.str;
         default = null;
       };
 
-      soMark = mkOption {
-        description = "Mark network packets with the SO_MARK sockoption with the specified value. Setting this option will also enable the required `CAP_NET_ADMIN` capability for the systemd service.";
-        type = types.nullOr types.int;
+      soMark = lib.mkOption {
+        description = ''
+          Mark network packets with the SO_MARK sockoption with the specified value.
+          Setting this option will also enable the required `CAP_NET_ADMIN` capability
+          for the systemd service.
+        '';
+        type = lib.types.nullOr lib.types.ints.unsigned;
         default = null;
       };
 
-      upgradePathPrefix = mkOption {
-        description = "Use a specific HTTP path prefix that will show up in the upgrade request to the `wstunnel` server. Useful when running `wstunnel` behind a reverse proxy.";
-        type = types.nullOr types.str;
+      upgradePathPrefix = lib.mkOption {
+        description = ''
+          Use a specific HTTP path prefix that will show up in the upgrade
+          request to the `wstunnel` server.
+          Useful when running `wstunnel` behind a reverse proxy.
+        '';
+        type = lib.types.nullOr lib.types.str;
         default = null;
         example = "wstunnel";
       };
 
-      hostHeader = mkOption {
-        description = "Use this as the HTTP host header instead of the real hostname. Useful for circumventing hostname-based firewalls.";
-        type = types.nullOr types.str;
-        default = null;
-      };
-
-      tlsSNI = mkOption {
+      tlsSNI = lib.mkOption {
         description = "Use this as the SNI while connecting via TLS. Useful for circumventing hostname-based firewalls.";
-        type = types.nullOr types.str;
+        type = lib.types.nullOr lib.types.str;
         default = null;
       };
 
-      tlsVerifyCertificate = mkOption {
+      tlsVerifyCertificate = lib.mkOption {
         description = "Whether to verify the TLS certificate of the server. It might be useful to set this to `false` when working with the `tlsSNI` option.";
-        type = types.bool;
+        type = lib.types.bool;
         default = true;
       };
 
       # The original argument name `websocketPingFrequency` is a misnomer, as the frequency is the inverse of the interval.
-      websocketPingInterval = mkOption {
-        description = "Do a heartbeat ping every N seconds to keep up the websocket connection.";
-        type = types.nullOr types.ints.unsigned;
+      websocketPingInterval = lib.mkOption {
+        description = "Frequency at which the client will send websocket ping to the server.";
+        type = lib.types.nullOr lib.types.ints.unsigned;
         default = null;
       };
 
-      upgradeCredentials = mkOption {
+      upgradeCredentials = lib.mkOption {
         description = ''
-          Use these credentials to authenticate during the HTTP upgrade request (Basic authorization type, `USER:[PASS]`).
+          Use these credentials to authenticate during the HTTP upgrade request
+          (Basic authorization type, `USER:[PASS]`).
 
           ::: {.warning}
-          Passwords specified here will be world-readable in the Nix store! To pass a password to the service, point the `environmentFile` option to a file containing `HTTP_PASSWORD=<your-password-here>` and set this option to `<user>:$HTTP_PASSWORD`. Note however that this will also locally leak the passwords at runtime via e.g. /proc/<pid>/cmdline.
+          Passwords specified here will be world-readable in the Nix store!
+          To pass a password to the service, point the `environmentFile` option
+          to a file containing `HTTP_PASSWORD=<your-password-here>` and set this
+          option to `<user>:$HTTP_PASSWORD`.
+          Note however that this will also locally leak the passwords at runtime
+          via e.g. /proc/<pid>/cmdline.
           :::
         '';
-        type = types.nullOr types.str;
+        type = lib.types.nullOr lib.types.str;
         default = null;
       };
 
-      customHeaders = mkOption {
+      customHeaders = lib.mkOption {
         description = "Custom HTTP headers to send during the upgrade request.";
-        type = types.attrsOf types.str;
-        default = {};
+        type = lib.types.attrsOf lib.types.str;
+        default = { };
         example = {
           "X-Some-Header" = "some-value";
         };
       };
     };
   };
+
   generateServerUnit = name: serverCfg: {
     name = "wstunnel-server-${name}";
-    value = {
-      description = "wstunnel server - ${name}";
-      requires = [ "network.target" "network-online.target" ];
-      after = [ "network.target" "network-online.target" ];
-      wantedBy = optional serverCfg.autoStart "multi-user.target";
-
-      serviceConfig = let
-        certConfig = config.security.acme.certs."${serverCfg.useACMEHost}";
-      in {
-        Type = "simple";
-        ExecStart = with serverCfg; let
-          resolvedTlsCertificate = if useACMEHost != null
-            then "${certConfig.directory}/fullchain.pem"
-            else tlsCertificate;
-          resolvedTlsKey = if useACMEHost != null
-            then "${certConfig.directory}/key.pem"
-            else tlsKey;
-        in ''
-          ${package}/bin/wstunnel \
-            --server \
-            ${optionalString (restrictTo != null)     "--restrictTo=${utils.escapeSystemdExecArg (hostPortToString restrictTo)}"} \
-            ${optionalString (resolvedTlsCertificate != null) "--tlsCertificate=${utils.escapeSystemdExecArg resolvedTlsCertificate}"} \
-            ${optionalString (resolvedTlsKey != null)         "--tlsKey=${utils.escapeSystemdExecArg resolvedTlsKey}"} \
-            ${optionalString verboseLogging "--verbose"} \
-            ${attrsToArgs extraArgs} \
-            ${utils.escapeSystemdExecArg "${if enableHTTPS then "wss" else "ws"}://${hostPortToString listen}"}
-        '';
-        EnvironmentFile = optional (serverCfg.environmentFile != null) serverCfg.environmentFile;
-        DynamicUser = true;
-        SupplementaryGroups = optional (serverCfg.useACMEHost != null) certConfig.group;
-        PrivateTmp = true;
-        AmbientCapabilities = optionals (serverCfg.listen.port < 1024) [ "CAP_NET_BIND_SERVICE" ];
-        NoNewPrivileges = true;
-        RestrictNamespaces = "uts ipc pid user cgroup";
-        ProtectSystem = "strict";
-        ProtectHome = true;
-        ProtectKernelTunables = true;
-        ProtectKernelModules = true;
-        ProtectControlGroups = true;
-        PrivateDevices = true;
-        RestrictSUIDSGID = true;
+    value =
+      let
+        certConfig = config.security.acme.certs.${serverCfg.useACMEHost};
+      in
+      {
+        description = "wstunnel server - ${name}";
+        requires = [ "network.target" "network-online.target" ];
+        after = [ "network.target" "network-online.target" ];
+        wantedBy = lib.optional serverCfg.autoStart "multi-user.target";
+
+        environment.RUST_LOG = serverCfg.loggingLevel;
+
+        serviceConfig = {
+          Type = "exec";
+          EnvironmentFile =
+            lib.optional (serverCfg.environmentFile != null) serverCfg.environmentFile;
+          DynamicUser = true;
+          SupplementaryGroups =
+            lib.optional (serverCfg.useACMEHost != null) certConfig.group;
+          PrivateTmp = true;
+          AmbientCapabilities =
+            lib.optionals (serverCfg.listen.port < 1024) [ "CAP_NET_BIND_SERVICE" ];
+          NoNewPrivileges = true;
+          RestrictNamespaces = "uts ipc pid user cgroup";
+          ProtectSystem = "strict";
+          ProtectHome = true;
+          ProtectKernelTunables = true;
+          ProtectKernelModules = true;
+          ProtectControlGroups = true;
+          PrivateDevices = true;
+          RestrictSUIDSGID = true;
+
+          Restart = "on-failure";
+          RestartSec = 2;
+          RestartSteps = 20;
+          RestartMaxDelaySec = "5min";
+        };
 
+        script = with serverCfg; ''
+          ${lib.getExe package} \
+            server \
+            ${lib.cli.toGNUCommandLineShell { } (
+              lib.recursiveUpdate
+              {
+                restrict-to = map hostPortToString restrictTo;
+                tls-certificate = if useACMEHost != null
+                                  then "${certConfig.directory}/fullchain.pem"
+                                  else "${tlsCertificate}";
+                tls-private-key = if useACMEHost != null
+                                  then "${certConfig.directory}/key.pem"
+                                  else "${tlsKey}";
+              }
+              extraArgs
+            )} \
+            ${lib.escapeShellArg "${if enableHTTPS then "wss" else "ws"}://${hostPortToString listen}"}
+        '';
       };
-    };
   };
+
   generateClientUnit = name: clientCfg: {
     name = "wstunnel-client-${name}";
     value = {
       description = "wstunnel client - ${name}";
       requires = [ "network.target" "network-online.target" ];
       after = [ "network.target" "network-online.target" ];
-      wantedBy = optional clientCfg.autoStart "multi-user.target";
+      wantedBy = lib.optional clientCfg.autoStart "multi-user.target";
+
+      environment.RUST_LOG = clientCfg.loggingLevel;
 
       serviceConfig = {
-        Type = "simple";
-        ExecStart = with clientCfg; ''
-          ${package}/bin/wstunnel \
-            ${concatStringsSep " " (builtins.map (x:          "--localToRemote=${localRemoteToString x}") localToRemote)} \
-            ${concatStringsSep " " (mapAttrsToList (n: v:     "--customHeaders=\"${n}: ${v}\"") customHeaders)} \
-            ${optionalString (dynamicToRemote != null)        "--dynamicToRemote=${utils.escapeSystemdExecArg (hostPortToString dynamicToRemote)}"} \
-            ${optionalString udp                              "--udp"} \
-            ${optionalString (httpProxy != null)              "--httpProxy=${httpProxy}"} \
-            ${optionalString (soMark != null)                 "--soMark=${toString soMark}"} \
-            ${optionalString (upgradePathPrefix != null)      "--upgradePathPrefix=${upgradePathPrefix}"} \
-            ${optionalString (hostHeader != null)             "--hostHeader=${hostHeader}"} \
-            ${optionalString (tlsSNI != null)                 "--tlsSNI=${tlsSNI}"} \
-            ${optionalString tlsVerifyCertificate             "--tlsVerifyCertificate"} \
-            ${optionalString (websocketPingInterval != null)  "--websocketPingFrequency=${toString websocketPingInterval}"} \
-            ${optionalString (upgradeCredentials != null)     "--upgradeCredentials=${upgradeCredentials}"} \
-            --udpTimeoutSec=${toString udpTimeout} \
-            ${optionalString verboseLogging "--verbose"} \
-            ${attrsToArgs extraArgs} \
-            ${utils.escapeSystemdExecArg "${if enableHTTPS then "wss" else "ws"}://${hostPortToString connectTo}"}
-        '';
-        EnvironmentFile = optional (clientCfg.environmentFile != null) clientCfg.environmentFile;
+        Type = "exec";
+        EnvironmentFile =
+          lib.optional (clientCfg.environmentFile != null) clientCfg.environmentFile;
         DynamicUser = true;
         PrivateTmp = true;
-        AmbientCapabilities = (optionals (clientCfg.soMark != null) [ "CAP_NET_ADMIN" ]) ++ (optionals ((clientCfg.dynamicToRemote.port or 1024) < 1024 || (any (x: x.local.port < 1024) clientCfg.localToRemote)) [ "CAP_NET_BIND_SERVICE" ]);
+        AmbientCapabilities =
+          (lib.optionals clientCfg.addNetBind [ "CAP_NET_BIND_SERVICE" ]) ++
+          (lib.optionals (clientCfg.soMark != null) [ "CAP_NET_ADMIN" ]);
         NoNewPrivileges = true;
         RestrictNamespaces = "uts ipc pid user cgroup";
         ProtectSystem = "strict";
@@ -350,80 +351,118 @@ let
         ProtectControlGroups = true;
         PrivateDevices = true;
         RestrictSUIDSGID = true;
+
+        Restart = "on-failure";
+        RestartSec = 2;
+        RestartSteps = 20;
+        RestartMaxDelaySec = "5min";
       };
+
+      script = with clientCfg; ''
+        ${lib.getExe package} \
+          client \
+          ${lib.cli.toGNUCommandLineShell { } (
+            lib.recursiveUpdate
+            {
+              local-to-remote = localToRemote;
+              remote-to-local = remoteToLocal;
+              http-headers = lib.mapAttrsToList (n: v: "${n}:${v}") customHeaders;
+              http-proxy = httpProxy;
+              socket-so-mark = soMark;
+              http-upgrade-path-prefix = upgradePathPrefix;
+              tls-sni-override = tlsSNI;
+              tls-verify-certificate = tlsVerifyCertificate;
+              websocket-ping-frequency-sec = websocketPingInterval;
+              http-upgrade-credentials = upgradeCredentials;
+            }
+            extraArgs
+          )} \
+          ${lib.escapeShellArg connectTo}
+      '';
     };
   };
-in {
+in
+{
   options.services.wstunnel = {
-    enable = mkEnableOption "wstunnel";
+    enable = lib.mkEnableOption "wstunnel";
 
-    servers = mkOption {
+    servers = lib.mkOption {
       description = "`wstunnel` servers to set up.";
-      type = types.attrsOf (types.submodule serverSubmodule);
-      default = {};
+      type = lib.types.attrsOf (lib.types.submodule serverSubmodule);
+      default = { };
       example = {
         "wg-tunnel" = {
-          listen.port = 8080;
+          listen = {
+            host = "0.0.0.0";
+            port = 8080;
+          };
           enableHTTPS = true;
           tlsCertificate = "/var/lib/secrets/fullchain.pem";
           tlsKey = "/var/lib/secrets/key.pem";
-          restrictTo = {
+          restrictTo = [{
             host = "127.0.0.1";
             port = 51820;
-          };
+          }];
         };
       };
     };
 
-    clients = mkOption {
+    clients = lib.mkOption {
       description = "`wstunnel` clients to set up.";
-      type = types.attrsOf (types.submodule clientSubmodule);
-      default = {};
+      type = lib.types.attrsOf (lib.types.submodule clientSubmodule);
+      default = { };
       example = {
         "wg-tunnel" = {
-          connectTo = {
-            host = "example.com";
-            port = 8080;
-          };
-          enableHTTPS = true;
-          localToRemote = {
-            local = {
-              host = "127.0.0.1";
-              port = 51820;
-            };
-            remote = {
-              host = "127.0.0.1";
-              port = 51820;
-            };
-          };
-          udp = true;
+          connectTo = "wss://wstunnel.server.com:8443";
+          localToRemote = [
+            "tcp://1212:google.com:443"
+            "tcp://2:n.lan:4?proxy_protocol"
+          ];
+          remoteToLocal = [
+            "socks5://[::1]:1212"
+            "unix://wstunnel.sock:g.com:443"
+          ];
         };
       };
     };
   };
 
-  config = mkIf cfg.enable {
-    systemd.services = (mapAttrs' generateServerUnit (filterAttrs (n: v: v.enable) cfg.servers)) // (mapAttrs' generateClientUnit (filterAttrs (n: v: v.enable) cfg.clients));
-
-    assertions = (mapAttrsToList (name: serverCfg: {
-      assertion = !(serverCfg.useACMEHost != null && (serverCfg.tlsCertificate != null || serverCfg.tlsKey != null));
-      message = ''
-        Options services.wstunnel.servers."${name}".useACMEHost and services.wstunnel.servers."${name}".{tlsCertificate, tlsKey} are mutually exclusive.
-      '';
-    }) cfg.servers) ++
-    (mapAttrsToList (name: serverCfg: {
-      assertion = !((serverCfg.tlsCertificate != null || serverCfg.tlsKey != null) && !(serverCfg.tlsCertificate != null && serverCfg.tlsKey != null));
-      message = ''
-        services.wstunnel.servers."${name}".tlsCertificate and services.wstunnel.servers."${name}".tlsKey need to be set together.
-      '';
-    }) cfg.servers) ++
-    (mapAttrsToList (name: clientCfg: {
-      assertion = !(clientCfg.localToRemote == [] && clientCfg.dynamicToRemote == null);
-      message = ''
-        Either one of services.wstunnel.clients."${name}".localToRemote or services.wstunnel.clients."${name}".dynamicToRemote must be set.
-      '';
-    }) cfg.clients);
+  config = lib.mkIf cfg.enable {
+    systemd.services =
+      (lib.mapAttrs' generateServerUnit (lib.filterAttrs (n: v: v.enable) cfg.servers)) //
+      (lib.mapAttrs' generateClientUnit (lib.filterAttrs (n: v: v.enable) cfg.clients));
+
+    assertions =
+      (lib.mapAttrsToList
+        (name: serverCfg: {
+          assertion =
+            !(serverCfg.useACMEHost != null && serverCfg.tlsCertificate != null);
+          message = ''
+            Options services.wstunnel.servers."${name}".useACMEHost and services.wstunnel.servers."${name}".{tlsCertificate, tlsKey} are mutually exclusive.
+          '';
+        })
+        cfg.servers) ++
+
+      (lib.mapAttrsToList
+        (name: serverCfg: {
+          assertion =
+            (serverCfg.tlsCertificate == null && serverCfg.tlsKey == null) ||
+            (serverCfg.tlsCertificate != null && serverCfg.tlsKey != null);
+          message = ''
+            services.wstunnel.servers."${name}".tlsCertificate and services.wstunnel.servers."${name}".tlsKey need to be set together.
+          '';
+        })
+        cfg.servers) ++
+
+      (lib.mapAttrsToList
+        (name: clientCfg: {
+          assertion = !(clientCfg.localToRemote == [ ] && clientCfg.remoteToLocal == [ ]);
+          message = ''
+            Either one of services.wstunnel.clients."${name}".localToRemote or services.wstunnel.clients."${name}".remoteToLocal must be set.
+          '';
+        })
+        cfg.clients);
   };
 
-  meta.maintainers = with maintainers; [ alyaeanyx ];
+  meta.maintainers = with lib.maintainers; [ alyaeanyx rvdp neverbehave ];
 }
diff --git a/nixos/modules/services/networking/zerotierone.nix b/nixos/modules/services/networking/zerotierone.nix
index 86c1efc629a98..68c04118fdd58 100644
--- a/nixos/modules/services/networking/zerotierone.nix
+++ b/nixos/modules/services/networking/zerotierone.nix
@@ -4,7 +4,9 @@ with lib;
 
 let
   cfg = config.services.zerotierone;
-  localConfFile = pkgs.writeText "zt-local.conf" (builtins.toJSON cfg.localConf);
+
+  settingsFormat = pkgs.formats.json {};
+  localConfFile = settingsFormat.generate "zt-local.conf" cfg.localConf;
   localConfFilePath = "/var/lib/zerotier-one/local.conf";
 in
 {
@@ -41,7 +43,7 @@ in
     example = {
       settings.allowTcpFallbackRelay = false;
     };
-    type = types.nullOr types.attrs;
+    type = settingsFormat.type;
   };
 
   config = mkIf cfg.enable {
@@ -60,7 +62,7 @@ in
         chown -R root:root /var/lib/zerotier-one
       '' + (concatMapStrings (netId: ''
         touch "/var/lib/zerotier-one/networks.d/${netId}.conf"
-      '') cfg.joinNetworks) + optionalString (cfg.localConf != null) ''
+      '') cfg.joinNetworks) + optionalString (cfg.localConf != {}) ''
         if [ -L "${localConfFilePath}" ]
         then
           rm ${localConfFilePath}