Skip to content

Commit dd38f42

Browse files
authored
feat(#441): lot 5 netcross_core, tls_diagnostics et quic_diagnostics
Résumés DEBUG par capture/diagnostic, cas particuliers en TRACE ; échec AEAD QUIC en TRACE au lieu de logger.exception.
1 parent 0473d9a commit dd38f42

3 files changed

Lines changed: 105 additions & 1 deletion

File tree

‎src/netcross_core/quic_diagnostics.py‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ def _parse_long_header(payload: bytes) -> dict | None:
163163

164164
version = int.from_bytes(payload[1:5], "big")
165165
if version != QUIC_V1:
166+
logger.trace("_parse_long_header: version QUIC 0x{:08x} non geree, paquet ignore", version)
166167
return None # seule la v1 (RFC 9001) est geree -- voir limites en tete de fichier
167168

168169
i = 5
@@ -196,6 +197,7 @@ def _parse_long_header(payload: bytes) -> dict | None:
196197
pn_offset = i
197198

198199
if pn_offset + remaining_len > len(payload) or remaining_len < 4:
200+
logger.trace("_parse_long_header: longueur annoncee {} incoherente ({} octets)", remaining_len, len(payload))
199201
return None # incoherent : pas assez de place pour PN(<=4)+charge utile+tag(16)
200202

201203
return {
@@ -213,6 +215,7 @@ def _remove_header_protection(payload: bytearray, pn_offset: int, hp_key: bytes)
213215
-- c'est ce dernier qui fait foi (voir _decrypt_initial)."""
214216
sample_start = pn_offset + 4
215217
if sample_start + 16 > len(payload):
218+
logger.trace("_remove_header_protection: echantillon hors du paquet ({} octets)", len(payload))
216219
return None
217220
sample = bytes(payload[sample_start : sample_start + 16])
218221
encryptor = Cipher(algorithms.AES(hp_key), modes.ECB()).encryptor()
@@ -259,7 +262,8 @@ def _decrypt_initial(payload: bytes, client_secret: bytes, header_info: dict) ->
259262
try:
260263
return AESGCM(key).decrypt(bytes(nonce), ciphertext, header)
261264
except InvalidTag:
262-
logger.exception("erreur: InvalidTag")
265+
# resultat normal (voir docstring), a chaque paquet : TRACE, sans traceback
266+
logger.trace("_decrypt_initial: tag AEAD invalide, paquet {} non dechiffrable", packet_number)
263267
return None
264268

265269

@@ -286,10 +290,12 @@ def _extract_client_hello_from_crypto(plaintext: bytes) -> bytes | None:
286290
if i + length > len(plaintext):
287291
return None
288292
if offset != 0:
293+
logger.trace("_extract_client_hello_from_crypto: CRYPTO a l'offset {}, ClientHello fragmente", offset)
289294
return None # ClientHello fragmente sur plusieurs paquets : hors perimetre
290295
return plaintext[i : i + length]
291296
# autre type de frame (ACK, etc.) : pas de parseur generique pour
292297
# sauter son corps proprement, on s'arrete la plutot que deviner
298+
logger.trace("_extract_client_hello_from_crypto: trame 0x{:02x} avant CRYPTO, arret", frame_type)
293299
return None
294300
return None
295301

@@ -300,6 +306,7 @@ def _parse_tls_handshake_from_crypto(crypto_data: bytes) -> dict | None:
300306
body_len = int.from_bytes(crypto_data[1:4], "big")
301307
body = crypto_data[4 : 4 + body_len]
302308
if len(body) < body_len:
309+
logger.trace("_parse_tls_handshake_from_crypto: ClientHello incomplet ({}/{} octets)", len(body), body_len)
303310
return None # ClientHello incomplet dans ce paquet (fragmente) : hors perimetre
304311
return parse_client_hello(body)
305312

@@ -311,6 +318,7 @@ def parse_quic_capture(label: str, path: str) -> list[QuicEvent]:
311318
cas, jamais de donnee inventee)."""
312319
events: list[QuicEvent] = []
313320
raw_packets = pcap_parser.parse_capture(path, raise_on_error=False)
321+
logger.debug("parse_quic_capture: point {}, {} paquet(s) lus depuis {}", label, len(raw_packets), path)
314322

315323
for raw in raw_packets:
316324
if raw.proto != "UDP" or not raw.payload:
@@ -357,6 +365,13 @@ def parse_quic_capture(label: str, path: str) -> list[QuicEvent]:
357365
)
358366
)
359367

368+
logger.debug(
369+
"parse_quic_capture: point {}, {} Initial QUIC v1 dont {} non dechiffrable(s), {} avec SNI",
370+
label,
371+
len(events),
372+
sum(1 for ev in events if not ev.decryptable),
373+
sum(1 for ev in events if ev.sni),
374+
)
360375
return events
361376

362377

@@ -377,7 +392,14 @@ def diagnose_quic(events: list[QuicEvent], points_order: list[str] | None = None
377392
if not e.decryptable:
378393
decrypt_failures[e.point] = decrypt_failures.get(e.point, 0) + 1
379394

395+
logger.debug(
396+
"diagnose_quic: {} evenement(s), {} connexion(s) (DCID), echecs de dechiffrement {}",
397+
len(events),
398+
len(by_dcid),
399+
decrypt_failures,
400+
)
380401
if not points_order:
402+
logger.debug("diagnose_quic: sans ordre des points, seuls les echecs de dechiffrement sont rapportes")
381403
for p, n in decrypt_failures.items():
382404
findings.append(
383405
TlsFinding(
@@ -431,10 +453,12 @@ def diagnose_quic(events: list[QuicEvent], points_order: list[str] | None = None
431453
)
432454
)
433455

456+
logger.debug("diagnose_quic: {} constat(s)", len(findings))
434457
return findings
435458

436459

437460
def print_quic_diagnostics(findings) -> None:
461+
logger.debug("print_quic_diagnostics: {} constat(s)", len(findings))
438462
print("=" * 70)
439463
print("DIAGNOSTIC QUIC/HTTP3 -- ClientHello vus par point (SNI)")
440464
print("=" * 70)

‎src/netcross_core/tls_diagnostics.py‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ def parse_client_hello(body: bytes) -> dict:
142142
approximative en cas de doute.
143143
"""
144144
if len(body) < 34:
145+
logger.trace("parse_client_hello: corps trop court ({} octets), ignore", len(body))
145146
return {}
146147
version = _tls_version_str(body[0], body[1])
147148
i = 34 # client_version(2) + random(32)
@@ -159,6 +160,7 @@ def parse_client_hello(body: bytes) -> dict:
159160
i += 1 + comp_len
160161
ext_total_len = _read_u16(body, i)
161162
if ext_total_len is None:
163+
logger.trace("parse_client_hello: ClientHello sans extensions, SNI inconnu")
162164
return {"tls_version": version}
163165
i += 2
164166
ext_end = min(i + ext_total_len, len(body))
@@ -180,6 +182,7 @@ def parse_client_hello(body: bytes) -> dict:
180182
def parse_server_hello(body: bytes) -> dict:
181183
"""Extrait la version TLS negociee et le cipher suite choisi."""
182184
if len(body) < 35:
185+
logger.trace("parse_server_hello: corps trop court ({} octets), ignore", len(body))
183186
return {}
184187
version = _tls_version_str(body[0], body[1])
185188
i = 34
@@ -193,6 +196,7 @@ def parse_server_hello(body: bytes) -> dict:
193196

194197
def parse_alert(body: bytes) -> dict:
195198
if len(body) < 2:
199+
logger.trace("parse_alert: alerte tronquee ({} octet(s))", len(body))
196200
return {}
197201
level = ALERT_LEVELS.get(body[0], f"unknown({body[0]})")
198202
desc = ALERT_DESCRIPTIONS.get(body[1], f"unknown({body[1]})")
@@ -210,13 +214,15 @@ def _iter_tls_records(
210214
content_type, major, minor = payload[i], payload[i + 1], payload[i + 2]
211215
length = int.from_bytes(payload[i + 3 : i + 5], "big")
212216
if content_type not in TLS_CONTENT_TYPES or major != 3:
217+
logger.trace("_iter_tls_records: plus de TLS bien forme a l'offset {} (type {})", i, content_type)
213218
break
214219
body_start = i + 5
215220
body_end = body_start + length
216221
truncated = body_end > n
217222
body = payload[body_start : min(body_end, n)]
218223
yield content_type, major, minor, length, body, truncated
219224
if truncated:
225+
logger.trace("_iter_tls_records: record de {} octets tronque a {}", length, len(body))
220226
break # le reste est dans un segment suivant, pas de reassemblage ici
221227
i = body_end
222228

@@ -233,6 +239,7 @@ def _iter_handshake_messages(body: bytes) -> Iterator[tuple[int, bytes, bool]]:
233239
msg_start = i + 4
234240
msg_end = msg_start + hlen
235241
if msg_end > n:
242+
logger.trace("_iter_handshake_messages: message {} tronque ({}/{} octets)", htype, n - msg_start, hlen)
236243
yield htype, body[msg_start:n], True
237244
break
238245
yield htype, body[msg_start:msg_end], False
@@ -251,6 +258,8 @@ def parse_tls_capture(label: str, path: str) -> list[TlsEvent]:
251258
planter tout le run -- coherent avec le reste de netcross_core."""
252259
events: list[TlsEvent] = []
253260
raw_packets = pcap_parser.parse_capture(path, raise_on_error=False)
261+
logger.debug("parse_tls_capture: point {}, {} paquet(s) lus depuis {}", label, len(raw_packets), path)
262+
tls_segments = 0
254263

255264
for raw in raw_packets:
256265
if raw.proto != "TCP" or not raw.payload:
@@ -259,6 +268,7 @@ def parse_tls_capture(label: str, path: str) -> list[TlsEvent]:
259268
payload = raw.payload
260269
if not _looks_like_tls(payload):
261270
continue
271+
tls_segments += 1
262272

263273
ts = raw.ts
264274
src, dst = raw.src, raw.dst
@@ -326,6 +336,13 @@ def parse_tls_capture(label: str, path: str) -> list[TlsEvent]:
326336
)
327337
)
328338

339+
logger.debug(
340+
"parse_tls_capture: point {}, {} segment(s) TLS, {} evenement(s) dont {} tronque(s)",
341+
label,
342+
tls_segments,
343+
len(events),
344+
sum(1 for ev in events if ev.truncated),
345+
)
329346
return events
330347

331348

@@ -388,6 +405,12 @@ def build_handshake_status(
388405
elif ev.record_type == "application_data":
389406
st.application_data_seen = True
390407

408+
logger.debug(
409+
"build_handshake_status: {} evenement(s) -> {} flux sur {} point(s)",
410+
len(events),
411+
sum(len(per_point) for per_point in status.values()),
412+
len(status),
413+
)
391414
return status
392415

393416

@@ -413,6 +436,9 @@ def diagnose_tls(
413436
"""
414437
findings: list[TlsFinding] = []
415438
points = points_order or sorted(status_by_point)
439+
logger.debug(
440+
"diagnose_tls: {} point(s), ordre {}", len(points), "fourni" if points_order else "alphabetique (par defaut)"
441+
)
416442

417443
# -- alertes fatales, un constat par point --
418444
for point in points:
@@ -439,6 +465,7 @@ def diagnose_tls(
439465
if st_b is None:
440466
# flux pas vu au point suivant -- hors perimetre de ce module
441467
# (voir netcross_core.analyse pour les pertes)
468+
logger.trace("diagnose_tls: flux {} absent en {}, ignore", fid, b)
442469
continue
443470
if st_a.verdict == "complete" and st_b.verdict != "complete":
444471
broken_here.append((st_a, st_b))
@@ -478,10 +505,16 @@ def diagnose_tls(
478505
f.segment,
479506
)
480507
)
508+
logger.debug(
509+
"diagnose_tls: {} constat(s) dont {} anomalie(s)",
510+
len(findings),
511+
sum(1 for f in findings if f.severity == "anomalie"),
512+
)
481513
return findings
482514

483515

484516
def print_tls_diagnostics(findings: list[TlsFinding]) -> None:
517+
logger.debug("print_tls_diagnostics: {} constat(s)", len(findings))
485518
print("=" * 70)
486519
print("DIAGNOSTIC TLS -- etat des handshakes par point")
487520
print("=" * 70)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"""#441, lot 5 : journalisation de tls_diagnostics et quic_diagnostics."""
2+
3+
from __future__ import annotations
4+
5+
import pytest
6+
from loguru import logger
7+
8+
from netcross_core import quic_diagnostics as quic
9+
from netcross_core import tls_diagnostics as tls
10+
11+
12+
@pytest.fixture
13+
def journal():
14+
logger.enable("netcross_core")
15+
lignes: list[tuple[str, str]] = []
16+
sink = logger.add(lambda m: lignes.append((m.record["level"].name, m.record["message"])), level="TRACE")
17+
yield lignes
18+
logger.remove(sink)
19+
20+
21+
def _vu(journal, niveau, fragment):
22+
return any(n == niveau and fragment in m for n, m in journal)
23+
24+
25+
def test_cas_particuliers_tls_en_trace(journal):
26+
assert tls.parse_alert(b"\x02") == {}
27+
assert tls.parse_client_hello(b"\x03\x03") == {}
28+
assert tls.parse_server_hello(b"") == {}
29+
assert _vu(journal, "TRACE", "alerte tronquee")
30+
assert _vu(journal, "TRACE", "parse_client_hello: corps trop court")
31+
assert _vu(journal, "TRACE", "parse_server_hello: corps trop court")
32+
33+
34+
def test_resumes_tls_en_debug(journal):
35+
status = tls.build_handshake_status([])
36+
tls.diagnose_tls(status)
37+
assert _vu(journal, "DEBUG", "build_handshake_status: 0 evenement(s)")
38+
assert _vu(journal, "DEBUG", "ordre alphabetique")
39+
assert _vu(journal, "DEBUG", "diagnose_tls: 0 constat(s)")
40+
41+
42+
def test_quic_version_non_geree_et_resume(journal):
43+
paquet = bytes([0xC0]) + (2).to_bytes(4, "big") + b"\x00" * 10
44+
assert quic._parse_long_header(paquet) is None
45+
assert quic.diagnose_quic([]) == []
46+
assert _vu(journal, "TRACE", "version QUIC 0x00000002 non geree")
47+
assert _vu(journal, "DEBUG", "sans ordre des points")

0 commit comments

Comments
 (0)