Coverage for src/meshadmin/server/networks/tests/test_services.py: 100%
215 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-10 16:08 +0200
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-10 16:08 +0200
1from datetime import timedelta
3import pytest
4import yaml
5from django.db import IntegrityError
6from django.utils import timezone
7from jwcrypto.jwk import JWK
9from meshadmin.common.utils import create_keys
10from meshadmin.server.networks.models import Host, Rule
11from meshadmin.server.networks.services import (
12 create_available_hosts_iterator,
13 create_group,
14 create_network_ca,
15 create_template,
16 enrollment,
17 generate_config_yaml,
18 generate_enrollment_token,
19 network_available_hosts_iterator,
20)
23def test_create_available_hosts_iterator():
24 cidr = "192.168.1.0/24"
25 iterator = create_available_hosts_iterator(cidr, [])
26 first_ip = next(iterator)
27 assert str(first_ip) == "192.168.1.1"
30def test_create_available_hosts_iterator_with_unavailable_ips():
31 cidr = "192.168.1.0/24"
32 unavailable = ["192.168.1.1", "192.168.1.2"]
33 iterator = create_available_hosts_iterator(cidr, unavailable)
34 first_available = next(iterator)
35 assert str(first_available) == "192.168.1.3"
38def test_network_available_hosts_iterator(test_network):
39 network = test_network(name="testnet", cidr="10.0.0.0/24")
40 Host.objects.create(
41 network=network,
42 name="host1",
43 assigned_ip="10.0.0.1",
44 public_key="test",
45 )
46 Host.objects.create(
47 network=network,
48 name="host2",
49 assigned_ip="10.0.0.2",
50 public_key="test",
51 )
52 iterator = network_available_hosts_iterator(network)
53 first_available = next(iterator)
54 assert str(first_available) == "10.0.0.3"
57def test_create_network(test_network):
58 network_name = "testnet"
59 network_cidr = "100.100.64.0/24"
60 test_net = test_network(name=network_name, cidr=network_cidr)
61 assert test_net.name == network_name
62 assert test_net.cidr == network_cidr
63 assert test_net.signingca.ca.name == "auto created initial ca"
64 assert test_net.signingca.ca.cert is not None
65 assert test_net.signingca.ca.key is not None
66 assert test_net.signingca.ca.cert_print is not None
67 assert (
68 test_net.signingca.ca.cert_print["details"]["name"] == "auto created initial ca"
69 )
70 assert test_net.signingca.ca.cert_print["details"]["isCa"] is True
73def test_cannot_create_duplicate_groups_for_the_same_network(test_network):
74 test_net = test_network(name="testnet", cidr="100.100.64.0/24")
75 create_group(test_net.pk, "group1")
76 with pytest.raises(IntegrityError):
77 create_group(test_net.pk, "group1")
80def test_cannot_create_a_template_based_on_a_non_existing_group(test_network):
81 test_net = test_network(name="testnet", cidr="100.100.64.0/24")
82 with pytest.raises(LookupError):
83 create_template(
84 "hosts",
85 test_net.name,
86 is_lighthouse=False,
87 is_relay=False,
88 use_relay=True,
89 groups=["group1", "group2"],
90 )
93def test_generate_config_yaml_with_firewall(test_network):
94 network = test_network(name="testnet", cidr="10.0.0.0/24")
95 create_network_ca("test_ca", network)
96 _, public_key = create_keys()
98 host = Host.objects.create(
99 network=network,
100 name="test_host",
101 assigned_ip="10.0.0.1",
102 public_key=public_key,
103 interface="nebula1",
104 )
106 security_group = create_group(
107 network.id, "test_security_group", "Test security group"
108 )
109 host.groups.add(security_group)
110 host.save()
112 group1 = create_group(network.id, "group1")
113 group2 = create_group(network.id, "group2")
114 rule = Rule.objects.create(
115 security_group=security_group,
116 direction=Rule.Direction.INBOUND,
117 port="80",
118 proto="tcp",
119 cidr="0.0.0.0/0",
120 local_cidr="10.0.0.0/24",
121 group=group1,
122 )
123 rule.groups.add(group1, group2)
125 # Outbound rules
126 Rule.objects.create(
127 security_group=security_group,
128 direction=Rule.Direction.OUTBOUND,
129 port="443",
130 proto="tcp",
131 cidr="0.0.0.0/0",
132 )
133 Rule.objects.create(
134 security_group=security_group,
135 direction=Rule.Direction.OUTBOUND,
136 proto="udp",
137 )
139 config_yaml = generate_config_yaml(host.id)
140 config_dict = yaml.safe_load(config_yaml)
142 # Verify firewall configuration exists
143 assert "firewall" in config_dict
144 assert "inbound" in config_dict["firewall"]
145 assert "outbound" in config_dict["firewall"]
146 assert len(config_dict["firewall"]["inbound"]) == 1
147 assert len(config_dict["firewall"]["outbound"]) == 2
149 # Verify inbound rule with all attributes
150 inbound_rule = config_dict["firewall"]["inbound"][0]
151 assert inbound_rule["port"] == "80"
152 assert inbound_rule["proto"] == "tcp"
153 assert inbound_rule["cidr"] == "0.0.0.0/0"
154 assert inbound_rule["local_cidr"] == "10.0.0.0/24"
155 assert inbound_rule["group"] == "group1"
156 assert set(inbound_rule["groups"]) == {"group1", "group2"}
158 # Verify simple outbound rule
159 outbound_rule = config_dict["firewall"]["outbound"][0]
160 assert outbound_rule["port"] == "443"
161 assert outbound_rule["proto"] == "tcp"
162 assert outbound_rule["cidr"] == "0.0.0.0/0"
164 # Verify minimal outbound rule
165 minimal_rule = config_dict["firewall"]["outbound"][1]
166 assert minimal_rule["proto"] == "udp"
167 assert minimal_rule["port"] == "any"
168 assert "cidr" not in minimal_rule
169 assert "local_cidr" not in minimal_rule
170 assert "group" not in minimal_rule
171 assert "groups" not in minimal_rule
174def test_lighthouse_relay_configuration(test_network):
175 network = test_network(name="testnet", cidr="10.0.0.0/24")
176 create_network_ca("test_ca", network)
178 lighthouse = Host.objects.create(
179 network=network,
180 name="lighthouse",
181 assigned_ip="10.0.0.1",
182 public_key=create_keys()[1],
183 is_lighthouse=True,
184 is_relay=True,
185 public_ip_or_hostname="public.lighthouse.com",
186 )
187 host = Host.objects.create(
188 network=network,
189 name="client",
190 assigned_ip="10.0.0.2",
191 public_key=create_keys()[1],
192 use_relay=True,
193 )
195 lighthouse_config = yaml.safe_load(generate_config_yaml(lighthouse.id))
196 assert lighthouse_config["lighthouse"]["am_lighthouse"] is True
197 assert lighthouse_config["relay"]["am_relay"] is True
199 client_config = yaml.safe_load(generate_config_yaml(host.id))
200 assert client_config["lighthouse"]["am_lighthouse"] is False
201 assert client_config["lighthouse"]["hosts"] == ["10.0.0.1"]
202 assert client_config["static_host_map"]["10.0.0.1"] == [
203 "public.lighthouse.com:4242"
204 ]
205 assert client_config["relay"]["use_relays"] is True
208def test_enrollment_with_existing_host_cases(test_network):
209 network = test_network(name="testnet", cidr="10.0.0.0/24")
210 template = create_template("test_template", network.name)
211 token = generate_enrollment_token(template)
213 auth_key = JWK.generate(kty="RSA")
214 public_auth_key = auth_key.export_public()
215 _, public_key = create_keys()
217 # Initial host
218 host1 = enrollment(
219 enrollment_key=token,
220 public_net_key=public_key,
221 public_auth_key=public_auth_key,
222 preferred_hostname="test_host",
223 public_ip="127.0.0.1",
224 enroll_on_existence=False,
225 )
226 host1_id = host1.id
228 # Case 1: enroll_on_existence=True should raise ValueError
229 with pytest.raises(ValueError, match="Host already enrolled"):
230 enrollment(
231 enrollment_key=token,
232 public_net_key=public_key,
233 public_auth_key=public_auth_key,
234 preferred_hostname="test_host2",
235 public_ip="127.0.0.2",
236 enroll_on_existence=True,
237 )
239 # Case 2: enroll_on_existence=False should delete old host and create new one
240 host2 = enrollment(
241 enrollment_key=token,
242 public_net_key=public_key,
243 public_auth_key=public_auth_key,
244 preferred_hostname="test_host2",
245 public_ip="127.0.0.2",
246 enroll_on_existence=False,
247 )
248 assert host2.id != host1_id
249 assert host2.name == "test_host2"
250 assert host2.public_ip_or_hostname == "127.0.0.2"
251 assert not Host.objects.filter(id=host1_id).exists()
254def test_enrollment_lighthouse_without_public_ip(test_network):
255 network = test_network(name="testnet", cidr="10.0.0.0/24")
256 template = create_template(
257 "test_template", network.name, is_lighthouse=True, is_relay=False
258 )
259 token = generate_enrollment_token(template)
261 with pytest.raises(
262 ValueError, match="Cannot enroll a lighthouse without public_ip"
263 ):
264 enrollment(
265 enrollment_key=token,
266 public_net_key="test_key",
267 public_auth_key=JWK.generate(kty="RSA").export_public(),
268 preferred_hostname="test_host",
269 public_ip=None,
270 enroll_on_existence=False,
271 )
274def test_enrollment_hostname_increment(test_network):
275 network = test_network(name="testnet", cidr="10.0.0.0/24")
276 template = create_template("test_template", network.name)
277 token = generate_enrollment_token(template)
279 for i in range(3):
280 auth_key = JWK.generate(kty="RSA")
281 _, public_key = create_keys()
283 host = enrollment(
284 enrollment_key=token,
285 public_net_key=public_key,
286 public_auth_key=auth_key.export_public(),
287 preferred_hostname="test-host",
288 public_ip=f"127.0.0.{i + 1}",
289 enroll_on_existence=False,
290 )
292 if i == 0:
293 assert host.name == "test-host"
294 else:
295 assert host.name == f"test-host-{i}"
298def test_template_with_security_group(test_network):
299 network = test_network(name="testnet", cidr="10.0.0.0/24")
300 create_network_ca("test_ca", network)
301 security_group = create_group(
302 network.id, "test_security_group", "Test security group"
303 )
304 group1 = create_group(network.id, "group1")
305 Rule.objects.create(
306 security_group=security_group,
307 direction=Rule.Direction.INBOUND,
308 port="80",
309 proto="tcp",
310 group=group1,
311 )
312 template = create_template(
313 "test_template",
314 network.name,
315 is_lighthouse=False,
316 is_relay=False,
317 use_relay=True,
318 groups=[security_group.name],
319 )
320 token = generate_enrollment_token(template)
321 auth_key = JWK.generate(kty="RSA")
322 public_auth_key = auth_key.export_public()
323 _, public_key = create_keys()
325 host = enrollment(
326 enrollment_key=token,
327 public_net_key=public_key,
328 public_auth_key=public_auth_key,
329 preferred_hostname="test-host",
330 public_ip="127.0.0.1",
331 enroll_on_existence=False,
332 )
334 assert host.groups.count() == 1
335 assert host.groups.first() == security_group
338def test_non_reusable_enrollment_key(test_network):
339 network = test_network(name="testnet", cidr="10.0.0.0/24")
340 template = create_template(
341 "single_use_template",
342 network.name,
343 reusable=False,
344 )
345 token = generate_enrollment_token(template)
347 # First enrollment should succeed
348 auth_key = JWK.generate(kty="RSA")
349 public_auth_key = auth_key.export_public()
350 _, public_key = create_keys()
352 host = enrollment(
353 enrollment_key=token,
354 public_net_key=public_key,
355 public_auth_key=public_auth_key,
356 preferred_hostname="test-host",
357 public_ip="127.0.0.1",
358 enroll_on_existence=False,
359 )
361 assert host is not None
362 assert host.name == "test-host"
364 # Second enrollment with same key should fail
365 auth_key2 = JWK.generate(kty="RSA")
366 public_auth_key2 = auth_key2.export_public()
367 _, public_key2 = create_keys()
369 with pytest.raises(
370 ValueError, match="Single-use enrollment key has already been used"
371 ):
372 enrollment(
373 enrollment_key=token,
374 public_net_key=public_key2,
375 public_auth_key=public_auth_key2,
376 preferred_hostname="another-host",
377 public_ip="127.0.0.2",
378 enroll_on_existence=False,
379 )
382def test_enrollment_key_with_usage_limit(test_network):
383 network = test_network(name="testnet", cidr="10.0.0.0/24")
384 template = create_template(
385 "limited_use_template",
386 network.name,
387 reusable=True,
388 usage_limit=2,
389 )
390 token = generate_enrollment_token(template)
391 # First two enrollments should succeed
392 for i in range(2):
393 auth_key = JWK.generate(kty="RSA")
394 public_auth_key = auth_key.export_public()
395 _, public_key = create_keys()
397 host = enrollment(
398 enrollment_key=token,
399 public_net_key=public_key,
400 public_auth_key=public_auth_key,
401 preferred_hostname=f"test-host-{i}",
402 public_ip=f"127.0.0.{i + 1}",
403 enroll_on_existence=False,
404 )
406 assert host is not None
407 assert host.name == f"test-host-{i}"
409 # Third enrollment should fail
410 auth_key = JWK.generate(kty="RSA")
411 public_auth_key = auth_key.export_public()
412 _, public_key = create_keys()
414 with pytest.raises(ValueError, match="Enrollment key usage limit exceeded"):
415 enrollment(
416 enrollment_key=token,
417 public_net_key=public_key,
418 public_auth_key=public_auth_key,
419 preferred_hostname="test-host-3",
420 public_ip="127.0.0.3",
421 enroll_on_existence=False,
422 )
425def test_expired_enrollment_key(test_network):
426 network = test_network(name="testnet", cidr="10.0.0.0/24")
427 template = create_template(
428 "expired_template",
429 network.name,
430 expires_at=timezone.now() - timedelta(days=1),
431 )
432 token = generate_enrollment_token(template)
434 # Enrollment should fail
435 auth_key = JWK.generate(kty="RSA")
436 public_auth_key = auth_key.export_public()
437 _, public_key = create_keys()
439 with pytest.raises(ValueError, match="Enrollment token has expired"):
440 enrollment(
441 enrollment_key=token,
442 public_net_key=public_key,
443 public_auth_key=public_auth_key,
444 preferred_hostname="test-host",
445 public_ip="127.0.0.1",
446 enroll_on_existence=False,
447 )
450def test_ephemeral_peers_flag(test_network):
451 network = test_network(name="testnet", cidr="10.0.0.0/24")
452 template = create_template(
453 "ephemeral_template",
454 network.name,
455 ephemeral_peers=True,
456 )
457 token = generate_enrollment_token(template)
459 auth_key = JWK.generate(kty="RSA")
460 public_auth_key = auth_key.export_public()
461 _, public_key = create_keys()
463 host = enrollment(
464 enrollment_key=token,
465 public_net_key=public_key,
466 public_auth_key=public_auth_key,
467 preferred_hostname="test-host",
468 public_ip="127.0.0.1",
469 enroll_on_existence=False,
470 )
472 assert host is not None
473 assert host.is_ephemeral is True
475 # Test with ephemeral_peers=False
476 template2 = create_template(
477 "non_ephemeral_template",
478 network.name,
479 ephemeral_peers=False,
480 )
481 token2 = generate_enrollment_token(template2)
482 auth_key2 = JWK.generate(kty="RSA")
483 public_auth_key2 = auth_key2.export_public()
484 _, public_key2 = create_keys()
486 host2 = enrollment(
487 enrollment_key=token2,
488 public_net_key=public_key2,
489 public_auth_key=public_auth_key2,
490 preferred_hostname="test-host-2",
491 public_ip="127.0.0.2",
492 enroll_on_existence=False,
493 )
495 assert host2 is not None
496 assert host2.is_ephemeral is False
499def test_token_with_nonexistent_template(test_network):
500 network = test_network(name="testnet", cidr="10.0.0.0/24")
501 template = create_template("test_template", network.name)
502 token = generate_enrollment_token(template)
503 template.delete()
504 auth_key = JWK.generate(kty="RSA")
505 public_auth_key = auth_key.export_public()
506 _, public_key = create_keys()
507 with pytest.raises(ValueError, match="Template not found"):
508 enrollment(
509 enrollment_key=token,
510 public_net_key=public_key,
511 public_auth_key=public_auth_key,
512 preferred_hostname="test-host",
513 public_ip="127.0.0.1",
514 enroll_on_existence=False,
515 )