When building NFTA_{FLOWTABLE_,}HOOK_DEVS attributes, detect trailing asterisks in interface names and transmit the leading part in a NFTA_DEVICE_PREFIX attribute. Deserialization (i.e., appending asterisk to interface prefixes returned in NFTA_DEVICE_PREFIX atributes happens in libnftnl. Signed-off-by: Phil Sutter --- Changes since v4: - Introduce and use NFTA_DEVICE_PREFIX which contains a NUL-terminated string as well but signals the kernel to interpret it as a prefix to match interfaces on. - Do not send wildcards in NFTA_HOOK_DEV: On one hand, the kernel can't detect them anymore since they are NUL-terminated as well. On the other, it would defeat the purpose of having NFTA_DEVICE_PREFIX, which is to not crash old user space. Changes since v3: - Use uint16_t for 'attr' parameter and size_t for 'len' variable - Use mnl_nft_ prefix for the helper function Changes since v2: - Introduce mnl_attr_put_ifname() to perform the conditional mnl_attr_put() parameter adjustment - Sanity-check array index in above function to avoid out-of-bounds access --- include/linux/netfilter/nf_tables.h | 2 ++ src/mnl.c | 26 +++++++++++++++++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/include/linux/netfilter/nf_tables.h b/include/linux/netfilter/nf_tables.h index f57963e89fd16..b38d4780ae8c8 100644 --- a/include/linux/netfilter/nf_tables.h +++ b/include/linux/netfilter/nf_tables.h @@ -1774,10 +1774,12 @@ enum nft_synproxy_attributes { * enum nft_device_attributes - nf_tables device netlink attributes * * @NFTA_DEVICE_NAME: name of this device (NLA_STRING) + * @NFTA_DEVICE_PREFIX: device name prefix, a simple wildcard (NLA_STRING) */ enum nft_devices_attributes { NFTA_DEVICE_UNSPEC, NFTA_DEVICE_NAME, + NFTA_DEVICE_PREFIX, __NFTA_DEVICE_MAX }; #define NFTA_DEVICE_MAX (__NFTA_DEVICE_MAX - 1) diff --git a/src/mnl.c b/src/mnl.c index 43229f2498e55..b532b8ff00c1e 100644 --- a/src/mnl.c +++ b/src/mnl.c @@ -795,6 +795,26 @@ static void nft_dev_array_free(const struct nft_dev *dev_array) free_const(dev_array); } +static bool is_wildcard_str(const char *str) +{ + size_t len = strlen(str); + + if (len < 1 || str[len - 1] != '*') + return false; + if (len < 2 || str[len - 2] != '\\') + return true; + /* XXX: ignore backslash escaping for now */ + return false; +} + +static void mnl_nft_attr_put_ifname(struct nlmsghdr *nlh, const char *ifname) +{ + uint16_t attr = is_wildcard_str(ifname) ? + NFTA_DEVICE_PREFIX : NFTA_DEVICE_NAME; + + mnl_attr_put_strz(nlh, attr, ifname); +} + static void mnl_nft_chain_devs_build(struct nlmsghdr *nlh, struct cmd *cmd) { const struct expr *dev_expr = cmd->chain->dev_expr; @@ -803,14 +823,14 @@ static void mnl_nft_chain_devs_build(struct nlmsghdr *nlh, struct cmd *cmd) int i, num_devs = 0; dev_array = nft_dev_array(dev_expr, &num_devs); - if (num_devs == 1) { + if (num_devs == 1 && !is_wildcard_str(dev_array[0].ifname)) { cmd_add_loc(cmd, nlh, dev_array[0].location); mnl_attr_put_strz(nlh, NFTA_HOOK_DEV, dev_array[0].ifname); } else { nest_dev = mnl_attr_nest_start(nlh, NFTA_HOOK_DEVS); for (i = 0; i < num_devs; i++) { cmd_add_loc(cmd, nlh, dev_array[i].location); - mnl_attr_put_strz(nlh, NFTA_DEVICE_NAME, dev_array[i].ifname); + mnl_nft_attr_put_ifname(nlh, dev_array[i].ifname); } mnl_attr_nest_end(nlh, nest_dev); } @@ -2091,7 +2111,7 @@ static void mnl_nft_ft_devs_build(struct nlmsghdr *nlh, struct cmd *cmd) nest_dev = mnl_attr_nest_start(nlh, NFTA_FLOWTABLE_HOOK_DEVS); for (i = 0; i < num_devs; i++) { cmd_add_loc(cmd, nlh, dev_array[i].location); - mnl_attr_put_strz(nlh, NFTA_DEVICE_NAME, dev_array[i].ifname); + mnl_nft_attr_put_ifname(nlh, dev_array[i].ifname); } mnl_attr_nest_end(nlh, nest_dev); -- 2.49.0 All clauses are identical, so instead of adding a third one for ASTERISK_STRING, use a single one for 'string' (which combines all three variants). Signed-off-by: Phil Sutter --- Changes since v3: - Cover interface wildcards in nft.8 --- doc/nft.txt | 30 ++++++++++++++++++++++++++---- src/parser_bison.y | 11 +---------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/doc/nft.txt b/doc/nft.txt index 8712981943d78..42cdd38a27b67 100644 --- a/doc/nft.txt +++ b/doc/nft.txt @@ -387,13 +387,19 @@ add table inet mytable CHAINS ------ [verse] -{*add* | *create*} *chain* ['family'] 'table' 'chain' [*{ type* 'type' *hook* 'hook' [*device* 'device'] *priority* 'priority' *;* [*policy* 'policy' *;*] [*comment* 'comment' *;*] *}*] +____ +{*add* | *create*} *chain* ['family'] 'table' 'chain' [*{ type* 'type' *hook* 'hook' ['DEVICE'] *priority* 'priority' *;* [*policy* 'policy' *;*] [*comment* 'comment' *;*] *}*] {*delete* | *destroy* | *list* | *flush*} *chain* ['family'] 'table' 'chain' *list chains* ['family'] *delete chain* ['family'] 'table' *handle* 'handle' *destroy chain* ['family'] 'table' *handle* 'handle' *rename chain* ['family'] 'table' 'chain' 'newname' +'DEVICE' := {*device* 'DEVICE_NAME' | *devices = {* 'DEVICE_LIST' *}*} +'DEVICE_LIST' := 'DEVICE_NAME' [*,* 'DEVICE_LIST'] +'DEVICE_NAME' := 'string' | 'string'*** +____ + Chains are containers for rules. They exist in two kinds, base chains and regular chains. A base chain is an entry point for packets from the networking stack, a regular chain may be used as jump target and is used for better rule @@ -436,7 +442,7 @@ Apart from the special cases illustrated above (e.g. *nat* type not supporting * The netdev family supports merely two combinations, namely *filter* type with *ingress* hook and *filter* type with *egress* hook. Base chains in this - family also require the *device* parameter to be present since they exist per + family also require the 'DEVICE' parameter to be present since they exist per interface only. * The arp family supports only the *input* and *output* hooks, both in chains of type *filter*. @@ -449,7 +455,13 @@ Apart from the special cases illustrated above (e.g. *nat* type not supporting The *device* parameter accepts a network interface name as a string, and is required when adding a base chain that filters traffic on the ingress or egress hooks. Any ingress or egress chains will only filter traffic from the -interface specified in the *device* parameter. +interface specified in the *device* parameter. The same base chain may be used +for multiple devices by using the *devices* parameter instead. + +With newer kernels there is also basic support for wildcards in 'DEVICE_NAME' +by specifying an asterisk suffix. The chain will apply to all interfaces +matching the given prefix. Use the *list hooks* command to see the current +status. The *priority* parameter accepts a signed integer value or a standard priority name which specifies the order in which chains with the same *hook* value are @@ -763,11 +775,16 @@ per element comment field FLOWTABLES ----------- [verse] -{*add* | *create*} *flowtable* ['family'] 'table' 'flowtable' *{ hook* 'hook' *priority* 'priority' *; devices = {* 'device'[*,* ...] *} ; }* +____ +{*add* | *create*} *flowtable* ['family'] 'table' 'flowtable' *{ hook* 'hook' *priority* 'priority' *; devices = {* 'DEVICE_LIST' *} ; }* *list flowtables* ['family'] ['table'] {*delete* | *destroy* | *list*} *flowtable* ['family'] 'table' 'flowtable' *delete* *flowtable* ['family'] 'table' *handle* 'handle' +'DEVICE_LIST' := 'DEVICE_NAME' [*,* 'DEVICE_LIST'] +'DEVICE_NAME' := 'string' | 'string'*** +____ + Flowtables allow you to accelerate packet forwarding in software. Flowtables entries are represented through a tuple that is composed of the input interface, source and destination address, source and destination port; and layer 3/4 @@ -786,6 +803,11 @@ The *priority* can be a signed integer or *filter* which stands for 0. Addition and subtraction can be used to set relative priority, e.g. filter + 5 equals to 5. +With newer kernels there is basic support for wildcards in 'DEVICE_LIST' by +specifying an asterisk suffix. The flowtable will apply to all interfaces +matching the given prefix. Use the *list hooks* command to see the current +status. + [horizontal] *add*:: Add a new flowtable for the given family with the given name. *delete*:: Delete the specified flowtable. diff --git a/src/parser_bison.y b/src/parser_bison.y index 0b1ea699c6102..d665f2fa8c7c1 100644 --- a/src/parser_bison.y +++ b/src/parser_bison.y @@ -2473,16 +2473,7 @@ flowtable_list_expr : flowtable_expr_member | flowtable_list_expr COMMA opt_newline ; -flowtable_expr_member : QUOTED_STRING - { - struct expr *expr = ifname_expr_alloc(&@$, state->msgs, $1); - - if (!expr) - YYERROR; - - $$ = expr; - } - | STRING +flowtable_expr_member : string { struct expr *expr = ifname_expr_alloc(&@$, state->msgs, $1); -- 2.49.0 Assert that: - Non-matching interface specs are accepted - Existing interfaces are hooked into upon flowtable/chain creation - A new device matching the spec is hooked into immediately - No stale hooks remain in 'nft list hooks' output - Wildcard hooks basically work Signed-off-by: Phil Sutter --- .../features/list_hooks_flowtable_info.sh | 7 +++ .../netdev_chain_name_based_hook_0.json-nft | 34 ++++++++++++++ .../dumps/netdev_chain_name_based_hook_0.nft | 5 +++ .../chains/netdev_chain_name_based_hook_0 | 44 ++++++++++++++++++ .../testcases/flowtable/0016name_based_hook_0 | 45 +++++++++++++++++++ .../dumps/0016name_based_hook_0.json-nft | 32 +++++++++++++ .../flowtable/dumps/0016name_based_hook_0.nft | 6 +++ 7 files changed, 173 insertions(+) create mode 100755 tests/shell/features/list_hooks_flowtable_info.sh create mode 100644 tests/shell/testcases/chains/dumps/netdev_chain_name_based_hook_0.json-nft create mode 100644 tests/shell/testcases/chains/dumps/netdev_chain_name_based_hook_0.nft create mode 100755 tests/shell/testcases/chains/netdev_chain_name_based_hook_0 create mode 100755 tests/shell/testcases/flowtable/0016name_based_hook_0 create mode 100644 tests/shell/testcases/flowtable/dumps/0016name_based_hook_0.json-nft create mode 100644 tests/shell/testcases/flowtable/dumps/0016name_based_hook_0.nft diff --git a/tests/shell/features/list_hooks_flowtable_info.sh b/tests/shell/features/list_hooks_flowtable_info.sh new file mode 100755 index 0000000000000..58bc57e040959 --- /dev/null +++ b/tests/shell/features/list_hooks_flowtable_info.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +# check for flowtable info in 'list hooks' output + +unshare -n bash -c " \ +$NFT \"table inet t { flowtable ft { hook ingress priority 0; devices = { lo }; }; }\"; \ +$NFT list hooks netdev device lo | grep -q flowtable\ inet\ t\ ft" diff --git a/tests/shell/testcases/chains/dumps/netdev_chain_name_based_hook_0.json-nft b/tests/shell/testcases/chains/dumps/netdev_chain_name_based_hook_0.json-nft new file mode 100644 index 0000000000000..00706271e96a4 --- /dev/null +++ b/tests/shell/testcases/chains/dumps/netdev_chain_name_based_hook_0.json-nft @@ -0,0 +1,34 @@ +{ + "nftables": [ + { + "metainfo": { + "version": "VERSION", + "release_name": "RELEASE_NAME", + "json_schema_version": 1 + } + }, + { + "table": { + "family": "netdev", + "name": "t", + "handle": 0 + } + }, + { + "chain": { + "family": "netdev", + "table": "t", + "name": "c", + "handle": 0, + "dev": [ + "foo*", + "lo" + ], + "type": "filter", + "hook": "ingress", + "prio": 0, + "policy": "accept" + } + } + ] +} diff --git a/tests/shell/testcases/chains/dumps/netdev_chain_name_based_hook_0.nft b/tests/shell/testcases/chains/dumps/netdev_chain_name_based_hook_0.nft new file mode 100644 index 0000000000000..ac5acacd12e6d --- /dev/null +++ b/tests/shell/testcases/chains/dumps/netdev_chain_name_based_hook_0.nft @@ -0,0 +1,5 @@ +table netdev t { + chain c { + type filter hook ingress devices = { "foo*", "lo" } priority filter; policy accept; + } +} diff --git a/tests/shell/testcases/chains/netdev_chain_name_based_hook_0 b/tests/shell/testcases/chains/netdev_chain_name_based_hook_0 new file mode 100755 index 0000000000000..8a8a601784084 --- /dev/null +++ b/tests/shell/testcases/chains/netdev_chain_name_based_hook_0 @@ -0,0 +1,44 @@ +#!/bin/bash + +# NFT_TEST_REQUIRES(NFT_TEST_HAVE_ifname_based_hooks) + +cspec=' chain netdev t c ' +$NFT add table netdev t +$NFT add $cspec '{ type filter hook ingress priority 0; devices = { lo, foo* }; }' +$NFT list hooks netdev device lo | grep -q "$cspec" || { + echo "Existing device lo not hooked into chain as expected" + exit 1 +} + +[[ $($NFT list hooks | grep -c "$cspec") -eq 1 ]] || { + echo "Chain hooks into more than just lo" + exit 2 +} + +ip link add foo1 type dummy +$NFT list hooks netdev device foo1 | grep -q "$cspec" || { + echo "Chain did not hook into new device foo1" + exit 3 +} +[[ $($NFT list hooks | grep -c "$cspec") -eq 2 ]] || { + echo "Chain expected to hook into exactly two devices" + exit 4 +} + +ip link del foo1 +$NFT list hooks netdev device foo1 | grep -q "$cspec" && { + echo "Chain still hooks into removed device foo1" + exit 5 +} +[[ $($NFT list hooks | grep -c "$cspec") -eq 1 ]] || { + echo "Chain expected to hook into just lo" + exit 6 +} + +for ((i = 0; i < 100; i++)); do + ip link add foo$i type dummy +done +[[ $($NFT list hooks | grep -c "$cspec") -eq 101 ]] || { + echo "Chain did not hook into all 100 new devices" + exit 7 +} diff --git a/tests/shell/testcases/flowtable/0016name_based_hook_0 b/tests/shell/testcases/flowtable/0016name_based_hook_0 new file mode 100755 index 0000000000000..9a55596027158 --- /dev/null +++ b/tests/shell/testcases/flowtable/0016name_based_hook_0 @@ -0,0 +1,45 @@ +#!/bin/bash + +# NFT_TEST_REQUIRES(NFT_TEST_HAVE_ifname_based_hooks) +# NFT_TEST_REQUIRES(NFT_TEST_HAVE_list_hooks_flowtable_info) + +ftspec=' flowtable ip t ft ' +$NFT add table t +$NFT add $ftspec '{ hook ingress priority 0; devices = { lo, foo* }; }' +$NFT list hooks netdev device lo | grep -q "$ftspec" || { + echo "Existing device lo not hooked into flowtable as expected" + exit 1 +} + +[[ $($NFT list hooks | grep -c "$ftspec") -eq 1 ]] || { + echo "Flowtable hooks into more than just lo" + exit 2 +} + +ip link add foo1 type dummy +$NFT list hooks netdev device foo1 | grep -q "$ftspec" || { + echo "Flowtable did not hook into new device foo1" + exit 3 +} +[[ $($NFT list hooks | grep -c "$ftspec") -eq 2 ]] || { + echo "Flowtable expected to hook into exactly two devices" + exit 4 +} + +ip link del foo1 +$NFT list hooks netdev device foo1 | grep -q "$ftspec" && { + echo "Flowtable still hooks into removed device foo1" + exit 5 +} +[[ $($NFT list hooks | grep -c "$ftspec") -eq 1 ]] || { + echo "Flowtable expected to hook into just lo" + exit 6 +} + +for ((i = 0; i < 100; i++)); do + ip link add foo$i type dummy +done +[[ $($NFT list hooks | grep -c "$ftspec") -eq 101 ]] || { + echo "Flowtable did not hook into all 100 new devices" + exit 7 +} diff --git a/tests/shell/testcases/flowtable/dumps/0016name_based_hook_0.json-nft b/tests/shell/testcases/flowtable/dumps/0016name_based_hook_0.json-nft new file mode 100644 index 0000000000000..93e263323ff95 --- /dev/null +++ b/tests/shell/testcases/flowtable/dumps/0016name_based_hook_0.json-nft @@ -0,0 +1,32 @@ +{ + "nftables": [ + { + "metainfo": { + "version": "VERSION", + "release_name": "RELEASE_NAME", + "json_schema_version": 1 + } + }, + { + "table": { + "family": "ip", + "name": "t", + "handle": 0 + } + }, + { + "flowtable": { + "family": "ip", + "name": "ft", + "table": "t", + "handle": 0, + "hook": "ingress", + "prio": 0, + "dev": [ + "foo*", + "lo" + ] + } + } + ] +} diff --git a/tests/shell/testcases/flowtable/dumps/0016name_based_hook_0.nft b/tests/shell/testcases/flowtable/dumps/0016name_based_hook_0.nft new file mode 100644 index 0000000000000..b4810664a956f --- /dev/null +++ b/tests/shell/testcases/flowtable/dumps/0016name_based_hook_0.nft @@ -0,0 +1,6 @@ +table ip t { + flowtable ft { + hook ingress priority filter + devices = { "foo*", "lo" } + } +} -- 2.49.0