From cfaf5ee0ba972b929169d2e1d24b61e0b2f50bd2 Mon Sep 17 00:00:00 2001 From: Sylvain Afchain Date: Mon, 25 Nov 2024 14:22:17 +0100 Subject: [PATCH] [CWS] add kernel bpf filter for raw packet (#30288) --- LICENSE-3rdparty.csv | 1 + go.mod | 3 +- go.sum | 6 +- .../ebpf/c/include/constants/custom.h | 8 +- pkg/security/ebpf/c/include/helpers/network.h | 5 - .../ebpf/c/include/hooks/network/dns.h | 4 +- .../ebpf/c/include/hooks/network/raw.h | 55 ++-- .../ebpf/c/include/hooks/network/router.h | 4 +- .../ebpf/c/include/hooks/network/tc.h | 39 ++- pkg/security/ebpf/c/include/maps.h | 5 +- pkg/security/ebpf/c/include/structs/network.h | 2 +- .../tests/activity_dump_ratelimiter_test.h | 6 +- pkg/security/ebpf/c/include/tests/baloum.h | 1 + .../ebpf/c/include/tests/discarders_test.h | 8 +- .../ebpf/c/include/tests/raw_packet_test.h | 30 +++ pkg/security/ebpf/c/include/tests/tests.h | 1 + pkg/security/ebpf/probes/all.go | 8 +- pkg/security/ebpf/probes/const.go | 15 +- pkg/security/ebpf/probes/raw_packet.go | 15 ++ .../ebpf/probes/rawpacket/bpffilter.go | 19 ++ pkg/security/ebpf/probes/rawpacket/pcap.go | 251 ++++++++++++++++++ .../ebpf/probes/rawpacket/pcap_unsupported.go | 39 +++ pkg/security/ebpf/probes/tc.go | 49 ++-- .../tests/activity_dump_ratelimiter_test.go | 8 +- pkg/security/ebpf/tests/discarders_test.go | 8 +- pkg/security/ebpf/tests/raw_packet_test.go | 194 ++++++++++++++ pkg/security/probe/model_ebpf.go | 10 +- pkg/security/probe/probe_ebpf.go | 121 +++++++-- ...ilter_unix.go => oo_packet_filter_unix.go} | 14 +- ...ted.go => oo_packet_filter_unsupported.go} | 0 pkg/security/tests/network_test.go | 2 +- tasks/security_agent.py | 17 +- 32 files changed, 805 insertions(+), 143 deletions(-) create mode 100644 pkg/security/ebpf/c/include/tests/raw_packet_test.h create mode 100644 pkg/security/ebpf/probes/raw_packet.go create mode 100644 pkg/security/ebpf/probes/rawpacket/bpffilter.go create mode 100644 pkg/security/ebpf/probes/rawpacket/pcap.go create mode 100644 pkg/security/ebpf/probes/rawpacket/pcap_unsupported.go create mode 100644 pkg/security/ebpf/tests/raw_packet_test.go rename pkg/security/secl/model/{packet_filter_unix.go => oo_packet_filter_unix.go} (83%) rename pkg/security/secl/model/{packet_filter_unsupported.go => oo_packet_filter_unsupported.go} (100%) diff --git a/LICENSE-3rdparty.csv b/LICENSE-3rdparty.csv index 0c14f7e40d15b..3bd5a574fa3fe 100644 --- a/LICENSE-3rdparty.csv +++ b/LICENSE-3rdparty.csv @@ -690,6 +690,7 @@ core,github.com/cilium/ebpf/perf,MIT,"Copyright (c) 2017 Nathan Sweet | Copyrigh core,github.com/cilium/ebpf/ringbuf,MIT,"Copyright (c) 2017 Nathan Sweet | Copyright (c) 2018, 2019 Cloudflare | Copyright (c) 2019 Authors of Cilium" core,github.com/cilium/ebpf/rlimit,MIT,"Copyright (c) 2017 Nathan Sweet | Copyright (c) 2018, 2019 Cloudflare | Copyright (c) 2019 Authors of Cilium" core,github.com/clbanning/mxj,MIT,Copyright (c) 2012-2016 Charles Banning . All rights reserved | Copyright 2009 The Go Authors. All rights reserved +core,github.com/cloudflare/cbpfc,BSD-3-Clause,"Copyright (c) 2019, Cloudflare. All rights reserved" core,github.com/cloudflare/circl/dh/x25519,BSD-3-Clause,Copyright (c) 2009 The Go Authors. All rights reserved | Copyright (c) 2019 Cloudflare. All rights reserved core,github.com/cloudflare/circl/dh/x448,BSD-3-Clause,Copyright (c) 2009 The Go Authors. All rights reserved | Copyright (c) 2019 Cloudflare. All rights reserved core,github.com/cloudflare/circl/ecc/goldilocks,BSD-3-Clause,Copyright (c) 2009 The Go Authors. All rights reserved | Copyright (c) 2019 Cloudflare. All rights reserved diff --git a/go.mod b/go.mod index 26930c22eacb8..d39bc25dbbadd 100644 --- a/go.mod +++ b/go.mod @@ -510,7 +510,7 @@ require ( github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rs/cors v1.11.1 // indirect - github.com/safchain/baloum v0.0.0-20221229104256-b1fc8f70a86b + github.com/safchain/baloum v0.0.0-20241120122234-f22c9bd19f3b github.com/saracen/walker v0.1.3 // indirect github.com/sassoftware/go-rpmutils v0.3.0 // indirect github.com/secure-systems-lab/go-securesystemslib v0.8.0 // indirect @@ -604,6 +604,7 @@ require ( github.com/DataDog/datadog-agent/pkg/util/defaultpaths v0.0.0-00010101000000-000000000000 github.com/DataDog/datadog-agent/pkg/util/utilizationtracker v0.0.0 github.com/NVIDIA/go-nvml v0.12.4-0 + github.com/cloudflare/cbpfc v0.0.0-20240920015331-ff978e94500b github.com/containerd/containerd/api v1.8.0 github.com/containerd/errdefs v1.0.0 github.com/distribution/reference v0.6.0 diff --git a/go.sum b/go.sum index 799c0bcda1aba..a8cc5f6743724 100644 --- a/go.sum +++ b/go.sum @@ -463,6 +463,8 @@ github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I= github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/cbpfc v0.0.0-20240920015331-ff978e94500b h1:EgR1t4Lnq6uP6QxJQ+oIFtENOHUY3/7gMOE76vL0KcA= +github.com/cloudflare/cbpfc v0.0.0-20240920015331-ff978e94500b/go.mod h1:X/9cHz8JVzKlvoZyKBgMgrogKZlLf+pWjmm5gSUm5dI= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/cloudfoundry-community/go-cfclient/v2 v2.0.1-0.20230503155151-3d15366c5820 h1:ixkQUDJYG6eSxgUEl6LLE2l2TD2C5AYmlm+fVhsr6Zs= @@ -1638,8 +1640,8 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= -github.com/safchain/baloum v0.0.0-20221229104256-b1fc8f70a86b h1:cTiH46CYvPhgOlE0t82N+rgQw44b7vB39ay+P+wiVz8= -github.com/safchain/baloum v0.0.0-20221229104256-b1fc8f70a86b/go.mod h1:1+GWOH32bsIEAHknYja6/H1efcDs+/Q2XrtYMM200Ho= +github.com/safchain/baloum v0.0.0-20241120122234-f22c9bd19f3b h1:ZeznXGJOGRRGKuU7GEUmNobE4swH0PbMqukrQS3XCLE= +github.com/safchain/baloum v0.0.0-20241120122234-f22c9bd19f3b/go.mod h1:azfM30OkV7er0g2EIbpI+Jl4P6T5RMpsED0+7Up/Gog= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= diff --git a/pkg/security/ebpf/c/include/constants/custom.h b/pkg/security/ebpf/c/include/constants/custom.h index dd522026b761c..88be17fa3c80b 100644 --- a/pkg/security/ebpf/c/include/constants/custom.h +++ b/pkg/security/ebpf/c/include/constants/custom.h @@ -61,12 +61,14 @@ enum DENTRY_ERPC_RESOLUTION_CODE enum TC_TAIL_CALL_KEYS { - UNKNOWN, - DNS_REQUEST, + DNS_REQUEST = 1, DNS_REQUEST_PARSER, IMDS_REQUEST, - RAW_PACKET, +}; + +enum TC_RAWPACKET_KEYS { RAW_PACKET_FILTER, + // reserved keys for raw packet filter tail calls }; #define DNS_MAX_LENGTH 256 diff --git a/pkg/security/ebpf/c/include/helpers/network.h b/pkg/security/ebpf/c/include/helpers/network.h index 0899d6e88e30c..1198e49d41071 100644 --- a/pkg/security/ebpf/c/include/helpers/network.h +++ b/pkg/security/ebpf/c/include/helpers/network.h @@ -82,10 +82,6 @@ __attribute__((always_inline)) void fill_network_context(struct network_context_ fill_network_device_context(&net_ctx->device, skb, pkt); } -__attribute__((always_inline)) void tail_call_to_classifier(struct __sk_buff *skb, int classifier_id) { - bpf_tail_call_compat(skb, &classifier_router, classifier_id); -} - __attribute__((always_inline)) void parse_tuple(struct nf_conntrack_tuple *tuple, struct flow_t *flow) { flow->sport = tuple->src.u.all; flow->dport = tuple->dst.u.all; @@ -94,7 +90,6 @@ __attribute__((always_inline)) void parse_tuple(struct nf_conntrack_tuple *tuple bpf_probe_read(&flow->daddr, sizeof(flow->daddr), &tuple->dst.u3.all); } - __attribute__((always_inline)) struct packet_t * parse_packet(struct __sk_buff *skb, int direction) { struct cursor c = {}; tc_cursor_init(&c, skb); diff --git a/pkg/security/ebpf/c/include/hooks/network/dns.h b/pkg/security/ebpf/c/include/hooks/network/dns.h index d6f40c2277ea5..46fd79393fa7d 100644 --- a/pkg/security/ebpf/c/include/hooks/network/dns.h +++ b/pkg/security/ebpf/c/include/hooks/network/dns.h @@ -80,7 +80,7 @@ int classifier_dns_request(struct __sk_buff *skb) { evt->id = htons(header.id); // tail call to the dns request parser - tail_call_to_classifier(skb, DNS_REQUEST_PARSER); + bpf_tail_call_compat(skb, &classifier_router, DNS_REQUEST_PARSER); // tail call failed, ignore packet return ACT_OK; @@ -116,7 +116,7 @@ int classifier_dns_request_parser(struct __sk_buff *skb) { send_event_with_size_ptr(skb, EVENT_DNS, evt, offsetof(struct dns_event_t, name) + qname_length); if (!is_dns_request_parsing_done(skb, pkt)) { - tail_call_to_classifier(skb, DNS_REQUEST_PARSER); + bpf_tail_call_compat(skb, &classifier_router, DNS_REQUEST_PARSER); } return ACT_OK; diff --git a/pkg/security/ebpf/c/include/hooks/network/raw.h b/pkg/security/ebpf/c/include/hooks/network/raw.h index 2b707952a177a..6f46f6b4eb1a2 100644 --- a/pkg/security/ebpf/c/include/hooks/network/raw.h +++ b/pkg/security/ebpf/c/include/hooks/network/raw.h @@ -4,61 +4,44 @@ #include "helpers/network.h" #include "perf_ring.h" -__attribute__((always_inline)) struct raw_packet_t *get_raw_packet_event() { +__attribute__((always_inline)) struct raw_packet_event_t *get_raw_packet_event() { u32 key = 0; - return bpf_map_lookup_elem(&raw_packets, &key); + return bpf_map_lookup_elem(&raw_packet_event, &key); } -SEC("classifier/raw_packet") -int classifier_raw_packet(struct __sk_buff *skb) { +SEC("classifier/raw_packet_sender") +int classifier_raw_packet_sender(struct __sk_buff *skb) { struct packet_t *pkt = get_packet(); if (pkt == NULL) { // should never happen return ACT_OK; } - struct raw_packet_t *evt = get_raw_packet_event(); - if ((evt == NULL) || (skb == NULL)) { + struct raw_packet_event_t *evt = get_raw_packet_event(); + if (evt == NULL || skb == NULL || evt->len == 0) { // should never happen return ACT_OK; } - bpf_skb_pull_data(skb, 0); + // process context + fill_network_process_context(&evt->process, pkt); - u32 len = *(u32 *)(skb + offsetof(struct __sk_buff, len)); - if (len > sizeof(evt->data)) { - len = sizeof(evt->data); + struct proc_cache_t *entry = get_proc_cache(evt->process.pid); + if (entry == NULL) { + evt->container.container_id[0] = 0; + } else { + copy_container_id_no_tracing(entry->container.container_id, &evt->container.container_id); } - // NOTE(safchain) inline asm because clang isn't generating the proper instructions for : - // if (len == 0) return ACT_OK; - /*asm ("r4 = %[len]\n" - "if r4 > 0 goto + 2\n" - "r0 = 0\n" - "exit\n" :: [len]"r"((u64)len));*/ - - if (len > 1) { - if (bpf_skb_load_bytes(skb, 0, evt->data, len) < 0) { - return ACT_OK; - } - evt->len = skb->len; - - // process context - fill_network_process_context(&evt->process, pkt); - - struct proc_cache_t *entry = get_proc_cache(evt->process.pid); - if (entry == NULL) { - evt->container.container_id[0] = 0; - } else { - copy_container_id_no_tracing(entry->container.container_id, &evt->container.container_id); - } + fill_network_device_context(&evt->device, skb, pkt); - fill_network_device_context(&evt->device, skb, pkt); - - u32 size = offsetof(struct raw_packet_t, data) + len; - send_event_with_size_ptr(skb, EVENT_RAW_PACKET, evt, size); + u32 len = evt->len; + if (len > sizeof(evt->data)) { + len = sizeof(evt->data); } + send_event_with_size_ptr(skb, EVENT_RAW_PACKET, evt, offsetof(struct raw_packet_event_t, data) + len); + return ACT_OK; } diff --git a/pkg/security/ebpf/c/include/hooks/network/router.h b/pkg/security/ebpf/c/include/hooks/network/router.h index e2b8361869c46..93cca5f4889ee 100644 --- a/pkg/security/ebpf/c/include/hooks/network/router.h +++ b/pkg/security/ebpf/c/include/hooks/network/router.h @@ -9,14 +9,14 @@ __attribute__((always_inline)) int route_pkt(struct __sk_buff *skb, struct packe // route DNS requests if (is_event_enabled(EVENT_DNS)) { if (pkt->l4_protocol == IPPROTO_UDP && pkt->translated_ns_flow.flow.dport == htons(53)) { - tail_call_to_classifier(skb, DNS_REQUEST); + bpf_tail_call_compat(skb, &classifier_router, DNS_REQUEST); } } // route IMDS requests if (is_event_enabled(EVENT_IMDS)) { if (pkt->l4_protocol == IPPROTO_TCP && ((pkt->ns_flow.flow.saddr[0] & 0xFFFFFFFF) == get_imds_ip() || (pkt->ns_flow.flow.daddr[0] & 0xFFFFFFFF) == get_imds_ip())) { - tail_call_to_classifier(skb, IMDS_REQUEST); + bpf_tail_call_compat(skb, &classifier_router, IMDS_REQUEST); } } diff --git a/pkg/security/ebpf/c/include/hooks/network/tc.h b/pkg/security/ebpf/c/include/hooks/network/tc.h index 92222aff85474..2bb8f8b5791c8 100644 --- a/pkg/security/ebpf/c/include/hooks/network/tc.h +++ b/pkg/security/ebpf/c/include/hooks/network/tc.h @@ -26,6 +26,33 @@ int classifier_egress(struct __sk_buff *skb) { return route_pkt(skb, pkt, EGRESS); }; +__attribute__((always_inline)) int prepare_raw_packet_event(struct __sk_buff *skb) { + struct raw_packet_event_t *evt = get_raw_packet_event(); + if (evt == NULL) { + // should never happen + return ACT_OK; + } + + bpf_skb_pull_data(skb, 0); + + u32 len = *(u32 *)(skb + offsetof(struct __sk_buff, len)); + if (len > sizeof(evt->data)) { + len = sizeof(evt->data); + } + + if (len > 1) { + if (bpf_skb_load_bytes(skb, 0, evt->data, len) < 0) { + return ACT_OK; + } + evt->len = skb->len; + } else { + evt->len = 0; + } + + return ACT_OK; +} + + SEC("classifier/ingress") int classifier_raw_packet_ingress(struct __sk_buff *skb) { struct packet_t *pkt = parse_packet(skb, INGRESS); @@ -33,7 +60,11 @@ int classifier_raw_packet_ingress(struct __sk_buff *skb) { return ACT_OK; } - tail_call_to_classifier(skb, RAW_PACKET_FILTER); + if (prepare_raw_packet_event(skb) != ACT_OK) { + return ACT_OK; + } + + bpf_tail_call_compat(skb, &raw_packet_classifier_router, RAW_PACKET_FILTER); return ACT_OK; }; @@ -45,7 +76,11 @@ int classifier_raw_packet_egress(struct __sk_buff *skb) { return ACT_OK; } - tail_call_to_classifier(skb, RAW_PACKET_FILTER); + if (prepare_raw_packet_event(skb) != ACT_OK) { + return ACT_OK; + } + + bpf_tail_call_compat(skb, &raw_packet_classifier_router, RAW_PACKET_FILTER); return ACT_OK; }; diff --git a/pkg/security/ebpf/c/include/maps.h b/pkg/security/ebpf/c/include/maps.h index 02974e0286dd5..c5050fd5545c7 100644 --- a/pkg/security/ebpf/c/include/maps.h +++ b/pkg/security/ebpf/c/include/maps.h @@ -88,14 +88,15 @@ BPF_PERCPU_ARRAY_MAP(packets, struct packet_t, 1) BPF_PERCPU_ARRAY_MAP(selinux_write_buffer, struct selinux_write_buffer_t, 1) BPF_PERCPU_ARRAY_MAP(is_new_kthread, u32, 1) BPF_PERCPU_ARRAY_MAP(syscalls_stats, struct syscalls_stats_t, EVENT_MAX) -BPF_PERCPU_ARRAY_MAP(raw_packets, struct raw_packet_t, 1) +BPF_PERCPU_ARRAY_MAP(raw_packet_event, struct raw_packet_event_t, 1) BPF_PROG_ARRAY(args_envs_progs, 3) BPF_PROG_ARRAY(dentry_resolver_kprobe_or_fentry_callbacks, EVENT_MAX) BPF_PROG_ARRAY(dentry_resolver_tracepoint_callbacks, EVENT_MAX) BPF_PROG_ARRAY(dentry_resolver_kprobe_or_fentry_progs, 6) BPF_PROG_ARRAY(dentry_resolver_tracepoint_progs, 3) -BPF_PROG_ARRAY(classifier_router, 100) +BPF_PROG_ARRAY(classifier_router, 10) BPF_PROG_ARRAY(sys_exit_progs, 64) +BPF_PROG_ARRAY(raw_packet_classifier_router, 32) #endif diff --git a/pkg/security/ebpf/c/include/structs/network.h b/pkg/security/ebpf/c/include/structs/network.h index d212e09d1b3c9..9efed0aa257b3 100644 --- a/pkg/security/ebpf/c/include/structs/network.h +++ b/pkg/security/ebpf/c/include/structs/network.h @@ -83,7 +83,7 @@ struct network_context_t { u16 l4_protocol; }; -struct raw_packet_t { +struct raw_packet_event_t { struct kevent_t event; struct process_context_t process; struct span_context_t span; diff --git a/pkg/security/ebpf/c/include/tests/activity_dump_ratelimiter_test.h b/pkg/security/ebpf/c/include/tests/activity_dump_ratelimiter_test.h index f65e82e941805..6d6ab486f8011 100644 --- a/pkg/security/ebpf/c/include/tests/activity_dump_ratelimiter_test.h +++ b/pkg/security/ebpf/c/include/tests/activity_dump_ratelimiter_test.h @@ -39,7 +39,7 @@ int test_ad_ratelimiter_basic() { assert_zero(activity_dump_rate_limiter_allow(&config, cookie, now, 0), "event allowed which should not be"); } - return 0; + return 1; } SEC("test/ad_ratelimiter_basic_half") @@ -73,7 +73,7 @@ int test_ad_ratelimiter_basic_half() { assert_zero(activity_dump_rate_limiter_allow(&config, cookie, now, 0), "event allowed which should not be"); } - return 0; + return 1; } __attribute__((always_inline)) int test_ad_ratelimiter_variable_droprate(int algo) { @@ -106,7 +106,7 @@ __attribute__((always_inline)) int test_ad_ratelimiter_variable_droprate(int alg assert_greater_than(total_allowed, AD_RL_TEST_RATE * 3 / 4, "nope"); assert_lesser_than(total_allowed, AD_RL_TEST_RATE / 10, "nope"); } - return 0; + return 1; } SEC("test/ad_ratelimiter_decreasing_droprate") diff --git a/pkg/security/ebpf/c/include/tests/baloum.h b/pkg/security/ebpf/c/include/tests/baloum.h index 5b6f263c18b73..4128525735140 100644 --- a/pkg/security/ebpf/c/include/tests/baloum.h +++ b/pkg/security/ebpf/c/include/tests/baloum.h @@ -15,6 +15,7 @@ static int (*baloum_call)(struct baloum_ctx *ctx, const char *section) = (void * static int (*baloum_strcmp)(const char *s1, const char *s2) = (void *)0xfffd; static int (*baloum_memcmp)(const void *b1, const void *b2, __u32 size) = (void *)0xfffc; static int (*baloum_sleep)(__u64 ns) = (void *)0xfffb; +static int (*baloum_memcpy)(const void *b1, const void *b2, __u32 size) = (void *)0xfffa; #define assert_memcmp(b1, b2, s, msg) \ if (baloum_memcmp(b1, b2, s) != 0) { \ diff --git a/pkg/security/ebpf/c/include/tests/discarders_test.h b/pkg/security/ebpf/c/include/tests/discarders_test.h index 6738e64eca392..24970f491920a 100644 --- a/pkg/security/ebpf/c/include/tests/discarders_test.h +++ b/pkg/security/ebpf/c/include/tests/discarders_test.h @@ -53,7 +53,7 @@ int test_discarders_event_mask() { ret = _is_discarded_by_inode(EVENT_CHMOD, mount_id, inode); assert_not_zero(ret, "inode should be discarded"); - return 0; + return 1; } SEC("test/discarders_retention") @@ -93,7 +93,7 @@ int test_discarders_retention() { ret = _is_discarded_by_inode(EVENT_OPEN, mount_id, inode); assert_not_zero(ret, "inode should be discarded"); - return 0; + return 1; } SEC("test/discarders_revision") @@ -142,7 +142,7 @@ int test_discarders_revision() { ret = _is_discarded_by_inode(EVENT_OPEN, mount_id1, inode1); assert_not_zero(ret, "inode should be discarded"); - return 0; + return 1; } SEC("test/discarders_mount_revision") @@ -183,7 +183,7 @@ int test_discarders_mount_revision() { ret = _is_discarded_by_inode(EVENT_OPEN, mount_id1, inode1); assert_not_zero(ret, "inode should be discarded"); - return 0; + return 1; } #endif diff --git a/pkg/security/ebpf/c/include/tests/raw_packet_test.h b/pkg/security/ebpf/c/include/tests/raw_packet_test.h new file mode 100644 index 0000000000000..a00f55225b6ea --- /dev/null +++ b/pkg/security/ebpf/c/include/tests/raw_packet_test.h @@ -0,0 +1,30 @@ +#ifndef _RAW_PACKET_TEST_H +#define _RAW_PACKET_TEST_H + +#include "helpers/network.h" +#include "baloum.h" + +SEC("test/raw_packet_tail_calls") +int raw_packet_tail_calls(struct __sk_buff *skb) { + struct raw_packet_event_t *evt = get_raw_packet_event(); + assert_not_null(evt, "unable to get raw packet event") + + // tcp dst port 5555 and tcp[tcpflags] == tcp-syn + unsigned char data[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x45, 0x10, + 0x00, 0x30, 0xf4, 0xa2, 0x40, 0x00, 0x40, 0x06, + 0x48, 0x13, 0x7f, 0x00, 0x00, 0x01, 0x7f, 0x00, + 0x00, 0x01, 0xa2, 0x36, 0x15, 0xb3, 0x1c, 0x5b, + 0x89, 0x33, 0x00, 0x00, 0x00, 0x00, 0x70, 0x02, + 0xff, 0xd7, 0xfe, 0x24, 0x00, 0x00, 0x02, 0x04, + 0xff, 0xd7, 0x01, 0x03, 0x03, 0x07 + }; + baloum_memcpy(evt->data, data, sizeof(data)); + + bpf_tail_call_compat(skb, &raw_packet_classifier_router, RAW_PACKET_FILTER); + + return 1; +} + +#endif diff --git a/pkg/security/ebpf/c/include/tests/tests.h b/pkg/security/ebpf/c/include/tests/tests.h index 9e9652d486857..2abe84b70854f 100644 --- a/pkg/security/ebpf/c/include/tests/tests.h +++ b/pkg/security/ebpf/c/include/tests/tests.h @@ -3,5 +3,6 @@ #include "discarders_test.h" #include "activity_dump_ratelimiter_test.h" +#include "raw_packet_test.h" #endif diff --git a/pkg/security/ebpf/probes/all.go b/pkg/security/ebpf/probes/all.go index e501a61ff926f..770d883b64dd7 100644 --- a/pkg/security/ebpf/probes/all.go +++ b/pkg/security/ebpf/probes/all.go @@ -131,7 +131,7 @@ func AllMaps() []*manager.Map { {Name: "syscalls_stats_enabled"}, {Name: "kill_list"}, // used by raw packet filters - {Name: "packets"}, + {Name: "raw_packet_event"}, } } @@ -252,14 +252,14 @@ func AllRingBuffers() []*manager.RingBuffer { } // AllTailRoutes returns the list of all the tail call routes -func AllTailRoutes(ERPCDentryResolutionEnabled, networkEnabled, supportMmapableMaps bool) []manager.TailCallRoute { +func AllTailRoutes(eRPCDentryResolutionEnabled, networkEnabled, rawPacketEnabled, supportMmapableMaps bool) []manager.TailCallRoute { var routes []manager.TailCallRoute routes = append(routes, getExecTailCallRoutes()...) - routes = append(routes, getDentryResolverTailCallRoutes(ERPCDentryResolutionEnabled, supportMmapableMaps)...) + routes = append(routes, getDentryResolverTailCallRoutes(eRPCDentryResolutionEnabled, supportMmapableMaps)...) routes = append(routes, getSysExitTailCallRoutes()...) if networkEnabled { - routes = append(routes, getTCTailCallRoutes()...) + routes = append(routes, getTCTailCallRoutes(rawPacketEnabled)...) } return routes diff --git a/pkg/security/ebpf/probes/const.go b/pkg/security/ebpf/probes/const.go index 7b10d86ca29ae..3d984a02b9b0b 100644 --- a/pkg/security/ebpf/probes/const.go +++ b/pkg/security/ebpf/probes/const.go @@ -77,6 +77,11 @@ const ( DentryResolverCGroupWriteCallbackTracepointKey ) +const ( + // RawPacketFilterMaxTailCall defines the maximum of tail calls + RawPacketFilterMaxTailCall = 5 +) + const ( // TCDNSRequestKey is the key to the DNS request program TCDNSRequestKey uint32 = iota + 1 @@ -84,10 +89,14 @@ const ( TCDNSRequestParserKey // TCIMDSRequestParserKey is the key to the IMDS request program TCIMDSRequestParserKey - // TCRawPacketParserKey is the key to the raw packet program - TCRawPacketParserKey +) + +const ( // TCRawPacketFilterKey is the key to the raw packet filter program - TCRawPacketFilterKey + // reserve 5 tail calls for the filtering + TCRawPacketFilterKey uint32 = iota + // TCRawPacketParserSenderKey is the key to the raw packet sender program + TCRawPacketParserSenderKey = TCRawPacketFilterKey + RawPacketFilterMaxTailCall // reserved key for filter tail calls ) const ( diff --git a/pkg/security/ebpf/probes/raw_packet.go b/pkg/security/ebpf/probes/raw_packet.go new file mode 100644 index 0000000000000..18dc27434d043 --- /dev/null +++ b/pkg/security/ebpf/probes/raw_packet.go @@ -0,0 +1,15 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +//go:build linux + +// Package probes holds probes related files +package probes + +// RawPacketTCProgram returns the list of TC classifier sections +var RawPacketTCProgram = []string{ + "classifier_raw_packet_egress", + "classifier_raw_packet_ingress", +} diff --git a/pkg/security/ebpf/probes/rawpacket/bpffilter.go b/pkg/security/ebpf/probes/rawpacket/bpffilter.go new file mode 100644 index 0000000000000..b01e57dffa64f --- /dev/null +++ b/pkg/security/ebpf/probes/rawpacket/bpffilter.go @@ -0,0 +1,19 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +//go:build linux + +// Package rawpacket holds rawpacket related files +package rawpacket + +import ( + "github.com/DataDog/datadog-agent/pkg/security/secl/compiler/eval" +) + +// Filter defines a raw packet filter +type Filter struct { + RuleID eval.RuleID + BPFFilter string +} diff --git a/pkg/security/ebpf/probes/rawpacket/pcap.go b/pkg/security/ebpf/probes/rawpacket/pcap.go new file mode 100644 index 0000000000000..8ffc7c451c6ab --- /dev/null +++ b/pkg/security/ebpf/probes/rawpacket/pcap.go @@ -0,0 +1,251 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +//go:build linux && pcap && cgo + +// Package rawpacket holds rawpacket related files +package rawpacket + +import ( + "errors" + "fmt" + + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/asm" + "github.com/cloudflare/cbpfc" + "github.com/google/gopacket/layers" + "github.com/google/gopacket/pcap" + "github.com/hashicorp/go-multierror" + "golang.org/x/net/bpf" + + "github.com/DataDog/datadog-agent/pkg/security/ebpf/probes" +) + +const ( + // progPrefix prefix used for raw packet filter programs + progPrefix = "raw_packet_prog_" + + // packetCaptureSize see kernel definition + packetCaptureSize = 256 +) + +// ProgOpts defines options +type ProgOpts struct { + *cbpfc.EBPFOpts + + // MaxTailCalls maximun number of tail calls generated + MaxTailCalls int + // number of instructions + MaxProgSize int + // Number of nop instruction inserted in each program + NopInstLen int + + // internals + sendEventLabel string + ctxSave asm.Register + tailCallMapFd int +} + +// DefaultProgOpts default options +var DefaultProgOpts = ProgOpts{ + EBPFOpts: &cbpfc.EBPFOpts{ + PacketStart: asm.R1, + PacketEnd: asm.R2, + Result: asm.R3, + Working: [4]asm.Register{ + asm.R4, + asm.R5, + asm.R6, + asm.R7, + }, + StackOffset: 16, // adapt using the stack size used outside of the filter itself, ex: map_lookup + }, + sendEventLabel: "send_event", + ctxSave: asm.R9, + MaxTailCalls: probes.RawPacketFilterMaxTailCall, + MaxProgSize: 4000, +} + +// BPFFilterToInsts compile a bpf filter expression +func BPFFilterToInsts(index int, filter string, opts ProgOpts) (asm.Instructions, error) { + pcapBPF, err := pcap.CompileBPFFilter(layers.LinkTypeEthernet, 256, filter) + if err != nil { + return nil, err + } + bpfInsts := make([]bpf.Instruction, len(pcapBPF)) + for i, ri := range pcapBPF { + bpfInsts[i] = bpf.RawInstruction{Op: ri.Code, Jt: ri.Jt, Jf: ri.Jf, K: ri.K}.Disassemble() + } + + var cbpfcOpts cbpfc.EBPFOpts + if opts.EBPFOpts != nil { + // make a copy so that we can modify the labels + cbpfcOpts = *opts.EBPFOpts + } + cbpfcOpts.LabelPrefix = fmt.Sprintf("cbpfc_%d_", index) + cbpfcOpts.ResultLabel = fmt.Sprintf("check_result_%d", index) + + insts, err := cbpfc.ToEBPF(bpfInsts, cbpfcOpts) + if err != nil { + return nil, err + } + + resultLabel := cbpfcOpts.ResultLabel + + // add nop insts, used to test the max insts and artificially generate tail calls + for i := 0; i != opts.NopInstLen; i++ { + insts = append(insts, + asm.JEq.Imm(asm.R9, 0, opts.sendEventLabel).WithSymbol(resultLabel), + ) + resultLabel = "" + } + + // filter result + insts = append(insts, + asm.JNE.Imm(cbpfcOpts.Result, 0, opts.sendEventLabel).WithSymbol(resultLabel), + ) + + return insts, nil +} + +func filtersToProgs(filters []Filter, opts ProgOpts, headerInsts, senderInsts asm.Instructions) ([]asm.Instructions, *multierror.Error) { + var ( + progInsts []asm.Instructions + mErr *multierror.Error + tailCalls int + header bool + ) + + // prepend a return instruction in case of fail + footerInsts := append(asm.Instructions{ + asm.Return(), + }, senderInsts...) + + isMaxSizeExceeded := func(filterInsts, tailCallInsts asm.Instructions) bool { + return len(filterInsts)+len(tailCallInsts)+len(footerInsts) > opts.MaxProgSize + } + + for i, filter := range filters { + filterInsts, err := BPFFilterToInsts(i, filter.BPFFilter, opts) + if err != nil { + mErr = multierror.Append(mErr, fmt.Errorf("unable to generate eBPF bytecode for rule `%s`: %s", filter.RuleID, err)) + continue + } + + var tailCallInsts asm.Instructions + + // insert tail call to the current filter if not the last prog + if i+1 < len(filters) { + tailCallInsts = asm.Instructions{ + asm.Mov.Reg(asm.R1, opts.ctxSave), + asm.LoadMapPtr(asm.R2, opts.tailCallMapFd), + asm.Mov.Imm(asm.R3, int32(probes.TCRawPacketFilterKey+uint32(tailCalls)+1)), + asm.FnTailCall.Call(), + } + } + + // single program exceeded the limit + if isMaxSizeExceeded(filterInsts, tailCallInsts) { + mErr = multierror.Append(mErr, fmt.Errorf("max number of intructions exceeded for rule `%s`", filter.RuleID)) + continue + } + + if !header { + progInsts = append(progInsts, headerInsts) + header = true + } + progInsts[tailCalls] = append(progInsts[tailCalls], filterInsts...) + + // max size exceeded, generate a new tail call + if isMaxSizeExceeded(progInsts[tailCalls], tailCallInsts) { + if opts.MaxTailCalls != 0 && tailCalls >= opts.MaxTailCalls { + mErr = multierror.Append(mErr, fmt.Errorf("maximum allowed tail calls reach: %d vs %d", tailCalls, opts.MaxTailCalls)) + break + } + + // insert tail call to the current filter if not the last prog + progInsts[tailCalls] = append(progInsts[tailCalls], tailCallInsts...) + + // insert the event sender instructions + progInsts[tailCalls] = append(progInsts[tailCalls], footerInsts...) + + // start a new program + header = false + tailCalls++ + } + } + + if tailCalls < len(progInsts) && header { + progInsts[tailCalls] = append(progInsts[tailCalls], footerInsts...) + } + + return progInsts, mErr +} + +// FiltersToProgramSpecs returns list of program spec from raw packet filters definitions +func FiltersToProgramSpecs(rawPacketEventMapFd, clsRouterMapFd int, filters []Filter, opts ProgOpts) ([]*ebpf.ProgramSpec, error) { + var mErr *multierror.Error + + const ( + // raw packet data, see kernel definition + dataSize = 256 + dataOffset = 164 + ) + + opts.tailCallMapFd = clsRouterMapFd + + headerInsts := append(asm.Instructions{}, + // save ctx + asm.Mov.Reg(opts.ctxSave, asm.R1), + // load raw event + asm.Mov.Reg(asm.R2, asm.RFP), + asm.Add.Imm(asm.R2, -4), + asm.StoreImm(asm.R2, 0, 0, asm.Word), // index 0 + asm.LoadMapPtr(asm.R1, rawPacketEventMapFd), + asm.FnMapLookupElem.Call(), + asm.JNE.Imm(asm.R0, 0, "raw-packet-event-not-null"), + asm.Return(), + // place in result in the start register and end register + asm.Mov.Reg(opts.PacketStart, asm.R0).WithSymbol("raw-packet-event-not-null"), + asm.Add.Imm(opts.PacketStart, dataOffset), + asm.Mov.Reg(opts.PacketEnd, opts.PacketStart), + asm.Add.Imm(opts.PacketEnd, dataSize), + ) + + senderInsts := asm.Instructions{ + asm.Mov.Reg(asm.R1, opts.ctxSave).WithSymbol(opts.sendEventLabel), + asm.LoadMapPtr(asm.R2, clsRouterMapFd), + asm.Mov.Imm(asm.R3, int32(probes.TCRawPacketParserSenderKey)), + asm.FnTailCall.Call(), + asm.Mov.Imm(asm.R0, 0), + asm.Return(), + } + + // compile and convert to eBPF progs + progInsts, err := filtersToProgs(filters, opts, headerInsts, senderInsts) + if err.ErrorOrNil() != nil { + mErr = multierror.Append(mErr, err) + } + + // should be possible + if len(progInsts) == 0 { + return nil, errors.New("no program were generated") + } + + progSpecs := make([]*ebpf.ProgramSpec, len(progInsts)) + + for i, insts := range progInsts { + name := fmt.Sprintf("%s%d", progPrefix, i) + + progSpecs[i] = &ebpf.ProgramSpec{ + Name: name, + Type: ebpf.SchedCLS, + Instructions: insts, + License: "GPL", + } + } + + return progSpecs, mErr.ErrorOrNil() +} diff --git a/pkg/security/ebpf/probes/rawpacket/pcap_unsupported.go b/pkg/security/ebpf/probes/rawpacket/pcap_unsupported.go new file mode 100644 index 0000000000000..f2d8896930dea --- /dev/null +++ b/pkg/security/ebpf/probes/rawpacket/pcap_unsupported.go @@ -0,0 +1,39 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +//go:build linux && !(pcap && cgo) + +// Package rawpacket holds raw_packet related files +package rawpacket + +import ( + "errors" + + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/asm" +) + +// ProgOpts defines options +type ProgOpts struct { + // MaxTailCalls maximun number of tail calls generated + MaxTailCalls int + // number of instructions + MaxProgSize int + // Number of nop instruction inserted in each program + NopInstLen int +} + +// DefaultProgOpts default options +var DefaultProgOpts ProgOpts + +// BPFFilterToInsts compile a bpf filter expression +func BPFFilterToInsts(_ int, _ string, _ ProgOpts) (asm.Instructions, error) { + return asm.Instructions{}, errors.New("not supported") +} + +// FiltersToProgramSpecs returns list of program spec from raw packet filters definitions +func FiltersToProgramSpecs(_, _ int, _ []Filter, _ ProgOpts) ([]*ebpf.ProgramSpec, error) { + return nil, errors.New("not supported") +} diff --git a/pkg/security/ebpf/probes/tc.go b/pkg/security/ebpf/probes/tc.go index 0a26151ce7590..e0a721986e9f5 100644 --- a/pkg/security/ebpf/probes/tc.go +++ b/pkg/security/ebpf/probes/tc.go @@ -10,8 +10,6 @@ package probes import ( manager "github.com/DataDog/ebpf-manager" - "github.com/cilium/ebpf" - "github.com/cilium/ebpf/asm" "golang.org/x/sys/unix" ) @@ -68,27 +66,12 @@ func GetTCProbes(withNetworkIngress bool, withRawPacket bool) []*manager.Probe { return out } -// RawPacketTCProgram returns the list of TC classifier sections -var RawPacketTCProgram = []string{ - "classifier_raw_packet_egress", - "classifier_raw_packet_ingress", -} - -// GetRawPacketTCFilterProg returns a first tc filter -func GetRawPacketTCFilterProg(_, clsRouterMapFd int) (*ebpf.ProgramSpec, error) { - insts := asm.Instructions{ - asm.LoadMapPtr(asm.R2, clsRouterMapFd), - asm.Mov.Imm(asm.R3, int32(TCRawPacketParserKey)), - asm.FnTailCall.Call(), - asm.Mov.Imm(asm.R0, 0), - asm.Return(), +// GetRawPacketTCProgramFunctions returns the raw packet functions +func GetRawPacketTCProgramFunctions() []string { + return []string{ + "classifier_raw_packet", + "classifier_raw_packet_sender", } - - return &ebpf.ProgramSpec{ - Type: ebpf.SchedCLS, - Instructions: insts, - License: "GPL", - }, nil } // GetAllTCProgramFunctions returns the list of TC classifier sections @@ -97,9 +80,10 @@ func GetAllTCProgramFunctions() []string { "classifier_dns_request_parser", "classifier_dns_request", "classifier_imds_request", - "classifier_raw_packet", } + output = append(output, GetRawPacketTCProgramFunctions()...) + for _, tcProbe := range GetTCProbes(true, true) { output = append(output, tcProbe.EBPFFuncName) } @@ -115,8 +99,8 @@ func GetAllTCProgramFunctions() []string { return output } -func getTCTailCallRoutes() []manager.TailCallRoute { - return []manager.TailCallRoute{ +func getTCTailCallRoutes(withRawPacket bool) []manager.TailCallRoute { + tcr := []manager.TailCallRoute{ { ProgArrayName: "classifier_router", Key: TCDNSRequestKey, @@ -138,12 +122,17 @@ func getTCTailCallRoutes() []manager.TailCallRoute { EBPFFuncName: "classifier_imds_request", }, }, - { - ProgArrayName: "classifier_router", - Key: TCRawPacketParserKey, + } + + if withRawPacket { + tcr = append(tcr, manager.TailCallRoute{ + ProgArrayName: "raw_packet_classifier_router", + Key: TCRawPacketParserSenderKey, ProbeIdentificationPair: manager.ProbeIdentificationPair{ - EBPFFuncName: "classifier_raw_packet", + EBPFFuncName: "classifier_raw_packet_sender", }, - }, + }) } + + return tcr } diff --git a/pkg/security/ebpf/tests/activity_dump_ratelimiter_test.go b/pkg/security/ebpf/tests/activity_dump_ratelimiter_test.go index 15b099d093980..06a30a1a3ce1a 100644 --- a/pkg/security/ebpf/tests/activity_dump_ratelimiter_test.go +++ b/pkg/security/ebpf/tests/activity_dump_ratelimiter_test.go @@ -17,7 +17,7 @@ import ( func TestActivityDumpRateLimiterBasic(t *testing.T) { var ctx baloum.StdContext code, err := newVM(t).RunProgram(&ctx, "test/ad_ratelimiter_basic") - if err != nil || code != 0 { + if err != nil || code != 1 { t.Errorf("unexpected error: %v, %d", err, code) } } @@ -25,7 +25,7 @@ func TestActivityDumpRateLimiterBasic(t *testing.T) { func TestActivityDumpRateLimiterBasicHalf(t *testing.T) { var ctx baloum.StdContext code, err := newVM(t).RunProgram(&ctx, "test/ad_ratelimiter_basic_half") - if err != nil || code != 0 { + if err != nil || code != 1 { t.Errorf("unexpected error: %v, %d", err, code) } } @@ -33,7 +33,7 @@ func TestActivityDumpRateLimiterBasicHalf(t *testing.T) { func TestActivityDumpRateLimiterDecreasingDroprate(t *testing.T) { var ctx baloum.StdContext code, err := newVM(t).RunProgram(&ctx, "test/ad_ratelimiter_decreasing_droprate") - if err != nil || code != 0 { + if err != nil || code != 1 { t.Errorf("unexpected error: %v, %d", err, code) } } @@ -41,7 +41,7 @@ func TestActivityDumpRateLimiterDecreasingDroprate(t *testing.T) { func TestActivityDumpRateLimiterIncreasingDroprate(t *testing.T) { var ctx baloum.StdContext code, err := newVM(t).RunProgram(&ctx, "test/ad_ratelimiter_increasing_droprate") - if err != nil || code != 0 { + if err != nil || code != 1 { t.Errorf("unexpected error: %v, %d", err, code) } } diff --git a/pkg/security/ebpf/tests/discarders_test.go b/pkg/security/ebpf/tests/discarders_test.go index 11fdb6f63272f..c044a2d752bc0 100644 --- a/pkg/security/ebpf/tests/discarders_test.go +++ b/pkg/security/ebpf/tests/discarders_test.go @@ -17,7 +17,7 @@ import ( func TestDiscarderEventMask(t *testing.T) { var ctx baloum.StdContext code, err := newVM(t).RunProgram(&ctx, "test/discarders_event_mask") - if err != nil || code != 0 { + if err != nil || code != 1 { t.Errorf("unexpected error: %v, %d", err, code) } } @@ -25,7 +25,7 @@ func TestDiscarderEventMask(t *testing.T) { func TestDiscarderRetention(t *testing.T) { var ctx baloum.StdContext code, err := newVM(t).RunProgram(&ctx, "test/discarders_retention") - if err != nil || code != 0 { + if err != nil || code != 1 { t.Errorf("unexpected error: %v, %d", err, code) } } @@ -33,7 +33,7 @@ func TestDiscarderRetention(t *testing.T) { func TestDiscarderRevision(t *testing.T) { var ctx baloum.StdContext code, err := newVM(t).RunProgram(&ctx, "test/discarders_revision") - if err != nil || code != 0 { + if err != nil || code != 1 { t.Errorf("unexpected error: %v, %d", err, code) } } @@ -41,7 +41,7 @@ func TestDiscarderRevision(t *testing.T) { func TestDiscarderMountRevision(t *testing.T) { var ctx baloum.StdContext code, err := newVM(t).RunProgram(&ctx, "test/discarders_mount_revision") - if err != nil || code != 0 { + if err != nil || code != 1 { t.Errorf("unexpected error: %v, %d", err, code) } } diff --git a/pkg/security/ebpf/tests/raw_packet_test.go b/pkg/security/ebpf/tests/raw_packet_test.go new file mode 100644 index 0000000000000..e0238036542e5 --- /dev/null +++ b/pkg/security/ebpf/tests/raw_packet_test.go @@ -0,0 +1,194 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +//go:build linux && ebpf_bindata && pcap && cgo + +// Package tests holds tests related files +package tests + +import ( + "testing" + + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/asm" + "github.com/safchain/baloum/pkg/baloum" + "github.com/stretchr/testify/assert" + + "github.com/DataDog/datadog-agent/pkg/security/ebpf/probes" + "github.com/DataDog/datadog-agent/pkg/security/ebpf/probes/rawpacket" +) + +func testRawPacketFilter(t *testing.T, filters []rawpacket.Filter, expRetCode int64, expProgNum int, opts rawpacket.ProgOpts, catchCompilerError bool) { + var ctx baloum.StdContext + + vm := newVM(t) + + rawPacketEventMap, err := vm.LoadMap("raw_packet_event") + assert.Nil(t, err, "map not found") + + routerMap, err := vm.LoadMap("raw_packet_classifier_router") + assert.Nil(t, err, "map not found") + + progSpecs, err := rawpacket.FiltersToProgramSpecs(rawPacketEventMap.FD(), routerMap.FD(), filters, opts) + if err != nil { + if catchCompilerError { + t.Fatal(err) + } else { + t.Log(err) + } + } + + assert.Equal(t, expProgNum, len(progSpecs), "number of expected programs") + + for i, progSpec := range progSpecs { + fd := vm.AddProgram(progSpec) + + _, err := routerMap.Update(probes.TCRawPacketFilterKey+uint32(i), fd, baloum.BPF_ANY) + assert.Nil(t, err, "map update error") + } + + // override the TCRawPacketParserSenderKey program with a test program + sendProgSpec := ebpf.ProgramSpec{ + Type: ebpf.SchedCLS, + Instructions: asm.Instructions{ + asm.Mov.Imm(asm.R0, 2), // put 2 as a success return value + asm.Return(), + }, + License: "GPL", + } + sendProgFD := vm.AddProgram(&sendProgSpec) + + _, err = routerMap.Update(probes.TCRawPacketParserSenderKey, sendProgFD, baloum.BPF_ANY) + assert.Nil(t, err, "map update error") + + code, err := vm.RunProgram(&ctx, "test/raw_packet_tail_calls", ebpf.SchedCLS) + if expRetCode != -1 { + assert.Nil(t, err, "program execution error") + } + assert.Equal(t, expRetCode, code, "return code error: %v", err) +} + +func TestRawPacketTailCalls(t *testing.T) { + t.Run("syn-port-std-ok", func(t *testing.T) { + filters := []rawpacket.Filter{ + { + RuleID: "ok", + BPFFilter: "tcp dst port 5555 and tcp[tcpflags] == tcp-syn", + }, + } + testRawPacketFilter(t, filters, 2, 1, rawpacket.DefaultProgOpts, true) + }) + + t.Run("syn-port-std-ko", func(t *testing.T) { + filters := []rawpacket.Filter{ + { + RuleID: "ko", + BPFFilter: "tcp dst port 6666 and tcp[tcpflags] == tcp-syn", + }, + } + testRawPacketFilter(t, filters, 0, 1, rawpacket.DefaultProgOpts, true) + }) + + t.Run("syn-port-std-limit-ko", func(t *testing.T) { + filters := []rawpacket.Filter{ + { + RuleID: "ko", + BPFFilter: "tcp dst port 5555 and tcp[tcpflags] == tcp-syn", + }, + } + + opts := rawpacket.DefaultProgOpts + opts.NopInstLen = opts.MaxProgSize + + testRawPacketFilter(t, filters, -1, 0, opts, false) + }) + + t.Run("syn-port-std-syntax-err", func(t *testing.T) { + filters := []rawpacket.Filter{ + { + RuleID: "ok", + BPFFilter: "tcp dst port number and tcp[tcpflags] == tcp-syn", + }, + } + testRawPacketFilter(t, filters, -1, 0, rawpacket.DefaultProgOpts, false) + }) + + t.Run("syn-port-multi-ok", func(t *testing.T) { + filters := []rawpacket.Filter{ + { + RuleID: "ko", + BPFFilter: "tcp dst port 6666 and tcp[tcpflags] == tcp-syn", + }, + { + RuleID: "ok", + BPFFilter: "tcp dst port 5555 and tcp[tcpflags] == tcp-syn", + }, + } + + opts := rawpacket.DefaultProgOpts + opts.NopInstLen = opts.MaxProgSize - 50 + + testRawPacketFilter(t, filters, 2, 2, opts, true) + }) + + t.Run("syn-port-multi-ko", func(t *testing.T) { + filters := []rawpacket.Filter{ + { + RuleID: "ko1", + BPFFilter: "tcp dst port 6666 and tcp[tcpflags] == tcp-syn", + }, + { + RuleID: "ko2", + BPFFilter: "tcp dst port 7777 and tcp[tcpflags] == tcp-syn", + }, + } + + opts := rawpacket.DefaultProgOpts + opts.NopInstLen = opts.MaxProgSize - 50 + + testRawPacketFilter(t, filters, 0, 2, opts, true) + }) + + t.Run("syn-port-multi-syntax-err", func(t *testing.T) { + filters := []rawpacket.Filter{ + { + RuleID: "ko", + BPFFilter: "tcp dst port number and tcp[tcpflags] == tcp-syn", + }, + { + RuleID: "ok", + BPFFilter: "tcp dst port 5555 and tcp[tcpflags] == tcp-syn", + }, + } + + opts := rawpacket.DefaultProgOpts + opts.NopInstLen = opts.MaxProgSize - 50 + + testRawPacketFilter(t, filters, 2, 1, opts, false) + }) + + t.Run("syn-port-multi-limit-ok", func(t *testing.T) { + filters := []rawpacket.Filter{ + { + RuleID: "ok", + BPFFilter: "tcp dst port 5555 and tcp[tcpflags] == tcp-syn", + }, + { + RuleID: "ko1", + BPFFilter: "tcp dst port number and tcp[tcpflags] == tcp-syn", + }, + { + RuleID: "ko2", + BPFFilter: "tcp dst port 7777 and tcp[tcpflags] == tcp-syn", + }, + } + + opts := rawpacket.DefaultProgOpts + opts.MaxTailCalls = 0 + opts.NopInstLen = opts.MaxProgSize - 50 + + testRawPacketFilter(t, filters, 2, 2, opts, false) + }) +} diff --git a/pkg/security/probe/model_ebpf.go b/pkg/security/probe/model_ebpf.go index 2854cc2f8cf4b..c26e96a0b91f3 100644 --- a/pkg/security/probe/model_ebpf.go +++ b/pkg/security/probe/model_ebpf.go @@ -12,6 +12,7 @@ import ( "fmt" "time" + "github.com/DataDog/datadog-agent/pkg/security/ebpf/probes/rawpacket" "github.com/DataDog/datadog-agent/pkg/security/probe/constantfetch" "github.com/DataDog/datadog-agent/pkg/security/secl/compiler/eval" "github.com/DataDog/datadog-agent/pkg/security/secl/model" @@ -20,7 +21,7 @@ import ( // NewEBPFModel returns a new model with some extra field validation func NewEBPFModel(probe *EBPFProbe) *model.Model { return &model.Model{ - ExtraValidateFieldFnc: func(field eval.Field, _ eval.FieldValue) error { + ExtraValidateFieldFnc: func(field eval.Field, value eval.FieldValue) error { switch field { case "bpf.map.name": if offset, found := probe.constantOffsets[constantfetch.OffsetNameBPFMapStructName]; !found || offset == constantfetch.ErrorSentinel { @@ -31,6 +32,13 @@ func NewEBPFModel(probe *EBPFProbe) *model.Model { if offset, found := probe.constantOffsets[constantfetch.OffsetNameBPFProgAuxStructName]; !found || offset == constantfetch.ErrorSentinel { return fmt.Errorf("%s is not available on this kernel version", field) } + case "packet.filter": + if probe.isRawPacketNotSupported() { + return fmt.Errorf("%s is not available on this kernel version", field) + } + if _, err := rawpacket.BPFFilterToInsts(0, value.Value.(string), rawpacket.DefaultProgOpts); err != nil { + return err + } } return nil diff --git a/pkg/security/probe/probe_ebpf.go b/pkg/security/probe/probe_ebpf.go index ae258025ee7f5..37db8edde5422 100644 --- a/pkg/security/probe/probe_ebpf.go +++ b/pkg/security/probe/probe_ebpf.go @@ -40,6 +40,7 @@ import ( "github.com/DataDog/datadog-agent/pkg/security/ebpf" "github.com/DataDog/datadog-agent/pkg/security/ebpf/kernel" "github.com/DataDog/datadog-agent/pkg/security/ebpf/probes" + "github.com/DataDog/datadog-agent/pkg/security/ebpf/probes/rawpacket" "github.com/DataDog/datadog-agent/pkg/security/events" "github.com/DataDog/datadog-agent/pkg/security/metrics" pconfig "github.com/DataDog/datadog-agent/pkg/security/probe/config" @@ -117,8 +118,9 @@ type EBPFProbe struct { cancelFnc context.CancelFunc wg sync.WaitGroup - // TC Classifier - newTCNetDevices chan model.NetDevice + // TC Classifier & raw packets + newTCNetDevices chan model.NetDevice + rawPacketFilterCollection *lib.Collection // Ring eventStream EventStream @@ -197,6 +199,14 @@ func (p *EBPFProbe) selectFentryMode() { p.useFentry = supported } +func (p *EBPFProbe) isNetworkNotSupported() bool { + return p.kernelVersion.IsRH7Kernel() +} + +func (p *EBPFProbe) isRawPacketNotSupported() bool { + return p.isNetworkNotSupported() || (p.kernelVersion.IsAmazonLinuxKernel() && p.kernelVersion.Code < kernel.Kernel4_15) +} + func (p *EBPFProbe) sanityChecks() error { // make sure debugfs is mounted if _, err := tracefs.Root(); err != nil { @@ -207,11 +217,16 @@ func (p *EBPFProbe) sanityChecks() error { return errors.New("eBPF not supported in lockdown `confidentiality` mode") } - if p.config.Probe.NetworkEnabled && p.kernelVersion.IsRH7Kernel() { - seclog.Warnf("The network feature of CWS isn't supported on Centos7, setting event_monitoring_config.network.enabled to false") + if p.config.Probe.NetworkEnabled && p.isNetworkNotSupported() { + seclog.Warnf("the network feature of CWS isn't supported on this kernel version") p.config.Probe.NetworkEnabled = false } + if p.config.Probe.NetworkRawPacketEnabled && p.isRawPacketNotSupported() { + seclog.Warnf("the raw packet feature of CWS isn't supported on this kernel version") + p.config.Probe.NetworkRawPacketEnabled = false + } + return nil } @@ -350,39 +365,87 @@ func (p *EBPFProbe) IsRuntimeCompiled() bool { return p.runtimeCompiled } -func (p *EBPFProbe) setupRawPacketProgs() error { - packetsMap, _, err := p.Manager.GetMap("packets") +func (p *EBPFProbe) setupRawPacketProgs(rs *rules.RuleSet) error { + rawPacketEventMap, _, err := p.Manager.GetMap("raw_packet_event") if err != nil { return err } - routerMap, _, err := p.Manager.GetMap("classifier_router") + if rawPacketEventMap == nil { + return errors.New("unable to find `rawpacket_event` map") + } + + routerMap, _, err := p.Manager.GetMap("raw_packet_classifier_router") if err != nil { return err } + if routerMap == nil { + return errors.New("unable to find `classifier_router` map") + } - progSpec, err := probes.GetRawPacketTCFilterProg(packetsMap.FD(), routerMap.FD()) + var rawPacketFilters []rawpacket.Filter + for id, rule := range rs.GetRules() { + for _, field := range rule.GetFieldValues("packet.filter") { + rawPacketFilters = append(rawPacketFilters, rawpacket.Filter{ + RuleID: id, + BPFFilter: field.Value.(string), + }) + } + } + + // unload the previews one + if p.rawPacketFilterCollection != nil { + p.rawPacketFilterCollection.Close() + ddebpf.RemoveNameMappingsCollection(p.rawPacketFilterCollection) + } + + // adapt max instruction limits depending of the kernel version + opts := rawpacket.DefaultProgOpts + if p.kernelVersion.Code >= kernel.Kernel5_2 { + opts.MaxProgSize = 1_000_000 + } + + seclog.Debugf("generate rawpacker filter programs with a limit of %d max instructions", opts.MaxProgSize) + + // compile the filters + progSpecs, err := rawpacket.FiltersToProgramSpecs(rawPacketEventMap.FD(), routerMap.FD(), rawPacketFilters, opts) if err != nil { return err } + if len(progSpecs) == 0 { + return nil + } + colSpec := lib.CollectionSpec{ - Programs: map[string]*lib.ProgramSpec{ - progSpec.Name: progSpec, - }, + Programs: make(map[string]*lib.ProgramSpec), + } + for _, progSpec := range progSpecs { + colSpec.Programs[progSpec.Name] = progSpec } col, err := lib.NewCollection(&colSpec) if err != nil { return fmt.Errorf("failed to load program: %w", err) } + p.rawPacketFilterCollection = col + + // check that the sender program is not overridden. The default opts should avoid this. + if probes.TCRawPacketFilterKey+uint32(len(progSpecs)) >= probes.TCRawPacketParserSenderKey { + return fmt.Errorf("sender program overridden") + } - return p.Manager.UpdateTailCallRoutes( - manager.TailCallRoute{ + // setup tail calls + for i, progSpec := range progSpecs { + if err := p.Manager.UpdateTailCallRoutes(manager.TailCallRoute{ Program: col.Programs[progSpec.Name], - Key: probes.TCRawPacketFilterKey, - ProgArrayName: "classifier_router", - }, - ) + Key: probes.TCRawPacketFilterKey + uint32(i), + ProgArrayName: "raw_packet_classifier_router", + }); err != nil { + return err + } + } + + return nil } // Setup the probe @@ -402,12 +465,6 @@ func (p *EBPFProbe) Setup() error { p.profileManagers.Start(p.ctx, &p.wg) - if p.probe.IsNetworkRawPacketEnabled() { - if err := p.setupRawPacketProgs(); err != nil { - return err - } - } - return nil } @@ -1500,6 +1557,10 @@ func (p *EBPFProbe) Close() error { // we wait until both the reorderer and the monitor are stopped p.wg.Wait() + if p.rawPacketFilterCollection != nil { + p.rawPacketFilterCollection.Close() + } + ddebpf.RemoveNameMappings(p.Manager) ebpftelemetry.UnregisterTelemetry(p.Manager) // Stopping the manager will stop the perf map reader and unload eBPF programs @@ -1720,6 +1781,12 @@ func (p *EBPFProbe) ApplyRuleSet(rs *rules.RuleSet) (*kfilters.ApplyRuleSetRepor } } + if p.probe.IsNetworkRawPacketEnabled() { + if err := p.setupRawPacketProgs(rs); err != nil { + seclog.Errorf("unable to load raw packet filter programs: %v", err) + } + } + // do not replay the snapshot if we are in the first rule set version, this was already done in the start method if p.ruleSetVersion != 0 { p.playSnapShotState.Store(true) @@ -1990,15 +2057,17 @@ func NewEBPFProbe(probe *Probe, config *config.Config, opts Opts, telemetry tele } // tail calls - p.managerOptions.TailCallRouter = probes.AllTailRoutes(config.Probe.ERPCDentryResolutionEnabled, config.Probe.NetworkEnabled, useMmapableMaps) + p.managerOptions.TailCallRouter = probes.AllTailRoutes(config.Probe.ERPCDentryResolutionEnabled, config.Probe.NetworkEnabled, config.Probe.NetworkRawPacketEnabled, useMmapableMaps) if !config.Probe.ERPCDentryResolutionEnabled || useMmapableMaps { // exclude the programs that use the bpf_probe_write_user helper p.managerOptions.ExcludedFunctions = probes.AllBPFProbeWriteUserProgramFunctions() } - if !config.Probe.NetworkEnabled { - // prevent all TC classifiers from loading + // prevent some TC classifiers from loading + if !p.config.Probe.NetworkEnabled { p.managerOptions.ExcludedFunctions = append(p.managerOptions.ExcludedFunctions, probes.GetAllTCProgramFunctions()...) + } else if !p.config.Probe.NetworkRawPacketEnabled { + p.managerOptions.ExcludedFunctions = append(p.managerOptions.ExcludedFunctions, probes.GetRawPacketTCProgramFunctions()...) } if p.useFentry { diff --git a/pkg/security/secl/model/packet_filter_unix.go b/pkg/security/secl/model/oo_packet_filter_unix.go similarity index 83% rename from pkg/security/secl/model/packet_filter_unix.go rename to pkg/security/secl/model/oo_packet_filter_unix.go index af1329d84040f..18f3d72bec50a 100644 --- a/pkg/security/secl/model/packet_filter_unix.go +++ b/pkg/security/secl/model/oo_packet_filter_unix.go @@ -32,7 +32,7 @@ func errorNonStaticPacketFilterField(a eval.Evaluator, b eval.Evaluator) error { return fmt.Errorf("field `%s` only supports matching a single static value", field) } -func newPacketFilterEvaluator(field string, value string) (*eval.BoolEvaluator, error) { +func newPacketFilterEvaluator(field string, value string, state *eval.State) (*eval.BoolEvaluator, error) { switch field { case "packet.filter": captureLength := 256 // sizeof(struct raw_packet_t.data) @@ -40,6 +40,12 @@ func newPacketFilterEvaluator(field string, value string) (*eval.BoolEvaluator, if err != nil { return nil, fmt.Errorf("failed to compile packet filter `%s` on field `%s`: %v", value, field, err) } + + // needed to track filter values and to apply tc filters + if err := state.UpdateFieldValues(field, eval.FieldValue{Value: value, Type: eval.ScalarValueType}); err != nil { + return nil, err + } + return &eval.BoolEvaluator{ EvalFnc: func(ctx *eval.Context) bool { ev := ctx.Event.(*Event) @@ -53,11 +59,11 @@ func newPacketFilterEvaluator(field string, value string) (*eval.BoolEvaluator, // PacketFilterMatching is a set of overrides for packet filter fields, it only supports matching a single static value var PacketFilterMatching = &eval.OpOverrides{ - StringEquals: func(a *eval.StringEvaluator, b *eval.StringEvaluator, _ *eval.State) (*eval.BoolEvaluator, error) { + StringEquals: func(a *eval.StringEvaluator, b *eval.StringEvaluator, state *eval.State) (*eval.BoolEvaluator, error) { if a.IsStatic() { - return newPacketFilterEvaluator(b.GetField(), a.Value) + return newPacketFilterEvaluator(b.GetField(), a.Value, state) } else if b.IsStatic() { - return newPacketFilterEvaluator(a.GetField(), b.Value) + return newPacketFilterEvaluator(a.GetField(), b.Value, state) } return nil, errorNonStaticPacketFilterField(a, b) }, diff --git a/pkg/security/secl/model/packet_filter_unsupported.go b/pkg/security/secl/model/oo_packet_filter_unsupported.go similarity index 100% rename from pkg/security/secl/model/packet_filter_unsupported.go rename to pkg/security/secl/model/oo_packet_filter_unsupported.go diff --git a/pkg/security/tests/network_test.go b/pkg/security/tests/network_test.go index 0f2e27ad5baf4..e094e20ba528f 100644 --- a/pkg/security/tests/network_test.go +++ b/pkg/security/tests/network_test.go @@ -82,7 +82,7 @@ func TestRawPacket(t *testing.T) { checkKernelCompatibility(t, "RHEL, SLES, SUSE and Oracle kernels", func(kv *kernel.Version) bool { // TODO: Oracle because we are missing offsets // OpenSUSE distributions are missing the dummy kernel module - return kv.IsRH7Kernel() || kv.IsOracleUEKKernel() || kv.IsSLESKernel() || kv.IsOpenSUSELeapKernel() + return kv.IsRH7Kernel() || kv.IsOracleUEKKernel() || kv.IsSLESKernel() || kv.IsOpenSUSELeapKernel() || (kv.IsAmazonLinuxKernel() && kv.Code < kernel.Kernel4_15) }) if testEnvironment != DockerEnvironment && !env.IsContainerized() { diff --git a/tasks/security_agent.py b/tasks/security_agent.py index 6b5f20ac20092..3d0dc7e752d95 100644 --- a/tasks/security_agent.py +++ b/tasks/security_agent.py @@ -727,12 +727,23 @@ def e2e_prepare_win(ctx): @task -def run_ebpf_unit_tests(ctx, verbose=False, trace=False): +def run_ebpf_unit_tests(ctx, verbose=False, trace=False, testflags=''): build_cws_object_files( ctx, major_version='7', kernel_release=None, with_unit_test=True, bundle_ebpf=True, arch=CURRENT_ARCH ) - flags = '-tags ebpf_bindata' + env = {"CGO_ENABLED": "1"} + + build_libpcap(ctx) + cgo_flags = get_libpcap_cgo_flags(ctx) + # append libpcap cgo-related environment variables to any existing ones + for k, v in cgo_flags.items(): + if k in env: + env[k] += f" {v}" + else: + env[k] = v + + flags = '-tags ebpf_bindata,cgo,pcap' if verbose: flags += " -test.v" @@ -740,7 +751,7 @@ def run_ebpf_unit_tests(ctx, verbose=False, trace=False): if trace: args += " -trace" - ctx.run(f"go test {flags} ./pkg/security/ebpf/tests/... {args}") + ctx.run(f"go test {flags} ./pkg/security/ebpf/tests/... {args} {testflags}", env=env) @task