diff --git a/maintainers/maintainer-list.nix b/maintainers/maintainer-list.nix index abd81d745a8bc..3bb1c985c6c1b 100644 --- a/maintainers/maintainer-list.nix +++ b/maintainers/maintainer-list.nix @@ -29457,6 +29457,12 @@ githubId = 69384921; name = "ZeStig"; }; + zsuper = { + email = "piyushkumbhare10@gmail.com"; + github = "zSuperx"; + githubId = 130249145; + name = "Piyush Kumbhare"; + }; ztzg = { email = "dd@crosstwine.com"; github = "ztzg"; diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 2bf95dc1c926d..0837fa1fe75e3 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1120,6 +1120,7 @@ ./services/networking/bitlbee.nix ./services/networking/blockbook-frontend.nix ./services/networking/blocky.nix + ./services/networking/bore/bore.nix ./services/networking/byedpi.nix ./services/networking/cato-client.nix ./services/networking/centrifugo.nix diff --git a/nixos/modules/services/networking/bore/bore.nix b/nixos/modules/services/networking/bore/bore.nix new file mode 100644 index 0000000000000..5f6b3203704de --- /dev/null +++ b/nixos/modules/services/networking/bore/bore.nix @@ -0,0 +1,162 @@ +{ + config, + pkgs, + lib, + ... +}: +let + cfg = config.services.bore; + inherit (lib) + mkOption + mkPackageOption + mkEnableOption + types + optionalString + ; + inherit (types) attrsOf submodule; +in +{ + options = { + services.bore = { + package = mkPackageOption pkgs "bore-cli" { }; + + local = mkOption { + type = attrsOf (submodule ./local-options.nix); + description = "Definition of bore local proxies."; + default = { }; + }; + + server = mkOption { + type = attrsOf (submodule ./server-options.nix); + description = "Definition of bore remote proxy servers."; + default = { }; + }; + + enableLocalPortCheck = mkEnableOption "local port check" // { + default = true; + example = false; + }; + + enableServerPortCheck = mkEnableOption "server port check" // { + default = true; + example = false; + }; + }; + }; + + config = + let + getEnabled = s: builtins.filter (value: value.enable) (lib.attrValues s); + mkBoreService = + kind: v: + let + isServer = (kind == "server"); + in + { + name = "bore-${kind}-${v.name}"; + value = { + description = "bore remote proxy service for ${v.name}"; + enable = true; + after = [ + "network-online.target" + "nss-lookup.target" + ]; + + requires = [ + "network-online.target" + "nss-lookup.target" + ]; + + wantedBy = [ "multi-user.target" ]; + + environment = + if isServer then + { + BORE_MIN_PORT = toString v.min-port; + BORE_MAX_PORT = toString v.max-port; + } + else + { + BORE_SERVER = v.to; + BORE_LOCAL_PORT = toString v.local-port; + }; + + script = '' + ${optionalString (v.secretFile != null) ''export BORE_SECRET="$(<${v.secretFile})"''} + + ${lib.getExe' cfg.package "bore"} ${kind} \ + '' + + ( + if isServer then + ''--bind-addr="${v.bind-addr}" --bind-tunnels="${v.bind-tunnels}"'' + else + ''--local-host="${v.local-host}" --port=${toString v.remote-port}'' + ); + + serviceConfig = { + Restart = "on-failure"; + RestartSec = 10; + }; + }; + }; + in + { + systemd.services = + (lib.genAttrs' (getEnabled cfg.server) (mkBoreService "server")) + // (lib.genAttrs' (getEnabled cfg.local) (mkBoreService "local")); + + assertions = + let + localTuples = map (v: "${v.local-host}:${toString v.local-port}") (getEnabled cfg.local); + + # For remote tuples, filter out entries where remote-port == 0, since that's a legal default + remoteTuples = map (v: "${v.to}:${toString v.remote-port}") ( + builtins.filter (v: v.remote-port != 0) (getEnabled cfg.local) + ); + + bindAddrs = map (v: v.bind-addr) (getEnabled cfg.server); + + findDuplicates = + list: + lib.pipe list [ + (builtins.groupBy toString) + (builtins.mapAttrs (_: builtins.length)) + (lib.filterAttrs (_: len: len > 1)) + builtins.attrNames + ]; + in + [ + (lib.mkIf (cfg.enableLocalPortCheck) { + assertion = lib.allUnique localTuples; + message = '' + Detected duplicate values of the following `local-host:local-port` tuples: + ${lib.concatStringsSep ", " (findDuplicates localTuples)} + Ensure these tuples are unique across attributes in `services.bore.local`! + (To turn off this assertion, set `services.bore.enableLocalPortCheck = false;`) + ''; + }) + + (lib.mkIf (cfg.enableLocalPortCheck) { + assertion = lib.allUnique remoteTuples; + message = '' + Detected duplicate values for `to:remote-port` tuples: + ${lib.concatStringsSep ", " (findDuplicates remoteTuples)} + Ensure these tuples are unique across attributes in `services.bore.local`! + (To turn off this assertion, set `services.bore.enableLocalPortCheck = false;`) + ''; + }) + + (lib.mkIf (cfg.enableServerPortCheck) { + assertion = lib.allUnique bindAddrs; + message = '' + Detected duplicate values for bore remote `bind-addr`s: + ${lib.concatStringsSep ", " (findDuplicates bindAddrs)} + Ensure `bind-addr` is unique across attributes in `services.bore.server`! + (To turn off this assertion, set `services.bore.enableServerPortCheck = false;`) + ''; + }) + ]; + }; + + meta.maintainers = with lib.maintainers; [ zsuper ]; +} diff --git a/nixos/modules/services/networking/bore/common-options.nix b/nixos/modules/services/networking/bore/common-options.nix new file mode 100644 index 0000000000000..795d41ac93250 --- /dev/null +++ b/nixos/modules/services/networking/bore/common-options.nix @@ -0,0 +1,28 @@ +{ + name, + lib, + ... +}: +let + inherit (lib) mkOption types literalMD; + inherit (types) nullOr str path; +in +{ + options = { + + name = mkOption { + type = str; + default = name; + defaultText = literalMD "the attribute name"; + description = "name of the service"; + }; + + secretFile = mkOption { + type = nullOr path; + default = null; + description = '' + Optional path to file containing secret for authentication. + ''; + }; + }; +} diff --git a/nixos/modules/services/networking/bore/local-options.nix b/nixos/modules/services/networking/bore/local-options.nix new file mode 100644 index 0000000000000..9f41c9149f16a --- /dev/null +++ b/nixos/modules/services/networking/bore/local-options.nix @@ -0,0 +1,49 @@ +{ + lib, + ... +}: +let + inherit (lib) mkOption mkEnableOption types; + inherit (types) port str; +in +{ + imports = [ + ./common-options.nix + ]; + + options = { + enable = mkEnableOption "Bore TCP tunnel (local proxy to the remote server) for ``"; + + local-host = mkOption { + type = str; + default = "localhost"; + description = '' + The local host to expose. + ''; + }; + + to = mkOption { + type = str; + default = "bore.pub"; + description = '' + Address of the remote server to expose local ports to. + ''; + }; + + remote-port = mkOption { + type = port; + default = 0; + description = '' + Optional port on the remote server to select. + (If set to 0, a random available port will be assigned by the remote server.) + ''; + }; + + local-port = mkOption { + type = port; + description = '' + The local port to expose. + ''; + }; + }; +} diff --git a/nixos/modules/services/networking/bore/server-options.nix b/nixos/modules/services/networking/bore/server-options.nix new file mode 100644 index 0000000000000..837bf84f7905b --- /dev/null +++ b/nixos/modules/services/networking/bore/server-options.nix @@ -0,0 +1,55 @@ +{ + lib, + config, + options, + ... +}: +let + inherit (lib) mkOption mkEnableOption types; + inherit (types) + port + str + ; +in +{ + imports = [ + ./common-options.nix + ]; + + options = { + enable = mkEnableOption "Bore TCP tunnel (remote proxy server) for ``"; + + min-port = mkOption { + type = port; + default = 1024; + description = '' + Minimum accepted TCP port number. + ''; + }; + + max-port = mkOption { + type = port; + default = 65535; + description = '' + Maximum accepted TCP port number. + ''; + }; + + bind-addr = mkOption { + type = str; + default = "0.0.0.0"; + description = '' + IP address to bind to, clients must reach this. + ''; + }; + + bind-tunnels = mkOption { + type = str; + default = config.bind-addr; + defaultText = lib.literalExpression "config.${options.bind-addr}"; + description = '' + IP address where tunnels will listen on. + ''; + }; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 8ab11239fadfc..2f2219de44c66 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -307,6 +307,7 @@ in boot-stage1 = runTest ./boot-stage1.nix; boot-stage2 = runTest ./boot-stage2.nix; bootspec = handleTestOn [ "x86_64-linux" ] ./bootspec.nix { }; + bore = runTest ./bore.nix; borgbackup = runTest ./borgbackup.nix; borgmatic = runTest ./borgmatic.nix; botamusique = runTest ./botamusique.nix; diff --git a/nixos/tests/bore.nix b/nixos/tests/bore.nix new file mode 100644 index 0000000000000..307cbe2345ba0 --- /dev/null +++ b/nixos/tests/bore.nix @@ -0,0 +1,65 @@ +{ + name = "bore"; + + nodes = { + serverStandalone = { + services.bore = { + server = { + example.enable = true; + }; + }; + }; + + bothStandard = { + services.bore = { + server.example.enable = true; + + local.example = { + enable = true; + to = "0.0.0.0"; + local-port = 3000; + remote-port = 4000; + }; + }; + }; + + bothWithSecret = + { pkgs, ... }: + { + services.bore = { + server.example = { + secretFile = pkgs.writeText "server-secret" "super-secret-message!"; + enable = true; + }; + + local.example = { + enable = true; + to = "0.0.0.0"; + local-port = 3000; + remote-port = 4000; + secretFile = pkgs.writeText "local-secret" "super-secret-message!"; + }; + }; + }; + }; + + testScript = '' + with subtest("Standalone server"): + serverStandalone.wait_for_unit("bore-server-example.service") + serverStandalone.wait_for_open_port(7835, "0.0.0.0") + + with subtest("Server & Proxy running together"): + bothStandard.wait_for_unit("bore-server-example.service") + bothStandard.wait_for_open_port(7835, "0.0.0.0") + + bothStandard.wait_for_unit("bore-local-example.service") + bothStandard.wait_for_open_port(4000, "0.0.0.0") + + with subtest("Server & Proxy + authentication secret"): + bothWithSecret.wait_for_unit("bore-server-example.service") + bothWithSecret.wait_for_open_port(7835, "0.0.0.0") + + bothWithSecret.wait_for_unit("bore-local-example.service") + bothWithSecret.wait_for_open_port(4000, "0.0.0.0") + ''; +}