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 }
        }
}
```
This commit is contained in:
Topi Miettinen
2023-08-26 21:37:09 +03:00
parent 274ffe1abb
commit fc289dd0ad
12 changed files with 656 additions and 0 deletions
+242
View File
@@ -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();
}
+50
View File
@@ -4,6 +4,7 @@
#include <stdbool.h>
#include <stdint.h>
#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);