diff --git a/doc/usage/bfcli.rst b/doc/usage/bfcli.rst index 2db7090e..65217ad7 100644 --- a/doc/usage/bfcli.rst +++ b/doc/usage/bfcli.rst @@ -632,6 +632,12 @@ IPv4 - :rspan:`1` ``$PROTOCOL`` - :rspan:`1` ``$PROTOCOL`` must be a transport layer protocol name (e.g. "ICMP", case insensitive), or a valid decimal or hexadecimal `internet protocol number`_. * - ``not`` + * - :rspan:`1` DSCP + - :rspan:`1` ``ip4.dscp`` + - ``eq`` + - :rspan:`1` ``$DSCP`` + - :rspan:`1` ``$DSCP`` is a DSCP class name (e.g. "ef", "cs1", "af21", case insensitive) or a numeric value (0-63, decimal or hexadecimal). "be" is accepted as an alias for "cs0". + * - ``not`` IPv6 @@ -675,8 +681,14 @@ IPv6 * - :rspan:`1` Next header - :rspan:`1` ``ip6.nexthdr`` - ``eq`` - - :rspan:`3` ``$NEXT_HEADER`` - - :rspan:`3` ``$NEXT_HEADER`` is a transport layer protocol name (e.g. "ICMP", case insensitive), an IPv6 extension header name, or a valid decimal or hexadecimal `internet protocol number`_. + - :rspan:`1` ``$NEXT_HEADER`` + - :rspan:`1` ``$NEXT_HEADER`` is a transport layer protocol name (e.g. "ICMP", case insensitive), an IPv6 extension header name, or a valid decimal or hexadecimal `internet protocol number`_. + * - ``not`` + * - :rspan:`1` DSCP + - :rspan:`1` ``ip6.dscp`` + - ``eq`` + - :rspan:`1` ``$DSCP`` + - :rspan:`1` ``$DSCP`` is a DSCP class name (e.g. "ef", "cs1", "af21", case insensitive) or a numeric value (0-63, decimal or hexadecimal). "be" is accepted as an alias for "cs0". * - ``not`` .. tip:: diff --git a/src/libbpfilter/include/bpfilter/matcher.h b/src/libbpfilter/include/bpfilter/matcher.h index d49b2ca7..d403203f 100644 --- a/src/libbpfilter/include/bpfilter/matcher.h +++ b/src/libbpfilter/include/bpfilter/matcher.h @@ -424,6 +424,9 @@ int bf_ethertype_from_str(const char *str, uint16_t *ethertype); const char *bf_ipproto_to_str(uint8_t ipproto); int bf_ipproto_from_str(const char *str, uint8_t *ipproto); +const char *bf_dscp_class_to_str(uint8_t dscp); +int bf_dscp_class_from_str(const char *str, uint8_t *dscp); + const char *bf_icmp_type_to_str(uint8_t type); int bf_icmp_type_from_str(const char *str, uint8_t *type); diff --git a/src/libbpfilter/matcher.c b/src/libbpfilter/matcher.c index 0b9d645f..5e2256b9 100644 --- a/src/libbpfilter/matcher.c +++ b/src/libbpfilter/matcher.c @@ -737,6 +737,9 @@ static int _bf_parse_dscp(enum bf_matcher_type type, enum bf_matcher_op op, assert(payload); assert(raw_payload); + if (!bf_dscp_class_from_str(raw_payload, payload)) + return 0; + if (!_bf_strtoul(raw_payload, &value) && value <= BF_DSCP_MAX) { *(uint8_t *)payload = (uint8_t)value; return 0; @@ -744,7 +747,7 @@ static int _bf_parse_dscp(enum bf_matcher_type type, enum bf_matcher_op op, return bf_err_r( -EINVAL, - "\"%s %s\" expects a DSCP value (0-63) in decimal or hexadecimal notation, not '%s'", + "\"%s %s\" expects a DSCP value (0-63, decimal/hex) or class name, not '%s'", bf_matcher_type_to_str(type), bf_matcher_op_to_str(op), raw_payload); } @@ -752,7 +755,12 @@ static void _bf_print_dscp(const void *payload) { assert(payload); - (void)fprintf(stdout, "%" PRIu8, *(uint8_t *)payload); + const char *name = bf_dscp_class_to_str(*(uint8_t *)payload); + + if (name) + (void)fprintf(stdout, "%s", name); + else + (void)fprintf(stdout, "%" PRIu8, *(uint8_t *)payload); } static int _bf_parse_icmpv6_type(enum bf_matcher_type type, @@ -1665,6 +1673,46 @@ int bf_ipproto_from_str(const char *str, uint8_t *ipproto) return -EINVAL; } +/* DSCP class name to codepoint mapping, based on iptables + * dscp_helper.c and http://www.iana.org/assignments/dscp-registry. + * BE is an alias for CS0. */ +static const struct +{ + const char *name; + uint8_t dscp; +} _bf_dscp_classes[] = { + {"cs0", 0}, {"cs1", 8}, {"cs2", 16}, {"cs3", 24}, {"cs4", 32}, + {"cs5", 40}, {"cs6", 48}, {"cs7", 56}, {"af11", 10}, {"af12", 12}, + {"af13", 14}, {"af21", 18}, {"af22", 20}, {"af23", 22}, {"af31", 26}, + {"af32", 28}, {"af33", 30}, {"af41", 34}, {"af42", 36}, {"af43", 38}, + {"ef", 46}, {"be", 0}, +}; + +const char *bf_dscp_class_to_str(uint8_t dscp) +{ + for (size_t i = 0; i < ARRAY_SIZE(_bf_dscp_classes); ++i) { + if (_bf_dscp_classes[i].dscp == dscp) + return _bf_dscp_classes[i].name; + } + + return NULL; +} + +int bf_dscp_class_from_str(const char *str, uint8_t *dscp) +{ + assert(str); + assert(dscp); + + for (size_t i = 0; i < ARRAY_SIZE(_bf_dscp_classes); ++i) { + if (bf_streq_i(str, _bf_dscp_classes[i].name)) { + *dscp = _bf_dscp_classes[i].dscp; + return 0; + } + } + + return -EINVAL; +} + #define ICMP_ROUTERADVERT 9 #define ICMP_ROUTERSOLICIT 10 diff --git a/tests/e2e/parsing/ip4_dscp.sh b/tests/e2e/parsing/ip4_dscp.sh index 52cb1a37..f8262873 100755 --- a/tests/e2e/parsing/ip4_dscp.sh +++ b/tests/e2e/parsing/ip4_dscp.sh @@ -19,6 +19,16 @@ ${BFCLI} ruleset set --dry-run --from-str "chain xdp BF_HOOK_XDP ACCEPT rule ip4 (! ${BFCLI} ruleset set --dry-run --from-str "chain xdp BF_HOOK_XDP ACCEPT rule ip4.dscp eq invalid counter DROP") (! ${BFCLI} ruleset set --dry-run --from-str "chain xdp BF_HOOK_XDP ACCEPT rule ip4.dscp eq 0x40 counter DROP") +# Test valid class name keywords +${BFCLI} ruleset set --dry-run --from-str "chain xdp BF_HOOK_XDP ACCEPT rule ip4.dscp eq ef counter DROP" +${BFCLI} ruleset set --dry-run --from-str "chain xdp BF_HOOK_XDP ACCEPT rule ip4.dscp not cs1 counter DROP" +${BFCLI} ruleset set --dry-run --from-str "chain xdp BF_HOOK_XDP ACCEPT rule ip4.dscp eq AF21 counter DROP" +${BFCLI} ruleset set --dry-run --from-str "chain xdp BF_HOOK_XDP ACCEPT rule ip4.dscp not BE counter DROP" + +# Test invalid class name keywords (should fail) +(! ${BFCLI} ruleset set --dry-run --from-str "chain xdp BF_HOOK_XDP ACCEPT rule ip4.dscp eq cs8 counter DROP") +(! ${BFCLI} ruleset set --dry-run --from-str "chain xdp BF_HOOK_XDP ACCEPT rule ip4.dscp eq AF14 counter DROP") + # Test with 'not' operator ${BFCLI} ruleset set --dry-run --from-str "chain xdp BF_HOOK_XDP ACCEPT rule ip4.dscp not 0 counter DROP" ${BFCLI} ruleset set --dry-run --from-str "chain xdp BF_HOOK_XDP ACCEPT rule ip4.dscp not 16 counter DROP" diff --git a/tests/e2e/parsing/ip6_dscp.sh b/tests/e2e/parsing/ip6_dscp.sh index c389ee1c..6effad15 100755 --- a/tests/e2e/parsing/ip6_dscp.sh +++ b/tests/e2e/parsing/ip6_dscp.sh @@ -20,6 +20,16 @@ ${BFCLI} ruleset set --dry-run --from-str "chain xdp BF_HOOK_XDP ACCEPT rule ip6 (! ${BFCLI} ruleset set --dry-run --from-str "chain xdp BF_HOOK_XDP ACCEPT rule ip6.dscp eq -0x01 counter DROP") (! ${BFCLI} ruleset set --dry-run --from-str "chain xdp BF_HOOK_XDP ACCEPT rule ip6.dscp eq 0x40 counter DROP") +# Test valid class name keywords +${BFCLI} ruleset set --dry-run --from-str "chain xdp BF_HOOK_XDP ACCEPT rule ip6.dscp eq ef counter DROP" +${BFCLI} ruleset set --dry-run --from-str "chain xdp BF_HOOK_XDP ACCEPT rule ip6.dscp not cs1 counter DROP" +${BFCLI} ruleset set --dry-run --from-str "chain xdp BF_HOOK_XDP ACCEPT rule ip6.dscp eq AF21 counter DROP" +${BFCLI} ruleset set --dry-run --from-str "chain xdp BF_HOOK_XDP ACCEPT rule ip6.dscp not BE counter DROP" + +# Test invalid class name keywords (should fail) +(! ${BFCLI} ruleset set --dry-run --from-str "chain xdp BF_HOOK_XDP ACCEPT rule ip6.dscp eq cs8 counter DROP") +(! ${BFCLI} ruleset set --dry-run --from-str "chain xdp BF_HOOK_XDP ACCEPT rule ip6.dscp eq AF14 counter DROP") + # Test valid decimal values with 'not' operator ${BFCLI} ruleset set --dry-run --from-str "chain xdp BF_HOOK_XDP ACCEPT rule ip6.dscp not 0 counter DROP" ${BFCLI} ruleset set --dry-run --from-str "chain xdp BF_HOOK_XDP ACCEPT rule ip6.dscp not 46 counter DROP" diff --git a/tests/unit/libbpfilter/matcher.c b/tests/unit/libbpfilter/matcher.c index 833fe9ed..367ebca8 100644 --- a/tests/unit/libbpfilter/matcher.c +++ b/tests/unit/libbpfilter/matcher.c @@ -381,6 +381,40 @@ static void ipproto_conversions(void **state) assert_err(bf_ipproto_from_str("invalid", &ipproto)); } +static void dscp_class_conversions(void **state) +{ + uint8_t dscp; + + (void)state; + + // Test to_str for known classes + assert_string_equal(bf_dscp_class_to_str(0), "cs0"); + assert_string_equal(bf_dscp_class_to_str(46), "ef"); + + // Test to_str for values without a named class + assert_null(bf_dscp_class_to_str(63)); + assert_null(bf_dscp_class_to_str(64)); + + // Test from_str + assert_ok(bf_dscp_class_from_str("ef", &dscp)); + assert_int_equal(dscp, 46); + + assert_ok(bf_dscp_class_from_str("af21", &dscp)); + assert_int_equal(dscp, 18); + + // Test BE alias and case insensitivity + assert_ok(bf_dscp_class_from_str("BE", &dscp)); + assert_int_equal(dscp, 0); + + assert_ok(bf_dscp_class_from_str("Af11", &dscp)); + assert_int_equal(dscp, 10); + + // Test invalid + assert_err(bf_dscp_class_from_str("cs8", &dscp)); + assert_err(bf_dscp_class_from_str("af14", &dscp)); + assert_err(bf_dscp_class_from_str("invalid", &dscp)); +} + static void icmp_type_conversions(void **state) { uint8_t type; @@ -1111,6 +1145,20 @@ static void ip6_dscp(void **state) bf_matcher_dump(matcher, &prefix); bf_matcher_free(&matcher); + // Test with class name keyword + assert_ok(bf_matcher_new_from_raw(&matcher, BF_MATCHER_IP6_DSCP, + BF_MATCHER_EQ, "ef")); + assert_non_null(matcher); + assert_int_equal(*(uint8_t *)bf_matcher_payload(matcher), 46); + bf_matcher_free(&matcher); + + // Test with BE alias (case insensitive) + assert_ok(bf_matcher_new_from_raw(&matcher, BF_MATCHER_IP6_DSCP, + BF_MATCHER_EQ, "BE")); + assert_non_null(matcher); + assert_int_equal(*(uint8_t *)bf_matcher_payload(matcher), 0); + bf_matcher_free(&matcher); + // Test print function via ops assert_ok(bf_matcher_new_from_raw(&matcher, BF_MATCHER_IP6_DSCP, BF_MATCHER_EQ, "0x20")); @@ -1130,9 +1178,9 @@ static void ip6_dscp_invalid(void **state) assert_err(bf_matcher_new_from_raw(&matcher, BF_MATCHER_IP6_DSCP, BF_MATCHER_EQ, "64")); - // Test with invalid string + // Test with invalid class name assert_err(bf_matcher_new_from_raw(&matcher, BF_MATCHER_IP6_DSCP, - BF_MATCHER_EQ, "not_a_number")); + BF_MATCHER_EQ, "cs8")); // Test with negative value assert_err(bf_matcher_new_from_raw(&matcher, BF_MATCHER_IP6_DSCP, @@ -1573,6 +1621,20 @@ static void ip4_dscp(void **state) assert_non_null(matcher); bf_matcher_free(&matcher); + // Test ip4.dscp with class name keyword + assert_ok(bf_matcher_new_from_raw(&matcher, BF_MATCHER_IP4_DSCP, + BF_MATCHER_EQ, "ef")); + assert_non_null(matcher); + assert_int_equal(*(uint8_t *)bf_matcher_payload(matcher), 46); + bf_matcher_free(&matcher); + + // Test ip4.dscp with BE alias (case insensitive) + assert_ok(bf_matcher_new_from_raw(&matcher, BF_MATCHER_IP4_DSCP, + BF_MATCHER_EQ, "BE")); + assert_non_null(matcher); + assert_int_equal(*(uint8_t *)bf_matcher_payload(matcher), 0); + bf_matcher_free(&matcher); + // Test ip4.dscp print function assert_ok(bf_matcher_new_from_raw(&matcher, BF_MATCHER_IP4_DSCP, BF_MATCHER_EQ, "0x3f")); @@ -1589,6 +1651,10 @@ static void ip4_dscp(void **state) assert_err(bf_matcher_new_from_raw(&matcher, BF_MATCHER_IP4_DSCP, BF_MATCHER_EQ, "0x40")); + // Test ip4.dscp with invalid class name + assert_err(bf_matcher_new_from_raw(&matcher, BF_MATCHER_IP4_DSCP, + BF_MATCHER_EQ, "cs8")); + // Test ip4.dscp with invalid string assert_err(bf_matcher_new_from_raw(&matcher, BF_MATCHER_IP4_DSCP, BF_MATCHER_EQ, "invalid")); @@ -1738,6 +1804,7 @@ int main(void) cmocka_unit_test(tcp_flag_conversions), cmocka_unit_test(ethertype_conversions), cmocka_unit_test(ipproto_conversions), + cmocka_unit_test(dscp_class_conversions), cmocka_unit_test(icmp_type_conversions), cmocka_unit_test(icmpv6_type_conversions), cmocka_unit_test(get_meta),