diff --git a/doc/usage/bfcli.rst b/doc/usage/bfcli.rst index e49019fb8..50fc3b769 100644 --- a/doc/usage/bfcli.rst +++ b/doc/usage/bfcli.rst @@ -148,7 +148,8 @@ bfcli will print log entries as they are published by the chain. Hit ``Ctrl+C`` Every log entry begins with a shared header: the receive timestamp, the matching rule's index, and the applied verdict. The remaining fields depend on the hook type: -- For packet-based hooks, the header also includes the matched packet size. It is followed by each requested layer's protocol headers (see the ``log`` action below). If a requested layer could not be processed by the chain, the corresponding output will be truncated. +- For packet-based hooks using packet-layer logging, the header also includes the matched packet size. It is followed by each requested layer's protocol headers (see the ``log`` action below). If a requested layer could not be processed by the chain, the corresponding output will be truncated. +- For packet-based hooks using 5-tuple logging, the source and destination addresses and ports are printed on a single line with the transport protocol. - For ``BF_HOOK_CGROUP_SOCK_ADDR_*`` hooks, the entry includes destination address, destination port, process ID, and process name. Sendmsg hooks additionally include the source address. **Options** @@ -432,12 +433,13 @@ Rules are defined such as: With: - ``$MATCHER``: zero or more matchers. Matchers are defined later. - - ``log``: optional. Two forms are supported: + - ``log``: optional. Three forms are supported: - ``log $HEADERS``: log specific packet headers. ``$HEADERS`` is a comma-separated list of ``link`` (layer 2), ``internet`` (layer 3), and/or ``transport`` (layer 4). Only supported by packet-based hooks (XDP, TC, NF, cgroup_skb). + - ``log 5-tuple``: log source and destination addresses and ports, and the transport protocol. This mode is only supported by packet-based hooks and only emits entries for IPv4/IPv6 packets using TCP or UDP. It is mutually exclusive with the packet-layer options; unsupported packets are not logged. - ``log``: log all available data for the hook type. For packet-based hooks, this is equivalent to ``log link,internet,transport``. For ``BF_HOOK_CGROUP_SOCK_ADDR_*`` hooks, this records the process ID, process name, destination address, and destination port. Sendmsg hooks additionally include the source address. - Either form accepts an optional ``every $FREQUENCY`` suffix to rate-limit log events. ``$FREQUENCY`` is a positive number (integer or decimal) followed by a unit: ``ns``, ``us``, ``ms``, or ``s`` (e.g. ``every 1s``, ``every 500ms``, ``every 1.5s``). At most one log entry is emitted per ``$FREQUENCY`` interval per rule. Without ``every``, every match is logged. + Each form accepts an optional ``every $FREQUENCY`` suffix to rate-limit log events. ``$FREQUENCY`` is a positive number (integer or decimal) followed by a unit: ``ns``, ``us``, ``ms``, or ``s`` (e.g. ``every 1s``, ``every 500ms``, ``every 1.5s``). At most one log entry is emitted per ``$FREQUENCY`` interval per rule. Without ``every``, every match is logged. - ``counter``: optional literal. If set, the filter will count the number of events matched by the rule. For packet-based hooks, this includes both the number of packets and the total bytes. For ``BF_HOOK_CGROUP_SOCK_ADDR_*`` hooks, this counts the number of socket operations (``connect()`` or ``sendmsg()`` calls). - ``mark``: optional, ``$MARK`` must be a valid decimal or hexadecimal 32-bits value. If set, write the packet's marker value. This marker can be used later on in a rule (see ``meta.mark``) or with a TC filter. - ``$VERDICT``: action taken by the rule if the packet is matched against **all** the criteria: either ``ACCEPT``, ``DROP``, ``CONTINUE``, ``NEXT``, or ``REDIRECT``. diff --git a/src/bfcli/lexer.l b/src/bfcli/lexer.l index 1ecb0597b..4329c3875 100644 --- a/src/bfcli/lexer.l +++ b/src/bfcli/lexer.l @@ -290,7 +290,7 @@ tcp\.flags { BEGIN(STATE_MATCHER_TCP_FLAGS); yylval.sval = strdup(yytext); } } -[a-zA-Z0-9_]+ { yylval.sval = strdup(yytext); return STRING; } +[a-zA-Z0-9_-]+ { yylval.sval = strdup(yytext); return STRING; } . { return *yytext; } diff --git a/src/bfcli/print.c b/src/bfcli/print.c index 82799510f..c8ddc69d2 100644 --- a/src/bfcli/print.c +++ b/src/bfcli/print.c @@ -677,6 +677,55 @@ static void _bf_chain_log_sock_addr(const struct bf_log *log) bf_logger_get_color(BF_COLOR_RESET, BF_STYLE_RESET)); } +static void _bf_chain_log_5_tuple(const struct bf_log *log) +{ + char src_addr[INET6_ADDRSTRLEN]; + char dst_addr[INET6_ADDRSTRLEN]; + const char *protocol; + int family; + + assert(log); + + if (log->l3_proto == ETH_P_IP) { + family = AF_INET; + } else if (log->l3_proto == ETH_P_IPV6) { + family = AF_INET6; + } else { + (void)fprintf(stdout, " 5-tuple : \n", + log->l3_proto); + return; + } + + inet_ntop(family, log->pkt_5_tuple.saddr, src_addr, sizeof(src_addr)); + inet_ntop(family, log->pkt_5_tuple.daddr, dst_addr, sizeof(dst_addr)); + protocol = bf_ipproto_to_str(log->l4_proto); + /* Tuple logging only emits TCP or UDP records, both of which are known to + * bf_ipproto_to_str(). */ + + (void)fprintf(stdout, " 5-tuple : %s%s%s ", + bf_logger_get_color(BF_COLOR_LIGHT_MAGENTA, BF_STYLE_BOLD), + protocol ?: "unknown", + bf_logger_get_color(BF_COLOR_RESET, BF_STYLE_RESET)); + + if (family == AF_INET6) { + (void)fprintf(stdout, "%s[%s]:%u%s → %s[%s]:%u%s\n", + bf_logger_get_color(BF_COLOR_LIGHT_CYAN, BF_STYLE_BOLD), + src_addr, log->pkt_5_tuple.sport, + bf_logger_get_color(BF_COLOR_RESET, BF_STYLE_RESET), + bf_logger_get_color(BF_COLOR_LIGHT_CYAN, BF_STYLE_BOLD), + dst_addr, log->pkt_5_tuple.dport, + bf_logger_get_color(BF_COLOR_RESET, BF_STYLE_RESET)); + } else { + (void)fprintf(stdout, "%s%s:%u%s → %s%s:%u%s\n", + bf_logger_get_color(BF_COLOR_CYAN, BF_STYLE_BOLD), + src_addr, log->pkt_5_tuple.sport, + bf_logger_get_color(BF_COLOR_RESET, BF_STYLE_RESET), + bf_logger_get_color(BF_COLOR_CYAN, BF_STYLE_BOLD), + dst_addr, log->pkt_5_tuple.dport, + bf_logger_get_color(BF_COLOR_RESET, BF_STYLE_RESET)); + } +} + void bfc_print_log(const struct bf_log *log) { assert(log); @@ -695,6 +744,9 @@ void bfc_print_log(const struct bf_log *log) case BF_LOG_TYPE_SOCK_ADDR: _bf_chain_log_sock_addr(log); break; + case BF_LOG_TYPE_PACKET_5_TUPLE: + _bf_chain_log_5_tuple(log); + break; default: break; } diff --git a/src/libbpfilter/CMakeLists.txt b/src/libbpfilter/CMakeLists.txt index 4d3bf86a6..314614c98 100644 --- a/src/libbpfilter/CMakeLists.txt +++ b/src/libbpfilter/CMakeLists.txt @@ -117,6 +117,7 @@ bf_target_add_elfstubs(libbpfilter "pkt_log" "flow_hash" "sock_addr_log" + "pkt_5_tuple_log" ) target_compile_definitions(libbpfilter diff --git a/src/libbpfilter/bpf/pkt_5_tuple_log.bpf.c b/src/libbpfilter/bpf/pkt_5_tuple_log.bpf.c new file mode 100644 index 000000000..b448c59ea --- /dev/null +++ b/src/libbpfilter/bpf/pkt_5_tuple_log.bpf.c @@ -0,0 +1,74 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "cgen/runtime.h" + +__u8 bf_pkt_5_tuple_log(struct bf_runtime *ctx, __u32 rule_id, __u32 verdict, + __u32 l3_l4_proto) +{ + struct bf_log *log; + __u16 l3_proto = (__u16)(l3_l4_proto >> 16); + __u8 l4_proto = (__u8)l3_l4_proto; + + log = bpf_ringbuf_reserve(ctx->log_map, sizeof(struct bf_log), 0); + if (!log) { + bpf_printk("failed to reserve %d bytes in ringbuf", + sizeof(struct bf_log)); + return 1; + } + + __builtin_memset(log, 0, sizeof(*log)); + + log->ts = bpf_ktime_get_ns(); + log->rule_id = rule_id; + log->verdict = verdict; + log->l3_proto = bpf_ntohs(l3_proto); + log->l4_proto = l4_proto; + log->log_type = BF_LOG_TYPE_PACKET_5_TUPLE; + + if (l3_proto == bpf_htons(ETH_P_IP)) { + struct iphdr *ip4 = ctx->l3_hdr; + + __builtin_memcpy(log->pkt_5_tuple.saddr, &ip4->saddr, + sizeof(ip4->saddr)); + __builtin_memcpy(log->pkt_5_tuple.daddr, &ip4->daddr, + sizeof(ip4->daddr)); + } else { + struct ipv6hdr *ip6 = ctx->l3_hdr; + + __builtin_memcpy(log->pkt_5_tuple.saddr, &ip6->saddr, + sizeof(ip6->saddr)); + __builtin_memcpy(log->pkt_5_tuple.daddr, &ip6->daddr, + sizeof(ip6->daddr)); + } + + if (l4_proto == IPPROTO_TCP) { + struct tcphdr *tcp = ctx->l4_hdr; + + log->pkt_5_tuple.sport = bpf_ntohs(tcp->source); + log->pkt_5_tuple.dport = bpf_ntohs(tcp->dest); + } else { + struct udphdr *udp = ctx->l4_hdr; + + log->pkt_5_tuple.sport = bpf_ntohs(udp->source); + log->pkt_5_tuple.dport = bpf_ntohs(udp->dest); + } + + bpf_ringbuf_submit(log, 0); + + return 0; +} diff --git a/src/libbpfilter/cgen/packet.c b/src/libbpfilter/cgen/packet.c index ef078c255..5e2c1c173 100644 --- a/src/libbpfilter/cgen/packet.c +++ b/src/libbpfilter/cgen/packet.c @@ -421,21 +421,40 @@ int bf_packet_gen_inline_matcher(struct bf_program *program, int bf_packet_gen_inline_log(struct bf_program *program, const struct bf_rule *rule) { + uint8_t headers; + bool is_5_tuple; + assert(program); assert(rule); + is_5_tuple = rule->log == BF_FLAG(BF_LOG_OPT_5_TUPLE); + headers = + rule->log == BF_LOG_OPT_DEFAULT ? BF_LOG_PACKET_HEADERS : rule->log; + EMIT(program, BPF_MOV64_REG(BPF_REG_1, BPF_REG_10)); EMIT(program, BPF_ALU64_IMM(BPF_ADD, BPF_REG_1, BF_PROG_CTX_OFF(arg))); EMIT(program, BPF_MOV64_IMM(BPF_REG_2, rule->index)); - EMIT(program, BPF_MOV64_IMM(BPF_REG_3, rule->log)); - EMIT(program, BPF_MOV64_IMM(BPF_REG_4, rule->verdict)); - // Pack l3_proto and l4_proto - EMIT(program, BPF_MOV64_REG(BPF_REG_5, BPF_REG_7)); - EMIT(program, BPF_ALU64_IMM(BPF_LSH, BPF_REG_5, 16)); - EMIT(program, BPF_ALU64_REG(BPF_OR, BPF_REG_5, BPF_REG_8)); + if (is_5_tuple) { + EMIT(program, BPF_MOV64_IMM(BPF_REG_3, rule->verdict)); + + // Pack l3_proto and l4_proto + EMIT(program, BPF_MOV64_REG(BPF_REG_4, BPF_REG_7)); + EMIT(program, BPF_ALU64_IMM(BPF_LSH, BPF_REG_4, 16)); + EMIT(program, BPF_ALU64_REG(BPF_OR, BPF_REG_4, BPF_REG_8)); - EMIT_FIXUP_ELFSTUB(program, BF_ELFSTUB_PKT_LOG); + EMIT_FIXUP_ELFSTUB(program, BF_ELFSTUB_PKT_5_TUPLE_LOG); + } else { + EMIT(program, BPF_MOV64_IMM(BPF_REG_3, headers)); + EMIT(program, BPF_MOV64_IMM(BPF_REG_4, rule->verdict)); + + // Pack l3_proto and l4_proto + EMIT(program, BPF_MOV64_REG(BPF_REG_5, BPF_REG_7)); + EMIT(program, BPF_ALU64_IMM(BPF_LSH, BPF_REG_5, 16)); + EMIT(program, BPF_ALU64_REG(BPF_OR, BPF_REG_5, BPF_REG_8)); + + EMIT_FIXUP_ELFSTUB(program, BF_ELFSTUB_PKT_LOG); + } return 0; } diff --git a/src/libbpfilter/cgen/program.c b/src/libbpfilter/cgen/program.c index 85282af0c..4ee41245c 100644 --- a/src/libbpfilter/cgen/program.c +++ b/src/libbpfilter/cgen/program.c @@ -7,8 +7,11 @@ #include #include +#include +#include // NOLINT #include +#include #include #include #include @@ -504,6 +507,76 @@ static int _bf_program_check_proto(struct bf_program *program, return 0; } +static int _bf_program_generate_log(struct bf_program *program, + const struct bf_rule *rule) +{ + _clean_bf_jmpctx_ struct bf_jmpctx l3_ctx = bf_jmpctx_default(); + _clean_bf_jmpctx_ struct bf_jmpctx l4_ctx = bf_jmpctx_default(); + _clean_bf_jmpctx_ struct bf_jmpctx null_ctx = bf_jmpctx_default(); + _clean_bf_jmpctx_ struct bf_jmpctx rate_ctx = bf_jmpctx_default(); + + assert(program); + assert(rule); + + if (!rule->log) + return 0; + + if (rule->log == BF_FLAG(BF_LOG_OPT_5_TUPLE)) { + /* A 5-tuple is only complete for IPv4/IPv6 packets using TCP/UDP. + * Skip only the log action for other packets, leaving the rule's + * remaining actions and verdict unchanged. */ + EMIT(program, BPF_JMP_IMM(BPF_JEQ, BPF_REG_7, htobe16(ETH_P_IP), 2)); + EMIT(program, BPF_JMP_IMM(BPF_JEQ, BPF_REG_7, htobe16(ETH_P_IPV6), 1)); + l3_ctx = bf_jmpctx_get(program, BPF_JMP_A(0)); + + EMIT(program, BPF_JMP_IMM(BPF_JEQ, BPF_REG_8, IPPROTO_TCP, 2)); + EMIT(program, BPF_JMP_IMM(BPF_JEQ, BPF_REG_8, IPPROTO_UDP, 1)); + l4_ctx = bf_jmpctx_get(program, BPF_JMP_A(0)); + } + + if (rule->log_rate_ns) { + const struct bpf_insn rate_insn[2] = { + BPF_LD_IMM64(BPF_REG_1, rule->log_rate_ns), + }; + + /* Rate-limited log: check last_log_ts in the state map before logging. + * + * R9 (callee-saved) holds the pointer to this rule's state entry + * across the bpf_ktime_get_ns() call. */ + EMIT(program, BPF_LDX_MEM(BPF_DW, BPF_REG_9, BPF_REG_10, + BF_PROG_CTX_OFF(state_map))); + + /* Skip the log if state_map is NULL. This shouldn't happen at runtime, + * but the verifier requires the NULL check. */ + null_ctx = + bf_jmpctx_get(program, BPF_JMP_IMM(BPF_JEQ, BPF_REG_9, 0, 0)); + + if (rule->index > 0) { + EMIT(program, BPF_ALU64_IMM(BPF_ADD, BPF_REG_9, + (int)(rule->index * + sizeof(struct bf_rule_state)))); + } + + EMIT(program, BPF_EMIT_CALL(BPF_FUNC_ktime_get_ns)); + + EMIT(program, BPF_LDX_MEM(BPF_DW, BPF_REG_1, BPF_REG_9, 0)); + EMIT(program, BPF_MOV64_REG(BPF_REG_2, BPF_REG_0)); + EMIT(program, BPF_ALU64_REG(BPF_SUB, BPF_REG_2, BPF_REG_1)); + + // Load log_rate_ns as a 64-bit immediate into R1. + EMIT(program, rate_insn[0]); + EMIT(program, rate_insn[1]); + + // Skip the log while delta is smaller than log_rate_ns. + rate_ctx = bf_jmpctx_get(program, + BPF_JMP_REG(BPF_JLT, BPF_REG_2, BPF_REG_1, 0)); + + EMIT(program, BPF_STX_MEM(BPF_DW, BPF_REG_9, BPF_REG_0, 0)); + } + + return program->runtime.ops->gen_inline_log(program, rule); +} + static int _bf_program_generate_rule(struct bf_program *program, struct bf_rule *rule) { @@ -567,58 +640,9 @@ static int _bf_program_generate_rule(struct bf_program *program, } } - if (rule->log && rule->log_rate_ns) { - // Rate-limited log: check last_log_ts in the state map before logging. - // - // R9 (callee-saved) holds the pointer to this rule's state entry - // across the bpf_ktime_get_ns() call. - EMIT(program, BPF_LDX_MEM(BPF_DW, BPF_REG_9, BPF_REG_10, - BF_PROG_CTX_OFF(state_map))); - { - // Outer skip: state_map is NULL (shouldn't happen at runtime, - // but the verifier requires the NULL check). - _clean_bf_jmpctx_ struct bf_jmpctx null_ctx = - bf_jmpctx_get(program, BPF_JMP_IMM(BPF_JEQ, BPF_REG_9, 0, 0)); - - if (rule->index > 0) { - EMIT(program, - BPF_ALU64_IMM( - BPF_ADD, BPF_REG_9, - (int)(rule->index * sizeof(struct bf_rule_state)))); - } - - EMIT(program, BPF_EMIT_CALL(BPF_FUNC_ktime_get_ns)); - - EMIT(program, BPF_LDX_MEM(BPF_DW, BPF_REG_1, BPF_REG_9, 0)); - EMIT(program, BPF_MOV64_REG(BPF_REG_2, BPF_REG_0)); - EMIT(program, BPF_ALU64_REG(BPF_SUB, BPF_REG_2, BPF_REG_1)); - - { - // Load log_rate_ns as a 64-bit immediate into R1. - const struct bpf_insn rate_insn[2] = { - BPF_LD_IMM64(BPF_REG_1, rule->log_rate_ns), - }; - EMIT(program, rate_insn[0]); - EMIT(program, rate_insn[1]); - } - - { - // Inner skip: delta < log_rate_ns means still within window. - _clean_bf_jmpctx_ struct bf_jmpctx rate_ctx = bf_jmpctx_get( - program, BPF_JMP_REG(BPF_JLT, BPF_REG_2, BPF_REG_1, 0)); - - EMIT(program, BPF_STX_MEM(BPF_DW, BPF_REG_9, BPF_REG_0, 0)); - - r = program->runtime.ops->gen_inline_log(program, rule); - if (r) - return r; - } - } - } else if (rule->log) { - r = program->runtime.ops->gen_inline_log(program, rule); - if (r) - return r; - } + r = _bf_program_generate_log(program, rule); + if (r) + return r; if (rule->has_counters) { EMIT(program, BPF_MOV64_REG(BPF_REG_1, BPF_REG_10)); diff --git a/src/libbpfilter/chain.c b/src/libbpfilter/chain.c index 4da4193c1..7491997e3 100644 --- a/src/libbpfilter/chain.c +++ b/src/libbpfilter/chain.c @@ -153,6 +153,7 @@ static int _bf_rule_has_incompatible_matchers(const struct bf_chain *chain, static int _bf_chain_check_rule(struct bf_chain *chain, struct bf_rule *rule) { + const uint8_t tuple = BF_FLAG(BF_LOG_OPT_5_TUPLE); int r; assert(rule); @@ -170,6 +171,13 @@ static int _bf_chain_check_rule(struct bf_chain *chain, struct bf_rule *rule) rule->disabled = r; } + if (rule->log != BF_LOG_OPT_DEFAULT && (rule->log & tuple) && + rule->log != tuple) { + return bf_err_r( + -EINVAL, + "5-tuple logging can't be combined with packet layer options"); + } + if (rule->log && rule->log != BF_LOG_OPT_DEFAULT && bf_hook_to_flavor(chain->hook) == BF_FLAVOR_CGROUP_SOCK_ADDR) { return bf_err_r( diff --git a/src/libbpfilter/include/bpfilter/elfstub.h b/src/libbpfilter/include/bpfilter/elfstub.h index 3f40334c7..854b3b5b2 100644 --- a/src/libbpfilter/include/bpfilter/elfstub.h +++ b/src/libbpfilter/include/bpfilter/elfstub.h @@ -170,6 +170,22 @@ enum bf_elfstub_id */ BF_ELFSTUB_SOCK_ADDR_LOG, + /** + * Log a packet 5-tuple to a ring buffer. + * + * `__u8 bf_pkt_5_tuple_log(struct bf_runtime *ctx, __u32 rule_id, __u32 verdict, __u32 l3_l4_proto)` + * + * **Parameters** + * - `ctx`: address of the `bf_runtime` context of the program. + * - `rule_id`: id of the matched rule. + * - `verdict`: verdict of the matched rule. + * - `l3_l4_proto`: layer 3 and layer 4 protocols packed as + * `(l3 << 16 | l4)`. + * + * **Return** 0 on success, or 1 on error. + */ + BF_ELFSTUB_PKT_5_TUPLE_LOG, + _BF_ELFSTUB_MAX, }; diff --git a/src/libbpfilter/include/bpfilter/runtime.h b/src/libbpfilter/include/bpfilter/runtime.h index 34c1db068..f1e740025 100644 --- a/src/libbpfilter/include/bpfilter/runtime.h +++ b/src/libbpfilter/include/bpfilter/runtime.h @@ -90,12 +90,23 @@ enum bf_log_opt */ BF_LOG_OPT_TRANSPORT, + /** + * Packet 5-tuple: source and destination addresses and ports, and the + * transport protocol. + */ + BF_LOG_OPT_5_TUPLE, + _BF_LOG_OPT_MAX, /** Log all available data for the hook type. */ BF_LOG_OPT_DEFAULT = 0xFF, }; +/** Log all available packet headers. */ +#define BF_LOG_PACKET_HEADERS \ + ((1ULL << BF_LOG_OPT_LINK) | (1ULL << BF_LOG_OPT_INTERNET) | \ + (1ULL << BF_LOG_OPT_TRANSPORT)) + /** * @brief Log entry type discriminator. */ @@ -107,6 +118,9 @@ enum bf_log_type /** Socket address log entry (cgroup_sock_addr). */ BF_LOG_TYPE_SOCK_ADDR, + /** Packet 5-tuple log entry (XDP, TC, NF, cgroup_skb). */ + BF_LOG_TYPE_PACKET_5_TUPLE, + _BF_LOG_TYPE_MAX, }; @@ -161,6 +175,24 @@ struct bf_log_sock_addr bf_aligned(8) __u8 daddr[sizeof(struct in6_addr)]; }; +/** + * @brief Packet 5-tuple log payload fields (XDP, TC, NF, cgroup_skb). + */ +struct bf_log_pkt_5_tuple +{ + /** Source address (4 bytes for IPv4, 16 for IPv6). */ + bf_aligned(8) __u8 saddr[sizeof(struct in6_addr)]; + + /** Destination address (4 bytes for IPv4, 16 for IPv6). */ + bf_aligned(8) __u8 daddr[sizeof(struct in6_addr)]; + + /** Source port in host byteorder. */ + __u16 sport; + + /** Destination port in host byteorder. */ + __u16 dport; +}; + /** * @brief Log structure published by a chain when the `log` action is hit. * @@ -196,11 +228,14 @@ struct bf_log * byteorder. * - `BF_LOG_TYPE_SOCK_ADDR`: use `sock_addr` — socket address, port, * and process metadata. + * - `BF_LOG_TYPE_PACKET_5_TUPLE`: use `pkt_5_tuple` — packet source and + * destination addresses and ports. */ union { struct bf_log_pkt pkt; struct bf_log_sock_addr sock_addr; + struct bf_log_pkt_5_tuple pkt_5_tuple; } BF_ANONYMOUS_MEMBER(payload); }; diff --git a/src/libbpfilter/rule.c b/src/libbpfilter/rule.c index f13b05780..61b5fa597 100644 --- a/src/libbpfilter/rule.c +++ b/src/libbpfilter/rule.c @@ -23,6 +23,7 @@ static const char *_bf_log_opt_strs[] = { [BF_LOG_OPT_LINK] = "link", [BF_LOG_OPT_INTERNET] = "internet", [BF_LOG_OPT_TRANSPORT] = "transport", + [BF_LOG_OPT_5_TUPLE] = "5-tuple", }; static_assert_enum_mapping(_bf_log_opt_strs, _BF_LOG_OPT_MAX); diff --git a/tests/e2e/CMakeLists.txt b/tests/e2e/CMakeLists.txt index eb1361071..083a8101d 100644 --- a/tests/e2e/CMakeLists.txt +++ b/tests/e2e/CMakeLists.txt @@ -152,6 +152,7 @@ bf_add_e2e_shell_test(e2e hooks/cgroup_sock_addr_sendmsg6.sh ROOT) bf_add_e2e_shell_test(e2e rulesets/rulesets.sh ROOT) bf_add_e2e_c_test(verdicts/next.cpp) +bf_add_e2e_c_test(actions/log_5_tuple.cpp) bf_add_e2e_c_test(matchers/icmp_code.cpp) bf_add_e2e_c_test(matchers/icmp_type.cpp) diff --git a/tests/e2e/actions/log_5_tuple.cpp b/tests/e2e/actions/log_5_tuple.cpp new file mode 100644 index 000000000..8f6ae7936 --- /dev/null +++ b/tests/e2e/actions/log_5_tuple.cpp @@ -0,0 +1,210 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + */ + +#include +#include + +#include "Chain.hpp" +#include "Matcher.hpp" +#include "Rule.hpp" +#include "test.hpp" + +extern "C" { +#include + +#include +#include + +#include +#include +} + +namespace +{ + +constexpr uint8_t kTupleLog = BF_FLAG(BF_LOG_OPT_5_TUPLE); + +static void assertTuple(const struct bf_log &log, uint16_t l3Proto, + uint8_t l4Proto, const char *saddr, const char *daddr, + uint16_t sport, uint16_t dport) +{ + int family = l3Proto == ETH_P_IP ? AF_INET : AF_INET6; + + assert_int_equal(BF_LOG_TYPE_PACKET_5_TUPLE, log.log_type); + assert_int_equal(l3Proto, log.l3_proto); + assert_int_equal(l4Proto, log.l4_proto); + assert_int_equal(0, log.rule_id); + assert_int_equal(BF_VERDICT_DROP, log.verdict); + bft_assert_log_address(log.pkt_5_tuple.saddr, family, saddr); + bft_assert_log_address(log.pkt_5_tuple.daddr, family, daddr); + assert_int_equal(sport, log.pkt_5_tuple.sport); + assert_int_equal(dport, log.pkt_5_tuple.dport); +} + +static void tupleLogging(void **state) +{ + _cleanup_close_ int fd = -1; + struct bft_log_capture capture; + struct ring_buffer *rb; + + (void)state; + + BFT_CHAIN_SET(bf::Chain("test_tuple_log", BF_HOOK_XDP, BF_VERDICT_ACCEPT) + << bf::Rule(BF_VERDICT_DROP, bf_counter(), kTupleLog, + {bf::Matcher::alwaysMatch()})); + + fd = bf_chain_logs_fd("test_tuple_log"); + assert_true(fd >= 0); + rb = ring_buffer__new(fd, bft_capture_log, &capture, nullptr); + assert_non_null(rb); + + bft_assert_prog_run( + "test_tuple_log", BF_HOOK_XDP, + bft::Ethernet() / + bft::IPv4 {.saddr = "192.0.2.10", .daddr = "198.51.100.20"} / + bft::TCP {.sport = 12345, .dport = 443}, + bft_hook_drop(BF_HOOK_XDP)); + assert_int_equal(1, ring_buffer__consume(rb)); + assertTuple(capture.entries.back(), ETH_P_IP, IPPROTO_TCP, "192.0.2.10", + "198.51.100.20", 12345, 443); + + bft_assert_prog_run( + "test_tuple_log", BF_HOOK_XDP, + bft::Ethernet() / + bft::IPv4 {.saddr = "203.0.113.1", .daddr = "203.0.113.2"} / + bft::UDP {.sport = 5353, .dport = 53}, + bft_hook_drop(BF_HOOK_XDP)); + assert_int_equal(1, ring_buffer__consume(rb)); + assertTuple(capture.entries.back(), ETH_P_IP, IPPROTO_UDP, "203.0.113.1", + "203.0.113.2", 5353, 53); + + bft_assert_prog_run( + "test_tuple_log", BF_HOOK_XDP, + bft::Ethernet() / + bft::IPv6 {.saddr = "2001:db8::10", .daddr = "2001:db8::20"} / + bft::TCP {.sport = 22, .dport = 60000}, + bft_hook_drop(BF_HOOK_XDP)); + assert_int_equal(1, ring_buffer__consume(rb)); + assertTuple(capture.entries.back(), ETH_P_IPV6, IPPROTO_TCP, "2001:db8::10", + "2001:db8::20", 22, 60000); + + bft_assert_prog_run( + "test_tuple_log", BF_HOOK_XDP, + bft::Ethernet() / + bft::IPv6 {.saddr = "2001:db8:1::1", .daddr = "2001:db8:1::2"} / + bft::UDP {.sport = 10000, .dport = 20000}, + bft_hook_drop(BF_HOOK_XDP)); + assert_int_equal(1, ring_buffer__consume(rb)); + assertTuple(capture.entries.back(), ETH_P_IPV6, IPPROTO_UDP, + "2001:db8:1::1", "2001:db8:1::2", 10000, 20000); + + /* Unsupported transport protocols still receive the rule verdict and are + * counted, but do not emit a log entry. */ + bft_assert_prog_run("test_tuple_log", BF_HOOK_XDP, + bft::Ethernet() / bft::IPv4 {} / bft::ICMPv4 {}, + bft_hook_drop(BF_HOOK_XDP)); + assert_int_equal(0, ring_buffer__consume(rb)); + assert_int_equal(4, capture.entries.size()); + + bft::Packet arp; + arp.len = bft::Ethernet().write(arp.data.data(), ETH_P_ARP); + bft_assert_prog_run("test_tuple_log", BF_HOOK_XDP, arp, + bft_hook_drop(BF_HOOK_XDP)); + assert_int_equal(0, ring_buffer__consume(rb)); + assert_int_equal(4, capture.entries.size()); + bft_assert_counter_eq("test_tuple_log", 0, 6, -1); + + ring_buffer__free(rb); +} + +static void rawLoggingUnchanged(void **state) +{ + _cleanup_close_ int fd = -1; + struct bft_log_capture capture; + struct ring_buffer *rb; + + (void)state; + + BFT_CHAIN_SET(bf::Chain("test_raw_log", BF_HOOK_XDP, BF_VERDICT_ACCEPT) + << bf::Rule(BF_VERDICT_DROP, std::nullopt, BF_LOG_OPT_DEFAULT, + {bf::Matcher::alwaysMatch()})); + + fd = bf_chain_logs_fd("test_raw_log"); + assert_true(fd >= 0); + rb = ring_buffer__new(fd, bft_capture_log, &capture, nullptr); + assert_non_null(rb); + + bft_assert_prog_run("test_raw_log", BF_HOOK_XDP, + bft::Ethernet() / bft::IPv4 {} / bft::TCP {}, + bft_hook_drop(BF_HOOK_XDP)); + assert_int_equal(1, ring_buffer__consume(rb)); + assert_int_equal(1, capture.entries.size()); + assert_int_equal(BF_LOG_TYPE_PACKET, capture.entries[0].log_type); + assert_int_equal(BF_LOG_PACKET_HEADERS, capture.entries[0].pkt.req_headers); + + ring_buffer__free(rb); +} + +static void tupleLogRateEligibility(void **state) +{ + _cleanup_close_ int fd = -1; + struct bft_log_capture capture; + struct ring_buffer *rb; + bf::Rule rule(BF_VERDICT_DROP, bf_counter(), kTupleLog, + {bf::Matcher::alwaysMatch()}, 60'000'000'000ULL); + + (void)state; + + BFT_CHAIN_SET( + bf::Chain("test_tuple_log_rate", BF_HOOK_XDP, BF_VERDICT_ACCEPT) + << rule); + + fd = bf_chain_logs_fd("test_tuple_log_rate"); + assert_true(fd >= 0); + rb = ring_buffer__new(fd, bft_capture_log, &capture, nullptr); + assert_non_null(rb); + + bft_assert_prog_run("test_tuple_log_rate", BF_HOOK_XDP, + bft::Ethernet() / bft::IPv4 {} / bft::ICMPv4 {}, + bft_hook_drop(BF_HOOK_XDP)); + assert_int_equal(0, ring_buffer__consume(rb)); + + /* The ignored ICMP packet must not consume the rate-limit interval. */ + bft_assert_prog_run("test_tuple_log_rate", BF_HOOK_XDP, + bft::Ethernet() / bft::IPv4 {} / bft::TCP {}, + bft_hook_drop(BF_HOOK_XDP)); + assert_int_equal(1, ring_buffer__consume(rb)); + assert_int_equal(1, capture.entries.size()); + bft_assert_counter_eq("test_tuple_log_rate", 0, 2, -1); + + ring_buffer__free(rb); +} + +} // namespace + +int main() +{ + int r = bf_ctx_setup(false, "/sys/fs/bpf", 0); + if (r != 0) { + bf_err("failed to setup bpfilter context: %s", std::strerror(-r)); + return 1; + } + + const struct CMUnitTest tests[] = { + cmocka_unit_test_setup_teardown(tupleLogging, bft_matcher_test_setup, + bft_matcher_test_teardown), + cmocka_unit_test_setup_teardown(tupleLogRateEligibility, + bft_matcher_test_setup, + bft_matcher_test_teardown), + cmocka_unit_test_setup_teardown(rawLoggingUnchanged, + bft_matcher_test_setup, + bft_matcher_test_teardown), + }; + + r = cmocka_run_group_tests(tests, nullptr, nullptr); + bf_ctx_teardown(); + + return r; +} diff --git a/tests/e2e/rules/log.sh b/tests/e2e/rules/log.sh index ce6b8712b..ce095dab3 100755 --- a/tests/e2e/rules/log.sh +++ b/tests/e2e/rules/log.sh @@ -20,6 +20,8 @@ DROP" ${BFCLI} ruleset set --dry-run --from-str "chain c1 BF_HOOK_TC_INGRESS ACCEPT rule ip4.proto icmp log NEXT" ${BFCLI} ruleset set --dry-run --from-str "chain c1 BF_HOOK_TC_INGRESS ACCEPT rule ip4.proto icmp log REDIRECT 1 in" ${BFCLI} ruleset set --dry-run --from-str "chain c1 BF_HOOK_TC_INGRESS ACCEPT rule ip4.proto icmp log mark 0x1 DROP" +${BFCLI} ruleset set --dry-run --from-str "chain c1 BF_HOOK_XDP ACCEPT rule ip4.proto tcp log 5-tuple DROP" +${BFCLI} ruleset set --dry-run --from-str "chain c1 BF_HOOK_XDP ACCEPT rule ip4.proto tcp log 5-tuple every 1s DROP" # log every : integer and float values, with and without headers ${BFCLI} ruleset set --dry-run --from-str "chain c1 BF_HOOK_XDP ACCEPT rule ip4.proto icmp log every 1s DROP" ${BFCLI} ruleset set --dry-run --from-str "chain c1 BF_HOOK_XDP ACCEPT rule ip4.proto icmp log every 500ms DROP" @@ -32,6 +34,10 @@ ${BFCLI} ruleset set --dry-run --from-str "chain c1 BF_HOOK_XDP ACCEPT rule ip4. (! ${BFCLI} ruleset set --dry-run --from-str "chain c1 BF_HOOK_XDP ACCEPT rule ip4.proto icmp log link,ip DROP") (! ${BFCLI} ruleset set --dry-run --from-str "chain c1 BF_HOOK_XDP ACCEPT rule ip4.proto icmp log link,,internet DROP") (! ${BFCLI} ruleset set --dry-run --from-str "chain c1 BF_HOOK_XDP ACCEPT rule ip4.proto icmp log @DROP") +(! ${BFCLI} ruleset set --dry-run --from-str "chain c1 BF_HOOK_XDP ACCEPT rule ip4.proto tcp log 5-tuple,link DROP") +(! ${BFCLI} ruleset set --dry-run --from-str "chain c1 BF_HOOK_XDP ACCEPT rule ip4.proto tcp log link,5-tuple DROP") +(! ${BFCLI} ruleset set --dry-run --from-str "chain c1 BF_HOOK_XDP ACCEPT rule ip4.proto tcp log 5_tuple DROP") +(! ${BFCLI} ruleset set --dry-run --from-str "chain c1 BF_HOOK_CGROUP_SOCK_ADDR_CONNECT4{cgpath=/sys/fs/cgroup} ACCEPT rule meta.l4_proto tcp log 5-tuple DROP") make_sandbox @@ -40,4 +46,4 @@ ${FROM_NS} ${BFCLI} chain set --from-str "chain chain_load_xdp_3 BF_HOOK_XDP ACC ${FROM_NS} ${BFCLI} chain set --from-str "chain chain_load_xdp_3 BF_HOOK_XDP ACCEPT rule ip4.proto icmp log link counter DROP" ${FROM_NS} ${BFCLI} chain set --from-str "chain chain_load_xdp_3 BF_HOOK_XDP ACCEPT rule ip4.proto icmp log link,internet counter DROP" ${FROM_NS} ${BFCLI} chain set --from-str "chain chain_load_xdp_3 BF_HOOK_XDP ACCEPT rule ip4.proto icmp log link,transport counter DROP" -${FROM_NS} ${BFCLI} chain set --from-str "chain chain_load_xdp_3 BF_HOOK_XDP ACCEPT rule ip4.proto icmp log internet,link counter DROP" \ No newline at end of file +${FROM_NS} ${BFCLI} chain set --from-str "chain chain_load_xdp_3 BF_HOOK_XDP ACCEPT rule ip4.proto icmp log internet,link counter DROP" diff --git a/tests/fuzz/keywords.dict b/tests/fuzz/keywords.dict index ea79bb994..c7df51987 100644 --- a/tests/fuzz/keywords.dict +++ b/tests/fuzz/keywords.dict @@ -137,6 +137,7 @@ "link" "internet" "transport" +"5-tuple" "ip4" "ip6" "ip4,tcp" diff --git a/tests/harness/Matcher.hpp b/tests/harness/Matcher.hpp index 2ad43449d..acc0d382d 100644 --- a/tests/harness/Matcher.hpp +++ b/tests/harness/Matcher.hpp @@ -5,7 +5,9 @@ #pragma once +#include #include +#include #include extern "C" { @@ -39,6 +41,15 @@ class Matcher _payload {std::move(payload)}, _negate {negate} {}; + static Matcher alwaysMatch() + { + float probability = 100.0f; + std::vector payload(sizeof(probability)); + + std::memcpy(payload.data(), &probability, sizeof(probability)); + return {BF_MATCHER_META_PROBABILITY, BF_MATCHER_EQ, std::move(payload)}; + } + [[nodiscard]] bf_matcher_type type() const { return _type; diff --git a/tests/harness/Rule.hpp b/tests/harness/Rule.hpp index c282de47b..15095bcfc 100644 --- a/tests/harness/Rule.hpp +++ b/tests/harness/Rule.hpp @@ -36,15 +36,18 @@ class Rule bf_verdict _verdict; std::optional _counters; uint8_t _log; + std::optional _logRateNs; std::vector _matchers; public: Rule(bf_verdict verdict, std::optional counters = std::nullopt, - uint8_t log = 0, std::vector matchers = {}): + uint8_t log = 0, std::vector matchers = {}, + std::optional logRateNs = std::nullopt): _verdict {verdict}, _counters {counters}, _log {log}, + _logRateNs {logRateNs}, _matchers {std::move(matchers)} {} @@ -64,6 +67,7 @@ class Rule throw std::runtime_error("failed to create bf_rule"); rule->log = _log; + rule->log_rate_ns = _logRateNs.value_or(0); rule->has_counters = _counters.has_value(); rule->verdict = _verdict; diff --git a/tests/harness/test.cpp b/tests/harness/test.cpp index ba518d4cf..3d6f4ed4a 100644 --- a/tests/harness/test.cpp +++ b/tests/harness/test.cpp @@ -69,6 +69,28 @@ void bft_assert_prog_run(const char *chain_name, enum bf_hook hook, assert_int_equal(expected, r); } +int bft_capture_log(void *ctx, void *data, size_t size) +{ + auto *capture = static_cast(ctx); + struct bf_log log = {}; + + assert_int_equal(sizeof(log), size); + std::memcpy(&log, data, sizeof(log)); + capture->entries.push_back(log); + + return 0; +} + +void bft_assert_log_address(const uint8_t *actual, int family, + const char *expected) +{ + std::array addr = {}; + size_t len = family == AF_INET ? sizeof(struct in_addr) : sizeof(addr); + + assert_int_equal(1, inet_pton(family, expected, addr.data())); + assert_memory_equal(addr.data(), actual, len); +} + namespace { // TCX_PASS/TCX_DROP/TCX_NEXT are enum values in linux/bpf.h, but may conflict diff --git a/tests/harness/test.hpp b/tests/harness/test.hpp index 469192a79..2e96189c8 100644 --- a/tests/harness/test.hpp +++ b/tests/harness/test.hpp @@ -78,6 +78,16 @@ int bft_matcher_test_teardown(void **state); void bft_assert_prog_run(const char *chain_name, enum bf_hook hook, const bft::Packet &pkt, int expected); +struct bft_log_capture +{ + std::vector entries; +}; + +int bft_capture_log(void *ctx, void *data, size_t size); + +void bft_assert_log_address(const uint8_t *actual, int family, + const char *expected); + /** * @brief Encode a port number as a 2-byte big-endian payload. * diff --git a/tests/unit/libbpfilter/chain.c b/tests/unit/libbpfilter/chain.c index a7e86db1b..a011a63b8 100644 --- a/tests/unit/libbpfilter/chain.c +++ b/tests/unit/libbpfilter/chain.c @@ -346,17 +346,52 @@ static void invalid_log_opts_for_hook(void **state) { (void)state; - // Per-field log options on a sock_addr hook - _free_bf_chain_ struct bf_chain *chain = NULL; - _clean_bf_list_ bf_list rules = bf_list_default(bf_rule_free, bf_rule_pack); - struct bf_rule *r0 = NULL; + { + // Per-field log options on a sock_addr hook + _free_bf_chain_ struct bf_chain *chain = NULL; + _clean_bf_list_ bf_list rules = + bf_list_default(bf_rule_free, bf_rule_pack); + struct bf_rule *r0 = NULL; - assert_ok(bf_rule_new(&r0)); - r0->log = BF_FLAG(BF_LOG_OPT_LINK); - assert_ok(bf_list_add_tail(&rules, r0)); + assert_ok(bf_rule_new(&r0)); + r0->log = BF_LOG_PACKET_HEADERS; + assert_ok(bf_list_add_tail(&rules, r0)); - assert_err(bf_chain_new(&chain, "test", BF_HOOK_CGROUP_SOCK_ADDR_CONNECT4, - BF_VERDICT_ACCEPT, NULL, &rules)); + assert_err(bf_chain_new(&chain, "test", + BF_HOOK_CGROUP_SOCK_ADDR_CONNECT4, + BF_VERDICT_ACCEPT, NULL, &rules)); + } + + { + // 5-tuple logging on a sock_addr hook + _free_bf_chain_ struct bf_chain *chain = NULL; + _clean_bf_list_ bf_list rules = + bf_list_default(bf_rule_free, bf_rule_pack); + struct bf_rule *r0 = NULL; + + assert_ok(bf_rule_new(&r0)); + r0->log = BF_FLAG(BF_LOG_OPT_5_TUPLE); + assert_ok(bf_list_add_tail(&rules, r0)); + + assert_err(bf_chain_new(&chain, "test", + BF_HOOK_CGROUP_SOCK_ADDR_CONNECT4, + BF_VERDICT_ACCEPT, NULL, &rules)); + } + + { + // 5-tuple is exclusive from packet layer options + _free_bf_chain_ struct bf_chain *chain = NULL; + _clean_bf_list_ bf_list rules = + bf_list_default(bf_rule_free, bf_rule_pack); + struct bf_rule *r0 = NULL; + + assert_ok(bf_rule_new(&r0)); + r0->log = BF_FLAG(BF_LOG_OPT_5_TUPLE) | BF_FLAG(BF_LOG_OPT_INTERNET); + assert_ok(bf_list_add_tail(&rules, r0)); + + assert_err(bf_chain_new(&chain, "test", BF_HOOK_XDP, BF_VERDICT_ACCEPT, + NULL, &rules)); + } } static void get_set_by_name(void **state)