From c7fa3d4e8f97cea21d1b3d18c7b9989298e8a2f6 Mon Sep 17 00:00:00 2001 From: Leo Parente <23251360+leoparente@users.noreply.github.com> Date: Mon, 18 May 2026 15:33:36 -0300 Subject: [PATCH 1/3] test(flow): cover NetFlow v1/v7 and sFlow IPv6 parse paths Adds three minimal hand-rolled pcap fixtures (nf1.pcap, nf7.pcap, sflow_ipv6.pcap) and accompanying test cases that exercise dispatch arms in NetflowData.h (versions 1 and 7) and the IPv6 sampled-header path in SflowData.h. Existing flow tests only cover v5/v9/IPFIX and sFlow with IPv4 payloads. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/handlers/flow/test_flows.cpp | 83 +++++++++++++++++++++++++++++ src/tests/fixtures/nf1.pcap | Bin 0 -> 194 bytes src/tests/fixtures/nf7.pcap | Bin 0 -> 210 bytes src/tests/fixtures/sflow_ipv6.pcap | Bin 0 -> 222 bytes 4 files changed, 83 insertions(+) create mode 100644 src/tests/fixtures/nf1.pcap create mode 100644 src/tests/fixtures/nf7.pcap create mode 100644 src/tests/fixtures/sflow_ipv6.pcap diff --git a/src/handlers/flow/test_flows.cpp b/src/handlers/flow/test_flows.cpp index 675b55570..0b46ed509 100644 --- a/src/handlers/flow/test_flows.cpp +++ b/src/handlers/flow/test_flows.cpp @@ -661,3 +661,86 @@ TEST_CASE("Flow specialized_merge + to_prometheus + to_opentelemetry with all gr target->to_opentelemetry(scope, start_ts, end_ts, {}); CHECK(otel_gauge_value(scope, "flow_records_flows") == pre_b1 + pre_b2); } + +// The fixtures below were hand-rolled to exercise parser branches the +// pre-existing nf5/nf9/ipfix/ecmp captures don't touch — NetflowData.h's +// v1 and v7 dispatch arms and SflowData.h's IPv6 sample-header path. + +TEST_CASE("Parse netflow v1 stream", "[netflow][flow]") +{ + FlowInputStream stream{"netflow-v1-test"}; + stream.config_set("flow_type", "netflow"); + stream.config_set("pcap_file", "tests/fixtures/nf1.pcap"); + + visor::Config c; + auto stream_proxy = stream.add_event_proxy(c); + c.config_set("num_periods", 1); + FlowStreamHandler flow_handler{"flow-v1", stream_proxy, &c}; + + flow_handler.start(); + stream.start(); + stream.stop(); + flow_handler.stop(); + + auto event_data = flow_handler.metrics()->bucket(0)->event_data_locked(); + CHECK(event_data.num_events->value() == 1); + CHECK(event_data.num_samples->value() == 1); + + nlohmann::json j; + flow_handler.metrics()->bucket(0)->to_json(j); + // Fixture has 2 v1 records (UDP + TCP) sourced from 10.0.0.1. + CHECK(j["devices"]["10.0.0.1"]["records_flows"] == 2); +} + +TEST_CASE("Parse netflow v7 stream", "[netflow][flow]") +{ + FlowInputStream stream{"netflow-v7-test"}; + stream.config_set("flow_type", "netflow"); + stream.config_set("pcap_file", "tests/fixtures/nf7.pcap"); + + visor::Config c; + auto stream_proxy = stream.add_event_proxy(c); + c.config_set("num_periods", 1); + FlowStreamHandler flow_handler{"flow-v7", stream_proxy, &c}; + + flow_handler.start(); + stream.start(); + stream.stop(); + flow_handler.stop(); + + auto event_data = flow_handler.metrics()->bucket(0)->event_data_locked(); + CHECK(event_data.num_events->value() == 1); + CHECK(event_data.num_samples->value() == 1); + + nlohmann::json j; + flow_handler.metrics()->bucket(0)->to_json(j); + // Fixture has 2 v7 records (UDP + TCP) from agent 10.0.0.1. + CHECK(j["devices"]["10.0.0.1"]["records_flows"] == 2); +} + +TEST_CASE("Parse sflow IPv6 sample", "[sflow][flow]") +{ + FlowInputStream stream{"sflow-ipv6-test"}; + stream.config_set("flow_type", "sflow"); + stream.config_set("pcap_file", "tests/fixtures/sflow_ipv6.pcap"); + + visor::Config c; + auto stream_proxy = stream.add_event_proxy(c); + c.config_set("num_periods", 1); + FlowStreamHandler flow_handler{"flow-sflow-v6", stream_proxy, &c}; + + flow_handler.start(); + stream.start(); + stream.stop(); + flow_handler.stop(); + + auto event_data = flow_handler.metrics()->bucket(0)->event_data_locked(); + // Single sFlow datagram with one flow sample carrying an IPv6 packet. + CHECK(event_data.num_events->value() == 1); + CHECK(event_data.num_samples->value() == 1); + + nlohmann::json j; + flow_handler.metrics()->bucket(0)->to_json(j); + // Agent address is 10.0.0.99 (IPv4 agent address with an IPv6 payload). + CHECK(j["devices"]["10.0.0.99"]["records_flows"] >= 1); +} diff --git a/src/tests/fixtures/nf1.pcap b/src/tests/fixtures/nf1.pcap new file mode 100644 index 0000000000000000000000000000000000000000..352e287edc9388bbaa23363cffbdcc9f8d90c8fa GIT binary patch literal 194 zcmca|c+)~A1{MYw`2U}Qff2~*isH@!iOquIRlCpJea8qUnK&3+85nwmOc)p(1Q{5( zfEs|9$-t6>ouLA#oRNXaCUUhv7I#z^$bq3UIiOGq1H&&M#r%SSf&Bu| g?f?b`HUc#`7JpB literal 0 HcmV?d00001 diff --git a/src/tests/fixtures/sflow_ipv6.pcap b/src/tests/fixtures/sflow_ipv6.pcap new file mode 100644 index 0000000000000000000000000000000000000000..81134a2be97f7b88f1e2bc9a5167ea84c20c716d GIT binary patch literal 222 zcmca|c+)~A1{MYw`2U}Qff2~*isH@!iEV@8RlCpJea8qUnK&3+85mXwnJ_Ro2r@8m z0W|kYL~tbWmXA-2q}k0V5h8=oSW31`Y-W0Pxcqf&c&j literal 0 HcmV?d00001 From 853f87bee27a96c905df5a87358136f07a52391b Mon Sep 17 00:00:00 2001 From: Leo Parente <23251360+leoparente@users.noreply.github.com> Date: Mon, 18 May 2026 16:05:30 -0300 Subject: [PATCH 2/3] test(flow): cover sflow SFLFLOW_IPV4 / SFLFLOW_IPV6 element decoders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds sflow_keyed.pcap, a single sFlow v5 datagram with two flow_samples whose elements use tag=3 (SFLFLOW_IPV4) and tag=4 (SFLFLOW_IPV6) — the key-only sampled-flow formats that exercise readFlowSample_IPv4 and readFlowSample_IPv6 in SflowData.h. Previous sflow fixtures only carried SFLFLOW_HEADER (tag=1) elements, so those two decoders were uncovered. Asserts that the resulting per-interface metrics reflect both elements: one IPv4 TCP flow (1500 bytes) and one IPv6 UDP flow (1280 bytes). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/handlers/flow/test_flows.cpp | 37 ++++++++++++++++++++++++++++ src/tests/fixtures/sflow_keyed.pcap | Bin 0 -> 294 bytes 2 files changed, 37 insertions(+) create mode 100644 src/tests/fixtures/sflow_keyed.pcap diff --git a/src/handlers/flow/test_flows.cpp b/src/handlers/flow/test_flows.cpp index 0b46ed509..6b4a4b747 100644 --- a/src/handlers/flow/test_flows.cpp +++ b/src/handlers/flow/test_flows.cpp @@ -744,3 +744,40 @@ TEST_CASE("Parse sflow IPv6 sample", "[sflow][flow]") // Agent address is 10.0.0.99 (IPv4 agent address with an IPv6 payload). CHECK(j["devices"]["10.0.0.99"]["records_flows"] >= 1); } + +TEST_CASE("Parse sflow IPv4/IPv6 keyed sample elements", "[sflow][flow]") +{ + // Two flow_samples in the datagram, each carrying a non-header element: + // SFLFLOW_IPV4 (tag=3) → readFlowSample_IPv4 in SflowData.h + // SFLFLOW_IPV6 (tag=4) → readFlowSample_IPv6 in SflowData.h + // Existing sflow fixtures only emit SFLFLOW_HEADER (tag=1). + FlowInputStream stream{"sflow-keyed-test"}; + stream.config_set("flow_type", "sflow"); + stream.config_set("pcap_file", "tests/fixtures/sflow_keyed.pcap"); + + visor::Config c; + auto stream_proxy = stream.add_event_proxy(c); + c.config_set("num_periods", 1); + FlowStreamHandler flow_handler{"flow-sflow-keyed", stream_proxy, &c}; + + flow_handler.start(); + stream.start(); + stream.stop(); + flow_handler.stop(); + + auto event_data = flow_handler.metrics()->bucket(0)->event_data_locked(); + CHECK(event_data.num_events->value() == 1); + CHECK(event_data.num_samples->value() == 1); + + nlohmann::json j; + flow_handler.metrics()->bucket(0)->to_json(j); + auto &dev = j["devices"]["10.0.0.77"]; + CHECK(dev["records_flows"] == 2); + // The IPv4 element: src 192.0.2.10 → dst 192.0.2.20, TCP, length 1500. + // The IPv6 element: src 2001:db8::1 → dst 2001:db8::2, UDP, length 1280. + auto &iface = dev["interfaces"]["1"]; + CHECK(iface["in_ipv4_packets"] == 1); + CHECK(iface["in_ipv6_packets"] == 1); + CHECK(iface["in_tcp_bytes"] == 1500); + CHECK(iface["in_udp_bytes"] == 1280); +} diff --git a/src/tests/fixtures/sflow_keyed.pcap b/src/tests/fixtures/sflow_keyed.pcap new file mode 100644 index 0000000000000000000000000000000000000000..ce7b02561abbb42d0c7d44628c473947e1c6b5c3 GIT binary patch literal 294 zcmca|c+)~A1{MYw`2U}Qff2~5P2$c1iT#7(RlCpJea8qUnK&3+85lkYnJ_Ro2r@8m z0W| Date: Mon, 18 May 2026 16:16:56 -0300 Subject: [PATCH 3/3] test(flow): assert IPv6-specific counters in sflow IPv6 test Codex pointed out that the IPv6 sflow test asserted only records_flows >= 1, which would pass even if decodeIPV6 regressed. Investigating uncovered a real bug in the original fixture: it set SFLHEADER_IPv6 to 11 (which is SFLHEADER_IPv4), so the parser took the IPv4 branch and decodeIPV6 never ran. Regenerates sflow_ipv6.pcap with header_protocol=12 and asserts the IPv6/UDP per-interface counters land in their IPv6 buckets. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/handlers/flow/test_flows.cpp | 9 +++++++-- src/tests/fixtures/sflow_ipv6.pcap | Bin 222 -> 222 bytes 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/handlers/flow/test_flows.cpp b/src/handlers/flow/test_flows.cpp index 6b4a4b747..e2d6d3b47 100644 --- a/src/handlers/flow/test_flows.cpp +++ b/src/handlers/flow/test_flows.cpp @@ -741,8 +741,13 @@ TEST_CASE("Parse sflow IPv6 sample", "[sflow][flow]") nlohmann::json j; flow_handler.metrics()->bucket(0)->to_json(j); - // Agent address is 10.0.0.99 (IPv4 agent address with an IPv6 payload). - CHECK(j["devices"]["10.0.0.99"]["records_flows"] >= 1); + auto &dev = j["devices"]["10.0.0.99"]; + CHECK(dev["records_flows"] == 1); + // Embedded payload is IPv6/UDP; assert IPv6-specific counters to + // prove decodeIPV6 actually ran (a bare records_flows check would + // still pass if the protocol classification regressed). + CHECK(dev["interfaces"]["1"]["in_ipv6_packets"] == 1); + CHECK(dev["interfaces"]["1"]["in_udp_packets"] == 1); } TEST_CASE("Parse sflow IPv4/IPv6 keyed sample elements", "[sflow][flow]") diff --git a/src/tests/fixtures/sflow_ipv6.pcap b/src/tests/fixtures/sflow_ipv6.pcap index 81134a2be97f7b88f1e2bc9a5167ea84c20c716d..12e9f8640afbb82de3977a398043d2c8230c8c66 100644 GIT binary patch delta 24 gcmcb|c#m;{1XEq=L@8h9mJ)-BS>}v96BpS40BQaR9RL6T delta 24 gcmcb|c#m;{1XEYkL@8h9$khT9v&<{9