1{ pkgs, ... }:
2
3let
4 port = 1888;
5 tlsPort = 1889;
6 anonPort = 1890;
7 password = "VERY_secret";
8 hashedPassword = "$7$101$/WJc4Mp+I+uYE9sR$o7z9rD1EYXHPwEP5GqQj6A7k4W1yVbePlb8TqNcuOLV9WNCiDgwHOB0JHC1WCtdkssqTBduBNUnUGd6kmZvDSw==";
9 topic = "test/foo";
10
11 snakeOil =
12 pkgs.runCommand "snakeoil-certs"
13 {
14 buildInputs = [ pkgs.gnutls.bin ];
15 caTemplate = pkgs.writeText "snakeoil-ca.template" ''
16 cn = server
17 expiration_days = -1
18 cert_signing_key
19 ca
20 '';
21 certTemplate = pkgs.writeText "snakeoil-cert.template" ''
22 cn = server
23 expiration_days = -1
24 tls_www_server
25 encryption_key
26 signing_key
27 '';
28 userCertTemplate = pkgs.writeText "snakeoil-user-cert.template" ''
29 organization = snakeoil
30 cn = client1
31 expiration_days = -1
32 tls_www_client
33 encryption_key
34 signing_key
35 '';
36 }
37 ''
38 mkdir "$out"
39
40 certtool -p --bits 2048 --outfile "$out/ca.key"
41 certtool -s --template "$caTemplate" --load-privkey "$out/ca.key" \
42 --outfile "$out/ca.crt"
43 certtool -p --bits 2048 --outfile "$out/server.key"
44 certtool -c --template "$certTemplate" \
45 --load-ca-privkey "$out/ca.key" \
46 --load-ca-certificate "$out/ca.crt" \
47 --load-privkey "$out/server.key" \
48 --outfile "$out/server.crt"
49
50 certtool -p --bits 2048 --outfile "$out/client1.key"
51 certtool -c --template "$userCertTemplate" \
52 --load-privkey "$out/client1.key" \
53 --load-ca-privkey "$out/ca.key" \
54 --load-ca-certificate "$out/ca.crt" \
55 --outfile "$out/client1.crt"
56 '';
57
58in
59{
60 name = "mosquitto";
61 meta = with pkgs.lib; {
62 maintainers = with maintainers; [ peterhoeg ];
63 };
64
65 nodes =
66 let
67 client =
68 { pkgs, ... }:
69 {
70 environment.systemPackages = with pkgs; [ mosquitto ];
71 };
72 in
73 {
74 server =
75 { pkgs, ... }:
76 {
77 networking.firewall.allowedTCPPorts = [
78 port
79 tlsPort
80 anonPort
81 ];
82 networking.useNetworkd = true;
83 services.mosquitto = {
84 enable = true;
85 settings = {
86 sys_interval = 1;
87 };
88 listeners = [
89 {
90 inherit port;
91 users = {
92 password_store = {
93 inherit password;
94 };
95 password_file = {
96 passwordFile = pkgs.writeText "mqtt-password" password;
97 };
98 hashed_store = {
99 inherit hashedPassword;
100 };
101 hashed_file = {
102 hashedPasswordFile = pkgs.writeText "mqtt-hashed-password" hashedPassword;
103 };
104
105 reader = {
106 inherit password;
107 acl = [
108 "read ${topic}"
109 "read $SYS/#" # so we always have something to read
110 ];
111 };
112 writer = {
113 inherit password;
114 acl = [ "write ${topic}" ];
115 };
116 };
117 }
118 {
119 port = tlsPort;
120 users.client1 = {
121 acl = [ "read $SYS/#" ];
122 };
123 settings = {
124 cafile = "${snakeOil}/ca.crt";
125 certfile = "${snakeOil}/server.crt";
126 keyfile = "${snakeOil}/server.key";
127 require_certificate = true;
128 use_identity_as_username = true;
129 };
130 }
131 {
132 port = anonPort;
133 omitPasswordAuth = true;
134 settings.allow_anonymous = true;
135 acl = [ "pattern read #" ];
136 users = {
137 anonWriter = {
138 password = "<ignored>" + password;
139 acl = [ "write ${topic}" ];
140 };
141 };
142 }
143 ];
144 };
145 };
146
147 client1 = client;
148 client2 = client;
149 };
150
151 testScript = ''
152 def mosquitto_cmd(binary, user, topic, port):
153 return (
154 "mosquitto_{} "
155 "-V mqttv311 "
156 "-h server "
157 "-p {} "
158 "-u {} "
159 "-P '${password}' "
160 "-t '{}'"
161 ).format(binary, port, user, topic)
162
163
164 def publish(args, user, topic="${topic}", port=${toString port}):
165 return "{} {}".format(mosquitto_cmd("pub", user, topic, port), args)
166
167 def subscribe(args, user, topic="${topic}", port=${toString port}):
168 return "{} -W 5 -C 1 {}".format(mosquitto_cmd("sub", user, topic, port), args)
169
170 def parallel(*fns):
171 from threading import Thread
172 threads = [ Thread(target=fn) for fn in fns ]
173 for t in threads: t.start()
174 for t in threads: t.join()
175
176 def wait_uuid(uuid):
177 server.wait_for_console_text(uuid)
178 return None
179
180
181 start_all()
182 server.wait_for_unit("mosquitto.service")
183
184 with subtest("check passwords"):
185 client1.succeed(publish("-m test", "password_store"))
186 client1.succeed(publish("-m test", "password_file"))
187 client1.succeed(publish("-m test", "hashed_store"))
188 client1.succeed(publish("-m test", "hashed_file"))
189
190 with subtest("check acl"):
191 client1.succeed(subscribe("", "reader", topic="$SYS/#"))
192 client1.fail(subscribe("", "writer", topic="$SYS/#"))
193
194 parallel(
195 lambda: client1.succeed(subscribe("-i 3688cdd7-aa07-42a4-be22-cb9352917e40", "reader")),
196 lambda: [
197 wait_uuid("3688cdd7-aa07-42a4-be22-cb9352917e40"),
198 client2.succeed(publish("-m test", "writer"))
199 ])
200
201 parallel(
202 lambda: client1.fail(subscribe("-i 24ff16a2-ae33-4a51-9098-1b417153c712", "reader")),
203 lambda: [
204 wait_uuid("24ff16a2-ae33-4a51-9098-1b417153c712"),
205 client2.succeed(publish("-m test", "reader"))
206 ])
207
208 with subtest("check tls"):
209 client1.succeed(
210 subscribe(
211 "--cafile ${snakeOil}/ca.crt "
212 "--cert ${snakeOil}/client1.crt "
213 "--key ${snakeOil}/client1.key",
214 topic="$SYS/#",
215 port=${toString tlsPort},
216 user="no_such_user"))
217
218 with subtest("check omitPasswordAuth"):
219 parallel(
220 lambda: client1.succeed(subscribe("-i fd56032c-d9cb-4813-a3b4-6be0e04c8fc3",
221 "anonReader", port=${toString anonPort})),
222 lambda: [
223 wait_uuid("fd56032c-d9cb-4813-a3b4-6be0e04c8fc3"),
224 client2.succeed(publish("-m test", "anonWriter", port=${toString anonPort}))
225 ])
226 '';
227}