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; }; diff --git a/api/input_parse.h b/api/input_parse.h index 0ad8ab0..994e344 100644 --- a/api/input_parse.h +++ b/api/input_parse.h @@ -3,9 +3,241 @@ #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}, +}; + +// 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)) + { + *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) +{ + if (validate_delimited_input(input_str, err_msg) != 0) + return -1; + + 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) 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