Repository navigation
Conversation
3d56c6e to
63e5923
Compare
qdeslandes
left a comment
There was a problem hiding this comment.
I went for an early review, mostly for the parsing part. On the BPF side, the map will contain the runtime values for a given rule's rate limit (current burst/allowance, limit, last update time...).
df768e9 to
fd116e7
Compare
This comment was marked as outdated.
This comment was marked as outdated.
0ecf4d4 to
699137e
Compare
|
This PR has had no activity for 14 days. It will be closed in 14 more days unless it is updated. Comment or push to keep it open. |
That's on me, looking at this today :) |
qdeslandes
left a comment
There was a problem hiding this comment.
Changing the rate limit to a matcher is the good call, it will provide more flexibility.
The implementation has been simplified a lot, so much that so of the functionalities we would deem necessary have been removed.
You will need to think about two things now:
- How do you handle the unit? For now, supporting only
s(e.g.meta.limit 10/s) would be enough, but allowing a unit to be passed will allow us to expand support for rate limiting later on. - How to handle access to the
bf_ratelimit(or whatever the name is) structure from a rule? Rules have IDs, but nothing prevents a rule from having multiple rate-limiting matchers. An BPF array map is a good pick, but you need to ensure ameta.limitmatcher is mapped to the correctbf_ratelimitstructure in the map.
Also, you'll have to rebase on main :)
26e37be to
1187fbb
Compare
|
I think I figured it out |
qdeslandes
left a comment
There was a problem hiding this comment.
Some comments, but it's on the right track.
| bf_parse_err("failed to create new limit"); | ||
|
|
||
| bf_list_add_tail(&ruleset->limits, limit); | ||
| snprintf(payload, sizeof(payload), "%zu", (uint32_t)idx); |
There was a problem hiding this comment.
The payload of the meta.limit matcher should not be a string, but an actual value.
| if (errno == 0) { | ||
| *(uint32_t *)payload = idx; | ||
| return 0; | ||
| } |
There was a problem hiding this comment.
You need to reject trailing characters (*endptr != \0\) and prevent integer overflow.
| assert(payload); | ||
|
|
||
| if (*(uint64_t *)payload >> 32 == BF_TIME_S) | ||
| (void)fprintf(stdout, "%d/s", *(uint32_t *)payload); |
There was a problem hiding this comment.
What are you trying to do here?
|
Since I switched from But that means Which feels wrong. |
|
@claude review |
Claude review of PR #346 (20c5847)Must fix
Suggestions
Nits
|
| return 0; | ||
| } | ||
|
|
||
| void bf_limit_free(struct bf_ratelimit **ratelimit) |
There was a problem hiding this comment.
Claude: must-fix: bf_limit_free is a no-op — it casts away the parameter and never frees the calloc'd struct. This means:
- Every
_free_bf_limit_-annotated local that goes out of scope on an error path leaks. bf_list_cleanon a chain'slimitslist (which usesbf_limit_freeas the free callback) leaks every node's data.
Every other _free function in the codebase follows the established contract (see bf_set_free in set.c, bf_rule_free in rule.c). Fix:
void bf_limit_free(struct bf_ratelimit **ratelimit)
{
assert(ratelimit);
if (!*ratelimit)
return;
BF_FREEP(ratelimit);
}| assert(program); | ||
| assert(matcher); | ||
|
|
||
| uint32_t key = *(uint64_t *)bf_matcher_payload(matcher); |
There was a problem hiding this comment.
Claude: must-fix: Out-of-bounds read: the payload was stored as uint32_t (4 bytes) in the parser (sizeof(limit_id)), but here it is read through a uint64_t *, reading 8 bytes from a 4-byte buffer. This is undefined behavior.
Every other meta matcher in this file casts to the type that was stored (e.g., *(uint32_t *)bf_matcher_payload(matcher) for iface at line 40). Fix:
uint32_t key = *(uint32_t *)bf_matcher_payload(matcher);|
|
||
| uint32_t key = *(uint64_t *)bf_matcher_payload(matcher); | ||
| struct bf_ratelimit *node = | ||
| bf_list_get_at(&program->runtime.chain->limits, key); |
There was a problem hiding this comment.
Claude: must-fix: bf_list_get_at returns NULL when the index is out of bounds (see core/list.c:222), but node is dereferenced on line 128 without a NULL check. The existing pattern in chain.c:55-58 guards against this:
set = bf_list_get_at(&chain->sets, set_index);
if (!set)
return bf_err_r(-EINVAL, "rule %u references non-existent set", ...);Add a similar guard here and return an appropriate error.
| assert(ratelimit); | ||
| assert(raw_payload); | ||
|
|
||
| limit = strtoul(raw_payload, &endptr, BF_BASE_10); |
There was a problem hiding this comment.
Claude: must-fix: The strtoul result validation has several issues:
limitisuint32_tbutstrtoulreturnsunsigned long(64-bit on LP64). The return value is silently truncated before the range check, so overflow is undetected.limit <= 0on an unsigned type is equivalent tolimit == 0— the<part is dead.limit > UINT32_MAXis always false for auint32_t.errnois never checked forERANGE.- No check for trailing characters after
's'(e.g.,"10/sfoo"would pass). - Line 54 returns
-ENOMEMbut this is a validation error — should be-EINVAL.
The existing pattern in matcher.c uses errno = 0 before the call and checks errno == ERANGE afterward. Use an unsigned long intermediate and validate the range before narrowing:
errno = 0;
unsigned long parsed = strtoul(raw_payload, &endptr, BF_BASE_10);
if (errno == ERANGE || parsed == 0 || parsed > UINT32_MAX ||
endptr[0] != '/' || endptr[1] != 's' || endptr[2] != '\0')
return bf_err_r(-EINVAL, "limit is not valid '%s'", raw_payload);
limit = (uint32_t)parsed;There was a problem hiding this comment.
Check how we parse integers for matchers, that logic could be reused.
| if (r) | ||
| return bf_err_r(r, "failed to load the state map"); | ||
|
|
||
| r = _bf_program_load_limit_map(prog); |
There was a problem hiding this comment.
Claude: suggestion: _bf_program_load_limit_map is called unconditionally for every program, even when no rules use meta.limit. This creates an unnecessary BPF array map and wastes a file descriptor for every chain.
Other optional maps guard on chain state before creating (e.g., _bf_program_load_log_map returns early when BF_CHAIN_LOG is unset). Consider returning 0 early when the chain has no limits:
if (bf_list_is_empty(&program->runtime.chain->limits))
return 0;| return bf_err_r(-ENOMEM, "limit is not valid '%s'", raw_payload); | ||
| } | ||
|
|
||
| _raw_payload = strdup(raw_payload); |
There was a problem hiding this comment.
Claude: suggestion: _raw_payload is strdup'd but never read — the parsed limit value is used directly. The allocation is cleaned up by _cleanup_free_ so there is no leak, but the strdup and its error path are dead code. This appears copied from bf_set_new_from_raw where the duplicate is actually tokenized. Remove lines 40 and 57-60.
|
|
||
| static int _bf_program_load_limit_map(struct bf_program *program) | ||
| { | ||
| _cleanup_free_ void *pstr = NULL; |
There was a problem hiding this comment.
Claude: suggestion: _cleanup_free_ void *pstr = NULL; is declared but never used. This was likely copied from _bf_program_load_printer_map where pstr holds the assembled printer output. Remove it.
| */ | ||
| struct bf_ratelimit_data | ||
| { | ||
| /** Current timestamp (at the last call of the BPF) */ |
There was a problem hiding this comment.
Claude: nit: The Doxygen comment says "Current timestamp (at the last call of the BPF)" but the field is a packet counter (ratelimit->current++ in limit.bpf.c), not a timestamp. Consider: "Number of packets seen in the current rate-limit window."
| * Matcher definition. | ||
| * | ||
| * Matchers are criterias to match the packet against. A set of matcher defines | ||
| * Matchers are criterias to match the packet against. A set of bf_matcher_limit defines |
There was a problem hiding this comment.
Claude: nit: Unrelated comment change: "A set of matcher defines" was changed to "A set of bf_matcher_limit defines". This Doxygen block documents the general bf_matcher concept, not rate limiting. Revert to the original wording.
| #include <stddef.h> | ||
| #include <stdint.h> | ||
|
|
||
| #include <bpfilter/core/hashset.h> |
There was a problem hiding this comment.
Claude: nit: Several includes are unused in this header: <bpfilter/core/hashset.h>, <bpfilter/dump.h>, <bpfilter/matcher.h>, <stdbool.h>, and <stddef.h>. Only <stdint.h> (for uint32_t) and <bpfilter/pack.h> (for bf_rpack_node_t, bf_wpack_t) are needed.
qdeslandes
left a comment
There was a problem hiding this comment.
The approach here is sound, but there are some minor fixes to do:
- Fix review comments and Claude comments
- Create atomic commits
- Add tests
| uint32_t limit_id = bf_list_size(&ruleset->limits); | ||
| int r; | ||
|
|
||
| struct bf_ratelimit *limit = NULL; |
There was a problem hiding this comment.
Group it with other variables.
| r = bf_map_new(&program->handle->rmap, _BF_LIMIT_MAP_NAME, | ||
| BF_MAP_TYPE_LIMIT, sizeof(uint32_t), | ||
| sizeof(struct bf_ratelimit_data), | ||
| bf_max(1, program->runtime.chain->limits.len)); |
There was a problem hiding this comment.
bf_max(1, program->runtime.chain->limits.len) I assume this it to prevent errors when the limits map is empty. If so, the map should not be created.
| _free_bf_limit_ struct bf_ratelimit *_ratelimit = NULL; | ||
| _cleanup_free_ char *_raw_payload = NULL; | ||
|
|
||
| char *endptr; | ||
| uint32_t limit; | ||
| uint32_t duration; | ||
| int r; |
There was a problem hiding this comment.
Group variables definition.
| assert(ratelimit); | ||
| assert(raw_payload); | ||
|
|
||
| limit = strtoul(raw_payload, &endptr, BF_BASE_10); |
There was a problem hiding this comment.
Check how we parse integers for matchers, that logic could be reused.
Fix #215
Hi, sorry for the long hiatus,
A bunch of stuff came up, and I didn't have the time to do the big rebase+refactor.
Changes:
ratelimitfrom anactionto amatcher, supports foreqwith anuint32elfstub.chain my_chain BF_HOOK_XDP{ifindex=2} DROP rule meta.ratelimit eq 10 ACCEPTchain my_chain BF_HOOK_XDP{ifindex=2} ACCEPT rule meta.ratelimit eq 10 DROPchain my_chain BF_HOOK_XDP{ifindex=2} DROP rule meta.ratelimit not eq 10 ACCEPTchain my_chain BF_HOOK_XDP{ifindex=2} ACCEPT rule meta.ratelimit not eq 10 DROPIf that implementation seems good, I'll go ahead and add QoL / the documentation
I'd really like to get this PR over the finish line at some point :)