Skip to content

Add MPLS support - #641

Draft
mmuzila wants to merge 7 commits into
DPDK:mainfrom
mmuzila:add_mpls
Draft

Add MPLS support#641
mmuzila wants to merge 7 commits into
DPDK:mainfrom
mmuzila:add_mpls

Conversation

@mmuzila

@mmuzila mmuzila commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

No description provided.

@vjardin

vjardin commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Please, can you explain how you would integrate it with frrouting ?

@mmuzila

mmuzila commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Please, can you explain how you would integrate it with frrouting ?

AFAIK the FRR zebra dplane plugin already handles IP routes, nexthops, and SRv6 bidirectionally.
MPLS would follow the same pattern:

FRR -> grout : FRR sends DPLANE_OP_LSP_INSTALL/UPDATE/DELETE when LDP, OSPF-SR,
ISIS-SR, or static MPLS configures a label switch. The plugin would extract the incoming
label and NHLFEs from the dplane context, create GR_NH_T_MPLS nexthops in grout (via
GR_NH_ADD), then bind them to the label with GR_MPLS_LABEL_ROUTE_ADD. For ECMP (multiple
NHLFEs), wrap individual MPLS nexthops in a GR_NH_T_GROUP.

grout -> FRR: Subscribe to GR_EVENT_MPLS_ROUTE_ADD/DEL in the zebra notification
handler, then call FRR's mpls_lsp_install()/mpls_lsp_uninstall() to reflect
externally-created label routes (e.g. from grcli) into FRR's LSP table.

Startup sync: After the existing nexthop/route sync passes, add a pass that streams
GR_MPLS_LABEL_ROUTE_LIST from grout and injects each entry into FRR via mpls_lsp_install().

The main complexity is that FRR's LSP NHLFEs don't carry nexthop IDs, so the plugin needs a
deterministic ID scheme (e.g. base_offset | (label << 4) | nhlfe_index) to
create/update/delete the grout nexthops. All of this goes in frr/rt_grout.c and
frr/zebra_dplane_grout.c, following the exact patterns already used for IP routes and SRv6.

Comment thread modules/mpls/api/gr_mpls.h Outdated
Comment on lines +20 to +25
// Reserved MPLS label values (RFC 3032).
#define GR_MPLS_LABEL_IPV4_EXPLICIT_NULL 0
#define GR_MPLS_LABEL_ROUTER_ALERT 1
#define GR_MPLS_LABEL_IPV6_EXPLICIT_NULL 2
#define GR_MPLS_LABEL_IMPLICIT_NULL 3
#define GR_MPLS_LABEL_FIRST_UNRESERVED 16

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to store these in an enum?

Comment thread modules/mpls/api/gr_mpls.h Outdated
Comment on lines +30 to +35
addr_family_t via_af; // Gateway address family (GR_AF_IP4 or GR_AF_IP6).
uint8_t _pad;
union {
ip4_addr_t ipv4;
struct rte_ipv6_addr ipv6;
} via; // Gateway address for ARP/NDP resolution.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could replace all of this with:

	struct l3_addr via;

Comment thread modules/mpls/control/mpls.h Outdated
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);

extern control_input_t mpls_output_node;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This internal identifier probably shouldn't be exposed in a header. See below...

Comment thread modules/mpls/control/resolve.c Outdated
Comment on lines +15 to +25
control_input_t mpls_output_node;

static 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_node, m) < 0) {
LOG(ERR, "post_to_stack: %s", strerror(errno));
return -errno;
}
return 0;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you move this to datapath/mpls_output.c and keep the control_input_t value static? See ndp_na_output for reference:

static control_input_t na_output;
int nh6_advertise(const struct nexthop *local, const struct nexthop *remote) {
assert(local != NULL);
assert(local->type == GR_NH_T_L3);
struct advertise_context *ctx = malloc(sizeof(*ctx));
if (ctx == NULL)
return errno_set(ENOMEM);
ctx->local = local;
ctx->remote = remote;
ctx->iface = iface_from_id(local->iface_id);
assert(ctx->iface != NULL);
int ret = post_to_stack(na_output, ctx);
if (ret < 0)
free(ctx);
return ret;
}

Comment thread modules/mpls/control/resolve.c Outdated
Comment on lines +109 to +111
static void mpls_resolve_init(struct event_base *) {
mpls_output_node = gr_control_input_register_handler("mpls_output", true);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be moved as a .register_callback in datapath/mpls_output.c as it is done in ndp_na_output:

static void ndp_na_output_register(void) {
na_output = gr_control_input_register_handler("ndp_na_output", false);
}

Comment on lines +64 to +88
if (label < GR_MPLS_LABEL_FIRST_UNRESERVED) {
rte_pktmbuf_adj(mbuf, sizeof(*mpls));

if (label == GR_MPLS_LABEL_IPV4_EXPLICIT_NULL) {
mbuf->packet_type = RTE_PTYPE_L3_IPV4;
edge = IP_INPUT;
} else if (label == GR_MPLS_LABEL_IPV6_EXPLICIT_NULL) {
mbuf->packet_type = RTE_PTYPE_L3_IPV6;
edge = IP6_INPUT;
} else if (label == 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;
}
} else {
edge = BAD_LABEL;
}
break;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like it could be rewritten with a switch case block.

Comment thread modules/mpls/datapath/mpls_input.c Outdated

struct rte_mpls_hdr trace_hdr = *rte_pktmbuf_mtod(mbuf, struct rte_mpls_hdr *);

while (true) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not fond of while (true) blocks in datapath nodes. Since we know there is a limit of GR_MPLS_MAX_LABELS, why not clamp the loop to this value so that the compiler has a chance to unroll the loop.

Comment thread modules/mpls/datapath/mpls_input.c Outdated

if (bos) {
rte_pktmbuf_adj(mbuf, sizeof(*mpls));
uint8_t ver = *rte_pktmbuf_mtod(mbuf, uint8_t *) >> 4;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fragile. The MPLS FIB should contain the necessary info about what payload is next. Maybe info->via.af?

I don't know how other stacks do this but it seems to me we shouldn't try to guess what is next.

csum = (csum >> 16) + (csum & 0xffff);
csum += csum >> 16;
ip->hdr_checksum = ~(uint16_t)csum;
mbuf->packet_type = RTE_PTYPE_L3_IPV4;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need this convoluted casting? Maybe this could be simplified by reusing the existing fixup_checksum_* functions from nat_datapath.h (which should be moved into a more generic place first):

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;

mmuzila added 7 commits July 27, 2026 13:59
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 <mmuzila@redhat.com>
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 <mmuzila@redhat.com>
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 <mmuzila@redhat.com>
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 <mmuzila@redhat.com>
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 <mmuzila@redhat.com>
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 <mmuzila@redhat.com>
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 <mmuzila@redhat.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants