From 8dab2a830317e198dfd1668df46337d64c1f0438 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Mu=C5=BEila?= Date: Mon, 25 May 2026 15:47:24 +0200 Subject: [PATCH 1/7] infra: add MPLS nexthop type and address family MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce the basic infrastructure types needed for MPLS support. Add GR_NH_T_MPLS to the nexthop type enum for MPLS label imposition and swap operations. Add GR_AF_MPLS (AF_MPLS = 28) to the address family enum so that MPLS can register per-AF operations such as nexthop resolution callbacks and per-VRF FIB lifecycle management. Reserve a fib_mpls slot in the private VRF struct for the per-VRF label forwarding table that will be populated by the MPLS module. Extend nexthop_af_ops_from_mbuf() to recognize MPLS packets held for ARP resolution. Packets marked with RTE_PTYPE_TUNNEL_MPLS_IN_GRE are dispatched to the MPLS AF ops so that they get resubmitted to the correct datapath node after the gateway nexthop becomes reachable. Signed-off-by: Matej Mužila --- api/gr_net_types.h | 4 ++++ modules/infra/api/gr_nexthop.h | 3 +++ modules/infra/control/l3_nexthop.c | 6 ++++++ modules/infra/control/nexthop.c | 1 + modules/infra/control/vrf.h | 1 + modules/l2/control/vxlan.c | 1 + 6 files changed, 16 insertions(+) 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/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; } From da9b9375cd5f3bb5d56bff8b9810021bfd4d93ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Mu=C5=BEila?= Date: Mon, 27 Jul 2026 10:57:41 +0200 Subject: [PATCH 2/7] infra: move checksum helpers to generic header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract fixup_checksum_16 and fixup_checksum_32 from nat_datapath.h into a standalone checksum.h so they can be reused outside of the NAT module. nat_datapath.h now includes the new header. Signed-off-by: Matej Mužila --- modules/infra/datapath/checksum.h | 33 ++++++++++++++++++++++++++ modules/policy/datapath/nat_datapath.h | 30 +---------------------- 2 files changed, 34 insertions(+), 29 deletions(-) create mode 100644 modules/infra/datapath/checksum.h 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/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, From 3ddda57fd0ffb1a4327d1e8390fde5c2ddf32011 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Mu=C5=BEila?= Date: Mon, 25 May 2026 16:05:57 +0200 Subject: [PATCH 3/7] mpls: add public API header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The nexthop info struct carries the output label stack, initial TTL, a gateway address for ARP/NDP resolution, and an optional payload type for egress label disposition. MPLS nexthops are managed through the generic GR_NH_ADD/DEL API, same as SRv6. Label routes map an incoming label to a nexthop in the per-VRF LFIB. The API provides add, delete, get, and streaming list operations with the corresponding event types for route change notifications. Signed-off-by: Matej Mužila --- modules/mpls/api/gr_mpls.h | 101 +++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 modules/mpls/api/gr_mpls.h 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); From b31f703bc058b2799b28dc9133e6e8869a17e635 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Mu=C5=BEila?= Date: Mon, 22 Jun 2026 15:29:57 +0200 Subject: [PATCH 4/7] mpls: add control plane module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement the MPLS nexthop type operations, the per-VRF label forwarding information base, and the API request handlers. The nexthop type ops handle creation, update, and teardown of MPLS nexthops. Each MPLS nexthop holds a reference to an L3 nexthop for gateway resolution. The LFIB is a flat array of nexthop pointers indexed directly by the 20-bit label value, allocated per VRF via the vrf_fib_ops mechanism. Signed-off-by: Matej Mužila --- modules/meson.build | 1 + modules/mpls/api/meson.build | 5 + modules/mpls/control/label.c | 109 +++++++++++++++++ modules/mpls/control/label_table.c | 184 +++++++++++++++++++++++++++++ modules/mpls/control/meson.build | 8 ++ modules/mpls/control/mpls.h | 26 ++++ modules/mpls/control/nexthop.c | 142 ++++++++++++++++++++++ modules/mpls/meson.build | 5 + 8 files changed, 480 insertions(+) create mode 100644 modules/mpls/api/meson.build create mode 100644 modules/mpls/control/label.c create mode 100644 modules/mpls/control/label_table.c create mode 100644 modules/mpls/control/meson.build create mode 100644 modules/mpls/control/mpls.h create mode 100644 modules/mpls/control/nexthop.c create mode 100644 modules/mpls/meson.build 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/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/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..cb726c454 --- /dev/null +++ b/modules/mpls/control/meson.build @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2025 Matej Muzila + +src += files( + 'label.c', + 'label_table.c', + 'nexthop.c', +) 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/meson.build b/modules/mpls/meson.build new file mode 100644 index 000000000..7d9a19856 --- /dev/null +++ b/modules/mpls/meson.build @@ -0,0 +1,5 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2025 Matej Muzila + +subdir('api') +subdir('control') From d1a6cbfe66632f25722db3c904b2f9be002a9f1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Mu=C5=BEila?= Date: Mon, 22 Jun 2026 16:17:01 +0200 Subject: [PATCH 5/7] mpls: add datapath nodes and resolve callbacks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement the three MPLS graph nodes (input, output, push) and the control plane callbacks for held packet resolution. The input node decodes the MPLS label stack in a bounded loop since rte_graph forbids self-loop edges. It handles label swap, pop with TTL propagation (RFC 1624 incremental checksum update for IPv4), and reserved labels 0/2/3. When popping the last label, the payload type is determined from the nexthop's payload_af field if set, falling back to IP version nibble detection otherwise. The output node resolves the gateway MAC from the via L3 nexthop. For unresolved gateways, the MPLS nexthop pointer is stashed in the mbuf priv data overlay before entering ip_hold, and restored on resubmit via the MPLS AF ops. The push node is dispatched from ip_output and ip6_output for MPLS nexthops. It checks MTU accounting for the label overhead, reads the payload TTL, and prepends the label stack. Signed-off-by: Matej Mužila --- modules/mpls/control/meson.build | 2 + modules/mpls/control/resolve.c | 97 +++++++++++++ modules/mpls/datapath/meson.build | 9 ++ modules/mpls/datapath/mpls_datapath.h | 26 ++++ modules/mpls/datapath/mpls_input.c | 197 ++++++++++++++++++++++++++ modules/mpls/datapath/mpls_output.c | 137 ++++++++++++++++++ modules/mpls/datapath/mpls_push.c | 116 +++++++++++++++ modules/mpls/meson.build | 1 + 8 files changed, 585 insertions(+) create mode 100644 modules/mpls/control/resolve.c create mode 100644 modules/mpls/datapath/meson.build create mode 100644 modules/mpls/datapath/mpls_datapath.h create mode 100644 modules/mpls/datapath/mpls_input.c create mode 100644 modules/mpls/datapath/mpls_output.c create mode 100644 modules/mpls/datapath/mpls_push.c diff --git a/modules/mpls/control/meson.build b/modules/mpls/control/meson.build index cb726c454..b404cc399 100644 --- a/modules/mpls/control/meson.build +++ b/modules/mpls/control/meson.build @@ -5,4 +5,6 @@ src += files( 'label.c', 'label_table.c', 'nexthop.c', + 'resolve.c', ) +inc += include_directories('.') 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 index 7d9a19856..870666fd5 100644 --- a/modules/mpls/meson.build +++ b/modules/mpls/meson.build @@ -3,3 +3,4 @@ subdir('api') subdir('control') +subdir('datapath') From cac545eaed435e87416eae57191f3ffd04bcf7df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Mu=C5=BEila?= Date: Mon, 29 Jun 2026 14:13:16 +0200 Subject: [PATCH 6/7] mpls: add CLI commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Register grcli commands for MPLS nexthop creation and label route management. The nexthop command supports push, pop, optional TTL override, and explicit payload type for egress disposition. A formatter is registered so that nexthop show displays MPLS-specific fields. Label route commands use a separate CLI context since label routes are keyed by numeric labels and cannot reuse the existing IP route CLI dispatch. Event printers for label route add/del notifications are also registered. Signed-off-by: Matej Mužila --- modules/mpls/cli/label.c | 213 +++++++++++++++++++++++++++++++++++ modules/mpls/cli/meson.build | 8 ++ modules/mpls/cli/nexthop.c | 191 +++++++++++++++++++++++++++++++ modules/mpls/meson.build | 1 + 4 files changed, 413 insertions(+) create mode 100644 modules/mpls/cli/label.c create mode 100644 modules/mpls/cli/meson.build create mode 100644 modules/mpls/cli/nexthop.c 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/meson.build b/modules/mpls/meson.build index 870666fd5..8bc77a9c3 100644 --- a/modules/mpls/meson.build +++ b/modules/mpls/meson.build @@ -2,5 +2,6 @@ # Copyright (c) 2025 Matej Muzila subdir('api') +subdir('cli') subdir('control') subdir('datapath') From 6969044062b82f3cb0d9f9a8534632b55275c009 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Mu=C5=BEila?= Date: Thu, 9 Jul 2026 10:25:28 +0200 Subject: [PATCH 7/7] mpls: add smoke tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cover the main MPLS datapath operations: label push, swap, pop with both auto-detected and explicit payload type, and IPv4 explicit null decapsulation. A separate CLI test exercises nexthop and label route CRUD through grcli. Tests that send MPLS-encapsulated traffic from Linux namespaces pre-resolve ARP with a plain IP ping before the MPLS test to avoid a race between the iptunnel encap path and neighbor resolution. Signed-off-by: Matej Mužila --- smoke/mpls_cli_test.sh | 56 ++++++++++++++++++++++++++++++++ smoke/mpls_explicit_null_test.sh | 38 ++++++++++++++++++++++ smoke/mpls_pop_payload_test.sh | 42 ++++++++++++++++++++++++ smoke/mpls_pop_test.sh | 42 ++++++++++++++++++++++++ smoke/mpls_push_test.sh | 43 ++++++++++++++++++++++++ smoke/mpls_swap_test.sh | 47 +++++++++++++++++++++++++++ 6 files changed, 268 insertions(+) create mode 100755 smoke/mpls_cli_test.sh create mode 100755 smoke/mpls_explicit_null_test.sh create mode 100755 smoke/mpls_pop_payload_test.sh create mode 100755 smoke/mpls_pop_test.sh create mode 100755 smoke/mpls_push_test.sh create mode 100755 smoke/mpls_swap_test.sh 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