From adfb02bcb9b3e4d83fdac2e807683c04e00065c2 Mon Sep 17 00:00:00 2001 From: Leo Di Donato <120051+leodido@users.noreply.github.com> Date: Mon, 4 May 2026 00:10:38 +0000 Subject: [PATCH 1/4] refactor: extend input unions with multi-value structs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add ethertypes and protos structs to the input union in both chain_entry (api/chain.h) and struct config (api/api.h.in). Add MAX_MULTI_VALUES define (8) shared across all multi-value types. No behavioral change — the new union members are not yet populated by any parser or consumed by any BPF program. Co-authored-by: Ona --- api/api.h.in | 13 +++++++++++-- api/chain.h | 15 +++++++++++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/api/api.h.in b/api/api.h.in index b89cfa4..5e39cab 100644 --- a/api/api.h.in +++ b/api/api.h.in @@ -7,6 +7,7 @@ /// defines #define TOOL_NAME "traffico" #define NUM_PROGRAMS ${PROGRAMS_COUNT} +#define MAX_MULTI_VALUES 8 #define PROGRAMS_DESCRIPTION ${PROGRAMS_DESCRIPTION} /// globals @@ -33,8 +34,16 @@ struct config bool has_input; union { - __u32 ip; - __u16 port; + __u32 ip; // allow_ipv4, allow_dns, block_ipv4 + __u16 port; // allow_port, block_port + struct { + __u16 values[MAX_MULTI_VALUES]; + __u8 count; + } ethertypes; // allow_ethertype (future) + struct { + __u8 values[MAX_MULTI_VALUES]; + __u8 count; + } protos; // allow_proto (future) } input; }; diff --git a/api/chain.h b/api/chain.h index 7d00d64..9d1ea43 100644 --- a/api/chain.h +++ b/api/chain.h @@ -19,14 +19,25 @@ #define MAX_CHAIN_LEN 8 #define BPFFS_BASE "/sys/fs/bpf/traffico" +/// Maximum number of values in a multi-value input (ethertypes, protos). +#define MAX_MULTI_VALUES 8 + /// A single entry in a chain: program type + input value. struct chain_entry { program_t program; bool has_input; union { - __u32 ip; - __u16 port; + __u32 ip; // allow_ipv4, allow_dns, block_ipv4 + __u16 port; // allow_port, block_port + struct { + __u16 values[MAX_MULTI_VALUES]; + __u8 count; + } ethertypes; // allow_ethertype (future) + struct { + __u8 values[MAX_MULTI_VALUES]; + __u8 count; + } protos; // allow_proto (future) } input; }; From e6ea8933bfb9828fd47d79b6119100cf61f84a5a Mon Sep 17 00:00:00 2001 From: Leo Di Donato <120051+leodido@users.noreply.github.com> Date: Mon, 4 May 2026 00:11:40 +0000 Subject: [PATCH 2/4] feat: add multi-value input parsers for ethertypes and protos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add parse_ethertypes() and parse_protos() helpers to input_parse.h. parse_ethertypes() splits on '+', accepts symbolic names (ipv4, ipv6, arp) and 0x-prefixed hex values. parse_protos() splits on '+', accepts symbolic names (tcp, udp, icmp) and decimal numbers. Both validate: no empty lists, no duplicates, no overflow beyond MAX_MULTI_VALUES, no out-of-range values. These helpers are not yet wired into parse_input() — the cases will be added when the corresponding BPF programs land and the enum values exist. Co-authored-by: Ona --- api/input_parse.h | 200 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 200 insertions(+) diff --git a/api/input_parse.h b/api/input_parse.h index 0ad8ab0..b87f33e 100644 --- a/api/input_parse.h +++ b/api/input_parse.h @@ -3,9 +3,209 @@ #include #include +#include +#include #include "api.h" +// Symbolic EtherType names for parse_ethertypes(). +struct ethertype_name +{ + const char *name; + __u16 value; +}; + +static const struct ethertype_name g_ethertype_names[] = { + {"ipv4", 0x0800}, + {"ipv6", 0x86DD}, + {"arp", 0x0806}, + {NULL, 0}, +}; + +// Symbolic IP protocol names for parse_protos(). +struct proto_name +{ + const char *name; + __u8 value; +}; + +static const struct proto_name g_proto_names[] = { + {"tcp", 6}, + {"udp", 17}, + {"icmp", 1}, + {NULL, 0}, +}; + +// Parse a '+'-delimited list of EtherTypes into conf->input.ethertypes. +// Each token is a symbolic name (ipv4, arp, ipv6) or a 0x-prefixed hex value. +// Returns 0 on success, -1 on error (with err_msg set). +static int parse_ethertypes(struct config *conf, const char *input_str, const char **err_msg) +{ + // Work on a mutable copy since strtok_r modifies the string + char buf[256]; + if (strlen(input_str) >= sizeof(buf)) + { + *err_msg = "EtherType list too long"; + return -1; + } + memcpy(buf, input_str, strlen(input_str) + 1); + + __u8 count = 0; + char *saveptr = NULL; + char *token = strtok_r(buf, "+", &saveptr); + + while (token) + { + if (count >= MAX_MULTI_VALUES) + { + *err_msg = "too many EtherType values"; + return -1; + } + + __u16 value = 0; + bool found = false; + + // Try symbolic name first + for (const struct ethertype_name *e = g_ethertype_names; e->name; e++) + { + if (strcasecmp(token, e->name) == 0) + { + value = e->value; + found = true; + break; + } + } + + // Try 0x hex value + if (!found) + { + if (token[0] == '0' && (token[1] == 'x' || token[1] == 'X')) + { + char *endptr; + unsigned long v = strtoul(token, &endptr, 16); + if (*endptr != '\0' || v == 0 || v > 0xFFFF) + { + *err_msg = "invalid EtherType hex value"; + return -1; + } + value = (__u16)v; + found = true; + } + } + + if (!found) + { + *err_msg = "unknown EtherType name"; + return -1; + } + + // Check for duplicates + for (__u8 i = 0; i < count; i++) + { + if (conf->input.ethertypes.values[i] == value) + { + *err_msg = "duplicate EtherType value"; + return -1; + } + } + + conf->input.ethertypes.values[count] = value; + count++; + token = strtok_r(NULL, "+", &saveptr); + } + + if (count == 0) + { + *err_msg = "empty EtherType list"; + return -1; + } + + conf->input.ethertypes.count = count; + conf->has_input = true; + return 0; +} + +// Parse a '+'-delimited list of IP protocol numbers into conf->input.protos. +// Each token is a symbolic name (tcp, udp, icmp) or a decimal number (0-255). +// Returns 0 on success, -1 on error (with err_msg set). +static int parse_protos(struct config *conf, const char *input_str, const char **err_msg) +{ + char buf[256]; + if (strlen(input_str) >= sizeof(buf)) + { + *err_msg = "protocol list too long"; + return -1; + } + memcpy(buf, input_str, strlen(input_str) + 1); + + __u8 count = 0; + char *saveptr = NULL; + char *token = strtok_r(buf, "+", &saveptr); + + while (token) + { + if (count >= MAX_MULTI_VALUES) + { + *err_msg = "too many protocol values"; + return -1; + } + + __u8 value = 0; + bool found = false; + + // Try symbolic name first + for (const struct proto_name *p = g_proto_names; p->name; p++) + { + if (strcasecmp(token, p->name) == 0) + { + value = p->value; + found = true; + break; + } + } + + // Try decimal number + if (!found) + { + char *endptr; + unsigned long v = strtoul(token, &endptr, 10); + if (*endptr != '\0' || v > 255) + { + *err_msg = "invalid protocol number"; + return -1; + } + // Protocol 0 (HOPOPT) is technically valid but unlikely intended; + // allow it since the BPF program can handle any __u8 value. + value = (__u8)v; + found = true; + } + + // Check for duplicates + for (__u8 i = 0; i < count; i++) + { + if (conf->input.protos.values[i] == value) + { + *err_msg = "duplicate protocol value"; + return -1; + } + } + + conf->input.protos.values[count] = value; + count++; + token = strtok_r(NULL, "+", &saveptr); + } + + if (count == 0) + { + *err_msg = "empty protocol list"; + return -1; + } + + conf->input.protos.count = count; + conf->has_input = true; + return 0; +} + // Parse the input string and populate conf->input based on the selected program. // Returns 0 on success, -1 on error (with err_msg set to a static string). static int parse_input(struct config *conf, const char *input_str, const char **err_msg) From 8bc21c8752f93bf2d93891962ede4d35dfc0bebb Mon Sep 17 00:00:00 2001 From: Leo Di Donato <120051+leodido@users.noreply.github.com> Date: Mon, 4 May 2026 00:12:53 +0000 Subject: [PATCH 3/4] feat: add multi-value rodata support to build system template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend api.lua to support table-format input_fields entries alongside plain strings. Multi-value programs use { field = "name", multi = true } which sets PROGNAME_IS_MULTI_VALUE in the template variables. The template's memcpy already handles both scalars and structs via sizeof(), so no template file changes are needed. The new variable is available for future template branching if needed. No input_fields entries are added yet — those come when the BPF programs (allow_ethertype, allow_proto) land. Co-authored-by: Ona --- xmake/modules/api.lua | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/xmake/modules/api.lua b/xmake/modules/api.lua index 8a6f301..6ba65e5 100644 --- a/xmake/modules/api.lua +++ b/xmake/modules/api.lua @@ -3,6 +3,11 @@ import("core.project.project") -- Map program names to their input union field in struct config. -- Programs in this table have const volatile rodata that can be -- configured at runtime via the input union in struct config. +-- +-- Scalar programs use a plain string: "ip", "port". +-- Multi-value programs use a table: { field = "ethertypes", multi = true }. +-- The template uses PROGNAME_INPUT_FIELD for the union member name and +-- PROGNAME_IS_MULTI_VALUE to distinguish array inputs from scalars. local input_fields = { allow_dns = "ip", allow_ipv4 = "ip", @@ -53,7 +58,15 @@ function gen(target, source_target) local tempconf = path.join(os.tmpdir(), confname) os.tryrm(tempconf) os.cp(configfile_template_path, tempconf) - local input_field = input_fields[progname] + local raw = input_fields[progname] + local input_field = nil + local is_multi = false + if type(raw) == "string" then + input_field = raw + elseif type(raw) == "table" then + input_field = raw.field + is_multi = raw.multi or false + end local has_rodata = input_field and 1 or 0 target:add("configfiles", tempconf, { @@ -62,6 +75,7 @@ function gen(target, source_target) OPERATION = "attach__", PROGNAME_WITH_RODATA = has_rodata, PROGNAME_INPUT_FIELD = input_field or "", + PROGNAME_IS_MULTI_VALUE = is_multi and 1 or 0, } }) end From 3f99ffb8a34f093fa02cf8d07bd94afdb42ad121 Mon Sep 17 00:00:00 2001 From: Leo Di Donato <120051+leodido@users.noreply.github.com> Date: Mon, 4 May 2026 00:41:37 +0000 Subject: [PATCH 4/4] fix: reject malformed delimiter input in multi-value parsers Add validate_delimited_input() that rejects leading '+', trailing '+', and consecutive '++' before tokenization. strtok_r collapses adjacent delimiters, so without this check 'ipv4+', '+ipv4', and 'ipv4++arp' would be silently accepted as valid input. Co-authored-by: Ona --- api/input_parse.h | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/api/input_parse.h b/api/input_parse.h index b87f33e..994e344 100644 --- a/api/input_parse.h +++ b/api/input_parse.h @@ -36,11 +36,40 @@ static const struct proto_name g_proto_names[] = { {NULL, 0}, }; +// Validate that a '+'-delimited input string has no empty tokens. +// Rejects leading '+', trailing '+', and consecutive '++'. +static int validate_delimited_input(const char *input_str, const char **err_msg) +{ + size_t len = strlen(input_str); + if (len == 0) + return 0; // Empty string handled by caller's count==0 check + + if (input_str[0] == '+') + { + *err_msg = "input must not start with '+'"; + return -1; + } + if (input_str[len - 1] == '+') + { + *err_msg = "input must not end with '+'"; + return -1; + } + if (strstr(input_str, "++")) + { + *err_msg = "input contains empty value between '+' delimiters"; + return -1; + } + return 0; +} + // Parse a '+'-delimited list of EtherTypes into conf->input.ethertypes. // Each token is a symbolic name (ipv4, arp, ipv6) or a 0x-prefixed hex value. // Returns 0 on success, -1 on error (with err_msg set). static int parse_ethertypes(struct config *conf, const char *input_str, const char **err_msg) { + if (validate_delimited_input(input_str, err_msg) != 0) + return -1; + // Work on a mutable copy since strtok_r modifies the string char buf[256]; if (strlen(input_str) >= sizeof(buf)) @@ -130,6 +159,9 @@ static int parse_ethertypes(struct config *conf, const char *input_str, const ch // Returns 0 on success, -1 on error (with err_msg set). static int parse_protos(struct config *conf, const char *input_str, const char **err_msg) { + if (validate_delimited_input(input_str, err_msg) != 0) + return -1; + char buf[256]; if (strlen(input_str) >= sizeof(buf)) {