feat(udp-notif): improve codec with configurable reassembly limits, align to draft, and add OTel metrics - #18
Conversation
d116f2e to
80eebcf
Compare
There was a problem hiding this comment.
Pull request overview
This PR hardens the UDP-Notif packet codec’s reassembly logic, aligns datagram-length validation with the draft (each UDP datagram is self-contained), and propagates new reassembly configuration + OpenTelemetry metrics through the udp-notif service stack.
Changes:
- Adds configurable reassembly limits (max segments per message, reassembly timeout) and wires them from collector config → supervisor → actor → per-peer codec.
- Updates codec behavior for protocol correctness (exact message-length match; options taken from first segment) and adds unit tests.
- Adds OTel instruments for reassembly events (timeouts, duplicates, max-segments exceeded) and an incomplete-messages gauge.
Reviewed changes
Copilot reviewed 11 out of 16 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| crates/udp-notif-service/src/supervisor.rs | Adds reassembly settings to supervisor config defaults and passes them into actor construction. |
| crates/udp-notif-service/src/actor.rs | Plumbs reassembly config into per-peer codecs and records new OTel reassembly metrics/gauge. |
| crates/udp-notif-service/examples/udp-notif-print.rs | Switches tracing filtering to RUST_LOG via EnvFilter. |
| crates/udp-notif-service/examples/udp-notif-print-decoded.rs | New example that decodes and prints fully-decoded UDP-Notif payloads with reassembly event logging. |
| crates/udp-notif-service/examples/udp-notif-actors.rs | Switches tracing filtering to RUST_LOG via EnvFilter. |
| crates/udp-notif-service/examples/udp-notif-actor.rs | Updates actor example to pass reassembly defaults into ActorHandle::new. |
| crates/udp-notif-service/examples/udp-notif-actor-decoded.rs | New actor example that decodes payloads to JSON and includes periodic peer purging. |
| crates/udp-notif-pkt/src/wire/test/pcap_tests.rs | Updates error text to reflect codec-level errors. |
| crates/udp-notif-pkt/src/codec.rs | Implements configurable reassembly limits, timeout eviction logic, strict datagram length checks, and adds unit tests + telemetry drain helper. |
| crates/pcap-decoder/src/handlers/udp_notif.rs | Updates test to treat truncated datagrams as malformed (no cross-datagram accumulation). |
| crates/collector/src/config.rs | Adds YAML-configurable defaults for reassembly max segments and timeout. |
| assets/pcaps/udp-notif/huawei/huawei-vrp-r25c10-udp-notif.json | Adds updated decoded output fixture data for UDP-Notif pcap assets. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
2167fae to
683c133
Compare
7224908 to
d97b6f7
Compare
d97b6f7 to
19c1834
Compare
19c1834 to
aac79f8
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 16 changed files in this pull request and generated 1 comment.
Suppressed comments (2)
crates/udp-notif-service/examples/udp-notif-print-decoded.rs:2
- The file header contains a duplicated copyright line.
// Copyright (C) 2026-present The NetCalyx Authors.
// Copyright (C) 2026-present The NetCalyx Authors.
crates/udp-notif-pkt/src/codec.rs:324
UdpPacketCodec::newacceptsmax_segments: u16but does not guard against0. Withmax_segments == 0, the current cap check effectively allows the first segment (because the buffer doesn't exist yet) and then fails on the second segment, which is surprising and makes the limit behave like1.
Clamping to a minimum of 1 at construction keeps the semantics consistent and avoids odd edge cases from config parsing.
pub fn new(max_segments: u16, reassembly_timeout: Duration) -> Self {
Self {
incomplete_messages: HashMap::new(),
max_segments,
reassembly_timeout,
reassembly_events: ReassemblyEventCounts::default(),
}
de73a05 to
b01705d
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 16 changed files in this pull request and generated no new comments.
Suppressed comments (4)
crates/udp-notif-service/src/actor.rs:906
- Reassembly-timeout eviction in
UdpPacketCodecruns insidedecode(). For peers that go silent mid-reassembly,reassembly_timeout(default 10s) won’t be enforced (andtimeout_evictionswon’t be emitted) until the next datagram from that peer or until the much-larger idle peer purge runs. If strict timeout enforcement/telemetry is required, consider adding a periodic sweep that drives eviction at ~reassembly_timeoutgranularity.
// Purges peers idle for longer than AUTO_PEER_PURGE_INTERVAL
let mut peer_purge_interval = tokio::time::interval(AUTO_PEER_PURGE_INTERVAL);
peer_purge_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
crates/udp-notif-service/examples/udp-notif-print-decoded.rs:2
- The new example has the copyright header line duplicated, which is likely unintentional noise in generated license headers.
// Copyright (C) 2026-present The NetCalyx Authors.
// Copyright (C) 2026-present The NetCalyx Authors.
crates/udp-notif-service/src/actor.rs:139
AUTO_PEER_PURGE_INTERVALis used as both the timer tick interval and the inactivity threshold. Because the purge only runs periodically and usessince > dur, peers may remain around for up to almost 2× this interval; the current doc comment reads like a hard maximum.
This issue also appears on line 903 of the same file.
/// How long a peer can go completely silent before its per-peer
/// [`UdpPacketCodec`] (and any reassembly buffer still pending in it) is
/// dropped automatically, independent of the external
/// [`ActorCommand::PurgeUnusedPeers`] command.
const AUTO_PEER_PURGE_INTERVAL: Duration = Duration::from_secs(600);
crates/udp-notif-service/src/actor.rs:826
- When purging an idle peer, the per-peer
reassembly_incomplete_messagesgauge is never reset. If the peer had pending buffers, the last recorded non-zero gauge value will linger even though the codec was dropped.
for peer in &cleared_peers {
if let Some(codec) = self.clients.remove(peer) {
let incomplete_count = codec.incomplete_messages_count();
if incomplete_count > 0 {
debug!(
decode() only evicted timed-out reassembly buffers when the same peer sent more data, so a peer that stopped sending mid-reassembly left its per-peer codec (and any pending buffer) in memory forever. - actor.rs: run() now calls purge_unused_peers automatically every 10 minutes, independent of inbound traffic or external callers. purge_unused_peers is now the single place peers/codecs are dropped, and logs any pending buffer discarded with them.
b01705d to
1b25eac
Compare
Changelog
Reassembly hardening
max_segments, default 64)reassembly_timeout, default 10 s) — eviction runs insidedecode()ReassemblyEventCountsstruct +take_reassembly_events()helper for draining counters to telemetryProtocol correctness
check_lennow enforcesbuf.len() == message_length(exact match, per draft §3.2 — each datagram is self-contained)in_message: boolstream-codec state (not relevant for UDP)Observability
Four new OTel instruments on the actor:
reassembly.timeout_evictions,reassembly.duplicate_drops,reassembly.max_segments_exceeded(counters),reassembly.incomplete_messages(gauge).Config propagation
reassembly_max_segments/reassembly_timeoutwired fromUdpNotifConfig→SupervisorConfig→ActorHandle→ per-peerUdpPacketCodec::new(…). Both fields are optional in YAML with defaults from the draft suggestions.Tests
7 new unit tests in
codec.rs. Replaced the incorrect stream-accumulation test inpcap-decoderwith a datagram-truncation test that matches the new behaviour.