diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index dea0ca13565d..4f93e1f361d7 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -806,6 +806,7 @@
./services/networking/smartdns.nix
./services/networking/smokeping.nix
./services/networking/softether.nix
+ ./services/networking/solanum.nix
./services/networking/spacecookie.nix
./services/networking/spiped.nix
./services/networking/squid.nix
diff --git a/nixos/modules/services/networking/solanum.nix b/nixos/modules/services/networking/solanum.nix
new file mode 100644
index 000000000000..989621b204ce
--- /dev/null
+++ b/nixos/modules/services/networking/solanum.nix
@@ -0,0 +1,104 @@
+{ config, lib, pkgs, ... }:
+
+let
+ inherit (lib) mkEnableOption mkIf mkOption types;
+ inherit (pkgs) solanum;
+ cfg = config.services.solanum;
+
+ configFile = pkgs.writeText "solanum.conf" cfg.config;
+in
+
+{
+
+ ###### interface
+
+ options = {
+
+ services.solanum = {
+
+ enable = mkEnableOption "Solanum IRC daemon";
+
+ config = mkOption {
+ type = types.str;
+ default = ''
+ serverinfo {
+ name = "irc.example.com";
+ sid = "1ix";
+ description = "irc!";
+
+ vhost = "0.0.0.0";
+ vhost6 = "::";
+ };
+
+ listen {
+ host = "0.0.0.0";
+ port = 6667;
+ };
+
+ auth {
+ user = "*@*";
+ class = "users";
+ flags = exceed_limit;
+ };
+ channel {
+ default_split_user_count = 0;
+ };
+ '';
+ description = ''
+ Solanum IRC daemon configuration file.
+ check for all options.
+ '';
+ };
+
+ openFilesLimit = mkOption {
+ type = types.int;
+ default = 1024;
+ description = ''
+ Maximum number of open files. Limits the clients and server connections.
+ '';
+ };
+
+ motd = mkOption {
+ type = types.nullOr types.lines;
+ default = null;
+ description = ''
+ Solanum MOTD text.
+
+ Solanum will read its MOTD from /etc/solanum/ircd.motd.
+ If set, the value of this option will be written to this path.
+ '';
+ };
+
+ };
+
+ };
+
+
+ ###### implementation
+
+ config = mkIf cfg.enable (lib.mkMerge [
+ {
+ systemd.services.solanum = {
+ description = "Solanum IRC daemon";
+ after = [ "network.target" ];
+ wantedBy = [ "multi-user.target" ];
+ environment = {
+ BANDB_DBPATH = "/var/lib/solanum/ban.db";
+ };
+ serviceConfig = {
+ ExecStart = "${solanum}/bin/solanum -foreground -logfile /dev/stdout -configfile ${configFile} -pidfile /run/solanum/ircd.pid";
+ DynamicUser = true;
+ User = "solanum";
+ StateDirectory = "solanum";
+ RuntimeDirectory = "solanum";
+ LimitNOFILE = "${toString cfg.openFilesLimit}";
+ };
+ };
+
+ }
+
+ (mkIf (cfg.motd != null) {
+ environment.etc."solanum/ircd.motd".text = cfg.motd;
+ })
+ ]);
+}
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 420e6eaf2e87..4ada4a5de804 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -384,6 +384,7 @@ in
snapcast = handleTest ./snapcast.nix {};
snapper = handleTest ./snapper.nix {};
sogo = handleTest ./sogo.nix {};
+ solanum = handleTest ./solanum.nix {};
solr = handleTest ./solr.nix {};
sonarr = handleTest ./sonarr.nix {};
spacecookie = handleTest ./spacecookie.nix {};
diff --git a/nixos/tests/solanum.nix b/nixos/tests/solanum.nix
new file mode 100644
index 000000000000..aabfb906aa81
--- /dev/null
+++ b/nixos/tests/solanum.nix
@@ -0,0 +1,89 @@
+let
+ clients = [
+ "ircclient1"
+ "ircclient2"
+ ];
+ server = "solanum";
+ ircPort = 6667;
+ channel = "nixos-cat";
+ iiDir = "/tmp/irc";
+in
+
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+ name = "solanum";
+ nodes = {
+ "${server}" = {
+ networking.firewall.allowedTCPPorts = [ ircPort ];
+ services.solanum = {
+ enable = true;
+ };
+ };
+ } // lib.listToAttrs (builtins.map (client: lib.nameValuePair client {
+ imports = [
+ ./common/user-account.nix
+ ];
+
+ systemd.services.ii = {
+ requires = [ "network.target" ];
+ wantedBy = [ "default.target" ];
+
+ serviceConfig = {
+ Type = "simple";
+ ExecPreStartPre = "mkdir -p ${iiDir}";
+ ExecStart = ''
+ ${lib.getBin pkgs.ii}/bin/ii -n ${client} -s ${server} -i ${iiDir}
+ '';
+ User = "alice";
+ };
+ };
+ }) clients);
+
+ testScript =
+ let
+ msg = client: "Hello, my name is ${client}";
+ clientScript = client: [
+ ''
+ ${client}.wait_for_unit("network.target")
+ ${client}.systemctl("start ii")
+ ${client}.wait_for_unit("ii")
+ ${client}.wait_for_file("${iiDir}/${server}/out")
+ ''
+ # wait until first PING from server arrives before joining,
+ # so we don't try it too early
+ ''
+ ${client}.wait_until_succeeds("grep 'PING' ${iiDir}/${server}/out")
+ ''
+ # join ${channel}
+ ''
+ ${client}.succeed("echo '/j #${channel}' > ${iiDir}/${server}/in")
+ ${client}.wait_for_file("${iiDir}/${server}/#${channel}/in")
+ ''
+ # send a greeting
+ ''
+ ${client}.succeed(
+ "echo '${msg client}' > ${iiDir}/${server}/#${channel}/in"
+ )
+ ''
+ # check that all greetings arrived on all clients
+ ] ++ builtins.map (other: ''
+ ${client}.succeed(
+ "grep '${msg other}$' ${iiDir}/${server}/#${channel}/out"
+ )
+ '') clients;
+
+ # foldl', but requires a non-empty list instead of a start value
+ reduce = f: list:
+ builtins.foldl' f (builtins.head list) (builtins.tail list);
+ in ''
+ start_all()
+ ${server}.systemctl("status solanum")
+ ${server}.wait_for_open_port(${toString ircPort})
+
+ # run clientScript for all clients so that every list
+ # entry is executed by every client before advancing
+ # to the next one.
+ '' + lib.concatStrings
+ (reduce
+ (lib.zipListsWith (cs: c: cs + c))
+ (builtins.map clientScript clients));
+})