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