diff --git a/man/systemd.network.xml b/man/systemd.network.xml index c39b364319..d0666c7ac0 100644 --- a/man/systemd.network.xml +++ b/man/systemd.network.xml @@ -2538,6 +2538,26 @@ IPv6Token=prefixstable:2002:da8:1:: + + [BridgeMDB] Section Options + The [BridgeMDB] section manages the multicast membership entries forwarding database table of a port and accepts the following + keys. Specify several [BridgeMDB] sections to configure several permanent multicast membership entries. + + + + MulticastGroupAddress= + + Specifies the IPv4 or IPv6 multicast group address to add. This setting is mandatory. + + + + VLANId= + + The VLAN ID for the new entry. Valid ranges are 0 (no VLAN) to 4094. Optional, defaults to 0. + + + + [LLDP] Section Options diff --git a/src/libsystemd/sd-netlink/netlink-types.c b/src/libsystemd/sd-netlink/netlink-types.c index 060458a534..23d2025156 100644 --- a/src/libsystemd/sd-netlink/netlink-types.c +++ b/src/libsystemd/sd-netlink/netlink-types.c @@ -968,6 +968,15 @@ static const NLTypeSystem rtnl_tca_type_system = { .types = rtnl_tca_types, }; +static const NLType mdb_types[] = { + [MDBA_SET_ENTRY] = { .size = sizeof(struct br_port_msg) }, +}; + +static const NLTypeSystem rtnl_mdb_type_system = { + .count = ELEMENTSOF(mdb_types), + .types = mdb_types, +}; + static const NLType error_types[] = { [NLMSGERR_ATTR_MSG] = { .type = NETLINK_TYPE_STRING }, [NLMSGERR_ATTR_OFFS] = { .type = NETLINK_TYPE_U32 }, @@ -1012,6 +1021,9 @@ static const NLType rtnl_types[] = { [RTM_NEWTCLASS] = { .type = NETLINK_TYPE_NESTED, .type_system = &rtnl_tca_type_system, .size = sizeof(struct tcmsg) }, [RTM_DELTCLASS] = { .type = NETLINK_TYPE_NESTED, .type_system = &rtnl_tca_type_system, .size = sizeof(struct tcmsg) }, [RTM_GETTCLASS] = { .type = NETLINK_TYPE_NESTED, .type_system = &rtnl_tca_type_system, .size = sizeof(struct tcmsg) }, + [RTM_NEWMDB] = { .type = NETLINK_TYPE_NESTED, .type_system = &rtnl_mdb_type_system, .size = sizeof(struct br_port_msg) }, + [RTM_DELMDB] = { .type = NETLINK_TYPE_NESTED, .type_system = &rtnl_mdb_type_system, .size = sizeof(struct br_port_msg) }, + [RTM_GETMDB] = { .type = NETLINK_TYPE_NESTED, .type_system = &rtnl_mdb_type_system, .size = sizeof(struct br_port_msg) }, }; const NLTypeSystem rtnl_type_system_root = { diff --git a/src/libsystemd/sd-netlink/netlink-util.h b/src/libsystemd/sd-netlink/netlink-util.h index 04e6a98e6b..ddf5686f13 100644 --- a/src/libsystemd/sd-netlink/netlink-util.h +++ b/src/libsystemd/sd-netlink/netlink-util.h @@ -51,6 +51,10 @@ static inline bool rtnl_message_type_is_tclass(uint16_t type) { return IN_SET(type, RTM_NEWTCLASS, RTM_DELTCLASS, RTM_GETTCLASS); } +static inline bool rtnl_message_type_is_mdb(uint16_t type) { + return IN_SET(type, RTM_NEWMDB, RTM_DELMDB, RTM_GETMDB); +} + int rtnl_set_link_name(sd_netlink **rtnl, int ifindex, const char *name); int rtnl_set_link_properties(sd_netlink **rtnl, int ifindex, const char *alias, const struct ether_addr *mac, uint32_t mtu); int rtnl_get_link_alternative_names(sd_netlink **rtnl, int ifindex, char ***ret); diff --git a/src/libsystemd/sd-netlink/rtnl-message.c b/src/libsystemd/sd-netlink/rtnl-message.c index 38563e5f34..268150044c 100644 --- a/src/libsystemd/sd-netlink/rtnl-message.c +++ b/src/libsystemd/sd-netlink/rtnl-message.c @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -1120,3 +1121,24 @@ int sd_rtnl_message_set_tclass_handle(sd_netlink_message *m, uint32_t handle) { return 0; } + +int sd_rtnl_message_new_mdb(sd_netlink *rtnl, sd_netlink_message **ret, uint16_t nlmsg_type, int mdb_ifindex) { + struct br_port_msg *bpm; + int r; + + assert_return(rtnl_message_type_is_mdb(nlmsg_type), -EINVAL); + assert_return(ret, -EINVAL); + + r = message_new(rtnl, ret, nlmsg_type); + if (r < 0) + return r; + + if (nlmsg_type == RTM_NEWMDB) + (*ret)->hdr->nlmsg_flags |= NLM_F_CREATE | NLM_F_EXCL; + + bpm = NLMSG_DATA((*ret)->hdr); + bpm->family = AF_BRIDGE; + bpm->ifindex = mdb_ifindex; + + return 0; +} diff --git a/src/network/meson.build b/src/network/meson.build index b3a88d9910..cb8f801031 100644 --- a/src/network/meson.build +++ b/src/network/meson.build @@ -87,6 +87,8 @@ sources = files(''' networkd-manager-bus.h networkd-manager.c networkd-manager.h + networkd-mdb.c + networkd-mdb.h networkd-ndisc.c networkd-ndisc.h networkd-neighbor.c diff --git a/src/network/networkd-link.c b/src/network/networkd-link.c index 47af15b6b3..297b26cb69 100644 --- a/src/network/networkd-link.c +++ b/src/network/networkd-link.c @@ -1254,6 +1254,30 @@ static int link_set_bridge_fdb(Link *link) { return 0; } +static int link_set_bridge_mdb(Link *link) { + MdbEntry *mdb_entry; + int r; + + if (!link->network) + return 0; + + if (LIST_IS_EMPTY(link->network->static_mdb_entries)) + return 0; + + if (!link->network->bridge) { + log_link_error(link, "Cannot configure MDB entries on non-bridge port"); + return 0; + } + + LIST_FOREACH(static_mdb_entries, mdb_entry, link->network->static_mdb_entries) { + r = mdb_entry_configure(link, mdb_entry); + if (r < 0) + return log_link_error_errno(link, r, "Failed to add entry to multicast group database: %m"); + } + + return 0; +} + static int static_address_ready_callback(Address *address) { Address *a; Link *link; @@ -3907,6 +3931,10 @@ static int link_carrier_gained(Link *link) { if (r < 0) return r; + r = link_set_bridge_mdb(link); + if (r < 0) + return r; + return 0; } diff --git a/src/network/networkd-mdb.c b/src/network/networkd-mdb.c new file mode 100644 index 0000000000..3dec19b1c2 --- /dev/null +++ b/src/network/networkd-mdb.c @@ -0,0 +1,252 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include +#include +#include + +#include "alloc-util.h" +#include "netlink-util.h" +#include "networkd-manager.h" +#include "networkd-mdb.h" +#include "util.h" +#include "vlan-util.h" + +#define STATIC_MDB_ENTRIES_PER_NETWORK_MAX 1024U + +/* create a new MDB entry or get an existing one. */ +static int mdb_entry_new_static( + Network *network, + const char *filename, + unsigned section_line, + MdbEntry **ret) { + + _cleanup_(network_config_section_freep) NetworkConfigSection *n = NULL; + _cleanup_(mdb_entry_freep) MdbEntry *mdb_entry = NULL; + int r; + + assert(network); + assert(ret); + assert(!!filename == (section_line > 0)); + + /* search entry in hashmap first. */ + if (filename) { + r = network_config_section_new(filename, section_line, &n); + if (r < 0) + return r; + + mdb_entry = hashmap_get(network->mdb_entries_by_section, n); + if (mdb_entry) { + *ret = TAKE_PTR(mdb_entry); + return 0; + } + } + + if (network->n_static_mdb_entries >= STATIC_MDB_ENTRIES_PER_NETWORK_MAX) + return -E2BIG; + + /* allocate space for an MDB entry. */ + mdb_entry = new(MdbEntry, 1); + if (!mdb_entry) + return -ENOMEM; + + /* init MDB structure. */ + *mdb_entry = (MdbEntry) { + .network = network, + }; + + LIST_PREPEND(static_mdb_entries, network->static_mdb_entries, mdb_entry); + network->n_static_mdb_entries++; + + if (filename) { + mdb_entry->section = TAKE_PTR(n); + + r = hashmap_ensure_allocated(&network->mdb_entries_by_section, &network_config_hash_ops); + if (r < 0) + return r; + + r = hashmap_put(network->mdb_entries_by_section, mdb_entry->section, mdb_entry); + if (r < 0) + return r; + } + + /* return allocated MDB structure. */ + *ret = TAKE_PTR(mdb_entry); + + return 0; +} + +/* parse the VLAN Id from config files. */ +int config_parse_mdb_vlan_id( + 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_(mdb_entry_free_or_set_invalidp) MdbEntry *mdb_entry = NULL; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = mdb_entry_new_static(network, filename, section_line, &mdb_entry); + if (r < 0) + return log_oom(); + + r = config_parse_vlanid(unit, filename, line, section, + section_line, lvalue, ltype, + rvalue, &mdb_entry->vlan_id, userdata); + if (r < 0) + return r; + + mdb_entry = NULL; + + return 0; +} + +/* parse the multicast group from config files. */ +int config_parse_mdb_group_address( + 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_(mdb_entry_free_or_set_invalidp) MdbEntry *mdb_entry = NULL; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = mdb_entry_new_static(network, filename, section_line, &mdb_entry); + if (r < 0) + return log_oom(); + + r = in_addr_from_string_auto(rvalue, &mdb_entry->family, &mdb_entry->group_addr); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, "Cannot parse multicast group address: %m"); + return 0; + } + + mdb_entry = NULL; + + return 0; +} + +/* remove and MDB entry. */ +MdbEntry *mdb_entry_free(MdbEntry *mdb_entry) { + if (!mdb_entry) + return NULL; + + if (mdb_entry->network) { + LIST_REMOVE(static_mdb_entries, mdb_entry->network->static_mdb_entries, mdb_entry); + assert(mdb_entry->network->n_static_mdb_entries > 0); + mdb_entry->network->n_static_mdb_entries--; + + if (mdb_entry->section) + hashmap_remove(mdb_entry->network->mdb_entries_by_section, mdb_entry->section); + } + + network_config_section_free(mdb_entry->section); + + return mfree(mdb_entry); +} + +static int set_mdb_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) { + int r; + + assert(link); + + if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER)) + return 1; + + r = sd_netlink_message_get_errno(m); + if (r < 0 && r != -EEXIST) { + log_link_message_warning_errno(link, m, r, "Could not add MDB entry"); + link_enter_failed(link); + return 1; + } + + return 1; +} + +int mdb_entry_verify(MdbEntry *mdb_entry) { + if (section_is_invalid(mdb_entry->section)) + return -EINVAL; + + if (in_addr_is_multicast(mdb_entry->family, &mdb_entry->group_addr) <= 0) { + log_error("No valid MulticastGroupAddress= assignment in this section"); + return -EINVAL; + } + + return 0; +} + +/* send a request to the kernel to add an MDB entry */ +int mdb_entry_configure(Link *link, MdbEntry *mdb_entry) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL; + struct br_mdb_entry entry; + int r; + + assert(link); + assert(link->network); + assert(link->manager); + assert(mdb_entry); + + entry = (struct br_mdb_entry) { + .state = MDB_PERMANENT, + .ifindex = link->ifindex, + .vid = mdb_entry->vlan_id, + }; + + /* create new RTM message */ + r = sd_rtnl_message_new_mdb(link->manager->rtnl, &req, RTM_NEWMDB, link->network->bridge->ifindex); + if (r < 0) + return log_link_error_errno(link, r, "Could not create RTM_NEWMDB message: %m"); + + switch (mdb_entry->family) { + case AF_INET: + entry.addr.u.ip4 = mdb_entry->group_addr.in.s_addr; + entry.addr.proto = htobe16(ETH_P_IP); + break; + + case AF_INET6: + entry.addr.u.ip6 = mdb_entry->group_addr.in6; + entry.addr.proto = htobe16(ETH_P_IPV6); + break; + + default: + assert_not_reached("Invalid address family"); + } + + r = sd_netlink_message_append_data(req, MDBA_SET_ENTRY, &entry, sizeof(entry)); + if (r < 0) + return log_link_error_errno(link, r, "Could not append MDBA_SET_ENTRY attribute: %m"); + + r = netlink_call_async(link->manager->rtnl, NULL, req, set_mdb_handler, + link_netlink_destroy_callback, link); + if (r < 0) + return log_link_error_errno(link, r, "Could not send rtnetlink message: %m"); + + link_ref(link); + + return 1; +} diff --git a/src/network/networkd-mdb.h b/src/network/networkd-mdb.h new file mode 100644 index 0000000000..9ac8a18188 --- /dev/null +++ b/src/network/networkd-mdb.h @@ -0,0 +1,32 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include "conf-parser.h" +#include "list.h" +#include "macro.h" +#include "networkd-util.h" + +typedef struct Network Network; +typedef struct MdbEntry MdbEntry; +typedef struct Link Link; +typedef struct NetworkConfigSection NetworkConfigSection; + +struct MdbEntry { + Network *network; + NetworkConfigSection *section; + + int family; + union in_addr_union group_addr; + uint16_t vlan_id; + + LIST_FIELDS(MdbEntry, static_mdb_entries); +}; + +int mdb_entry_verify(MdbEntry *mdb_entry); +MdbEntry *mdb_entry_free(MdbEntry *mdb_entry); +int mdb_entry_configure(Link *link, MdbEntry *mdb_entry); + +DEFINE_NETWORK_SECTION_FUNCTIONS(MdbEntry, mdb_entry_free); + +CONFIG_PARSER_PROTOTYPE(config_parse_mdb_group_address); +CONFIG_PARSER_PROTOTYPE(config_parse_mdb_vlan_id); diff --git a/src/network/networkd-network-gperf.gperf b/src/network/networkd-network-gperf.gperf index d07714f5a1..845fafe17e 100644 --- a/src/network/networkd-network-gperf.gperf +++ b/src/network/networkd-network-gperf.gperf @@ -265,6 +265,8 @@ BridgeFDB.VLANId, config_parse_fdb_vlan_id, BridgeFDB.Destination, config_parse_fdb_destination, 0, 0 BridgeFDB.VNI, config_parse_fdb_vxlan_vni, 0, 0 BridgeFDB.AssociatedWith, config_parse_fdb_ntf_flags, 0, 0 +BridgeMDB.MulticastGroupAddress, config_parse_mdb_group_address, 0, 0 +BridgeMDB.VLANId, config_parse_mdb_vlan_id, 0, 0 BridgeVLAN.PVID, config_parse_brvlan_pvid, 0, 0 BridgeVLAN.VLAN, config_parse_brvlan_vlan, 0, 0 BridgeVLAN.EgressUntagged, config_parse_brvlan_untagged, 0, 0 diff --git a/src/network/networkd-network.c b/src/network/networkd-network.c index a61e774e36..81876902f5 100644 --- a/src/network/networkd-network.c +++ b/src/network/networkd-network.c @@ -157,6 +157,7 @@ int network_verify(Network *network) { Prefix *prefix, *prefix_next; Route *route, *route_next; FdbEntry *fdb, *fdb_next; + MdbEntry *mdb, *mdb_next; TrafficControl *tc; SRIOV *sr_iov; @@ -305,6 +306,10 @@ int network_verify(Network *network) { if (section_is_invalid(fdb->section)) fdb_entry_free(fdb); + LIST_FOREACH_SAFE(static_mdb_entries, mdb, mdb_next, network->static_mdb_entries) + if (mdb_entry_verify(mdb) < 0) + mdb_entry_free(mdb); + LIST_FOREACH_SAFE(neighbors, neighbor, neighbor_next, network->neighbors) if (neighbor_section_verify(neighbor) < 0) neighbor_free(neighbor); @@ -508,6 +513,7 @@ int network_load_one(Manager *manager, OrderedHashmap **networks, const char *fi "IPv6NDPProxyAddress\0" "Bridge\0" "BridgeFDB\0" + "BridgeMDB\0" "BridgeVLAN\0" "IPv6PrefixDelegation\0" "IPv6Prefix\0" @@ -642,6 +648,7 @@ static Network *network_free(Network *network) { RoutingPolicyRule *rule; AddressLabel *label; FdbEntry *fdb_entry; + MdbEntry *mdb_entry; Neighbor *neighbor; Address *address; NextHop *nexthop; @@ -715,6 +722,9 @@ static Network *network_free(Network *network) { while ((fdb_entry = network->static_fdb_entries)) fdb_entry_free(fdb_entry); + while ((mdb_entry = network->static_mdb_entries)) + mdb_entry_free(mdb_entry); + while ((ipv6_proxy_ndp_address = network->ipv6_proxy_ndp_addresses)) ipv6_proxy_ndp_address_free(ipv6_proxy_ndp_address); @@ -737,6 +747,7 @@ static Network *network_free(Network *network) { hashmap_free(network->routes_by_section); hashmap_free(network->nexthops_by_section); hashmap_free(network->fdb_entries_by_section); + hashmap_free(network->mdb_entries_by_section); hashmap_free(network->neighbors_by_section); hashmap_free(network->address_labels_by_section); hashmap_free(network->prefixes_by_section); @@ -849,6 +860,7 @@ bool network_has_static_ipv6_configurations(Network *network) { Address *address; Route *route; FdbEntry *fdb; + MdbEntry *mdb; Neighbor *neighbor; assert(network); @@ -865,6 +877,10 @@ bool network_has_static_ipv6_configurations(Network *network) { if (fdb->family == AF_INET6) return true; + LIST_FOREACH(static_mdb_entries, mdb, network->static_mdb_entries) + if (mdb->family == AF_INET6) + return true; + LIST_FOREACH(neighbors, neighbor, network->neighbors) if (neighbor->family == AF_INET6) return true; diff --git a/src/network/networkd-network.h b/src/network/networkd-network.h index 5dcb3c548b..5ba0bc705d 100644 --- a/src/network/networkd-network.h +++ b/src/network/networkd-network.h @@ -23,6 +23,7 @@ #include "networkd-ipv6-proxy-ndp.h" #include "networkd-lldp-rx.h" #include "networkd-lldp-tx.h" +#include "networkd-mdb.h" #include "networkd-ndisc.h" #include "networkd-neighbor.h" #include "networkd-nexthop.h" @@ -288,6 +289,7 @@ struct Network { LIST_HEAD(Route, static_routes); LIST_HEAD(NextHop, static_nexthops); LIST_HEAD(FdbEntry, static_fdb_entries); + LIST_HEAD(MdbEntry, static_mdb_entries); LIST_HEAD(IPv6ProxyNDPAddress, ipv6_proxy_ndp_addresses); LIST_HEAD(Neighbor, neighbors); LIST_HEAD(AddressLabel, address_labels); @@ -299,6 +301,7 @@ struct Network { unsigned n_static_routes; unsigned n_static_nexthops; unsigned n_static_fdb_entries; + unsigned n_static_mdb_entries; unsigned n_ipv6_proxy_ndp_addresses; unsigned n_neighbors; unsigned n_address_labels; @@ -310,6 +313,7 @@ struct Network { Hashmap *routes_by_section; Hashmap *nexthops_by_section; Hashmap *fdb_entries_by_section; + Hashmap *mdb_entries_by_section; Hashmap *neighbors_by_section; Hashmap *address_labels_by_section; Hashmap *prefixes_by_section; diff --git a/src/systemd/sd-netlink.h b/src/systemd/sd-netlink.h index f9196491d6..ce7ed36d85 100644 --- a/src/systemd/sd-netlink.h +++ b/src/systemd/sd-netlink.h @@ -216,6 +216,8 @@ int sd_rtnl_message_new_tclass(sd_netlink *rtnl, sd_netlink_message **ret, uint1 int sd_rtnl_message_set_tclass_parent(sd_netlink_message *m, uint32_t parent); int sd_rtnl_message_set_tclass_handle(sd_netlink_message *m, uint32_t handle); +int sd_rtnl_message_new_mdb(sd_netlink *rtnl, sd_netlink_message **ret, uint16_t nlmsg_type, int mdb_ifindex); + /* genl */ int sd_genl_socket_open(sd_netlink **nl); int sd_genl_message_new(sd_netlink *nl, sd_genl_family family, uint8_t cmd, sd_netlink_message **m); diff --git a/test/fuzz/fuzz-network-parser/directives.network b/test/fuzz/fuzz-network-parser/directives.network index 5d80470719..054b630226 100644 --- a/test/fuzz/fuzz-network-parser/directives.network +++ b/test/fuzz/fuzz-network-parser/directives.network @@ -54,6 +54,9 @@ MACAddress= Destination= VNI= AssociatedWith= +[BridgeMDB] +MulticastGroupAddress= +VLANId= [DHCP] UseDomains= UseRoutes=