Skip to content

lib: log: support packet 5-tuple logging - #578

Open
blackdragoon26 wants to merge 1 commit into
facebook:mainfrom
blackdragoon26:issue-568-5-tuple-log
Open

lib: log: support packet 5-tuple logging#578
blackdragoon26 wants to merge 1 commit into
facebook:mainfrom
blackdragoon26:issue-568-5-tuple-log

Conversation

@blackdragoon26

Copy link
Copy Markdown
Contributor

Summary

Add log 5-tuple as an exclusive packet logging mode.

The implementation introduces a dedicated ELF stub that emits compact records
containing source and destination addresses, source and destination ports, and
the transport protocol. It supports complete IPv4/IPv6 TCP/UDP tuples while
preserving the existing fixed-size struct bf_log reservation and ring-buffer
capacity.

Unsupported packets do not emit a tuple record or fall back to raw-header
logging. Their counters, marks, verdicts, and normal rule execution remain
unchanged.

Related issue

Fixes #568

Testing

Added coverage for:

  • valid log 5-tuple syntax, with and without every
  • rejection of combinations with packet-layer options
  • rejection on cgroup_sock_addr
  • option conversion and rule serialization
  • IPv4 and IPv6 with TCP and UDP
  • tuple addresses, ports, protocols, rule ID, and verdict
  • no tuple record for ICMP and non-IP packets
  • counters and verdicts when tuple logging is skipped
  • unsupported packets not consuming the rate-limit interval
  • unchanged raw-header logging behaviour

Debug build with sanitizers and coverage enabled:

100% tests passed, 0 tests failed out of 122

Label Time Summary:
check          = 16.28 sec*proc (2 tests)
e2e            = 99.33 sec*proc (94 tests)
integration    =  0.14 sec*proc (2 tests)
unit           =  0.65 sec*proc (23 tests)

Release build:

100% tests passed, 0 tests failed out of 122

Label Time Summary:
check          = 15.02 sec*proc (2 tests)
e2e            = 57.04 sec*proc (94 tests)
integration    =  0.14 sec*proc (2 tests)
unit           =  0.22 sec*proc (23 tests)

Additional passed verification:

  • make -C build-issue568 fixstyle
  • make -C build-issue568 test_bin test
  • make -C build-issue568 doc
  • debug ASan/UBSan execution
  • coverage generation — 84.4% lines and 95.5% functions
  • final lint, IWYU, checkstyle, and diff whitespace checks

Notes for the reviewer

5-tuple is mutually exclusive with link, internet, and transport.
Tuple records are emitted only for IPv4/IPv6 packets using TCP or UDP, following
the eligibility behaviour discussed in #568. The eligibility check happens
before rate-limit bookkeeping, so an unsupported packet does not consume the
rule's every interval.
cgroup_sock_addr is intentionally unsupported for this mode. Existing raw
packet logging and socket-address logging remain unchanged.
The new log type and ELF-stub identifiers are appended so existing identifier
values remain stable.

AI disclosure

I used AI for codebase exploration, understanding the existing logging and BPF
code-generation paths, design iteration, parts of the implementation and tests,
verification, review, and wording of this PR.
I reviewed the resulting design and test evidence and remain responsible for
the submitted changes.

Checklist

  • I understand every line in this PR and can explain why it is correct
  • I built the project and ran the tests covering this change
  • make -C $BUILD test_bin test passes and the code follows the style guide
  • Commits are formatted as component: subcomponent: short description
  • I have disclosed any AI usage above

Comment thread doc/usage/bfcli.rst

- ``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 do not fall back to packet-layer logging.
- ``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

Comment thread src/bfcli/lexer.l Outdated
}
}

5-tuple { yylval.sval = strdup(yytext); return STRING; }

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.

Don't create a specific token for the 5-tuple keyword, update the definition of STRING below to support - in strings instead.

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.

OH Okay, will do that

Comment thread src/bfcli/print.c

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.

Comment thread tests/fuzz/keywords.dict Outdated
"link,internet"
"internet,transport"
"link,internet,transport"
"log 5-tuple"

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.

This can be removed.

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.

Okayy

Comment thread src/libbpfilter/chain.c
rule->disabled = r;
}

if (rule->log != BF_LOG_OPT_DEFAULT && (rule->log & tuple) &&

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.

BF_LOG_DEFAULT should represent link,internet,transport logs.

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.

Will, correct this masking situation

Comment thread tests/e2e/rules/log_5_tuple.cpp Outdated
Comment on lines +36 to +78
struct LogCapture
{
std::vector<struct bf_log> entries;
};

static int captureLog(void *ctx, void *data, size_t size)
{
auto *capture = static_cast<LogCapture *>(ctx);
struct bf_log log = {};

assert_int_equal(sizeof(log), size);
std::memcpy(&log, data, sizeof(log));
capture->entries.push_back(log);

return 0;
}

static void assertAddress(const uint8_t *actual, int family,
const char *expected)
{
std::array<uint8_t, sizeof(struct in6_addr)> 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);
}

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);
assertAddress(log.pkt_5_tuple.saddr, family, saddr);
assertAddress(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);
}

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.

Those are generic functions that should be part of the testing harness instead.

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.

Hmmm makes sense will do that

Comment thread src/libbpfilter/cgen/program.c Outdated
Comment on lines +638 to +640
// 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.

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.

Use /* */ for multiline comments.

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.

Understood

Comment thread src/libbpfilter/cgen/program.c Outdated
Comment on lines +519 to +522
// 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.

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.

Use /* */ for multiline comments.

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.

Okay

Comment thread src/libbpfilter/cgen/program.c Outdated
Comment on lines +526 to +527
// Outer skip: state_map is NULL (shouldn't happen at runtime,
// but the verifier requires the NULL check).

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.

Use /* */ for multiline comments.

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.

Cool

Comment thread src/libbpfilter/cgen/program.c Outdated
Comment on lines 637 to 662
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));
{
// 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));
_clean_bf_jmpctx_ struct bf_jmpctx 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));
{
// 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));
_clean_bf_jmpctx_ struct bf_jmpctx l4_ctx =
bf_jmpctx_get(program, BPF_JMP_A(0));

r = program->runtime.ops->gen_inline_log(program, rule);
r = _bf_program_generate_log(program, rule);
if (r)
return r;
}
}
} else if (rule->log) {
r = program->runtime.ops->gen_inline_log(program, rule);
} else {
r = _bf_program_generate_log(program, rule);
if (r)
return r;
}

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.

This could be simplified:

if log:
    if log 5-tuple:
        setup 5-tuple logging
    bf_program_generate_log();

In bf_program_generate_log:

if log_rate_ns:
    rate-limit setup

gen_inline_log();

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.

Agreed. I will keep tuple eligibility setup separate and then call that common logging generator once.

@blackdragoon26

Copy link
Copy Markdown
Contributor Author

Thanks a lot for your detailed review. @qdeslandes
I will be needing bit of time to replying and working on all of em, by the upcoming week.
Regards
Sankalp

@blackdragoon26

Copy link
Copy Markdown
Contributor Author

@qdeslandes , can you rerun this CI again.
Thanks

@blackdragoon26

Copy link
Copy Markdown
Contributor Author

@qdeslandes Could you review this pr.

@yaakov-stein

Copy link
Copy Markdown
Contributor

@claude review

@github-actions

github-actions Bot commented Aug 24, 2026

Copy link
Copy Markdown

Claude review of PR #578 (acc0dcd)

Suggestions

  • Pack format backward compatsrc/libbpfilter/chain.c:174 — New exclusivity check rejects old serialized log = 0xFF (former BF_LOG_OPT_DEFAULT); deserialized chains from older pack files would fail validation
  • Lexer STRING pattern overly broadsrc/bfcli/lexer.l:293 — Adding - to the global STRING character class affects all tokens, not just 5-tuple; a targeted keyword rule or non-leading-hyphen restriction would be safer

Nits

  • Use BF_FLAGS macrosrc/libbpfilter/include/bpfilter/runtime.h:103BF_LOG_DEFAULT manually spells (1ULL << X) | ... instead of using the project's BF_FLAGS() helper
  • inet_ntop uncheckedsrc/bfcli/print.c:698inet_ntop return not checked; buffers are uninitialized if it returns NULL
  • Commit message — Second commit lib: log: address 5-tuple review feedback doesn't describe what changed; per project guidelines the subject should be imperative and describe the change itself

Workflow run

Comment thread src/libbpfilter/chain.c Outdated
Comment thread src/bfcli/lexer.l
}

[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.

};

/** Log all available packet headers. */
#define BF_LOG_DEFAULT \

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: nit: BF_LOG_DEFAULT manually writes (1ULL << X) | ... while the project provides BF_FLAG() and BF_FLAGS() in helper.h for exactly this pattern (used extensively in matcher.c, hook.c, opts.c, set.c). Consider:

#define BF_LOG_DEFAULT BF_FLAGS(BF_LOG_OPT_LINK, BF_LOG_OPT_INTERNET, BF_LOG_OPT_TRANSPORT)

Comment thread src/bfcli/print.c
@yaakov-stein

yaakov-stein commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

I'll go over claude's comments (to check their accuracy) and give this a review sometime in the next few days

@blackdragoon26

Copy link
Copy Markdown
Contributor Author

I'll go over claude's comments (to check their accuracy) and give this a review sometime in the next few days

Alrighty!

@yaakov-stein yaakov-stein left a comment

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.

Overall looks pretty good, left a few comments and please squash the follow-up fix commit into the original commit so the change lands as one self-contained commit.

BF_LOG_OPT_5_TUPLE,

/** Log all available data for the hook type. */
BF_LOG_OPT_DEFAULT = 0xFF,

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.

At the moment, I don't believe we can remove this. While the other options (BF_LOG_OPT_(LINK|INTERNET|TRANSPORT)) are used for their specific bit (and thus _BF_LOG_OPT_MAX < 8 as it is a uint8_t), BF_LOG_OPT_DEFAULT is a sentinel meant to signify "log everything" in both the packet and CGROUP_SOCK_ADDR context (and anything else we may add in the future). This value was chosen because it is by definition both different than any other value (it's not a single bit, it's all the bits) and also will match every bit of the other values when used as a bit mask.

Therefore, if we remove this, we can no longer differentiate between log link,internet,transport and log, and thus 1) log link,internet,transport becomes valid syntax for CGROUP_SOCK_ADDR, which doesn't make much sense, and 2) not as much of a problem, but we can't print what the user gave as input for packet logging.

Check out #517 for a bit more background if you're interested.

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.

Oh I see, will be checking in that pr, I really appreciate your detailed reasoning.
I missed that BF_LOG_OPT_DEFAULT is intentionally a semantic sentinel for bare log, rather than only a packet-header mask.
I will restore the sentinel, retain BF_LOG_DEFAULT for the explicit link,internet,transport mask, and preserve that distinction through validation, CLI printing, and packet codegen.

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 instead of keeping BF_LOG_DEFAULT (which is confusing with BF_LOG_OPT_DEFAULT), let's rename it to something like BF_LOG_PACKET_HEADERS or something like that.

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.

Okayy, will do that.

Comment thread src/bfcli/lexer.l
}

[a-zA-Z0-9_]+ { yylval.sval = strdup(yytext); return STRING; }
[a-zA-Z0-9_-]+ { yylval.sval = strdup(yytext); return STRING; }

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 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.

Comment thread src/libbpfilter/chain.c Outdated
}

if (rule->log && rule->log != BF_LOG_OPT_DEFAULT &&
if ((rule->log & tuple) && (rule->log & tuple) != rule->log) {

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.

Per this comment, we'll need to switch this to:

if (rule->log != BF_LOG_OPT_DEFAULT &&
      (rule->log & tuple) && rule->log != tuple) {

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.

Oh Cool, I will use the suggested condition.

@blackdragoon26

Copy link
Copy Markdown
Contributor Author

Thanks for the review @yaakov-stein. I’ve addressed the requested changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support 5-tuple logging

4 participants