diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 603f647f0402..5dc1b20b0293 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1477,6 +1477,7 @@ in smokeping = runTest ./smokeping.nix; snapcast = runTest ./snapcast.nix; snapper = runTest ./snapper.nix; + snid = runTest ./snid.nix; snipe-it = runTest ./web-apps/snipe-it.nix; snips-sh = runTest ./snips-sh.nix; snmpd = runTest ./snmpd.nix; diff --git a/nixos/tests/snid.nix b/nixos/tests/snid.nix new file mode 100644 index 000000000000..7d87e6282130 --- /dev/null +++ b/nixos/tests/snid.nix @@ -0,0 +1,31 @@ +{ lib, ... }: +{ + _class = "nixosTest"; + name = "snid"; + + nodes = { + machine = + { pkgs, ... }: + { + system.services.snid = { + imports = [ pkgs.snid.services.default ]; + snid = { + listen = [ "tcp:8443" ]; + mode = "tcp"; + backendCidrs = [ "127.0.0.0/8" ]; + }; + }; + + networking.firewall.allowedTCPPorts = [ 8443 ]; + }; + }; + + testScript = '' + start_all() + machine.wait_for_unit("multi-user.target") + machine.wait_for_unit("snid.service") + machine.wait_for_open_port(8443) + ''; + + meta.maintainers = with lib.maintainers; [ tomfitzhenry ]; +} diff --git a/pkgs/by-name/sn/snid/package.nix b/pkgs/by-name/sn/snid/package.nix index 1ae9e9a66300..7b9880ce873b 100644 --- a/pkgs/by-name/sn/snid/package.nix +++ b/pkgs/by-name/sn/snid/package.nix @@ -2,6 +2,7 @@ lib, buildGoModule, fetchFromGitHub, + nixosTests, }: buildGoModule (finalAttrs: { @@ -17,6 +18,14 @@ buildGoModule (finalAttrs: { vendorHash = "sha256-cVarG6Tx4yWpZE5BLZsMtLV9LF1lsiFfIXxhYiNjQlY="; + passthru = { + tests.nixos = nixosTests.snid; + services.default = { + imports = [ (lib.modules.importApply ./service.nix { }) ]; + snid.package = finalAttrs.finalPackage; + }; + }; + meta = { description = "Zero config TLS proxy server that uses SNI"; homepage = "https://github.com/AGWA/snid"; diff --git a/pkgs/by-name/sn/snid/service.nix b/pkgs/by-name/sn/snid/service.nix new file mode 100644 index 000000000000..f536cdb54825 --- /dev/null +++ b/pkgs/by-name/sn/snid/service.nix @@ -0,0 +1,173 @@ +# Non-module dependencies (`importApply`) +{ }: + +# Service module +{ + lib, + config, + options, + ... +}: +let + inherit (lib) + concatMap + getExe + mkOption + optional + types + ; + cfg = config.snid; +in +{ + # https://nixos.org/manual/nixos/unstable/#modular-services + _class = "service"; + + options = { + snid = { + package = mkOption { + description = "Package to use for snid."; + defaultText = "The snid package that provided this module."; + type = types.package; + }; + + listen = mkOption { + description = '' + Addresses to listen on, in go-listener syntax. + + Examples: `"tcp:443"`, `"tcp:0.0.0.0:443"`, `"tcp:192.0.2.4:443"`. + ''; + type = types.listOf types.str; + example = [ "tcp:0.0.0.0:443" ]; + }; + + mode = mkOption { + description = '' + Proxy mode. One of `nat46`, `tcp`, or `unix`. + ''; + type = types.enum [ + "nat46" + "tcp" + "unix" + ]; + }; + + defaultHostname = mkOption { + description = '' + Hostname to use if a client does not include the SNI extension. + If null, SNI-less connections will be terminated with a TLS alert. + ''; + type = types.nullOr types.str; + default = null; + }; + + nat46Prefix = mkOption { + description = '' + IPv6 prefix for the source address when connecting to the backend + in NAT46 mode. The client's IPv4 address is placed in the lower 4 + bytes. + + Note: this prefix must be routed to the local host, e.g. + ``` + ip route add local 64:ff9b:1::/96 dev lo + ``` + ''; + type = types.nullOr types.str; + default = null; + example = "64:ff9b:1::"; + }; + + backendCidrs = mkOption { + description = '' + Subnets to which connections may be forwarded. Connections to + addresses outside these subnets are rejected. Used in `nat46` and + `tcp` modes. + ''; + type = types.listOf types.str; + default = [ ]; + example = [ + "2001:db8::/64" + "192.0.2.0/24" + ]; + }; + + backendPort = mkOption { + description = '' + Port number to connect to on the backend in TCP mode. If null, + snid uses the same port as the inbound connection. + ''; + type = types.nullOr types.port; + default = null; + }; + + unixDirectory = mkOption { + description = '' + Path to the directory containing UNIX domain sockets, used in + `unix` mode. + ''; + type = types.nullOr types.path; + default = null; + }; + + proxyProto = mkOption { + description = '' + Use PROXY protocol v2 to convey the client IP address to the + backend. Applicable in `tcp` and `unix` modes. + ''; + type = types.bool; + default = false; + }; + }; + }; + + config = { + assertions = [ + { + assertion = cfg.mode == "nat46" -> cfg.nat46Prefix != null; + message = "snid: `nat46Prefix` must be set when `mode` is `nat46`."; + } + { + assertion = cfg.mode == "nat46" -> cfg.backendCidrs != [ ]; + message = "snid: `backendCidrs` must be set when `mode` is `nat46`."; + } + { + assertion = cfg.mode == "tcp" -> cfg.backendCidrs != [ ]; + message = "snid: `backendCidrs` must be set when `mode` is `tcp`."; + } + { + assertion = cfg.mode == "unix" -> cfg.unixDirectory != null; + message = "snid: `unixDirectory` must be set when `mode` is `unix`."; + } + ]; + + process.argv = [ + (getExe cfg.package) + "-mode" + cfg.mode + ] + ++ concatMap (l: [ + "-listen" + l + ]) cfg.listen + ++ concatMap (c: [ + "-backend-cidr" + c + ]) cfg.backendCidrs + ++ optional (cfg.defaultHostname != null) "-default-hostname=${cfg.defaultHostname}" + ++ optional (cfg.nat46Prefix != null) "-nat46-prefix=${cfg.nat46Prefix}" + ++ optional (cfg.backendPort != null) "-backend-port=${toString cfg.backendPort}" + ++ optional (cfg.unixDirectory != null) "-unix-directory=${cfg.unixDirectory}" + ++ optional cfg.proxyProto "-proxy-proto"; + } + // lib.optionalAttrs (options ? systemd) { + systemd.service = { + after = [ "network.target" ]; + wants = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Restart = "on-failure"; + DynamicUser = true; + AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; + }; + }; + }; +}