diff --git a/.mk/bc.mk b/.mk/bc.mk index 431a34560..6973b2324 100644 --- a/.mk/bc.mk +++ b/.mk/bc.mk @@ -45,7 +45,8 @@ define MAPS "ipsec_ingress_map":"hash", "ipsec_egress_map":"hash", "ssl_data_event_map":"ringbuf", - "dns_name_map":"per_cpu_array" + "dns_name_map":"per_cpu_array", + "quic_flows":"per_cpu_hash" } endef diff --git a/Makefile b/Makefile index 768267c5d..e46ce4e58 100644 --- a/Makefile +++ b/Makefile @@ -46,6 +46,12 @@ PROTOC_ARTIFACTS := pkg/pbflow # regular expressions for excluded file patterns EXCLUDE_COVERAGE_FILES="(/cmd/)|(bpf_bpfe)|(/examples/)|(/pkg/pbflow/)" +# Container image for running linux tests from non-linux hosts (e.g. macOS). +# Prefer matching the *host* Go toolchain version (go env GOVERSION), which tends to +# be more reliable than GO_VERSION (used for generator tooling) and avoids "go: not found". +HOST_GO_VERSION := $(shell go env GOVERSION 2>/dev/null | sed 's/^go//') +TEST_CONTAINER_IMAGE ?= $(if $(HOST_GO_VERSION),golang:$(HOST_GO_VERSION),golang:$(GO_VERSION)) + .DEFAULT_GOAL := help # build a single arch target provided as argument @@ -162,7 +168,29 @@ compile: ## Compile ebpf agent project .PHONY: test test: ## Test code using go test @echo "### Testing code" - GOOS=$(GOOS) go test -mod vendor ./pkg/... ./cmd/... -coverpkg=./... -coverprofile cover.all.out + @if [ "$$(go env GOOS)" = "linux" ]; then \ + go test -mod vendor ./pkg/... ./cmd/... -coverpkg=./... -coverprofile cover.all.out; \ + else \ + $(MAKE) test-container; \ + fi + +.PHONY: test-container +test-container: ## Run linux tests in a container (useful on macOS) + @echo "### Testing in linux container ($(TEST_CONTAINER_IMAGE))" + @if [ -z "$(OCI_BIN_PATH)" ]; then \ + echo "ERROR: docker/podman not found in PATH. Install one, or run 'make test-unit' instead."; \ + exit 1; \ + fi + @$(OCI_BIN) run --rm \ + -u $$(id -u):$$(id -g) \ + -v "$$(pwd)":/src \ + -w /src \ + -e HOME=/tmp \ + -e GOPATH=/tmp/go \ + -e GOCACHE=/tmp/go-build \ + -e CGO_ENABLED=0 \ + $(TEST_CONTAINER_IMAGE) \ + sh -lc 'mkdir -p "$$GOPATH" "$$GOCACHE"; export PATH="/usr/local/go/bin:/go/bin:$$PATH"; command -v go >/dev/null || (echo "ERROR: go not found; PATH=$$PATH" && exit 127); go version && go test -mod vendor ./pkg/... ./cmd/... -coverpkg=./... -coverprofile cover.all.out && go test -v ./pkg/maps' .PHONY: verify-maps verify-maps: ## Verify map names consistency across all sources @@ -172,7 +200,29 @@ verify-maps: ## Verify map names consistency across all sources .PHONY: test-race test-race: ## Test code using go test -race @echo "### Testing code for race conditions" - GOOS=$(GOOS) go test -race -mod vendor ./pkg/... ./cmd/... + @if [ "$$(go env GOOS)" = "linux" ]; then \ + go test -race -mod vendor ./pkg/... ./cmd/...; \ + else \ + $(MAKE) test-race-container; \ + fi + +.PHONY: test-race-container +test-race-container: ## Run go test -race in a linux container (useful on macOS) + @echo "### Testing code for race conditions in linux container ($(TEST_CONTAINER_IMAGE))" + @if [ -z "$(OCI_BIN_PATH)" ]; then \ + echo "ERROR: docker/podman not found in PATH."; \ + exit 1; \ + fi + @$(OCI_BIN) run --rm \ + -u $$(id -u):$$(id -g) \ + -v "$$(pwd)":/src \ + -w /src \ + -e HOME=/tmp \ + -e GOPATH=/tmp/go \ + -e GOCACHE=/tmp/go-build \ + -e CGO_ENABLED=1 \ + $(TEST_CONTAINER_IMAGE) \ + sh -lc 'mkdir -p "$$GOPATH" "$$GOCACHE"; export PATH="/usr/local/go/bin:/go/bin:$$PATH"; command -v go >/dev/null || (echo "ERROR: go not found; PATH=$$PATH" && exit 127); go version && go test -race -mod vendor ./pkg/... ./cmd/...' .PHONY: cov-exclude-generated cov-exclude-generated: diff --git a/bpf/configs.h b/bpf/configs.h index 98533ddf4..be786a346 100644 --- a/bpf/configs.h +++ b/bpf/configs.h @@ -16,4 +16,5 @@ volatile const u8 network_events_monitoring_groupid = 0; volatile const u8 enable_pkt_translation_tracking = 0; volatile const u8 enable_ipsec = 0; volatile const u8 enable_openssl_tracking = 0; +volatile const u8 enable_quic_tracking = 0; #endif //__CONFIGS_H__ diff --git a/bpf/flows.c b/bpf/flows.c index 6eb08135a..a884c4a1a 100644 --- a/bpf/flows.c +++ b/bpf/flows.c @@ -62,6 +62,11 @@ */ #include "openssl_tracker.h" +/* + * Defines quic tracker + */ +#include "quic_tracker.h" + // return 0 on success, 1 if capacity reached static __always_inline int add_observed_intf(flow_metrics *value, pkt_info *pkt, u32 if_index, u8 direction) { @@ -186,6 +191,10 @@ static inline int flow_monitor(struct __sk_buff *skb, u8 direction) { if (enable_dns_tracking) { dns_errno = track_dns_packet(skb, &pkt); } + if (enable_quic_tracking) { + track_quic_packet(skb, &pkt, eth_protocol, direction, len); + } + flow_metrics *aggregate_flow = (flow_metrics *)bpf_map_lookup_elem(&aggregated_flows, &id); if (aggregate_flow != NULL) { update_existing_flow(aggregate_flow, &pkt, len, flow_sampling, skb->ifindex, direction); diff --git a/bpf/maps_definition.h b/bpf/maps_definition.h index b4c9522fe..b46fbbce4 100644 --- a/bpf/maps_definition.h +++ b/bpf/maps_definition.h @@ -152,5 +152,14 @@ struct { __uint(max_entries, 1 << 27); // 16KB * 1000 events/sec * 5sec "eviction time" = ~128MB __uint(pinning, LIBBPF_PIN_BY_NAME); } ssl_data_event_map SEC(".maps"); +// QUIC flow tracking map - keyed by flow_id (like other flow maps) +struct { + __uint(type, BPF_MAP_TYPE_PERCPU_HASH); + __type(key, flow_id); + __type(value, quic_metrics); + __uint(max_entries, 1 << 16); + __uint(map_flags, BPF_F_NO_PREALLOC); + __uint(pinning, LIBBPF_PIN_BY_NAME); +} quic_flows SEC(".maps"); #endif //__MAPS_DEFINITION_H__ diff --git a/bpf/quic_tracker.h b/bpf/quic_tracker.h new file mode 100644 index 000000000..ca0c2bec5 --- /dev/null +++ b/bpf/quic_tracker.h @@ -0,0 +1,273 @@ +/* + * QUIC Flow Tracker - Kernel-observed metrics using QUIC Invariants (RFC 8999) + * Works with unmodified QUIC implementations (quiche, etc.) + * + * UDP Packet with QUIC: + * +------------------+------------------+------------------+------------------+ + * | Ethernet | IP Header | UDP Header | QUIC Payload | + * | 14 bytes | 20/40 bytes | 8 bytes | | + * +------------------+------------------+------------------+------------------+ + * + * UDP Header (8 bytes): + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Source Port | Destination Port | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Length | Checksum | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * + * QUIC Long Header (handshake): + * +-+-+-+-+-+-+-+-+ + * |1|1| Type |Res| First byte: Form=1 (long), Fixed=1, Type (2 bits) + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Version (32) | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | DCID Len (8) | Destination Connection ID | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | SCID Len (8) | Source Connection ID | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | [Type-Specific Fields...] | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * + * QUIC Short Header (post-handshake): + * +-+-+-+-+-+-+-+-+ + * |0|1|S|R|R|K|P P| First byte: Form=0 (short), Fixed=1 + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Destination Connection ID | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | [Encrypted Payload...] | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ + +#ifndef __QUIC_TRACKER_H__ +#define __QUIC_TRACKER_H__ + +#include "utils.h" +#include "maps_definition.h" + +#define QUIC_PORT 443 // 443 is the default port for QUIC (RFC 9312) +#define QUIC_LONG_HEADER 0x80 // 0x80 is the fixed bit for long header (RFC 9000) +#define QUIC_FIXED_BIT 0x40 // 0x40 is the fixed bit for short header (RFC 9000) +#define QUIC_MIN_PACKET_SIZE \ + 21 //21 bytes ==> first byte (1) + version (4) + DCID Len (1) + 15 bytes of DCID room +#define QUIC_MAX_CID_LEN 20 // 20 bytes is the maximum length of a QUIC connection ID (RFC 9000) + +/* + * QUIC long-header (RFC 8999 invariants) byte offsets, relative to the start of + * the QUIC header (i.e. the first QUIC byte at `offset` in the UDP payload). + * + * Long Header format begins with: + * [0] First byte + * [1..4] Version (32-bit) + * [5] DCID Len + * [6..] DCID bytes... + * [6+dcid_len] SCID Len + * [... ] SCID bytes... + */ +#define QUIC_LH_VERSION_OFFSET 1 +#define QUIC_LH_DCID_LEN_OFFSET (QUIC_LH_VERSION_OFFSET + 4) +#define QUIC_LH_DCID_OFFSET (QUIC_LH_DCID_LEN_OFFSET + 1) +#define QUIC_LH_SCID_LEN_OFFSET(dcid_len) (QUIC_LH_DCID_OFFSET + (dcid_len)) +#define QUIC_LH_MIN_LEN (1 + 4 + 1 + 1) /* first byte + version + DCID len + SCID len */ + +typedef enum quic_header_type_t { + QUIC_HEADER_TYPE_NOT_QUIC = 0, + QUIC_HEADER_TYPE_SHORT = 1, + QUIC_HEADER_TYPE_LONG = 2, +} quic_header_type; + +// Parse QUIC header using QUIC invariants. +// Returns: QUIC_HEADER_TYPE_NOT_QUIC, QUIC_HEADER_TYPE_SHORT, QUIC_HEADER_TYPE_LONG. +// If long header, version is set and CID length fields are sanity-checked. +// payload_len is the UDP payload length (bytes) starting at offset. +static __always_inline quic_header_type parse_quic_header(struct __sk_buff *skb, u32 offset, + u32 payload_len, u32 *version) { + u8 first_byte; + if (bpf_skb_load_bytes(skb, offset, &first_byte, 1) < 0) { + if (trace_messages) { + bpf_printk("error loading first byte at offset %d\n", offset); + } + return QUIC_HEADER_TYPE_NOT_QUIC; + } + + // QUIC packets must have fixed bit set + if (!(first_byte & QUIC_FIXED_BIT)) { + if (trace_messages) { + bpf_printk("QUIC packet does not have fixed bit set at offset %d\n", offset); + } + return QUIC_HEADER_TYPE_NOT_QUIC; + } + + if (first_byte & QUIC_LONG_HEADER) { + if (trace_messages) { + bpf_printk("QUIC packet is a long header at offset %d\n", offset); + } + // Need at least: first byte (1) + version (4) + DCID Len (1) + SCID Len (1) + // (CID contents lengths are checked below) + if (payload_len < QUIC_LH_MIN_LEN) { + if (trace_messages) { + bpf_printk("QUIC packet payload length %d is less than minimum length %d\n", + payload_len, QUIC_LH_MIN_LEN); + } + return QUIC_HEADER_TYPE_NOT_QUIC; + } + + // Long header: read version (bytes 1-4) + u32 ver; + if (bpf_skb_load_bytes(skb, offset + QUIC_LH_VERSION_OFFSET, &ver, 4) < 0) { + if (trace_messages) { + bpf_printk("error loading version at offset %d\n", offset + QUIC_LH_VERSION_OFFSET); + } + return QUIC_HEADER_TYPE_NOT_QUIC; + } + *version = bpf_ntohl(ver); + + // Sanity-check DCID/SCID length fields (QUIC invariants / RFC 8999) + u8 dcid_len = 0; + if (bpf_skb_load_bytes(skb, offset + QUIC_LH_DCID_LEN_OFFSET, &dcid_len, 1) < 0) { + if (trace_messages) { + bpf_printk("error loading DCID length at offset %d\n", + offset + QUIC_LH_DCID_LEN_OFFSET); + } + return QUIC_HEADER_TYPE_NOT_QUIC; + } + if (dcid_len > QUIC_MAX_CID_LEN) { + if (trace_messages) { + bpf_printk("DCID length %d is greater than maximum length %d\n", dcid_len, + QUIC_MAX_CID_LEN); + } + return QUIC_HEADER_TYPE_NOT_QUIC; + } + if (payload_len < (u32)(QUIC_LH_SCID_LEN_OFFSET(dcid_len) + 1)) { + if (trace_messages) { + bpf_printk("QUIC packet payload length %d is less than minimum length %d\n", + payload_len, QUIC_LH_SCID_LEN_OFFSET(dcid_len) + 1); + } + return QUIC_HEADER_TYPE_NOT_QUIC; + } + + u8 scid_len = 0; + if (bpf_skb_load_bytes(skb, offset + QUIC_LH_SCID_LEN_OFFSET(dcid_len), &scid_len, 1) < 0) { + if (trace_messages) { + bpf_printk("error loading SCID length at offset %d\n", + offset + QUIC_LH_SCID_LEN_OFFSET(dcid_len)); + } + return QUIC_HEADER_TYPE_NOT_QUIC; + } + if (scid_len > QUIC_MAX_CID_LEN) { + if (trace_messages) { + bpf_printk("SCID length %d is greater than maximum length %d\n", scid_len, + QUIC_MAX_CID_LEN); + } + return QUIC_HEADER_TYPE_NOT_QUIC; + } + if (payload_len < (u32)(QUIC_LH_MIN_LEN + dcid_len + scid_len)) { + if (trace_messages) { + bpf_printk("QUIC packet payload length %d is less than minimum length %d\n", + payload_len, QUIC_LH_MIN_LEN + dcid_len + scid_len); + } + return QUIC_HEADER_TYPE_NOT_QUIC; + } + + return QUIC_HEADER_TYPE_LONG; + } + + if (trace_messages) { + bpf_printk("QUIC packet is a short header at offset %d\n", offset); + } + return QUIC_HEADER_TYPE_SHORT; +} + +static __always_inline int track_quic_packet(struct __sk_buff *skb, pkt_info *pkt, u16 eth_protocol, + u8 direction, u32 len) { + if (pkt->id->transport_protocol != IPPROTO_UDP) { + if (trace_messages) { + bpf_printk("QUIC packet is not UDP\n"); + } + return 0; + } + + // Mode selection via enable_quic_tracking: + // QUIC_CONFIG_DISABLED = 0, QUIC_CONFIG_ENABLED = 1, QUIC_CONFIG_ANY_UDP_PORT = 2. + if (enable_quic_tracking == (u8)QUIC_CONFIG_ENABLED) { + if (pkt->id->dst_port != QUIC_PORT && pkt->id->src_port != QUIC_PORT) { + if (trace_messages) { + bpf_printk("QUIC packet is not on port %d\n", QUIC_PORT); + } + return 0; + } + } + + struct udphdr *udp = (struct udphdr *)pkt->l4_hdr; + if (!udp) { + if (trace_messages) { + bpf_printk("UDP header not found\n"); + } + return 0; + } + u16 udp_len = bpf_ntohs(udp->len); + if (udp_len < sizeof(struct udphdr) + QUIC_MIN_PACKET_SIZE) { + if (trace_messages) { + bpf_printk("UDP packet length %d is less than minimum length %d\n", udp_len, + sizeof(struct udphdr) + QUIC_MIN_PACKET_SIZE); + } + return 0; + } + u32 quic_offset = (u32)((long)udp - (long)skb->data) + sizeof(struct udphdr); + u32 quic_payload_len = (u32)udp_len - sizeof(struct udphdr); + + // Parse QUIC header + u32 version = 0; + quic_header_type hdr_type = parse_quic_header(skb, quic_offset, quic_payload_len, &version); + if (hdr_type == QUIC_HEADER_TYPE_NOT_QUIC) { + if (trace_messages) { + bpf_printk("QUIC packet is not QUIC at offset %d\n", quic_offset); + } + return 0; + } + + u64 now = pkt->current_ts; + flow_id *id = pkt->id; + + // Lookup or create QUIC metrics + quic_metrics *flow = bpf_map_lookup_elem(&quic_flows, id); + if (flow) { + flow->end_mono_time_ts = now; + flow->packets++; + flow->bytes += len; + if (hdr_type == QUIC_HEADER_TYPE_LONG) { + flow->seen_long_hdr = 1; + if (version != 0) + flow->version = version; + } else { + flow->seen_short_hdr = 1; + } + } else { + quic_metrics new_flow = { + .start_mono_time_ts = now, + .end_mono_time_ts = now, + .bytes = len, + .packets = 1, + .version = version, + .eth_protocol = eth_protocol, + .seen_long_hdr = (hdr_type == QUIC_HEADER_TYPE_LONG) ? 1 : 0, + .seen_short_hdr = (hdr_type == QUIC_HEADER_TYPE_SHORT) ? 1 : 0, + }; + long ret = bpf_map_update_elem(&quic_flows, id, &new_flow, BPF_NOEXIST); + if (ret != 0) { + if (trace_messages && ret != -EEXIST) { + bpf_printk("error adding quic flow %d\n", ret); + } + if (ret == -EEXIST) { + quic_metrics *flow = bpf_map_lookup_elem(&quic_flows, id); + if (flow) { + flow->end_mono_time_ts = now; + flow->packets++; + flow->bytes += len; + } + } + } + } + + return 0; +} +#endif /* __QUIC_TRACKER_H__ */ diff --git a/bpf/types.h b/bpf/types.h index 76dacb9ce..e573056ed 100644 --- a/bpf/types.h +++ b/bpf/types.h @@ -316,5 +316,28 @@ struct ssl_data_event_t { // Force emitting enums/structs into the ELF const static struct ssl_data_event_t *unused14 __attribute__((unused)); +// QUIC flow metrics +typedef struct quic_metrics_t { + u64 start_mono_time_ts; + u64 end_mono_time_ts; + u64 bytes; + u32 packets; + u32 version; // QUIC version (from long header), 0 if unknown + u16 eth_protocol; // ETH_P_IP or ETH_P_IPV6 + u8 seen_long_hdr; // Saw handshake packets (long header) + u8 seen_short_hdr; // Saw established packets (short header) +} quic_metrics; + +// Force emitting struct into the ELF +const static struct quic_metrics_t *unused15 __attribute__((unused)); + +typedef enum quic_config_t { + QUIC_CONFIG_DISABLED, + QUIC_CONFIG_ENABLED, + QUIC_CONFIG_ANY_UDP_PORT, +} quic_config; + +// Force emitting enums/structs into the ELF/BTF (for bpf2go -type quic_config_t) +const static enum quic_config_t *unused16 __attribute__((unused, used)); #endif /* __TYPES_H__ */ diff --git a/docs/config.md b/docs/config.md index 9e7afb106..925cc4a23 100644 --- a/docs/config.md +++ b/docs/config.md @@ -80,6 +80,11 @@ The following environment variables are available to configure the NetObserv eBP See [docs](./rtt_calculations.md) for more details on this feature. * `ENABLE_PKT_DROPS` (default: `false` disabled). If `true` enables packet drops eBPF hook to be able to capture drops flows in the ebpf agent. * `ENABLE_DNS_TRACKING` (default: `false` disabled). If `true` enables DNS tracking to calculate DNS latency for the captured flows in the ebpf agent. +* `QUIC_TRACKING_MODE` (default: `0`). + * `0`: disable QUIC tracking + * `1`: enable QUIC tracking, limited to **UDP/443** (lower overhead / fewer false positives) + * `2`: enable QUIC tracking on **any UDP port** (RFC 9312-friendly) +* Deprecated (kept for backwards compatibility): * `ENABLE_PCA` (default: `false` disabled). If `true` enables Packet Capture Agent. * `PCA_FILTER` (default: `none`). Works only when `ENABLE_PCA` is set. Accepted format . Example `PCA_FILTER=tcp,22`. diff --git a/examples/test-quic.sh b/examples/test-quic.sh new file mode 100755 index 000000000..c8cea2f45 --- /dev/null +++ b/examples/test-quic.sh @@ -0,0 +1,714 @@ +#!/usr/bin/env bash +# Quick end-to-end QUIC (HTTP/3) test suite on an existing cluster context. +# +# What it does: +# - deploy an in-cluster HTTP/3 (QUIC) server (Caddy) as a Service with TCP/UDP 443 +# - run HTTP/3 client pods against that Service (multiple cases) +# - verify QUIC tracking via *agent logs*: +# looks for "QUIC flow metrics sample" with quicFlowsLogged>0, plus UDP/443 assertions +# +# Usage: +# ./examples/test-quic.sh +# +# Suite cases (always run with defaults): +# - smoke: QUIC_REQUESTS=2, QUIC_PARALLEL_CLIENTS=1 +# - parallel clients: QUIC_REQUESTS=1, QUIC_PARALLEL_CLIENTS=3 +# - non-443 port: verifies QUIC detection on a non-443 UDP port by temporarily setting QUIC_TRACKING_MODE=2 +set -euo pipefail + +ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." &>/dev/null && pwd)" + +CLIENT_IMAGE="mrchoke/curl-http3" +DEBUG_KEEP_POD="false" +CLIENT_TIMEOUT_SECONDS="15" +RUN_TCP_SANITY="false" +QUIC_REQUESTS="2" +QUIC_PARALLEL_CLIENTS="1" +RUN_NEGATIVE_NO_UDP="true" + +QUIC_SERVER_NAME="quic-server" +QUIC_NAMESPACE="quic" +QUIC_SERVER_IMAGE="caddy:2" +QUIC_ALT_SERVER_NAME="quic-server-alt" +QUIC_ALT_PORT="8443" +NETOBSERV_NAMESPACE="netobserv-privileged" +AGENT_LOG_WAIT_SECONDS="20" + +AGENT_LABEL="k8s-app=netobserv-ebpf-agent" + +log() { printf '%s\n' "$*"; } + +START_TS="" + +# -------------------- +# Small helpers +# -------------------- +is_pos_int() { + [[ "${1:-}" =~ ^[1-9][0-9]*$ ]] +} + +cleanup_quic_resources() { + # Best-effort cleanup. This runs on EXIT/INT/TERM so keep it resilient. + if [[ "${DEBUG_KEEP_POD}" == "true" ]]; then + log "" + log "Note: DEBUG_KEEP_POD=true so QUIC resources were not cleaned up." + return 0 + fi + + log "" + log "==> Cleaning up QUIC test resources" + # The script owns the whole namespace; deleting it is the simplest cleanup. + kubectl delete namespace/"${QUIC_NAMESPACE}" --ignore-not-found=true >/dev/null 2>&1 || true +} + +wait_pod_done() { + local ns="$1" + local pod="$2" + local timeout_seconds="${3:-120}" + local start + start="$(date +%s)" + + while true; do + local phase="" + phase="$(kubectl get pod -n "$ns" "$pod" -o jsonpath='{.status.phase}' 2>/dev/null || true)" + if [[ "$phase" == "Succeeded" || "$phase" == "Failed" ]]; then + return 0 + fi + if (( $(date +%s) - start > timeout_seconds )); then + return 1 + fi + sleep 2 + done +} + +run_client_pod() { + local ns="$1" + local pod="$2" + local image="$3" + + kubectl delete pod -n "${ns}" "${pod}" --ignore-not-found=true >/dev/null 2>&1 || true + shift 3 + # IMPORTANT: Do not assume the client image has a shell. Execute the command directly. + kubectl run -n "${ns}" "${pod}" --restart=Never --image="${image}" --labels="quic-test=true" --command -- "$@" >/dev/null +} + +# -------------------- +# QUIC target helpers (Service DNS + optional PodIP override for reliability) +# -------------------- +get_quic_server_pod_ip() { + local server_name="${1:-${QUIC_SERVER_NAME}}" + kubectl get pod -n "${QUIC_NAMESPACE}" -l app="${server_name}" -o jsonpath='{.items[0].status.podIP}' 2>/dev/null || true +} + +get_quic_service_cluster_ip() { + local server_name="${1:-${QUIC_SERVER_NAME}}" + kubectl get svc -n "${QUIC_NAMESPACE}" "${server_name}" -o jsonpath='{.spec.clusterIP}' 2>/dev/null || true +} + +curl_resolve_args_for_quic_server() { + # For reliability, prefer targeting the server PodIP (bypasses kube-proxy UDP LB quirks) + # while still using the Service DNS name for SNI/Host via curl --resolve. + local host="$1" + local port="${2:-443}" + local server_name="${3:-${QUIC_SERVER_NAME}}" + local pod_ip="" + pod_ip="$(get_quic_server_pod_ip "${server_name}")" + if [[ -n "${pod_ip:-}" ]]; then + printf -- "--resolve\n%s:%s:%s\n" "${host}" "${port}" "${pod_ip}" + fi +} + +# Build an array of curl args (Bash 3 compatible) to route the Service DNS name to the server PodIP. +build_resolve_args() { + local host="$1" + local port="${2:-443}" + local server_name="${3:-${QUIC_SERVER_NAME}}" + local out=() + local arg="" + while IFS= read -r arg; do + [[ -n "${arg:-}" ]] && out+=("${arg}") + done < <(curl_resolve_args_for_quic_server "${host}" "${port}" "${server_name}") + + # Print one arg per line so callers can capture via a while-read loop. + for arg in "${out[@]}"; do + printf '%s\n' "${arg}" + done +} + +repeat_url_args() { + local url="$1" + local count="$2" + local i=1 + while [[ "$i" -le "$count" ]]; do + printf '%s\n' "${url}" + i=$((i+1)) + done +} + +set_quic_service_tcp_only() { + local server_name="${1:-${QUIC_SERVER_NAME}}" + local port="${2:-443}" + log "==> Negative test setup: updating Service to TCP-only (removing UDP/${port})" + # Prefer patching the existing Service (more robust than re-applying a whole manifest). + if ! kubectl patch svc -n "${QUIC_NAMESPACE}" "${server_name}" --type=merge \ + -p "{\"spec\":{\"ports\":[{\"name\":\"https-tcp\",\"port\":${port},\"targetPort\":${port},\"protocol\":\"TCP\"}]}}" >/dev/null 2>&1; then + log "Warning: could not patch Service; falling back to apply" + kubectl apply -f - >/dev/null < Deploying in-cluster QUIC (HTTP/3) server ($QUIC_SERVER_IMAGE) as ${QUIC_NAMESPACE}/${server_name} (port ${port})" + local quic_server_fqdn="${server_name}.${QUIC_NAMESPACE}.svc.cluster.local" + + # Keep the server stable across suite cases: avoid delete/recreate, which can cause + # transient downtime and QUIC client timeouts (especially with parallel clients). + + kubectl apply -f - >/dev/null </dev/null 2>&1; then + log "" + log "Warning: QUIC server deployment did not become Available." + kubectl get pods -n "${QUIC_NAMESPACE}" -l app="${server_name}" -o wide || true + log "" + log "==> Debug: QUIC server logs" + kubectl logs -n "${QUIC_NAMESPACE}" -l app="${server_name}" --tail=200 2>/dev/null || true + log "" + log "==> Debug: QUIC server describe" + kubectl describe pods -n "${QUIC_NAMESPACE}" -l app="${server_name}" 2>/dev/null || true + return 1 + fi + + kubectl get pods -n "${QUIC_NAMESPACE}" -l app="${server_name}" -o wide || true + return 0 +} + +ensure_agent_exists() { + if ! kubectl get ds -n "$NETOBSERV_NAMESPACE" netobserv-ebpf-agent >/dev/null 2>&1; then + log "==> Agent DaemonSet not found; deploying from scripts/agent.yml" + kubectl apply -f "$ROOT_DIR/scripts/agent.yml" >/dev/null + fi + kubectl rollout status -n "$NETOBSERV_NAMESPACE" ds/netobserv-ebpf-agent --timeout=180s >/dev/null +} + +set_agent_quic_tracking_mode() { + # Args: 1|2 + local mode="${1:-1}" + if [[ "${mode}" != "1" && "${mode}" != "2" ]]; then + log "ERROR: set_agent_quic_tracking_mode expects 1|2 (got: ${mode})" + return 2 + fi + + log "==> Updating agent env: QUIC_TRACKING_MODE=${mode}" + # Use set env for portability across DS manifests. + kubectl -n "$NETOBSERV_NAMESPACE" set env ds/netobserv-ebpf-agent \ + QUIC_TRACKING_MODE="${mode}" \ + --overwrite >/dev/null + kubectl rollout status -n "$NETOBSERV_NAMESPACE" ds/netobserv-ebpf-agent --timeout=180s >/dev/null +} + +# -------------------- +# Test cases +# -------------------- +run_suite() { + log "==> Running QUIC test suite (multiple cases)" + + # Negative test runs by default; disable with RUN_NEGATIVE_NO_UDP=false. + if [[ "${RUN_NEGATIVE_NO_UDP}" == "true" ]]; then + log "==> Case: negative (no UDP Service port; expect HTTP/3 failure)" + run_quic_negative_no_udp + fi + + log "==> Case: single client" + run_quic_client 2 1 + check_agent_logs + + log "==> Case: parallel clients" + run_quic_client 1 3 + check_agent_logs + + log "==> Case: non-443 QUIC Service port (requires QUIC_TRACKING_MODE=2)" + run_quic_non_443_port +} + +run_quic_negative_no_udp() { + if [[ "${RUN_NEGATIVE_NO_UDP}" != "true" ]]; then + return 0 + fi + + # Ensure the server exists so we can reliably identify its IP for log assertions. + if ! deploy_incluster_quic_server; then + log "" + log "ERROR: in-cluster QUIC server failed to start; cannot run negative test." + return 3 + fi + + set_quic_service_tcp_only + + local quic_url="https://${QUIC_SERVER_NAME}.${QUIC_NAMESPACE}.svc.cluster.local/" + local pod + pod="quic-client-neg-$(date +%s)" + + local server_pod_ip="" + local service_ip="" + server_pod_ip="$(get_quic_server_pod_ip)" + service_ip="$(get_quic_service_cluster_ip)" + + log "==> Negative test: HTTP/3 should FAIL when Service has no UDP/443" + log " URL: $quic_url" + log " Server pod IP: ${server_pod_ip:-unknown}" + log " Service ClusterIP: ${service_ip:-unknown}" + + START_TS="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + + # Expect failure: no UDP listener in Service and --http3-only forbids fallback. + run_client_pod "${QUIC_NAMESPACE}" "${pod}" "${CLIENT_IMAGE}" \ + curl --http3-only -sS -D- -o /dev/null -I --max-time "${CLIENT_TIMEOUT_SECONDS}" -4 -k "${quic_url}" >/dev/null + + wait_pod_done "${QUIC_NAMESPACE}" "${pod}" 180 || true + client_logs="$(kubectl logs -n "${QUIC_NAMESPACE}" "${pod}" 2>/dev/null || true)" + if [[ -n "${client_logs:-}" ]]; then + log "$client_logs" + fi + + local exit_code="" + exit_code="$(kubectl get pod -n "${QUIC_NAMESPACE}" "${pod}" -o jsonpath='{.status.containerStatuses[0].state.terminated.exitCode}' 2>/dev/null || true)" + if [[ "${exit_code:-}" == "0" ]]; then + log "" + log "FAIL: Negative test expected curl to fail, but it exited 0." + return 1 + fi + + # Assert the agent did not report QUIC samples for this server/service since START_TS. + sleep "$AGENT_LOG_WAIT_SECONDS" + agent_logs_since="$(kubectl logs -n "$NETOBSERV_NAMESPACE" -l "$AGENT_LABEL" --since-time="$START_TS" 2>/dev/null || true)" + if [[ -z "${agent_logs_since:-}" ]]; then + agent_logs_since="$(kubectl logs -n "$NETOBSERV_NAMESPACE" -l "$AGENT_LABEL" --since=5m 2>/dev/null || true)" + fi + + # Best-effort: match QUIC sample lines that mention our server pod IP or service ClusterIP on dst :443 and UDP. + local needle1="" + local needle2="" + needle1="${server_pod_ip:+>${server_pod_ip}:443}" + needle2="${service_ip:+>${service_ip}:443}" + + if echo "$agent_logs_since" | grep -E 'QUIC flow metrics sample' | grep -q 'p=17'; then + if [[ -n "${needle1}" ]] && echo "$agent_logs_since" | grep -E 'QUIC flow metrics sample' | grep -q "${needle1}"; then + log "" + log "FAIL: Agent reported a QUIC sample to the server pod IP during the negative test." + echo "$agent_logs_since" | grep -E 'QUIC flow metrics sample' | tail -n 50 || true + return 1 + fi + if [[ -n "${needle2}" ]] && echo "$agent_logs_since" | grep -E 'QUIC flow metrics sample' | grep -q "${needle2}"; then + log "" + log "FAIL: Agent reported a QUIC sample to the service ClusterIP during the negative test." + echo "$agent_logs_since" | grep -E 'QUIC flow metrics sample' | tail -n 50 || true + return 1 + fi + fi + + if [[ "$DEBUG_KEEP_POD" != "true" ]]; then + kubectl delete pod -n "${QUIC_NAMESPACE}" "${pod}" --ignore-not-found=true >/dev/null 2>&1 || true + else + log "" + log "Note: DEBUG_KEEP_POD=true so the negative-test pod was not deleted: ${QUIC_NAMESPACE}/${pod}" + fi + + log "" + log "SUCCESS: Negative test passed (HTTP/3 failed as expected; no QUIC samples for the target)." + log "" + return 0 +} + +run_quic_non_443_port() { + if ! is_pos_int "${QUIC_ALT_PORT}"; then + log "ERROR: QUIC_ALT_PORT must be a positive integer (got: ${QUIC_ALT_PORT})" + return 2 + fi + if [[ "${QUIC_ALT_PORT}" == "443" ]]; then + log "ERROR: QUIC_ALT_PORT must be != 443 for the non-443 port case." + return 2 + fi + + # Enable any-port detection for this case, then restore to false to keep the default suite behavior. + set_agent_quic_tracking_mode 2 + + run_quic_client "${QUIC_REQUESTS}" "${QUIC_PARALLEL_CLIENTS}" "${QUIC_ALT_SERVER_NAME}" "${QUIC_ALT_PORT}" + check_agent_logs_port "${QUIC_ALT_PORT}" + + set_agent_quic_tracking_mode 1 +} + +run_tcp_sanity() { + local ns="$1" + local pod="$2" + local url="$3" + local resolve_args_file="${4:-}" + + if [[ "${RUN_TCP_SANITY}" != "true" ]]; then + return 0 + fi + + local resolve_args=() + if [[ -n "${resolve_args_file:-}" && -f "${resolve_args_file}" ]]; then + local a="" + while IFS= read -r a; do + [[ -n "${a:-}" ]] && resolve_args+=("${a}") + done < "${resolve_args_file}" + fi + + log "==> Sanity check: HTTPS over TCP to the same URL (no HTTP/3 flags)" + run_client_pod "${ns}" "${pod}" "${CLIENT_IMAGE}" \ + curl -sS -o /dev/null -I \ + -w '__TCP_HEAD_RESULT__ http_version=%{http_version} status=%{response_code}\n' \ + --max-time "${CLIENT_TIMEOUT_SECONDS}" -4 -k \ + "${resolve_args[@]}" \ + "${url}" >/dev/null || true + wait_pod_done "${ns}" "${pod}" 60 || true + kubectl logs -n "${ns}" "${pod}" 2>/dev/null || true + kubectl delete pod -n "${ns}" "${pod}" --ignore-not-found=true >/dev/null 2>&1 || true + log "" +} + +spawn_quic_client_pods() { + # Args: ns pod_base parallel_count head_urls_file get_urls_file resolve_args_file + local ns="$1" + local pod_base="$2" + local parallel="$3" + local head_urls_file="$4" + local get_urls_file="$5" + local resolve_args_file="$6" + + local resolve_args=() + local a="" + while IFS= read -r a; do + [[ -n "${a:-}" ]] && resolve_args+=("${a}") + done < "${resolve_args_file}" + + local head_urls=() + while IFS= read -r a; do + [[ -n "${a:-}" ]] && head_urls+=("${a}") + done < "${head_urls_file}" + + local get_urls=() + while IFS= read -r a; do + [[ -n "${a:-}" ]] && get_urls+=("${a}") + done < "${get_urls_file}" + + local i=1 + while [[ "$i" -le "$parallel" ]]; do + local head_pod="${pod_base}-${i}-head" + local get_pod="${pod_base}-${i}-get" + + run_client_pod "${ns}" "${head_pod}" "${CLIENT_IMAGE}" \ + curl --http3-only -sS -o /dev/null -I \ + -w '__HEAD_RESULT__ http_version=%{http_version} status=%{response_code}\n' \ + --retry 2 --retry-delay 1 --retry-connrefused \ + --max-time "${CLIENT_TIMEOUT_SECONDS}" -4 -k \ + "${resolve_args[@]}" \ + "${head_urls[@]}" \ + >/dev/null + + run_client_pod "${ns}" "${get_pod}" "${CLIENT_IMAGE}" \ + curl --http3-only -sS -o /dev/null \ + -w '__GET_RESULT__ http_version=%{http_version} status=%{response_code} size=%{size_download}\n' \ + --retry 2 --retry-delay 1 --retry-connrefused \ + --max-time "${CLIENT_TIMEOUT_SECONDS}" -4 -k \ + "${resolve_args[@]}" \ + "${get_urls[@]}" \ + >/dev/null + + i=$((i+1)) + done +} + +validate_quic_client_pods() { + local ns="$1" + local pod_base="$2" + local parallel="$3" + + local failures=0 + local i=1 + while [[ "$i" -le "$parallel" ]]; do + local pod="" + for pod in "${pod_base}-${i}-head" "${pod_base}-${i}-get"; do + if ! wait_pod_done "${ns}" "${pod}" 180; then + log "" + log "Warning: QUIC client pod did not complete within the timeout: ${ns}/${pod}" + kubectl describe pod -n "${ns}" "${pod}" || true + fi + + client_logs="$(kubectl logs -n "${ns}" "${pod}" 2>/dev/null || true)" + [[ -n "${client_logs:-}" ]] && log "$client_logs" + + # Fail fast if the image doesn't actually support HTTP/3. + if echo "${client_logs:-}" | grep -q "option --http3-only:.*does not support"; then + log "" + log "ERROR: The client image does not support HTTP/3, so no QUIC traffic was generated." + log "Fix: set CLIENT_IMAGE to an HTTP/3-capable client." + failures=$((failures+1)) + continue + fi + + if [[ "${pod}" == *-head ]]; then + if ! echo "${client_logs:-}" | grep -Eq '__HEAD_RESULT__.*http_version=3(\\.[0-9]+)?[[:space:]].*status=2[0-9]{2}'; then + log "" + log "ERROR: QUIC HEAD did not report http_version=3 and a 2xx status (${ns}/${pod})." + log "==> Debug: HEAD result lines" + echo "${client_logs:-}" | grep -E '__HEAD_RESULT__' | tail -n 10 || true + failures=$((failures+1)) + fi + else + if ! echo "${client_logs:-}" | grep -Eq '__GET_RESULT__.*http_version=3(\\.[0-9]+)?[[:space:]].*status=2[0-9]{2}.*size=([1-9][0-9]*)(\\.[0-9]+)?'; then + log "" + log "ERROR: QUIC GET did not report http_version=3 with a 2xx and non-zero download size (${ns}/${pod})." + log "==> Debug: GET result lines" + echo "${client_logs:-}" | grep -E '__GET_RESULT__' | tail -n 10 || true + failures=$((failures+1)) + fi + fi + + exit_code="$(kubectl get pod -n "${ns}" "${pod}" -o jsonpath='{.status.containerStatuses[0].state.terminated.exitCode}' 2>/dev/null || true)" + if [[ "${exit_code:-}" != "0" ]]; then + local phase="" + local reason="" + local message="" + phase="$(kubectl get pod -n "${ns}" "${pod}" -o jsonpath='{.status.phase}' 2>/dev/null || true)" + reason="$(kubectl get pod -n "${ns}" "${pod}" -o jsonpath='{.status.containerStatuses[0].state.terminated.reason}' 2>/dev/null || true)" + message="$(kubectl get pod -n "${ns}" "${pod}" -o jsonpath='{.status.containerStatuses[0].state.terminated.message}' 2>/dev/null || true)" + log "" + log "==> Debug: QUIC client pod status (phase=${phase:-unknown}, exit_code=${exit_code:-unknown}, reason=${reason:-}, message=${message:-})" + kubectl describe pod -n "${ns}" "${pod}" || true + failures=$((failures+1)) + fi + + if [[ "$DEBUG_KEEP_POD" != "true" ]]; then + kubectl delete pod -n "${ns}" "${pod}" --ignore-not-found=true >/dev/null 2>&1 || true + fi + done + i=$((i+1)) + done + + if (( failures > 0 )); then + log "" + log "ERROR: ${failures} QUIC client pod(s) failed." + log "" + log "==> Debug: QUIC server logs (tail 200)" + kubectl logs -n "${ns}" -l app="${QUIC_SERVER_NAME}" --tail=200 2>/dev/null || true + log "" + return 1 + fi + return 0 +} + +run_quic_client() { + local requests="${1:-${QUIC_REQUESTS}}" + local parallel="${2:-${QUIC_PARALLEL_CLIENTS}}" + local server_name="${3:-${QUIC_SERVER_NAME}}" + local port="${4:-443}" + + if ! deploy_incluster_quic_server "${server_name}" "${port}"; then + log "" + log "ERROR: in-cluster QUIC server failed to start; cannot run QUIC client." + return 3 + fi + + local quic_host="${server_name}.${QUIC_NAMESPACE}.svc.cluster.local" + local quic_url="https://${quic_host}:${port}/" + log "==> Generating QUIC (HTTP/3) traffic: $quic_url" + log " Using client image: $CLIENT_IMAGE" + log " requests=${requests}, parallel_clients=${parallel}" + + if ! is_pos_int "${requests}"; then + log "ERROR: requests must be a positive integer (got: ${requests})" + return 2 + fi + if ! is_pos_int "${parallel}"; then + log "ERROR: parallel_clients must be a positive integer (got: ${parallel})" + return 2 + fi + + # Record a timestamp so we can query agent logs after the request. + START_TS="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + + local pod_base + pod_base="quic-client-$(date +%s)" + + # Build resolve args and URL repetition as temporary files to keep the call sites simple + # (and avoid passing arrays around in Bash 3). + local tmp_dir + tmp_dir="$(mktemp -d 2>/dev/null || mktemp -d -t quic)" + local resolve_args_file="${tmp_dir}/resolve.args" + local head_urls_file="${tmp_dir}/head.urls" + local get_urls_file="${tmp_dir}/get.urls" + + build_resolve_args "${quic_host}" "${port}" "${server_name}" > "${resolve_args_file}" || true + repeat_url_args "${quic_url}" "${requests}" > "${head_urls_file}" + repeat_url_args "${quic_url}" "${requests}" > "${get_urls_file}" + + run_tcp_sanity "${QUIC_NAMESPACE}" "${pod_base}-tcp" "${quic_url}" "${resolve_args_file}" || true + + spawn_quic_client_pods "${QUIC_NAMESPACE}" "${pod_base}" "${parallel}" "${head_urls_file}" "${get_urls_file}" "${resolve_args_file}" + validate_quic_client_pods "${QUIC_NAMESPACE}" "${pod_base}" "${parallel}" + + rm -rf "${tmp_dir}" >/dev/null 2>&1 || true +} + +check_agent_logs_port() { + local expected_port="${1:-443}" + log "==> Checking agent logs for QUIC flow metrics marker" + log " (Looking for: \"QUIC flow metrics sample\" with quicFlowsLogged>0)" + + # Give the agent time to flush/merge maps (depends on cache timeout). + sleep "$AGENT_LOG_WAIT_SECONDS" + + # Prefer --since-time; fall back to tail if unavailable. + agent_logs_since="$(kubectl logs -n "$NETOBSERV_NAMESPACE" -l "$AGENT_LABEL" --since-time="$START_TS" 2>/dev/null || true)" + if [[ -z "${agent_logs_since:-}" ]]; then + agent_logs_since="$(kubectl logs -n "$NETOBSERV_NAMESPACE" -l "$AGENT_LABEL" --since=5m 2>/dev/null || true)" + fi + + # Match any log line that contains the marker and a non-zero quicFlowsLogged field. + # logrus usually formats as: ... msg="QUIC flow metrics sample" ... quicFlowsLogged=3 ... + if echo "$agent_logs_since" | grep -E 'QUIC flow metrics sample' | grep -Eq 'quicFlowsLogged=([1-9][0-9]*)'; then + log "" + log "SUCCESS: Agent reports QUIC flows with metrics:" + echo "$agent_logs_since" | grep -E 'QUIC flow metrics sample' | tail -n 20 + log "" + log "==> QUIC flow metrics (sample)" + echo "$agent_logs_since" | grep -E 'QUIC flow metrics sample' | tail -n 50 || true + + # Extra assertions for coverage: confirm the sample includes UDP (p=17) and port 443 + # (either src or dst, depending on which direction is sampled/logged). + if ! echo "$agent_logs_since" | grep -E 'QUIC flow metrics sample' | grep -Eq 'p=17'; then + log "FAIL: QUIC metrics sample did not include UDP transport (p=17)." + return 1 + fi + if ! echo "$agent_logs_since" | grep -E 'QUIC flow metrics sample' | grep -Eq "(:${expected_port}>|>:${expected_port}[[:space:]])"; then + log "FAIL: QUIC metrics sample did not include a flow where src or dst port is ${expected_port}." + log "" + log "==> Debug: QUIC flow metrics sample (tail 20)" + echo "$agent_logs_since" | grep -E 'QUIC flow metrics sample' | tail -n 20 || true + return 1 + fi + return 0 + fi + + log "" + log "FAIL: Did not find QUIC flow metrics marker with quicFlowsLogged>0 since $START_TS." + log "" + log "==> Debug: recent agent log lines with 'QUIC flow metrics sample' (tail 200 overall)" + kubectl logs -n "$NETOBSERV_NAMESPACE" -l "$AGENT_LABEL" --tail=200 2>/dev/null | grep -E 'QUIC flow metrics sample' || true + log "" + log "==> Debug: agent pods" + kubectl get pods -n "$NETOBSERV_NAMESPACE" -l "$AGENT_LABEL" -o wide || true + return 1 +} + +check_agent_logs() { + check_agent_logs_port 443 +} + +main() { + trap cleanup_quic_resources EXIT INT TERM + + kubectl cluster-info >/dev/null + + ensure_agent_exists + run_suite +} + +main diff --git a/pkg/agent/agent.go b/pkg/agent/agent.go index db43c03aa..29523dfb0 100644 --- a/pkg/agent/agent.go +++ b/pkg/agent/agent.go @@ -178,6 +178,7 @@ func FlowsAgent(cfg *config.Agent) (*Flows, error) { UseEbpfManager: cfg.EbpfProgramManagerMode, BpfManBpfFSPath: cfg.BpfManBpfFSPath, EnableIPsecTracker: cfg.EnableIPsecTracking, + QUICTrackingMode: cfg.QUICTrackingMode, FilterConfig: filterRules, EnableOpenSSLTracking: cfg.EnableOpenSSLTracking, OpenSSLPath: cfg.OpenSSLPath, diff --git a/pkg/config/config.go b/pkg/config/config.go index f9bf6d7f1..8a1d84ade 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -284,6 +284,12 @@ type Agent struct { // OpenSSLPath path to the openssl binary OpenSSLPath string `env:"OPENSSL_PATH" envDefault:"/usr/bin/openssl"` + // QUICTrackingMode configures QUIC parsing in eBPF: + // - 0: disabled + // - 1: enabled (UDP/443 only) + // - 2: enabled (any UDP port) + QUICTrackingMode int `env:"QUIC_TRACKING_MODE" envDefault:"0"` + /* Deprecated configs are listed below this line * See manageDeprecatedConfigs function for details */ diff --git a/pkg/decode/decode_protobuf.go b/pkg/decode/decode_protobuf.go index c377e62ff..86be112b5 100644 --- a/pkg/decode/decode_protobuf.go +++ b/pkg/decode/decode_protobuf.go @@ -170,6 +170,12 @@ func RecordToMap(fr *model.Record) config.GenericMap { out["NetworkEvents"] = fr.NetworkMonitorEventsMD } + if fr.Metrics.QuicMetrics != nil { + out["QuicVersion"] = fr.Metrics.QuicMetrics.Version + out["QuicSeenLongHdr"] = fr.Metrics.QuicMetrics.SeenLongHdr + out["QuicSeenShortHdr"] = fr.Metrics.QuicMetrics.SeenShortHdr + } + return out } diff --git a/pkg/decode/decode_protobuf_test.go b/pkg/decode/decode_protobuf_test.go index 251a186af..f739d94eb 100644 --- a/pkg/decode/decode_protobuf_test.go +++ b/pkg/decode/decode_protobuf_test.go @@ -1,10 +1,13 @@ package decode import ( + "net" "testing" "time" "github.com/netobserv/flowlogs-pipeline/pkg/config" + "github.com/netobserv/netobserv-ebpf-agent/pkg/ebpf" + "github.com/netobserv/netobserv-ebpf-agent/pkg/model" "github.com/netobserv/netobserv-ebpf-agent/pkg/pbflow" "github.com/netobserv/netobserv-ebpf-agent/pkg/utils" @@ -100,6 +103,11 @@ func TestPBFlowToMap(t *testing.T) { }, IpsecEncrypted: 1, IpsecEncryptedRet: 0, + Quic: &pbflow.Quic{ + Version: 1, + SeenLongHdr: 1, + SeenShortHdr: 1, + }, } out := PBFlowToMap(flow) @@ -151,16 +159,131 @@ func TestPBFlowToMap(t *testing.T) { "Direction": "egress", }, }, - "XlatSrcAddr": "1.2.3.4", - "XlatDstAddr": "5.6.7.8", - "XlatSrcPort": uint16(1), - "XlatDstPort": uint16(2), - "ZoneId": uint16(100), - "IPSecRetCode": int32(0), - "IPSecStatus": "success", + "XlatSrcAddr": "1.2.3.4", + "XlatDstAddr": "5.6.7.8", + "XlatSrcPort": uint16(1), + "XlatDstPort": uint16(2), + "ZoneId": uint16(100), + "IPSecRetCode": int32(0), + "IPSecStatus": "success", + "QuicVersion": uint32(1), + "QuicSeenLongHdr": uint8(1), + "QuicSeenShortHdr": uint8(1), }, out) } +func TestRecordToMap_OptionalMetrics(t *testing.T) { + someTime := time.Unix(1700000000, 0).UTC() + makeFlow := func(withQuic bool) *model.Record { + f := &model.Record{ + ID: ebpf.BpfFlowId{ + SrcIp: model.IPAddr{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x06, 0x07, 0x08, 0x09}, + DstIp: model.IPAddr{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x0a, 0x0b, 0x0c, 0x0d}, + SrcPort: 23000, + DstPort: 443, + TransportProtocol: 17, + }, + Metrics: model.BpfFlowContent{ + BpfFlowMetrics: &ebpf.BpfFlowMetrics{ + EthProtocol: 2048, + Bytes: 456, + Packets: 123, + }, + }, + Interfaces: []model.IntfDirUdn{model.NewIntfDirUdn("eth0", model.DirectionEgress, nil)}, + TimeFlowStart: someTime, + TimeFlowEnd: someTime, + AgentIP: net.IPv4(0x0a, 0x0b, 0x0c, 0x0d), + } + if withQuic { + f.Metrics.QuicMetrics = &ebpf.BpfQuicMetrics{Version: 1, SeenLongHdr: 1, SeenShortHdr: 1} + } + return f + } + + tests := []struct { + name string + withQuic bool + expectKeys bool + }{ + {name: "without optional metrics", withQuic: false, expectKeys: false}, + {name: "with optional metrics", withQuic: true, expectKeys: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + out := RecordToMap(makeFlow(tt.withQuic)) + _, ok := out["QuicVersion"] + assert.Equal(t, tt.expectKeys, ok) + _, ok = out["QuicSeenLongHdr"] + assert.Equal(t, tt.expectKeys, ok) + _, ok = out["QuicSeenShortHdr"] + assert.Equal(t, tt.expectKeys, ok) + + if tt.expectKeys { + assert.Equal(t, uint32(1), out["QuicVersion"]) + assert.Equal(t, uint8(1), out["QuicSeenLongHdr"]) + assert.Equal(t, uint8(1), out["QuicSeenShortHdr"]) + } + }) + } +} + +func TestPBFlowRoundTrip_OptionalFields(t *testing.T) { + now := time.Unix(1700000000, 0).UTC() + + tests := []struct { + name string + withQuic bool + }{ + {name: "without optional fields", withQuic: false}, + {name: "with optional fields", withQuic: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + in := &model.Record{ + ID: ebpf.BpfFlowId{ + TransportProtocol: 17, + SrcIp: model.IPAddr{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 1, 2, 3, 4}, + DstIp: model.IPAddr{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 5, 6, 7, 8}, + SrcPort: 12345, + DstPort: 443, + }, + Metrics: model.BpfFlowContent{ + BpfFlowMetrics: &ebpf.BpfFlowMetrics{ + EthProtocol: 2048, + DirectionFirstSeen: 1, + Bytes: 10, + Packets: 1, + }, + }, + TimeFlowStart: now, + TimeFlowEnd: now, + } + if tt.withQuic { + in.Metrics.QuicMetrics = &ebpf.BpfQuicMetrics{Version: 1, SeenLongHdr: 1, SeenShortHdr: 1} + } + + pb := pbflow.FlowToPB(in) + if tt.withQuic { + if assert.NotNil(t, pb.Quic) { + assert.Equal(t, uint32(1), pb.Quic.Version) + } + } else { + assert.Nil(t, pb.Quic) + } + + back := pbflow.PBToFlow(pb) + if tt.withQuic { + assert.NotNil(t, back.Metrics.QuicMetrics) + } else { + assert.Nil(t, back.Metrics.QuicMetrics) + } + }) + } +} + func TestDnsRawNameToDotted(t *testing.T) { tests := []struct { name string diff --git a/pkg/ebpf/bpf_arm64_bpfel.go b/pkg/ebpf/bpf_arm64_bpfel.go index 27cfd7d36..08b0001b9 100644 --- a/pkg/ebpf/bpf_arm64_bpfel.go +++ b/pkg/ebpf/bpf_arm64_bpfel.go @@ -196,6 +196,29 @@ type BpfPktDropMetricsT struct { _ [3]byte } +type BpfQuicConfigT uint32 + +const ( + BpfQuicConfigTQUIC_CONFIG_DISABLED BpfQuicConfigT = 0 + BpfQuicConfigTQUIC_CONFIG_ENABLED BpfQuicConfigT = 1 + BpfQuicConfigTQUIC_CONFIG_ANY_UDP_PORT BpfQuicConfigT = 2 +) + +type BpfQuicMetrics BpfQuicMetricsT + +type BpfQuicMetricsT struct { + _ structs.HostLayout + StartMonoTimeTs uint64 + EndMonoTimeTs uint64 + Bytes uint64 + Packets uint32 + Version uint32 + EthProtocol uint16 + SeenLongHdr uint8 + SeenShortHdr uint8 + _ [4]byte +} + type BpfSslDataEventT struct { _ structs.HostLayout TimestampNs uint64 @@ -317,6 +340,7 @@ type BpfMapSpecs struct { IpsecIngressMap *ebpf.MapSpec `ebpf:"ipsec_ingress_map"` PacketRecord *ebpf.MapSpec `ebpf:"packet_record"` PeerFilterMap *ebpf.MapSpec `ebpf:"peer_filter_map"` + QuicFlows *ebpf.MapSpec `ebpf:"quic_flows"` SslDataEventMap *ebpf.MapSpec `ebpf:"ssl_data_event_map"` } @@ -332,6 +356,7 @@ type BpfVariableSpecs struct { EnableOpensslTracking *ebpf.VariableSpec `ebpf:"enable_openssl_tracking"` EnablePca *ebpf.VariableSpec `ebpf:"enable_pca"` EnablePktTranslationTracking *ebpf.VariableSpec `ebpf:"enable_pkt_translation_tracking"` + EnableQuicTracking *ebpf.VariableSpec `ebpf:"enable_quic_tracking"` EnableRtt *ebpf.VariableSpec `ebpf:"enable_rtt"` FilterKey *ebpf.VariableSpec `ebpf:"filter_key"` FilterValue *ebpf.VariableSpec `ebpf:"filter_value"` @@ -379,6 +404,7 @@ type BpfMaps struct { IpsecIngressMap *ebpf.Map `ebpf:"ipsec_ingress_map"` PacketRecord *ebpf.Map `ebpf:"packet_record"` PeerFilterMap *ebpf.Map `ebpf:"peer_filter_map"` + QuicFlows *ebpf.Map `ebpf:"quic_flows"` SslDataEventMap *ebpf.Map `ebpf:"ssl_data_event_map"` } @@ -399,6 +425,7 @@ func (m *BpfMaps) Close() error { m.IpsecIngressMap, m.PacketRecord, m.PeerFilterMap, + m.QuicFlows, m.SslDataEventMap, ) } @@ -415,6 +442,7 @@ type BpfVariables struct { EnableOpensslTracking *ebpf.Variable `ebpf:"enable_openssl_tracking"` EnablePca *ebpf.Variable `ebpf:"enable_pca"` EnablePktTranslationTracking *ebpf.Variable `ebpf:"enable_pkt_translation_tracking"` + EnableQuicTracking *ebpf.Variable `ebpf:"enable_quic_tracking"` EnableRtt *ebpf.Variable `ebpf:"enable_rtt"` FilterKey *ebpf.Variable `ebpf:"filter_key"` FilterValue *ebpf.Variable `ebpf:"filter_value"` diff --git a/pkg/ebpf/bpf_arm64_bpfel.o b/pkg/ebpf/bpf_arm64_bpfel.o index e66d6ad3a..1839aafd3 100644 Binary files a/pkg/ebpf/bpf_arm64_bpfel.o and b/pkg/ebpf/bpf_arm64_bpfel.o differ diff --git a/pkg/ebpf/bpf_powerpc_bpfel.go b/pkg/ebpf/bpf_powerpc_bpfel.go index 26106ff74..e72a6dcc9 100644 --- a/pkg/ebpf/bpf_powerpc_bpfel.go +++ b/pkg/ebpf/bpf_powerpc_bpfel.go @@ -196,6 +196,29 @@ type BpfPktDropMetricsT struct { _ [3]byte } +type BpfQuicConfigT uint32 + +const ( + BpfQuicConfigTQUIC_CONFIG_DISABLED BpfQuicConfigT = 0 + BpfQuicConfigTQUIC_CONFIG_ENABLED BpfQuicConfigT = 1 + BpfQuicConfigTQUIC_CONFIG_ANY_UDP_PORT BpfQuicConfigT = 2 +) + +type BpfQuicMetrics BpfQuicMetricsT + +type BpfQuicMetricsT struct { + _ structs.HostLayout + StartMonoTimeTs uint64 + EndMonoTimeTs uint64 + Bytes uint64 + Packets uint32 + Version uint32 + EthProtocol uint16 + SeenLongHdr uint8 + SeenShortHdr uint8 + _ [4]byte +} + type BpfSslDataEventT struct { _ structs.HostLayout TimestampNs uint64 @@ -317,6 +340,7 @@ type BpfMapSpecs struct { IpsecIngressMap *ebpf.MapSpec `ebpf:"ipsec_ingress_map"` PacketRecord *ebpf.MapSpec `ebpf:"packet_record"` PeerFilterMap *ebpf.MapSpec `ebpf:"peer_filter_map"` + QuicFlows *ebpf.MapSpec `ebpf:"quic_flows"` SslDataEventMap *ebpf.MapSpec `ebpf:"ssl_data_event_map"` } @@ -332,6 +356,7 @@ type BpfVariableSpecs struct { EnableOpensslTracking *ebpf.VariableSpec `ebpf:"enable_openssl_tracking"` EnablePca *ebpf.VariableSpec `ebpf:"enable_pca"` EnablePktTranslationTracking *ebpf.VariableSpec `ebpf:"enable_pkt_translation_tracking"` + EnableQuicTracking *ebpf.VariableSpec `ebpf:"enable_quic_tracking"` EnableRtt *ebpf.VariableSpec `ebpf:"enable_rtt"` FilterKey *ebpf.VariableSpec `ebpf:"filter_key"` FilterValue *ebpf.VariableSpec `ebpf:"filter_value"` @@ -379,6 +404,7 @@ type BpfMaps struct { IpsecIngressMap *ebpf.Map `ebpf:"ipsec_ingress_map"` PacketRecord *ebpf.Map `ebpf:"packet_record"` PeerFilterMap *ebpf.Map `ebpf:"peer_filter_map"` + QuicFlows *ebpf.Map `ebpf:"quic_flows"` SslDataEventMap *ebpf.Map `ebpf:"ssl_data_event_map"` } @@ -399,6 +425,7 @@ func (m *BpfMaps) Close() error { m.IpsecIngressMap, m.PacketRecord, m.PeerFilterMap, + m.QuicFlows, m.SslDataEventMap, ) } @@ -415,6 +442,7 @@ type BpfVariables struct { EnableOpensslTracking *ebpf.Variable `ebpf:"enable_openssl_tracking"` EnablePca *ebpf.Variable `ebpf:"enable_pca"` EnablePktTranslationTracking *ebpf.Variable `ebpf:"enable_pkt_translation_tracking"` + EnableQuicTracking *ebpf.Variable `ebpf:"enable_quic_tracking"` EnableRtt *ebpf.Variable `ebpf:"enable_rtt"` FilterKey *ebpf.Variable `ebpf:"filter_key"` FilterValue *ebpf.Variable `ebpf:"filter_value"` diff --git a/pkg/ebpf/bpf_powerpc_bpfel.o b/pkg/ebpf/bpf_powerpc_bpfel.o index 400e41c33..5d79a011f 100644 Binary files a/pkg/ebpf/bpf_powerpc_bpfel.o and b/pkg/ebpf/bpf_powerpc_bpfel.o differ diff --git a/pkg/ebpf/bpf_s390_bpfeb.go b/pkg/ebpf/bpf_s390_bpfeb.go index ac61f02fd..c2ac69f62 100644 --- a/pkg/ebpf/bpf_s390_bpfeb.go +++ b/pkg/ebpf/bpf_s390_bpfeb.go @@ -196,6 +196,29 @@ type BpfPktDropMetricsT struct { _ [3]byte } +type BpfQuicConfigT uint32 + +const ( + BpfQuicConfigTQUIC_CONFIG_DISABLED BpfQuicConfigT = 0 + BpfQuicConfigTQUIC_CONFIG_ENABLED BpfQuicConfigT = 1 + BpfQuicConfigTQUIC_CONFIG_ANY_UDP_PORT BpfQuicConfigT = 2 +) + +type BpfQuicMetrics BpfQuicMetricsT + +type BpfQuicMetricsT struct { + _ structs.HostLayout + StartMonoTimeTs uint64 + EndMonoTimeTs uint64 + Bytes uint64 + Packets uint32 + Version uint32 + EthProtocol uint16 + SeenLongHdr uint8 + SeenShortHdr uint8 + _ [4]byte +} + type BpfSslDataEventT struct { _ structs.HostLayout TimestampNs uint64 @@ -317,6 +340,7 @@ type BpfMapSpecs struct { IpsecIngressMap *ebpf.MapSpec `ebpf:"ipsec_ingress_map"` PacketRecord *ebpf.MapSpec `ebpf:"packet_record"` PeerFilterMap *ebpf.MapSpec `ebpf:"peer_filter_map"` + QuicFlows *ebpf.MapSpec `ebpf:"quic_flows"` SslDataEventMap *ebpf.MapSpec `ebpf:"ssl_data_event_map"` } @@ -332,6 +356,7 @@ type BpfVariableSpecs struct { EnableOpensslTracking *ebpf.VariableSpec `ebpf:"enable_openssl_tracking"` EnablePca *ebpf.VariableSpec `ebpf:"enable_pca"` EnablePktTranslationTracking *ebpf.VariableSpec `ebpf:"enable_pkt_translation_tracking"` + EnableQuicTracking *ebpf.VariableSpec `ebpf:"enable_quic_tracking"` EnableRtt *ebpf.VariableSpec `ebpf:"enable_rtt"` FilterKey *ebpf.VariableSpec `ebpf:"filter_key"` FilterValue *ebpf.VariableSpec `ebpf:"filter_value"` @@ -379,6 +404,7 @@ type BpfMaps struct { IpsecIngressMap *ebpf.Map `ebpf:"ipsec_ingress_map"` PacketRecord *ebpf.Map `ebpf:"packet_record"` PeerFilterMap *ebpf.Map `ebpf:"peer_filter_map"` + QuicFlows *ebpf.Map `ebpf:"quic_flows"` SslDataEventMap *ebpf.Map `ebpf:"ssl_data_event_map"` } @@ -399,6 +425,7 @@ func (m *BpfMaps) Close() error { m.IpsecIngressMap, m.PacketRecord, m.PeerFilterMap, + m.QuicFlows, m.SslDataEventMap, ) } @@ -415,6 +442,7 @@ type BpfVariables struct { EnableOpensslTracking *ebpf.Variable `ebpf:"enable_openssl_tracking"` EnablePca *ebpf.Variable `ebpf:"enable_pca"` EnablePktTranslationTracking *ebpf.Variable `ebpf:"enable_pkt_translation_tracking"` + EnableQuicTracking *ebpf.Variable `ebpf:"enable_quic_tracking"` EnableRtt *ebpf.Variable `ebpf:"enable_rtt"` FilterKey *ebpf.Variable `ebpf:"filter_key"` FilterValue *ebpf.Variable `ebpf:"filter_value"` diff --git a/pkg/ebpf/bpf_s390_bpfeb.o b/pkg/ebpf/bpf_s390_bpfeb.o index 8f5639bfd..f1c74a94c 100644 Binary files a/pkg/ebpf/bpf_s390_bpfeb.o and b/pkg/ebpf/bpf_s390_bpfeb.o differ diff --git a/pkg/ebpf/bpf_x86_bpfel.go b/pkg/ebpf/bpf_x86_bpfel.go index 9c6310ac5..ef6de6c92 100644 --- a/pkg/ebpf/bpf_x86_bpfel.go +++ b/pkg/ebpf/bpf_x86_bpfel.go @@ -196,6 +196,29 @@ type BpfPktDropMetricsT struct { _ [3]byte } +type BpfQuicConfigT uint32 + +const ( + BpfQuicConfigTQUIC_CONFIG_DISABLED BpfQuicConfigT = 0 + BpfQuicConfigTQUIC_CONFIG_ENABLED BpfQuicConfigT = 1 + BpfQuicConfigTQUIC_CONFIG_ANY_UDP_PORT BpfQuicConfigT = 2 +) + +type BpfQuicMetrics BpfQuicMetricsT + +type BpfQuicMetricsT struct { + _ structs.HostLayout + StartMonoTimeTs uint64 + EndMonoTimeTs uint64 + Bytes uint64 + Packets uint32 + Version uint32 + EthProtocol uint16 + SeenLongHdr uint8 + SeenShortHdr uint8 + _ [4]byte +} + type BpfSslDataEventT struct { _ structs.HostLayout TimestampNs uint64 @@ -317,6 +340,7 @@ type BpfMapSpecs struct { IpsecIngressMap *ebpf.MapSpec `ebpf:"ipsec_ingress_map"` PacketRecord *ebpf.MapSpec `ebpf:"packet_record"` PeerFilterMap *ebpf.MapSpec `ebpf:"peer_filter_map"` + QuicFlows *ebpf.MapSpec `ebpf:"quic_flows"` SslDataEventMap *ebpf.MapSpec `ebpf:"ssl_data_event_map"` } @@ -332,6 +356,7 @@ type BpfVariableSpecs struct { EnableOpensslTracking *ebpf.VariableSpec `ebpf:"enable_openssl_tracking"` EnablePca *ebpf.VariableSpec `ebpf:"enable_pca"` EnablePktTranslationTracking *ebpf.VariableSpec `ebpf:"enable_pkt_translation_tracking"` + EnableQuicTracking *ebpf.VariableSpec `ebpf:"enable_quic_tracking"` EnableRtt *ebpf.VariableSpec `ebpf:"enable_rtt"` FilterKey *ebpf.VariableSpec `ebpf:"filter_key"` FilterValue *ebpf.VariableSpec `ebpf:"filter_value"` @@ -379,6 +404,7 @@ type BpfMaps struct { IpsecIngressMap *ebpf.Map `ebpf:"ipsec_ingress_map"` PacketRecord *ebpf.Map `ebpf:"packet_record"` PeerFilterMap *ebpf.Map `ebpf:"peer_filter_map"` + QuicFlows *ebpf.Map `ebpf:"quic_flows"` SslDataEventMap *ebpf.Map `ebpf:"ssl_data_event_map"` } @@ -399,6 +425,7 @@ func (m *BpfMaps) Close() error { m.IpsecIngressMap, m.PacketRecord, m.PeerFilterMap, + m.QuicFlows, m.SslDataEventMap, ) } @@ -415,6 +442,7 @@ type BpfVariables struct { EnableOpensslTracking *ebpf.Variable `ebpf:"enable_openssl_tracking"` EnablePca *ebpf.Variable `ebpf:"enable_pca"` EnablePktTranslationTracking *ebpf.Variable `ebpf:"enable_pkt_translation_tracking"` + EnableQuicTracking *ebpf.Variable `ebpf:"enable_quic_tracking"` EnableRtt *ebpf.Variable `ebpf:"enable_rtt"` FilterKey *ebpf.Variable `ebpf:"filter_key"` FilterValue *ebpf.Variable `ebpf:"filter_value"` diff --git a/pkg/ebpf/bpf_x86_bpfel.o b/pkg/ebpf/bpf_x86_bpfel.o index 616bb44f1..1caf8352d 100644 Binary files a/pkg/ebpf/bpf_x86_bpfel.o and b/pkg/ebpf/bpf_x86_bpfel.o differ diff --git a/pkg/ebpf/gen.go b/pkg/ebpf/gen.go index 8f69d5979..1a145fe2d 100644 --- a/pkg/ebpf/gen.go +++ b/pkg/ebpf/gen.go @@ -1,4 +1,4 @@ package ebpf // $BPF_CLANG and $BPF_CFLAGS are set by the Makefile. -//go:generate bpf2go -cc $BPF_CLANG -cflags $BPF_CFLAGS -target amd64,arm64,ppc64le,s390x -type flow_metrics_t -type flow_id_t -type flow_record_t -type pkt_drop_metrics_t -type dns_metrics_t -type global_counters_key_t -type direction_t -type filter_action_t -type tcp_flags_t -type network_events_metrics_t -type xlat_metrics_t Bpf ../../bpf/flows.c -- -I../../bpf/headers +//go:generate bpf2go -cc $BPF_CLANG -cflags $BPF_CFLAGS -target amd64,arm64,ppc64le,s390x -type flow_metrics_t -type flow_id_t -type flow_record_t -type pkt_drop_metrics_t -type dns_metrics_t -type global_counters_key_t -type direction_t -type filter_action_t -type tcp_flags_t -type network_events_metrics_t -type xlat_metrics_t -type quic_metrics_t -type quic_config_t Bpf ../../bpf/flows.c -- -I../../bpf/headers diff --git a/pkg/exporter/converters_test.go b/pkg/exporter/converters_test.go index cd6c01b84..a44bc2a5d 100644 --- a/pkg/exporter/converters_test.go +++ b/pkg/exporter/converters_test.go @@ -101,6 +101,11 @@ func TestConversions(t *testing.T) { Dscp: 64, Sampling: 2, }, + QuicMetrics: &ebpf.BpfQuicMetrics{ + Version: 1, + SeenLongHdr: 1, + SeenShortHdr: 1, + }, }, Interfaces: []model.IntfDirUdn{model.NewIntfDirUdn("eth0", model.DirectionEgress, nil)}, TimeFlowStart: someTime, @@ -108,24 +113,27 @@ func TestConversions(t *testing.T) { AgentIP: net.IPv4(0x0a, 0x0b, 0x0c, 0x0d), }, expected: &config.GenericMap{ - "IfDirections": []int{1}, - "Bytes": 456, - "SrcAddr": "6.7.8.9", - "DstAddr": "10.11.12.13", - "Dscp": 64, - "DstMac": "0a:0b:0c:0d:0e:0f", - "SrcMac": "04:05:06:07:08:09", - "Etype": 2048, - "Packets": 123, - "Proto": 17, - "Sampling": 2, - "SrcPort": 23000, - "DstPort": 443, - "TimeFlowStartMs": someTime.UnixMilli(), - "TimeFlowEndMs": someTime.UnixMilli(), - "Interfaces": []string{"eth0"}, - "Udns": []string{""}, - "AgentIP": "10.11.12.13", + "IfDirections": []int{1}, + "Bytes": 456, + "SrcAddr": "6.7.8.9", + "DstAddr": "10.11.12.13", + "Dscp": 64, + "DstMac": "0a:0b:0c:0d:0e:0f", + "SrcMac": "04:05:06:07:08:09", + "Etype": 2048, + "Packets": 123, + "Proto": 17, + "Sampling": 2, + "SrcPort": 23000, + "DstPort": 443, + "TimeFlowStartMs": someTime.UnixMilli(), + "TimeFlowEndMs": someTime.UnixMilli(), + "Interfaces": []string{"eth0"}, + "Udns": []string{""}, + "AgentIP": "10.11.12.13", + "QuicVersion": 1, + "QuicSeenLongHdr": 1, + "QuicSeenShortHdr": 1, }, }, { diff --git a/pkg/maps/maps.go b/pkg/maps/maps.go index a2dbb3bfa..89dd834bc 100644 --- a/pkg/maps/maps.go +++ b/pkg/maps/maps.go @@ -18,4 +18,5 @@ var Maps = []string{ "ipsec_egress_map", "ssl_data_event_map", "dns_name_map", + "quic_flows", } diff --git a/pkg/model/flow_content.go b/pkg/model/flow_content.go index 3cd3c1c92..5f729185a 100644 --- a/pkg/model/flow_content.go +++ b/pkg/model/flow_content.go @@ -11,6 +11,7 @@ type BpfFlowContent struct { NetworkEventsMetrics *ebpf.BpfNetworkEventsMetrics XlatMetrics *ebpf.BpfXlatMetrics AdditionalMetrics *ebpf.BpfAdditionalMetrics + QuicMetrics *ebpf.BpfQuicMetrics } // nolint:gocritic // hugeParam: metric is reported as heavy; but it needs to be copied anyway, we don't want a pointer here @@ -171,6 +172,26 @@ func (p *BpfFlowContent) AccumulateAdditional(other *ebpf.BpfAdditionalMetrics) } } +func (p *BpfFlowContent) AccumulateQuic(other *ebpf.BpfQuicMetrics) { + if other == nil { + return + } + p.buildBaseFromAdditional(other.StartMonoTimeTs, other.EndMonoTimeTs, other.EthProtocol) + if p.QuicMetrics == nil { + p.QuicMetrics = other + } + // QUIC + if p.QuicMetrics.Version < other.Version { + p.QuicMetrics.Version = other.Version + } + if p.QuicMetrics.SeenLongHdr < other.SeenLongHdr { + p.QuicMetrics.SeenLongHdr = other.SeenLongHdr + } + if p.QuicMetrics.SeenShortHdr < other.SeenShortHdr { + p.QuicMetrics.SeenShortHdr = other.SeenShortHdr + } +} + func AllZerosMac(s [6]uint8) bool { for _, v := range s { if v != 0 { diff --git a/pkg/model/flow_content_test.go b/pkg/model/flow_content_test.go index b289a797d..0d3f00584 100644 --- a/pkg/model/flow_content_test.go +++ b/pkg/model/flow_content_test.go @@ -237,6 +237,96 @@ func TestAccumulateAdditional(t *testing.T) { }, flow) } +func TestAccumulateQuic(t *testing.T) { + flow := BpfFlowContent{BpfFlowMetrics: &ebpf.BpfFlowMetrics{ + StartMonoTimeTs: 10, + EndMonoTimeTs: 20, + Packets: 3, + }} + + // First QUIC metric should set base timestamps and initialize QuicMetrics. + flow.AccumulateQuic(&ebpf.BpfQuicMetrics{ + StartMonoTimeTs: 25, + EndMonoTimeTs: 25, + EthProtocol: 3, + Version: 1, + SeenLongHdr: 1, + SeenShortHdr: 0, + }) + assert.Equal(t, BpfFlowContent{ + BpfFlowMetrics: &ebpf.BpfFlowMetrics{StartMonoTimeTs: 10, EndMonoTimeTs: 25, Packets: 3, EthProtocol: 3}, + QuicMetrics: &ebpf.BpfQuicMetrics{ + StartMonoTimeTs: 25, + EndMonoTimeTs: 25, + EthProtocol: 3, + Version: 1, + SeenLongHdr: 1, + SeenShortHdr: 0, + }, + }, flow) + + // Second QUIC metric should update max fields. + flow.AccumulateQuic(&ebpf.BpfQuicMetrics{ + StartMonoTimeTs: 30, + EndMonoTimeTs: 30, + EthProtocol: 3, + Version: 2, + SeenLongHdr: 0, + SeenShortHdr: 1, + }) + assert.Equal(t, BpfFlowContent{ + BpfFlowMetrics: &ebpf.BpfFlowMetrics{StartMonoTimeTs: 10, EndMonoTimeTs: 30, Packets: 3, EthProtocol: 3}, + QuicMetrics: &ebpf.BpfQuicMetrics{ + StartMonoTimeTs: 25, + EndMonoTimeTs: 25, + EthProtocol: 3, + Version: 2, + SeenLongHdr: 1, + SeenShortHdr: 1, + }, + }, flow) +} + +func TestAccumulateQuic_NilNoop(t *testing.T) { + flow := BpfFlowContent{BpfFlowMetrics: &ebpf.BpfFlowMetrics{ + StartMonoTimeTs: 10, + EndMonoTimeTs: 20, + Packets: 3, + EthProtocol: 2048, + }} + before := flow + flow.AccumulateQuic(nil) + assert.Equal(t, before, flow) +} + +func TestAccumulateQuic_DoesNotDecrease(t *testing.T) { + flow := BpfFlowContent{BpfFlowMetrics: &ebpf.BpfFlowMetrics{ + StartMonoTimeTs: 10, + EndMonoTimeTs: 20, + Packets: 3, + EthProtocol: 2048, + }} + flow.AccumulateQuic(&ebpf.BpfQuicMetrics{ + StartMonoTimeTs: 25, + EndMonoTimeTs: 25, + EthProtocol: 2048, + Version: 2, + SeenLongHdr: 1, + SeenShortHdr: 1, + }) + flow.AccumulateQuic(&ebpf.BpfQuicMetrics{ + StartMonoTimeTs: 30, + EndMonoTimeTs: 30, + EthProtocol: 2048, + Version: 1, // lower than existing + SeenLongHdr: 0, + SeenShortHdr: 0, + }) + assert.Equal(t, uint32(2), flow.QuicMetrics.Version) + assert.Equal(t, uint8(1), flow.QuicMetrics.SeenLongHdr) + assert.Equal(t, uint8(1), flow.QuicMetrics.SeenShortHdr) +} + func TestAccumulateNowBase(t *testing.T) { flow := BpfFlowContent{BpfFlowMetrics: &ebpf.BpfFlowMetrics{}} flow.AccumulateDNS(&ebpf.BpfDnsMetrics{StartMonoTimeTs: 25, EndMonoTimeTs: 25}) @@ -272,4 +362,11 @@ func TestAccumulateNowBase(t *testing.T) { BpfFlowMetrics: &ebpf.BpfFlowMetrics{StartMonoTimeTs: 25, EndMonoTimeTs: 25}, AdditionalMetrics: &ebpf.BpfAdditionalMetrics{StartMonoTimeTs: 25, EndMonoTimeTs: 25}, }, flow) + + flow = BpfFlowContent{BpfFlowMetrics: &ebpf.BpfFlowMetrics{}} + flow.AccumulateQuic(&ebpf.BpfQuicMetrics{StartMonoTimeTs: 25, EndMonoTimeTs: 25, EthProtocol: 3}) + assert.Equal(t, BpfFlowContent{ + BpfFlowMetrics: &ebpf.BpfFlowMetrics{StartMonoTimeTs: 25, EndMonoTimeTs: 25, EthProtocol: 3}, + QuicMetrics: &ebpf.BpfQuicMetrics{StartMonoTimeTs: 25, EndMonoTimeTs: 25, EthProtocol: 3}, + }, flow) } diff --git a/pkg/pbflow/flow.pb.go b/pkg/pbflow/flow.pb.go index 4dfe8aa34..d5b047305 100644 --- a/pkg/pbflow/flow.pb.go +++ b/pkg/pbflow/flow.pb.go @@ -300,6 +300,7 @@ type Record struct { IpsecEncrypted uint32 `protobuf:"varint,30,opt,name=ipsec_encrypted,json=ipsecEncrypted,proto3" json:"ipsec_encrypted,omitempty"` IpsecEncryptedRet int32 `protobuf:"varint,31,opt,name=ipsec_encrypted_ret,json=ipsecEncryptedRet,proto3" json:"ipsec_encrypted_ret,omitempty"` DnsName string `protobuf:"bytes,32,opt,name=dns_name,json=dnsName,proto3" json:"dns_name,omitempty"` + Quic *Quic `protobuf:"bytes,33,opt,name=quic,proto3" json:"quic,omitempty"` } func (x *Record) Reset() { @@ -556,6 +557,13 @@ func (x *Record) GetDnsName() string { return "" } +func (x *Record) GetQuic() *Quic { + if x != nil { + return x.Quic + } + return nil +} + type DataLink struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -889,6 +897,67 @@ func (x *Xlat) GetZoneId() uint32 { return 0 } +type Quic struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Version uint32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` + SeenLongHdr uint32 `protobuf:"varint,2,opt,name=seen_long_hdr,json=seenLongHdr,proto3" json:"seen_long_hdr,omitempty"` + SeenShortHdr uint32 `protobuf:"varint,3,opt,name=seen_short_hdr,json=seenShortHdr,proto3" json:"seen_short_hdr,omitempty"` +} + +func (x *Quic) Reset() { + *x = Quic{} + mi := &file_proto_flow_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Quic) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Quic) ProtoMessage() {} + +func (x *Quic) ProtoReflect() protoreflect.Message { + mi := &file_proto_flow_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Quic.ProtoReflect.Descriptor instead. +func (*Quic) Descriptor() ([]byte, []int) { + return file_proto_flow_proto_rawDescGZIP(), []int{10} +} + +func (x *Quic) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *Quic) GetSeenLongHdr() uint32 { + if x != nil { + return x.SeenLongHdr + } + return 0 +} + +func (x *Quic) GetSeenShortHdr() uint32 { + if x != nil { + return x.SeenShortHdr + } + return 0 +} + var File_proto_flow_proto protoreflect.FileDescriptor var file_proto_flow_proto_rawDesc = []byte{ @@ -917,7 +986,7 @@ var file_proto_flow_proto_rawDesc = []byte{ 0x0b, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xbc, 0x0a, 0x0a, 0x06, 0x52, 0x65, 0x63, + 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xde, 0x0a, 0x0a, 0x06, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x65, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x65, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x2f, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, @@ -1001,45 +1070,54 @@ var file_proto_flow_proto_rawDesc = []byte{ 0x72, 0x65, 0x74, 0x18, 0x1f, 0x20, 0x01, 0x28, 0x05, 0x52, 0x11, 0x69, 0x70, 0x73, 0x65, 0x63, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x52, 0x65, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x64, 0x6e, 0x73, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x20, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, - 0x64, 0x6e, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x3c, 0x0a, 0x08, 0x44, 0x61, 0x74, 0x61, 0x4c, - 0x69, 0x6e, 0x6b, 0x12, 0x17, 0x0a, 0x07, 0x73, 0x72, 0x63, 0x5f, 0x6d, 0x61, 0x63, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x73, 0x72, 0x63, 0x4d, 0x61, 0x63, 0x12, 0x17, 0x0a, 0x07, - 0x64, 0x73, 0x74, 0x5f, 0x6d, 0x61, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x64, - 0x73, 0x74, 0x4d, 0x61, 0x63, 0x22, 0x6b, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, - 0x12, 0x25, 0x0a, 0x08, 0x73, 0x72, 0x63, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x70, 0x62, 0x66, 0x6c, 0x6f, 0x77, 0x2e, 0x49, 0x50, 0x52, 0x07, - 0x73, 0x72, 0x63, 0x41, 0x64, 0x64, 0x72, 0x12, 0x25, 0x0a, 0x08, 0x64, 0x73, 0x74, 0x5f, 0x61, - 0x64, 0x64, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x70, 0x62, 0x66, 0x6c, - 0x6f, 0x77, 0x2e, 0x49, 0x50, 0x52, 0x07, 0x64, 0x73, 0x74, 0x41, 0x64, 0x64, 0x72, 0x12, 0x12, - 0x0a, 0x04, 0x64, 0x73, 0x63, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x64, 0x73, - 0x63, 0x70, 0x22, 0x3d, 0x0a, 0x02, 0x49, 0x50, 0x12, 0x14, 0x0a, 0x04, 0x69, 0x70, 0x76, 0x34, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x07, 0x48, 0x00, 0x52, 0x04, 0x69, 0x70, 0x76, 0x34, 0x12, 0x14, - 0x0a, 0x04, 0x69, 0x70, 0x76, 0x36, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x04, - 0x69, 0x70, 0x76, 0x36, 0x42, 0x0b, 0x0a, 0x09, 0x69, 0x70, 0x5f, 0x66, 0x61, 0x6d, 0x69, 0x6c, - 0x79, 0x22, 0x5d, 0x0a, 0x09, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x19, - 0x0a, 0x08, 0x73, 0x72, 0x63, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, - 0x52, 0x07, 0x73, 0x72, 0x63, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x64, 0x73, 0x74, - 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x64, 0x73, 0x74, - 0x50, 0x6f, 0x72, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, - 0x22, 0xa3, 0x01, 0x0a, 0x04, 0x58, 0x6c, 0x61, 0x74, 0x12, 0x25, 0x0a, 0x08, 0x73, 0x72, 0x63, - 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x70, 0x62, - 0x66, 0x6c, 0x6f, 0x77, 0x2e, 0x49, 0x50, 0x52, 0x07, 0x73, 0x72, 0x63, 0x41, 0x64, 0x64, 0x72, - 0x12, 0x25, 0x0a, 0x08, 0x64, 0x73, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x70, 0x62, 0x66, 0x6c, 0x6f, 0x77, 0x2e, 0x49, 0x50, 0x52, 0x07, - 0x64, 0x73, 0x74, 0x41, 0x64, 0x64, 0x72, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x72, 0x63, 0x5f, 0x70, - 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x73, 0x72, 0x63, 0x50, 0x6f, - 0x72, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x64, 0x73, 0x74, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x64, 0x73, 0x74, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x17, 0x0a, - 0x07, 0x7a, 0x6f, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, - 0x7a, 0x6f, 0x6e, 0x65, 0x49, 0x64, 0x2a, 0x24, 0x0a, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x12, 0x0b, 0x0a, 0x07, 0x49, 0x4e, 0x47, 0x52, 0x45, 0x53, 0x53, 0x10, 0x00, - 0x12, 0x0a, 0x0a, 0x06, 0x45, 0x47, 0x52, 0x45, 0x53, 0x53, 0x10, 0x01, 0x32, 0x3e, 0x0a, 0x09, - 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x31, 0x0a, 0x04, 0x53, 0x65, 0x6e, - 0x64, 0x12, 0x0f, 0x2e, 0x70, 0x62, 0x66, 0x6c, 0x6f, 0x77, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, - 0x64, 0x73, 0x1a, 0x16, 0x2e, 0x70, 0x62, 0x66, 0x6c, 0x6f, 0x77, 0x2e, 0x43, 0x6f, 0x6c, 0x6c, - 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x42, 0x0a, 0x5a, 0x08, - 0x2e, 0x2f, 0x70, 0x62, 0x66, 0x6c, 0x6f, 0x77, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x64, 0x6e, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x04, 0x71, 0x75, 0x69, 0x63, 0x18, + 0x21, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x70, 0x62, 0x66, 0x6c, 0x6f, 0x77, 0x2e, 0x51, + 0x75, 0x69, 0x63, 0x52, 0x04, 0x71, 0x75, 0x69, 0x63, 0x22, 0x3c, 0x0a, 0x08, 0x44, 0x61, 0x74, + 0x61, 0x4c, 0x69, 0x6e, 0x6b, 0x12, 0x17, 0x0a, 0x07, 0x73, 0x72, 0x63, 0x5f, 0x6d, 0x61, 0x63, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x73, 0x72, 0x63, 0x4d, 0x61, 0x63, 0x12, 0x17, + 0x0a, 0x07, 0x64, 0x73, 0x74, 0x5f, 0x6d, 0x61, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, + 0x06, 0x64, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x22, 0x6b, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, + 0x72, 0x6b, 0x12, 0x25, 0x0a, 0x08, 0x73, 0x72, 0x63, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x70, 0x62, 0x66, 0x6c, 0x6f, 0x77, 0x2e, 0x49, 0x50, + 0x52, 0x07, 0x73, 0x72, 0x63, 0x41, 0x64, 0x64, 0x72, 0x12, 0x25, 0x0a, 0x08, 0x64, 0x73, 0x74, + 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x70, 0x62, + 0x66, 0x6c, 0x6f, 0x77, 0x2e, 0x49, 0x50, 0x52, 0x07, 0x64, 0x73, 0x74, 0x41, 0x64, 0x64, 0x72, + 0x12, 0x12, 0x0a, 0x04, 0x64, 0x73, 0x63, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, + 0x64, 0x73, 0x63, 0x70, 0x22, 0x3d, 0x0a, 0x02, 0x49, 0x50, 0x12, 0x14, 0x0a, 0x04, 0x69, 0x70, + 0x76, 0x34, 0x18, 0x01, 0x20, 0x01, 0x28, 0x07, 0x48, 0x00, 0x52, 0x04, 0x69, 0x70, 0x76, 0x34, + 0x12, 0x14, 0x0a, 0x04, 0x69, 0x70, 0x76, 0x36, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, + 0x52, 0x04, 0x69, 0x70, 0x76, 0x36, 0x42, 0x0b, 0x0a, 0x09, 0x69, 0x70, 0x5f, 0x66, 0x61, 0x6d, + 0x69, 0x6c, 0x79, 0x22, 0x5d, 0x0a, 0x09, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, + 0x12, 0x19, 0x0a, 0x08, 0x73, 0x72, 0x63, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0d, 0x52, 0x07, 0x73, 0x72, 0x63, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x64, + 0x73, 0x74, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x64, + 0x73, 0x74, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, + 0x6f, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, + 0x6f, 0x6c, 0x22, 0xa3, 0x01, 0x0a, 0x04, 0x58, 0x6c, 0x61, 0x74, 0x12, 0x25, 0x0a, 0x08, 0x73, + 0x72, 0x63, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0a, 0x2e, + 0x70, 0x62, 0x66, 0x6c, 0x6f, 0x77, 0x2e, 0x49, 0x50, 0x52, 0x07, 0x73, 0x72, 0x63, 0x41, 0x64, + 0x64, 0x72, 0x12, 0x25, 0x0a, 0x08, 0x64, 0x73, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x70, 0x62, 0x66, 0x6c, 0x6f, 0x77, 0x2e, 0x49, 0x50, + 0x52, 0x07, 0x64, 0x73, 0x74, 0x41, 0x64, 0x64, 0x72, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x72, 0x63, + 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x73, 0x72, 0x63, + 0x50, 0x6f, 0x72, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x64, 0x73, 0x74, 0x5f, 0x70, 0x6f, 0x72, 0x74, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x64, 0x73, 0x74, 0x50, 0x6f, 0x72, 0x74, 0x12, + 0x17, 0x0a, 0x07, 0x7a, 0x6f, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, + 0x52, 0x06, 0x7a, 0x6f, 0x6e, 0x65, 0x49, 0x64, 0x22, 0x6a, 0x0a, 0x04, 0x51, 0x75, 0x69, 0x63, + 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x22, 0x0a, 0x0d, 0x73, 0x65, + 0x65, 0x6e, 0x5f, 0x6c, 0x6f, 0x6e, 0x67, 0x5f, 0x68, 0x64, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0d, 0x52, 0x0b, 0x73, 0x65, 0x65, 0x6e, 0x4c, 0x6f, 0x6e, 0x67, 0x48, 0x64, 0x72, 0x12, 0x24, + 0x0a, 0x0e, 0x73, 0x65, 0x65, 0x6e, 0x5f, 0x73, 0x68, 0x6f, 0x72, 0x74, 0x5f, 0x68, 0x64, 0x72, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0c, 0x73, 0x65, 0x65, 0x6e, 0x53, 0x68, 0x6f, 0x72, + 0x74, 0x48, 0x64, 0x72, 0x2a, 0x24, 0x0a, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x0b, 0x0a, 0x07, 0x49, 0x4e, 0x47, 0x52, 0x45, 0x53, 0x53, 0x10, 0x00, 0x12, 0x0a, + 0x0a, 0x06, 0x45, 0x47, 0x52, 0x45, 0x53, 0x53, 0x10, 0x01, 0x32, 0x3e, 0x0a, 0x09, 0x43, 0x6f, + 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x31, 0x0a, 0x04, 0x53, 0x65, 0x6e, 0x64, 0x12, + 0x0f, 0x2e, 0x70, 0x62, 0x66, 0x6c, 0x6f, 0x77, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, + 0x1a, 0x16, 0x2e, 0x70, 0x62, 0x66, 0x6c, 0x6f, 0x77, 0x2e, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, + 0x74, 0x6f, 0x72, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x42, 0x0a, 0x5a, 0x08, 0x2e, 0x2f, + 0x70, 0x62, 0x66, 0x6c, 0x6f, 0x77, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1055,7 +1133,7 @@ func file_proto_flow_proto_rawDescGZIP() []byte { } var file_proto_flow_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_proto_flow_proto_msgTypes = make([]protoimpl.MessageInfo, 11) +var file_proto_flow_proto_msgTypes = make([]protoimpl.MessageInfo, 12) var file_proto_flow_proto_goTypes = []any{ (Direction)(0), // 0: pbflow.Direction (*CollectorReply)(nil), // 1: pbflow.CollectorReply @@ -1068,37 +1146,39 @@ var file_proto_flow_proto_goTypes = []any{ (*IP)(nil), // 8: pbflow.IP (*Transport)(nil), // 9: pbflow.Transport (*Xlat)(nil), // 10: pbflow.Xlat - nil, // 11: pbflow.NetworkEvent.EventsEntry - (*timestamppb.Timestamp)(nil), // 12: google.protobuf.Timestamp - (*durationpb.Duration)(nil), // 13: google.protobuf.Duration + (*Quic)(nil), // 11: pbflow.Quic + nil, // 12: pbflow.NetworkEvent.EventsEntry + (*timestamppb.Timestamp)(nil), // 13: google.protobuf.Timestamp + (*durationpb.Duration)(nil), // 14: google.protobuf.Duration } var file_proto_flow_proto_depIdxs = []int32{ 5, // 0: pbflow.Records.entries:type_name -> pbflow.Record 0, // 1: pbflow.DupMapEntry.direction:type_name -> pbflow.Direction - 11, // 2: pbflow.NetworkEvent.events:type_name -> pbflow.NetworkEvent.EventsEntry + 12, // 2: pbflow.NetworkEvent.events:type_name -> pbflow.NetworkEvent.EventsEntry 0, // 3: pbflow.Record.direction:type_name -> pbflow.Direction - 12, // 4: pbflow.Record.time_flow_start:type_name -> google.protobuf.Timestamp - 12, // 5: pbflow.Record.time_flow_end:type_name -> google.protobuf.Timestamp + 13, // 4: pbflow.Record.time_flow_start:type_name -> google.protobuf.Timestamp + 13, // 5: pbflow.Record.time_flow_end:type_name -> google.protobuf.Timestamp 6, // 6: pbflow.Record.data_link:type_name -> pbflow.DataLink 7, // 7: pbflow.Record.network:type_name -> pbflow.Network 9, // 8: pbflow.Record.transport:type_name -> pbflow.Transport 8, // 9: pbflow.Record.agent_ip:type_name -> pbflow.IP - 13, // 10: pbflow.Record.dns_latency:type_name -> google.protobuf.Duration - 13, // 11: pbflow.Record.time_flow_rtt:type_name -> google.protobuf.Duration + 14, // 10: pbflow.Record.dns_latency:type_name -> google.protobuf.Duration + 14, // 11: pbflow.Record.time_flow_rtt:type_name -> google.protobuf.Duration 3, // 12: pbflow.Record.dup_list:type_name -> pbflow.DupMapEntry 4, // 13: pbflow.Record.network_events_metadata:type_name -> pbflow.NetworkEvent 10, // 14: pbflow.Record.xlat:type_name -> pbflow.Xlat - 8, // 15: pbflow.Network.src_addr:type_name -> pbflow.IP - 8, // 16: pbflow.Network.dst_addr:type_name -> pbflow.IP - 8, // 17: pbflow.Xlat.src_addr:type_name -> pbflow.IP - 8, // 18: pbflow.Xlat.dst_addr:type_name -> pbflow.IP - 2, // 19: pbflow.Collector.Send:input_type -> pbflow.Records - 1, // 20: pbflow.Collector.Send:output_type -> pbflow.CollectorReply - 20, // [20:21] is the sub-list for method output_type - 19, // [19:20] is the sub-list for method input_type - 19, // [19:19] is the sub-list for extension type_name - 19, // [19:19] is the sub-list for extension extendee - 0, // [0:19] is the sub-list for field type_name + 11, // 15: pbflow.Record.quic:type_name -> pbflow.Quic + 8, // 16: pbflow.Network.src_addr:type_name -> pbflow.IP + 8, // 17: pbflow.Network.dst_addr:type_name -> pbflow.IP + 8, // 18: pbflow.Xlat.src_addr:type_name -> pbflow.IP + 8, // 19: pbflow.Xlat.dst_addr:type_name -> pbflow.IP + 2, // 20: pbflow.Collector.Send:input_type -> pbflow.Records + 1, // 21: pbflow.Collector.Send:output_type -> pbflow.CollectorReply + 21, // [21:22] is the sub-list for method output_type + 20, // [20:21] is the sub-list for method input_type + 20, // [20:20] is the sub-list for extension type_name + 20, // [20:20] is the sub-list for extension extendee + 0, // [0:20] is the sub-list for field type_name } func init() { file_proto_flow_proto_init() } @@ -1116,7 +1196,7 @@ func file_proto_flow_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_proto_flow_proto_rawDesc, NumEnums: 1, - NumMessages: 11, + NumMessages: 12, NumExtensions: 0, NumServices: 1, }, diff --git a/pkg/pbflow/proto.go b/pkg/pbflow/proto.go index 6d3a0e120..8f0d88eed 100644 --- a/pkg/pbflow/proto.go +++ b/pkg/pbflow/proto.go @@ -101,6 +101,13 @@ func FlowToPB(fr *model.Record) *Record { pbflowRecord.IpsecEncrypted = uint32(1) } } + if fr.Metrics.QuicMetrics != nil { + pbflowRecord.Quic = &Quic{ + Version: uint32(fr.Metrics.QuicMetrics.Version), + SeenLongHdr: uint32(fr.Metrics.QuicMetrics.SeenLongHdr), + SeenShortHdr: uint32(fr.Metrics.QuicMetrics.SeenShortHdr), + } + } pbflowRecord.DupList = make([]*DupMapEntry, 0) for _, intf := range fr.Interfaces { pbflowRecord.DupList = append(pbflowRecord.DupList, &DupMapEntry{ @@ -195,6 +202,13 @@ func PBToFlow(pb *Record) *model.Record { if pb.IpsecEncrypted != 0 { out.Metrics.AdditionalMetrics.IpsecEncrypted = true } + if pb.Quic != nil { + out.Metrics.QuicMetrics = &ebpf.BpfQuicMetrics{ + Version: pb.Quic.Version, + SeenLongHdr: uint8(pb.Quic.SeenLongHdr), + SeenShortHdr: uint8(pb.Quic.SeenShortHdr), + } + } if len(pb.GetDupList()) != 0 { for _, entry := range pb.GetDupList() { out.Interfaces = append(out.Interfaces, model.IntfDirUdn{ diff --git a/pkg/tracer/tracer.go b/pkg/tracer/tracer.go index 8fb737b78..f1d367db3 100644 --- a/pkg/tracer/tracer.go +++ b/pkg/tracer/tracer.go @@ -46,6 +46,7 @@ const ( pcaRecordsMap = "packet_record" ipsecInputMap = "ipsec_ingress_map" ipsecOutputMap = "ipsec_egress_map" + quicFlowsMap = "quic_flows" // constants defined in flows.c as "volatile const" constSampling = "sampling" constHasFilterSampling = "has_filter_sampling" @@ -70,6 +71,7 @@ const ( constEnableOpenSSLTracking = "enable_openssl_tracking" sslDataEventMap = "ssl_data_event_map" dnsNameMap = "dns_name_map" + constEnableQUICTracking = "enable_quic_tracking" ) const ( @@ -130,9 +132,12 @@ type FlowFetcherConfig struct { UseEbpfManager bool BpfManBpfFSPath string EnableIPsecTracker bool - FilterConfig []*FilterConfig - EnableOpenSSLTracking bool - OpenSSLPath string + // QUICTrackingMode: + // 0: disabled, 1: enabled (UDP/443 only), 2: enabled (any UDP port) + QUICTrackingMode int + FilterConfig []*FilterConfig + EnableOpenSSLTracking bool + OpenSSLPath string } type variablesMapping struct { @@ -191,6 +196,7 @@ func NewFlowFetcher(cfg *FlowFetcherConfig, m *metrics.Metrics) (*FlowFetcher, e ipsecOutputMap, sslDataEventMap, dnsNameMap, + quicFlowsMap, } { spec.Maps[m].Pinning = 0 } @@ -419,6 +425,9 @@ func NewFlowFetcher(cfg *FlowFetcherConfig, m *metrics.Metrics) (*FlowFetcher, e log.Infof("BPFManager mode: loading DNS name pinned maps") mPath = path.Join(pinDir, dnsNameMap) objects.BpfMaps.DnsNameMap, err = cilium.LoadPinnedMap(mPath, opts) + log.Infof("BPFManager mode: loading QUIC flows pinned maps") + mPath = path.Join(pinDir, quicFlowsMap) + objects.BpfMaps.QuicFlows, err = cilium.LoadPinnedMap(mPath, opts) if err != nil { return nil, fmt.Errorf("failed to load %s: %w", mPath, err) } @@ -942,6 +951,12 @@ func (m *FlowFetcher) Close() error { if err := m.objects.DnsNameMap.Close(); err != nil { errs = append(errs, err) } + if err := m.objects.QuicFlows.Unpin(); err != nil { + errs = append(errs, err) + } + if err := m.objects.QuicFlows.Close(); err != nil { + errs = append(errs, err) + } if len(errs) == 0 { m.objects = nil } @@ -1092,8 +1107,8 @@ func (m *FlowFetcher) LookupAndDeleteMap(met *metrics.Metrics) map[ebpf.BpfFlowI if m.config.EnableDNSTracker { var dns []ebpf.BpfDnsMetrics countDNS := lookupAndDeletePerCPUMap(flows, &dns, m.objects.AggregatedFlowsDns, met, func(flow *model.BpfFlowContent) { - for _, entry := range dns { - flow.AccumulateDNS(&entry) + for i := range dns { + flow.AccumulateDNS(&dns[i]) } }) met.FlowBufferSizeGauge.WithBufferName("dnsmap").Set(float64(countDNS)) @@ -1101,8 +1116,8 @@ func (m *FlowFetcher) LookupAndDeleteMap(met *metrics.Metrics) map[ebpf.BpfFlowI if m.config.EnablePktDrops { var pktDrops []ebpf.BpfPktDropMetrics countDrops := lookupAndDeletePerCPUMap(flows, &pktDrops, m.objects.AggregatedFlowsPktDrop, met, func(flow *model.BpfFlowContent) { - for _, entry := range pktDrops { - flow.AccumulateDrops(&entry) + for i := range pktDrops { + flow.AccumulateDrops(&pktDrops[i]) } }) met.FlowBufferSizeGauge.WithBufferName("pktdropsmap").Set(float64(countDrops)) @@ -1110,8 +1125,8 @@ func (m *FlowFetcher) LookupAndDeleteMap(met *metrics.Metrics) map[ebpf.BpfFlowI if m.config.EnableNetworkEventsMonitoring { var netev []ebpf.BpfNetworkEventsMetrics countNetEv := lookupAndDeletePerCPUMap(flows, &netev, m.objects.AggregatedFlowsNetworkEvents, met, func(flow *model.BpfFlowContent) { - for _, entry := range netev { - flow.AccumulateNetworkEvents(&entry) + for i := range netev { + flow.AccumulateNetworkEvents(&netev[i]) } }) met.FlowBufferSizeGauge.WithBufferName("networkeventsmap").Set(float64(countNetEv)) @@ -1119,8 +1134,8 @@ func (m *FlowFetcher) LookupAndDeleteMap(met *metrics.Metrics) map[ebpf.BpfFlowI if m.config.EnablePktTranslation { var xlat []ebpf.BpfXlatMetrics countXlat := lookupAndDeletePerCPUMap(flows, &xlat, m.objects.AggregatedFlowsXlat, met, func(flow *model.BpfFlowContent) { - for _, entry := range xlat { - flow.AccumulateXlat(&entry) + for i := range xlat { + flow.AccumulateXlat(&xlat[i]) } }) met.FlowBufferSizeGauge.WithBufferName("xlatmap").Set(float64(countXlat)) @@ -1128,12 +1143,57 @@ func (m *FlowFetcher) LookupAndDeleteMap(met *metrics.Metrics) map[ebpf.BpfFlowI if m.config.EnableRTT || m.config.EnableIPsecTracker { var addit []ebpf.BpfAdditionalMetrics countAddit := lookupAndDeletePerCPUMap(flows, &addit, m.objects.AdditionalFlowMetrics, met, func(flow *model.BpfFlowContent) { - for _, entry := range addit { - flow.AccumulateAdditional(&entry) + for i := range addit { + flow.AccumulateAdditional(&addit[i]) } }) met.FlowBufferSizeGauge.WithBufferName("additionalmap").Set(float64(countAddit)) } + if m.config.QUICTrackingMode != 0 { + var quic []ebpf.BpfQuicMetrics + countQuic := lookupAndDeletePerCPUMap(flows, &quic, m.objects.QuicFlows, met, func(flow *model.BpfFlowContent) { + for i := range quic { + flow.AccumulateQuic(&quic[i]) + } + }) + met.FlowBufferSizeGauge.WithBufferName("quicmap").Set(float64(countQuic)) + + if m.config.Debug { + logged := 0 + const maxLogged = 10 + var b strings.Builder + for id, f := range flows { + if logged >= maxLogged { + break + } + if f.QuicMetrics == nil { + continue + } + qm := f.QuicMetrics + if qm.SeenLongHdr == 0 && qm.SeenShortHdr == 0 && qm.Version == 0 { + continue + } + if logged > 0 { + b.WriteString(" | ") + } + // Format: src>dst p= v= lh= sh= + b.WriteString(fmt.Sprintf( + "%s:%d>%s:%d p=%d v=%d lh=%d sh=%d", + model.IP(model.IPAddr(id.SrcIp)).String(), id.SrcPort, + model.IP(model.IPAddr(id.DstIp)).String(), id.DstPort, + id.TransportProtocol, + qm.Version, qm.SeenLongHdr, qm.SeenShortHdr, + )) + logged++ + } + if logged > 0 { + log.WithFields(logrus.Fields{ + "quicFlowsLogged": logged, + "quicFlowsSample": b.String(), + }).Debug("QUIC flow metrics sample") + } + } + } met.FlowBufferSizeGauge.WithBufferName("flowmap").Set(float64(countMain)) met.FlowBufferSizeGauge.WithBufferName("merged-maps").Set(float64(len(flows))) @@ -1329,6 +1389,7 @@ func kernelSpecificLoadAndAssign(oldKernel, rtKernel, supportNetworkEvents bool, IpsecEgressMap: newObjects.IpsecEgressMap, SslDataEventMap: newObjects.SslDataEventMap, DnsNameMap: newObjects.DnsNameMap, + QuicFlows: newObjects.QuicFlows, }, } @@ -1399,6 +1460,7 @@ func kernelSpecificLoadAndAssign(oldKernel, rtKernel, supportNetworkEvents bool, IpsecEgressMap: newObjects.IpsecEgressMap, SslDataEventMap: newObjects.SslDataEventMap, DnsNameMap: newObjects.DnsNameMap, + QuicFlows: newObjects.QuicFlows, }, } @@ -1469,6 +1531,7 @@ func kernelSpecificLoadAndAssign(oldKernel, rtKernel, supportNetworkEvents bool, IpsecEgressMap: newObjects.IpsecEgressMap, SslDataEventMap: newObjects.SslDataEventMap, DnsNameMap: newObjects.DnsNameMap, + QuicFlows: newObjects.QuicFlows, }, } @@ -1539,6 +1602,7 @@ func kernelSpecificLoadAndAssign(oldKernel, rtKernel, supportNetworkEvents bool, IpsecEgressMap: newObjects.IpsecEgressMap, SslDataEventMap: newObjects.SslDataEventMap, DnsNameMap: newObjects.DnsNameMap, + QuicFlows: newObjects.QuicFlows, }, } @@ -1615,6 +1679,7 @@ func NewPacketFetcher(cfg *FlowFetcherConfig) (*PacketFetcher, error) { ipsecOutputMap, sslDataEventMap, dnsNameMap, + quicFlowsMap, } { spec.Maps[m].Pinning = 0 } @@ -1645,6 +1710,7 @@ func NewPacketFetcher(cfg *FlowFetcherConfig) (*PacketFetcher, error) { delete(spec.Programs, additionalFlowMetrics) delete(spec.Programs, ipsecInputMap) delete(spec.Programs, ipsecOutputMap) + delete(spec.Programs, quicFlowsMap) delete(spec.Programs, constSampling) delete(spec.Programs, constHasFilterSampling) delete(spec.Programs, constTraceMessages) @@ -1658,6 +1724,7 @@ func NewPacketFetcher(cfg *FlowFetcherConfig) (*PacketFetcher, error) { delete(spec.Programs, constEnableIPsec) delete(spec.Programs, constEnableOpenSSLTracking) delete(spec.Programs, dnsNameMap) + delete(spec.Programs, constEnableQUICTracking) if err := spec.LoadAndAssign(&newObjects, &cilium.CollectionOptions{Maps: cilium.MapOptions{PinPath: ""}}); err != nil { var ve *cilium.VerifierError @@ -2154,6 +2221,16 @@ func configureFlowSpecVariables(spec *cilium.CollectionSpec, cfg *FlowFetcherCon if cfg.EnableOpenSSLTracking { enableOpenSSLTracking = 1 } + + // enable_quic_tracking mode: + // QUIC_CONFIG_DISABLED = 0, QUIC_CONFIG_ENABLED = 1, QUIC_CONFIG_ANY_UDP_PORT = 2. + enableQUICTracking := ebpf.BpfQuicConfigTQUIC_CONFIG_DISABLED + switch cfg.QUICTrackingMode { + case 2: + enableQUICTracking = ebpf.BpfQuicConfigTQUIC_CONFIG_ANY_UDP_PORT + case 1: + enableQUICTracking = ebpf.BpfQuicConfigTQUIC_CONFIG_ENABLED + } // When adding constants here, remember to delete them in NewPacketFetcher variables := []variablesMapping{ {constSampling, uint32(cfg.Sampling)}, @@ -2168,6 +2245,7 @@ func configureFlowSpecVariables(spec *cilium.CollectionSpec, cfg *FlowFetcherCon {constEnablePktTranslation, uint8(enablePktTranslation)}, {constEnableIPsec, uint8(enableIPsec)}, {constEnableOpenSSLTracking, uint8(enableOpenSSLTracking)}, + {constEnableQUICTracking, uint8(enableQUICTracking)}, } for _, mapping := range variables { diff --git a/proto/flow.proto b/proto/flow.proto index e0758848f..f575db987 100644 --- a/proto/flow.proto +++ b/proto/flow.proto @@ -70,6 +70,7 @@ message Record { uint32 ipsec_encrypted = 30; int32 ipsec_encrypted_ret = 31; string dns_name = 32; + Quic quic = 33; } message DataLink { @@ -112,3 +113,9 @@ message Xlat { uint32 dst_port = 4; uint32 zone_id = 5; } + +message Quic { + uint32 version = 1; + uint32 seen_long_hdr = 2; + uint32 seen_short_hdr = 3; +} \ No newline at end of file diff --git a/scripts/agent.yml b/scripts/agent.yml index 77e463971..060c51646 100644 --- a/scripts/agent.yml +++ b/scripts/agent.yml @@ -52,6 +52,8 @@ spec: value: "true" - name: OPENSSL_PATH value: "/usr/lib/aarch64-linux-gnu/libssl.so.1.1" + - name: QUIC_TRACKING_MODE + value: "1" volumeMounts: - name: bpf-kernel-debug mountPath: /sys/kernel/debug