3.0
If you upgrade from 2.5 without code changes, expect:
- UDP servers will drop
Access-Requestpackets that don't carryMessage-Authenticator. Passrequire_message_authenticator=Falseto restore the old default. - UDP clients will refuse
Access-Accept/Reject/Challengereplies missingMessage-Authenticator. Passenforce_ma=False. Serverwill reject packets whose Request Authenticator fails MD5 verification. Passenable_pkt_verify=False.RadSecServer/RadSecClientwon't accept TLS 1.2 connections unless you passminimum_tls_version=ssl.TLSVersion.TLSv1_2.- Existing log pipelines watching for
[pyrad2 trace]lines onPYRAD2_TRACE=1will go silent untilPYRAD2_TRACE_UNSAFE=1is also set.
This is a security and defaults overhaul. Every UDP server / client in pyrad2 now ships with BlastRADIUS-safe defaults out of the box, RadSec deployments default to TLS 1.3, and the sync server now verifies request authenticators before invoking your handlers , matching the long-standing ServerAsync behaviour. All of these are observable behaviour breaks for existing deployments; the section below documents each opt-out.
Security defaults (BREAKING)
- BlastRADIUS (CVE-2024-3596) mitigated by default.
Server,ServerAsync,Client, andClientAsyncnow default to enforcingMessage-Authenticatoron everyAccess-Requestand on the matchingAccess-Accept/Reject/Challengereply. To bridge a legacy NAS that doesn't emit the attribute, passrequire_message_authenticator=False(servers) orenforce_ma=False(clients). - Sync
Servernow verifies request authenticators by default. MirrorsServerAsync.enable_pkt_verifywhich previously was the only side that ran the check. Passenable_pkt_verify=Falseto opt out for legacy NASes that emit malformed authenticators. - RadSec defaults to TLS 1.3.
RadSecServer.DEFAULT_MINIMUM_TLS_VERSIONandRadSecClient.DEFAULT_MINIMUM_TLS_VERSIONare nowssl.TLSVersion.TLSv1_3(RFC 9325 deprecates TLS 1.1-, treats 1.2 as legacy; RFC 9750 mandates 1.3 for RADIUS/1.1). Passminimum_tls_version=ssl.TLSVersion.TLSv1_2to bridge a legacy peer that can't yet negotiate 1.3. The floor is auto-promoted to 1.3 whenradius_versionsincludesV1_1, regardless. - Constant-time MAC and MD5 comparisons across the verify path. All switched from
==tohmac.compare_digest, closing a timing-side-channel that let an off-path attacker probe valid MACs one byte at a time. - Zero-authenticator Access-Request rejected.
AuthPacket.verify_auth_requestnow rejects v1.0 Access-Requests whose Request Authenticator is all-zero.Packet.salt_cryptno longer falls back to a zero authenticator when one is missing , it callscreate_authenticator()so salt-encrypted attributes (Tunnel-Password, MS-MPPE keys) can't be recovered without the shared secret. PYRAD2_TRACEnow requires a second-step acknowledgement. SettingPYRAD2_TRACE=1no longer activates the wire trace on its own. The trace dumps the Request Authenticator and obfuscatedUser-Passwordbytes; with the shared secret known (commonly to anyone reading the log archive), the plaintext is fully recoverable. SetPYRAD2_TRACE_UNSAFE=1alongside to acknowledge and enable. A warning is logged on activation.
Security hardening
$INCLUDEsandboxing.Dictionary/DictFilenow confine$INCLUDEresolution to a base directory (defaults to the entry-point file's parent; configurable viainclude_base_dir=). A dictionary with$INCLUDE /etc/passwdor$INCLUDE ../../fooraisesParseError. The entry-point file is exempt , it defines the base.ParseErrorsignature tightened. The old**dataswallowsilently droppedname=,path=, and similar typos. The new signature explicitly acceptsfile=andline=; the filename now consistently surfaces in error messages.- Vendor-id range check.
DictionaryrejectsVENDORdeclarations outside0..0xFFFFFF(RFC 2865 §5.26 SMI PEN range) instead of silently passing through values that later corrupt the VSA encoder. eap.password_from_packetno longer falls back to User-Name. ReturningUser-Nameas the EAP-MD5 secret silently mis-keyed the challenge; any observer who saw the request could reproduce the digest. RaisesPacketErrorwhenUser-Passwordis absent.pw_decryptwarns on shared-secret mismatch. Non-UTF-8 bytes after de-obfuscation almost always indicate the receiver's secret doesn't match the sender's. The function now logs aWARNINGonce per call and continues with a lossy decode so legacy handlers don't crash.- Dedup cache failed-handler behaviour documented. A handler that exits without sending a reply still drops its in-flight marker, so a retransmission within the TTL window is processed fresh rather than suppressed , bug acknowledged.
Architecture
- New
pyrad2.router.RequestRouterunifies the dispatch state machine thatServerandServerAsyncpreviously each owned separately. - Packet subclass deduplication.
- Cross-client factory deduplication.
Per-transport identifier management
DatagramProtocolClient.create_idscans for a free slot instead of blindly returning(packet_id + 1) % 256. Raises a new typedIdentifierExhausted(inpyrad2.exceptions) when all 256 slots on a single (source IP, source port) flow are in flight. The bare-Exceptioncollision error insend_packetis now also typed.- Module-level
CURRENT_IDis thread-safe. Athreading.Lockserialises the increment so concurrentPacket()construction across threads can't read+write the same counter and end up with colliding ids.
Performance
- RFC 2865 keystream loops rewritten for
_salt_en_decrypt,pw_crypt, andpw_decrypt. The byte-at-a-timebytes((hash[i] ^ buf[i],))concat chain is replaced by an int-XOR +bytearrayaccumulator. ~3× faster on User-Password and Tunnel-Password sized inputs. - Asyncio deprecation cleanup.
DX / quality
- Sync
Client._send_packetno longer mutates the caller'sAcct-Delay-Time. The retry loop still bumps the value while a request is in flight, but it now snapshots the caller's original list (or absence) and restores it on exit , so reusing the sameAcctPacketacross multiplesend_packetcalls no longer compounds the delay. getaddrinfofixes.- Test suite migrated to pytest-style.
- Packaging metadata consolidated.
setup.pyandsetup.cfg
deleted.pyproject.tomlcarries the full PEP 639license = "BSD-3-Clause"+license-files = ["LICENSE.txt"], full
classifiers,dynamic = ["version"]reading from
pyrad2/__init__.py, and[build-system]with hatchling. - Documentation tooling no longer ships to runtime users.