diff options
-rw-r--r-- | nixos/modules/module-list.nix | 1 | ||||
-rw-r--r-- | nixos/modules/services/networking/nats.nix | 159 | ||||
-rw-r--r-- | nixos/tests/all-tests.nix | 1 | ||||
-rw-r--r-- | nixos/tests/nats.nix | 65 |
4 files changed, 226 insertions, 0 deletions
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index f26977b12441..7b47dd77ec28 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -767,6 +767,7 @@ ./services/networking/namecoind.nix ./services/networking/nar-serve.nix ./services/networking/nat.nix + ./services/networking/nats.nix ./services/networking/ndppd.nix ./services/networking/nebula.nix ./services/networking/networkmanager.nix diff --git a/nixos/modules/services/networking/nats.nix b/nixos/modules/services/networking/nats.nix new file mode 100644 index 000000000000..eb0c65bc6561 --- /dev/null +++ b/nixos/modules/services/networking/nats.nix @@ -0,0 +1,159 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.nats; + + format = pkgs.formats.json { }; + + configFile = format.generate "nats.conf" cfg.settings; + +in { + + ### Interface + + options = { + services.nats = { + enable = mkEnableOption "NATS messaging system"; + + user = mkOption { + type = types.str; + default = "nats"; + description = "User account under which NATS runs."; + }; + + group = mkOption { + type = types.str; + default = "nats"; + description = "Group under which NATS runs."; + }; + + serverName = mkOption { + default = "nats"; + example = "n1-c3"; + type = types.str; + description = '' + Name of the NATS server, must be unique if clustered. + ''; + }; + + jetstream = mkEnableOption "JetStream"; + + port = mkOption { + default = 4222; + example = 4222; + type = types.port; + description = '' + Port on which to listen. + ''; + }; + + dataDir = mkOption { + default = "/var/lib/nats"; + type = types.path; + description = '' + The NATS data directory. Only used if JetStream is enabled, for + storing stream metadata and messages. + + If left as the default value this directory will automatically be + created before the NATS server starts, otherwise the sysadmin is + responsible for ensuring the directory exists with appropriate + ownership and permissions. + ''; + }; + + settings = mkOption { + default = { }; + type = format.type; + example = literalExample '' + { + jetstream = { + max_mem = "1G"; + max_file = "10G"; + }; + }; + ''; + description = '' + Declarative NATS configuration. See the + <link xlink:href="https://docs.nats.io/nats-server/configuration"> + NATS documentation</link> for a list of options. + ''; + }; + }; + }; + + ### Implementation + + config = mkIf cfg.enable { + services.nats.settings = { + server_name = cfg.serverName; + port = cfg.port; + jetstream = optionalAttrs cfg.jetstream { store_dir = cfg.dataDir; }; + }; + + systemd.services.nats = { + description = "NATS messaging system"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + + serviceConfig = mkMerge [ + (mkIf (cfg.dataDir == "/var/lib/nats") { + StateDirectory = "nats"; + StateDirectoryMode = "0750"; + }) + { + Type = "simple"; + ExecStart = "${pkgs.nats-server}/bin/nats-server -c ${configFile}"; + ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; + ExecStop = "${pkgs.coreutils}/bin/kill -SIGINT $MAINPID"; + Restart = "on-failure"; + + User = cfg.user; + Group = cfg.group; + + # Hardening + CapabilityBoundingSet = ""; + LimitNOFILE = 800000; # JetStream requires 2 FDs open per stream. + LockPersonality = true; + MemoryDenyWriteExecute = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateTmp = true; + PrivateUsers = true; + ProcSubset = "pid"; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProtectSystem = "strict"; + ReadOnlyPaths = [ ]; + ReadWritePaths = [ cfg.dataDir ]; + RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ]; + UMask = "0077"; + } + ]; + }; + + users.users = mkIf (cfg.user == "nats") { + nats = { + description = "NATS daemon user"; + isSystemUser = true; + group = cfg.group; + home = cfg.dataDir; + }; + }; + + users.groups = mkIf (cfg.group == "nats") { nats = { }; }; + }; + +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index e1ad011b22df..d17904c776e7 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -283,6 +283,7 @@ in nat.firewall = handleTest ./nat.nix { withFirewall = true; }; nat.firewall-conntrack = handleTest ./nat.nix { withFirewall = true; withConntrackHelpers = true; }; nat.standalone = handleTest ./nat.nix { withFirewall = false; }; + nats = handleTest ./nats.nix {}; navidrome = handleTest ./navidrome.nix {}; ncdns = handleTest ./ncdns.nix {}; ndppd = handleTest ./ndppd.nix {}; diff --git a/nixos/tests/nats.nix b/nixos/tests/nats.nix new file mode 100644 index 000000000000..bee36f262f4c --- /dev/null +++ b/nixos/tests/nats.nix @@ -0,0 +1,65 @@ +let + + port = 4222; + username = "client"; + password = "password"; + topic = "foo.bar"; + +in import ./make-test-python.nix ({ pkgs, lib, ... }: { + name = "nats"; + meta = with pkgs.lib; { maintainers = with maintainers; [ c0deaddict ]; }; + + nodes = let + client = { pkgs, ... }: { + environment.systemPackages = with pkgs; [ natscli ]; + }; + in { + server = { pkgs, ... }: { + networking.firewall.allowedTCPPorts = [ port ]; + services.nats = { + inherit port; + enable = true; + settings = { + authorization = { + users = [{ + user = username; + inherit password; + }]; + }; + }; + }; + }; + + client1 = client; + client2 = client; + }; + + testScript = let file = "/tmp/msg"; + in '' + def nats_cmd(*args): + return ( + "nats " + "--server=nats://server:${toString port} " + "--user=${username} " + "--password=${password} " + "{}" + ).format(" ".join(args)) + + start_all() + server.wait_for_unit("nats.service") + + client1.fail("test -f ${file}") + + # Subscribe on topic on client1 and echo messages to file. + client1.execute("({} | tee ${file} &)".format(nats_cmd("sub", "--raw", "${topic}"))) + + # Give client1 some time to subscribe. + client1.execute("sleep 2") + + # Publish message on client2. + client2.execute(nats_cmd("pub", "${topic}", "hello")) + + # Check if message has been received. + client1.succeed("grep -q hello ${file}") + ''; +}) |