@@ -5,7 +5,119 @@ All notable changes to this project will be documented in this file.
55The format is based on [ Keep a Changelog] ( https://keepachangelog.com/en/1.1.0/ ) ,
66and this project adheres to [ Semantic Versioning] ( https://semver.org/spec/v2.0.0.html ) .
77
8- ## [ Unreleased]
8+ ## [ 1.5.1] - 2026-02-15
9+
10+ ### Added
11+
12+ - ** SC TLS stress testing** : All BACnet/SC stress tests and benchmarks now use
13+ mutual TLS 1.3 with a mock CA (EC P-256) by default, matching production
14+ requirements (Annex AB.7.4). A shared ` docker/lib/sc_pki.py ` module generates
15+ the test PKI. Docker SC scenarios use init containers to generate certificates
16+ into shared volumes. The local ` bench_sc.py ` benchmark accepts ` --no-tls ` to
17+ fall back to plaintext for comparison.
18+
19+ - ** ` add_route() ` API** : Added ` add_route(network, router_address) ` to ` Client ` ,
20+ ` BACnetApplication ` , and ` NetworkLayer ` for pre-populating the router cache.
21+ Enables communication with devices on remote networks without broadcast-based
22+ router discovery — required in Docker bridge networks where ephemeral-port
23+ clients cannot receive broadcast responses on the standard BACnet port.
24+
25+ ### Fixed
26+
27+ - ** Router per-port broadcast address** : Added ` broadcast_address ` field to
28+ ` RouterPortConfig ` and pass it to ` BIPTransport ` in router mode. Previously,
29+ all router ports used the default global broadcast (` 255.255.255.255 ` ), which
30+ fails in Docker bridge networks where directed subnet broadcasts are required.
31+ Docker router services now use ` BROADCAST_ADDRESS_1 ` /` BROADCAST_ADDRESS_2 `
32+ environment variables for per-port configuration.
33+ - ** BIPTransport ephemeral port broadcast** : Fixed ` BIPTransport.send_broadcast() `
34+ sending to port 0 when the transport was created with ` port=0 ` (ephemeral).
35+ The bound port is now stored after socket binding so broadcasts and BBMD
36+ advertisements use the correct port.
37+ - ** Docker router stress test** : Fixed the pre-existing router stress test failure
38+ caused by three compounding issues: (1) per-port broadcast addresses not being
39+ passed to the router's BIPTransport, (2) ephemeral port clients unable to
40+ receive broadcast I-Am responses forwarded by the router, and (3) missing
41+ router cache pre-population. The test now uses ` add_route() ` and a direct
42+ server address (` SERVER_ADDRESS ` ) to bypass all broadcast-dependent discovery.
43+
44+ ### Security
45+
46+ - ** SC hub VMAC collision race fix** : Added ` _pending_vmacs ` reservation set to
47+ ` SCHubFunction ` to prevent a TOCTOU race between VMAC collision check and
48+ connection registration during the handshake window (Annex AB.6.2).
49+ - ** SC URI scheme validation** : ` SCNodeSwitch.establish_direct() ` now validates
50+ that hub-provided peer URIs use ` ws:// ` or ` wss:// ` schemes before
51+ connecting, preventing SSRF-like redirection to non-WebSocket endpoints.
52+ - ** SC header options count cap** : BVLC-SC header option decoding now limits
53+ lists to 32 options per message (defense-in-depth against malformed payloads).
54+ - ** SC pending resolution cache cap** : ` SCNodeSwitch.resolve_address() ` now
55+ rejects new resolution requests when the cache reaches ` max_connections ` ,
56+ preventing unbounded memory growth from address resolution flooding.
57+ - ** BBMD max BDT entries** : Added ` max_bdt_entries ` parameter (default 128) to
58+ ` BBMDManager ` . Write-BDT requests exceeding the limit are NAKed, preventing
59+ oversized BDT payloads from consuming unbounded memory.
60+ - ** IPv6 VMAC cache size limit** : ` VMACCache ` now accepts a ` max_entries `
61+ parameter (default 4096). When full, stale entries are evicted first; if still
62+ full, the oldest entry is dropped.
63+ - ** IPv6 pending resolution cap** : ` BIP6Transport.send_unicast() ` now limits
64+ the pending VMAC resolution cache to 1024 entries, preventing unbounded growth
65+ from resolution requests to many unknown VMACs.
66+ - ** H1: ` decode_real ` /` decode_double ` buffer validation** : Added explicit length
67+ checks before ` struct.unpack_from ` in ` encoding/primitives.py ` , raising
68+ ` ValueError ` instead of the opaque ` struct.error ` on truncated input.
69+ - ** H2: ErrorPDU bounds check** : Added bounds checks after each ` decode_tag() ` in
70+ ` _decode_error() ` (` encoding/apdu.py ` ) to reject truncated error class/code
71+ fields before slicing.
72+ - ** H3: ` extract_context_value ` overflow check** : Added bounds validation in
73+ ` encoding/tags.py ` to reject primitive tags whose length extends past the
74+ buffer end, preventing silent reads of stale/adjacent memory.
75+ - ** H4: Ethernet 802.3 minimum length** : ` _decode_frame() ` in
76+ ` transport/ethernet.py ` now rejects frames with length field < LLC header
77+ size (3 bytes), preventing underflow in NPDU extraction.
78+ - ** C1: Service decoder list caps** : Added ` _MAX_DECODED_ITEMS = 10,000 ` cap to
79+ all unbounded decode loops across 8 service files (19 loops total):
80+ ` read_property_multiple ` , ` write_property_multiple ` , ` alarm_summary ` , ` cov ` ,
81+ ` write_group ` , ` virtual_terminal ` , ` object_mgmt ` , and ` audit ` .
82+ - ** C2: ` ObjectType ` vendor cache cap** : ` ObjectType._missing_() ` now clears the
83+ vendor cache at 4096 entries, matching the ` PropertyIdentifier ` pattern and
84+ preventing unbounded growth from vendor-proprietary object types.
85+ - ** C3: Segmentation reassembly size cap** : ` SegmentReceiver ` now tracks total
86+ reassembly bytes and returns ` ABORT ` when the cumulative size exceeds 1 MiB.
87+ Added ` created_at ` timestamp field for stale receiver detection.
88+ - ** C4: Audit nesting depth enforcement** : Added depth checks (max 32) to all
89+ manual tag-nesting loops in ` services/audit.py ` (4 loops), preventing stack
90+ exhaustion from deeply nested opening tags in audit decode paths.
91+ - ** S1: Hub pending VMAC TTL and cap** : Converted ` _pending_vmacs ` from ` set ` to
92+ ` dict[SCVMAC, float] ` with 30-second TTL purge and ` max_connections ` cap in
93+ ` transport/sc/hub_function.py ` , preventing unbounded growth from slow or
94+ abandoned handshakes.
95+ - ** S2: SC header option data size cap** : Added ` _MAX_OPTION_DATA_SIZE = 512 `
96+ limit to ` SCHeaderOption.decode_list() ` in ` transport/sc/bvlc.py ` , rejecting
97+ oversized option data (up to 65535 per option) early in the decode path.
98+ - ** S3: SC WebSocket oversized frame rate limit** : ` SCWebSocket._process_frame() `
99+ now tracks consecutive oversized frames and raises ` ConnectionClosedError `
100+ after 3 in a row, preventing log flooding from misbehaving peers.
101+ - ** S4: SC WebSocket pending events cap** : Capped ` _pending_events ` buffer at 64
102+ entries in ` transport/sc/websocket.py ` , silently dropping excess frames when a
103+ single TCP segment delivers many WebSocket frames.
104+ - ** S5: SC address resolution URI cap** : ` AddressResolutionAckPayload.decode() `
105+ now truncates the URI list to 16 entries, preventing unbounded allocations
106+ from malformed address resolution responses.
107+ - ** B1: FDT TTL upper bound** : Foreign device registration TTL is now capped at
108+ 3600 seconds (1 hour) in ` transport/bbmd.py ` , preventing unreasonably long
109+ registration durations.
110+ - ** A1: Change callback cap** : ` ObjectDatabase.register_change_callback() ` now
111+ raises ` ValueError ` when a single property exceeds 100 registered callbacks,
112+ preventing unbounded list growth.
113+ - ** COV nesting depth enforcement** : Added depth check (max 32) to the manual
114+ tag-nesting loop in ` COVPropertyValue.decode() ` (` services/cov.py ` ),
115+ preventing stack exhaustion from deeply nested opening tags in COV value
116+ decode paths.
117+ - ** ` decode_boolean ` buffer validation** : Added explicit length check before
118+ accessing ` data[0] ` in ` decode_boolean() ` (` encoding/primitives.py ` ),
119+ raising ` ValueError ` on empty input for consistency with ` decode_real ` and
120+ ` decode_double ` .
9121
10122## [ 1.5.0] - 2026-02-15
11123
0 commit comments