diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index d7f28342a..cf877d808 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -73,7 +73,7 @@ jobs: libibverbs-dev libasan8 libcmocka-dev libedit-dev libarchive-dev \ libevent-dev libsmartcols-dev libmnl-dev libnuma-dev python3-pyelftools \ socat tcpdump traceroute graphviz iproute2 iputils-ping ndisc6 jq \ - dnsmasq \ + dnsmasq scapy \ "linux-modules-extra-$(uname -r)" if echo $MESON_EXTRA_OPTS | grep -q frr=enabled ; then sudo apt-get install -qy --no-install-recommends \ diff --git a/README.md b/README.md index 7861103e0..5467ffebd 100644 --- a/README.md +++ b/README.md @@ -281,13 +281,13 @@ In order to run the `smoke-tests`, `lint`, `check-patches` and `update-graph` targets, you'll need additional packages: ```sh -dnf install gawk gdb clang-tools-extra jq codespell curl traceroute graphviz ndisc6 +dnf install gawk gdb clang-tools-extra jq codespell curl traceroute graphviz ndisc6 scapy ``` or ```sh -apt install gawk gdb clang-format jq codespell curl traceroute graphviz ndisc6 +apt install gawk gdb clang-format jq codespell curl traceroute graphviz ndisc6 scapy ``` ### Build diff --git a/docs/graph.svg b/docs/graph.svg index 46738fe78..d8c9e2e65 100644 --- a/docs/graph.svg +++ b/docs/graph.svg @@ -1,810 +1,840 @@ - - - - + + + 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 - - + + + + + +srv6_l2_encap + +srv6_l2_encap + + + +port_rx->srv6_l2_encap + + 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 - - + + l2_redirect - -l2_redirect + +l2_redirect lacp_output - -lacp_output + +lacp_output lacp_output->eth_output - - + + - + 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 - - + + ospf_redirect - -ospf_redirect + +ospf_redirect - + ospf_redirect->l2_redirect - - + + + + + +sr6_output + +sr6_output + + + +srv6_l2_encap->sr6_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 - - + + - + ip_input_local->ospf_redirect - - + + - + 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 - - + + - + 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 + + - + 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 - - + + - + ip6_input_local->ospf_redirect - - + + - + icmp6_input - -icmp6_input + +icmp6_input - + ip6_input_local->icmp6_input - - + + - + ip6_input_local->l4_input_local - - + + - + 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->bond_output + + + + + +sr6_local->port_output + + - + 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 - - + + diff --git a/frr/rt_grout.c b/frr/rt_grout.c index b915af2aa..db252c0ad 100644 --- a/frr/rt_grout.c +++ b/frr/rt_grout.c @@ -266,6 +266,9 @@ static int grout_gr_nexthop_to_frr_nexthop( case SR_BEHAVIOR_END_DT46: action = ZEBRA_SEG6_LOCAL_ACTION_END_DT46; break; + case SR_BEHAVIOR_END_DX2: + action = ZEBRA_SEG6_LOCAL_ACTION_END_DX2; + break; } ctx.table = ifindex_grout_to_frr(sr6->out_vrf_id); @@ -726,6 +729,11 @@ grout_add_nexthop(uint32_t nh_id, gr_nh_origin_t origin, const struct nexthop *n nh->nh_srv6->seg6local_ctx.table ); break; + case ZEBRA_SEG6_LOCAL_ACTION_END_DX2: + sr6_local->behavior = SR_BEHAVIOR_END_DX2; + // FIXME + sr6_local->out_vrf_id = ifindex_frr_to_grout(GR_VRF_ID_ALL); + break; default: gr_log_err( "not supported srv6 local behaviour action=%u", diff --git a/modules/infra/api/gr_infra.h b/modules/infra/api/gr_infra.h index 045b5c787..11b87b2c5 100644 --- a/modules/infra/api/gr_infra.h +++ b/modules/infra/api/gr_infra.h @@ -51,6 +51,7 @@ typedef enum : uint16_t { typedef enum : uint8_t { GR_IFACE_MODE_L3 = 0, GR_IFACE_MODE_L1_XC, + GR_IFACE_MODE_SRV6_XC, GR_IFACE_MODE_COUNT } gr_iface_mode_t; @@ -74,6 +75,7 @@ struct __gr_iface_base { uint16_t vrf_id; // L3 addressing and routing domain uint16_t domain_id; // L2 xconnect peer interface id }; + uint32_t mode_data; // Abstract mode data uint32_t speed; // Link speed in Megabit/sec. }; @@ -452,6 +454,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_SRV6_XC: + return "srv6-evpn"; case GR_IFACE_MODE_COUNT: break; } diff --git a/modules/infra/cli/gr_cli_iface.h b/modules/infra/cli/gr_cli_iface.h index 90ee95427..ead9692ae 100644 --- a/modules/infra/cli/gr_cli_iface.h +++ b/modules/infra/cli/gr_cli_iface.h @@ -17,10 +17,21 @@ struct cli_iface_type { void (*list_info)(struct gr_api_client *c, const struct gr_iface *, char *, size_t); }; +struct cli_iface_mode { + STAILQ_ENTRY(cli_iface_mode) next; + gr_iface_mode_t mode_id; + const char *str; + int (*init)(struct ec_node *); + void (*show)(struct gr_api_client *, const struct gr_iface *); + void (*list_info)(struct gr_api_client *, const struct gr_iface *, char *, size_t); +}; + void register_iface_type(struct cli_iface_type *); +void register_iface_mode(struct cli_iface_mode *); const struct cli_iface_type *type_from_name(const char *name); const struct cli_iface_type *type_from_id(gr_iface_type_t type_id); +const struct cli_iface_mode *mode_from_id(gr_iface_mode_t mode_id); struct gr_iface *iface_from_name(struct gr_api_client *c, const char *name); struct gr_iface *iface_from_id(struct gr_api_client *c, uint16_t ifid); @@ -51,7 +62,7 @@ int complete_iface_names( #define INTERFACE_SET_CTX(root) \ CLI_CONTEXT(root, INTERFACE_ARG, CTX_ARG("set", "Modify an existing interface.")) -#define IFACE_ATTRS_CMD "(up|down),(promisc PROMISC),(mtu MTU),(vrf VRF)" +#define IFACE_ATTRS_CMD "(up|down),(promisc PROMISC),(mtu MTU),(vrf VRF),(mode IFACE_MODE)" #define IFACE_ATTRS_ARGS \ with_help("Set the interface UP.", ec_node_str("up", "up")), \ @@ -67,6 +78,10 @@ int complete_iface_names( with_help( \ "L3 addressing/routing domain ID.", \ ec_node_uint("VRF", 0, UINT16_MAX - 1, 10) \ + ), \ + with_help( \ + "interface mode.", \ + EC_NODE_OR("IFACE_MODE", with_help("L3 routing.", ec_node_str("", "l3"))) \ ) uint64_t parse_iface_args( @@ -76,3 +91,12 @@ uint64_t parse_iface_args( size_t info_size, bool update ); + +#define CALLBACK_ATTR_IFACE_MODE "callback_iface_mode" +typedef cmd_status_t (*cmd_iface_set_cb_t)( + struct gr_api_client *, + struct gr_iface *, + const struct ec_pnode *, + uint64_t * +); +struct ec_node *with_iface_set_callback(cmd_iface_set_cb_t cb, struct ec_node *node); diff --git a/modules/infra/cli/iface.c b/modules/infra/cli/iface.c index c1cf79667..034a65a7a 100644 --- a/modules/infra/cli/iface.c +++ b/modules/infra/cli/iface.c @@ -18,11 +18,40 @@ #include static STAILQ_HEAD(, cli_iface_type) types = STAILQ_HEAD_INITIALIZER(types); +static STAILQ_HEAD(, cli_iface_mode) modes = STAILQ_HEAD_INITIALIZER(modes); void register_iface_type(struct cli_iface_type *type) { STAILQ_INSERT_TAIL(&types, type, next); } +void register_iface_mode(struct cli_iface_mode *mode) { + STAILQ_INSERT_TAIL(&modes, mode, next); +} + +struct ec_node *with_iface_set_callback(cmd_iface_set_cb_t cb, struct ec_node *node) { + if (node == NULL) + return NULL; + struct ec_dict *attrs = ec_node_attrs(node); + if (attrs == NULL || ec_dict_set(attrs, CALLBACK_ATTR_IFACE_MODE, cb, NULL) < 0) { + ec_node_free(node); + node = NULL; + } + return node; +} + +static cmd_iface_set_cb_t find_cmd_iface_mode_callback(const struct ec_pnode *parsed) { + const struct ec_pnode *p; + + for (p = parsed; p != NULL; p = EC_PNODE_ITER_NEXT(parsed, p, true)) { + const struct ec_node *node = ec_pnode_get_node(p); + cmd_iface_set_cb_t cb = ec_dict_get(ec_node_attrs(node), CALLBACK_ATTR_IFACE_MODE); + if (cb != NULL) + return cb; + } + + return errno_set_null(EOPNOTSUPP); +} + const struct cli_iface_type *type_from_name(const char *name) { const struct cli_iface_type *type; @@ -57,6 +86,7 @@ int complete_iface_types( } return 0; } + const struct cli_iface_type *type_from_id(gr_iface_type_t type_id) { const struct cli_iface_type *type; @@ -68,6 +98,17 @@ const struct cli_iface_type *type_from_id(gr_iface_type_t type_id) { return NULL; } +const struct cli_iface_mode *mode_from_id(gr_iface_mode_t mode_id) { + const struct cli_iface_mode *mode; + + STAILQ_FOREACH (mode, &modes, next) { + if (mode->mode_id == mode_id) + return mode; + } + errno = ENODEV; + return NULL; +} + int complete_iface_names( struct gr_api_client *c, const struct ec_node *node, @@ -147,6 +188,7 @@ uint64_t parse_iface_args( size_t info_size, bool update ) { + const struct ec_pnode *node; const char *name, *promisc; uint64_t set_attrs = 0; @@ -191,6 +233,19 @@ uint64_t parse_iface_args( if (arg_u16(p, "VRF", &iface->vrf_id) == 0) set_attrs |= GR_IFACE_SET_VRF; + if ((node = ec_pnode_find(p, "IFACE_MODE")) != NULL) { + if (ec_pnode_find(node, "l3")) { + set_attrs |= GR_IFACE_SET_MODE; + set_attrs |= GR_IFACE_SET_VRF; + iface->mode = GR_IFACE_MODE_L3; + iface->vrf_id = 0; + } else { + cmd_iface_set_cb_t cb = find_cmd_iface_mode_callback(node); + if (cb) + cb(c, iface, node, &set_attrs); + } + } + return set_attrs; err: return 0; @@ -232,10 +287,12 @@ static cmd_status_t iface_list(struct gr_api_client *c, const struct ec_pnode *p scols_table_new_column(table, "DOMAIN", 0, 0); scols_table_new_column(table, "TYPE", 0, 0); scols_table_new_column(table, "INFO", 0, 0); + scols_table_new_column(table, "", 0, 0); scols_table_set_column_separator(table, " "); gr_api_client_stream_foreach (iface, ret, c, GR_INFRA_IFACE_LIST, sizeof(req), &req) { const struct cli_iface_type *type = type_from_id(iface->type); + const struct cli_iface_mode *mode = mode_from_id(iface->mode); struct libscols_line *line = scols_table_new_line(table, NULL); char buf[128]; @@ -253,17 +310,8 @@ static cmd_status_t iface_list(struct gr_api_client *c, const struct ec_pnode *p scols_line_set_data(line, 2, buf); // mode - switch (iface->mode) { - case GR_IFACE_MODE_L1_XC: - scols_line_set_data(line, 3, "XC"); - break; - case GR_IFACE_MODE_L3: - scols_line_set_data(line, 3, "L3"); - break; - default: - scols_line_sprintf(line, 3, "%u", iface->mode); - break; - } + assert(mode != NULL); + scols_line_sprintf(line, 3, "%s", mode->str); // vrf scols_line_sprintf(line, 4, "%u", iface->vrf_id); @@ -276,6 +324,11 @@ static cmd_status_t iface_list(struct gr_api_client *c, const struct ec_pnode *p buf[0] = 0; type->list_info(c, iface, buf, sizeof(buf)); scols_line_set_data(line, 6, buf); + if (mode->list_info) { + buf[0] = 0; + mode->list_info(c, iface, buf, sizeof(buf)); + scols_line_set_data(line, 7, buf); + } } scols_print_table(table); @@ -436,6 +489,7 @@ static cmd_status_t iface_rates(struct gr_api_client *c, const struct ec_pnode * static cmd_status_t iface_show(struct gr_api_client *c, const struct ec_pnode *p) { const struct cli_iface_type *type; + const struct cli_iface_mode *mode; char buf[128]; if (arg_str(p, "stats") != NULL) { @@ -473,12 +527,19 @@ static cmd_status_t iface_show(struct gr_api_client *c, const struct ec_pnode *p assert(type != NULL); type->show(c, iface); + mode = mode_from_id(iface->mode); + assert(mode); + if (mode->show) + mode->show(c, iface); + free(iface); return CMD_SUCCESS; } static int ctx_init(struct ec_node *root) { + struct ec_node *iface_mode = NULL; + struct cli_iface_mode *mode; int ret; if (INTERFACE_ADD_CTX(root) == NULL) @@ -487,6 +548,17 @@ static int ctx_init(struct ec_node *root) { if (INTERFACE_SET_CTX(root) == NULL) return -1; + // IFACE_MODE is present many times in the ecoli cli tree: + // It can be configured with "interface add" and "interface set". + // Also, each interface type (port, vlan, ... ) will need to be attached + // all interface modes. + while ((iface_mode = ec_node_find_next(root, iface_mode, "IFACE_MODE"))) { + STAILQ_FOREACH (mode, &modes, next) { + if ((ret = mode->init(iface_mode)) < 0) + return ret; + } + } + ret = CLI_COMMAND( INTERFACE_CTX(root), "del NAME", @@ -584,7 +656,19 @@ static struct cli_event_printer printer = { }, }; +static int l3_mode_init(struct ec_node *) { + // This default l3 mode is handled statically + return 0; +} + +static struct cli_iface_mode iface_mode_l3 = { + .mode_id = GR_IFACE_MODE_L3, + .str = "L3", + .init = l3_mode_init, +}; + static void __attribute__((constructor, used)) init(void) { cli_context_register(&ctx); cli_event_printer_register(&printer); + register_iface_mode(&iface_mode_l3); } diff --git a/modules/infra/cli/port.c b/modules/infra/cli/port.c index 5d14116a9..3ef839fcb 100644 --- a/modules/infra/cli/port.c +++ b/modules/infra/cli/port.c @@ -32,30 +32,15 @@ static void port_show(struct gr_api_client *c, const struct gr_iface *iface) { printf("n_txq: %u\n", port->n_txq); printf("rxq_size: %u\n", port->rxq_size); printf("txq_size: %u\n", port->txq_size); - - if (iface->mode == GR_IFACE_MODE_L1_XC) { - struct gr_iface *peer = iface_from_id(c, iface->domain_id); - if (peer != NULL) - printf("xc_peer: %s\n", peer->name); - else - printf("xc_peer: %u\n", iface->domain_id); - free(peer); - } } static void port_list_info(struct gr_api_client *c, const struct gr_iface *iface, char *buf, size_t len) { const struct gr_iface_info_port *port = (const struct gr_iface_info_port *)iface->info; - struct gr_iface *peer = NULL, *bond = NULL; + struct gr_iface *bond = NULL; size_t n = 0; SAFE_BUF(snprintf, len, "devargs=%s mac=" ETH_F, port->devargs, &port->mac); - if (iface->mode == GR_IFACE_MODE_L1_XC) { - if ((peer = iface_from_id(c, iface->domain_id)) != NULL) - SAFE_BUF(snprintf, len, " xc_peer=%s", peer->name); - else - SAFE_BUF(snprintf, len, " xc_peer=%u", iface->domain_id); - } if (port->bond_iface_id != GR_IFACE_ID_UNDEF) { if ((bond = iface_from_id(c, port->bond_iface_id)) != NULL) SAFE_BUF(snprintf, len, " bond=%s", bond->name); @@ -63,7 +48,6 @@ port_list_info(struct gr_api_client *c, const struct gr_iface *iface, char *buf, SAFE_BUF(snprintf, len, " bond=%u", port->bond_iface_id); } err: - free(peer); free(bond); } @@ -104,25 +88,6 @@ static uint64_t parse_port_args( set_attrs |= GR_PORT_SET_Q_SIZE; } - if (arg_str(p, "l3")) { - set_attrs |= GR_IFACE_SET_MODE; - set_attrs |= GR_IFACE_SET_VRF; - iface->mode = GR_IFACE_MODE_L3; - iface->vrf_id = 0; - } else if (arg_str(p, "xconnect")) { - struct gr_iface *peer = iface_from_name(c, arg_str(p, "PEER")); - if (peer == NULL) { - errno = ENODEV; - goto err; - } - - set_attrs |= GR_IFACE_SET_MODE; - set_attrs |= GR_IFACE_SET_DOMAIN; - iface->mode = GR_IFACE_MODE_L1_XC; - iface->domain_id = peer->id; - free(peer); - } - if (set_attrs == 0) errno = EINVAL; return set_attrs; @@ -180,19 +145,12 @@ static cmd_status_t port_set(struct gr_api_client *c, const struct ec_pnode *p) return ret; } -#define PORT_ATTRS_CMD \ - IFACE_ATTRS_CMD ",(mac MAC),(rxqs N_RXQ),(qsize Q_SIZE),(mode l3|(xconnect PEER))" +#define PORT_ATTRS_CMD IFACE_ATTRS_CMD ",(mac MAC),(rxqs N_RXQ),(qsize Q_SIZE)" #define PORT_ATTRS_ARGS \ IFACE_ATTRS_ARGS, with_help("Set the ethernet address.", ec_node_re("MAC", ETH_ADDR_RE)), \ with_help("Number of Rx queues.", ec_node_uint("N_RXQ", 0, UINT16_MAX - 1, 10)), \ - with_help("Rx/Tx queues size.", ec_node_uint("Q_SIZE", 0, UINT16_MAX - 1, 10)), \ - with_help("mode: \"l3\" or \"xconnect\"", ec_node_str("l3", "l3")), \ - with_help("mode: \"l3\" or \"xconnect\"", ec_node_str("xconnect", "xconnect")), \ - with_help( \ - "Peer interface for xconnect", \ - ec_node_dyn("PEER", complete_iface_names, INT2PTR(GR_IFACE_TYPE_PORT)) \ - ) + with_help("Rx/Tx queues size.", ec_node_uint("Q_SIZE", 0, UINT16_MAX - 1, 10)) static int ctx_init(struct ec_node *root) { int ret; @@ -231,7 +189,89 @@ static struct cli_context ctx = { .init = ctx_init, }; +static cmd_status_t l1_xc_set_peer( + struct gr_api_client *c, + struct gr_iface *iface, + const struct ec_pnode *p, + uint64_t *set_attrs +) { + struct gr_iface *peer = iface_from_name(c, arg_str(p, "PEER")); + cmd_status_t ret = CMD_ERROR; + if (peer == NULL) { + errno = ENODEV; + goto err; + } + + if (iface->type != GR_IFACE_TYPE_PORT || peer->type != GR_IFACE_TYPE_PORT) { + errno = EXDEV; + goto err; + } + + *set_attrs |= GR_IFACE_SET_MODE; + *set_attrs |= GR_IFACE_SET_DOMAIN; + iface->mode = GR_IFACE_MODE_L1_XC; + iface->domain_id = peer->id; + ret = CMD_SUCCESS; +err: + free(peer); + return ret; +} + +static int l1_xc_init(struct ec_node *mode) { + return ec_node_or_add( + mode, + with_help( + "L1 cross connect.", + EC_NODE_SEQ( + "", + ec_node_str("", "xconnect"), + with_iface_set_callback( + l1_xc_set_peer, + with_help( + "Peer interface for xconnect", + ec_node_dyn( + "PEER", + complete_iface_names, + INT2PTR(GR_IFACE_TYPE_PORT) + ) + ) + ) + ) + ) + ); +} + +static void l1_xc_show(struct gr_api_client *c, const struct gr_iface *iface) { + struct gr_iface *peer = iface_from_id(c, iface->domain_id); + if (peer != NULL) + printf("xc_peer: %s\n", peer->name); + else + printf("xc_peer: %u\n", iface->domain_id); + free(peer); +} + +static void +l1_xc_list_info(struct gr_api_client *c, const struct gr_iface *iface, char *buf, size_t len) { + struct gr_iface *peer = NULL; + size_t n = 0; + if ((peer = iface_from_id(c, iface->domain_id)) != NULL) + SAFE_BUF(snprintf, len, " xc_peer=%s", peer->name); + else + SAFE_BUF(snprintf, len, " xc_peer=%u", iface->domain_id); +err: + free(peer); +} + +static struct cli_iface_mode iface_mode_l1xc = { + .mode_id = GR_IFACE_MODE_L1_XC, + .str = "L1 XC", + .init = l1_xc_init, + .show = l1_xc_show, + .list_info = l1_xc_list_info, +}; + static void __attribute__((constructor, used)) init(void) { + register_iface_mode(&iface_mode_l1xc); cli_context_register(&ctx); register_iface_type(&port_type); } diff --git a/modules/infra/control/gr_iface.h b/modules/infra/control/gr_iface.h index ca3b11303..6f80130fd 100644 --- a/modules/infra/control/gr_iface.h +++ b/modules/infra/control/gr_iface.h @@ -18,6 +18,7 @@ struct __rte_cache_aligned iface { gr_vec struct iface **subinterfaces; char *name; + void *mode_info; int cp_id; // Control plane (Linux) port ID int cp_fd; // control plane fd struct event *cp_ev; // libevent to poll cp_fd @@ -57,8 +58,22 @@ struct iface_type { STAILQ_ENTRY(iface_type) next; }; +struct iface_mode { + gr_iface_mode_t id; + int (*init)(struct iface *, const void *api_info); + int (*reconfig)( + struct iface *, + uint64_t set_attrs, + const struct gr_iface *, + const void *api_info + ); + STAILQ_ENTRY(iface_mode) next; +}; + void iface_type_register(struct iface_type *); +void iface_mode_register(struct iface_mode *); const struct iface_type *iface_type_get(gr_iface_type_t type_id); +const struct iface_mode *iface_mode_get(gr_iface_mode_t type_id); struct iface *iface_create(const struct gr_iface *conf, const void *api_info); int iface_reconfig( uint16_t ifid, diff --git a/modules/infra/control/iface.c b/modules/infra/control/iface.c index af08ba1ff..86ab5fd5c 100644 --- a/modules/infra/control/iface.c +++ b/modules/infra/control/iface.c @@ -23,6 +23,7 @@ #include static STAILQ_HEAD(, iface_type) types = STAILQ_HEAD_INITIALIZER(types); +static STAILQ_HEAD(, iface_mode) modes = STAILQ_HEAD_INITIALIZER(modes); struct iface_stats iface_stats[MAX_IFACES][RTE_MAX_LCORE]; @@ -45,6 +46,23 @@ void iface_type_register(struct iface_type *type) { #define IFACE_ID_FIRST GR_IFACE_ID_UNDEF + 1 +const struct iface_mode *iface_mode_get(gr_iface_mode_t mode_id) { + struct iface_mode *m; + STAILQ_FOREACH (m, &modes, next) + if (m->id == mode_id) + return m; + errno = ENODEV; + return NULL; +} + +void iface_mode_register(struct iface_mode *mode) { + if (iface_mode_get(mode->id) != NULL) + ABORT("duplicate iface mode id: %u", mode->id); + if (mode->id >= GR_IFACE_MODE_COUNT) + ABORT("invalid iface mode id: %u", mode->id); + STAILQ_INSERT_TAIL(&modes, mode, next); +} + // the first slot is wasted by GR_IFACE_ID_UNDEF static struct iface **ifaces; @@ -75,12 +93,15 @@ static int next_ifid(uint16_t *ifid) { struct iface *iface_create(const struct gr_iface *conf, const void *api_info) { const struct iface_type *type = iface_type_get(conf->type); + const struct iface_mode *mode = iface_mode_get(conf->mode); struct iface *iface = NULL; bool vrf_ref = false; uint16_t ifid; if (type == NULL) goto fail; + if (mode == NULL) + goto fail; if (charset_check(conf->name, GR_IFACE_NAME_SIZE) < 0) goto fail; while ((iface = iface_next(GR_IFACE_TYPE_UNDEF, iface)) != NULL) { @@ -122,6 +143,9 @@ struct iface *iface_create(const struct gr_iface *conf, const void *api_info) { memset(iface_stats[ifid], 0, sizeof(iface_stats[ifid])); + if (mode->init && mode->init(iface, api_info) < 0) + goto fail; + gr_event_push(GR_EVENT_IFACE_ADD, iface); gr_event_push(GR_EVENT_IFACE_POST_ADD, iface); @@ -205,6 +229,10 @@ int iface_reconfig( if (set_attrs & GR_IFACE_SET_MODE) { iface->mode = conf->mode; + const struct iface_mode *mode = iface_mode_get(conf->mode); + assert(mode != NULL); + if (mode->reconfig) + mode->reconfig(iface, set_attrs, conf, api_info); } if (set_attrs & GR_IFACE_SET_MTU) { @@ -533,7 +561,12 @@ static struct gr_event_subscription iface_event_handler = { }, }; +static struct iface_mode iface_mode_l3 = { + .id = GR_IFACE_MODE_L3, +}; + RTE_INIT(iface_constructor) { + iface_mode_register(&iface_mode_l3); gr_register_module(&iface_module); gr_event_subscribe(&iface_event_handler); } diff --git a/modules/infra/control/xconnect.c b/modules/infra/control/xconnect.c index 2b2f027b4..c2ea92916 100644 --- a/modules/infra/control/xconnect.c +++ b/modules/infra/control/xconnect.c @@ -35,6 +35,11 @@ static struct gr_api_handler l2_mode_set_handler = { .callback = l2_mode_set, }; +static struct iface_mode iface_mode_xconnect = { + .id = GR_IFACE_MODE_L1_XC, +}; + RTE_INIT(l2_constructor) { + iface_mode_register(&iface_mode_xconnect); gr_register_api_handler(&l2_mode_set_handler); } diff --git a/modules/ip/control/icmp.c b/modules/ip/control/icmp.c index e63e60940..112edbe1b 100644 --- a/modules/ip/control/icmp.c +++ b/modules/ip/control/icmp.c @@ -186,9 +186,6 @@ static void icmp_init(struct event_base *) { static void icmp_fini(struct event_base *) { if (pool != NULL) { - struct icmp_queue_item *i, *tmp; - STAILQ_FOREACH_SAFE (i, &icmp_queue, next, tmp) - icmp_queue_pop(i, true); rte_mempool_free(pool); pool = NULL; } diff --git a/modules/ip6/control/icmp6.c b/modules/ip6/control/icmp6.c index fb09905cf..f40539d95 100644 --- a/modules/ip6/control/icmp6.c +++ b/modules/ip6/control/icmp6.c @@ -152,7 +152,7 @@ static struct gr_api_handler ip6_icmp_recv_handler = { #define ICMP6_LOCAL_QUEUE_SIZE 1024 -static void icmp_init(struct event_base *) { +static void icmp6_init(struct event_base *) { pool = rte_mempool_create( "icmp6_queue", ICMP6_LOCAL_QUEUE_SIZE, @@ -170,11 +170,8 @@ static void icmp_init(struct event_base *) { ABORT("rte_mempool_create(icmp6_queue) failed"); } -static void icmp_fini(struct event_base *) { +static void icmp6_fini(struct event_base *) { if (pool != NULL) { - struct icmp_queue_item *i, *tmp; - STAILQ_FOREACH_SAFE (i, &icmp_queue, next, tmp) - icmp6_queue_pop(i, true); rte_mempool_free(pool); pool = NULL; } @@ -182,8 +179,8 @@ static void icmp_fini(struct event_base *) { static struct gr_module icmp6_module = { .name = "icmp6", - .init = icmp_init, - .fini = icmp_fini, + .init = icmp6_init, + .fini = icmp6_fini, }; RTE_INIT(icmp_module_init) { diff --git a/modules/srv6/api/gr_srv6.h b/modules/srv6/api/gr_srv6.h index 86e86b1d5..593f23f2e 100644 --- a/modules/srv6/api/gr_srv6.h +++ b/modules/srv6/api/gr_srv6.h @@ -62,6 +62,7 @@ typedef enum : uint16_t { SR_BEHAVIOR_END_DT6 = 0x0012, // Decaps and IPv6 table lookup. SR_BEHAVIOR_END_DT4 = 0x0013, // Decaps and IPv4 table lookup. SR_BEHAVIOR_END_DT46 = 0x0014, // Decaps and IPv4/IPv6 table lookup. + SR_BEHAVIOR_END_DX2 = 0x0015, // Decaps and output to a specific interface. } gr_srv6_behavior_t; // Convert SRv6 behavior enum to string representation. @@ -77,6 +78,8 @@ static inline const char *gr_srv6_behavior_name(gr_srv6_behavior_t b) { return "end.dt4"; case SR_BEHAVIOR_END_DT46: return "end.dt46"; + case SR_BEHAVIOR_END_DX2: + return "end.dx2"; } return "?"; } diff --git a/modules/srv6/cli/iface_nh.c b/modules/srv6/cli/iface_nh.c new file mode 100644 index 000000000..13482a298 --- /dev/null +++ b/modules/srv6/cli/iface_nh.c @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2025 Christophe Fontaine + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +static void iface_srv6_xc_show(struct gr_api_client *c, const struct gr_iface *iface) { + struct gr_nh_list_req req = {.vrf_id = GR_VRF_ID_ALL, .type = GR_NH_T_SR6_OUTPUT}; + struct gr_nexthop *nh; + size_t len = 256; + char buf[len]; + int ret; + + gr_api_client_stream_foreach (nh, ret, c, GR_NH_LIST, sizeof(req), &req) { + if (nh->nh_id == iface->mode_data) { + cli_nexthop_format(buf, len, c, nh, false); + printf("SRV6 L2 Encap nexthop: %s\n", buf); + } + } +} + +static void iface_srv6_xc_list_info( + struct gr_api_client *c, + const struct gr_iface *iface, + char *buf, + size_t len +) { + struct gr_nh_list_req req = {.vrf_id = GR_VRF_ID_ALL, .type = GR_NH_T_SR6_OUTPUT}; + struct gr_nexthop *nh; + size_t n = 0; + int ret; + + gr_api_client_stream_foreach (nh, ret, c, GR_NH_LIST, sizeof(req), &req) { + if (nh->nh_id == iface->mode_data) { + SAFE_BUF(snprintf, len, "SRV6 L2 Encap: "); + SAFE_BUF(cli_nexthop_format, len, c, nh, false); + } +err: // this label must be in the _foreach call to free all objects + } +} + +static cmd_status_t srv6_l2_encap_set_nh( + struct gr_api_client *, + struct gr_iface *iface, + const struct ec_pnode *p, + uint64_t *set_attrs +) { + uint32_t nh = 0; + if (arg_u32(p, "NH", &nh) < 0) + return CMD_ERROR; + + iface->mode = GR_IFACE_MODE_SRV6_XC; + iface->mode_data = nh; + *set_attrs |= GR_IFACE_SET_MODE; + *set_attrs |= GR_IFACE_SET_DOMAIN; + + return CMD_SUCCESS; +} + +static int srv6_xc_init(struct ec_node *mode) { + return ec_node_or_add( + mode, + with_help( + "srv6 l2vpn", + EC_NODE_SEQ( + "", + ec_node_str("", "srv6-l2vpn"), + with_iface_set_callback( + srv6_l2_encap_set_nh, + with_help( + "Next Hop ID.", + ec_node_uint("NH", 1, UINT32_MAX - 1, 10) + ) + ) + ) + ) + ); +} + +static struct cli_iface_mode iface_mode_srv6_xc = { + .mode_id = GR_IFACE_MODE_SRV6_XC, + .str = "srv6 L2 XC", + .init = srv6_xc_init, + .show = iface_srv6_xc_show, + .list_info = iface_srv6_xc_list_info, +}; + +static void __attribute__((constructor, used)) init(void) { + register_iface_mode(&iface_mode_srv6_xc); +} diff --git a/modules/srv6/cli/localsid.c b/modules/srv6/cli/localsid.c index e0a8657da..6bb73d580 100644 --- a/modules/srv6/cli/localsid.c +++ b/modules/srv6/cli/localsid.c @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -23,6 +24,7 @@ static struct { {SR_BEHAVIOR_END_DT6, "end.dt6"}, {SR_BEHAVIOR_END_DT4, "end.dt4"}, {SR_BEHAVIOR_END_DT46, "end.dt46"}, + {SR_BEHAVIOR_END_DX2, "end.dx2"}, }; static int str_to_behavior(const char *str, gr_srv6_behavior_t *behavior) { @@ -78,9 +80,23 @@ static cmd_status_t srv6_localsid_add(struct gr_api_client *c, const struct ec_p n = ec_pnode_find(p, "BEHAVIOR"); if (n == NULL || ec_pnode_len(n) < 1) goto out; + if (str_to_behavior(ec_strvec_val(ec_pnode_get_strvec(n), 0), &sr6->behavior) < 0) goto out; + if (sr6->behavior == SR_BEHAVIOR_END_DX2) { + const char *iface_name = arg_str(p, "IFACE"); + struct gr_iface *iface; + + if (iface_name == NULL) + goto out; + iface = iface_from_name(c, iface_name); + if (iface == NULL) + goto out; + sr6->out_vrf_id = iface->id; + free(iface); + } + if (arg_u16(n, "TABLE", &sr6->out_vrf_id) < 0 && errno != ENOENT) goto out; @@ -115,6 +131,9 @@ static ssize_t format_nexthop_info_srv6_local(char *buf, size_t len, const void case SR_BEHAVIOR_END_DT46: SAFE_BUF(snprintf, len, " %s", vrf); break; + case SR_BEHAVIOR_END_DX2: + SAFE_BUF(snprintf, len, " out_iface=%d", sr6->out_vrf_id); + break; } return n; err: @@ -162,11 +181,21 @@ static int ctx_init(struct ec_node *root) { ), EC_NODE_CMD( EC_NO_ID, - "(end.dt4|end.dt6|end.dt46) [table TABLE]", + "(end.dx2 IFACE) | ((end.dt4|end.dt6|end.dt46) [table TABLE])", with_help( "L3 routing domain ID.", ec_node_uint("TABLE", 0, UINT16_MAX - 1, 10) ), + with_help( + "Endpoint with decapsulation and specific interface output.", + ec_node_str("end.dx2", "end.dx2") + ), + with_help( + "Interface to send decapsulated frames.", + ec_node_dyn( + "IFACE", complete_iface_names, INT2PTR(GR_IFACE_TYPE_UNDEF) + ) + ), with_help( "Endpoint with decapsulation and specific IPv4 table lookup.", ec_node_str("end.dt4", "end.dt4") diff --git a/modules/srv6/cli/meson.build b/modules/srv6/cli/meson.build index 6c4cb3f72..f048a2012 100644 --- a/modules/srv6/cli/meson.build +++ b/modules/srv6/cli/meson.build @@ -2,6 +2,7 @@ # Copyright (c) 2025 Maxime Leroy, Free Mobile cli_src += files( + 'iface_nh.c', 'localsid.c', 'route.c', ) diff --git a/modules/srv6/control/iface_nh.c b/modules/srv6/control/iface_nh.c new file mode 100644 index 000000000..0bf6978ac --- /dev/null +++ b/modules/srv6/control/iface_nh.c @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2025 Christophe Fontaine + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +static void srv6_iface_nh_cleanup_nexthop(struct nexthop *nh) { + struct iface *iface = NULL; + + if (nh->type != GR_NH_T_SR6_OUTPUT) + return; + + // Iterate over all interfaces + while ((iface = iface_next(GR_IFACE_TYPE_UNDEF, iface)) != NULL) { + if (iface->mode == GR_IFACE_MODE_SRV6_XC) { + if (iface->mode_data == nh->nh_id) { + iface->mode_data = (uintptr_t)0; + iface->mode_info = NULL; + } + } + } +} + +// Event handlers for cleanup +static void srv6_iface_nh_event_handler(uint32_t event, const void *obj) { + switch (event) { + case GR_EVENT_NEXTHOP_DELETE: { + const struct nexthop *nh = obj; + srv6_iface_nh_cleanup_nexthop((struct nexthop *)nh); + break; + } + } +} + +static struct gr_event_subscription srv6_iface_event_sub = { + .callback = srv6_iface_nh_event_handler, + .ev_count = 1, + .ev_types = {GR_EVENT_NEXTHOP_DELETE}, +}; + +static int srv6_l2_encap_mode_init( + struct iface *iface, + const void * /* api_info */ +) { + struct nexthop *nh = nexthop_lookup_by_id(iface->mode_data); + if (nh == NULL) + return -errno; + iface->mode_info = nh; + return 0; +} + +static int srv6_l2_encap_mode_reconfig( + struct iface *iface, + uint64_t /* set_attrs */, + const struct gr_iface *conf, + const void * /* api_info */ +) { + struct nexthop *nh = nexthop_lookup_by_id(conf->mode_data); + if (nh == NULL) + return -errno; + iface->mode_data = conf->mode_data; + iface->mode_info = nh; + return 0; +} + +static struct iface_mode iface_mode_srv6_l2_encap = { + .id = GR_IFACE_MODE_SRV6_XC, + .init = srv6_l2_encap_mode_init, + .reconfig = srv6_l2_encap_mode_reconfig, +}; + +RTE_INIT(srv6_iface_nh_constructor) { + iface_mode_register(&iface_mode_srv6_l2_encap); + gr_event_subscribe(&srv6_iface_event_sub); +} diff --git a/modules/srv6/control/meson.build b/modules/srv6/control/meson.build index 2ba3b9918..3939e684d 100644 --- a/modules/srv6/control/meson.build +++ b/modules/srv6/control/meson.build @@ -2,6 +2,7 @@ # Copyright (c) 2025 Maxime Leroy, Free Mobile src += files( + 'iface_nh.c', 'localsid.c', 'route.c', ) diff --git a/modules/srv6/datapath/l2_encap.c b/modules/srv6/datapath/l2_encap.c new file mode 100644 index 000000000..d1f2ace23 --- /dev/null +++ b/modules/srv6/datapath/l2_encap.c @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2025 Christophe Fontaine + +#include "srv6.h" + +#include +#include +#include + +enum edges { + ENCAP = 0, + NO_DATA, + EDGE_COUNT +}; + +static uint16_t srv6_l2_encap_process( + struct rte_graph *graph, + struct rte_node *node, + void **objs, + uint16_t nb_objs +) { + const struct iface *iface; + struct rte_mbuf *mbuf; + struct nexthop *nh; + + for (uint16_t i = 0; i < nb_objs; i++) { + mbuf = objs[i]; + iface = srv6_dx2_mbuf_data(mbuf)->iface; + + // Look up configured nexthop for this interface + nh = iface->mode_info; + srv6_dx2_mbuf_data(mbuf)->nh = nh; + + // Discard all ptype info to keep only the L2 + mbuf->packet_type = RTE_PTYPE_L2_ETHER; + + if (gr_mbuf_is_traced(mbuf)) + gr_mbuf_trace_add(mbuf, node, 0); + + rte_node_enqueue_x1(graph, node, nh ? ENCAP : NO_DATA, mbuf); + } + + return nb_objs; +} + +static struct rte_node_register srv6_l2_encap_node = { + .name = "srv6_l2_encap", + .process = srv6_l2_encap_process, + .nb_edges = EDGE_COUNT, + .next_nodes = { + [ENCAP] = "sr6_output", + [NO_DATA] = "error_no_encap_data", + }, +}; + +static void srv6_l2_encap_register(void) { + register_interface_mode(GR_IFACE_MODE_SRV6_XC, "srv6_l2_encap"); +} + +static struct gr_node_info info = { + .node = &srv6_l2_encap_node, + .type = GR_NODE_T_L2, + .register_callback = srv6_l2_encap_register, +}; + +GR_NODE_REGISTER(info); +GR_DROP_REGISTER(error_no_encap_data); diff --git a/modules/srv6/datapath/meson.build b/modules/srv6/datapath/meson.build index 994bc6145..acb03264b 100644 --- a/modules/srv6/datapath/meson.build +++ b/modules/srv6/datapath/meson.build @@ -2,6 +2,7 @@ # Copyright (c) 2025 Maxime Leroy src += files( + 'l2_encap.c', 'srv6_local.c', 'srv6_output.c', ) diff --git a/modules/srv6/datapath/srv6.h b/modules/srv6/datapath/srv6.h new file mode 100644 index 000000000..74be7e70f --- /dev/null +++ b/modules/srv6/datapath/srv6.h @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2025 Christophe Fontaine + +#pragma once + +#include +#include + +GR_MBUF_PRIV_DATA_TYPE(srv6_dx2_mbuf_data, { struct nexthop *nh; }) diff --git a/modules/srv6/datapath/srv6_local.c b/modules/srv6/datapath/srv6_local.c index 3e8bbe33a..d03ed91f4 100644 --- a/modules/srv6/datapath/srv6_local.c +++ b/modules/srv6/datapath/srv6_local.c @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -17,6 +18,8 @@ enum { IP_INPUT = 0, IP6_INPUT, IP6_LOCAL, + BOND_OUTPUT, + PORT_OUTPUT, INVALID_PACKET, UNEXPECTED_UPPER, NOT_ALLOWED_UPPER, @@ -227,7 +230,7 @@ static int process_upper_layer(struct rte_mbuf *m, struct ip6_info *ip6_info) { // // Decapsulation behaviors // -static int process_behav_decap( +static int process_behav_decap_ip( struct rte_mbuf *m, struct nexthop_info_srv6_local *sr_d, struct ip6_info *ip6_info @@ -278,6 +281,40 @@ static int process_behav_decap( return edge; } +static int process_behav_decap_eth( + struct rte_mbuf *m, + struct nexthop_info_srv6_local *sr_d, + struct ip6_info *ip6_info +) { + struct rte_ipv6_routing_ext *sr = ip6_info->sr; + const struct iface *iface; + rte_edge_t edge; + + // transit is not allowed + if (sr != NULL && sr->segments_left > 0) + return NO_TRANSIT; + + switch (ip6_info->proto) { + case IPPROTO_ETHERNET: + if (sr_d->behavior != SR_BEHAVIOR_END_DX2) + return UNEXPECTED_UPPER; + iface = iface_from_id(sr_d->out_vrf_id); + if (iface == NULL) + return DEST_UNREACH; + + if (iface->type == GR_IFACE_TYPE_PORT) + edge = PORT_OUTPUT; + else if (iface->type == GR_IFACE_TYPE_BOND) + edge = BOND_OUTPUT; + mbuf_data(m)->iface = iface; + m->hash.usr = rte_be_to_cpu_32(ip6_info->ip6_hdr->vtc_flow) & RTE_IPV6_HDR_FL_MASK; + rte_pktmbuf_adj(m, ip6_info->ext_offset); + return edge; + default: + return process_upper_layer(m, ip6_info); + } +} + // // End behavior // @@ -294,7 +331,7 @@ static int process_behav_end( // 4.16.3 USD // this packet could be decapsulated and forwarded if (sr_d->flags & GR_SR_FL_FLAVOR_USD) - return process_behav_decap(m, sr_d, ip6_info); + return process_behav_decap_ip(m, sr_d, ip6_info); // process locally return process_upper_layer(m, ip6_info); @@ -336,7 +373,10 @@ static inline rte_edge_t srv6_local_process_pkt( case SR_BEHAVIOR_END_DT4: case SR_BEHAVIOR_END_DT6: case SR_BEHAVIOR_END_DT46: - return process_behav_decap(m, sr_d, ip6_info); + return process_behav_decap_ip(m, sr_d, ip6_info); + + case SR_BEHAVIOR_END_DX2: + return process_behav_decap_eth(m, sr_d, ip6_info); default: return INVALID_PACKET; @@ -394,6 +434,8 @@ static struct rte_node_register srv6_local_node = { [IP_INPUT] = "ip_input", [IP6_INPUT] = "ip6_input", [IP6_LOCAL] = "ip6_input_local", + [PORT_OUTPUT] = "port_output", + [BOND_OUTPUT] = "bond_output", [INVALID_PACKET] = "sr6_local_invalid", [UNEXPECTED_UPPER] = "sr6_local_unexpected_upper", [NOT_ALLOWED_UPPER] = "sr6_local_not_allowed_upper", @@ -427,6 +469,7 @@ mock_func(uint16_t, drop_packets(struct rte_graph *, struct rte_node *, void **, mock_func(int, drop_format(char *, size_t, const void *, size_t)); mock_func(void, ip6_input_register_nexthop_type(gr_nh_type_t, const char *)); mock_func(struct iface *, get_vrf_iface(uint16_t)); +mock_func(struct iface *, iface_from_id(uint16_t)); struct ipv6_ext_base { uint8_t next_hdr; diff --git a/modules/srv6/datapath/srv6_output.c b/modules/srv6/datapath/srv6_output.c index 54d033f33..77aee9aff 100644 --- a/modules/srv6/datapath/srv6_output.c +++ b/modules/srv6/datapath/srv6_output.c @@ -1,6 +1,8 @@ // SPDX-License-Identifier: BSD-3-Clause // Copyright (c) 2025 Olivier Gournet +#include "srv6.h" + #include #include #include @@ -85,7 +87,10 @@ srv6_output_process(struct rte_graph *graph, struct rte_node *node, void **objs, inner_ip6 = rte_pktmbuf_mtod(m, struct rte_ipv6_hdr *); plen = rte_be_to_cpu_16(inner_ip6->payload_len); proto = IPPROTO_IPV6; - + } else if (m->packet_type & RTE_PTYPE_L2_ETHER) { + nh = srv6_dx2_mbuf_data(m)->nh; + plen = rte_pktmbuf_data_len(m); + proto = IPPROTO_ETHERNET; } else { edge = INVALID; goto next; diff --git a/smoke/srv6_l2_encap_scapy.py b/smoke/srv6_l2_encap_scapy.py new file mode 100644 index 000000000..01ad6447d --- /dev/null +++ b/smoke/srv6_l2_encap_scapy.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2025 Christophe Fontaine + +from scapy.all import * +from scapy.layers.inet import IP, ICMP +from scapy.layers.inet6 import IPv6, ICMPv6EchoReply, IPv6ExtHdrSegmentRouting + +# Interface where End.DX2 delivers Ethernet frames +IFACE = "veth0" +SR6IFACE = "x-p1" + +# SRv6 parameters +SRC_IPV6 = "2001:db8:1::2" +SEGMENTS = ["5f00::"] # SID list (last segment first per SRH rules) + + +def handle(pkt): + if pkt.getlayer(IPv6ExtHdrSegmentRouting): + return + + srh = IPv6ExtHdrSegmentRouting( + segleft=len(SEGMENTS) - 1, + addresses=SEGMENTS, + nh = 143 + ) + + outer_ip6 = IPv6( + src=SRC_IPV6, + dst=SEGMENTS[0] + ) / srh / pkt + + sendp(Ether() / outer_ip6, iface=SR6IFACE, verbose=False) + + +sniff(iface=IFACE, prn=handle, store=False) diff --git a/smoke/srv6_l2_encap_test.sh b/smoke/srv6_l2_encap_test.sh new file mode 100755 index 000000000..c79ebe4aa --- /dev/null +++ b/smoke/srv6_l2_encap_test.sh @@ -0,0 +1,64 @@ +#!/bin/bash +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2025 Christophe Fontaine + +. $(dirname $0)/_init.sh + +# Using 5f00::/16 as reserved for SRv6 +FUNC_DX2_NS0=5f00:: +FUNC_DX2_NS1=5f00:1:: + +port_add p0 +port_add p1 mtu 1600 + +for n in 0 1; do + p=x-p$n + ns=n$n + netns_add $ns + move_to_netns $p $ns +done + +ip -n n0 addr add 172.16.0.2/24 dev x-p0 +grcli address add 2001:db8:1::1/64 iface p1 +ip -n n1 addr add 2001:db8:1::2/64 dev x-p1 + +netns_add n2 +ip link add veth0 type veth peer name veth1 +ip link set netns n1 dev veth0 +ip -n n1 link set veth0 up + +ip link set netns n2 dev veth1 +ip -n n2 link set veth1 up + +ip -n n2 addr add 172.16.0.1/24 dev veth1 + +grcli nexthop add srv6 seglist $FUNC_DX2_NS1 encap h.encaps id 26 +grcli interface set port p0 mode srv6-l2vpn 26 + +grcli nexthop add srv6-local behavior end.dx2 p0 id 2 +grcli route add $FUNC_DX2_NS0/64 via id 2 +grcli route add $FUNC_DX2_NS1/64 via 2001:db8:1::2 + +ip netns exec n1 sysctl -wq net.ipv6.conf.all.accept_dad=0 +ip netns exec n1 sysctl -wq net.ipv6.conf.default.accept_dad=0 +ip netns exec n1 sysctl -wq net.ipv6.conf.all.forwarding=1 +ip netns exec n1 sysctl -wq net.ipv4.conf.all.rp_filter=0 +ip netns exec n1 sysctl -wq net.ipv4.conf.default.rp_filter=0 +ip netns exec n1 sysctl -wq net.ipv4.conf.all.forwarding=1 +ip netns exec n1 sysctl -wq net.ipv4.ip_forward=1 + +ip -n n1 -6 route add $FUNC_DX2_NS1/128 encap seg6local action End.DX2 oif veth0 dev x-p1 +ip -n n1 -6 route add $FUNC_DX2_NS0/128 via 2001:db8:1::1 +ip -n n1 -6 route add ::/0 via 2001:db8:1::1 + +ip netns exec n1 python3 smoke/srv6_l2_encap_scapy.py & +SCAPY_PID=$! +sleep 3 + +# Verify the scapy script is running +if ! kill -0 $SCAPY_PID 2>/dev/null; then + echo "ERROR: SRv6 encapsulation script failed to start" + exit 1 +fi + +ip netns exec n0 ping -i0.1 -c3 172.16.0.1