Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions doc/usage/bfcli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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**
Expand Down Expand Up @@ -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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"unsupported packets do not fall back to packet-layer logging"

-> "unsupported packets are not logged."

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alright, will simplify the wording


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``.
Expand Down
2 changes: 1 addition & 1 deletion src/bfcli/lexer.l
Original file line number Diff line number Diff line change
Expand Up @@ -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; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: suggestion: The pattern change from [a-zA-Z0-9_]+ to [a-zA-Z0-9_-]+ allows hyphens in all STRING tokens, but only 5-tuple needs it. Under flex longest-match rules, input like -foo (no leading space) becomes a single STRING rather than two tokens (- + foo). In practice this is low risk since the grammar expects whitespace between tokens, but a more targeted approach would be safer — e.g., matching 5-tuple as a dedicated keyword before the catch-all, or restricting hyphens to non-leading positions: [a-zA-Z0-9_][a-zA-Z0-9_-]*.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is probably fine as is. The weird part is that -- and - become valid set and chain names, but __ and _ already were.


. { return *yytext; }

Expand Down
52 changes: 52 additions & 0 deletions src/bfcli/print.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 : <unknown protocol 0x%04x>\n",
log->l3_proto);
return;
}

Comment thread
yaakov-stein marked this conversation as resolved.
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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a comment that this only works because l4_proto is either TCP or UDP, and those are defined in bf_ipproto_to_str.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha, will add comment for improved documentation and understanding.

/* 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);
Expand All @@ -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;
}
Expand Down
1 change: 1 addition & 0 deletions src/libbpfilter/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ bf_target_add_elfstubs(libbpfilter
"pkt_log"
"flow_hash"
"sock_addr_log"
"pkt_5_tuple_log"
)

target_compile_definitions(libbpfilter
Expand Down
74 changes: 74 additions & 0 deletions src/libbpfilter/bpf/pkt_5_tuple_log.bpf.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*/

#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/in.h>
#include <linux/ip.h>
#include <linux/ipv6.h>
#include <linux/tcp.h>
#include <linux/udp.h>

#include <bpf/bpf_endian.h>
#include <bpf/bpf_helpers.h>
#include <stddef.h>

#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;
}
33 changes: 26 additions & 7 deletions src/libbpfilter/cgen/packet.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
128 changes: 76 additions & 52 deletions src/libbpfilter/cgen/program.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@

#include <linux/bpf.h>
#include <linux/bpf_common.h>
#include <linux/if_ether.h>
#include <linux/in.h> // NOLINT
#include <linux/limits.h>

#include <endian.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
Expand Down Expand Up @@ -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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think once we are making a _bf_program_generate_log method, let's put all of the logging logic there and can keep it simple doing something like this:

  static int _bf_program_generate_log(struct bf_program *program,
                                      const struct bf_rule *rule)
  {
      // Declare and assert

      if (!rule->log)
          return 0;

      if (rule->log == BF_FLAG(BF_LOG_OPT_5_TUPLE)) {
          ...
      }

      if (rule->log_rate_ns) {
          ...
      }

      return program->runtime.ops->gen_inline_log(program, rule);
  }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alrighty, I will move the no-log early return, 5-tuple eligibility guards, rate-limit handling, and final gen_inline_log() call into _bf_program_generate_log().
so, _bf_program_generate_rule() will then call this helper once, while unsupported tuple packets will still continue through counters, marks, and verdict generation.

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)
{
Expand Down Expand Up @@ -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));
Expand Down
Loading
Loading