diff --git a/api/gr_net_types.h b/api/gr_net_types.h index efaf0c830..079bb9566 100644 --- a/api/gr_net_types.h +++ b/api/gr_net_types.h @@ -27,6 +27,7 @@ typedef enum : uint8_t { GR_AF_UNSPEC = AF_UNSPEC, GR_AF_IP4 = AF_INET, GR_AF_IP6 = AF_INET6, + GR_AF_MPLS = AF_MPLS, } addr_family_t; // Convert address family enum to string representation. @@ -38,6 +39,8 @@ static inline const char *gr_af_name(addr_family_t af) { return "ipv4"; case GR_AF_IP6: return "ipv6"; + case GR_AF_MPLS: + return "mpls"; } return "?"; } @@ -48,6 +51,7 @@ static inline bool gr_af_valid(addr_family_t af) { case GR_AF_UNSPEC: case GR_AF_IP4: case GR_AF_IP6: + case GR_AF_MPLS: return true; } return false; diff --git a/modules/infra/api/gr_nexthop.h b/modules/infra/api/gr_nexthop.h index cbc4e7939..fd2deb881 100644 --- a/modules/infra/api/gr_nexthop.h +++ b/modules/infra/api/gr_nexthop.h @@ -37,6 +37,7 @@ typedef enum : uint8_t { GR_NH_T_BLACKHOLE, // Drop packets silently. GR_NH_T_REJECT, // Drop packets with ICMP error. GR_NH_T_GROUP, // ECMP for multipath routing. + GR_NH_T_MPLS, // MPLS label imposition/swap. #define GR_NH_T_ALL UINT8_C(0xff) // Match all types in list operations. } gr_nh_type_t; @@ -200,6 +201,8 @@ static inline const char *gr_nh_type_name(const gr_nh_type_t type) { return "reject"; case GR_NH_T_GROUP: return "group"; + case GR_NH_T_MPLS: + return "MPLS"; } return "?"; } diff --git a/modules/infra/control/l3_nexthop.c b/modules/infra/control/l3_nexthop.c index 79258d54b..f333ef39e 100644 --- a/modules/infra/control/l3_nexthop.c +++ b/modules/infra/control/l3_nexthop.c @@ -46,6 +46,8 @@ const struct nexthop_af_ops *nexthop_af_ops_from_mbuf(const struct rte_mbuf *m) return af_ops[GR_AF_IP4]; if (m->packet_type & RTE_PTYPE_L3_IPV6) return af_ops[GR_AF_IP6]; + if ((m->packet_type & RTE_PTYPE_TUNNEL_MASK) == RTE_PTYPE_TUNNEL_MPLS_IN_GRE) + return af_ops[GR_AF_MPLS]; return NULL; } @@ -79,6 +81,9 @@ static inline void set_nexthop_key( key->ipv6.a[3] = iface_id & 0xff; } break; + case GR_AF_MPLS: + ABORT("AF_MPLS has no nexthop key with gw"); + break; case GR_AF_UNSPEC: ABORT("AF_UNSPEC has no nexthop key with gw"); break; @@ -189,6 +194,7 @@ static bool l3_equal(const struct nexthop *a, const struct nexthop *b) { case GR_AF_IP6: return rte_ipv6_addr_eq(&l3_a->ipv6, &l3_b->ipv6); case GR_AF_UNSPEC: + case GR_AF_MPLS: return true; } return false; diff --git a/modules/infra/control/nexthop.c b/modules/infra/control/nexthop.c index 1dcd6c0e2..e72579ab6 100644 --- a/modules/infra/control/nexthop.c +++ b/modules/infra/control/nexthop.c @@ -236,6 +236,7 @@ bool nexthop_type_valid(gr_nh_type_t type) { case GR_NH_T_BLACKHOLE: case GR_NH_T_REJECT: case GR_NH_T_GROUP: + case GR_NH_T_MPLS: return true; } return false; diff --git a/modules/infra/control/vrf.h b/modules/infra/control/vrf.h index 992670c1c..1db134d9d 100644 --- a/modules/infra/control/vrf.h +++ b/modules/infra/control/vrf.h @@ -15,6 +15,7 @@ GR_IFACE_INFO(GR_IFACE_TYPE_VRF, iface_info_vrf, { uint32_t vrf_ifindex; void *fib4; void *fib6; + void *fib_mpls; }); // Increment VRF reference count. diff --git a/modules/infra/datapath/checksum.h b/modules/infra/datapath/checksum.h new file mode 100644 index 000000000..71387f560 --- /dev/null +++ b/modules/infra/datapath/checksum.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2025 Matej Muzila + +#pragma once + +#include + +// RFC 1624 incremental checksum update for a 16-bit field. +static inline rte_be16_t +fixup_checksum_16(rte_be16_t old_cksum, rte_be16_t old_field, rte_be16_t new_field) { + uint32_t sum; + + sum = ~old_cksum & 0xffff; + sum += (~old_field & 0xffff) + new_field; + sum = (sum >> 16) + (sum & 0xffff); + sum += (sum >> 16); + + return ~sum & 0xffff; +} + +// RFC 1624 incremental checksum update for a 32-bit field. +static inline rte_be16_t +fixup_checksum_32(rte_be16_t old_cksum, ip4_addr_t old_addr, ip4_addr_t new_addr) { + uint32_t sum; + + sum = ~old_cksum & 0xffff; + sum += (~old_addr & 0xffff) + (new_addr & 0xffff); + sum += (~old_addr >> 16) + (new_addr >> 16); + sum = (sum >> 16) + (sum & 0xffff); + sum += (sum >> 16); + + return ~sum & 0xffff; +} diff --git a/modules/l2/control/vxlan.c b/modules/l2/control/vxlan.c index 666ca2129..9b63b03ee 100644 --- a/modules/l2/control/vxlan.c +++ b/modules/l2/control/vxlan.c @@ -197,6 +197,7 @@ static int iface_vxlan_reconfig( cur->template.ipv6.vxlan.vx_vni = vxlan_encode_vni(cur->vni); break; case GR_AF_UNSPEC: + case GR_AF_MPLS: break; } diff --git a/modules/meson.build b/modules/meson.build index 0b978a62b..8ddb1b3a2 100644 --- a/modules/meson.build +++ b/modules/meson.build @@ -5,6 +5,7 @@ subdir('infra') subdir('ip') subdir('ip6') subdir('ipip') +subdir('mpls') subdir('l2') subdir('l4') subdir('policy') diff --git a/modules/mpls/api/gr_mpls.h b/modules/mpls/api/gr_mpls.h new file mode 100644 index 000000000..a5b590516 --- /dev/null +++ b/modules/mpls/api/gr_mpls.h @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2025 Matej Muzila + +#pragma once + +#include +#include +#include + +#include + +#define GR_MPLS_MODULE 0xf00e + +#define GR_MPLS_MAX_LABELS 16 + +#define GR_MPLS_MAX_STACK_DEPTH 30 + +#define GR_MPLS_LABEL_MAX 0xFFFFF + +// Reserved MPLS label values (RFC 3032). +enum gr_mpls_reserved_labels : uint32_t { + GR_MPLS_LABEL_IPV4_EXPLICIT_NULL = 0, + GR_MPLS_LABEL_ROUTER_ALERT = 1, + GR_MPLS_LABEL_IPV6_EXPLICIT_NULL = 2, + GR_MPLS_LABEL_IMPLICIT_NULL = 3, + GR_MPLS_LABEL_FIRST_UNRESERVED = 16, +}; + +struct gr_nexthop_info_mpls { + uint8_t n_labels; + uint8_t ttl; // 0 = copy from payload. + addr_family_t payload_af; // Payload type after pop (GR_AF_UNSPEC = auto-detect). + struct l3_addr via; + uint32_t labels[GR_MPLS_MAX_LABELS]; +}; + +// label routes +enum gr_mpls_requests : uint32_t { + GR_MPLS_LABEL_ROUTE_ADD = GR_MSG_TYPE(GR_MPLS_MODULE, 0x0001), + GR_MPLS_LABEL_ROUTE_DEL, + GR_MPLS_LABEL_ROUTE_GET, + GR_MPLS_LABEL_ROUTE_LIST, +}; + +// MPLS label route entry. +struct gr_mpls_label_route { + uint16_t vrf_id; + uint32_t in_label; + uint32_t nh_id; + gr_nh_origin_t origin; +}; + +// Add a label route to the LFIB. +struct gr_mpls_label_route_add_req { + uint16_t vrf_id; + uint32_t in_label; // 0 to GR_MPLS_LABEL_MAX. + uint32_t nh_id; // Must reference a GR_NH_T_MPLS nexthop. + gr_nh_origin_t origin; + uint8_t exist_ok; +}; + +GR_REQ(GR_MPLS_LABEL_ROUTE_ADD, struct gr_mpls_label_route_add_req, struct gr_empty); + +// Delete a label route from the LFIB. +struct gr_mpls_label_route_del_req { + uint16_t vrf_id; + uint32_t in_label; + uint8_t missing_ok; +}; + +GR_REQ(GR_MPLS_LABEL_ROUTE_DEL, struct gr_mpls_label_route_del_req, struct gr_empty); + +// Get a single label route by label value. +struct gr_mpls_label_route_get_req { + uint16_t vrf_id; + uint32_t in_label; +}; + +GR_REQ(GR_MPLS_LABEL_ROUTE_GET, struct gr_mpls_label_route_get_req, struct gr_mpls_label_route); + +// List all label routes in a VRF. +struct gr_mpls_label_route_list_req { + uint16_t vrf_id; + uint16_t max_count; +}; + +GR_REQ_STREAM( + GR_MPLS_LABEL_ROUTE_LIST, + struct gr_mpls_label_route_list_req, + struct gr_mpls_label_route +); + +// events + +enum gr_mpls_events : uint32_t { + GR_EVENT_MPLS_ROUTE_ADD = GR_MSG_TYPE(GR_MPLS_MODULE, 0x1001), + GR_EVENT_MPLS_ROUTE_DEL, +}; + +GR_EVENT(GR_EVENT_MPLS_ROUTE_ADD, struct gr_mpls_label_route); +GR_EVENT(GR_EVENT_MPLS_ROUTE_DEL, struct gr_mpls_label_route); diff --git a/modules/mpls/api/meson.build b/modules/mpls/api/meson.build new file mode 100644 index 000000000..c15b0d016 --- /dev/null +++ b/modules/mpls/api/meson.build @@ -0,0 +1,5 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2025 Matej Muzila + +api_headers += files('gr_mpls.h') +api_inc += include_directories('.') diff --git a/modules/mpls/cli/label.c b/modules/mpls/cli/label.c new file mode 100644 index 000000000..4ba87e64c --- /dev/null +++ b/modules/mpls/cli/label.c @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2025 Matej Muzila + +#include "cli.h" +#include "cli_event.h" +#include "cli_iface.h" +#include "display.h" + +#include +#include +#include + +#include + +#include + +static cmd_status_t mpls_route_add(struct gr_api_client *c, const struct ec_pnode *p) { + struct gr_mpls_label_route_add_req req = { + .exist_ok = true, + .origin = GR_NH_ORIGIN_STATIC, + }; + + if (arg_u32(p, "LABEL", &req.in_label) < 0) + return CMD_ERROR; + if (arg_u32(p, "ID", &req.nh_id) < 0) + return CMD_ERROR; + if (arg_vrf(c, p, "VRF", &req.vrf_id) < 0) + return CMD_ERROR; + + if (gr_api_client_send_recv(c, GR_MPLS_LABEL_ROUTE_ADD, sizeof(req), &req, NULL) < 0) + return CMD_ERROR; + + return CMD_SUCCESS; +} + +static cmd_status_t mpls_route_del(struct gr_api_client *c, const struct ec_pnode *p) { + struct gr_mpls_label_route_del_req req = {.missing_ok = true}; + + if (arg_u32(p, "LABEL", &req.in_label) < 0) + return CMD_ERROR; + if (arg_vrf(c, p, "VRF", &req.vrf_id) < 0) + return CMD_ERROR; + + if (gr_api_client_send_recv(c, GR_MPLS_LABEL_ROUTE_DEL, sizeof(req), &req, NULL) < 0) + return CMD_ERROR; + + return CMD_SUCCESS; +} + +static cmd_status_t mpls_route_get(struct gr_api_client *c, const struct ec_pnode *p) { + const struct gr_mpls_label_route *resp; + struct gr_mpls_label_route_get_req req = {0}; + void *resp_ptr = NULL; + + if (arg_u32(p, "LABEL", &req.in_label) < 0) + return CMD_ERROR; + if (arg_vrf(c, p, "VRF", &req.vrf_id) < 0) + return CMD_ERROR; + + if (gr_api_client_send_recv(c, GR_MPLS_LABEL_ROUTE_GET, sizeof(req), &req, &resp_ptr) < 0) + return CMD_ERROR; + + resp = resp_ptr; + struct gr_object *o = gr_object_new(NULL); + gr_object_field(o, "vrf", 0, "%s", iface_name_from_id(c, resp->vrf_id)); + gr_object_field(o, "label", 0, "%u", resp->in_label); + gr_object_field(o, "nexthop_id", 0, "%u", resp->nh_id); + gr_object_field(o, "origin", 0, "%s", gr_nh_origin_name(resp->origin)); + gr_object_free(o); + free(resp_ptr); + + return CMD_SUCCESS; +} + +static cmd_status_t mpls_route_list(struct gr_api_client *c, const struct ec_pnode *p) { + uint16_t vrf_id = GR_VRF_ID_UNDEF; + uint16_t max_routes = 1000; + const struct gr_mpls_label_route *route; + int ret; + + if (arg_str(p, "VRF") != NULL && arg_vrf(c, p, "VRF", &vrf_id) < 0) + return CMD_ERROR; + if (arg_u16(p, "MAX", &max_routes) < 0 && errno != ENOENT) + return CMD_ERROR; + + struct gr_mpls_label_route_list_req req = { + .vrf_id = vrf_id, + .max_count = max_routes, + }; + + struct gr_table *table = gr_table_new(); + gr_table_column(table, "VRF", GR_DISP_LEFT); + gr_table_column(table, "LABEL", GR_DISP_RIGHT); + gr_table_column(table, "NEXTHOP_ID", GR_DISP_RIGHT); + gr_table_column(table, "ORIGIN", GR_DISP_LEFT); + + gr_api_client_stream_foreach (route, ret, c, GR_MPLS_LABEL_ROUTE_LIST, sizeof(req), &req) { + gr_table_cell(table, 0, "%s", iface_name_from_id(c, route->vrf_id)); + gr_table_cell(table, 1, "%u", route->in_label); + gr_table_cell(table, 2, "%u", route->nh_id); + gr_table_cell(table, 3, "%s", gr_nh_origin_name(route->origin)); + + if (gr_table_print_row(table) < 0) + break; + } + + gr_table_free(table); + + if (ret < 0 && errno == EXFULL) { + warnf("more routes not displayed"); + ret = 0; + } + + return ret < 0 ? CMD_ERROR : CMD_SUCCESS; +} + +#define MPLS_CTX(root) CLI_CONTEXT(root, CTX_ARG("mpls", "MPLS label switching.")) +#define MPLS_ROUTE_CTX(root) CLI_CONTEXT(MPLS_CTX(root), CTX_ARG("route", "Label routes.")) + +static int ctx_init(struct ec_node *root) { + int ret; + + ret = CLI_COMMAND( + MPLS_ROUTE_CTX(root), + "add LABEL nexthop id ID [vrf VRF]", + mpls_route_add, + "Add a label route.", + with_help("MPLS label value (0-1048575).", ec_node_uint("LABEL", 0, 1048575, 10)), + with_help("Nexthop user ID.", ec_node_uint("ID", 1, UINT32_MAX - 1, 10)), + with_help("L3 routing domain name.", ec_node_dyn("VRF", complete_vrf_names, NULL)) + ); + if (ret < 0) + return ret; + ret = CLI_COMMAND( + MPLS_ROUTE_CTX(root), + "del LABEL [vrf VRF]", + mpls_route_del, + "Delete a label route.", + with_help("MPLS label value.", ec_node_uint("LABEL", 0, 1048575, 10)), + with_help("L3 routing domain name.", ec_node_dyn("VRF", complete_vrf_names, NULL)) + ); + if (ret < 0) + return ret; + ret = CLI_COMMAND( + MPLS_ROUTE_CTX(root), + "get LABEL [vrf VRF]", + mpls_route_get, + "Get a label route.", + with_help("MPLS label value.", ec_node_uint("LABEL", 0, 1048575, 10)), + with_help("L3 routing domain name.", ec_node_dyn("VRF", complete_vrf_names, NULL)) + ); + if (ret < 0) + return ret; + ret = CLI_COMMAND( + MPLS_ROUTE_CTX(root), + "[show] [(vrf VRF),(max MAX)]", + mpls_route_list, + "Show label routes.", + with_help( + "Max. number of routes to display (default 1000, use 0 for unlimited).", + ec_node_uint("MAX", 0, UINT16_MAX, 10) + ), + with_help("L3 routing domain name.", ec_node_dyn("VRF", complete_vrf_names, NULL)) + ); + if (ret < 0) + return ret; + + return 0; +} + +static void mpls_event_print(uint32_t event, const void *obj) { + const struct gr_mpls_label_route *r = obj; + const char *action; + + switch (event) { + case GR_EVENT_MPLS_ROUTE_ADD: + action = "add"; + break; + case GR_EVENT_MPLS_ROUTE_DEL: + action = "del"; + break; + default: + action = "?"; + break; + } + + printf("mpls route %s: vrf=%s label=%u nh_id=%u origin=%s\n", + action, + iface_name_from_id(NULL, r->vrf_id), + r->in_label, + r->nh_id, + gr_nh_origin_name(r->origin)); +} + +static struct cli_event_printer printer = { + .name = "mpls_route", + .print = mpls_event_print, + .ev_count = 2, + .ev_types = { + GR_EVENT_MPLS_ROUTE_ADD, + GR_EVENT_MPLS_ROUTE_DEL, + }, +}; + +static struct cli_context ctx = { + .name = "mpls", + .init = ctx_init, +}; + +static void __attribute__((constructor, used)) init(void) { + cli_context_register(&ctx); + cli_event_printer_register(&printer); +} diff --git a/modules/mpls/cli/meson.build b/modules/mpls/cli/meson.build new file mode 100644 index 000000000..d28bae09e --- /dev/null +++ b/modules/mpls/cli/meson.build @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2025 Matej Muzila + +cli_src += files( + 'label.c', + 'nexthop.c', +) +cli_inc += include_directories('.') diff --git a/modules/mpls/cli/nexthop.c b/modules/mpls/cli/nexthop.c new file mode 100644 index 000000000..c6fadb1b6 --- /dev/null +++ b/modules/mpls/cli/nexthop.c @@ -0,0 +1,191 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2025 Matej Muzila + +#include "cli.h" +#include "cli_iface.h" +#include "cli_nexthop.h" +#include "display.h" + +#include +#include +#include + +#include + +#include + +static cmd_status_t nh_mpls_add(struct gr_api_client *c, const struct ec_pnode *p) { + struct gr_nexthop_info_mpls *info; + struct gr_nh_add_req *req = NULL; + cmd_status_t ret = CMD_ERROR; + const struct ec_pnode *n; + size_t len; + + len = sizeof(*req) + sizeof(*info); + req = calloc(1, len); + if (req == NULL) + goto out; + + req->exist_ok = true; + req->nh.type = GR_NH_T_MPLS; + req->nh.origin = GR_NH_ORIGIN_STATIC; + + if (arg_u32(p, "ID", &req->nh.nh_id) < 0 && errno != ENOENT) + goto out; + if (arg_iface(c, p, "IFACE", GR_IFACE_TYPE_UNDEF, &req->nh.iface_id) < 0) + goto out; + + info = (struct gr_nexthop_info_mpls *)req->nh.info; + + switch (arg_ip4(p, "VIA", &info->via.ipv4)) { + case 0: + info->via.af = GR_AF_IP4; + break; + case -EINVAL: + if (arg_ip6(p, "VIA", &info->via.ipv6) < 0) + goto out; + info->via.af = GR_AF_IP6; + break; + default: + errno = EINVAL; + goto out; + } + + if (arg_u8(p, "TTL", &info->ttl) < 0 && errno != ENOENT) + goto out; + + if (arg_str(p, "ipv4") != NULL) + info->payload_af = GR_AF_IP4; + else if (arg_str(p, "ipv6") != NULL) + info->payload_af = GR_AF_IP6; + + n = ec_pnode_find(p, "LABEL"); + if (n != NULL) { + n = ec_pnode_get_parent(n); + if (n == NULL || ec_pnode_len(n) < 1) { + errno = EINVAL; + goto out; + } + if (ec_pnode_len(n) > GR_MPLS_MAX_LABELS) { + errno = E2BIG; + goto out; + } + for (n = ec_pnode_get_first_child(n); n != NULL; n = ec_pnode_next(n)) { + const char *str = ec_strvec_val(ec_pnode_get_strvec(n), 0); + unsigned long val = strtoul(str, NULL, 10); + if (val > GR_MPLS_LABEL_MAX) { + errno = EINVAL; + goto out; + } + info->labels[info->n_labels++] = (uint32_t)val; + } + } + + if (gr_api_client_send_recv(c, GR_NH_ADD, len, req, NULL) < 0) + goto out; + + ret = CMD_SUCCESS; +out: + free(req); + return ret; +} + +static void add_columns_mpls(struct gr_table *table) { + gr_table_column(table, "VIA", GR_DISP_LEFT); + gr_table_column(table, "LABELS", GR_DISP_STR_ARRAY); + gr_table_column(table, "TTL", GR_DISP_RIGHT); +} + +static void fill_table_mpls(struct gr_table *table, unsigned start_col, const void *nexthop_info) { + const struct gr_nexthop_info_mpls *info = nexthop_info; + char buf[256] = ""; + ssize_t n = 0; + + if (info->via.af == GR_AF_IP4) + gr_table_cell(table, start_col, IP4_F, &info->via.ipv4); + else if (info->via.af == GR_AF_IP6) + gr_table_cell(table, start_col, IP6_F, &info->via.ipv6); + else + gr_table_cell(table, start_col, "-"); + + for (uint8_t i = 0; i < info->n_labels; i++) { + SAFE_BUF(snprintf, sizeof(buf), "%s%u", i > 0 ? " " : "", info->labels[i]); + if (sizeof(buf) - n < 20) { + SAFE_BUF(snprintf, sizeof(buf), " ... (%u more)", info->n_labels - i - 1); + break; + } + } +err: + if (info->n_labels > 0 && n > 0) + gr_table_cell(table, start_col + 1, "%s", buf); + else + gr_table_cell(table, start_col + 1, "(pop)"); + + if (info->ttl > 0) + gr_table_cell(table, start_col + 2, "%u", info->ttl); + else + gr_table_cell(table, start_col + 2, "-"); +} + +static void fill_object_mpls(struct gr_object *o, const void *nexthop_info) { + const struct gr_nexthop_info_mpls *info = nexthop_info; + + if (info->via.af == GR_AF_IP4) + gr_object_field(o, "via", 0, IP4_F, &info->via.ipv4); + else if (info->via.af == GR_AF_IP6) + gr_object_field(o, "via", 0, IP6_F, &info->via.ipv6); + + if (info->n_labels > 0) { + gr_object_array_open(o, "labels"); + for (uint8_t i = 0; i < info->n_labels; i++) + gr_object_array_item(o, GR_DISP_INT, "%u", info->labels[i]); + gr_object_array_close(o); + } else { + gr_object_field(o, "action", 0, "pop"); + } + + if (info->ttl > 0) + gr_object_field(o, "ttl", GR_DISP_INT, "%u", info->ttl); + if (info->payload_af != GR_AF_UNSPEC) + gr_object_field(o, "payload", 0, "%s", gr_af_name(info->payload_af)); +} + +static struct cli_nexthop_formatter mpls_formatter = { + .name = "mpls", + .type = GR_NH_T_MPLS, + .add_columns = add_columns_mpls, + .fill_table = fill_table_mpls, + .fill_object = fill_object_mpls, +}; + +static int ctx_init(struct ec_node *root) { + int ret; + + ret = CLI_COMMAND( + NEXTHOP_ADD_CTX(root), + "mpls iface IFACE via VIA [labels LABEL+] [(id ID),(ttl TTL),(payload ipv4|ipv6)]", + nh_mpls_add, + "Add an MPLS nexthop.", + with_help("Output interface.", ec_node_dyn("IFACE", complete_iface_names, NULL)), + with_help("Gateway IPv4/6 address.", ec_node_re("VIA", IP_ANY_RE)), + with_help("Output MPLS label (0-1048575).", ec_node_uint("LABEL", 0, 1048575, 10)), + with_help("Nexthop ID.", ec_node_uint("ID", 1, UINT32_MAX - 1, 10)), + with_help("Initial TTL (1-255, 0=copy).", ec_node_uint("TTL", 1, 255, 10)), + with_help("IPv4 payload.", ec_node_str("ipv4", "ipv4")), + with_help("IPv6 payload.", ec_node_str("ipv6", "ipv6")) + ); + if (ret < 0) + return ret; + + return 0; +} + +static struct cli_context ctx = { + .name = "mpls_nexthop", + .init = ctx_init, +}; + +static void __attribute__((constructor, used)) init(void) { + cli_context_register(&ctx); + cli_nexthop_formatter_register(&mpls_formatter); +} diff --git a/modules/mpls/control/label.c b/modules/mpls/control/label.c new file mode 100644 index 000000000..dffe5f83b --- /dev/null +++ b/modules/mpls/control/label.c @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2025 Matej Muzila + +#include "event.h" +#include "module.h" +#include "mpls.h" + +#include + +static struct api_out label_route_add(const void *request, struct api_ctx *) { + const struct gr_mpls_label_route_add_req *req = request; + struct nexthop *nh; + + if (req->in_label > GR_MPLS_LABEL_MAX) + return api_out(EINVAL, 0, NULL); + + nh = nexthop_lookup_id(req->nh_id); + if (nh == NULL) + return api_out(ENOENT, 0, NULL); + if (nh->type != GR_NH_T_MPLS) + return api_out(EINVAL, 0, NULL); + + int ret = mpls_rib_insert(req->vrf_id, req->in_label, nh, req->origin, req->exist_ok); + + return api_out(-ret, 0, NULL); +} + +static struct api_out label_route_del(const void *request, struct api_ctx *) { + const struct gr_mpls_label_route_del_req *req = request; + + int ret = mpls_rib_delete(req->vrf_id, req->in_label, req->missing_ok); + + return api_out(-ret, 0, NULL); +} + +static struct api_out label_route_get(const void *request, struct api_ctx *) { + const struct gr_mpls_label_route_get_req *req = request; + struct gr_mpls_label_route *resp; + const struct nexthop *nh; + + nh = mpls_fib_lookup(req->vrf_id, req->in_label); + if (nh == NULL) + return api_out(ENOENT, 0, NULL); + + resp = calloc(1, sizeof(*resp)); + if (resp == NULL) + return api_out(ENOMEM, 0, NULL); + + resp->vrf_id = req->vrf_id; + resp->in_label = req->in_label; + resp->nh_id = nh->nh_id; + resp->origin = nh->origin; + + return api_out(0, sizeof(*resp), resp); +} + +struct label_list_ctx { + struct api_ctx *ctx; + uint16_t max_count; + uint16_t count; +}; + +static int label_route_send(uint16_t vrf_id, uint32_t label, const struct nexthop *nh, void *priv) { + struct label_list_ctx *lctx = priv; + + if (lctx->max_count != 0 && lctx->count >= lctx->max_count) + return errno_set(EXFULL); + + struct gr_mpls_label_route route = { + .vrf_id = vrf_id, + .in_label = label, + .nh_id = nh->nh_id, + .origin = nh->origin, + }; + + api_send(lctx->ctx, sizeof(route), &route); + lctx->count++; + + return 0; +} + +static struct api_out label_route_list(const void *request, struct api_ctx *ctx) { + const struct gr_mpls_label_route_list_req *req = request; + struct label_list_ctx lctx = { + .ctx = ctx, + .max_count = req->max_count, + }; + + int ret = mpls_rib_iter(req->vrf_id, label_route_send, &lctx); + if (ret == -EXFULL) + ret = 0; + + return api_out(-ret, 0, NULL); +} + +static struct module mpls_module = { + .name = "mpls", + .depends_on = "nexthop", +}; + +RTE_INIT(mpls_constructor) { + module_register(&mpls_module); + api_handler(GR_MPLS_LABEL_ROUTE_ADD, label_route_add); + api_handler(GR_MPLS_LABEL_ROUTE_DEL, label_route_del); + api_handler(GR_MPLS_LABEL_ROUTE_GET, label_route_get); + api_handler(GR_MPLS_LABEL_ROUTE_LIST, label_route_list); + event_serializer(GR_EVENT_MPLS_ROUTE_ADD, NULL); + event_serializer(GR_EVENT_MPLS_ROUTE_DEL, NULL); +} diff --git a/modules/mpls/control/label_table.c b/modules/mpls/control/label_table.c new file mode 100644 index 000000000..523e1a76e --- /dev/null +++ b/modules/mpls/control/label_table.c @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2025 Matej Muzila + +#include "event.h" +#include "log.h" +#include "mpls.h" + +#include + +#include + +LOG_TYPE("mpls"); + +#define MPLS_LFIB_SIZE (GR_MPLS_LABEL_MAX + 1) + +static struct nexthop **get_lfib(uint16_t vrf_id) { + struct iface *iface = get_vrf_iface(vrf_id); + if (iface == NULL) + return NULL; + struct nexthop **lfib = iface_info_vrf(iface)->fib_mpls; + if (lfib == NULL) + return errno_set_null(ENONET); + return lfib; +} + +const struct nexthop *mpls_fib_lookup(uint16_t vrf_id, uint32_t label) { + struct nexthop **lfib = get_lfib(vrf_id); + + if (lfib == NULL || label > GR_MPLS_LABEL_MAX) + return NULL; + + return lfib[label]; +} + +int mpls_rib_insert( + uint16_t vrf_id, + uint32_t label, + struct nexthop *nh, + gr_nh_origin_t origin, + bool exist_ok +) { + struct nexthop **lfib = get_lfib(vrf_id); + struct nexthop *existing; + + if (lfib == NULL) + return -errno; + if (label > GR_MPLS_LABEL_MAX) + return errno_set(EINVAL); + + existing = lfib[label]; + if (existing != NULL) { + if (!exist_ok) + return errno_set(EEXIST); + if (existing == nh) + return 0; + } + + nexthop_incref(nh); + lfib[label] = nh; + + if (origin != GR_NH_ORIGIN_INTERNAL) { + event_push( + GR_EVENT_MPLS_ROUTE_ADD, + &(const struct gr_mpls_label_route) { + .vrf_id = vrf_id, + .in_label = label, + .nh_id = nh->nh_id, + .origin = origin, + } + ); + } + + if (existing != NULL) + nexthop_decref(existing); + + return 0; +} + +int mpls_rib_delete(uint16_t vrf_id, uint32_t label, bool missing_ok) { + struct nexthop **lfib = get_lfib(vrf_id); + struct nexthop *nh; + + if (lfib == NULL) + return -errno; + if (label > GR_MPLS_LABEL_MAX) + return errno_set(EINVAL); + + nh = lfib[label]; + if (nh == NULL) { + if (missing_ok) + return 0; + return errno_set(ENOENT); + } + + lfib[label] = NULL; + + if (nh->origin != GR_NH_ORIGIN_INTERNAL) { + event_push( + GR_EVENT_MPLS_ROUTE_DEL, + &(const struct gr_mpls_label_route) { + .vrf_id = vrf_id, + .in_label = label, + .nh_id = nh->nh_id, + .origin = nh->origin, + } + ); + } + + nexthop_decref(nh); + + return 0; +} + +int mpls_rib_iter(uint16_t vrf_id, mpls_rib_iter_cb cb, void *priv) { + if (vrf_id != GR_VRF_ID_UNDEF) { + struct nexthop **lfib = get_lfib(vrf_id); + if (lfib == NULL) + return -errno; + for (uint32_t i = 0; i < MPLS_LFIB_SIZE; i++) { + if (lfib[i] == NULL) + continue; + int ret = cb(vrf_id, i, lfib[i], priv); + if (ret < 0) + return ret; + } + } else { + for (uint16_t v = 1; v < GR_MAX_IFACES; v++) { + struct iface *iface = iface_from_id(v); + if (iface == NULL || iface->type != GR_IFACE_TYPE_VRF) + continue; + struct nexthop **lfib = iface_info_vrf(iface)->fib_mpls; + if (lfib == NULL) + continue; + for (uint32_t i = 0; i < MPLS_LFIB_SIZE; i++) { + if (lfib[i] == NULL) + continue; + int ret = cb(v, i, lfib[i], priv); + if (ret < 0) + return ret; + } + } + } + return 0; +} + +static int mpls_fib_init(struct iface *vrf) { + struct nexthop **lfib; + + lfib = rte_zmalloc("mpls_lfib", MPLS_LFIB_SIZE * sizeof(*lfib), RTE_CACHE_LINE_SIZE); + if (lfib == NULL) + return errno_log(rte_errno, "rte_zmalloc(mpls_lfib)"); + + iface_info_vrf(vrf)->fib_mpls = lfib; + + return 0; +} + +static int mpls_fib_reconfig(struct iface *) { + return 0; +} + +static void mpls_fib_fini(struct iface *vrf) { + struct nexthop **lfib = iface_info_vrf(vrf)->fib_mpls; + if (lfib == NULL) + return; + + for (uint32_t i = 0; i < MPLS_LFIB_SIZE; i++) { + if (lfib[i] != NULL) + nexthop_decref(lfib[i]); + } + + rte_free(lfib); + iface_info_vrf(vrf)->fib_mpls = NULL; +} + +static const struct vrf_fib_ops mpls_fib_ops = { + .init = mpls_fib_init, + .reconfig = mpls_fib_reconfig, + .fini = mpls_fib_fini, +}; + +RTE_INIT(mpls_label_table_init) { + vrf_fib_ops_register(GR_AF_MPLS, &mpls_fib_ops); +} diff --git a/modules/mpls/control/meson.build b/modules/mpls/control/meson.build new file mode 100644 index 000000000..b404cc399 --- /dev/null +++ b/modules/mpls/control/meson.build @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2025 Matej Muzila + +src += files( + 'label.c', + 'label_table.c', + 'nexthop.c', + 'resolve.c', +) +inc += include_directories('.') diff --git a/modules/mpls/control/mpls.h b/modules/mpls/control/mpls.h new file mode 100644 index 000000000..bd1fd8898 --- /dev/null +++ b/modules/mpls/control/mpls.h @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2025 Matej Muzila + +#pragma once + +#include "nexthop.h" +#include "vrf.h" + +#include + +GR_NH_TYPE_INFO(GR_NH_T_MPLS, nexthop_info_mpls, { + uint8_t n_labels; + uint8_t ttl; + addr_family_t payload_af; + uint32_t labels[GR_MPLS_MAX_LABELS]; + struct nexthop *via_nh; +}); + +// Look up a label in the per-VRF LFIB. Called from the datapath. +const struct nexthop *mpls_fib_lookup(uint16_t vrf_id, uint32_t label); + +int mpls_rib_insert(uint16_t, uint32_t, struct nexthop *, gr_nh_origin_t, bool exist_ok); +int mpls_rib_delete(uint16_t vrf_id, uint32_t label, bool missing_ok); + +typedef int (*mpls_rib_iter_cb)(uint16_t, uint32_t, const struct nexthop *, void *); +int mpls_rib_iter(uint16_t vrf_id, mpls_rib_iter_cb cb, void *priv); diff --git a/modules/mpls/control/nexthop.c b/modules/mpls/control/nexthop.c new file mode 100644 index 000000000..53b8d27b4 --- /dev/null +++ b/modules/mpls/control/nexthop.c @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2025 Matej Muzila + +#include "mpls.h" + +#include + +static int mpls_nh_import_info(struct nexthop *nh, const void *info) { + struct nexthop_info_mpls *priv = nexthop_info_mpls(nh); + const struct gr_nexthop_info_mpls *pub = info; + struct nexthop *via_nh, *old_via; + + if (pub->n_labels > GR_MPLS_MAX_LABELS) + return errno_set(EINVAL); + + for (uint8_t i = 0; i < pub->n_labels; i++) { + if (pub->labels[i] > GR_MPLS_LABEL_MAX) + return errno_set(EINVAL); + if (pub->labels[i] == GR_MPLS_LABEL_IMPLICIT_NULL) + return errno_set(EINVAL); + } + + switch (pub->payload_af) { + case GR_AF_UNSPEC: + case GR_AF_IP4: + case GR_AF_IP6: + break; + default: + return errno_set(EINVAL); + } + + if (pub->via.af != GR_AF_IP4 && pub->via.af != GR_AF_IP6) + return errno_set(EAFNOSUPPORT); + + via_nh = nexthop_lookup_l3(pub->via.af, nh->vrf_id, nh->iface_id, &pub->via.addr); + if (via_nh == NULL) { + struct gr_nexthop_info_l3 l3_info = {.af = pub->via.af}; + if (pub->via.af == GR_AF_IP4) + l3_info.ipv4 = pub->via.ipv4; + else + l3_info.ipv6 = pub->via.ipv6; + + via_nh = nexthop_new( + &(struct gr_nexthop_base) { + .type = GR_NH_T_L3, + .iface_id = nh->iface_id, + .vrf_id = nh->vrf_id, + .origin = GR_NH_ORIGIN_INTERNAL, + }, + &l3_info + ); + if (via_nh == NULL) + return -errno; + } else { + nexthop_incref(via_nh); + } + + priv->n_labels = pub->n_labels; + priv->ttl = pub->ttl; + priv->payload_af = pub->payload_af; + memcpy(priv->labels, pub->labels, pub->n_labels * sizeof(pub->labels[0])); + + old_via = priv->via_nh; + priv->via_nh = via_nh; + if (old_via != NULL) + nexthop_decref(old_via); + + return 0; +} + +static void mpls_nh_free(struct nexthop *nh) { + struct nexthop_info_mpls *priv = nexthop_info_mpls(nh); + if (priv->via_nh != NULL) + nexthop_decref(priv->via_nh); + priv->via_nh = NULL; +} + +static void mpls_nh_remove_via_cb(struct nexthop *nh, void *dying) { + if (nh->type != GR_NH_T_MPLS) + return; + struct nexthop_info_mpls *priv = nexthop_info_mpls(nh); + if (priv->via_nh == dying) + priv->via_nh = NULL; +} + +static void mpls_nh_remove_references(struct nexthop *dying) { + nexthop_iter(mpls_nh_remove_via_cb, dying); +} + +static bool mpls_nh_equal(const struct nexthop *a, const struct nexthop *b) { + const struct nexthop_info_mpls *ma = nexthop_info_mpls(a); + const struct nexthop_info_mpls *mb = nexthop_info_mpls(b); + + if (ma->n_labels != mb->n_labels) + return false; + if (ma->payload_af != mb->payload_af) + return false; + if (ma->via_nh != mb->via_nh) + return false; + return memcmp(ma->labels, mb->labels, ma->n_labels * sizeof(ma->labels[0])) == 0; +} + +static struct gr_nexthop *mpls_nh_to_api(const struct nexthop *nh, size_t *len) { + const struct nexthop_info_mpls *priv = nexthop_info_mpls(nh); + struct gr_nexthop_info_mpls *pub_info; + struct gr_nexthop *pub; + + *len = sizeof(*pub) + sizeof(*pub_info); + pub = calloc(1, *len); + if (pub == NULL) + return errno_set_null(ENOMEM); + + pub->base = nh->base; + pub_info = (struct gr_nexthop_info_mpls *)pub->info; + pub_info->n_labels = priv->n_labels; + pub_info->ttl = priv->ttl; + pub_info->payload_af = priv->payload_af; + memcpy(pub_info->labels, priv->labels, priv->n_labels * sizeof(priv->labels[0])); + + if (priv->via_nh != NULL) { + const struct nexthop_info_l3 *l3 = nexthop_info_l3(priv->via_nh); + pub_info->via.af = l3->af; + if (l3->af == GR_AF_IP4) + pub_info->via.ipv4 = l3->ipv4; + else if (l3->af == GR_AF_IP6) + pub_info->via.ipv6 = l3->ipv6; + } + + return pub; +} + +static struct nexthop_type_ops mpls_nh_ops = { + .import_info = mpls_nh_import_info, + .free = mpls_nh_free, + .remove_references = mpls_nh_remove_references, + .equal = mpls_nh_equal, + .to_api = mpls_nh_to_api, +}; + +RTE_INIT(mpls_nexthop_init) { + nexthop_type_ops_register(GR_NH_T_MPLS, &mpls_nh_ops); +} diff --git a/modules/mpls/control/resolve.c b/modules/mpls/control/resolve.c new file mode 100644 index 000000000..f414b09da --- /dev/null +++ b/modules/mpls/control/resolve.c @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2025 Matej Muzila + +#include "l3.h" +#include "log.h" +#include "mpls.h" +#include "mpls_datapath.h" + +#include + +LOG_TYPE("mpls"); + +static void mpls_resolve_cb(void *obj, uintptr_t, const struct control_queue_drain *drain) { + struct rte_mbuf *m = obj; + struct nexthop_info_l3 *l3; + struct nexthop *nh; + + nh = (struct nexthop *)l3_mbuf_data(m)->nh; + + if (drain != NULL) { + switch (drain->event) { + case GR_EVENT_IFACE_REMOVE: + if (mbuf_data(m)->iface == drain->obj) + goto free; + break; + case GR_EVENT_NEXTHOP_DELETE: + if (nh == drain->obj) + goto free; + if (mpls_hold_mbuf_data(m)->mpls_nh == drain->obj) + goto free; + break; + } + } + + l3 = nexthop_info_l3(nh); + + if (l3->state == GR_NH_S_REACHABLE) { + if (mpls_resubmit_cb(m, nh) < 0) + goto free; + return; + } + + if (l3->held_pkts < nh_conf.max_held_pkts) { + queue_mbuf_data(m)->next = NULL; + if (l3->held_pkts_head == NULL) + l3->held_pkts_head = m; + else + queue_mbuf_data(l3->held_pkts_tail)->next = m; + l3->held_pkts_tail = m; + l3->held_pkts++; + if (l3->state != GR_NH_S_PENDING) { + const struct nexthop_af_ops *ops = nexthop_af_ops_from_nh(nh); + if (ops != NULL) + ops->solicit(nh); + l3->state = GR_NH_S_PENDING; + } + return; + } + +free: + rte_pktmbuf_free(m); +} + +static int mpls_solicit(struct nexthop *nh) { + const struct nexthop_af_ops *ops = nexthop_af_ops_from_nh(nh); + if (ops != NULL) + return ops->solicit(nh); + return errno_set(ENOTSUP); +} + +static void mpls_rib_cleanup(struct nexthop *nh) { + for (uint16_t v = 1; v < GR_MAX_IFACES; v++) { + struct iface *iface = iface_from_id(v); + if (iface == NULL || iface->type != GR_IFACE_TYPE_VRF) + continue; + struct nexthop **lfib = iface_info_vrf(iface)->fib_mpls; + if (lfib == NULL) + continue; + for (uint32_t i = 0; i <= GR_MPLS_LABEL_MAX; i++) { + if (lfib[i] == nh) { + lfib[i] = NULL; + nexthop_decref(nh); + } + } + } +} + +static struct nexthop_af_ops mpls_af_ops = { + .resolve = mpls_resolve_cb, + .solicit = mpls_solicit, + .resubmit = mpls_resubmit_cb, + .cleanup_routes = mpls_rib_cleanup, +}; + +RTE_INIT(mpls_resolve_constructor) { + nexthop_af_ops_register(GR_AF_MPLS, &mpls_af_ops); +} diff --git a/modules/mpls/datapath/meson.build b/modules/mpls/datapath/meson.build new file mode 100644 index 000000000..6544ff8ac --- /dev/null +++ b/modules/mpls/datapath/meson.build @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2025 Matej Muzila + +src += files( + 'mpls_input.c', + 'mpls_output.c', + 'mpls_push.c', +) +inc += include_directories('.') diff --git a/modules/mpls/datapath/mpls_datapath.h b/modules/mpls/datapath/mpls_datapath.h new file mode 100644 index 000000000..aa99bb94f --- /dev/null +++ b/modules/mpls/datapath/mpls_datapath.h @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2025 Matej Muzila + +#pragma once + +#include "mbuf.h" +#include "nexthop.h" + +#include +#include + +static inline uint32_t mpls_hdr_get_label(const struct rte_mpls_hdr *h) { + return (rte_be_to_cpu_16(h->tag_msb) << 4) | h->tag_lsb; +} + +static inline void mpls_hdr_set_label(struct rte_mpls_hdr *h, uint32_t label) { + h->tag_msb = rte_cpu_to_be_16(label >> 4); + h->tag_lsb = label & 0xf; +} + +GR_MBUF_PRIV_DATA_TYPE(mpls_hold_mbuf_data, { + const struct nexthop *nh; + const struct nexthop *mpls_nh; +}); + +int mpls_resubmit_cb(struct rte_mbuf *, struct nexthop *); diff --git a/modules/mpls/datapath/mpls_input.c b/modules/mpls/datapath/mpls_input.c new file mode 100644 index 000000000..610c7b29e --- /dev/null +++ b/modules/mpls/datapath/mpls_input.c @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2025 Matej Muzila + +#include "checksum.h" +#include "eth.h" +#include "graph.h" +#include "l3.h" +#include "mbuf.h" +#include "mpls.h" +#include "mpls_datapath.h" + +#include + +#include +#include +#include + +enum { + MPLS_OUTPUT = 0, + IP_INPUT, + IP6_INPUT, + TTL_EXCEEDED, + NO_ROUTE, + BAD_LABEL, + EDGE_COUNT, +}; + +struct trace_mpls_data { + uint32_t label; + uint8_t tc; + uint8_t bs; + uint8_t ttl; +}; + +static int mpls_trace_format(char *buf, size_t len, const void *data, size_t /*data_len*/) { + const struct trace_mpls_data *t = data; + return snprintf(buf, len, "label=%u tc=%u bs=%u ttl=%u", t->label, t->tc, t->bs, t->ttl); +} + +static uint16_t +mpls_input_process(struct rte_graph *graph, struct rte_node *node, void **objs, uint16_t nb_objs) { + struct rte_mpls_hdr *mpls; + struct rte_mbuf *mbuf; + rte_edge_t edge; + + for (uint16_t i = 0; i < nb_objs; i++) { + mbuf = objs[i]; + edge = BAD_LABEL; + + struct rte_mpls_hdr trace_hdr = *rte_pktmbuf_mtod(mbuf, struct rte_mpls_hdr *); + + for (uint8_t depth = 0; depth < GR_MPLS_MAX_STACK_DEPTH; depth++) { + mpls = rte_pktmbuf_mtod(mbuf, struct rte_mpls_hdr *); + uint32_t label = mpls_hdr_get_label(mpls); + uint8_t bos = mpls->bs; + uint8_t ttl = mpls->ttl; + + if (ttl <= 1) { + edge = TTL_EXCEEDED; + break; + } + ttl -= 1; + + if (label < GR_MPLS_LABEL_FIRST_UNRESERVED) { + rte_pktmbuf_adj(mbuf, sizeof(*mpls)); + switch (label) { + case GR_MPLS_LABEL_IPV4_EXPLICIT_NULL: + mbuf->packet_type = RTE_PTYPE_L3_IPV4; + edge = IP_INPUT; + break; + case GR_MPLS_LABEL_IPV6_EXPLICIT_NULL: + mbuf->packet_type = RTE_PTYPE_L3_IPV6; + edge = IP6_INPUT; + break; + case GR_MPLS_LABEL_IMPLICIT_NULL:; + uint8_t ver = *rte_pktmbuf_mtod(mbuf, uint8_t *) >> 4; + if (ver == 4) { + mbuf->packet_type = RTE_PTYPE_L3_IPV4; + edge = IP_INPUT; + } else if (ver == 6) { + mbuf->packet_type = RTE_PTYPE_L3_IPV6; + edge = IP6_INPUT; + } else { + edge = BAD_LABEL; + } + break; + default: + edge = BAD_LABEL; + break; + } + break; + } + + const struct iface *iface = mbuf_data(mbuf)->iface; + const struct nexthop *nh = mpls_fib_lookup(iface->vrf_id, label); + if (nh == NULL) { + edge = NO_ROUTE; + break; + } + + const struct nexthop_info_mpls *info = nexthop_info_mpls(nh); + + if (info->n_labels > 0) { + mpls_hdr_set_label(mpls, info->labels[0]); + mpls->ttl = ttl; + l3_mbuf_data(mbuf)->nh = nh; + mbuf->packet_type = RTE_PTYPE_TUNNEL_MPLS_IN_GRE; + edge = MPLS_OUTPUT; + break; + } + + if (bos) { + if (info->via_nh == NULL) { + edge = NO_ROUTE; + break; + } + rte_pktmbuf_adj(mbuf, sizeof(*mpls)); + addr_family_t af = info->payload_af; + if (af == GR_AF_UNSPEC) { + uint8_t ver = *rte_pktmbuf_mtod(mbuf, uint8_t *) >> 4; + if (ver == 4) + af = GR_AF_IP4; + else if (ver == 6) + af = GR_AF_IP6; + } + if (af == GR_AF_IP4) { + struct rte_ipv4_hdr *ip; + ip = rte_pktmbuf_mtod(mbuf, struct rte_ipv4_hdr *); + ip->hdr_checksum = fixup_checksum_16( + ip->hdr_checksum, + rte_cpu_to_be_16(ip->time_to_live << 8), + rte_cpu_to_be_16(ttl << 8) + ); + ip->time_to_live = ttl; + mbuf->packet_type = RTE_PTYPE_L3_IPV4; + } else if (af == GR_AF_IP6) { + struct rte_ipv6_hdr *ip6; + ip6 = rte_pktmbuf_mtod(mbuf, struct rte_ipv6_hdr *); + ip6->hop_limits = ttl; + mbuf->packet_type = RTE_PTYPE_L3_IPV6; + } else { + edge = BAD_LABEL; + break; + } + l3_mbuf_data(mbuf)->nh = info->via_nh; + edge = MPLS_OUTPUT; + break; + } + + rte_pktmbuf_adj(mbuf, sizeof(*mpls)); + } + + if (gr_mbuf_is_traced(mbuf)) { + struct trace_mpls_data *t = gr_mbuf_trace_add(mbuf, node, sizeof(*t)); + t->label = mpls_hdr_get_label(&trace_hdr); + t->tc = trace_hdr.tc; + t->bs = trace_hdr.bs; + t->ttl = trace_hdr.ttl; + } + rte_node_enqueue_x1(graph, node, edge, mbuf); + } + + return nb_objs; +} + +static void mpls_input_register(void) { + gr_eth_input_add_type(RTE_BE16(RTE_ETHER_TYPE_MPLS), "mpls_input"); +} + +static struct rte_node_register mpls_input_node = { + .name = "mpls_input", + + .process = mpls_input_process, + + .nb_edges = EDGE_COUNT, + .next_nodes = { + [MPLS_OUTPUT] = "mpls_output", + [IP_INPUT] = "ip_input", + [IP6_INPUT] = "ip6_input", + [TTL_EXCEEDED] = "mpls_input_ttl_exceeded", + [NO_ROUTE] = "mpls_input_no_route", + [BAD_LABEL] = "mpls_input_bad_label", + }, +}; + +static struct gr_node_info info = { + .node = &mpls_input_node, + .type = GR_NODE_T_L3, + .trace_format = mpls_trace_format, + .register_callback = mpls_input_register, +}; + +GR_NODE_REGISTER(info); + +GR_DROP_REGISTER(mpls_input_ttl_exceeded); +GR_DROP_REGISTER(mpls_input_no_route); +GR_DROP_REGISTER(mpls_input_bad_label); diff --git a/modules/mpls/datapath/mpls_output.c b/modules/mpls/datapath/mpls_output.c new file mode 100644 index 000000000..96fe44f59 --- /dev/null +++ b/modules/mpls/datapath/mpls_output.c @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2025 Matej Muzila + +#include "control_input.h" +#include "eth.h" +#include "graph.h" +#include "l3.h" +#include "log.h" +#include "mbuf.h" +#include "mpls.h" +#include "mpls_datapath.h" + +#include +#include + +LOG_TYPE("mpls"); + +static control_input_t mpls_output_ctrl; + +int mpls_resubmit_cb(struct rte_mbuf *m, struct nexthop *) { + l3_mbuf_data(m)->nh = mpls_hold_mbuf_data(m)->mpls_nh; + mbuf_data(m)->iface = NULL; + if (post_to_stack(mpls_output_ctrl, m) < 0) { + LOG(ERR, "post_to_stack: %s", strerror(errno)); + return -errno; + } + return 0; +} + +enum { + ETH_OUTPUT = 0, + HOLD, + NO_ROUTE, + MTU_EXCEEDED, + EDGE_COUNT, +}; + +static uint16_t +mpls_output_process(struct rte_graph *graph, struct rte_node *node, void **objs, uint16_t nb_objs) { + struct eth_output_mbuf_data *eth_data; + const struct nexthop_info_l3 *l3; + const struct nexthop *via_nh; + const struct nexthop *nh; + const struct iface *iface; + struct rte_mbuf *mbuf; + rte_edge_t edge; + + for (uint16_t i = 0; i < nb_objs; i++) { + mbuf = objs[i]; + + nh = l3_mbuf_data(mbuf)->nh; + if (nh == NULL) { + edge = NO_ROUTE; + goto next; + } + + if (nh->type == GR_NH_T_MPLS) { + via_nh = nexthop_info_mpls(nh)->via_nh; + } else if (nh->type == GR_NH_T_L3) { + via_nh = nh; + } else { + edge = NO_ROUTE; + goto next; + } + + if (via_nh == NULL) { + edge = NO_ROUTE; + goto next; + } + + l3 = nexthop_info_l3(via_nh); + iface = iface_from_id(via_nh->iface_id); + if (iface == NULL) { + edge = NO_ROUTE; + goto next; + } + + if (rte_pktmbuf_pkt_len(mbuf) > iface->mtu) { + edge = MTU_EXCEEDED; + goto next; + } + + if (l3->state != GR_NH_S_REACHABLE) { + mpls_hold_mbuf_data(mbuf)->mpls_nh = nh; + l3_mbuf_data(mbuf)->nh = via_nh; + mbuf->packet_type |= RTE_PTYPE_TUNNEL_MPLS_IN_GRE; + edge = HOLD; + goto next; + } + + eth_data = eth_output_mbuf_data(mbuf); + eth_data->dst = l3->mac; + if (mbuf->packet_type & RTE_PTYPE_L3_IPV4) + eth_data->ether_type = RTE_BE16(RTE_ETHER_TYPE_IPV4); + else if (mbuf->packet_type & RTE_PTYPE_L3_IPV6) + eth_data->ether_type = RTE_BE16(RTE_ETHER_TYPE_IPV6); + else + eth_data->ether_type = RTE_BE16(RTE_ETHER_TYPE_MPLS); + mbuf_data(mbuf)->iface = iface; + edge = ETH_OUTPUT; +next: + if (gr_mbuf_is_traced(mbuf)) + gr_mbuf_trace_add(mbuf, node, 0); + rte_node_enqueue_x1(graph, node, edge, mbuf); + } + + return nb_objs; +} + +static struct rte_node_register mpls_output_node_reg = { + .name = "mpls_output", + + .process = mpls_output_process, + + .nb_edges = EDGE_COUNT, + .next_nodes = { + [ETH_OUTPUT] = "eth_output", + [HOLD] = "ip_hold", + [NO_ROUTE] = "mpls_output_no_route", + [MTU_EXCEEDED] = "mpls_output_mtu_exceeded", + }, +}; + +static void mpls_output_register(void) { + mpls_output_ctrl = gr_control_input_register_handler("mpls_output", true); +} + +static struct gr_node_info info = { + .node = &mpls_output_node_reg, + .type = GR_NODE_T_L3, + .register_callback = mpls_output_register, +}; + +GR_NODE_REGISTER(info); + +GR_DROP_REGISTER(mpls_output_no_route); +GR_DROP_REGISTER(mpls_output_mtu_exceeded); diff --git a/modules/mpls/datapath/mpls_push.c b/modules/mpls/datapath/mpls_push.c new file mode 100644 index 000000000..ba8b4b420 --- /dev/null +++ b/modules/mpls/datapath/mpls_push.c @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2025 Matej Muzila + +#include "graph.h" +#include "ip4_datapath.h" +#include "ip6_datapath.h" +#include "l3.h" +#include "mbuf.h" +#include "mpls.h" +#include "mpls_datapath.h" + +#include + +#include +#include + +enum { + MPLS_OUTPUT = 0, + NO_HEADROOM, + MTU_EXCEEDED, + EDGE_COUNT, +}; + +static uint16_t +mpls_push_process(struct rte_graph *graph, struct rte_node *node, void **objs, uint16_t nb_objs) { + const struct nexthop_info_mpls *info; + struct rte_mpls_hdr *mpls; + const struct nexthop *nh; + const struct iface *iface; + struct rte_mbuf *mbuf; + rte_edge_t edge; + uint8_t ttl; + + for (uint16_t i = 0; i < nb_objs; i++) { + mbuf = objs[i]; + + nh = l3_mbuf_data(mbuf)->nh; + info = nexthop_info_mpls(nh); + + if (info->n_labels == 0 || info->via_nh == NULL) { + edge = NO_HEADROOM; + goto next; + } + + iface = iface_from_id(info->via_nh->iface_id); + if (iface == NULL) { + edge = NO_HEADROOM; + goto next; + } + + uint16_t overhead = info->n_labels * sizeof(struct rte_mpls_hdr); + if (rte_pktmbuf_pkt_len(mbuf) + overhead > iface->mtu) { + edge = MTU_EXCEEDED; + goto next; + } + + if (mbuf->packet_type & RTE_PTYPE_L3_IPV4) + ttl = rte_pktmbuf_mtod(mbuf, struct rte_ipv4_hdr *)->time_to_live; + else + ttl = rte_pktmbuf_mtod(mbuf, struct rte_ipv6_hdr *)->hop_limits; + + if (info->ttl) + ttl = info->ttl; + + mpls = gr_mbuf_prepend(mbuf, mpls, (info->n_labels - 1) * sizeof(*mpls)); + if (unlikely(mpls == NULL)) { + edge = NO_HEADROOM; + goto next; + } + + for (uint8_t j = 0; j < info->n_labels; j++) { + mpls_hdr_set_label(&mpls[j], info->labels[j]); + mpls[j].tc = 0; + mpls[j].bs = (j == info->n_labels - 1) ? 1 : 0; + mpls[j].ttl = ttl; + } + + l3_mbuf_data(mbuf)->nh = nh; + mbuf->packet_type = RTE_PTYPE_TUNNEL_MPLS_IN_GRE; + edge = MPLS_OUTPUT; +next: + if (gr_mbuf_is_traced(mbuf)) + gr_mbuf_trace_add(mbuf, node, 0); + rte_node_enqueue_x1(graph, node, edge, mbuf); + } + + return nb_objs; +} + +static void mpls_push_register(void) { + ip_output_register_nexthop_type(GR_NH_T_MPLS, "mpls_push"); + ip6_output_register_nexthop_type(GR_NH_T_MPLS, "mpls_push"); +} + +static struct rte_node_register mpls_push_node = { + .name = "mpls_push", + + .process = mpls_push_process, + + .nb_edges = EDGE_COUNT, + .next_nodes = { + [MPLS_OUTPUT] = "mpls_output", + [NO_HEADROOM] = "error_no_headroom", + [MTU_EXCEEDED] = "mpls_push_mtu_exceeded", + }, +}; + +static struct gr_node_info info = { + .node = &mpls_push_node, + .type = GR_NODE_T_L3, + .register_callback = mpls_push_register, +}; + +GR_NODE_REGISTER(info); + +GR_DROP_REGISTER(mpls_push_mtu_exceeded); diff --git a/modules/mpls/meson.build b/modules/mpls/meson.build new file mode 100644 index 000000000..8bc77a9c3 --- /dev/null +++ b/modules/mpls/meson.build @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2025 Matej Muzila + +subdir('api') +subdir('cli') +subdir('control') +subdir('datapath') diff --git a/modules/policy/datapath/nat_datapath.h b/modules/policy/datapath/nat_datapath.h index 1e791d46d..8d0319740 100644 --- a/modules/policy/datapath/nat_datapath.h +++ b/modules/policy/datapath/nat_datapath.h @@ -3,6 +3,7 @@ #pragma once +#include "checksum.h" #include "iface.h" #include "nexthop.h" @@ -16,35 +17,6 @@ GR_NH_TYPE_INFO(GR_NH_T_DNAT, nexthop_info_dnat, { struct nexthop *arp; }); -static inline rte_be16_t -fixup_checksum_16(rte_be16_t old_cksum, rte_be16_t old_field, rte_be16_t new_field) { - uint32_t sum; - - // RFC 1624: HC' = ~(~HC + ~m + m') - // Note: 1's complement sum is endian-independent (RFC 1071, page 2). - sum = ~old_cksum & 0xffff; - sum += (~old_field & 0xffff) + new_field; - sum = (sum >> 16) + (sum & 0xffff); - sum += (sum >> 16); - - return ~sum & 0xffff; -} - -static inline rte_be16_t -fixup_checksum_32(rte_be16_t old_cksum, ip4_addr_t old_addr, ip4_addr_t new_addr) { - uint32_t sum; - - // Checksum 32-bit datum as as two 16-bit. Note, the first - // 32->16 bit reduction is not necessary. - sum = ~old_cksum & 0xffff; - sum += (~old_addr & 0xffff) + (new_addr & 0xffff); - sum += (~old_addr >> 16) + (new_addr >> 16); - sum = (sum >> 16) + (sum & 0xffff); - sum += (sum >> 16); - - return ~sum & 0xffff; -} - typedef enum { NAT_VERDICT_CONTINUE, NAT_VERDICT_FINAL, diff --git a/smoke/mpls_cli_test.sh b/smoke/mpls_cli_test.sh new file mode 100755 index 000000000..8b9173ab7 --- /dev/null +++ b/smoke/mpls_cli_test.sh @@ -0,0 +1,56 @@ +#!/bin/bash +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2025 Matej Muzila + +. $(dirname $0)/_init.sh + +port_add p0 +grcli address add 172.16.0.1/24 iface p0 + +netns_add n0 +move_to_netns x-p0 n0 +ip -n n0 addr add 172.16.0.2/24 dev x-p0 + +# create MPLS nexthop with single label +grcli nexthop add mpls iface p0 via 172.16.0.2 labels 100 id 10 + +# verify nexthop is visible +grcli nexthop show id 10 + +# create MPLS nexthop with multiple labels +grcli nexthop add mpls iface p0 via 172.16.0.2 labels 100 200 300 id 20 + +# verify multi-label nexthop +grcli nexthop show id 20 + +# create pop nexthop (no labels) +grcli nexthop add mpls iface p0 via 172.16.0.2 id 30 + +# verify pop nexthop +grcli nexthop show id 30 + +# list MPLS nexthops +grcli nexthop show type mpls + +# add label route +grcli mpls route add 500 nexthop id 10 + +# get label route +grcli mpls route get 500 + +# list label routes +grcli mpls route show + +# idempotent add (exist_ok) +grcli mpls route add 500 nexthop id 10 + +# delete label route +grcli mpls route del 500 + +# idempotent delete (missing_ok) +grcli mpls route del 500 + +# cleanup nexthops +grcli nexthop del 10 +grcli nexthop del 20 +grcli nexthop del 30 diff --git a/smoke/mpls_explicit_null_test.sh b/smoke/mpls_explicit_null_test.sh new file mode 100755 index 000000000..c7ae24825 --- /dev/null +++ b/smoke/mpls_explicit_null_test.sh @@ -0,0 +1,38 @@ +#!/bin/bash +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2025 Matej Muzila + +. $(dirname $0)/_init.sh + +port_add p0 +port_add p1 +grcli address add 172.16.0.1/24 iface p0 +grcli address add 172.16.1.1/24 iface p1 + +# IP route for the decapsulated packet (no MPLS nexthop needed) +grcli route add 10.0.1.0/24 via 172.16.1.2 + +# return path +grcli route add 10.0.0.0/24 via 172.16.0.2 + +for n in 0 1; do + p=x-p$n + ns=n$n + netns_add $ns + move_to_netns $p $ns + ip -n $ns addr add 172.16.$n.2/24 dev $p +done + +# n0: push explicit null (label 0) +ip -n n0 addr add 10.0.0.1/32 dev lo +ip -n n0 route add 10.0.1.0/24 encap mpls 0 via 172.16.0.1 dev x-p0 + +# n1: plain IP receiver +ip -n n1 addr add 10.0.1.1/32 dev lo +ip -n n1 route add default via 172.16.1.1 + +# resolve ARP before MPLS test +ip netns exec n0 ping -c1 -n 172.16.0.1 + +# n0 pushes MPLS(label=0) -> grout strips and routes via ip_input -> n1 receives plain IP +ip netns exec n0 ping -i0.01 -c3 -n 10.0.1.1 diff --git a/smoke/mpls_pop_payload_test.sh b/smoke/mpls_pop_payload_test.sh new file mode 100755 index 000000000..64c892c88 --- /dev/null +++ b/smoke/mpls_pop_payload_test.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2025 Matej Muzila + +. $(dirname $0)/_init.sh + +port_add p0 +port_add p1 +grcli address add 172.16.0.1/24 iface p0 +grcli address add 172.16.1.1/24 iface p1 + +# MPLS nexthop: pop with explicit payload type +grcli nexthop add mpls iface p1 via 172.16.1.2 id 42 payload ipv4 + +# label route: incoming label 100 -> pop +grcli mpls route add 100 nexthop id 42 + +# return path (plain IP) +grcli route add 10.0.0.0/24 via 172.16.0.2 + +for n in 0 1; do + p=x-p$n + ns=n$n + netns_add $ns + move_to_netns $p $ns + ip -n $ns addr add 172.16.$n.2/24 dev $p +done + +# n0: push label 100 when sending to 10.0.1.0/24 +ip netns exec n0 sysctl -wq net.mpls.platform_labels=1000 +ip -n n0 addr add 10.0.0.1/32 dev lo +ip -n n0 route add 10.0.1.0/24 encap mpls 100 via 172.16.0.1 dev x-p0 + +# n1: plain IP receiver +ip -n n1 addr add 10.0.1.1/32 dev lo +ip -n n1 route add default via 172.16.1.1 + +# resolve ARP before MPLS test +ip netns exec n0 ping -c1 -n 172.16.0.1 + +# n0 pushes MPLS(100) -> grout pops using explicit payload type -> n1 receives plain IP +ip netns exec n0 ping -i0.01 -c3 -n 10.0.1.1 diff --git a/smoke/mpls_pop_test.sh b/smoke/mpls_pop_test.sh new file mode 100755 index 000000000..89b8df967 --- /dev/null +++ b/smoke/mpls_pop_test.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2025 Matej Muzila + +. $(dirname $0)/_init.sh + +port_add p0 +port_add p1 +grcli address add 172.16.0.1/24 iface p0 +grcli address add 172.16.1.1/24 iface p1 + +# MPLS nexthop: pop (no labels), forward via n1 +grcli nexthop add mpls iface p1 via 172.16.1.2 id 42 + +# label route: incoming label 100 -> pop +grcli mpls route add 100 nexthop id 42 + +# return path (plain IP) +grcli route add 10.0.0.0/24 via 172.16.0.2 + +for n in 0 1; do + p=x-p$n + ns=n$n + netns_add $ns + move_to_netns $p $ns + ip -n $ns addr add 172.16.$n.2/24 dev $p +done + +# n0: push label 100 when sending to 10.0.1.0/24 +ip netns exec n0 sysctl -wq net.mpls.platform_labels=1000 +ip -n n0 addr add 10.0.0.1/32 dev lo +ip -n n0 route add 10.0.1.0/24 encap mpls 100 via 172.16.0.1 dev x-p0 + +# n1: plain IP receiver +ip -n n1 addr add 10.0.1.1/32 dev lo +ip -n n1 route add default via 172.16.1.1 + +# resolve ARP before MPLS test +ip netns exec n0 ping -c1 -n 172.16.0.1 + +# n0 pushes MPLS(100) -> grout pops label, propagates TTL -> n1 receives plain IP +ip netns exec n0 ping -i0.01 -c3 -n 10.0.1.1 diff --git a/smoke/mpls_push_test.sh b/smoke/mpls_push_test.sh new file mode 100755 index 000000000..69e37bf9f --- /dev/null +++ b/smoke/mpls_push_test.sh @@ -0,0 +1,43 @@ +#!/bin/bash +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2025 Matej Muzila + +. $(dirname $0)/_init.sh + +port_add p0 +port_add p1 +grcli address add 172.16.0.1/24 iface p0 +grcli address add 172.16.1.1/24 iface p1 + +# MPLS nexthop: push label 100, forward via n1 +grcli nexthop add mpls iface p1 via 172.16.1.2 labels 100 id 42 + +# IP route pointing to MPLS nexthop +grcli route add 10.0.1.0/24 via id 42 + +# return path (plain IP) +grcli route add 10.0.0.0/24 via 172.16.0.2 + +for n in 0 1; do + p=x-p$n + ns=n$n + netns_add $ns + move_to_netns $p $ns + ip -n $ns addr add 172.16.$n.2/24 dev $p +done + +# n0: plain IP sender +ip -n n0 addr add 10.0.0.1/32 dev lo +ip -n n0 route add default via 172.16.0.1 + +# n1: MPLS receiver, pop label 100 +ip netns exec n1 sysctl -wq net.mpls.platform_labels=1000 +ip netns exec n1 sysctl -wq net.mpls.conf.x-p1.input=1 +ip -n n1 addr add 10.0.1.1/32 dev lo +ip -n n1 -f mpls route add 100 dev lo + +# return path from n1 +ip -n n1 route add default via 172.16.1.1 + +# plain IP -> grout pushes MPLS label -> n1 pops label +ip netns exec n0 ping -i0.01 -c3 -n 10.0.1.1 diff --git a/smoke/mpls_swap_test.sh b/smoke/mpls_swap_test.sh new file mode 100755 index 000000000..9f8df4a25 --- /dev/null +++ b/smoke/mpls_swap_test.sh @@ -0,0 +1,47 @@ +#!/bin/bash +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2025 Matej Muzila + +. $(dirname $0)/_init.sh + +port_add p0 +port_add p1 +grcli address add 172.16.0.1/24 iface p0 +grcli address add 172.16.1.1/24 iface p1 + +# MPLS nexthop: swap to label 200, forward via n1 +grcli nexthop add mpls iface p1 via 172.16.1.2 labels 200 id 42 + +# label route: incoming label 100 -> swap to 200 +grcli mpls route add 100 nexthop id 42 + +# return path (plain IP) +grcli route add 10.0.0.0/24 via 172.16.0.2 + +for n in 0 1; do + p=x-p$n + ns=n$n + netns_add $ns + move_to_netns $p $ns + ip -n $ns addr add 172.16.$n.2/24 dev $p +done + +# n0: push label 100 when sending to 10.0.1.0/24 +ip netns exec n0 sysctl -wq net.mpls.platform_labels=1000 +ip -n n0 addr add 10.0.0.1/32 dev lo +ip -n n0 route add 10.0.1.0/24 encap mpls 100 via 172.16.0.1 dev x-p0 + +# n1: pop label 200 +ip netns exec n1 sysctl -wq net.mpls.platform_labels=1000 +ip netns exec n1 sysctl -wq net.mpls.conf.x-p1.input=1 +ip -n n1 addr add 10.0.1.1/32 dev lo +ip -n n1 -f mpls route add 200 dev lo + +# return path from n1 +ip -n n1 route add default via 172.16.1.1 + +# resolve ARP before MPLS test +ip netns exec n0 ping -c1 -n 172.16.0.1 + +# n0 pushes MPLS(100) -> grout swaps to MPLS(200) -> n1 pops +ip netns exec n0 ping -i0.01 -c3 -n 10.0.1.1