From: Topi Miettinen Date: Wed, 9 Aug 2023 20:07:21 +0000 (+0300) Subject: network: firewall integration with NFT sets X-Git-Tag: v255-rc1~612^2~2 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=fc289dd0ad4c223c0fa02dc7e91f7244143fa918;p=thirdparty%2Fsystemd.git network: firewall integration with NFT sets New directive `NFTSet=` provides a method for integrating network configuration into firewall rules with NFT sets. The benefit of using this setting is that static network configuration or dynamically obtained network addresses can be used in firewall rules with the indirection of NFT set types. For example, access could be granted for hosts in the local subnetwork only. Firewall rules using IP address of an interface are also instantly updated when the network configuration changes, for example via DHCP. This option expects a whitespace separated list of NFT set definitions. Each definition consists of a colon-separated tuple of source type (one of "address", "prefix", or "ifindex"), NFT address family (one of "arp", "bridge", "inet", "ip", "ip6", or "netdev"), table name and set name. The names of tables and sets must conform to lexical restrictions of NFT table names. The type of the element used in the NFT filter must match the type implied by the directive ("address", "prefix" or "ifindex") and address type (IPv4 or IPv6) as shown type implied by the directive ("address", "prefix" or "ifindex") and address type (IPv4 or IPv6) must also match the set definition. When an interface is configured with IP addresses, the addresses, subnetwork masks or interface index will be appended to the NFT sets. The information will be removed when the interface is deconfigured. systemd-networkd only inserts elements to (or removes from) the sets, so the related NFT rules, tables and sets must be prepared elsewhere in advance. Failures to manage the sets will be ignored. /etc/systemd/network/eth.network ``` [DHCPv4] ... NFTSet=prefix:netdev:filter:eth_ipv4_prefix ``` Example NFT rules: ``` table netdev filter { set eth_ipv4_prefix { type ipv4_addr flags interval } chain eth_ingress { type filter hook ingress device "eth0" priority filter; policy drop; ip saddr != @eth_ipv4_prefix drop accept } } ``` ``` $ sudo nft list set netdev filter eth_ipv4_prefix table netdev filter { set eth_ipv4_prefix { type ipv4_addr flags interval elements = { 10.0.0.0/24 } } } ``` --- diff --git a/man/systemd.network.xml b/man/systemd.network.xml index b4fb051be34..8d0306771e8 100644 --- a/man/systemd.network.xml +++ b/man/systemd.network.xml @@ -1200,6 +1200,86 @@ allow my_server_t localnet_peer_t:peer recv; + + + NFTSet=source:family:table:set + + This setting provides a method for integrating network configuration into firewall rules with + NFT sets. The benefit of + using the setting is that static network configuration (or dynamically obtained network addresses, + see similar directives in other sections) can be used in firewall rules with the indirection of NFT + set types. For example, access could be granted for hosts in the local subnetwork only. Firewall + rules using IP address of an interface are also instantly updated when the network configuration + changes, for example via DHCP. + + This option expects a whitespace separated list of NFT set definitions. Each definition + consists of a colon-separated tuple of source type (one of address, + prefix or ifindex), NFT address family (one of + arp, bridge, inet, ip, + ip6, or netdev), table name and set name. The names of tables + and sets must conform to lexical restrictions of NFT table names. The type of the element used in + the NFT filter must match the type implied by the directive (address, + prefix or ifindex) and address type (IPv4 or IPv6) as shown + in the table below. + + + Defined <varname>source type</varname> values + + + + + + + Source type + Description + Corresponding NFT type name + + + + + + address + host IP address + ipv4_addr or ipv6_addr + + + prefix + network prefix + ipv4_addr or ipv6_addr, with flags interval + + + ifindex + interface index + iface_index + + + +
+ + When an interface is configured with IP addresses, the addresses, subnetwork masks or + interface index will be appended to the NFT sets. The information will be removed when the + interface is deconfigured. systemd-networkd only inserts elements to (or removes + from) the sets, so the related NFT rules, tables and sets must be prepared elsewhere in + advance. Failures to manage the sets will be ignored. + + Example: + [Address] +NFTSet=prefix:netdev:filter:eth_ipv4_prefix + Corresponding NFT rules: + table netdev filter { + set eth_ipv4_prefix { + type ipv4_addr + flags interval + } + chain eth_ingress { + type filter hook ingress device "eth0" priority filter; policy drop; + ip daddr != @eth_ipv4_prefix drop + accept + } +} + +
+
@@ -2214,6 +2294,17 @@ allow my_server_t localnet_peer_t:peer recv; addresses. See NetLabel= in [Address] section for more details. + + + NFTSet= + + This applies the NFT set for the network configuration received with DHCP, like + NFTSet= in [Address] section applies it to static configuration. See + NFTSet= in [Address] section for more details. For address or + prefix source types, the type of the element used in the NFT filter must be + ipv4_addr. + + @@ -2355,6 +2446,17 @@ allow my_server_t localnet_peer_t:peer recv; + + NFTSet= + + This applies the NFT set for the network configuration received with DHCP, like + NFTSet= in [Address] section applies it to static configuration. See + NFTSet= in [Address] section for more details. For address + or prefix source types, the type of the element used in the NFT filter must be + ipv6_addr. + + + @@ -2460,6 +2562,17 @@ allow my_server_t localnet_peer_t:peer recv; addresses. See NetLabel= in [Address] section for more details. + + + NFTSet= + + This applies the NFT set for the network configuration received with DHCP, like + NFTSet= in [Address] section applies it to static configuration. See + NFTSet= in [Address] section for more details. For address or + prefix source types, the type of the element used in the NFT filter must be + ipv6_addr. + + @@ -2758,6 +2871,17 @@ Token=prefixstable:2002:da8:1:: addresses. See NetLabel= in [Address] section for more details. + + + NFTSet= + + This applies the NFT set for the network configuration received with RA, like + NFTSet= in [Address] section applies it to static configuration. See + NFTSet= in [Address] section for more details. For address or + prefix source types, the type of the element used in the NFT filter must be + ipv6_addr. + + diff --git a/src/basic/parse-util.c b/src/basic/parse-util.c index cbe5ad6a32e..b8fbe3f1c77 100644 --- a/src/basic/parse-util.c +++ b/src/basic/parse-util.c @@ -747,3 +747,22 @@ int parse_loadavg_fixed_point(const char *s, loadavg_t *ret) { return store_loadavg_fixed_point(i, f, ret); } + +/* Limitations are described in https://www.netfilter.org/projects/nftables/manpage.html and + * https://bugzilla.netfilter.org/show_bug.cgi?id=1175 */ +bool nft_identifier_valid(const char *id) { + if (!id) + return false; + + size_t len = strlen(id); + if (len == 0 || len > 31) + return false; + + if (!ascii_isalpha(id[0])) + return false; + + for (size_t i = 1; i < len; i++) + if (!ascii_isalpha(id[i]) && !ascii_isdigit(id[i]) && !IN_SET(id[i], '/', '\\', '_', '.')) + return false; + return true; +} diff --git a/src/basic/parse-util.h b/src/basic/parse-util.h index c480407c2a5..ca0252005ac 100644 --- a/src/basic/parse-util.h +++ b/src/basic/parse-util.h @@ -152,3 +152,5 @@ int parse_oom_score_adjust(const char *s, int *ret); * to a loadavg_t. */ int store_loadavg_fixed_point(unsigned long i, unsigned long f, loadavg_t *ret); int parse_loadavg_fixed_point(const char *s, loadavg_t *ret); + +bool nft_identifier_valid(const char *id); diff --git a/src/network/networkd-address.c b/src/network/networkd-address.c index 8d6dec36689..58eebc63c6e 100644 --- a/src/network/networkd-address.c +++ b/src/network/networkd-address.c @@ -173,6 +173,7 @@ Address *address_free(Address *address) { config_section_free(address->section); free(address->label); free(address->netlabel); + nft_set_context_clear(&address->nft_set_context); return mfree(address); } @@ -502,6 +503,8 @@ int address_dup(const Address *src, Address **ret) { dest->link = NULL; dest->label = NULL; dest->netlabel = NULL; + dest->nft_set_context.sets = NULL; + dest->nft_set_context.n_sets = 0; if (src->family == AF_INET) { r = free_and_strdup(&dest->label, src->label); @@ -513,6 +516,10 @@ int address_dup(const Address *src, Address **ret) { if (r < 0) return r; + r = nft_set_context_dup(&src->nft_set_context, &dest->nft_set_context); + if (r < 0) + return r; + *ret = TAKE_PTR(dest); return 0; } @@ -555,6 +562,82 @@ static int address_set_masquerade(Address *address, bool add) { return 0; } +static void address_modify_nft_set_context(Address *address, bool add, NFTSetContext *nft_set_context) { + int r; + + assert(address); + assert(address->link); + assert(address->link->manager); + assert(nft_set_context); + + if (!address->link->manager->fw_ctx) { + r = fw_ctx_new(&address->link->manager->fw_ctx); + if (r < 0) + return; + } + + FOREACH_ARRAY(nft_set, nft_set_context->sets, nft_set_context->n_sets) { + uint32_t ifindex; + + assert(nft_set); + + switch (nft_set->source) { + case NFT_SET_SOURCE_ADDRESS: + r = nft_set_element_modify_ip(address->link->manager->fw_ctx, add, nft_set->nfproto, address->family, nft_set->table, nft_set->set, + &address->in_addr); + break; + case NFT_SET_SOURCE_PREFIX: + r = nft_set_element_modify_iprange(address->link->manager->fw_ctx, add, nft_set->nfproto, address->family, nft_set->table, nft_set->set, + &address->in_addr, address->prefixlen); + break; + case NFT_SET_SOURCE_IFINDEX: + ifindex = address->link->ifindex; + r = nft_set_element_modify_any(address->link->manager->fw_ctx, add, nft_set->nfproto, nft_set->table, nft_set->set, + &ifindex, sizeof(ifindex)); + break; + default: + assert_not_reached(); + } + + if (r < 0) + log_warning_errno(r, "Failed to %s NFT set: family %s, table %s, set %s, IP address %s, ignoring", + add? "add" : "delete", + nfproto_to_string(nft_set->nfproto), nft_set->table, nft_set->set, + IN_ADDR_PREFIX_TO_STRING(address->family, &address->in_addr, address->prefixlen)); + else + log_debug("%s NFT set: family %s, table %s, set %s, IP address %s", + add ? "Added" : "Deleted", + nfproto_to_string(nft_set->nfproto), nft_set->table, nft_set->set, + IN_ADDR_PREFIX_TO_STRING(address->family, &address->in_addr, address->prefixlen)); + } +} + +static void address_modify_nft_set(Address *address, bool add) { + assert(address); + assert(address->link); + + if (!IN_SET(address->family, AF_INET, AF_INET6)) + return; + + if (!address->link->network) + return; + + switch (address->source) { + case NETWORK_CONFIG_SOURCE_DHCP4: + return address_modify_nft_set_context(address, add, &address->link->network->dhcp_nft_set_context); + case NETWORK_CONFIG_SOURCE_DHCP6: + return address_modify_nft_set_context(address, add, &address->link->network->dhcp6_nft_set_context); + case NETWORK_CONFIG_SOURCE_DHCP_PD: + return address_modify_nft_set_context(address, add, &address->link->network->dhcp_pd_nft_set_context); + case NETWORK_CONFIG_SOURCE_NDISC: + return address_modify_nft_set_context(address, add, &address->link->network->ndisc_nft_set_context); + case NETWORK_CONFIG_SOURCE_STATIC: + return address_modify_nft_set_context(address, add, &address->nft_set_context); + default: + return; + } +} + static int address_add(Link *link, Address *address) { int r; @@ -596,6 +679,8 @@ static int address_update(Address *address) { address_add_netlabel(address); + address_modify_nft_set(address, /* add = */ true); + if (address_is_ready(address) && address->callback) { r = address->callback(address); if (r < 0) @@ -615,6 +700,8 @@ static int address_drop(Address *address) { if (r < 0) log_link_warning_errno(link, r, "Failed to disable IP masquerading, ignoring: %m"); + address_modify_nft_set(address, /* add = */ false); + address_del_netlabel(address); address_free(address); @@ -1664,6 +1751,8 @@ int manager_rtnl_process_address(sd_netlink *rtnl, sd_netlink_message *message, address->source = a->source; address->provider = a->provider; (void) free_and_strdup_warn(&address->netlabel, a->netlabel); + nft_set_context_clear(&address->nft_set_context); + (void) nft_set_context_dup(&a->nft_set_context, &address->nft_set_context); address->requested_as_null = a->requested_as_null; address->callback = a->callback; } @@ -2352,3 +2441,41 @@ int network_drop_invalid_addresses(Network *network) { return 0; } + +int config_parse_address_ip_nft_set( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = userdata; + _cleanup_(address_free_or_set_invalidp) Address *n = NULL; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + assert(network); + + r = address_new_static(network, filename, section_line, &n); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to allocate a new address, ignoring assignment: %m"); + return 0; + } + + r = config_parse_nft_set(unit, filename, line, section, section_line, lvalue, ltype, rvalue, &n->nft_set_context, network); + if (r < 0) + return r; + + TAKE_PTR(n); + return 0; +} diff --git a/src/network/networkd-address.h b/src/network/networkd-address.h index 7a611529f76..4be8f92ef08 100644 --- a/src/network/networkd-address.h +++ b/src/network/networkd-address.h @@ -6,6 +6,7 @@ #include #include "conf-parser.h" +#include "firewall-util.h" #include "in-addr-util.h" #include "networkd-link.h" #include "networkd-util.h" @@ -59,6 +60,8 @@ struct Address { /* Called when address become ready */ address_ready_callback_t callback; + + NFTSetContext nft_set_context; }; const char* format_lifetime(char *buf, size_t l, usec_t lifetime_usec) _warn_unused_result_; @@ -132,3 +135,4 @@ CONFIG_PARSER_PROTOTYPE(config_parse_address_scope); CONFIG_PARSER_PROTOTYPE(config_parse_address_route_metric); CONFIG_PARSER_PROTOTYPE(config_parse_duplicate_address_detection); CONFIG_PARSER_PROTOTYPE(config_parse_address_netlabel); +CONFIG_PARSER_PROTOTYPE(config_parse_address_ip_nft_set); diff --git a/src/network/networkd-network-gperf.gperf b/src/network/networkd-network-gperf.gperf index 0f87ce24bb4..14734dadd5d 100644 --- a/src/network/networkd-network-gperf.gperf +++ b/src/network/networkd-network-gperf.gperf @@ -161,6 +161,7 @@ Address.DuplicateAddressDetection, config_parse_duplicate_address_dete Address.Scope, config_parse_address_scope, 0, 0 Address.RouteMetric, config_parse_address_route_metric, 0, 0 Address.NetLabel, config_parse_address_netlabel, 0, 0 +Address.NFTSet, config_parse_address_ip_nft_set, 0, 0 IPv6AddressLabel.Prefix, config_parse_address_label_prefix, 0, 0 IPv6AddressLabel.Label, config_parse_address_label, 0, 0 Neighbor.Address, config_parse_neighbor_address, 0, 0 @@ -255,6 +256,7 @@ DHCPv4.InitialAdvertisedReceiveWindow, config_parse_tcp_window, DHCPv4.FallbackLeaseLifetimeSec, config_parse_dhcp_fallback_lease_lifetime, 0, 0 DHCPv4.Use6RD, config_parse_bool, 0, offsetof(Network, dhcp_use_6rd) DHCPv4.NetLabel, config_parse_string, CONFIG_PARSE_STRING_SAFE, offsetof(Network, dhcp_netlabel) +DHCPv4.NFTSet, config_parse_nft_set, 0, offsetof(Network, dhcp_nft_set_context) DHCPv6.UseAddress, config_parse_bool, 0, offsetof(Network, dhcp6_use_address) DHCPv6.UseDelegatedPrefix, config_parse_bool, 0, offsetof(Network, dhcp6_use_pd_prefix) DHCPv6.UseDNS, config_parse_dhcp_use_dns, AF_INET6, 0 @@ -276,6 +278,7 @@ DHCPv6.DUIDRawData, config_parse_duid_rawdata, DHCPv6.RapidCommit, config_parse_bool, 0, offsetof(Network, dhcp6_use_rapid_commit) DHCPv6.NetLabel, config_parse_string, CONFIG_PARSE_STRING_SAFE, offsetof(Network, dhcp6_netlabel) DHCPv6.SendRelease, config_parse_bool, 0, offsetof(Network, dhcp6_send_release) +DHCPv6.NFTSet, config_parse_nft_set, 0, offsetof(Network, dhcp6_nft_set_context) IPv6AcceptRA.UseGateway, config_parse_bool, 0, offsetof(Network, ipv6_accept_ra_use_gateway) IPv6AcceptRA.UseRoutePrefix, config_parse_bool, 0, offsetof(Network, ipv6_accept_ra_use_route_prefix) IPv6AcceptRA.UseAutonomousPrefix, config_parse_bool, 0, offsetof(Network, ipv6_accept_ra_use_autonomous_prefix) @@ -297,6 +300,7 @@ IPv6AcceptRA.RouteAllowList, config_parse_in_addr_prefixes, IPv6AcceptRA.RouteDenyList, config_parse_in_addr_prefixes, AF_INET6, offsetof(Network, ndisc_deny_listed_route_prefix) IPv6AcceptRA.Token, config_parse_address_generation_type, 0, offsetof(Network, ndisc_tokens) IPv6AcceptRA.NetLabel, config_parse_string, CONFIG_PARSE_STRING_SAFE, offsetof(Network, ndisc_netlabel) +IPv6AcceptRA.NFTSet, config_parse_nft_set, 0, offsetof(Network, ndisc_nft_set_context) DHCPServer.ServerAddress, config_parse_dhcp_server_address, 0, 0 DHCPServer.UplinkInterface, config_parse_uplink, 0, 0 DHCPServer.RelayTarget, config_parse_in_addr_non_null, AF_INET, offsetof(Network, dhcp_server_relay_target) @@ -364,6 +368,7 @@ DHCPPrefixDelegation.ManageTemporaryAddress, config_parse_bool, DHCPPrefixDelegation.Token, config_parse_address_generation_type, 0, offsetof(Network, dhcp_pd_tokens) DHCPPrefixDelegation.RouteMetric, config_parse_uint32, 0, offsetof(Network, dhcp_pd_route_metric) DHCPPrefixDelegation.NetLabel, config_parse_string, CONFIG_PARSE_STRING_SAFE, offsetof(Network, dhcp_pd_netlabel) +DHCPPrefixDelegation.NFTSet, config_parse_nft_set, 0, offsetof(Network, dhcp_pd_nft_set_context) IPv6SendRA.RouterLifetimeSec, config_parse_router_lifetime, 0, offsetof(Network, router_lifetime_usec) IPv6SendRA.Managed, config_parse_bool, 0, offsetof(Network, router_managed) IPv6SendRA.OtherInformation, config_parse_bool, 0, offsetof(Network, router_other_information) diff --git a/src/network/networkd-network.c b/src/network/networkd-network.c index dbe8c59a625..57c0c999ac9 100644 --- a/src/network/networkd-network.c +++ b/src/network/networkd-network.c @@ -723,6 +723,7 @@ static Network *network_free(Network *network) { ordered_hashmap_free(network->dhcp_client_send_options); ordered_hashmap_free(network->dhcp_client_send_vendor_options); free(network->dhcp_netlabel); + nft_set_context_clear(&network->dhcp_nft_set_context); /* DHCPv6 client */ free(network->dhcp6_mudurl); @@ -732,11 +733,13 @@ static Network *network_free(Network *network) { ordered_hashmap_free(network->dhcp6_client_send_options); ordered_hashmap_free(network->dhcp6_client_send_vendor_options); free(network->dhcp6_netlabel); + nft_set_context_clear(&network->dhcp6_nft_set_context); /* DHCP PD */ free(network->dhcp_pd_uplink_name); set_free(network->dhcp_pd_tokens); free(network->dhcp_pd_netlabel); + nft_set_context_clear(&network->dhcp_pd_nft_set_context); /* Router advertisement */ ordered_set_free(network->router_search_domains); @@ -752,6 +755,7 @@ static Network *network_free(Network *network) { set_free(network->ndisc_allow_listed_route_prefix); set_free(network->ndisc_tokens); free(network->ndisc_netlabel); + nft_set_context_clear(&network->ndisc_nft_set_context); /* LLDP */ free(network->lldp_mudurl); diff --git a/src/network/networkd-network.h b/src/network/networkd-network.h index 031ba398e7e..9654911b58b 100644 --- a/src/network/networkd-network.h +++ b/src/network/networkd-network.h @@ -10,6 +10,7 @@ #include "bridge.h" #include "condition.h" #include "conf-parser.h" +#include "firewall-util.h" #include "hashmap.h" #include "ipoib.h" #include "net-condition.h" @@ -163,6 +164,7 @@ struct Network { OrderedHashmap *dhcp_client_send_options; OrderedHashmap *dhcp_client_send_vendor_options; char *dhcp_netlabel; + NFTSetContext dhcp_nft_set_context; /* DHCPv6 Client support */ bool dhcp6_use_address; @@ -191,6 +193,7 @@ struct Network { Set *dhcp6_request_options; char *dhcp6_netlabel; bool dhcp6_send_release; + NFTSetContext dhcp6_nft_set_context; /* DHCP Server Support */ bool dhcp_server; @@ -249,6 +252,7 @@ struct Network { int dhcp_pd_uplink_index; char *dhcp_pd_uplink_name; char *dhcp_pd_netlabel; + NFTSetContext dhcp_pd_nft_set_context; /* Bridge Support */ int use_bpdu; @@ -340,6 +344,7 @@ struct Network { Set *ndisc_allow_listed_route_prefix; Set *ndisc_tokens; char *ndisc_netlabel; + NFTSetContext ndisc_nft_set_context; /* LLDP support */ LLDPMode lldp_mode; /* LLDP reception */ diff --git a/src/shared/firewall-util-nft.c b/src/shared/firewall-util-nft.c index cc35b1c2de3..8adc7f8faa0 100644 --- a/src/shared/firewall-util-nft.c +++ b/src/shared/firewall-util-nft.c @@ -14,6 +14,8 @@ #include "sd-netlink.h" #include "alloc-util.h" +#include "escape.h" +#include "extract-word.h" #include "firewall-util.h" #include "firewall-util-private.h" #include "in-addr-util.h" @@ -21,6 +23,7 @@ #include "netlink-internal.h" #include "netlink-util.h" #include "socket-util.h" +#include "string-table.h" #include "time-util.h" #define NFT_SYSTEMD_DNAT_MAP_NAME "map_port_ipport" @@ -941,6 +944,67 @@ int nft_set_element_modify_iprange( return sd_nfnl_call_batch(ctx->nfnl, &m, 1, NFNL_DEFAULT_TIMEOUT_USECS, NULL); } +int nft_set_element_modify_ip( + FirewallContext *ctx, + bool add, + int nfproto, + int af, + const char *table, + const char *set, + const union in_addr_union *source) { + + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL; + int r; + + assert(ctx->nfnl); + assert(IN_SET(af, AF_INET, AF_INET6)); + assert(nfproto_is_valid(nfproto)); + assert(table); + assert(set); + + if (!source) + return -EINVAL; + + r = sd_nfnl_nft_message_new_setelems(ctx->nfnl, &m, add, nfproto, table, set); + if (r < 0) + return r; + + r = sd_netlink_message_open_container(m, NFTA_SET_ELEM_LIST_ELEMENTS); + if (r < 0) + return r; + + r = sd_nfnl_nft_message_append_setelem(m, 0, source, FAMILY_ADDRESS_SIZE(af), NULL, 0, 0); + if (r < 0) + return r; + + r = sd_netlink_message_close_container(m); /* NFTA_SET_ELEM_LIST_ELEMENTS */ + if (r < 0) + return r; + + return sd_nfnl_call_batch(ctx->nfnl, &m, 1, NFNL_DEFAULT_TIMEOUT_USECS, NULL); +} + +int nft_set_element_modify_any(FirewallContext *ctx, bool add, int nfproto, const char *table, const char *set, const void *element, size_t element_size) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL; + int r; + + assert(ctx); + assert(ctx->nfnl); + assert(nfproto_is_valid(nfproto)); + assert(table); + assert(set); + assert(element); + + if (add) + r = nft_add_element(ctx->nfnl, &m, nfproto, table, set, element, element_size, NULL, 0); + else + r = nft_del_element(ctx->nfnl, &m, nfproto, table, set, element, element_size, NULL, 0); + if (r < 0) + return r; + + return sd_nfnl_call_batch(ctx->nfnl, &m, 1, NFNL_DEFAULT_TIMEOUT_USECS, NULL); +} + static int af_to_nfproto(int af) { assert(IN_SET(af, AF_INET, AF_INET6)); @@ -1122,3 +1186,181 @@ int fw_nftables_add_local_dnat( /* table created anew; previous address already gone */ return fw_nftables_add_local_dnat_internal(ctx->nfnl, add, af, protocol, local_port, remote, remote_port, NULL); } + +static const char *const nfproto_table[] = { + [NFPROTO_ARP] = "arp", + [NFPROTO_BRIDGE] = "bridge", + [NFPROTO_INET] = "inet", + [NFPROTO_IPV4] = "ip", + [NFPROTO_IPV6] = "ip6", + [NFPROTO_NETDEV] = "netdev", +}; + +DEFINE_STRING_TABLE_LOOKUP(nfproto, int); + +static const char *const nft_set_source_table[] = { + [NFT_SET_SOURCE_ADDRESS] = "address", + [NFT_SET_SOURCE_PREFIX] = "prefix", + [NFT_SET_SOURCE_IFINDEX] = "ifindex", +}; + +DEFINE_STRING_TABLE_LOOKUP(nft_set_source, int); + +void nft_set_context_clear(NFTSetContext *s) { + assert(s); + + FOREACH_ARRAY(nft_set, s->sets, s->n_sets) { + free(nft_set->table); + free(nft_set->set); + } + + s->n_sets = 0; + s->sets = mfree(s->sets); +} + +static int nft_set_add(NFTSetContext *s, NFTSetSource source, int nfproto, const char *table, const char *set) { + _cleanup_free_ char *table_dup = NULL, *set_dup = NULL; + + assert(s); + assert(IN_SET(source, NFT_SET_SOURCE_ADDRESS, NFT_SET_SOURCE_PREFIX, NFT_SET_SOURCE_IFINDEX)); + assert(nfproto_is_valid(nfproto)); + assert(table); + assert(set); + + table_dup = strdup(table); + if (!table_dup) + return -ENOMEM; + + set_dup = strdup(set); + if (!set_dup) + return -ENOMEM; + + if (!GREEDY_REALLOC(s->sets, s->n_sets + 1)) + return -ENOMEM; + + s->sets[s->n_sets++] = (NFTSet) { + .source = source, + .nfproto = nfproto, + .table = TAKE_PTR(table_dup), + .set = TAKE_PTR(set_dup), + }; + + return 0; +} + +int nft_set_context_dup(const NFTSetContext *src, NFTSetContext *dst) { + int r; + _cleanup_(nft_set_context_clear) NFTSetContext d = (NFTSetContext) {}; + + assert(src); + assert(dst); + + FOREACH_ARRAY(nft_set, src->sets, src->n_sets) { + r = nft_set_add(&d, nft_set->source, nft_set->nfproto, nft_set->table, nft_set->set); + if (r < 0) + return r; + } + + *dst = TAKE_STRUCT(d); + + return 0; +} + +int config_parse_nft_set( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + NFTSetContext *nft_set_context = ASSERT_PTR(data); + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + assert(nft_set_context); + + if (isempty(rvalue)) { + nft_set_context_clear(nft_set_context); + + return 0; + } + + for (const char *p = rvalue;;) { + _cleanup_free_ char *tuple = NULL, *source_str = NULL, *family_str = NULL, *table = NULL, *set = NULL; + const char *q = NULL; + int nfproto; + NFTSetSource source; + + r = extract_first_word(&p, &tuple, NULL, EXTRACT_UNQUOTE|EXTRACT_RETAIN_ESCAPE); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + _cleanup_free_ char *esc = NULL; + + esc = cescape(rvalue); + log_syntax(unit, LOG_WARNING, filename, line, r, "Invalid syntax %s=%s, ignoring: %m", lvalue, strna(esc)); + return 0; + } + if (r == 0) + return 0; + + q = tuple; + r = extract_many_words(&q, ":", EXTRACT_CUNESCAPE, &source_str, &family_str, &table, &set, NULL); + if (r == -ENOMEM) + return log_oom(); + if (r != 4 || !isempty(q)) { + _cleanup_free_ char *esc = NULL; + + esc = cescape(tuple); + return log_syntax(unit, LOG_WARNING, filename, line, 0, "Failed to parse NFT set %s, ignoring", strna(esc)); + } + + assert(source_str); + assert(family_str); + assert(table); + assert(set); + + source = nft_set_source_from_string(source_str); + if (source < 0) { + _cleanup_free_ char *esc = NULL; + + esc = cescape(source_str); + return log_syntax(unit, LOG_WARNING, filename, line, 0, "Unknown NFT source %s, ignoring", strna(esc)); + } + + nfproto = nfproto_from_string(family_str); + if (nfproto < 0) { + _cleanup_free_ char *esc = NULL; + + esc = cescape(family_str); + return log_syntax(unit, LOG_WARNING, filename, line, 0, "Unknown NFT protocol family %s, ignoring", strna(esc)); + } + + if (!nft_identifier_valid(table)) { + _cleanup_free_ char *esc = NULL; + + esc = cescape(table); + return log_syntax(unit, LOG_WARNING, filename, line, 0, "Invalid table name %s, ignoring", strna(esc)); + } + + if (!nft_identifier_valid(set)) { + _cleanup_free_ char *esc = NULL; + + esc = cescape(set); + return log_syntax(unit, LOG_WARNING, filename, line, 0, "Invalid set name %s, ignoring", strna(esc)); + } + + r = nft_set_add(nft_set_context, source, nfproto, table, set); + if (r < 0) + return r; + } + + assert_not_reached(); +} diff --git a/src/shared/firewall-util.h b/src/shared/firewall-util.h index 4f3cd61bf44..bb35e76f21e 100644 --- a/src/shared/firewall-util.h +++ b/src/shared/firewall-util.h @@ -4,6 +4,7 @@ #include #include +#include "conf-parser.h" #include "in-addr-util.h" typedef struct FirewallContext FirewallContext; @@ -31,6 +32,35 @@ int fw_add_local_dnat( uint16_t remote_port, const union in_addr_union *previous_remote); +typedef enum NFTSetSource { + NFT_SET_SOURCE_ADDRESS, + NFT_SET_SOURCE_PREFIX, + NFT_SET_SOURCE_IFINDEX, + _NFT_SET_SOURCE_MAX, + _NFT_SET_SOURCE_INVALID = -EINVAL, +} NFTSetSource; + +typedef struct NFTSet { + NFTSetSource source; + int nfproto; + char *table; + char *set; +} NFTSet; + +typedef struct NFTSetContext { + NFTSet *sets; + size_t n_sets; +} NFTSetContext; + +void nft_set_context_clear(NFTSetContext *s); +int nft_set_context_dup(const NFTSetContext *src, NFTSetContext *dst); + +const char *nfproto_to_string(int i) _const_; +int nfproto_from_string(const char *s) _pure_; + +const char *nft_set_source_to_string(int i) _const_; +int nft_set_source_from_string(const char *s) _pure_; + int nft_set_element_modify_iprange( FirewallContext *ctx, bool add, @@ -40,3 +70,23 @@ int nft_set_element_modify_iprange( const char *set, const union in_addr_union *source, unsigned int source_prefixlen); + +int nft_set_element_modify_ip( + FirewallContext *ctx, + bool add, + int nfproto, + int af, + const char *table, + const char *set, + const union in_addr_union *source); + +int nft_set_element_modify_any( + FirewallContext *ctx, + bool add, + int nfproto, + const char *table, + const char *set, + const void *element, + size_t element_size); + +CONFIG_PARSER_PROTOTYPE(config_parse_nft_set); diff --git a/src/test/meson.build b/src/test/meson.build index 1857c9c53b7..6d39006d092 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -330,6 +330,10 @@ executables += [ 'conditions' : ['HAVE_KMOD'], 'type' : 'manual', }, + test_template + { + 'sources' : files('test-nft-set.c'), + 'type' : 'manual', + }, test_template + { 'sources' : files('test-nscd-flush.c'), 'conditions' : ['ENABLE_NSCD'], diff --git a/src/test/test-nft-set.c b/src/test/test-nft-set.c new file mode 100644 index 00000000000..df8e93aaa2e --- /dev/null +++ b/src/test/test-nft-set.c @@ -0,0 +1,70 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include + +#include "firewall-util.h" +#include "in-addr-util.h" +#include "log.h" +#include "netlink-internal.h" +#include "parse-util.h" +#include "string-util.h" +#include "tests.h" + +int main(int argc, char **argv) { + int r; + + assert_se(argc == 7); + + test_setup_logging(LOG_DEBUG); + + if (getuid() != 0) + return log_tests_skipped("not root"); + + int nfproto; + nfproto = nfproto_from_string(argv[2]); + assert_se(nfproto_is_valid(nfproto)); + + const char *table = argv[3], *set = argv[4]; + + FirewallContext *ctx; + r = fw_ctx_new(&ctx); + assert_se(r == 0); + + bool add; + if (streq(argv[1], "add")) + add = true; + else + add = false; + + if (streq(argv[5], "uint32")) { + uint32_t element; + + r = safe_atou32(argv[6], &element); + assert_se(r == 0); + + r = nft_set_element_modify_any(ctx, add, nfproto, table, set, &element, sizeof(element)); + assert_se(r == 0); + } else if (streq(argv[5], "in_addr")) { + union in_addr_union addr; + int af; + + r = in_addr_from_string_auto(argv[6], &af, &addr); + assert_se(r == 0); + + r = nft_set_element_modify_ip(ctx, add, nfproto, af, table, set, &addr); + assert_se(r == 0); + } else if (streq(argv[5], "network")) { + union in_addr_union addr; + int af; + unsigned char prefixlen; + + r = in_addr_prefix_from_string_auto(argv[6], &af, &addr, &prefixlen); + assert_se(r == 0); + + r = nft_set_element_modify_iprange(ctx, add, nfproto, af, table, set, &addr, prefixlen); + assert_se(r == 0); + } + + return 0; +}