diff --git a/nixos/modules/misc/ids.nix b/nixos/modules/misc/ids.nix
index a7ef2ca1803b..91df29236b8f 100644
--- a/nixos/modules/misc/ids.nix
+++ b/nixos/modules/misc/ids.nix
@@ -125,6 +125,7 @@
minecraft = 114;
monetdb = 115;
rippled = 116;
+ murmur = 117;
# When adding a uid, make sure it doesn't match an existing gid.
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index fa9c222fc8d1..86bb87e91dec 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -182,6 +182,7 @@
./services/networking/ircd-hybrid/default.nix
./services/networking/kippo.nix
./services/networking/minidlna.nix
+ ./services/networking/murmur.nix
./services/networking/nat.nix
./services/networking/networkmanager.nix
./services/networking/ngircd.nix
diff --git a/nixos/modules/services/networking/murmur.nix b/nixos/modules/services/networking/murmur.nix
new file mode 100644
index 000000000000..2a5549beaf89
--- /dev/null
+++ b/nixos/modules/services/networking/murmur.nix
@@ -0,0 +1,253 @@
+{ config, pkgs, ... }:
+
+with pkgs.lib;
+
+let
+ cfg = config.services.murmur;
+ configFile = pkgs.writeText "murmurd.ini" ''
+ database=/var/lib/murmur/murmur.sqlite
+ dbDriver=QSQLITE
+
+ autobanAttempts=${toString cfg.autobanAttempts}
+ autobanTimeframe=${toString cfg.autobanTimeframe}
+ autobanTime=${toString cfg.autobanTime}
+
+ logfile=/var/log/murmur/murmurd.log
+ pidfile=${cfg.pidfile}
+
+ welcome="${cfg.welcome}"
+ port=${toString cfg.port}
+
+ ${if cfg.hostName == "" then "" else "host="+cfg.hostName}
+ ${if cfg.password == "" then "" else "serverpassword="+cfg.password}
+
+ bandwidth=${toString cfg.bandwidth}
+ users=${toString cfg.users}
+
+ textmessagelength=${toString cfg.textMsgLength}
+ imagemessagelength=${toString cfg.imgMsgLength}
+ allowhtml=${if cfg.allowHtml then "true" else "false"}
+ logdays=${toString cfg.logDays}
+ bonjour=${if cfg.bonjour then "true" else "false"}
+ sendversion=${if cfg.sendVersion then "true" else "false"}
+
+ ${if cfg.registerName == "" then "" else "registerName="+cfg.registerName}
+ ${if cfg.registerPassword == "" then "" else "registerPassword="+cfg.registerPassword}
+ ${if cfg.registerUrl == "" then "" else "registerUrl="+cfg.registerUrl}
+ ${if cfg.registerHostname == "" then "" else "registerHostname="+cfg.registerHostname}
+
+ certrequired=${if cfg.clientCertRequired then "true" else "false"}
+ ${if cfg.sslCert == "" then "" else "sslCert="+cfg.sslCert}
+ ${if cfg.sslKey == "" then "" else "sslKey="+cfg.sslKey}
+ '';
+in
+{
+ options = {
+ services.murmur = {
+ enable = mkOption {
+ type = types.bool;
+ default = false;
+ description = "If enabled, start the Murmur Service.";
+ };
+
+ autobanAttempts = mkOption {
+ type = types.int;
+ default = 10;
+ description = ''
+ Number of attempts a client is allowed to make in
+ autobanTimeframe seconds, before being
+ banned for autobanTime.
+ '';
+ };
+
+ autobanTimeframe = mkOption {
+ type = types.int;
+ default = 120;
+ description = ''
+ Timeframe in which a client can connect without being banned
+ for repeated attempts (in seconds).
+ '';
+ };
+
+ autobanTime = mkOption {
+ type = types.int;
+ default = 300;
+ description = "The amount of time an IP ban lasts (in seconds).";
+ };
+
+ pidfile = mkOption {
+ type = types.path;
+ default = "/tmp/murmurd.pid";
+ description = "Path to PID file for Murmur daemon.";
+ };
+
+ welcome = mkOption {
+ type = types.str;
+ default = "";
+ description = "Welcome message for connected clients.";
+ };
+
+ port = mkOption {
+ type = types.int;
+ default = 64738;
+ description = "Ports to bind to (UDP and TCP).";
+ };
+
+ hostName = mkOption {
+ type = types.str;
+ default = "";
+ description = "Host to bind to. Defaults binding on all addresses.";
+ };
+
+ password = mkOption {
+ type = types.str;
+ default = "";
+ description = "Required password to join server, if specified.";
+ };
+
+ bandwidth = mkOption {
+ type = types.int;
+ default = 72000;
+ description = ''
+ Maximum bandwidth (in bits per second) that clients may send
+ speech at.
+ '';
+ };
+
+ users = mkOption {
+ type = types.int;
+ default = 100;
+ description = "Maximum number of concurrent clients allowed.";
+ };
+
+ textMsgLength = mkOption {
+ type = types.int;
+ default = 5000;
+ description = "Max length of text messages. Set 0 for no limit.";
+ };
+
+ imgMsgLength = mkOption {
+ type = types.int;
+ default = 131072;
+ description = "Max length of image messages. Set 0 for no limit.";
+ };
+
+ allowHtml = mkOption {
+ type = types.bool;
+ default = true;
+ description = ''
+ Allow HTML in client messages, comments, and channel
+ descriptions.
+ '';
+ };
+
+ logDays = mkOption {
+ type = types.int;
+ default = 31;
+ description = ''
+ How long to store RPC logs for in the database. Set 0 to
+ keep logs forever, or -1 to disable DB logging.
+ '';
+ };
+
+ bonjour = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Enable Bonjour auto-discovery, which allows clients over
+ your LAN to automatically discover Murmur servers.
+ '';
+ };
+
+ sendVersion = mkOption {
+ type = types.bool;
+ default = true;
+ description = "Send Murmur version in UDP response.";
+ };
+
+ registerName = mkOption {
+ type = types.str;
+ default = "";
+ description = ''
+ Public server registration name, and also the name of the
+ Root channel. Even if you don't publicly register your
+ server, you probably still want to set this.
+ '';
+ };
+
+ registerPassword = mkOption {
+ type = types.str;
+ default = "";
+ description = ''
+ Public server registry password, used authenticate your
+ server to the registry to prevent impersonation; required for
+ subsequent registry updates.
+ '';
+ };
+
+ registerUrl = mkOption {
+ type = types.str;
+ default = "";
+ description = "URL website for your server.";
+ };
+
+ registerHostname = mkOption {
+ type = types.str;
+ default = "";
+ description = ''
+ DNS hostname where your server can be reached. This is only
+ needed if you want your server to be accessed by its
+ hostname and not IP - but the name *must* resolve on the
+ internet properly.
+ '';
+ };
+
+ clientCertRequired = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Require clients to authenticate via certificates.";
+ };
+
+ sslCert = mkOption {
+ type = types.str;
+ default = "";
+ description = "Path to your SSL certificate.";
+ };
+
+ sslKey = mkOption {
+ type = types.str;
+ default = "";
+ description = "Path to your SSL key.";
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ users.extraUsers.murmur = {
+ description = "Murmur Service user";
+ home = "/var/lib/murmur";
+ createHome = true;
+ uid = config.ids.uids.murmur;
+ };
+
+ systemd.services.murmur = {
+ description = "Murmur Chat Service";
+ wantedBy = [ "multi-user.target" ];
+ after = [ "network.target "];
+
+ serviceConfig = {
+ Type = "forking";
+ PIDFile = cfg.pidfile;
+ Restart = "always";
+ User = "murmur";
+ ExecStart = "${pkgs.murmur}/bin/murmurd -ini ${configFile}";
+ PermissionsStartOnly = true;
+ };
+
+ preStart = ''
+ mkdir -p /var/log/murmur
+ chown -R murmur /var/log/murmur
+ '';
+ };
+ };
+}
diff --git a/nixos/tests/default.nix b/nixos/tests/default.nix
index 4aeb7f55ac39..955c87b2714e 100644
--- a/nixos/tests/default.nix
+++ b/nixos/tests/default.nix
@@ -25,6 +25,7 @@ with import ../lib/testing.nix { inherit system minimal; };
mysql = makeTest (import ./mysql.nix);
mysql_replication = makeTest (import ./mysql-replication.nix);
munin = makeTest (import ./munin.nix);
+ mumble = makeTest (import ./mumble.nix);
nat = makeTest (import ./nat.nix);
nfs3 = makeTest (import ./nfs.nix { version = 3; });
#nfs4 = makeTest (import ./nfs.nix { version = 4; });
diff --git a/nixos/tests/mumble.nix b/nixos/tests/mumble.nix
new file mode 100644
index 000000000000..509742f2899b
--- /dev/null
+++ b/nixos/tests/mumble.nix
@@ -0,0 +1,54 @@
+{ pkgs, ... }:
+
+let
+ client = { config, pkgs, ... }: {
+ imports = [ ./common/x11.nix ];
+ environment.systemPackages = [ pkgs.mumble ];
+ };
+in
+{
+ nodes = {
+ server = { config, pkgs, ... }: {
+ services.murmur.enable = true;
+ services.murmur.registerName = "NixOS tests";
+ };
+
+ client1 = client;
+ client2 = client;
+ };
+
+ testScript = ''
+ startAll;
+
+ $server->waitForUnit("murmur.service");
+ $client1->waitForX;
+ $client2->waitForX;
+
+ $client1->execute("mumble mumble://client1\@server/test &");
+ $client2->execute("mumble mumble://client2\@server/test &");
+
+ $server->sleep(10); # Wait for Mumble UI to pop up
+
+ # cancel client audio configuration
+ $client1->sendKeys("esc");
+ $client2->sendKeys("esc");
+ $server->sleep(1);
+
+ # cancel client cert configuration
+ $client1->sendKeys("esc");
+ $client2->sendKeys("esc");
+ $server->sleep(1);
+
+ # accept server certificate
+ $client1->sendChars("y");
+ $client2->sendChars("y");
+
+ # Find clients in logs
+ $server->waitUntilSucceeds("grep -q 'client1' /var/log/murmur/murmurd.log");
+ $server->waitUntilSucceeds("grep -q 'client2' /var/log/murmur/murmurd.log");
+
+ $server->sleep(5); # wait to get screenshot
+ $client1->screenshot("screen1");
+ $client2->screenshot("screen2");
+ '';
+}