diff --git a/nixos/tests/mosquitto.nix b/nixos/tests/mosquitto.nix index 1a534184066c..eb47e97ba04b 100644 --- a/nixos/tests/mosquitto.nix +++ b/nixos/tests/mosquitto.nix @@ -2,13 +2,59 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: let port = 1888; - username = "mqtt"; + tlsPort = 1889; password = "VERY_secret"; + hashedPassword = "$7$101$/WJc4Mp+I+uYE9sR$o7z9rD1EYXHPwEP5GqQj6A7k4W1yVbePlb8TqNcuOLV9WNCiDgwHOB0JHC1WCtdkssqTBduBNUnUGd6kmZvDSw=="; topic = "test/foo"; + + snakeOil = pkgs.runCommand "snakeoil-certs" { + buildInputs = [ pkgs.gnutls.bin ]; + caTemplate = pkgs.writeText "snakeoil-ca.template" '' + cn = server + expiration_days = -1 + cert_signing_key + ca + ''; + certTemplate = pkgs.writeText "snakeoil-cert.template" '' + cn = server + expiration_days = -1 + tls_www_server + encryption_key + signing_key + ''; + userCertTemplate = pkgs.writeText "snakeoil-user-cert.template" '' + organization = snakeoil + cn = client1 + expiration_days = -1 + tls_www_client + encryption_key + signing_key + ''; + } '' + mkdir "$out" + + certtool -p --bits 2048 --outfile "$out/ca.key" + certtool -s --template "$caTemplate" --load-privkey "$out/ca.key" \ + --outfile "$out/ca.crt" + certtool -p --bits 2048 --outfile "$out/server.key" + certtool -c --template "$certTemplate" \ + --load-ca-privkey "$out/ca.key" \ + --load-ca-certificate "$out/ca.crt" \ + --load-privkey "$out/server.key" \ + --outfile "$out/server.crt" + + certtool -p --bits 2048 --outfile "$out/client1.key" + certtool -c --template "$userCertTemplate" \ + --load-privkey "$out/client1.key" \ + --load-ca-privkey "$out/ca.key" \ + --load-ca-certificate "$out/ca.crt" \ + --outfile "$out/client1.crt" + ''; + in { name = "mosquitto"; meta = with pkgs.lib; { - maintainers = with maintainers; [ peterhoeg ]; + maintainers = with maintainers; [ pennae peterhoeg ]; }; nodes = let @@ -17,79 +63,131 @@ in { }; in { server = { pkgs, ... }: { - networking.firewall.allowedTCPPorts = [ port ]; + networking.firewall.allowedTCPPorts = [ port tlsPort ]; services.mosquitto = { enable = true; + settings = { + sys_interval = 1; + }; listeners = [ { inherit port; - users.${username} = { - inherit password; - acl = [ - "readwrite ${topic}" - ]; + users = { + password_store = { + inherit password; + }; + password_file = { + passwordFile = pkgs.writeText "mqtt-password" password; + }; + hashed_store = { + inherit hashedPassword; + }; + hashed_file = { + hashedPasswordFile = pkgs.writeText "mqtt-hashed-password" hashedPassword; + }; + + reader = { + inherit password; + acl = [ + "read ${topic}" + "read $SYS/#" # so we always have something to read + ]; + }; + writer = { + inherit password; + acl = [ "write ${topic}" ]; + }; + }; + } + { + port = tlsPort; + users.client1 = { + acl = [ "read $SYS/#" ]; + }; + settings = { + cafile = "${snakeOil}/ca.crt"; + certfile = "${snakeOil}/server.crt"; + keyfile = "${snakeOil}/server.key"; + require_certificate = true; + use_identity_as_username = true; }; } ]; }; - - # disable private /tmp for this test - systemd.services.mosquitto.serviceConfig.PrivateTmp = lib.mkForce false; }; client1 = client; client2 = client; }; - testScript = let - file = "/tmp/msg"; - in '' - def mosquitto_cmd(binary): + testScript = '' + def mosquitto_cmd(binary, user, topic, port): return ( - "${pkgs.mosquitto}/bin/mosquitto_{} " + "mosquitto_{} " "-V mqttv311 " "-h server " - "-p ${toString port} " - "-u ${username} " + "-p {} " + "-u {} " "-P '${password}' " - "-t ${topic}" - ).format(binary) + "-t '{}'" + ).format(binary, port, user, topic) - def publish(args): - return "{} {}".format(mosquitto_cmd("pub"), args) + def publish(args, user, topic="${topic}", port=${toString port}): + return "{} {}".format(mosquitto_cmd("pub", user, topic, port), args) - def subscribe(args): - return "({} -C 1 {} | tee ${file} &)".format(mosquitto_cmd("sub"), args) + def subscribe(args, user, topic="${topic}", port=${toString port}): + return "{} -C 1 {}".format(mosquitto_cmd("sub", user, topic, port), args) + + def parallel(*fns): + from threading import Thread + threads = [ Thread(target=fn) for fn in fns ] + for t in threads: t.start() + for t in threads: t.join() start_all() server.wait_for_unit("mosquitto.service") - for machine in server, client1, client2: - machine.fail("test -f ${file}") + def check_passwords(): + client1.succeed(publish("-m test", "password_store")) + client1.succeed(publish("-m test", "password_file")) + client1.succeed(publish("-m test", "hashed_store")) + client1.succeed(publish("-m test", "hashed_file")) - # QoS = 0, so only one subscribers should get it - server.execute(subscribe("-q 0")) + check_passwords() - # we need to give the subscribers some time to connect - client2.execute("sleep 5") - client2.succeed(publish("-m FOO -q 0")) + def check_acl(): + client1.succeed(subscribe("", "reader", topic="$SYS/#")) + client1.fail(subscribe("-W 5", "writer", topic="$SYS/#")) - server.wait_until_succeeds("grep -q FOO ${file}") - server.execute("rm ${file}") + parallel( + lambda: client1.succeed(subscribe("-i 3688cdd7-aa07-42a4-be22-cb9352917e40", "reader")), + lambda: [ + server.wait_for_console_text("3688cdd7-aa07-42a4-be22-cb9352917e40"), + client2.succeed(publish("-m test", "writer")) + ]) - # QoS = 1, so both subscribers should get it - server.execute(subscribe("-q 1")) - client1.execute(subscribe("-q 1")) + parallel( + lambda: client1.fail(subscribe("-W 5 -i 24ff16a2-ae33-4a51-9098-1b417153c712", "reader")), + lambda: [ + server.wait_for_console_text("24ff16a2-ae33-4a51-9098-1b417153c712"), + client2.succeed(publish("-m test", "reader")) + ]) - # we need to give the subscribers some time to connect - client2.execute("sleep 5") - client2.succeed(publish("-m BAR -q 1")) + check_acl() - for machine in server, client1: - machine.wait_until_succeeds("grep -q BAR ${file}") - machine.execute("rm ${file}") + def check_tls(): + client1.succeed( + subscribe( + "--cafile ${snakeOil}/ca.crt " + "--cert ${snakeOil}/client1.crt " + "--key ${snakeOil}/client1.key", + topic="$SYS/#", + port=${toString tlsPort}, + user="no_such_user")) + + check_tls() ''; })