From 5d42dea5e37f64ff09715fedf450cfee27546c0c Mon Sep 17 00:00:00 2001 From: Christophe Fontaine Date: Tue, 18 Nov 2025 11:59:17 +0100 Subject: [PATCH 1/7] infra: split L2 and L3 id Bridge interfaces belong to both a VRF and a bridge domain, and so can't be part of a union. Signed-off-by: Christophe Fontaine --- modules/infra/api/gr_infra.h | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/modules/infra/api/gr_infra.h b/modules/infra/api/gr_infra.h index 740841c74..8638af4e0 100644 --- a/modules/infra/api/gr_infra.h +++ b/modules/infra/api/gr_infra.h @@ -66,10 +66,8 @@ struct __gr_iface_base { gr_iface_flags_t flags; // Interface flags. Bit mask of GR_IFACE_F_*. gr_iface_state_t state; // Interface state. Bit mask of GR_IFACE_S_*. uint16_t mtu; // Maximum transmission unit size (incl. headers). - union { - uint16_t vrf_id; // L3 addressing and routing domain - uint16_t domain_id; // L2 xconnect peer interface id - }; + uint16_t vrf_id; // L3 addressing and routing domain + uint16_t domain_id; // L2 xconnect peer interface id uint32_t speed; //!< Link speed in Megabit/sec. }; From d217a39908b9b0992acebd0374bcf328213fe5e3 Mon Sep 17 00:00:00 2001 From: Christophe Fontaine Date: Thu, 27 Nov 2025 21:15:25 +0100 Subject: [PATCH 2/7] l2: add switch functionality Grout is meant to be a router. Yet, for some usecases (vxlan EVPN, SRv6 L2 encap, ... ), it is required to be able to switch packets in addition to route them. Add a 'l2' module to add a mac learning switch to grout. Expiration is based on libevent. Signed-off-by: Christophe Fontaine --- docs/graph.svg | 722 +++++++++++++++-------------- modules/infra/api/gr_infra.h | 3 + modules/infra/cli/iface.c | 12 +- modules/l2/api/gr_l2.h | 136 ++++++ modules/l2/api/meson.build | 5 + modules/l2/cli/bridge.c | 392 ++++++++++++++++ modules/l2/cli/bridge.h | 17 + modules/l2/cli/mac.c | 291 ++++++++++++ modules/l2/cli/meson.build | 8 + modules/l2/control/api_handlers.c | 323 +++++++++++++ modules/l2/control/bridge.c | 385 +++++++++++++++ modules/l2/control/gr_l2_control.h | 65 +++ modules/l2/control/mac_table.c | 540 +++++++++++++++++++++ modules/l2/control/meson.build | 9 + modules/l2/datapath/l2_bridge.c | 190 ++++++++ modules/l2/datapath/l2_flood.c | 168 +++++++ modules/l2/datapath/meson.build | 8 + modules/l2/meson.build | 7 + modules/meson.build | 1 + smoke/l2_bridge_test.sh | 76 +++ 20 files changed, 3018 insertions(+), 340 deletions(-) create mode 100644 modules/l2/api/gr_l2.h create mode 100644 modules/l2/api/meson.build create mode 100644 modules/l2/cli/bridge.c create mode 100644 modules/l2/cli/bridge.h create mode 100644 modules/l2/cli/mac.c create mode 100644 modules/l2/cli/meson.build create mode 100644 modules/l2/control/api_handlers.c create mode 100644 modules/l2/control/bridge.c create mode 100644 modules/l2/control/gr_l2_control.h create mode 100644 modules/l2/control/mac_table.c create mode 100644 modules/l2/control/meson.build create mode 100644 modules/l2/datapath/l2_bridge.c create mode 100644 modules/l2/datapath/l2_flood.c create mode 100644 modules/l2/datapath/meson.build create mode 100644 modules/l2/meson.build create mode 100755 smoke/l2_bridge_test.sh diff --git a/docs/graph.svg b/docs/graph.svg index 2c4bf7136..7ae123fa4 100644 --- a/docs/graph.svg +++ b/docs/graph.svg @@ -1,810 +1,858 @@ - - - - + + + bond_output - -bond_output + +bond_output port_output - -port_output + +port_output bond_output->port_output - - + + l1_xconnect - -l1_xconnect + +l1_xconnect l1_xconnect->port_output - - + + port_tx - -port_tx + +port_tx port_output->port_tx - - + + port_rx - -port_rx + +port_rx port_rx->l1_xconnect - - + + eth_input - -eth_input + +eth_input port_rx->eth_input - - + + + + + +l2_bridge + +l2_bridge + + + +port_rx->l2_bridge + + lacp_input - -lacp_input + +lacp_input eth_input->lacp_input - - + + snap_input - -snap_input + +snap_input eth_input->snap_input - - + + arp_input - -arp_input + +arp_input eth_input->arp_input - - + + ip_input - -ip_input + +ip_input eth_input->ip_input - - + + ip6_input - -ip6_input + +ip6_input eth_input->ip6_input - - + + eth_output - -eth_output + +eth_output eth_output->bond_output - - + + eth_output->port_output - - + + lacp_output - -lacp_output + +lacp_output lacp_output->eth_output - - + + - + l2_redirect - -l2_redirect + +l2_redirect - + snap_input->l2_redirect - - + + arp_input_reply - -arp_input_reply + +arp_input_reply - + arp_input->arp_input_reply - - + + arp_input_request - -arp_input_request + +arp_input_request - + arp_input->arp_input_request - - + + arp_output_reply - -arp_output_reply + +arp_output_reply - + arp_output_reply->eth_output - - + + arp_output_request - -arp_output_request + +arp_output_request - + arp_output_request->eth_output - - + + loopback_input - -loopback_input + +loopback_input loopback_input->ip_input - - + + loopback_input->ip6_input - - + + loopback_output - -loopback_output + +loopback_output loop_xvrf - -loop_xvrf + +loop_xvrf loop_xvrf->ip_input - - + + loop_xvrf->ip6_input - - + + ip_forward - -ip_forward + +ip_forward ip_output - -ip_output + +ip_output - + ip_forward->ip_output - - + + ip_fragment - -ip_fragment + +ip_fragment - + ip_fragment->ip_output - - + + ip_hold - -ip_hold + +ip_hold - + ip_input->ip_forward - - + + ip_input_local - -ip_input_local + +ip_input_local - + ip_input->ip_input_local - - + + - + ip_input->ip_output - - + + dnat44_dynamic - -dnat44_dynamic + +dnat44_dynamic - + ip_input->dnat44_dynamic - - + + dnat44_static - -dnat44_static + +dnat44_static - + ip_input->dnat44_static - - + + ip_loadbalance - -ip_loadbalance + +ip_loadbalance - + ip_loadbalance->ip_output - - + + ipip_input - -ipip_input + +ipip_input - + ip_input_local->ipip_input - - + + icmp_input - -icmp_input + +icmp_input - + ip_input_local->icmp_input - - + + l4_input_local - -l4_input_local + +l4_input_local - + ip_input_local->l4_input_local - - + + - + ospf_redirect - -ospf_redirect + +ospf_redirect - + ip_input_local->ospf_redirect - - + + - + ip_output->eth_output - - + + - + ip_output->loop_xvrf - - + + - + ip_output->ip_fragment - - + + - + ip_output->ip_hold - - + + - + ip_output->ip_loadbalance - - + + ipip_output - -ipip_output + +ipip_output - + ip_output->ipip_output - - + + sr6_output - -sr6_output + +sr6_output - + ip_output->sr6_output - - + + ip6_forward - -ip6_forward + +ip6_forward ip6_output - -ip6_output + +ip6_output - + ip6_forward->ip6_output - - + + ip6_hold - -ip6_hold + +ip6_hold - + ip6_input->ip6_forward - - + + ip6_input_local - -ip6_input_local + +ip6_input_local - + ip6_input->ip6_input_local - - + + - + ip6_input->ip6_output - - + + sr6_local - -sr6_local + +sr6_local - + ip6_input->sr6_local - - + + ip6_loadbalance - -ip6_loadbalance + +ip6_loadbalance - + ip6_loadbalance->ip6_output - - + + icmp6_input - -icmp6_input + +icmp6_input - + ip6_input_local->icmp6_input - - + + - + ip6_input_local->l4_input_local - - + + - + ip6_input_local->ospf_redirect - - + + - + ip6_output->eth_output - - + + - + ip6_output->loop_xvrf - - + + - + ip6_output->ip6_hold - - + + - + ip6_output->ip6_loadbalance - - + + - + ip6_output->sr6_output - - + + - + ipip_input->ip_input - - + + - + ipip_output->ip_output - - + + - + dnat44_dynamic->ip_forward - - + + - + dnat44_dynamic->ip_input_local - - + + - + dnat44_static->ip_forward - - + + - + dnat44_static->ip_input_local - - + + - + sr6_local->ip_input - - + + - + sr6_local->ip6_input - - + + - + sr6_local->ip6_input_local - - + + - + sr6_output->ip6_output - - + + icmp_output - -icmp_output + +icmp_output - + icmp_input->icmp_output - - + + icmp_local_send - -icmp_local_send + +icmp_local_send - + icmp_local_send->icmp_output - - + + - + icmp_output->ip_output - - + + icmp6_output - -icmp6_output + +icmp6_output - + icmp6_input->icmp6_output - - + + ndp_na_input - -ndp_na_input + +ndp_na_input - + icmp6_input->ndp_na_input - - + + ndp_ns_input - -ndp_ns_input + +ndp_ns_input - + icmp6_input->ndp_ns_input - - + + ndp_rs_input - -ndp_rs_input + +ndp_rs_input - + icmp6_input->ndp_rs_input - - + + - + icmp6_output->ip6_output - - + + icmp6_local_send - -icmp6_local_send + +icmp6_local_send - + icmp6_local_send->icmp6_output - - + + ndp_na_output - -ndp_na_output + +ndp_na_output - + ndp_na_output->icmp6_output - - + + ndp_ns_output - -ndp_ns_output + +ndp_ns_output - + ndp_ns_output->icmp6_output - - + + l4_loopback_output - -l4_loopback_output + +l4_loopback_output - + l4_input_local->l4_loopback_output - - + + - + dhcp_input - -dhcp_input + +dhcp_input - + l4_input_local->dhcp_input - - + + - + l4_loopback_output->loopback_output - - + + - + + +l2_bridge->bond_output + + + + + +l2_bridge->port_output + + + + + +l2_flood + +l2_flood + + +l2_bridge->l2_flood + + + + + ospf_redirect->l2_redirect - - + + + + + +l2_flood->bond_output + + + + + +l2_flood->port_output + + diff --git a/modules/infra/api/gr_infra.h b/modules/infra/api/gr_infra.h index 8638af4e0..69f0d936a 100644 --- a/modules/infra/api/gr_infra.h +++ b/modules/infra/api/gr_infra.h @@ -55,6 +55,7 @@ typedef enum : uint16_t { typedef enum : uint8_t { GR_IFACE_MODE_L3 = 0, GR_IFACE_MODE_L1_XC, + GR_IFACE_MODE_L2_BRIDGE, GR_IFACE_MODE_COUNT } gr_iface_mode_t; @@ -411,6 +412,8 @@ static inline const char *gr_iface_mode_name(gr_iface_mode_t mode) { return "l3"; case GR_IFACE_MODE_L1_XC: return "l1-xc"; + case GR_IFACE_MODE_L2_BRIDGE: + return "l2-bridge"; case GR_IFACE_MODE_COUNT: break; } diff --git a/modules/infra/cli/iface.c b/modules/infra/cli/iface.c index c1cf79667..57c0127c0 100644 --- a/modules/infra/cli/iface.c +++ b/modules/infra/cli/iface.c @@ -256,18 +256,24 @@ static cmd_status_t iface_list(struct gr_api_client *c, const struct ec_pnode *p switch (iface->mode) { case GR_IFACE_MODE_L1_XC: scols_line_set_data(line, 3, "XC"); + scols_line_set_data(line, 4, ""); break; case GR_IFACE_MODE_L3: scols_line_set_data(line, 3, "L3"); + // VRF + scols_line_sprintf(line, 4, "vrf: %u", iface->vrf_id); + break; + case GR_IFACE_MODE_L2_BRIDGE: + scols_line_set_data(line, 3, "L2"); + // Bridge domain + scols_line_sprintf(line, 4, "br: %u", iface->domain_id); break; default: scols_line_sprintf(line, 3, "%u", iface->mode); + scols_line_set_data(line, 4, ""); break; } - // vrf - scols_line_sprintf(line, 4, "%u", iface->vrf_id); - // type scols_line_sprintf(line, 5, "%s", gr_iface_type_name(iface->type)); diff --git a/modules/l2/api/gr_l2.h b/modules/l2/api/gr_l2.h new file mode 100644 index 000000000..8f74da71a --- /dev/null +++ b/modules/l2/api/gr_l2.h @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2025 Christophe Fontaine + +#pragma once + +#include +#include +#include + +#include + +#define GR_L2_MODULE 0x1200 + +// Bridge domain limits +#define GR_BRIDGE_ID_UNDEF 0 +#define GR_MAX_BRIDGE_DOMAINS 256 +#define GR_L2_BRIDGE_NAME_SIZE 32 + +typedef enum : uint8_t { + GR_L2_MAC_DYNAMIC = 0, + GR_L2_MAC_STATIC = 1, +} gr_l2_mac_type_t; + +// Bridge domain configuration +struct gr_l2_bridge_config { + uint32_t aging_time; // MAC aging time in seconds (0 = no aging) + uint32_t max_mac_count; // Maximum MAC entries per bridge (0 = no limit) + bool flood_unknown; // Flood unknown unicast frames +}; + +// Bridge domain information +struct gr_l2_bridge { + uint16_t bridge_id; + uint16_t iface_id; // Bridge interface ID (for L3 integration) + char name[GR_L2_BRIDGE_NAME_SIZE]; + struct gr_l2_bridge_config config; + uint32_t mac_count; // Current MAC table size + uint32_t member_count; // Number of member interfaces +}; + +// Bridge domain management +#define GR_L2_BRIDGE_ADD REQUEST_TYPE(GR_L2_MODULE, 0x0001) +struct gr_l2_bridge_add_req { + char name[GR_L2_BRIDGE_NAME_SIZE]; + struct gr_l2_bridge_config config; +}; + +#define GR_L2_BRIDGE_DEL REQUEST_TYPE(GR_L2_MODULE, 0x0002) +struct gr_l2_bridge_del_req { + uint16_t bridge_id; +}; + +#define GR_L2_BRIDGE_LIST REQUEST_TYPE(GR_L2_MODULE, 0x0003) +// struct gr_l2_bridge_list {}; + +#define GR_L2_BRIDGE_GET REQUEST_TYPE(GR_L2_MODULE, 0x0004) +struct gr_l2_bridge_get_req { + uint16_t bridge_id; + char name[GR_L2_BRIDGE_NAME_SIZE]; +}; + +// Bridge member interface management +#define GR_L2_BRIDGE_MEMBER_ADD REQUEST_TYPE(GR_L2_MODULE, 0x0010) +struct gr_l2_bridge_member_add_req { + uint16_t bridge_id; + uint16_t iface_id; +}; + +#define GR_L2_BRIDGE_MEMBER_DEL REQUEST_TYPE(GR_L2_MODULE, 0x0011) +struct gr_l2_bridge_member_del_req { + uint16_t bridge_id; + uint16_t iface_id; +}; + +#define GR_L2_BRIDGE_MEMBER_LIST REQUEST_TYPE(GR_L2_MODULE, 0x0012) +struct gr_l2_bridge_member_list_req { + uint16_t bridge_id; +}; + +// Bridge member information +struct gr_l2_bridge_member { + uint16_t bridge_id; + uint16_t iface_id; + char iface_name[GR_IFACE_NAME_SIZE]; +}; + +// MAC table entry +struct gr_l2_mac_entry { + uint16_t bridge_id; + uint16_t iface_id; + struct rte_ether_addr mac; + gr_l2_mac_type_t type; + uint32_t age; // Age in seconds (0 for static entries) +}; + +// MAC table management +#define GR_L2_MAC_ADD REQUEST_TYPE(GR_L2_MODULE, 0x0020) +struct gr_l2_mac_add_req { + uint16_t bridge_id; + uint16_t iface_id; + struct rte_ether_addr mac; + gr_l2_mac_type_t type; +}; + +#define GR_L2_MAC_DEL REQUEST_TYPE(GR_L2_MODULE, 0x0021) +struct gr_l2_mac_del_req { + uint16_t bridge_id; + struct rte_ether_addr mac; +}; + +#define GR_L2_MAC_LIST REQUEST_TYPE(GR_L2_MODULE, 0x0022) +struct gr_l2_mac_list_req { + uint16_t bridge_id; // 0 = all bridges +}; + +#define GR_L2_MAC_FLUSH REQUEST_TYPE(GR_L2_MODULE, 0x0023) +struct gr_l2_mac_flush_req { + uint16_t bridge_id; // 0 = all bridges + uint16_t iface_id; // 0 = all interfaces + bool dynamic_only; // true = flush only dynamic entries +}; + +// Bridge domain configuration +#define GR_L2_BRIDGE_CONFIG_GET REQUEST_TYPE(GR_L2_MODULE, 0x0030) +struct gr_l2_bridge_config_get_req { + uint16_t bridge_id; +}; + +#define GR_L2_BRIDGE_CONFIG_SET REQUEST_TYPE(GR_L2_MODULE, 0x0031) +struct gr_l2_bridge_config_set_req { + uint16_t bridge_id; + struct gr_l2_bridge_config config; +}; + +// Bridge reconfig attributes +#define GR_BRIDGE_SET_BRIDGE_ID GR_BIT64(32) diff --git a/modules/l2/api/meson.build b/modules/l2/api/meson.build new file mode 100644 index 000000000..e10118c44 --- /dev/null +++ b/modules/l2/api/meson.build @@ -0,0 +1,5 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2025 Christophe Fontaine + +api_headers = files('gr_l2.h') +api_inc += include_directories('.') diff --git a/modules/l2/cli/bridge.c b/modules/l2/cli/bridge.c new file mode 100644 index 000000000..21d31a7da --- /dev/null +++ b/modules/l2/cli/bridge.c @@ -0,0 +1,392 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2025 Christophe Fontaine + +#include "bridge.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +// Bridge domain management commands +static cmd_status_t bridge_add(struct gr_api_client *c, const struct ec_pnode *p) { + struct gr_l2_bridge_add_req req = {0}; + uint32_t aging_time, max_mac_count; + struct gr_l2_bridge *br; + const char *name; + void *resp_ptr; + int ret; + + name = arg_str(p, "NAME"); + if (name == NULL) { + errno = EINVAL; + return CMD_ERROR; + } + + strncpy(req.name, name, sizeof(req.name) - 1); + + // Set default configuration + req.config.aging_time = 300; + req.config.max_mac_count = 1024; + req.config.flood_unknown = true; + + // Parse optional parameters + if (arg_u32(p, "AGING_TIME", &aging_time) == 0) + req.config.aging_time = aging_time; + + if (arg_u32(p, "MAX_MAC_COUNT", &max_mac_count) == 0) + req.config.max_mac_count = max_mac_count; + + if (arg_str(p, "no_flood")) + req.config.flood_unknown = false; + + ret = gr_api_client_send_recv(c, GR_L2_BRIDGE_ADD, sizeof(req), &req, &resp_ptr); + if (ret < 0) + return CMD_ERROR; + br = resp_ptr; + printf("bridge %s created with id %u.\n", br->name, br->bridge_id); + free(br); + return CMD_SUCCESS; +} + +static cmd_status_t bridge_del(struct gr_api_client *c, const struct ec_pnode *p) { + struct gr_l2_bridge_del_req req; + struct gr_l2_bridge *bridge; + const char *bridge_name; + int ret; + + bridge_name = arg_str(p, "BRIDGE"); + if (bridge_name == NULL) { + errno = EINVAL; + return CMD_ERROR; + } + + bridge = bridge_from_name(c, bridge_name); + if (bridge == NULL) { + errno = ENOENT; + return CMD_ERROR; + } + + req.bridge_id = bridge->bridge_id; + free(bridge); + + ret = gr_api_client_send_recv(c, GR_L2_BRIDGE_DEL, sizeof(req), &req, NULL); + + return ret < 0 ? CMD_ERROR : CMD_SUCCESS; +} + +static cmd_status_t bridge_show_cmd(struct gr_api_client *c, const struct ec_pnode *p) { + const struct gr_l2_bridge_member *member; + struct gr_l2_bridge_get_req req; + struct libscols_table *table; + struct gr_l2_bridge *bridge; + const char *bridge_name; + int ret; + + bridge_name = arg_str(p, "BRIDGE"); + if (bridge_name == NULL) { + errno = EINVAL; + return CMD_ERROR; + } + bridge = bridge_from_name(c, bridge_name); + if (bridge == NULL) { + errno = ENOENT; + return CMD_ERROR; + } + + printf("Bridge Domain %u:\n", bridge->bridge_id); + printf(" Name: %s\n", bridge->name); + printf(" Bridge Interface ID: %u\n", bridge->iface_id); + printf(" MAC Aging Time: %u seconds\n", bridge->config.aging_time); + printf(" Max MAC Count: %u\n", bridge->config.max_mac_count); + printf(" Flood Unknown: %s\n", bridge->config.flood_unknown ? "yes" : "no"); + printf(" Current MAC Count: %u\n", bridge->mac_count); + printf(" Member Count: %u\n", bridge->member_count); + + table = scols_new_table(); + if (table == NULL) { + ret = -1; + errno = ENOMEM; + goto cleanup; + } + + scols_table_new_column(table, "BRIDGE_ID", 0, SCOLS_FL_RIGHT); + scols_table_new_column(table, "IFACE_ID", 0, SCOLS_FL_RIGHT); + scols_table_new_column(table, "IFACE_NAME", 0, 0); + + req.bridge_id = bridge->bridge_id; + gr_api_client_stream_foreach (member, ret, c, GR_L2_BRIDGE_MEMBER_LIST, sizeof(req), &req) { + struct libscols_line *line = scols_table_new_line(table, NULL); + + scols_line_sprintf(line, 0, "%u", member->bridge_id); + scols_line_sprintf(line, 1, "%u", member->iface_id); + scols_line_set_data(line, 2, member->iface_name); + } + + scols_print_table(table); + scols_unref_table(table); +cleanup: + free(bridge); + return ret < 0 ? CMD_ERROR : CMD_SUCCESS; +} + +static cmd_status_t bridge_list(struct gr_api_client *c, const struct ec_pnode *) { + const struct gr_l2_bridge *bridge; + struct libscols_table *table; + int ret; + + table = scols_new_table(); + if (table == NULL) + return CMD_ERROR; + + scols_table_new_column(table, "ID", 0, SCOLS_FL_RIGHT); + scols_table_new_column(table, "NAME", 0, 0); + scols_table_new_column(table, "IFACE", 0, SCOLS_FL_RIGHT); + scols_table_new_column(table, "AGING", 0, SCOLS_FL_RIGHT); + scols_table_new_column(table, "MAC_COUNT", 0, SCOLS_FL_RIGHT); + scols_table_new_column(table, "MEMBERS", 0, SCOLS_FL_RIGHT); + scols_table_new_column(table, "FLOOD", 0, 0); + + gr_api_client_stream_foreach (bridge, ret, c, GR_L2_BRIDGE_LIST, 0, NULL) { + struct libscols_line *line = scols_table_new_line(table, NULL); + + scols_line_sprintf(line, 0, "%u", bridge->bridge_id); + scols_line_set_data(line, 1, bridge->name); + scols_line_sprintf(line, 2, "%u", bridge->iface_id); + scols_line_sprintf(line, 3, "%u", bridge->config.aging_time); + scols_line_sprintf(line, 4, "%u", bridge->mac_count); + scols_line_sprintf(line, 5, "%u", bridge->member_count); + scols_line_set_data(line, 6, bridge->config.flood_unknown ? "yes" : "no"); + } + + if (ret < 0) { + scols_unref_table(table); + return CMD_ERROR; + } + + scols_print_table(table); + scols_unref_table(table); + return CMD_SUCCESS; +} + +// Bridge member management +static cmd_status_t bridge_member_add(struct gr_api_client *c, const struct ec_pnode *p) { + struct gr_l2_bridge_member_add_req req; + const char *iface_name, *bridge_name; + struct gr_l2_bridge *bridge = NULL; + struct gr_iface *iface = NULL; + int ret = -1; + + bridge_name = arg_str(p, "BRIDGE"); + if (bridge_name == NULL) { + errno = EINVAL; + goto cleanup; + } + bridge = bridge_from_name(c, bridge_name); + if (bridge == NULL) { + errno = ENOENT; + goto cleanup; + } + + iface_name = arg_str(p, "IFACE"); + if (iface_name == NULL) { + errno = EINVAL; + goto cleanup; + } + + iface = iface_from_name(c, iface_name); + if (iface == NULL) { + errno = ENODEV; + goto cleanup; + } + + req.bridge_id = bridge->bridge_id; + req.iface_id = iface->id; + + ret = gr_api_client_send_recv(c, GR_L2_BRIDGE_MEMBER_ADD, sizeof(req), &req, NULL); + +cleanup: + free(bridge); + free(iface); + + return ret < 0 ? CMD_ERROR : CMD_SUCCESS; +} + +static cmd_status_t bridge_member_del(struct gr_api_client *c, const struct ec_pnode *p) { + struct gr_l2_bridge_member_del_req req; + struct gr_iface *iface = NULL; + const char *iface_name; + int ret; + + iface_name = arg_str(p, "IFACE"); + if (iface_name == NULL) { + errno = EINVAL; + ret = -1; + goto cleanup; + } + + iface = iface_from_name(c, iface_name); + if (iface == NULL) { + errno = ENODEV; + ret = -1; + goto cleanup; + } + + if (iface->mode != GR_IFACE_MODE_L2_BRIDGE) { + errno = EINVAL; + ret = -1; + goto cleanup; + } + + req.bridge_id = iface->domain_id; + req.iface_id = iface->id; + + ret = gr_api_client_send_recv(c, GR_L2_BRIDGE_MEMBER_DEL, sizeof(req), &req, NULL); + +cleanup: + free(iface); + if (ret < 0) + return CMD_ERROR; + + return CMD_SUCCESS; +} + +struct gr_l2_bridge *bridge_from_name(struct gr_api_client *c, const char *name) { + struct gr_l2_bridge_get_req req = {.bridge_id = GR_BRIDGE_ID_UNDEF}; + void *bridge; + + if (name == NULL) + return errno_set_null(EINVAL); + + memccpy(req.name, name, 0, sizeof(req.name)); + + if (gr_api_client_send_recv(c, GR_L2_BRIDGE_GET, sizeof(req), &req, &bridge) < 0) + return NULL; + return bridge; +} + +int complete_bridge_names( + struct gr_api_client *c, + const struct ec_node *node, + struct ec_comp *comp, + const char *arg, + void * /*cb_arg*/ +) { + const struct gr_l2_bridge *bridge; + int result = 0; + int ret; + + gr_api_client_stream_foreach (bridge, ret, c, GR_L2_BRIDGE_LIST, 0, NULL) { + if (ec_str_startswith(bridge->name, arg)) { + if (!ec_comp_add_item(comp, node, EC_COMP_FULL, arg, bridge->name)) + result = -1; + } + } + + return ret < 0 ? -1 : result; +} + +// CLI command registration +static int ctx_init(struct ec_node *root) { + int ret; + + // Bridge domain commands + ret = CLI_COMMAND( + CLI_CONTEXT(root, CTX_ARG("bridge", "Layer 2 bridge configuration.")), + "add NAME [(aging_time AGING_TIME),(max_mac_count MAX_MAC_COUNT),(no_flood)]", + bridge_add, + "Create a new bridge domain.", + with_help("Bridge domain name.", ec_node("any", "NAME")), + with_help( + "MAC aging time in seconds (default: 300).", + ec_node_uint("AGING_TIME", 0, UINT32_MAX, 10) + ), + with_help( + "Maximum MAC entries (default: 1024).", + ec_node_uint("MAX_MAC_COUNT", 1, UINT32_MAX, 10) + ), + with_help( + "Disable flooding of unknown unicast.", ec_node_str("no_flood", "no_flood") + ) + ); + if (ret < 0) + return ret; + + ret = CLI_COMMAND( + CLI_CONTEXT(root, CTX_ARG("bridge", "Layer 2 bridge configuration.")), + "del BRIDGE", + bridge_del, + "Delete a bridge domain.", + with_help("Bridge name.", ec_node_dyn("BRIDGE", complete_bridge_names, NULL)) + ); + if (ret < 0) + return ret; + + ret = CLI_COMMAND( + CLI_CONTEXT(root, CTX_ARG("bridge", "Layer 2 bridge configuration.")), + "show BRIDGE", + bridge_show_cmd, + "Show bridge domain details.", + with_help("Bridge name.", ec_node_dyn("BRIDGE", complete_bridge_names, NULL)) + ); + if (ret < 0) + return ret; + + ret = CLI_COMMAND( + CLI_CONTEXT(root, CTX_ARG("bridge", "Layer 2 bridge configuration.")), + "[list]", + bridge_list, + "List all bridge domains." + ); + if (ret < 0) + return ret; + + // Bridge member commands + ret = CLI_COMMAND( + INTERFACE_SET_CTX(root), + "IFACE mode bridge BRIDGE", + bridge_member_add, + "Add interface to bridge domain.", + with_help("Bridge name.", ec_node_dyn("BRIDGE", complete_bridge_names, NULL)), + with_help( + "Interface name.", + ec_node_dyn("IFACE", complete_iface_names, INT2PTR(GR_IFACE_TYPE_UNDEF)) + ) + ); + if (ret < 0) + return ret; + + ret = CLI_COMMAND( + INTERFACE_SET_CTX(root), + "IFACE mode l3", + bridge_member_del, + "Remove interface from bridge domain.", + with_help( + "Interface name.", + ec_node_dyn("IFACE", complete_iface_names, INT2PTR(GR_IFACE_TYPE_UNDEF)) + ) + ); + if (ret < 0) + return ret; + + return 0; +} + +static struct cli_context ctx = { + .name = "bridge", + .init = ctx_init, +}; + +static void __attribute__((constructor, used)) init(void) { + cli_context_register(&ctx); +} diff --git a/modules/l2/cli/bridge.h b/modules/l2/cli/bridge.h new file mode 100644 index 000000000..159bfd855 --- /dev/null +++ b/modules/l2/cli/bridge.h @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2025 Christophe Fontaine + +#pragma once + +#include + +#include + +struct gr_l2_bridge *bridge_from_name(struct gr_api_client *c, const char *name); +int complete_bridge_names( + struct gr_api_client *c, + const struct ec_node *node, + struct ec_comp *comp, + const char *arg, + void * /*cb_arg*/ +); diff --git a/modules/l2/cli/mac.c b/modules/l2/cli/mac.c new file mode 100644 index 000000000..59763011e --- /dev/null +++ b/modules/l2/cli/mac.c @@ -0,0 +1,291 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2025 Christophe Fontaine + +#include "bridge.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +// MAC table management commands +static cmd_status_t mac_add(struct gr_api_client *c, const struct ec_pnode *p) { + struct gr_l2_mac_add_req req; + struct gr_l2_bridge *bridge; + struct rte_ether_addr mac; + const char *bridge_name; + const char *iface_name; + struct gr_iface *iface; + int ret; + + bridge_name = arg_str(p, "BRIDGE"); + if (bridge_name == NULL) { + errno = EINVAL; + return CMD_ERROR; + } + bridge = bridge_from_name(c, bridge_name); + if (bridge == NULL) { + errno = ENOENT; + return CMD_ERROR; + } + + if (arg_eth_addr(p, "MAC", &mac) < 0) { + printf("Error: Invalid or missing MAC address\n"); + return CMD_ERROR; + } + + iface_name = arg_str(p, "IFACE"); + if (iface_name == NULL) { + printf("Error: Interface name required\n"); + return CMD_ERROR; + } + + iface = iface_from_name(c, iface_name); + if (iface == NULL) + return CMD_ERROR; + + req.bridge_id = bridge->bridge_id; + free(bridge); + req.iface_id = iface->id; + req.mac = mac; + req.type = arg_str(p, "static") ? GR_L2_MAC_STATIC : GR_L2_MAC_DYNAMIC; + + ret = gr_api_client_send_recv(c, GR_L2_MAC_ADD, sizeof(req), &req, NULL); + + free(iface); + + if (ret < 0) + return CMD_ERROR; + + return CMD_SUCCESS; +} + +static cmd_status_t mac_del(struct gr_api_client *c, const struct ec_pnode *p) { + struct gr_l2_mac_del_req req; + struct gr_l2_bridge *bridge; + struct rte_ether_addr mac; + const char *bridge_name; + int ret; + + bridge_name = arg_str(p, "BRIDGE"); + if (bridge_name == NULL) { + errno = EINVAL; + return CMD_ERROR; + } + bridge = bridge_from_name(c, bridge_name); + if (bridge == NULL) { + errno = ENOENT; + return CMD_ERROR; + } + + if (arg_eth_addr(p, "MAC", &mac) < 0) { + printf("Error: Invalid or missing MAC address\n"); + return CMD_ERROR; + } + + req.bridge_id = bridge->bridge_id; + free(bridge); + req.mac = mac; + + ret = gr_api_client_send_recv(c, GR_L2_MAC_DEL, sizeof(req), &req, NULL); + if (ret < 0) + return CMD_ERROR; + + return CMD_SUCCESS; +} + +static cmd_status_t mac_list(struct gr_api_client *c, const struct ec_pnode *p) { + struct gr_l2_mac_list_req req = {0}; + const struct gr_l2_mac_entry *entry; + struct libscols_table *table; + struct gr_l2_bridge *bridge; + const char *bridge_name; + int ret; + + // Optional bridge ID filter + bridge_name = arg_str(p, "BRIDGE"); + if (bridge_name) { + bridge = bridge_from_name(c, bridge_name); + if (bridge == NULL) { + errno = ENOENT; + return CMD_ERROR; + } + req.bridge_id = bridge->bridge_id; + free(bridge); + } + + table = scols_new_table(); + if (table == NULL) + return CMD_ERROR; + + scols_table_new_column(table, "BRIDGE", 0, SCOLS_FL_RIGHT); + scols_table_new_column(table, "MAC_ADDRESS", 0, 0); + scols_table_new_column(table, "IFACE_ID", 0, SCOLS_FL_RIGHT); + scols_table_new_column(table, "IFACE_NAME", 0, 0); + scols_table_new_column(table, "TYPE", 0, 0); + scols_table_new_column(table, "AGE", 0, SCOLS_FL_RIGHT); + + gr_api_client_stream_foreach (entry, ret, c, GR_L2_MAC_LIST, sizeof(req), &req) { + struct libscols_line *line = scols_table_new_line(table, NULL); + char mac_str[18]; // xx:xx:xx:xx:xx:xx format + struct gr_iface *iface; + + snprintf(mac_str, sizeof(mac_str), ETH_F, &entry->mac); + + // Get interface name + iface = iface_from_id(c, entry->iface_id); + + scols_line_sprintf(line, 0, "%u", entry->bridge_id); + scols_line_set_data(line, 1, mac_str); + scols_line_sprintf(line, 2, "%u", entry->iface_id); + + if (iface != NULL) { + scols_line_set_data(line, 3, iface->name); + free(iface); + } else { + scols_line_set_data(line, 3, "?"); + } + + scols_line_set_data( + line, 4, entry->type == GR_L2_MAC_STATIC ? "static" : "dynamic" + ); + + if (entry->type == GR_L2_MAC_STATIC) { + scols_line_set_data(line, 5, "-"); + } else { + scols_line_sprintf(line, 5, "%u", entry->age); + } + } + + if (ret < 0) { + scols_unref_table(table); + return CMD_ERROR; + } + + scols_print_table(table); + scols_unref_table(table); + return CMD_SUCCESS; +} + +static cmd_status_t mac_flush(struct gr_api_client *c, const struct ec_pnode *p) { + struct gr_l2_mac_flush_req req = {0}; + struct gr_iface *iface = NULL; + struct gr_l2_bridge *bridge; + const char *bridge_name; + const char *iface_name; + int ret; + + bridge_name = arg_str(p, "BRIDGE"); + if (bridge_name == NULL) { + errno = EINVAL; + return CMD_ERROR; + } + bridge = bridge_from_name(c, bridge_name); + if (bridge == NULL) { + errno = ENOENT; + return CMD_ERROR; + } + req.bridge_id = bridge->bridge_id; + free(bridge); + req.dynamic_only = arg_str(p, "dynamic_only") != NULL; + + // Optional interface filter + iface_name = arg_str(p, "IFACE"); + if (iface_name != NULL) { + iface = iface_from_name(c, iface_name); + if (iface == NULL) + return CMD_ERROR; + req.iface_id = iface->id; + } + + ret = gr_api_client_send_recv(c, GR_L2_MAC_FLUSH, sizeof(req), &req, NULL); + + if (iface != NULL) + free(iface); + + if (ret < 0) + return CMD_ERROR; + + return CMD_SUCCESS; +} + +// CLI command registration +static int ctx_init(struct ec_node *root) { + int ret; + + // MAC table commands + ret = CLI_COMMAND( + CLI_CONTEXT(root, CTX_ARG("bridge", "Layer 2 bridge configuration.")), + "mac add br BRIDGE mac MAC iface IFACE [static]", + mac_add, + "Add MAC address entry.", + with_help("Bridge name.", ec_node_dyn("BRIDGE", complete_bridge_names, NULL)), + with_help("MAC address (xx:xx:xx:xx:xx:xx).", ec_node_re("MAC", ETH_ADDR_RE)), + with_help( + "Interface name.", + ec_node_dyn("IFACE", complete_iface_names, INT2PTR(GR_IFACE_TYPE_UNDEF)) + ), + with_help("Create static entry.", ec_node_str("static", "static")) + ); + if (ret < 0) + return ret; + + ret = CLI_COMMAND( + CLI_CONTEXT(root, CTX_ARG("bridge", "Layer 2 bridge configuration.")), + "mac del br BRIDGE mac MAC", + mac_del, + "Delete MAC address entry.", + with_help("Bridge name.", ec_node_dyn("BRIDGE", complete_bridge_names, NULL)), + with_help("MAC address (xx:xx:xx:xx:xx:xx).", ec_node_re("MAC", ETH_ADDR_RE)) + ); + if (ret < 0) + return ret; + + ret = CLI_COMMAND( + CLI_CONTEXT(root, CTX_ARG("bridge", "Layer 2 bridge configuration.")), + "mac list [BRIDGE]", + mac_list, + "List MAC address entries.", + with_help("Bridge name.", ec_node_dyn("BRIDGE", complete_bridge_names, NULL)) + ); + if (ret < 0) + return ret; + + ret = CLI_COMMAND( + CLI_CONTEXT(root, CTX_ARG("bridge", "Layer 2 bridge configuration.")), + "mac flush br BRIDGE [iface IFACE] [dynamic_only]", + mac_flush, + "Flush MAC address entries.", + with_help("Bridge name.", ec_node_dyn("BRIDGE", complete_bridge_names, NULL)), + with_help( + "Interface name (optional).", + ec_node_dyn("IFACE", complete_iface_names, INT2PTR(GR_IFACE_TYPE_UNDEF)) + ), + with_help( + "Flush only dynamic entries.", ec_node_str("dynamic_only", "dynamic_only") + ) + ); + if (ret < 0) + return ret; + + return 0; +} + +static struct cli_context ctx = { + .name = "bridge", + .init = ctx_init, +}; + +static void __attribute__((constructor, used)) init(void) { + cli_context_register(&ctx); +} diff --git a/modules/l2/cli/meson.build b/modules/l2/cli/meson.build new file mode 100644 index 000000000..628007ddf --- /dev/null +++ b/modules/l2/cli/meson.build @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2025 Christophe Fontaine + +inc += include_directories('.') +cli_src += files( + 'bridge.c', + 'mac.c', +) diff --git a/modules/l2/control/api_handlers.c b/modules/l2/control/api_handlers.c new file mode 100644 index 000000000..76a77ecb1 --- /dev/null +++ b/modules/l2/control/api_handlers.c @@ -0,0 +1,323 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2025 Christophe Fontaine + +#include +#include +#include +#include +#include +#include + +#include +#include + +static struct api_out l2_bridge_add_api(const void *request, struct api_ctx *ctx __rte_unused) { + const struct gr_l2_bridge_add_req *req = request; + struct bridge_info *bridge; + struct gr_l2_bridge *resp; + + bridge = bridge_add(req->name, &req->config); + + if (bridge == NULL) + return api_out(ENOENT, 0, NULL); + + resp = malloc(sizeof(*resp)); + if (resp == NULL) + return api_out(ENOMEM, 0, NULL); + + bridge_to_api(resp, bridge); + return api_out(0, sizeof(*resp), resp); +} + +static struct api_out l2_bridge_del_api(const void *request, struct api_ctx *ctx __rte_unused) { + const struct gr_l2_bridge_del_req *req = request; + int ret; + + ret = bridge_del(req->bridge_id); + if (ret < 0) + return api_out(errno, 0, NULL); + + return api_out(0, 0, NULL); +} + +static struct api_out l2_bridge_list_api(const void *request __rte_unused, struct api_ctx *ctx) { + struct bridge_info *bridge; + uint16_t bridge_id = 0; + + while ((bridge = bridge_get_next(&bridge_id)) != NULL) { + struct gr_l2_bridge bridge_data; + bridge_to_api(&bridge_data, bridge); + api_send(ctx, sizeof(bridge_data), &bridge_data); + } + + return api_out(0, 0, NULL); +} + +static struct api_out l2_bridge_get_api(const void *request, struct api_ctx *ctx __rte_unused) { + const struct gr_l2_bridge_get_req *req = request; + struct bridge_info *bridge; + struct gr_l2_bridge *resp; + uint16_t bridge_id = 0; + + if (req->bridge_id != GR_BRIDGE_ID_UNDEF) { + bridge = bridge_get(req->bridge_id); + } else { + while ((bridge = bridge_get_next(&bridge_id)) != NULL) { + if (strncmp(bridge->name, req->name, sizeof(req->name)) == 0) + break; + } + } + if (bridge == NULL) + return api_out(ENOENT, 0, NULL); + + resp = malloc(sizeof(*resp)); + if (resp == NULL) + return api_out(ENOMEM, 0, NULL); + + bridge_to_api(resp, bridge); + return api_out(0, sizeof(*resp), resp); +} + +// Bridge member API handlers +static struct api_out +l2_bridge_member_add_api(const void *request, struct api_ctx *ctx __rte_unused) { + const struct gr_l2_bridge_member_add_req *req = request; + int ret; + + ret = bridge_member_add(req->bridge_id, req->iface_id); + if (ret < 0) + return api_out(errno, 0, NULL); + + return api_out(0, 0, NULL); +} + +static struct api_out +l2_bridge_member_del_api(const void *request, struct api_ctx *ctx __rte_unused) { + const struct gr_l2_bridge_member_del_req *req = request; + int ret; + + ret = bridge_member_del(req->bridge_id, req->iface_id); + if (ret < 0) + return api_out(errno, 0, NULL); + + return api_out(0, 0, NULL); +} + +static struct api_out l2_bridge_member_list_api(const void *request, struct api_ctx *ctx) { + const struct gr_l2_bridge_member_list_req *req = request; + struct bridge_info *bridge; + struct iface *iface; + uint16_t *member; + + bridge = bridge_get(req->bridge_id); + if (bridge == NULL) + return api_out(ENOENT, 0, NULL); + + gr_vec_foreach_ref (member, bridge->members) { + iface = iface_from_id(*member); + if (iface == NULL) + continue; + + struct gr_l2_bridge_member member_data = { + .bridge_id = req->bridge_id, + .iface_id = *member, + }; + strncpy(member_data.iface_name, iface->name, sizeof(member_data.iface_name) - 1); + member_data.iface_name[sizeof(member_data.iface_name) - 1] = '\0'; + + api_send(ctx, sizeof(member_data), &member_data); + } + + return api_out(0, 0, NULL); +} + +// Bridge config API handlers +static struct api_out +l2_bridge_config_get_api(const void *request, struct api_ctx *ctx __rte_unused) { + const struct gr_l2_bridge_config_get_req *req = request; + struct gr_l2_bridge_config *resp; + int ret; + + resp = malloc(sizeof(*resp)); + if (resp == NULL) + return api_out(ENOMEM, 0, NULL); + + ret = bridge_config_get(req->bridge_id, resp); + if (ret < 0) { + free(resp); + return api_out(errno, 0, NULL); + } + + return api_out(0, sizeof(*resp), resp); +} + +static struct api_out +l2_bridge_config_set_api(const void *request, struct api_ctx *ctx __rte_unused) { + const struct gr_l2_bridge_config_set_req *req = request; + int ret; + + ret = bridge_config_set(req->bridge_id, &req->config); + if (ret < 0) + return api_out(errno, 0, NULL); + + return api_out(0, 0, NULL); +} + +// MAC table API handlers +static struct api_out l2_mac_add_api(const void *request, struct api_ctx *ctx __rte_unused) { + const struct gr_l2_mac_add_req *req = request; + int ret; + + ret = mac_entry_add(req->bridge_id, req->iface_id, &req->mac, req->type); + if (ret < 0) + return api_out(errno, 0, NULL); + + return api_out(0, 0, NULL); +} + +static struct api_out l2_mac_del_api(const void *request, struct api_ctx *ctx __rte_unused) { + const struct gr_l2_mac_del_req *req = request; + int ret; + + ret = mac_entry_del(req->bridge_id, &req->mac); + if (ret < 0) + return api_out(errno, 0, NULL); + + return api_out(0, 0, NULL); +} + +static struct api_out l2_mac_list_api(const void *request, struct api_ctx *ctx) { + const struct gr_l2_mac_list_req *req = request; + struct bridge_info *bridge; + struct mac_entry *entry; + uint16_t bridge_id; + + if (req->bridge_id != 0) { + // List entries for specific bridge + entry = NULL; + while ((entry = mac_entry_get_next(req->bridge_id, entry)) != NULL) { + struct gr_l2_mac_entry mac_data; + mac_entry_to_api(&mac_data, entry); + api_send(ctx, sizeof(mac_data), &mac_data); + } + } else { + // List entries for all bridges + bridge_id = 0; + while ((bridge = bridge_get_next(&bridge_id)) != NULL) { + entry = NULL; + while ((entry = mac_entry_get_next(bridge_id, entry)) != NULL) { + struct gr_l2_mac_entry mac_data; + mac_entry_to_api(&mac_data, entry); + api_send(ctx, sizeof(mac_data), &mac_data); + } + } + } + + return api_out(0, 0, NULL); +} + +static struct api_out l2_mac_flush_api(const void *request, struct api_ctx *ctx __rte_unused) { + const struct gr_l2_mac_flush_req *req = request; + int ret; + + ret = mac_table_flush(req->bridge_id, req->iface_id, req->dynamic_only); + if (ret < 0) + return api_out(errno, 0, NULL); + + return api_out(0, 0, NULL); +} + +// API handler registration +static struct gr_api_handler bridge_add_handler = { + .name = "l2 bridge add", + .request_type = GR_L2_BRIDGE_ADD, + .callback = l2_bridge_add_api, +}; + +static struct gr_api_handler bridge_del_handler = { + .name = "l2 bridge del", + .request_type = GR_L2_BRIDGE_DEL, + .callback = l2_bridge_del_api, +}; + +static struct gr_api_handler bridge_list_handler = { + .name = "l2 bridge list", + .request_type = GR_L2_BRIDGE_LIST, + .callback = l2_bridge_list_api, +}; + +static struct gr_api_handler bridge_get_handler = { + .name = "l2 bridge get", + .request_type = GR_L2_BRIDGE_GET, + .callback = l2_bridge_get_api, +}; + +static struct gr_api_handler mac_add_handler = { + .name = "l2 mac add", + .request_type = GR_L2_MAC_ADD, + .callback = l2_mac_add_api, +}; + +static struct gr_api_handler mac_del_handler = { + .name = "l2 mac del", + .request_type = GR_L2_MAC_DEL, + .callback = l2_mac_del_api, +}; + +static struct gr_api_handler mac_list_handler = { + .name = "l2 mac list", + .request_type = GR_L2_MAC_LIST, + .callback = l2_mac_list_api, +}; + +static struct gr_api_handler bridge_member_add_handler = { + .name = "l2 bridge member add", + .request_type = GR_L2_BRIDGE_MEMBER_ADD, + .callback = l2_bridge_member_add_api, +}; + +static struct gr_api_handler bridge_member_del_handler = { + .name = "l2 bridge member del", + .request_type = GR_L2_BRIDGE_MEMBER_DEL, + .callback = l2_bridge_member_del_api, +}; + +static struct gr_api_handler bridge_member_list_handler = { + .name = "l2 bridge member list", + .request_type = GR_L2_BRIDGE_MEMBER_LIST, + .callback = l2_bridge_member_list_api, +}; + +static struct gr_api_handler bridge_config_get_handler = { + .name = "l2 bridge config get", + .request_type = GR_L2_BRIDGE_CONFIG_GET, + .callback = l2_bridge_config_get_api, +}; + +static struct gr_api_handler bridge_config_set_handler = { + .name = "l2 bridge config set", + .request_type = GR_L2_BRIDGE_CONFIG_SET, + .callback = l2_bridge_config_set_api, +}; + +static struct gr_api_handler mac_flush_handler = { + .name = "l2 mac flush", + .request_type = GR_L2_MAC_FLUSH, + .callback = l2_mac_flush_api, +}; + +RTE_INIT(l2_api_handlers_init) { + gr_register_api_handler(&bridge_add_handler); + gr_register_api_handler(&bridge_del_handler); + gr_register_api_handler(&bridge_list_handler); + gr_register_api_handler(&bridge_get_handler); + gr_register_api_handler(&bridge_member_add_handler); + gr_register_api_handler(&bridge_member_del_handler); + gr_register_api_handler(&bridge_member_list_handler); + gr_register_api_handler(&bridge_config_get_handler); + gr_register_api_handler(&bridge_config_set_handler); + gr_register_api_handler(&mac_add_handler); + gr_register_api_handler(&mac_del_handler); + gr_register_api_handler(&mac_list_handler); + gr_register_api_handler(&mac_flush_handler); +} diff --git a/modules/l2/control/bridge.c b/modules/l2/control/bridge.c new file mode 100644 index 000000000..7c5591b6e --- /dev/null +++ b/modules/l2/control/bridge.c @@ -0,0 +1,385 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2025 Christophe Fontaine + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +// bridge_info struct is now defined in gr_l2_control.h + +static struct bridge_info bridges[GR_MAX_BRIDGE_DOMAINS]; +static uint16_t next_bridge_id = 1; + +// Hash table for bridge name lookup +static struct rte_hash *bridge_name_hash; + +static struct rte_hash_parameters bridge_name_hash_params = { + .name = "bridge_name_hash", + .entries = GR_MAX_BRIDGE_DOMAINS, + .key_len = GR_L2_BRIDGE_NAME_SIZE, + .hash_func = NULL, + .hash_func_init_val = 0, + .socket_id = SOCKET_ID_ANY, +}; + +static int bridge_init(void) { + bridge_name_hash = rte_hash_create(&bridge_name_hash_params); + if (bridge_name_hash == NULL) { + LOG(ERR, "Failed to create bridge name hash table: %s", rte_strerror(rte_errno)); + return -rte_errno; + } + + // Initialize bridge table + memset(bridges, 0, sizeof(bridges)); + + return 0; +} + +static void bridge_fini(void) { + // Clean up all bridges + for (uint16_t i = 0; i < GR_MAX_BRIDGE_DOMAINS; i++) { + if (bridges[i].active) { + bridge_del(i); + } + } + + if (bridge_name_hash != NULL) { + rte_hash_free(bridge_name_hash); + bridge_name_hash = NULL; + } +} + +static uint16_t alloc_bridge_id(void) { + for (uint16_t i = 0; i < GR_MAX_BRIDGE_DOMAINS; i++) { + uint16_t id = (next_bridge_id + i) % GR_MAX_BRIDGE_DOMAINS; + if (id == 0) // Skip ID 0 + continue; + if (!bridges[id].active) { + next_bridge_id = (id + 1) % GR_MAX_BRIDGE_DOMAINS; + return id; + } + } + return 0; // No free bridge ID +} + +struct bridge_info *bridge_get(uint16_t bridge_id) { + if (bridge_id >= GR_MAX_BRIDGE_DOMAINS || !bridges[bridge_id].active) + return NULL; + return &bridges[bridge_id]; +} + +struct bridge_info *bridge_get_by_name(const char *name) { + void *data; + int ret; + + ret = rte_hash_lookup_data(bridge_name_hash, name, &data); + if (ret < 0) + return NULL; + + uint16_t bridge_id = (uintptr_t)data; + return bridge_get(bridge_id); +} + +struct bridge_info *bridge_add(const char *name, const struct gr_l2_bridge_config *config) { + struct bridge_info *bridge; + uint16_t bridge_id; + int ret; + + if (name == NULL || strlen(name) == 0 || strlen(name) >= GR_L2_BRIDGE_NAME_SIZE) + return errno_set_null(EBADMSG); + + // Check if name already exists + if (bridge_get_by_name(name) != NULL) + return errno_set_null(EEXIST); + + bridge_id = alloc_bridge_id(); + if (bridge_id == 0) + return errno_set_null(EINVAL); + + bridge = &bridges[bridge_id]; + bridge->bridge_id = bridge_id; + memccpy(bridge->name, name, 0, sizeof(bridge->name) - 1); + bridge->name[sizeof(bridge->name) - 1] = '\0'; + + if (config != NULL) + bridge->config = *config; + else { + // Default configuration + bridge->config.aging_time = 300; // 5 minutes + bridge->config.max_mac_count = 1024; + bridge->config.flood_unknown = true; + } + + bridge->members = NULL; + bridge->mac_count = 0; + bridge->bridge_iface = NULL; + bridge->active = true; + + // Add to name hash table + ret = rte_hash_add_key_data(bridge_name_hash, name, (void *)(uintptr_t)bridge_id); + if (ret < 0) { + LOG(ERR, "Failed to add bridge %s to name hash: %s", name, rte_strerror(-ret)); + bridge->active = false; + return errno_set_null(-ret); + } + + LOG(INFO, "Created bridge domain %u (%s)", bridge_id, name); + return bridge; +} + +int bridge_del(uint16_t bridge_id) { + struct bridge_info *bridge; + int ret; + + bridge = bridge_get(bridge_id); + if (bridge == NULL) + return errno_set(ENOENT); + + // Remove all members first + while (gr_vec_len(bridge->members) > 0) { + uint16_t iface_id = bridge->members[0]; + bridge_member_del(bridge_id, iface_id); + } + + // Remove from name hash table + ret = rte_hash_del_key(bridge_name_hash, bridge->name); + if (ret < 0) { + LOG(WARNING, + "Failed to remove bridge %s from name hash: %s", + bridge->name, + rte_strerror(-ret)); + } + + // Clean up bridge interface if it exists + if (bridge->bridge_iface != NULL) { + // TODO: Destroy bridge interface + bridge->bridge_iface = NULL; + } + + // Free member list + gr_vec_free(bridge->members); + + LOG(INFO, "Deleted bridge domain %u (%s)", bridge_id, bridge->name); + + // Clear bridge info + memset(bridge, 0, sizeof(*bridge)); + + return 0; +} + +int bridge_member_add(uint16_t bridge_id, uint16_t iface_id) { + struct bridge_info *bridge; + struct iface *iface; + uint16_t *member; + + bridge = bridge_get(bridge_id); + if (bridge == NULL) + return errno_set(ENOENT); + + iface = iface_from_id(iface_id); + if (iface == NULL) + return errno_set(ENODEV); + + // Check if interface is already a member of this bridge + gr_vec_foreach_ref (member, bridge->members) { + if (*member == iface_id) + return errno_set(EEXIST); + } + + // Check if interface is already a member of another bridge + for (uint16_t i = 1; i < GR_MAX_BRIDGE_DOMAINS; i++) { + if (i == bridge_id || !bridges[i].active) + continue; + gr_vec_foreach_ref (member, bridges[i].members) { + if (*member == iface_id) + return errno_set(EBUSY); + } + } + + // Add interface to bridge member list + gr_vec_add(bridge->members, iface_id); + + // Set interface to L2 bridge mode and assign bridge domain + iface->mode = GR_IFACE_MODE_L2_BRIDGE; + iface->domain_id = bridge_id; + iface_set_promisc(iface->id, true); + LOG(INFO, "Added interface %u to bridge %u (%s)", iface_id, bridge_id, bridge->name); + + return 0; +} + +int bridge_member_del(uint16_t bridge_id, uint16_t iface_id) { + struct bridge_info *bridge; + struct iface *iface; + uint16_t *member; + size_t index = 0; + + bridge = bridge_get(bridge_id); + if (bridge == NULL) + return errno_set(ENOENT); + + iface = iface_from_id(iface_id); + + // Find and remove interface from member list + gr_vec_foreach_ref (member, bridge->members) { + if (*member == iface_id) { + gr_vec_del(bridge->members, index); + + LOG(INFO, + "Removed interface %u from bridge %u (%s)", + iface_id, + bridge_id, + bridge->name); + + // Reset interface mode to L3 + if (iface) { + iface_set_promisc(iface->id, false); + iface->mode = GR_IFACE_MODE_L3; + iface->domain_id = 0; + return 0; + } else { + return errno_set(ENODEV); + } + } + index++; + } + + return errno_set(ENOENT); +} + +int bridge_config_set(uint16_t bridge_id, const struct gr_l2_bridge_config *config) { + struct bridge_info *bridge; + + if (config == NULL) + return errno_set(EINVAL); + + bridge = bridge_get(bridge_id); + if (bridge == NULL) + return errno_set(ENOENT); + + bridge->config = *config; + + LOG(INFO, "Updated configuration for bridge %u (%s)", bridge_id, bridge->name); + return 0; +} + +int bridge_config_get(uint16_t bridge_id, struct gr_l2_bridge_config *config) { + struct bridge_info *bridge; + + if (config == NULL) + return errno_set(EINVAL); + + bridge = bridge_get(bridge_id); + if (bridge == NULL) + return errno_set(ENOENT); + + *config = bridge->config; + return 0; +} + +void bridge_to_api(struct gr_l2_bridge *api_bridge, const struct bridge_info *bridge) { + api_bridge->bridge_id = bridge->bridge_id; + api_bridge->iface_id = bridge->bridge_iface ? bridge->bridge_iface->id : 0; + memccpy(api_bridge->name, bridge->name, 0, sizeof(api_bridge->name) - 1); + api_bridge->config = bridge->config; + api_bridge->mac_count = bridge->mac_count; + api_bridge->member_count = gr_vec_len(bridge->members); +} + +// Iterator for bridge list +struct bridge_info *bridge_get_next(uint16_t *bridge_id) { + if (*bridge_id == 0) + *bridge_id = 1; + else + (*bridge_id)++; + + while (*bridge_id < GR_MAX_BRIDGE_DOMAINS) { + if (bridges[*bridge_id].active) + return &bridges[*bridge_id]; + (*bridge_id)++; + } + + return NULL; +} + +// Clean up MAC entries for a removed interface +void bridge_cleanup_interface_macs(uint16_t iface_id) { + int total_flushed = 0; + + // Remove all MAC entries associated with this interface from all bridges + for (uint16_t bridge_id = 1; bridge_id < GR_MAX_BRIDGE_DOMAINS; bridge_id++) { + if (!bridges[bridge_id].active) + continue; + + int flushed = mac_table_flush(bridge_id, iface_id, false); + if (flushed > 0) { + LOG(INFO, + "Removed %d MAC entries for interface %u from bridge %u (%s)", + flushed, + iface_id, + bridge_id, + bridges[bridge_id].name); + total_flushed += flushed; + } + } + + if (total_flushed > 0) { + LOG(INFO, + "Total MAC entries cleaned up for interface %u: %d", + iface_id, + total_flushed); + } +} + +static void bridge_module_init(struct event_base *base __rte_unused) { + if (bridge_init() < 0) { + ABORT("bridge_init failed: %s", strerror(errno)); + } +} + +static void bridge_module_fini(struct event_base *base __rte_unused) { + bridge_fini(); + mac_table_fini(); +} + +static struct gr_module bridge_module = { + .name = "l2 bridge", + .depends_on = "iface", + .init = bridge_module_init, + .fini = bridge_module_fini, +}; + +static void bridge_iface_event_handler(uint32_t event, const void *obj) { + const struct iface *iface = obj; + + if (event != GR_EVENT_IFACE_PRE_REMOVE) + return; + + if (iface->mode == GR_IFACE_MODE_L2_BRIDGE) + bridge_member_del(iface->domain_id, iface->id); +} + +static struct gr_event_subscription bridge_event_sub = { + .callback = bridge_iface_event_handler, + .ev_count = 1, + .ev_types = {GR_EVENT_IFACE_PRE_REMOVE}, +}; + +RTE_INIT(bridge_constructor) { + gr_register_module(&bridge_module); + gr_event_subscribe(&bridge_event_sub); +} diff --git a/modules/l2/control/gr_l2_control.h b/modules/l2/control/gr_l2_control.h new file mode 100644 index 000000000..85175539f --- /dev/null +++ b/modules/l2/control/gr_l2_control.h @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2025 Christophe Fontaine + +#pragma once + +#include +#include + +#include + +// Forward declarations and structures +struct mac_entry; + +struct bridge_info { + uint16_t bridge_id; + struct iface *bridge_iface; + char name[GR_L2_BRIDGE_NAME_SIZE]; + struct gr_l2_bridge_config config; + gr_vec uint16_t *members; // Member interface IDs + uint32_t mac_count; + bool active; +}; + +// Bridge domain management +struct bridge_info *bridge_get(uint16_t bridge_id); +struct bridge_info *bridge_get_by_name(const char *name); +struct bridge_info *bridge_add(const char *name, const struct gr_l2_bridge_config *config); +int bridge_del(uint16_t bridge_id); + +int bridge_member_add(uint16_t bridge_id, uint16_t iface_id); +int bridge_member_del(uint16_t bridge_id, uint16_t iface_id); + +int bridge_config_set(uint16_t bridge_id, const struct gr_l2_bridge_config *config); +int bridge_config_get(uint16_t bridge_id, struct gr_l2_bridge_config *config); + +void bridge_to_api(struct gr_l2_bridge *api_bridge, const struct bridge_info *bridge); +struct bridge_info *bridge_get_next(uint16_t *bridge_id); + +// Interface cleanup +void bridge_cleanup_interface_macs(uint16_t iface_id); + +// MAC table management +void mac_table_fini(void); + +int mac_entry_add( + uint16_t bridge_id, + uint16_t iface_id, + const struct rte_ether_addr *mac, + gr_l2_mac_type_t type +); +int mac_entry_del(uint16_t bridge_id, const struct rte_ether_addr *mac); +int mac_entry_lookup(uint16_t bridge_id, const struct rte_ether_addr *mac, uint16_t *iface_id); +int mac_table_flush(uint16_t bridge_id, uint16_t iface_id, bool dynamic_only); + +void mac_entry_to_api(struct gr_l2_mac_entry *api_entry, const struct mac_entry *entry); +struct mac_entry *mac_entry_get_next(uint16_t bridge_id, struct mac_entry *current); + +// MAC aging +void mac_aging_timer_start(void); +void mac_aging_timer_stop(void); + +// Bridge interface management (for L3 integration) +int bridge_iface_create(uint16_t bridge_id); +int bridge_iface_destroy(uint16_t bridge_id); +struct iface *bridge_get_iface(uint16_t bridge_id); diff --git a/modules/l2/control/mac_table.c b/modules/l2/control/mac_table.c new file mode 100644 index 000000000..c9c0428f8 --- /dev/null +++ b/modules/l2/control/mac_table.c @@ -0,0 +1,540 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2025 Christophe Fontaine + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +// Full MAC table implementation with aging timers + +// MAC table entry structure +struct mac_entry { + uint16_t bridge_id; + uint16_t iface_id; + struct rte_ether_addr mac; + gr_l2_mac_type_t type; + time_t created; + time_t last_seen; + struct mac_entry *next; // For hash collision chaining +}; + +// MAC table key structure +struct mac_key { + uint16_t bridge_id; + struct rte_ether_addr mac; +}; + +// Per-bridge MAC table +struct bridge_mac_table { + struct rte_hash *hash; + struct mac_entry *entries; // Linked list of all entries + uint32_t count; + uint32_t max_count; +}; + +static struct bridge_mac_table mac_tables[GR_MAX_BRIDGE_DOMAINS]; +static struct event *aging_event; +static struct event_base *ev_base; + +// Simple hash function for MAC addresses +static inline uint32_t +mac_hash_func(const void *key, uint32_t key_len __rte_unused, uint32_t init_val) { + const struct mac_key *mac_key = key; + + // Simple hash combining bridge_id and MAC address + uint32_t hash = init_val; + hash ^= mac_key->bridge_id; + + for (int i = 0; i < RTE_ETHER_ADDR_LEN; i++) { + hash = hash * 31 + mac_key->mac.addr_bytes[i]; + } + + return hash; +} + +static int create_bridge_mac_table(uint16_t bridge_id, uint32_t max_entries) { + struct bridge_mac_table *table = &mac_tables[bridge_id]; + char name[64]; + + if (table->hash != NULL) + return 0; // Already exists + + snprintf(name, sizeof(name), "mac_table_%u", bridge_id); + + struct rte_hash_parameters params = { + .name = name, + .entries = max_entries > 0 ? max_entries : 1024, + .key_len = sizeof(struct mac_key), + .hash_func = mac_hash_func, + .hash_func_init_val = 0, + .socket_id = SOCKET_ID_ANY, + .extra_flag = RTE_HASH_EXTRA_FLAGS_RW_CONCURRENCY_LF + | RTE_HASH_EXTRA_FLAGS_TRANS_MEM_SUPPORT, + }; + + table->hash = rte_hash_create(¶ms); + if (table->hash == NULL) { + LOG(ERR, + "Failed to create MAC table for bridge %u: %s", + bridge_id, + rte_strerror(rte_errno)); + return -rte_errno; + } + + table->entries = NULL; + table->count = 0; + table->max_count = params.entries; + + LOG(DEBUG, "Created MAC table for bridge %u with %u entries", bridge_id, table->max_count); + + return 0; +} + +static void destroy_bridge_mac_table(uint16_t bridge_id) { + struct bridge_mac_table *table = &mac_tables[bridge_id]; + struct mac_entry *entry, *next; + + if (table->hash == NULL) + return; + + // Free all entries + entry = table->entries; + while (entry != NULL) { + next = entry->next; + rte_free(entry); + entry = next; + } + + rte_hash_free(table->hash); + memset(table, 0, sizeof(*table)); + + LOG(DEBUG, "Destroyed MAC table for bridge %u", bridge_id); +} + +static void mac_table_module_init(struct event_base *base) { + memset(mac_tables, 0, sizeof(mac_tables)); + aging_event = NULL; + ev_base = base; +} + +static void mac_table_module_fini(struct event_base *base __rte_unused) { + mac_table_fini(); +} + +void mac_table_fini(void) { + // Stop aging timer + if (aging_event != NULL) { + event_del(aging_event); + event_free(aging_event); + aging_event = NULL; + } + + // Destroy all MAC tables + for (uint16_t i = 0; i < GR_MAX_BRIDGE_DOMAINS; i++) { + destroy_bridge_mac_table(i); + } +} + +static struct mac_entry *alloc_mac_entry(void) { + return rte_zmalloc("mac_entry", sizeof(struct mac_entry), 0); +} + +static void free_mac_entry(struct mac_entry *entry) { + rte_free(entry); +} + +int mac_entry_add( + uint16_t bridge_id, + uint16_t iface_id, + const struct rte_ether_addr *mac, + gr_l2_mac_type_t type +) { + struct bridge_mac_table *table; + struct mac_entry *entry; + struct mac_key key; + time_t now; + int ret; + + if (bridge_id >= GR_MAX_BRIDGE_DOMAINS || mac == NULL) + return errno_set(EINVAL); + + table = &mac_tables[bridge_id]; + if (table->hash == NULL) { + ret = create_bridge_mac_table(bridge_id, 0); + if (ret < 0) + return ret; + } + + // Check if table is full (only for dynamic entries) + if (type == GR_L2_MAC_DYNAMIC && table->max_count > 0 && table->count >= table->max_count) { + return errno_set(ENOSPC); + } + + key.bridge_id = bridge_id; + key.mac = *mac; + now = time(NULL); + + // Check if entry already exists + void *entry_data; + ret = rte_hash_lookup_data(table->hash, &key, &entry_data); + entry = (struct mac_entry *)entry_data; + if (ret >= 0) { + // Update existing entry + entry->iface_id = iface_id; + entry->type = type; + entry->last_seen = now; + return 0; + } + + // Create new entry + entry = alloc_mac_entry(); + if (entry == NULL) + return errno_set(ENOMEM); + + entry->bridge_id = bridge_id; + entry->iface_id = iface_id; + entry->mac = *mac; + entry->type = type; + entry->created = now; + entry->last_seen = now; + + // Add to hash table + ret = rte_hash_add_key_data(table->hash, &key, entry); + if (ret < 0) { + free_mac_entry(entry); + return errno_set(-ret); + } + + // Add to linked list + entry->next = table->entries; + table->entries = entry; + table->count++; + + LOG(DEBUG, + "Added MAC entry " RTE_ETHER_ADDR_PRT_FMT " -> iface %u in bridge %u (%s)", + RTE_ETHER_ADDR_BYTES(&entry->mac), + iface_id, + bridge_id, + type == GR_L2_MAC_STATIC ? "static" : "dynamic"); + + return 0; +} + +int mac_entry_del(uint16_t bridge_id, const struct rte_ether_addr *mac) { + struct bridge_mac_table *table; + struct mac_entry *entry, *prev; + struct mac_key key; + int ret; + + if (bridge_id >= GR_MAX_BRIDGE_DOMAINS || mac == NULL) + return errno_set(EINVAL); + + table = &mac_tables[bridge_id]; + if (table->hash == NULL) + return errno_set(ENOENT); + + key.bridge_id = bridge_id; + key.mac = *mac; + + // Find entry in hash table + void *entry_data; + ret = rte_hash_lookup_data(table->hash, &key, &entry_data); + entry = (struct mac_entry *)entry_data; + if (ret < 0) + return errno_set(ENOENT); + + // Remove from hash table + ret = rte_hash_del_key(table->hash, &key); + if (ret < 0) + return errno_set(-ret); + + // Remove from linked list + if (table->entries == entry) { + table->entries = entry->next; + } else { + prev = table->entries; + while (prev != NULL && prev->next != entry) + prev = prev->next; + if (prev != NULL) + prev->next = entry->next; + } + + table->count--; + + LOG(DEBUG, + "Deleted MAC entry " RTE_ETHER_ADDR_PRT_FMT " from bridge %u", + RTE_ETHER_ADDR_BYTES(&entry->mac), + bridge_id); + + free_mac_entry(entry); + return 0; +} + +int mac_entry_lookup(uint16_t bridge_id, const struct rte_ether_addr *mac, uint16_t *iface_id) { + struct bridge_mac_table *table; + struct mac_entry *entry; + struct mac_key key; + time_t now; + int ret; + + if (bridge_id >= GR_MAX_BRIDGE_DOMAINS || mac == NULL || iface_id == NULL) + return errno_set(EINVAL); + + table = &mac_tables[bridge_id]; + if (table->hash == NULL) + return errno_set(ENOENT); + + key.bridge_id = bridge_id; + key.mac = *mac; + + void *entry_data; + ret = rte_hash_lookup_data(table->hash, &key, &entry_data); + entry = (struct mac_entry *)entry_data; + if (ret < 0) + return errno_set(ENOENT); + + *iface_id = entry->iface_id; + + // Update last seen time for dynamic entries + if (entry->type == GR_L2_MAC_DYNAMIC) { + now = time(NULL); + entry->last_seen = now; + } + + return 0; +} + +int mac_table_flush(uint16_t bridge_id, uint16_t iface_id, bool dynamic_only) { + struct mac_entry *entry, *next, *prev; + struct bridge_mac_table *table; + struct mac_key key; + int count = 0; + + if (bridge_id >= GR_MAX_BRIDGE_DOMAINS) + return errno_set(EINVAL); + + table = &mac_tables[bridge_id]; + if (table->hash == NULL) + return 0; + + prev = NULL; + entry = table->entries; + + while (entry != NULL) { + next = entry->next; + + bool should_delete = false; + + // Check if entry matches criteria + if (iface_id == 0 || entry->iface_id == iface_id) { + if (!dynamic_only || entry->type == GR_L2_MAC_DYNAMIC) { + should_delete = true; + } + } + + if (should_delete) { + // Remove from hash table + key.bridge_id = entry->bridge_id; + key.mac = entry->mac; + rte_hash_del_key(table->hash, &key); + + // Remove from linked list + if (prev == NULL) { + table->entries = next; + } else { + prev->next = next; + } + + table->count--; + count++; + + LOG(DEBUG, + "Flushed MAC entry " RTE_ETHER_ADDR_PRT_FMT " from bridge %u", + RTE_ETHER_ADDR_BYTES(&entry->mac), + bridge_id); + + free_mac_entry(entry); + } else { + prev = entry; + } + + entry = next; + } + + LOG(INFO, "Flushed %d MAC entries from bridge %u", count, bridge_id); + return count; +} + +void mac_entry_to_api(struct gr_l2_mac_entry *api_entry, const struct mac_entry *entry) { + time_t now = time(NULL); + + api_entry->bridge_id = entry->bridge_id; + api_entry->iface_id = entry->iface_id; + api_entry->mac = entry->mac; + api_entry->type = entry->type; + + if (entry->type == GR_L2_MAC_STATIC) { + api_entry->age = 0; + } else { + api_entry->age = (uint32_t)(now - entry->last_seen); + } +} + +struct mac_entry *mac_entry_get_next(uint16_t bridge_id, struct mac_entry *current) { + struct bridge_mac_table *table; + + if (bridge_id >= GR_MAX_BRIDGE_DOMAINS) + return NULL; + + table = &mac_tables[bridge_id]; + if (table->hash == NULL) + return NULL; + + if (current == NULL) + return table->entries; + + return current->next; +} + +// MAC aging timer callback +static void mac_aging_callback( + evutil_socket_t fd __rte_unused, + short events __rte_unused, + void *arg __rte_unused +) { + struct mac_entry *entry, *next, *prev; + struct bridge_mac_table *table; + struct bridge_info *bridge; + time_t now, aging_time; + uint16_t bridge_id = 0; + struct mac_key key; + int aged_count = 0; + + now = time(NULL); + + // Age entries in all bridges + while ((bridge = bridge_get_next(&bridge_id)) != NULL) { + table = &mac_tables[bridge_id]; + if (table->hash == NULL) + continue; + + aging_time = bridge->config.aging_time; + if (aging_time == 0) + continue; // No aging for this bridge + + prev = NULL; + entry = table->entries; + + while (entry != NULL) { + next = entry->next; + + // Only age dynamic entries + if (entry->type == GR_L2_MAC_DYNAMIC + && (now - entry->last_seen) > aging_time) { + // Remove from hash table + key.bridge_id = entry->bridge_id; + key.mac = entry->mac; + rte_hash_del_key(table->hash, &key); + + // Remove from linked list + if (prev == NULL) { + table->entries = next; + } else { + prev->next = next; + } + + table->count--; + aged_count++; + + LOG(DEBUG, + "Aged out MAC entry " RTE_ETHER_ADDR_PRT_FMT " from bridge %u", + RTE_ETHER_ADDR_BYTES(&entry->mac), + bridge_id); + + free_mac_entry(entry); + } else { + prev = entry; + } + + entry = next; + } + } + + if (aged_count > 0) { + LOG(DEBUG, "MAC aging: removed %d entries", aged_count); + } +} + +void mac_aging_timer_start(void) { + struct timeval tv = {30, 0}; // 30 seconds + + if (aging_event != NULL) + return; // Already started + + aging_event = event_new(ev_base, -1, EV_PERSIST, mac_aging_callback, NULL); + if (aging_event == NULL) { + LOG(ERR, "Failed to create MAC aging event"); + return; + } + + if (event_add(aging_event, &tv) < 0) { + LOG(ERR, "Failed to add MAC aging event"); + event_free(aging_event); + aging_event = NULL; + return; + } + + LOG(INFO, "Started MAC aging timer"); +} + +void mac_aging_timer_stop(void) { + if (aging_event != NULL) { + event_del(aging_event); + event_free(aging_event); + aging_event = NULL; + LOG(INFO, "Stopped MAC aging timer"); + } +} + +// Interface event handler to clean up MAC entries when interface is removed +static void mac_table_iface_event_handler(uint32_t event, const void *obj) { + const struct iface *iface = obj; + + if (event != GR_EVENT_IFACE_PRE_REMOVE) + return; + + // Clean up MAC entries for this interface + bridge_cleanup_interface_macs(iface->id); +} + +static struct gr_event_subscription mac_table_event_sub = { + .callback = mac_table_iface_event_handler, + .ev_count = 1, + .ev_types = {GR_EVENT_IFACE_PRE_REMOVE}, +}; + +static struct gr_module mac_table_module = { + .name = "l2 mac table", + .init = mac_table_module_init, + .fini = mac_table_module_fini, +}; + +RTE_INIT(mac_table_constructor) { + gr_register_module(&mac_table_module); + gr_event_subscribe(&mac_table_event_sub); +} diff --git a/modules/l2/control/meson.build b/modules/l2/control/meson.build new file mode 100644 index 000000000..6a3e0476b --- /dev/null +++ b/modules/l2/control/meson.build @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2025 Christophe Fontaine + +src += files( + 'bridge.c', + 'mac_table.c', + 'api_handlers.c', +) +inc += include_directories('.') diff --git a/modules/l2/datapath/l2_bridge.c b/modules/l2/datapath/l2_bridge.c new file mode 100644 index 000000000..36ddd76a7 --- /dev/null +++ b/modules/l2/datapath/l2_bridge.c @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2025 Christophe Fontaine + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +enum edges { + PORT_OUTPUT = 0, // Send to specific port + BOND_OUTPUT, + FLOOD, // Flood to all bridge members + DROP, // Drop packet + EDGE_COUNT +}; + +struct l2_bridge_trace { + uint16_t bridge_id; + uint16_t src_iface; + uint16_t dst_iface; + struct rte_ether_addr src_mac; + struct rte_ether_addr dst_mac; +}; + +static uint16_t +l2_bridge_process(struct rte_graph *graph, struct rte_node *node, void **objs, uint16_t nb_objs) { + uint16_t bridge_id, dst_iface_id; + struct bridge_info *bridge; + struct rte_ether_hdr *eth; + struct rte_mbuf *mbuf; + struct iface *iface; + rte_edge_t edge; + int ret; + + for (uint16_t i = 0; i < nb_objs; i++) { + mbuf = objs[i]; + iface = (struct iface *)mbuf_data(mbuf)->iface; + bridge_id = iface->domain_id; + dst_iface_id = 0; + eth = NULL; + + // Get bridge information + bridge = bridge_get(bridge_id); + if (bridge == NULL) { + edge = DROP; + goto next; + } + + eth = rte_pktmbuf_mtod(mbuf, struct rte_ether_hdr *); + + // Learn source MAC address (for dynamic learning) + if (!rte_is_zero_ether_addr(ð->src_addr)) { + ret = mac_entry_lookup(bridge_id, ð->src_addr, &dst_iface_id); + if (ret < 0) { + // MAC not found, learn it + ret = mac_entry_add( + bridge_id, iface->id, ð->src_addr, GR_L2_MAC_DYNAMIC + ); + } else if (dst_iface_id != iface->id) { + // MAC moved to different interface, update + ret = mac_entry_add( + bridge_id, iface->id, ð->src_addr, GR_L2_MAC_DYNAMIC + ); + } + } + + // Handle special destination addresses + if (rte_is_broadcast_ether_addr(ð->dst_addr) + || rte_is_multicast_ether_addr(ð->dst_addr)) { + // Broadcast/multicast - flood to all bridge members except source + edge = FLOOD; + goto next; + } + + // Look up destination MAC + ret = mac_entry_lookup(bridge_id, ð->dst_addr, &dst_iface_id); + if (ret == 0) { + // Don't forward back to source interface + if (dst_iface_id == iface->id) { + edge = DROP; + goto next; + } + + // Get destination interface + struct iface *dst_iface = iface_from_id(dst_iface_id); + if (dst_iface == NULL) { + edge = DROP; + goto next; + } + + // Set up for port output + mbuf_data(mbuf)->iface = dst_iface; + switch (dst_iface->type) { + case GR_IFACE_TYPE_PORT: + edge = PORT_OUTPUT; + mbuf->port = iface_info_port(dst_iface)->port_id; + break; + case GR_IFACE_TYPE_BOND: + edge = BOND_OUTPUT; + break; + default: + edge = DROP; + break; + } + } else { + if (bridge->config.flood_unknown) { + // Flood unknown unicast + edge = FLOOD; + } else { + // Drop unknown unicast + edge = DROP; + } + } + +next: + // Add trace information if tracing is enabled + if (gr_mbuf_is_traced(mbuf)) { + struct l2_bridge_trace *trace = gr_mbuf_trace_add( + mbuf, node, sizeof(*trace) + ); + trace->bridge_id = bridge_id; + trace->src_iface = iface->id; + trace->dst_iface = dst_iface_id; + if (eth) { + trace->src_mac = eth->src_addr; + trace->dst_mac = eth->dst_addr; + } + } + + rte_node_enqueue_x1(graph, node, edge, mbuf); + } + + return nb_objs; +} + +static int l2_bridge_trace_format(char *buf, size_t len, const void *data, size_t data_len) { + const struct l2_bridge_trace *t = data; + int n = 0; + + if (data_len < sizeof(*t)) + return -1; + + n = snprintf( + buf, + len, + "bridge=%u src_iface=%u dst_iface=%u src=" ETH_F " dst=" ETH_F, + t->bridge_id, + t->src_iface, + t->dst_iface, + &t->src_mac, + &t->dst_mac + ); + + return n; +} + +static struct rte_node_register l2_bridge_node = { + .name = "l2_bridge", + .process = l2_bridge_process, + .nb_edges = EDGE_COUNT, + .next_nodes = { + [PORT_OUTPUT] = "port_output", + [BOND_OUTPUT] = "bond_output", + [FLOOD] = "l2_flood", + [DROP] = "l2_bridge_drop", + }, +}; + +static void l2_bridge_register(void) { + register_interface_mode(GR_IFACE_MODE_L2_BRIDGE, "l2_bridge"); +} + +static struct gr_node_info info = { + .node = &l2_bridge_node, + .register_callback = l2_bridge_register, + .trace_format = l2_bridge_trace_format, +}; + +GR_NODE_REGISTER(info); +GR_DROP_REGISTER(l2_bridge_drop); diff --git a/modules/l2/datapath/l2_flood.c b/modules/l2/datapath/l2_flood.c new file mode 100644 index 000000000..2a65021f0 --- /dev/null +++ b/modules/l2/datapath/l2_flood.c @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2025 Christophe Fontaine + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +enum edges { + PORT_OUTPUT = 0, + BOND_OUTPUT, + DROP, + EDGE_COUNT +}; + +struct l2_flood_trace { + uint16_t bridge_id; + uint16_t src_iface; + uint16_t dst_iface; + uint16_t flood_count; + struct rte_ether_addr dst_mac; +}; + +static __thread struct rte_mbuf *clones[MAX_IFACES]; +static __thread rte_edge_t edges[MAX_IFACES]; + +static uint16_t +l2_flood_process(struct rte_graph *graph, struct rte_node *node, void **objs, uint16_t nb_objs) { + struct rte_mbuf *mbuf, *clone; + const struct iface *src_iface; + uint16_t bridge_id, *member; + struct bridge_info *bridge; + struct iface *dst_iface; + uint16_t flood_count; + uint16_t sent = 0; + + for (uint16_t i = 0; i < nb_objs; i++) { + mbuf = objs[i]; + src_iface = mbuf_data(mbuf)->iface; + bridge_id = src_iface->domain_id; + flood_count = 0; + + // Get bridge information + bridge = bridge_get(bridge_id); + if (bridge == NULL) + goto next; + + // Flood to all bridge members except source interface + gr_vec_foreach_ref (member, bridge->members) { + if (*member == src_iface->id) + continue; // Don't flood back to source + + dst_iface = iface_from_id(*member); + if (dst_iface == NULL) // TODO: add xstat + continue; + + if (!(dst_iface->flags & GR_IFACE_F_UP)) + continue; // Skip down interfaces + + // Copy packet for each destination (except the first one) + if (flood_count == 0) { + clone = mbuf; + } else { + clone = gr_mbuf_copy(mbuf, UINT32_MAX, sizeof(struct mbuf_data)); + if (clone == NULL) { + // TODO: add xstat + continue; + } + } + clones[flood_count] = clone; + + // Set up for port output + mbuf_data(clone)->iface = dst_iface; + switch (dst_iface->type) { + case GR_IFACE_TYPE_PORT: + clone->port = iface_info_port(dst_iface)->port_id; + edges[flood_count] = PORT_OUTPUT; + break; + case GR_IFACE_TYPE_BOND: + edges[flood_count] = BOND_OUTPUT; + break; + default: + edges[flood_count] = DROP; + break; + } + flood_count++; + } + +next: + // If no flooding occurred, drop the original packet + if (flood_count == 0) { + clones[0] = mbuf; + edges[0] = DROP; + flood_count = 1; + } + + if (gr_mbuf_is_traced(mbuf)) { + for (uint16_t i = 0; i < flood_count; i++) { + struct l2_flood_trace *trace = gr_mbuf_trace_add( + clones[i], node, sizeof(*trace) + ); + trace->bridge_id = bridge_id; + trace->src_iface = src_iface->id; + trace->dst_iface = mbuf_data(clones[i])->iface->id; + trace->flood_count = i; + + struct rte_ether_hdr *eth = rte_pktmbuf_mtod( + clones[i], struct rte_ether_hdr * + ); + trace->dst_mac = eth->dst_addr; + } + } + rte_node_enqueue_next(graph, node, edges, (void *)clones, flood_count); + sent += flood_count; + } + + return sent; +} + +static int l2_flood_trace_format(char *buf, size_t len, const void *data, size_t data_len) { + const struct l2_flood_trace *t = data; + int n = 0; + + if (data_len < sizeof(*t)) + return -1; + + n = snprintf( + buf, + len, + "bridge=%u src_iface=%u dst_iface=%u flood_count=%u dst=" ETH_F, + t->bridge_id, + t->src_iface, + t->dst_iface, + t->flood_count, + &t->dst_mac + ); + + return n; +} + +static struct rte_node_register l2_flood_node = { + .name = "l2_flood", + .process = l2_flood_process, + .nb_edges = EDGE_COUNT, + .next_nodes = { + [PORT_OUTPUT] = "port_output", + [BOND_OUTPUT] = "bond_output", + [DROP] = "l2_flood_drop", + }, +}; + +static struct gr_node_info info = { + .node = &l2_flood_node, + .trace_format = l2_flood_trace_format, +}; + +GR_NODE_REGISTER(info); +GR_DROP_REGISTER(l2_flood_drop); diff --git a/modules/l2/datapath/meson.build b/modules/l2/datapath/meson.build new file mode 100644 index 000000000..2831c9616 --- /dev/null +++ b/modules/l2/datapath/meson.build @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2025 Christophe Fontaine + +src += files( + 'l2_bridge.c', + 'l2_flood.c', +) +inc += include_directories('.') diff --git a/modules/l2/meson.build b/modules/l2/meson.build new file mode 100644 index 000000000..21a7dc07a --- /dev/null +++ b/modules/l2/meson.build @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2025 Christophe Fontaine + +subdir('api') +subdir('cli') +subdir('control') +subdir('datapath') diff --git a/modules/meson.build b/modules/meson.build index 1859be3d3..0b978a62b 100644 --- a/modules/meson.build +++ b/modules/meson.build @@ -5,6 +5,7 @@ subdir('infra') subdir('ip') subdir('ip6') subdir('ipip') +subdir('l2') subdir('l4') subdir('policy') subdir('srv6') diff --git a/smoke/l2_bridge_test.sh b/smoke/l2_bridge_test.sh new file mode 100755 index 000000000..bc7f34a51 --- /dev/null +++ b/smoke/l2_bridge_test.sh @@ -0,0 +1,76 @@ +#!/bin/bash +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2025 Christophe Fontaine + +. $(dirname $0)/_init.sh + +echo "=== L2 Bridge Smoke Test ===" + +# Create ports +port_add p0 +port_add p1 +port_add p2 + +# Create bridge domain +echo "Creating bridge domain..." +grcli bridge add testbr aging_time 300 max_mac_count 1024 + +# Add interfaces to bridge +echo "Adding interfaces to bridge..." +grcli interface set p0 mode bridge testbr +grcli interface set p1 mode bridge testbr +grcli interface set p2 mode bridge testbr + +# Verify bridge configuration +echo "Bridge configuration & members" +grcli bridge show testbr + +# Set up test namespaces connected to bridge ports +echo "Setting up test namespaces..." +for n in 0 1 2; do + p=x-p$n + ns=n$n + netns_add $ns + ip link set $p netns $ns + ip -n $ns link set $p up + ip -n $ns addr add 192.168.100.$((n+10))/24 dev $p + ip -n $ns route add default via 192.168.100.1 +done + +# Test static MAC entry +echo "Adding static MAC entry..." +# Get MAC address of n0 interface +MAC0=$(ip netns exec n0 cat /sys/class/net/x-p0/address) +grcli bridge mac add br testbr mac $MAC0 iface p0 static + +echo "MAC table after adding static entry:" +grcli bridge mac list testbr + +# Wait a moment for interfaces to come up +sleep 2 + +echo "Testing L2 connectivity (same subnet)..." +# Test L2 connectivity between hosts in same bridge +ip netns exec n0 ping -i0.01 -c3 -W1 -n 192.168.100.11 || echo "L2 ping n0->n1 failed" +ip netns exec n1 ping -i0.01 -c3 -W1 -n 192.168.100.12 || echo "L2 ping n1->n2 failed" +ip netns exec n2 ping -i0.01 -c3 -W1 -n 192.168.100.10 || echo "L2 ping n2->n0 failed" + +# Check MAC learning +echo "MAC table entries:" +grcli bridge mac list testbr + +# Test MAC flush +echo "Flushing dynamic MAC entries..." +grcli bridge mac flush br testbr dynamic_only + +echo "MAC table after flush:" +grcli bridge mac list testbr + +# Test bridge removal (cleanup) +echo "Cleaning up..." +grcli interface set p0 mode l3 +grcli interface set p1 mode l3 +grcli interface set p2 mode l3 +grcli bridge del testbr + +echo "=== L2 Bridge Smoke Test Complete ===" From 3c8d599204024a3529de96053766e8fa3b1d7802 Mon Sep 17 00:00:00 2001 From: Christophe Fontaine Date: Thu, 27 Nov 2025 17:28:19 +0100 Subject: [PATCH 3/7] l2: add bridge interface to route packets Add a new type of interface to be able to route the packets received on the bridge. Signed-off-by: Christophe Fontaine --- docs/graph.svg | 746 +++++++++++++++--------------- modules/infra/api/gr_infra.h | 3 + modules/infra/control/ctlplane.c | 11 +- modules/l2/api/gr_l2.h | 10 + modules/l2/cli/bridge.c | 95 ++++ modules/l2/control/bridge.c | 2 + modules/l2/control/bridge_iface.c | 296 ++++++++++++ modules/l2/control/meson.build | 1 + modules/l2/datapath/l2_bridge.c | 7 +- modules/l2/datapath/l2_flood.c | 5 + smoke/l2_bridge_test.sh | 18 + 11 files changed, 828 insertions(+), 366 deletions(-) create mode 100644 modules/l2/control/bridge_iface.c diff --git a/docs/graph.svg b/docs/graph.svg index 7ae123fa4..d85435138 100644 --- a/docs/graph.svg +++ b/docs/graph.svg @@ -4,855 +4,873 @@ - - - + + + bond_output - -bond_output + +bond_output port_output - -port_output + +port_output bond_output->port_output - - + + l1_xconnect - -l1_xconnect + +l1_xconnect - + l1_xconnect->port_output - - + + port_tx - -port_tx + +port_tx - + port_output->port_tx - - + + port_rx - -port_rx + +port_rx - + port_rx->l1_xconnect - - + + eth_input - -eth_input + +eth_input - + port_rx->eth_input - - + + l2_bridge - -l2_bridge + +l2_bridge - + port_rx->l2_bridge - - + + lacp_input - -lacp_input + +lacp_input eth_input->lacp_input - - + + snap_input - -snap_input + +snap_input eth_input->snap_input - - + + arp_input - -arp_input + +arp_input eth_input->arp_input - - + + ip_input - -ip_input + +ip_input eth_input->ip_input - - + + ip6_input - -ip6_input + +ip6_input eth_input->ip6_input - - + + eth_output - -eth_output + +eth_output eth_output->bond_output - - + + eth_output->port_output - - + + + + + +eth_output->l2_bridge + + lacp_output - -lacp_output + +lacp_output - + lacp_output->eth_output - - + + l2_redirect - -l2_redirect + +l2_redirect - + snap_input->l2_redirect - - + + arp_input_reply - -arp_input_reply + +arp_input_reply - + arp_input->arp_input_reply - - + + arp_input_request - -arp_input_request + +arp_input_request - + arp_input->arp_input_request - - + + arp_output_reply - -arp_output_reply + +arp_output_reply - + arp_output_reply->eth_output - - + + arp_output_request - -arp_output_request + +arp_output_request - + arp_output_request->eth_output - - + + loopback_input - -loopback_input + +loopback_input - + loopback_input->ip_input - - + + - + loopback_input->ip6_input - - + + loopback_output - -loopback_output + +loopback_output loop_xvrf - -loop_xvrf + +loop_xvrf - + loop_xvrf->ip_input - - + + - + loop_xvrf->ip6_input - - + + ip_forward - -ip_forward + +ip_forward ip_output - -ip_output + +ip_output - + ip_forward->ip_output - - + + ip_fragment - -ip_fragment + +ip_fragment - + ip_fragment->ip_output - - + + ip_hold - -ip_hold + +ip_hold - + ip_input->ip_forward - - + + ip_input_local - -ip_input_local + +ip_input_local - + ip_input->ip_input_local - - + + - + ip_input->ip_output - - + + dnat44_dynamic - -dnat44_dynamic + +dnat44_dynamic - + ip_input->dnat44_dynamic - - + + dnat44_static - -dnat44_static + +dnat44_static - + ip_input->dnat44_static - - + + ip_loadbalance - -ip_loadbalance + +ip_loadbalance - + ip_loadbalance->ip_output - - + + ipip_input - -ipip_input + +ipip_input - + ip_input_local->ipip_input - - + + icmp_input - -icmp_input + +icmp_input - + ip_input_local->icmp_input - - + + l4_input_local - -l4_input_local + +l4_input_local - + ip_input_local->l4_input_local - - + + ospf_redirect - -ospf_redirect + +ospf_redirect - + ip_input_local->ospf_redirect - - + + - + ip_output->eth_output - - + + - + ip_output->loop_xvrf - - + + - + ip_output->ip_fragment - - + + - + ip_output->ip_hold - - + + - + ip_output->ip_loadbalance - - + + ipip_output - -ipip_output + +ipip_output - + ip_output->ipip_output - - + + sr6_output - -sr6_output + +sr6_output - + ip_output->sr6_output - - + + ip6_forward - -ip6_forward + +ip6_forward ip6_output - -ip6_output + +ip6_output - + ip6_forward->ip6_output - - + + ip6_hold - -ip6_hold + +ip6_hold - + ip6_input->ip6_forward - - + + ip6_input_local - -ip6_input_local + +ip6_input_local - + ip6_input->ip6_input_local - - + + - + ip6_input->ip6_output - - + + sr6_local - -sr6_local + +sr6_local - + ip6_input->sr6_local - - + + ip6_loadbalance - -ip6_loadbalance + +ip6_loadbalance - + ip6_loadbalance->ip6_output - - + + icmp6_input - -icmp6_input + +icmp6_input - + ip6_input_local->icmp6_input - - + + - + ip6_input_local->l4_input_local - - + + - + ip6_input_local->ospf_redirect - - + + - + ip6_output->eth_output - - + + - + ip6_output->loop_xvrf - - + + - + ip6_output->ip6_hold - - + + - + ip6_output->ip6_loadbalance - - + + - + ip6_output->sr6_output - - + + - + ipip_input->ip_input - - + + - + ipip_output->ip_output - - + + - + dnat44_dynamic->ip_forward - - + + - + dnat44_dynamic->ip_input_local - - + + - + dnat44_static->ip_forward - - + + - + dnat44_static->ip_input_local - - + + - + sr6_local->ip_input - - + + - + sr6_local->ip6_input - - + + - + sr6_local->ip6_input_local - - + + - + sr6_output->ip6_output - - + + icmp_output - -icmp_output + +icmp_output - + icmp_input->icmp_output - - + + icmp_local_send - -icmp_local_send + +icmp_local_send - + icmp_local_send->icmp_output - - + + - + icmp_output->ip_output - - + + icmp6_output - -icmp6_output + +icmp6_output - + icmp6_input->icmp6_output - - + + ndp_na_input - -ndp_na_input + +ndp_na_input - + icmp6_input->ndp_na_input - - + + ndp_ns_input - -ndp_ns_input + +ndp_ns_input - + icmp6_input->ndp_ns_input - - + + ndp_rs_input - -ndp_rs_input + +ndp_rs_input - + icmp6_input->ndp_rs_input - - + + - + icmp6_output->ip6_output - - + + icmp6_local_send - -icmp6_local_send + +icmp6_local_send - + icmp6_local_send->icmp6_output - - + + ndp_na_output - -ndp_na_output + +ndp_na_output - + ndp_na_output->icmp6_output - - + + ndp_ns_output - -ndp_ns_output + +ndp_ns_output - + ndp_ns_output->icmp6_output - - + + l4_loopback_output - -l4_loopback_output + +l4_loopback_output - + l4_input_local->l4_loopback_output - - + + dhcp_input - -dhcp_input + +dhcp_input - + l4_input_local->dhcp_input - - + + - + l4_loopback_output->loopback_output - - + + - + l2_bridge->bond_output - - + + - + l2_bridge->port_output - - + + + + + +l2_bridge->eth_input + + l2_flood - -l2_flood + +l2_flood - + l2_bridge->l2_flood - - + + - + ospf_redirect->l2_redirect - - + + - + l2_flood->bond_output - - + + - + l2_flood->port_output - - + + + + + +l2_flood->eth_input + + diff --git a/modules/infra/api/gr_infra.h b/modules/infra/api/gr_infra.h index 69f0d936a..999fb5457 100644 --- a/modules/infra/api/gr_infra.h +++ b/modules/infra/api/gr_infra.h @@ -20,6 +20,7 @@ typedef enum : uint8_t { GR_IFACE_TYPE_VLAN, GR_IFACE_TYPE_IPIP, GR_IFACE_TYPE_BOND, + GR_IFACE_TYPE_BRIDGE, GR_IFACE_TYPE_COUNT } gr_iface_type_t; @@ -399,6 +400,8 @@ static inline const char *gr_iface_type_name(gr_iface_type_t type) { return "ipip"; case GR_IFACE_TYPE_BOND: return "bond"; + case GR_IFACE_TYPE_BRIDGE: + return "bridge"; case GR_IFACE_TYPE_COUNT: break; } diff --git a/modules/infra/control/ctlplane.c b/modules/infra/control/ctlplane.c index c9443b9b6..175572a1e 100644 --- a/modules/infra/control/ctlplane.c +++ b/modules/infra/control/ctlplane.c @@ -41,6 +41,7 @@ static struct rte_mempool *cp_pool; static struct event_base *ev_base; static control_input_t port_output_id; +static control_input_t bridge_output_id; static void finalize_fd(struct event *ev, void * /*priv*/) { int fd = event_get_fd(ev); @@ -108,10 +109,13 @@ static void iface_cp_poll(evutil_socket_t, short reason, void *ev_iface) { struct rte_vlan_hdr *vlan; struct rte_mbuf *mbuf; rte_be16_t ether_type; + control_input_t node; size_t read_len; size_t len; char *data; + node = port_output_id; + if (reason & EV_CLOSED) { LOG(ERR, "tap device %s deleted", iface->name); iface_destroy(iface->id); @@ -227,7 +231,10 @@ static void iface_cp_poll(evutil_socket_t, short reason, void *ev_iface) { goto err; } iface = child_iface; + } else if (iface->type == GR_IFACE_TYPE_BRIDGE) { + node = bridge_output_id; } + mbuf_data(mbuf)->iface = iface; stats = iface_get_stats(rte_lcore_id(), iface->id); @@ -237,7 +244,7 @@ static void iface_cp_poll(evutil_socket_t, short reason, void *ev_iface) { if (gr_config.log_packets) trace_log_packet(mbuf, "cp rx", iface->name); - post_to_stack(port_output_id, mbuf); + post_to_stack(node, mbuf); return; err: @@ -423,6 +430,7 @@ static void iface_event(uint32_t event, const void *obj) { case GR_IFACE_TYPE_PORT: case GR_IFACE_TYPE_VLAN: case GR_IFACE_TYPE_BOND: + case GR_IFACE_TYPE_BRIDGE: break; default: return; @@ -468,6 +476,7 @@ static void cp_module_init(struct event_base *base) { ABORT("pktmbuf_pool returned NULL"); ev_base = base; port_output_id = gr_control_input_register_handler("port_output", true); + bridge_output_id = gr_control_input_register_handler("l2_bridge", true); } static void cp_module_fini(struct event_base *) { diff --git a/modules/l2/api/gr_l2.h b/modules/l2/api/gr_l2.h index 8f74da71a..243e1172e 100644 --- a/modules/l2/api/gr_l2.h +++ b/modules/l2/api/gr_l2.h @@ -132,5 +132,15 @@ struct gr_l2_bridge_config_set_req { struct gr_l2_bridge_config config; }; +// Bridge interface info for GR_IFACE_TYPE_BRIDGE +struct __gr_iface_info_bridge_base { + uint16_t bridge_id; + struct rte_ether_addr mac; +}; + +struct gr_iface_info_bridge { + BASE(__gr_iface_info_bridge_base); +}; + // Bridge reconfig attributes #define GR_BRIDGE_SET_BRIDGE_ID GR_BIT64(32) diff --git a/modules/l2/cli/bridge.c b/modules/l2/cli/bridge.c index 21d31a7da..42bd987ae 100644 --- a/modules/l2/cli/bridge.c +++ b/modules/l2/cli/bridge.c @@ -18,6 +18,88 @@ #include #include +// Bridge interface type CLI support +static void bridge_show(struct gr_api_client *, const struct gr_iface *iface) { + const struct gr_iface_info_bridge *bridge_info = PAYLOAD(iface); + + printf("bridge_id: %u mac: " ETH_F "\n", bridge_info->bridge_id, &bridge_info->base.mac); +} + +static void +bridge_list_info(struct gr_api_client *, const struct gr_iface *iface, char *buf, size_t len) { + const struct gr_iface_info_bridge *bridge_info = PAYLOAD(iface); + + snprintf( + buf, + len, + "bridge_id: %u mac: " ETH_F, + bridge_info->bridge_id, + &bridge_info->base.mac + ); +} + +static struct cli_iface_type bridge_iface_type = { + .type_id = GR_IFACE_TYPE_BRIDGE, + .show = bridge_show, + .list_info = bridge_list_info, +}; + +// Bridge interface creation command +static cmd_status_t bridge_iface_add(struct gr_api_client *c, const struct ec_pnode *p) { + struct gr_infra_iface_add_req *req = NULL; + struct gr_l2_bridge *bridge; + const char *bridge_name; + void *resp_ptr = NULL; + size_t len; + int ret; + + len = sizeof(*req) + sizeof(struct gr_iface_info_bridge); + if ((req = calloc(1, len)) == NULL) { + errno = ENOMEM; + ret = -1; + goto cleanup; + } + + req->iface.type = GR_IFACE_TYPE_BRIDGE; + req->iface.flags = GR_IFACE_F_UP; + req->iface.mode = GR_IFACE_MODE_L3; // Bridge interfaces are L3 for IP processing + req->iface.mtu = 1500; + + const char *name = arg_str(p, "NAME"); + if (name == NULL) { + errno = EINVAL; + ret = -1; + goto cleanup; + } + strncpy(req->iface.name, name, sizeof(req->iface.name) - 1); + + bridge_name = arg_str(p, "BRIDGE"); + if (bridge_name == NULL) { + errno = EINVAL; + ret = -1; + goto cleanup; + } + bridge = bridge_from_name(c, bridge_name); + if (bridge == NULL) { + errno = ENOENT; + ret = -1; + goto cleanup; + } + + req->iface.domain_id = bridge->bridge_id; + + struct gr_iface_info_bridge *bridge_info = (struct gr_iface_info_bridge *)req->iface.info; + bridge_info->bridge_id = bridge->bridge_id; + free(bridge); + + ret = gr_api_client_send_recv(c, GR_INFRA_IFACE_ADD, len, req, &resp_ptr); + +cleanup: + free(resp_ptr); + free(req); + return ret < 0 ? CMD_ERROR : CMD_SUCCESS; +} + // Bridge domain management commands static cmd_status_t bridge_add(struct gr_api_client *c, const struct ec_pnode *p) { struct gr_l2_bridge_add_req req = {0}; @@ -300,6 +382,18 @@ int complete_bridge_names( static int ctx_init(struct ec_node *root) { int ret; + // Bridge interface creation command + ret = CLI_COMMAND( + INTERFACE_ADD_CTX(root), + "bridge NAME bridge BRIDGE", + bridge_iface_add, + "Create a new bridge interface.", + with_help("Interface name.", ec_node("any", "NAME")), + with_help("Bridge name.", ec_node_dyn("BRIDGE", complete_bridge_names, NULL)) + ); + if (ret < 0) + return ret; + // Bridge domain commands ret = CLI_COMMAND( CLI_CONTEXT(root, CTX_ARG("bridge", "Layer 2 bridge configuration.")), @@ -389,4 +483,5 @@ static struct cli_context ctx = { static void __attribute__((constructor, used)) init(void) { cli_context_register(&ctx); + register_iface_type(&bridge_iface_type); } diff --git a/modules/l2/control/bridge.c b/modules/l2/control/bridge.c index 7c5591b6e..56c31ed0d 100644 --- a/modules/l2/control/bridge.c +++ b/modules/l2/control/bridge.c @@ -218,6 +218,7 @@ int bridge_member_add(uint16_t bridge_id, uint16_t iface_id) { iface->domain_id = bridge_id; iface_set_promisc(iface->id, true); LOG(INFO, "Added interface %u to bridge %u (%s)", iface_id, bridge_id, bridge->name); + gr_event_push(GR_EVENT_IFACE_POST_RECONFIG, iface); return 0; } @@ -250,6 +251,7 @@ int bridge_member_del(uint16_t bridge_id, uint16_t iface_id) { iface_set_promisc(iface->id, false); iface->mode = GR_IFACE_MODE_L3; iface->domain_id = 0; + gr_event_push(GR_EVENT_IFACE_POST_RECONFIG, iface); return 0; } else { return errno_set(ENODEV); diff --git a/modules/l2/control/bridge_iface.c b/modules/l2/control/bridge_iface.c new file mode 100644 index 000000000..436615716 --- /dev/null +++ b/modules/l2/control/bridge_iface.c @@ -0,0 +1,296 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2025 Christophe Fontaine + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +// Hash table for bridge interface lookup +static struct rte_hash *bridge_iface_hash; + +// Define the bridge interface info structure +GR_IFACE_INFO(GR_IFACE_TYPE_BRIDGE, iface_info_bridge, { struct gr_iface_info_bridge base; }); + +static int iface_bridge_reconfig( + struct iface *iface, + uint64_t set_attrs, + const struct gr_iface *, + const void *api_info +) { + struct iface_info_bridge *cur = iface_info_bridge(iface); + const struct gr_iface_info_bridge *next = api_info; + bool reconfig = set_attrs != IFACE_SET_ALL; + + // Update bridge ID if changed + if (set_attrs & GR_BRIDGE_SET_BRIDGE_ID) { + if (reconfig && cur->base.bridge_id != next->bridge_id) { + // Remove from old bridge if needed + if (cur->base.bridge_id != 0) { + struct bridge_info *old_bridge = bridge_get(cur->base.bridge_id); + if (old_bridge != NULL && old_bridge->bridge_iface == iface) { + old_bridge->bridge_iface = NULL; + } + } + } + cur->base.bridge_id = next->bridge_id; + iface->domain_id = next->bridge_id; + + // Associate with new bridge + struct bridge_info *bridge = bridge_get(cur->base.bridge_id); + if (bridge != NULL) { + bridge->bridge_iface = iface; + } + } + + // Generate a default MAC address if not set + if (!reconfig) { + // Use a locally administered MAC address based on bridge ID + // and iface id + cur->base.mac = (struct rte_ether_addr) { + .addr_bytes = { + 0x02, + 0x00, + (cur->base.bridge_id >> 8) & 0xff, + cur->base.bridge_id & 0xff, + (iface->id >> 8) & 0xff, + iface->id & 0xff, + } + }; + } + + return 0; +} + +static int iface_bridge_init(struct iface *iface, const void *api_info) { + int ret; + + ret = iface_bridge_reconfig(iface, IFACE_SET_ALL, NULL, api_info); + if (ret < 0) { + errno = -ret; + return ret; + } + + // Add to hash table for lookup + struct iface_info_bridge *bridge_info = iface_info_bridge(iface); + ret = rte_hash_add_key_data(bridge_iface_hash, &bridge_info->base.bridge_id, iface); + if (ret < 0) { + LOG(ERR, "Failed to add bridge interface to hash: %s", rte_strerror(-ret)); + errno = -ret; + return ret; + } + + LOG(INFO, + "Created bridge interface %u for bridge domain %u", + iface->id, + bridge_info->base.bridge_id); + + return 0; +} + +static int iface_bridge_fini(struct iface *iface) { + struct iface_info_bridge *bridge_info = iface_info_bridge(iface); + int ret; + + // Remove from hash table + ret = rte_hash_del_key(bridge_iface_hash, &bridge_info->base.bridge_id); + if (ret < 0) { + LOG(WARNING, "Failed to remove bridge interface from hash: %s", rte_strerror(-ret)); + } + + // Dissociate from bridge + struct bridge_info *bridge = bridge_get(bridge_info->base.bridge_id); + if (bridge != NULL && bridge->bridge_iface == iface) { + bridge->bridge_iface = NULL; + } + + LOG(INFO, + "Destroyed bridge interface %u for bridge domain %u", + iface->id, + bridge_info->base.bridge_id); + + return 0; +} + +static int iface_bridge_get_eth_addr(const struct iface *iface, struct rte_ether_addr *mac) { + const struct iface_info_bridge *bridge_info = iface_info_bridge(iface); + *mac = bridge_info->base.mac; + return 0; +} + +static int iface_bridge_set_eth_addr(struct iface *iface, const struct rte_ether_addr *mac) { + struct iface_info_bridge *bridge_info = iface_info_bridge(iface); + + if (mac == NULL) + return errno_set(EINVAL); + + bridge_info->base.mac = *mac; + return 0; +} + +static int iface_bridge_set_up_down(struct iface *iface, const bool up) { + if (up) { + iface->flags |= GR_IFACE_F_UP; + iface->state |= GR_IFACE_S_RUNNING; + } else { + iface->flags &= ~GR_IFACE_F_UP; + iface->state &= ~GR_IFACE_S_RUNNING; + } + return 0; +} + +static void bridge_iface_to_api(void *info, const struct iface *iface) { + const struct iface_info_bridge *bridge_info = iface_info_bridge(iface); + struct gr_iface_info_bridge *api = info; + + *api = bridge_info->base; +} + +static struct iface_type iface_type_bridge = { + .id = GR_IFACE_TYPE_BRIDGE, + .name = "bridge", + .pub_size = sizeof(struct gr_iface_info_bridge), + .priv_size = sizeof(struct iface_info_bridge), + .init = iface_bridge_init, + .reconfig = iface_bridge_reconfig, + .fini = iface_bridge_fini, + .get_eth_addr = iface_bridge_get_eth_addr, + .set_eth_addr = iface_bridge_set_eth_addr, + .set_up_down = iface_bridge_set_up_down, + .to_api = bridge_iface_to_api, +}; + +// Bridge interface management functions +int bridge_iface_create(uint16_t bridge_id) { + struct gr_iface_info_bridge bridge_info = {0}; + char name[GR_IFACE_NAME_SIZE]; + struct bridge_info *bridge; + struct gr_iface iface = {0}; + + bridge = bridge_get(bridge_id); + if (bridge == NULL) + return errno_set(ENOENT); + + if (bridge->bridge_iface != NULL) + return errno_set(EEXIST); + + // Create interface name + snprintf(name, sizeof(name), "br%u", bridge_id); + + // Set up interface info + iface.type = GR_IFACE_TYPE_BRIDGE; + iface.mode = GR_IFACE_MODE_L3; // Bridge interfaces are L3 for IP processing + iface.flags = GR_IFACE_F_UP; + iface.mtu = 1500; + memccpy(iface.name, name, 0, sizeof(iface.name) - 1); + + bridge_info.bridge_id = bridge_id; + + struct iface *new_iface = iface_create(&iface, &bridge_info); + if (new_iface == NULL) + return -errno; + + LOG(INFO, + "Created bridge interface %s (id=%u) for bridge domain %u", + name, + new_iface->id, + bridge_id); + + return new_iface->id; +} + +int bridge_iface_destroy(uint16_t bridge_id) { + struct bridge_info *bridge; + + bridge = bridge_get(bridge_id); + if (bridge == NULL) + return errno_set(ENOENT); + + if (bridge->bridge_iface == NULL) + return errno_set(ENOENT); + + uint16_t iface_id = bridge->bridge_iface->id; + int ret = iface_destroy(iface_id); + if (ret < 0) + return ret; + + LOG(INFO, "Destroyed bridge interface %u for bridge domain %u", iface_id, bridge_id); + + return 0; +} + +struct iface *bridge_get_iface(uint16_t bridge_id) { + void *data; + + if (rte_hash_lookup_data(bridge_iface_hash, &bridge_id, &data) < 0) + return NULL; + + return (struct iface *)data; +} + +static void bridge_iface_init_module(struct event_base *) { + struct rte_hash_parameters params = { + .name = "bridge_iface", + .entries = GR_MAX_BRIDGE_DOMAINS, + .key_len = sizeof(uint16_t), + .socket_id = SOCKET_ID_ANY, + .extra_flag = RTE_HASH_EXTRA_FLAGS_RW_CONCURRENCY_LF + | RTE_HASH_EXTRA_FLAGS_TRANS_MEM_SUPPORT, + }; + + bridge_iface_hash = rte_hash_create(¶ms); + if (bridge_iface_hash == NULL) + ABORT("rte_hash_create(bridge_iface): %s", rte_strerror(rte_errno)); + + iface_type_register(&iface_type_bridge); +} + +static void bridge_iface_fini_module(struct event_base *) { + if (bridge_iface_hash != NULL) { + rte_hash_free(bridge_iface_hash); + bridge_iface_hash = NULL; + } +} + +static struct gr_module bridge_iface_module = { + .name = "bridge interface", + .init = bridge_iface_init_module, + .fini = bridge_iface_fini_module, +}; + +static void iface_event(uint32_t, const void *obj) { + const struct iface *iface = obj; + if (iface->type != GR_IFACE_TYPE_BRIDGE) + return; + + if (iface->state & GR_IFACE_S_RUNNING) + gr_event_push(GR_EVENT_IFACE_STATUS_UP, iface); + else + gr_event_push(GR_EVENT_IFACE_STATUS_DOWN, iface); +} + +static struct gr_event_subscription iface_event_handler = { + .callback = iface_event, + .ev_count = 2, + .ev_types = { + GR_EVENT_IFACE_POST_ADD, + GR_EVENT_IFACE_POST_RECONFIG, + }, +}; + +RTE_INIT(bridge_iface_constructor) { + gr_register_module(&bridge_iface_module); + gr_event_subscribe(&iface_event_handler); +} diff --git a/modules/l2/control/meson.build b/modules/l2/control/meson.build index 6a3e0476b..5fd34592a 100644 --- a/modules/l2/control/meson.build +++ b/modules/l2/control/meson.build @@ -3,6 +3,7 @@ src += files( 'bridge.c', + 'bridge_iface.c', 'mac_table.c', 'api_handlers.c', ) diff --git a/modules/l2/datapath/l2_bridge.c b/modules/l2/datapath/l2_bridge.c index 36ddd76a7..d64e0bd72 100644 --- a/modules/l2/datapath/l2_bridge.c +++ b/modules/l2/datapath/l2_bridge.c @@ -19,6 +19,7 @@ enum edges { PORT_OUTPUT = 0, // Send to specific port BOND_OUTPUT, + L2_INPUT, // Send to L3 processing (bridge interface) FLOOD, // Flood to all bridge members DROP, // Drop packet EDGE_COUNT @@ -108,9 +109,11 @@ l2_bridge_process(struct rte_graph *graph, struct rte_node *node, void **objs, u case GR_IFACE_TYPE_BOND: edge = BOND_OUTPUT; break; + case GR_IFACE_TYPE_BRIDGE: + edge = L2_INPUT; + break; default: edge = DROP; - break; } } else { if (bridge->config.flood_unknown) { @@ -169,6 +172,7 @@ static struct rte_node_register l2_bridge_node = { .process = l2_bridge_process, .nb_edges = EDGE_COUNT, .next_nodes = { + [L2_INPUT] = "eth_input", [PORT_OUTPUT] = "port_output", [BOND_OUTPUT] = "bond_output", [FLOOD] = "l2_flood", @@ -178,6 +182,7 @@ static struct rte_node_register l2_bridge_node = { static void l2_bridge_register(void) { register_interface_mode(GR_IFACE_MODE_L2_BRIDGE, "l2_bridge"); + eth_output_register_interface_type(GR_IFACE_TYPE_BRIDGE, "l2_bridge"); } static struct gr_node_info info = { diff --git a/modules/l2/datapath/l2_flood.c b/modules/l2/datapath/l2_flood.c index 2a65021f0..2e280dba0 100644 --- a/modules/l2/datapath/l2_flood.c +++ b/modules/l2/datapath/l2_flood.c @@ -19,6 +19,7 @@ enum edges { PORT_OUTPUT = 0, BOND_OUTPUT, + L2_INPUT, DROP, EDGE_COUNT }; @@ -89,6 +90,9 @@ l2_flood_process(struct rte_graph *graph, struct rte_node *node, void **objs, ui case GR_IFACE_TYPE_BOND: edges[flood_count] = BOND_OUTPUT; break; + case GR_IFACE_TYPE_BRIDGE: + edges[flood_count] = L2_INPUT; + break; default: edges[flood_count] = DROP; break; @@ -155,6 +159,7 @@ static struct rte_node_register l2_flood_node = { .next_nodes = { [PORT_OUTPUT] = "port_output", [BOND_OUTPUT] = "bond_output", + [L2_INPUT] = "eth_input", [DROP] = "l2_flood_drop", }, }; diff --git a/smoke/l2_bridge_test.sh b/smoke/l2_bridge_test.sh index bc7f34a51..df196f330 100755 --- a/smoke/l2_bridge_test.sh +++ b/smoke/l2_bridge_test.sh @@ -25,6 +25,15 @@ grcli interface set p2 mode bridge testbr echo "Bridge configuration & members" grcli bridge show testbr +# Create bridge interface for L3 integration +echo "Creating bridge interface..." +grcli interface add bridge br1 bridge testbr +grcli interface set br1 mode bridge testbr + +# Assign IP address to bridge interface +echo "Assigning IP to bridge interface..." +grcli address add 192.168.100.1/24 iface br1 + # Set up test namespaces connected to bridge ports echo "Setting up test namespaces..." for n in 0 1 2; do @@ -55,6 +64,15 @@ ip netns exec n0 ping -i0.01 -c3 -W1 -n 192.168.100.11 || echo "L2 ping n0->n1 f ip netns exec n1 ping -i0.01 -c3 -W1 -n 192.168.100.12 || echo "L2 ping n1->n2 failed" ip netns exec n2 ping -i0.01 -c3 -W1 -n 192.168.100.10 || echo "L2 ping n2->n0 failed" +echo "Testing L3 connectivity (from bridge interface)..." +grcli ping 192.168.100.10 count 3 delay 10 + +echo "Testing L3 connectivity (to bridge interface)..." +# Test L3 connectivity to bridge interface +ip netns exec n0 ping -i0.01 -c3 -W1 -n 192.168.100.1 || echo "L3 ping n0->bridge failed" +ip netns exec n1 ping -i0.01 -c3 -W1 -n 192.168.100.1 || echo "L3 ping n1->bridge failed" +ip netns exec n2 ping -i0.01 -c3 -W1 -n 192.168.100.1 || echo "L3 ping n2->bridge failed" + # Check MAC learning echo "MAC table entries:" grcli bridge mac list testbr From 201028831ee2b88a3d737c3ec1673f6888b7ceaa Mon Sep 17 00:00:00 2001 From: Christophe Fontaine Date: Tue, 2 Dec 2025 23:12:06 +0100 Subject: [PATCH 4/7] bridge: add mac add/del events Add new events when a mac is added and removed to/from a bridge. Signed-off-by: Christophe Fontaine --- modules/l2/api/gr_l2.h | 5 +++++ modules/l2/cli/mac.c | 32 ++++++++++++++++++++++++++++++++ modules/l2/control/mac_table.c | 31 +++++++++++++++++++++++++++---- 3 files changed, 64 insertions(+), 4 deletions(-) diff --git a/modules/l2/api/gr_l2.h b/modules/l2/api/gr_l2.h index 243e1172e..56515a481 100644 --- a/modules/l2/api/gr_l2.h +++ b/modules/l2/api/gr_l2.h @@ -144,3 +144,8 @@ struct gr_iface_info_bridge { // Bridge reconfig attributes #define GR_BRIDGE_SET_BRIDGE_ID GR_BIT64(32) + +typedef enum { + GR_EVENT_BRIDGE_MAC_ADD = EVENT_TYPE(GR_L2_MODULE, 0x0001), + GR_EVENT_BRIDGE_MAC_DEL = EVENT_TYPE(GR_L2_MODULE, 0x0002), +} gr_event_bridge_t; diff --git a/modules/l2/cli/mac.c b/modules/l2/cli/mac.c index 59763011e..df4653c14 100644 --- a/modules/l2/cli/mac.c +++ b/modules/l2/cli/mac.c @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -286,6 +287,37 @@ static struct cli_context ctx = { .init = ctx_init, }; +static void mac_event_print(uint32_t event, const void *obj) { + const struct gr_l2_mac_entry *entry = obj; + const char *action; + + switch (event) { + case GR_EVENT_BRIDGE_MAC_ADD: + action = "add"; + break; + case GR_EVENT_BRIDGE_MAC_DEL: + action = "del"; + break; + default: + action = "?"; + break; + } + printf("mac %s: " ETH_F " bridge=%u iface=%u\n", + action, + &entry->mac, + entry->bridge_id, + entry->iface_id); +} + +static struct cli_event_printer printer = { + .print = mac_event_print, + .ev_count = 2, + .ev_types = { + GR_EVENT_BRIDGE_MAC_ADD, + GR_EVENT_BRIDGE_MAC_DEL, + }, +}; static void __attribute__((constructor, used)) init(void) { + cli_event_printer_register(&printer); cli_context_register(&ctx); } diff --git a/modules/l2/control/mac_table.c b/modules/l2/control/mac_table.c index c9c0428f8..11a8541f9 100644 --- a/modules/l2/control/mac_table.c +++ b/modules/l2/control/mac_table.c @@ -24,10 +24,7 @@ // MAC table entry structure struct mac_entry { - uint16_t bridge_id; - uint16_t iface_id; - struct rte_ether_addr mac; - gr_l2_mac_type_t type; + struct gr_l2_mac_entry; time_t created; time_t last_seen; struct mac_entry *next; // For hash collision chaining @@ -232,6 +229,7 @@ int mac_entry_add( bridge_id, type == GR_L2_MAC_STATIC ? "static" : "dynamic"); + gr_event_push(GR_EVENT_BRIDGE_MAC_ADD, entry); return 0; } @@ -280,6 +278,7 @@ int mac_entry_del(uint16_t bridge_id, const struct rte_ether_addr *mac) { "Deleted MAC entry " RTE_ETHER_ADDR_PRT_FMT " from bridge %u", RTE_ETHER_ADDR_BYTES(&entry->mac), bridge_id); + gr_event_push(GR_EVENT_BRIDGE_MAC_DEL, entry); free_mac_entry(entry); return 0; @@ -368,6 +367,7 @@ int mac_table_flush(uint16_t bridge_id, uint16_t iface_id, bool dynamic_only) { RTE_ETHER_ADDR_BYTES(&entry->mac), bridge_id); + gr_event_push(GR_EVENT_BRIDGE_MAC_DEL, entry); free_mac_entry(entry); } else { prev = entry; @@ -465,6 +465,7 @@ static void mac_aging_callback( "Aged out MAC entry " RTE_ETHER_ADDR_PRT_FMT " from bridge %u", RTE_ETHER_ADDR_BYTES(&entry->mac), bridge_id); + gr_event_push(GR_EVENT_BRIDGE_MAC_DEL, entry); free_mac_entry(entry); } else { @@ -534,7 +535,29 @@ static struct gr_module mac_table_module = { .fini = mac_table_module_fini, }; +static int mac_serialize(const void *obj, void **buf) { + struct gr_l2_mac_entry *entry; + size_t len = sizeof(*entry); + + if ((entry = malloc(len)) == NULL) + return 0; + + mac_entry_to_api(entry, obj); + *buf = entry; + return len; +} + +static struct gr_event_serializer mac_serializer = { + .callback = mac_serialize, + .ev_count = 2, + .ev_types = { + GR_EVENT_BRIDGE_MAC_ADD, + GR_EVENT_BRIDGE_MAC_DEL, + }, +}; + RTE_INIT(mac_table_constructor) { + gr_event_register_serializer(&mac_serializer); gr_register_module(&mac_table_module); gr_event_subscribe(&mac_table_event_sub); } From 44ddd671b903b1f921f1a4c8082ab525287f4dc2 Mon Sep 17 00:00:00 2001 From: Christophe Fontaine Date: Fri, 5 Dec 2025 00:24:07 +0100 Subject: [PATCH 5/7] bridge: create a default bridge interface for all bridges Automate the creation of a bridge interface when a bridge is created. Instead of generating an interface name based on the bridge id "br%u", use the bridge name directly. Update l2 bridge test to use the proper name. Add a helper to create/delete a bridge. Signed-off-by: Christophe Fontaine --- modules/l2/control/bridge.c | 37 +++++++++++++++++++++++++++--- modules/l2/control/bridge_iface.c | 6 +---- modules/l2/control/gr_l2_control.h | 2 +- smoke/_init.sh | 6 +++++ smoke/l2_bridge_test.sh | 11 ++------- 5 files changed, 44 insertions(+), 18 deletions(-) diff --git a/modules/l2/control/bridge.c b/modules/l2/control/bridge.c index 56c31ed0d..01234a7b9 100644 --- a/modules/l2/control/bridge.c +++ b/modules/l2/control/bridge.c @@ -97,7 +97,7 @@ struct bridge_info *bridge_get_by_name(const char *name) { struct bridge_info *bridge_add(const char *name, const struct gr_l2_bridge_config *config) { struct bridge_info *bridge; uint16_t bridge_id; - int ret; + int ret, iface_ret; if (name == NULL || strlen(name) == 0 || strlen(name) >= GR_L2_BRIDGE_NAME_SIZE) return errno_set_null(EBADMSG); @@ -137,6 +137,26 @@ struct bridge_info *bridge_add(const char *name, const struct gr_l2_bridge_confi return errno_set_null(-ret); } + // Automatically create bridge interface (similar to Linux behavior) + iface_ret = bridge_iface_create(bridge_id, bridge->name); + if (iface_ret < 0) { + LOG(ERR, + "Failed to create bridge interface for %s (id=%u): %s", + name, + bridge_id, + strerror(-iface_ret)); + // Continue without bridge interface - bridge domain still functional + bridge->active = false; + return errno_set_null(-ret); + } else { + LOG(INFO, + "Created bridge interface %d for bridge domain %u (%s)", + iface_ret, + bridge_id, + name); + } + bridge_member_add(bridge_id, iface_ret); + LOG(INFO, "Created bridge domain %u (%s)", bridge_id, name); return bridge; } @@ -166,8 +186,19 @@ int bridge_del(uint16_t bridge_id) { // Clean up bridge interface if it exists if (bridge->bridge_iface != NULL) { - // TODO: Destroy bridge interface - bridge->bridge_iface = NULL; + int iface_ret = bridge_iface_destroy(bridge_id); + if (iface_ret < 0) { + LOG(WARNING, + "Failed to destroy bridge interface for bridge %u (%s): %s", + bridge_id, + bridge->name, + strerror(-iface_ret)); + } else { + LOG(INFO, + "Destroyed bridge interface for bridge %u (%s)", + bridge_id, + bridge->name); + } } // Free member list diff --git a/modules/l2/control/bridge_iface.c b/modules/l2/control/bridge_iface.c index 436615716..6a261e839 100644 --- a/modules/l2/control/bridge_iface.c +++ b/modules/l2/control/bridge_iface.c @@ -173,9 +173,8 @@ static struct iface_type iface_type_bridge = { }; // Bridge interface management functions -int bridge_iface_create(uint16_t bridge_id) { +int bridge_iface_create(uint16_t bridge_id, const char *name) { struct gr_iface_info_bridge bridge_info = {0}; - char name[GR_IFACE_NAME_SIZE]; struct bridge_info *bridge; struct gr_iface iface = {0}; @@ -186,9 +185,6 @@ int bridge_iface_create(uint16_t bridge_id) { if (bridge->bridge_iface != NULL) return errno_set(EEXIST); - // Create interface name - snprintf(name, sizeof(name), "br%u", bridge_id); - // Set up interface info iface.type = GR_IFACE_TYPE_BRIDGE; iface.mode = GR_IFACE_MODE_L3; // Bridge interfaces are L3 for IP processing diff --git a/modules/l2/control/gr_l2_control.h b/modules/l2/control/gr_l2_control.h index 85175539f..015f782db 100644 --- a/modules/l2/control/gr_l2_control.h +++ b/modules/l2/control/gr_l2_control.h @@ -60,6 +60,6 @@ void mac_aging_timer_start(void); void mac_aging_timer_stop(void); // Bridge interface management (for L3 integration) -int bridge_iface_create(uint16_t bridge_id); +int bridge_iface_create(uint16_t bridge_id, const char *name); int bridge_iface_destroy(uint16_t bridge_id); struct iface *bridge_get_iface(uint16_t bridge_id); diff --git a/smoke/_init.sh b/smoke/_init.sh index 584cc1cea..d77800cbc 100644 --- a/smoke/_init.sh +++ b/smoke/_init.sh @@ -163,6 +163,12 @@ port_add() { tap_counter=$((tap_counter + 1)) } +bridge_add() { + local name="$1" + grcli bridge add $name aging_time 300 max_mac_count 1024 + echo "grcli bridge del $name" >> $tmp/cleanup +} + llocal_addr() { grcli address show iface "$1" | sed -En "s/^$1[[:space:]]+(fe80:.+)\\/64\$/\\1/p" } diff --git a/smoke/l2_bridge_test.sh b/smoke/l2_bridge_test.sh index df196f330..a5e06557c 100755 --- a/smoke/l2_bridge_test.sh +++ b/smoke/l2_bridge_test.sh @@ -13,7 +13,7 @@ port_add p2 # Create bridge domain echo "Creating bridge domain..." -grcli bridge add testbr aging_time 300 max_mac_count 1024 +bridge_add testbr # Add interfaces to bridge echo "Adding interfaces to bridge..." @@ -25,14 +25,9 @@ grcli interface set p2 mode bridge testbr echo "Bridge configuration & members" grcli bridge show testbr -# Create bridge interface for L3 integration -echo "Creating bridge interface..." -grcli interface add bridge br1 bridge testbr -grcli interface set br1 mode bridge testbr - # Assign IP address to bridge interface echo "Assigning IP to bridge interface..." -grcli address add 192.168.100.1/24 iface br1 +grcli address add 192.168.100.1/24 iface testbr # Set up test namespaces connected to bridge ports echo "Setting up test namespaces..." @@ -89,6 +84,4 @@ echo "Cleaning up..." grcli interface set p0 mode l3 grcli interface set p1 mode l3 grcli interface set p2 mode l3 -grcli bridge del testbr - echo "=== L2 Bridge Smoke Test Complete ===" From 794dd6a76fa63aa75f496a35021ffd04c4e14d0a Mon Sep 17 00:00:00 2001 From: Christophe Fontaine Date: Fri, 5 Dec 2025 02:17:42 +0100 Subject: [PATCH 6/7] frr: add bridge support Add bridge interface type support to FRR, and populate the fields "bridge_ifindex" and "master_ifindex" to create the proper hierarchy in frr. Signed-off-by: Christophe Fontaine --- frr/if_grout.c | 43 +++++++++++++++++++++++++++++++++++++--- frr/zebra_dplane_grout.c | 2 ++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/frr/if_grout.c b/frr/if_grout.c index 4fa7783c8..18ceb3c55 100644 --- a/frr/if_grout.c +++ b/frr/if_grout.c @@ -37,6 +37,27 @@ static uint64_t gr_if_flags_to_netlink(struct gr_iface *gr_if, enum zebra_link_t return frr_if_flags; } +static ifindex_t gr_bridge_id_to_if_index(uint16_t domain_id) { + struct gr_l2_bridge_get_req req = {.bridge_id = domain_id}; + struct gr_l2_bridge *bridge_info = NULL; + ifindex_t idx = IFINDEX_INTERNAL; + void *resp_ptr; + + if (grout_client_send_recv(GR_L2_BRIDGE_GET, sizeof(req), &req, &resp_ptr) < 0) { + gr_log_err("error requesting bridge_info with id %u", domain_id); + goto cleanup; + } + bridge_info = resp_ptr; + if (bridge_info->iface_id == 0) { + printf("No bridge interface for domain %u\n", domain_id); + goto cleanup; + } + idx = ifindex_grout_to_frr(bridge_info->iface_id); +cleanup: + free(bridge_info); + return idx; +} + void grout_link_change(struct gr_iface *gr_if, bool new, bool startup) { enum zebra_slave_iftype slave_type = ZEBRA_IF_SLAVE_NONE; enum zebra_link_type link_type = ZEBRA_LLT_UNKNOWN; @@ -44,8 +65,11 @@ void grout_link_change(struct gr_iface *gr_if, bool new, bool startup) { const struct gr_iface_info_vlan *gr_vlan = NULL; const struct gr_iface_info_port *gr_port = NULL; const struct gr_iface_info_bond *gr_bond = NULL; + const struct gr_iface_info_bridge *gr_bridge = NULL; ifindex_t link_ifindex = IFINDEX_INTERNAL; ifindex_t bond_ifindex = IFINDEX_INTERNAL; + ifindex_t bridge_ifindex = IFINDEX_INTERNAL; + ifindex_t master_ifindex = IFINDEX_INTERNAL; const struct rte_ether_addr *mac = NULL; struct zebra_dplane_ctx *ctx; uint32_t txqlen = 1000; @@ -85,6 +109,13 @@ void grout_link_change(struct gr_iface *gr_if, bool new, bool startup) { link_type = ZEBRA_LLT_ETHER; zif_type = ZEBRA_IF_VRF; break; + case GR_IFACE_TYPE_BRIDGE: + gr_bridge = (struct gr_iface_info_bridge *)&gr_if->info; + link_type = ZEBRA_LLT_ETHER; + zif_type = ZEBRA_IF_BRIDGE; + mac = &gr_bridge->base.mac; + // bridge_ifindex = ifindex_grout_to_frr(gr_if->id); + break; case GR_IFACE_TYPE_UNDEF: default: gr_log_err( @@ -95,6 +126,13 @@ void grout_link_change(struct gr_iface *gr_if, bool new, bool startup) { return; } + if (gr_if->mode == GR_IFACE_MODE_L2_BRIDGE) { + master_ifindex = gr_bridge_id_to_if_index(gr_if->domain_id); + if (master_ifindex == IFINDEX_INTERNAL) + return; + slave_type = ZEBRA_IF_SLAVE_BRIDGE; + } + ctx = dplane_ctx_alloc(); dplane_ctx_set_ns_id(ctx, GROUT_NS); dplane_ctx_set_ifp_link_nsid(ctx, GROUT_NS); @@ -111,9 +149,8 @@ void grout_link_change(struct gr_iface *gr_if, bool new, bool startup) { dplane_ctx_set_status(ctx, ZEBRA_DPLANE_REQUEST_QUEUED); dplane_ctx_set_ifp_mtu(ctx, gr_if->base.mtu); - // no bridge support in grout - dplane_ctx_set_ifp_bridge_ifindex(ctx, IFINDEX_INTERNAL); - dplane_ctx_set_ifp_master_ifindex(ctx, IFINDEX_INTERNAL); + dplane_ctx_set_ifp_bridge_ifindex(ctx, bridge_ifindex); + dplane_ctx_set_ifp_master_ifindex(ctx, master_ifindex); dplane_ctx_set_ifp_bond_ifindex(ctx, bond_ifindex); dplane_ctx_set_ifp_zif_slave_type(ctx, slave_type); dplane_ctx_set_ifp_bypass(ctx, 0); diff --git a/frr/zebra_dplane_grout.c b/frr/zebra_dplane_grout.c index 6d76259aa..c073f57b7 100644 --- a/frr/zebra_dplane_grout.c +++ b/frr/zebra_dplane_grout.c @@ -333,6 +333,8 @@ static const char *gr_req_type_to_str(uint32_t e) { return TOSTRING(GR_IP6_ROUTE_LIST); case GR_SRV6_TUNSRC_SET: return TOSTRING(GR_SRV6_TUNSRC_SET); + case GR_L2_BRIDGE_GET: + return TOSTRING(GR_L2_BRIDGE_GET); default: return "unknown"; } From b73345f1505d17dbe8a38135998e9cab0f0ddbf0 Mon Sep 17 00:00:00 2001 From: Christophe Fontaine Date: Thu, 4 Dec 2025 23:23:17 +0100 Subject: [PATCH 7/7] frr: support GR_EVENT_BRIDGE_MAC_ADD/DEL Register the proper events when a mac is added/migrated/removed to a bridge, and forward the info to zebra. Signed-off-by: Christophe Fontaine --- frr/if_grout.c | 30 ++++++++++++++++++++++++++++++ frr/if_grout.h | 2 ++ frr/zebra_dplane_grout.c | 14 ++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/frr/if_grout.c b/frr/if_grout.c index 18ceb3c55..7343cdce6 100644 --- a/frr/if_grout.c +++ b/frr/if_grout.c @@ -329,3 +329,33 @@ enum zebra_dplane_result grout_set_sr_tunsrc(struct zebra_dplane_ctx *ctx) { return ZEBRA_DPLANE_REQUEST_SUCCESS; } + +void grout_bridge_mac_change(bool new, const struct gr_l2_mac_entry *entry) { + struct zebra_dplane_ctx *ctx = dplane_ctx_alloc(); + struct in_addr vtep_ip = {0}; + struct ethaddr mac; + + memcpy(&mac, entry->mac.addr_bytes, sizeof(mac)); + + dplane_ctx_set_op(ctx, new ? DPLANE_OP_NEIGH_INSTALL : DPLANE_OP_NEIGH_DELETE); + dplane_ctx_set_ns_id(ctx, GROUT_NS); + dplane_ctx_set_ifindex(ctx, ifindex_grout_to_frr(entry->iface_id)); + dplane_ctx_mac_set_addr(ctx, &mac); + dplane_ctx_mac_set_nhg_id(ctx, 0); + dplane_ctx_mac_set_ndm_state(ctx, 0); + dplane_ctx_mac_set_ndm_flags(ctx, 0); + dplane_ctx_mac_set_dst_present(ctx, false); + dplane_ctx_mac_set_vtep_ip(ctx, &vtep_ip); + dplane_ctx_mac_set_vid(ctx, 0); + dplane_ctx_mac_set_dp_static(ctx, entry->age == 0); + dplane_ctx_mac_set_local_inactive(ctx, false); + dplane_ctx_mac_set_vni(ctx, 0); + dplane_ctx_mac_set_is_sticky(ctx, false); + dplane_provider_enqueue_to_zebra(ctx); + zlog_debug( + "grout_bridge_mac_change %s bridge %u iface %u mac", + new ? "add" : "del", + entry->bridge_id, + entry->iface_id + ); +} diff --git a/frr/if_grout.h b/frr/if_grout.h index f4c97f00e..d88ff8394 100644 --- a/frr/if_grout.h +++ b/frr/if_grout.h @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -17,3 +18,4 @@ enum zebra_dplane_result grout_set_sr_tunsrc(struct zebra_dplane_ctx *ctx); void grout_interface_addr4_change(bool new, const struct gr_ip4_ifaddr *ifa); void grout_interface_addr6_change(bool new, const struct gr_ip6_ifaddr *ifa); void grout_link_change(struct gr_iface *gr_if, bool new, bool startup); +void grout_bridge_mac_change(bool new, const struct gr_l2_mac_entry *entry); diff --git a/frr/zebra_dplane_grout.c b/frr/zebra_dplane_grout.c index c073f57b7..31c3fb165 100644 --- a/frr/zebra_dplane_grout.c +++ b/frr/zebra_dplane_grout.c @@ -248,6 +248,8 @@ static void dplane_grout_connect(struct event *) { {.type = GR_EVENT_IP6_ADDR_ADD, .suppress_self_events = false}, {.type = GR_EVENT_IP_ADDR_DEL, .suppress_self_events = false}, {.type = GR_EVENT_IP6_ADDR_DEL, .suppress_self_events = false}, + {.type = GR_EVENT_BRIDGE_MAC_ADD, .suppress_self_events = false}, + {.type = GR_EVENT_BRIDGE_MAC_DEL, .suppress_self_events = false}, }; if (grout_notif_subscribe(&grout_ctx.dplane_notifs, gr_evts, ARRAY_SIZE(gr_evts)) < 0) @@ -415,6 +417,10 @@ static const char *gr_evt_to_str(uint32_t e) { return TOSTRING(GR_EVENT_NEXTHOP_UPDATE); case GR_EVENT_NEXTHOP_DELETE: return TOSTRING(GR_EVENT_NEXTHOP_DELETE); + case GR_EVENT_BRIDGE_MAC_ADD: + return TOSTRING(GR_EVENT_BRIDGE_MAC_ADD); + case GR_EVENT_BRIDGE_MAC_DEL: + return TOSTRING(GR_EVENT_BRIDGE_MAC_DEL); default: return "unknown"; } @@ -423,6 +429,7 @@ static const char *gr_evt_to_str(uint32_t e) { static void dplane_read_notifications(struct event *event) { struct event_loop *dg_master = dplane_get_thread_master(); struct gr_api_event *gr_e = NULL; + struct gr_l2_mac_entry *mac_entry; struct gr_ip4_ifaddr *ifa4; struct gr_ip6_ifaddr *ifa6; struct gr_iface *iface; @@ -483,6 +490,13 @@ static void dplane_read_notifications(struct event *event) { ); grout_interface_addr6_change(new, ifa6); break; + case GR_EVENT_BRIDGE_MAC_ADD: + new = true; + // fallthrough + case GR_EVENT_BRIDGE_MAC_DEL: + mac_entry = PAYLOAD(gr_e); + grout_bridge_mac_change(new, mac_entry); + break; default: gr_log_debug( "Unknown notification %s (0x%x) received",