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

1from datetime import timedelta 

2 

3import pytest 

4import yaml 

5from django.db import IntegrityError 

6from django.utils import timezone 

7from jwcrypto.jwk import JWK 

8 

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) 

21 

22 

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" 

28 

29 

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" 

36 

37 

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" 

55 

56 

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 

71 

72 

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") 

78 

79 

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 ) 

91 

92 

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() 

97 

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 ) 

105 

106 security_group = create_group( 

107 network.id, "test_security_group", "Test security group" 

108 ) 

109 host.groups.add(security_group) 

110 host.save() 

111 

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) 

124 

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 ) 

138 

139 config_yaml = generate_config_yaml(host.id) 

140 config_dict = yaml.safe_load(config_yaml) 

141 

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 

148 

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"} 

157 

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" 

163 

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 

172 

173 

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) 

177 

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 ) 

194 

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 

198 

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 

206 

207 

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) 

212 

213 auth_key = JWK.generate(kty="RSA") 

214 public_auth_key = auth_key.export_public() 

215 _, public_key = create_keys() 

216 

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 

227 

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 ) 

238 

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() 

252 

253 

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) 

260 

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 ) 

272 

273 

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) 

278 

279 for i in range(3): 

280 auth_key = JWK.generate(kty="RSA") 

281 _, public_key = create_keys() 

282 

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 ) 

291 

292 if i == 0: 

293 assert host.name == "test-host" 

294 else: 

295 assert host.name == f"test-host-{i}" 

296 

297 

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() 

324 

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 ) 

333 

334 assert host.groups.count() == 1 

335 assert host.groups.first() == security_group 

336 

337 

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) 

346 

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() 

351 

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 ) 

360 

361 assert host is not None 

362 assert host.name == "test-host" 

363 

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() 

368 

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 ) 

380 

381 

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() 

396 

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 ) 

405 

406 assert host is not None 

407 assert host.name == f"test-host-{i}" 

408 

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() 

413 

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 ) 

423 

424 

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) 

433 

434 # Enrollment should fail 

435 auth_key = JWK.generate(kty="RSA") 

436 public_auth_key = auth_key.export_public() 

437 _, public_key = create_keys() 

438 

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 ) 

448 

449 

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) 

458 

459 auth_key = JWK.generate(kty="RSA") 

460 public_auth_key = auth_key.export_public() 

461 _, public_key = create_keys() 

462 

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 ) 

471 

472 assert host is not None 

473 assert host.is_ephemeral is True 

474 

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() 

485 

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 ) 

494 

495 assert host2 is not None 

496 assert host2.is_ephemeral is False 

497 

498 

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 )