Hello BIRD maintainers,

I would like to ask for clarification about BIRD's MPLS L3VPN/VPNv4 nexthop
validation behavior. I am reporting this as a forwarding-correctness and
documentation concern, not as a security vulnerability or a strict RFC 4364
violation.

## Summary

With BIRD 2.19.0+, I can reproduce a case where a VPNv4 route with a VPN
service
label is accepted as a unicast route when its BGP NEXT_HOP is resolved only
by
an ordinary IP route. The resolved nexthop contains the VPN service label,
but
no transport label for reaching the remote PE.

In the reproduced setup:

```text
VPN prefix:        65000:100 203.0.113.0/24
BGP NEXT_HOP:      10.10.0.1
Underlay route:    10.10.0.1/32 via 192.0.2.1 dev lo onlink
VPN service label: 100
Transport label:   none
```

BIRD reports the route as reachable:

```text
65000:100 203.0.113.0/24 mpls 1000 unicast [vpn_peer1 ... from 127.0.0.11]
* (100/?) [AS65100i]
        via 192.0.2.1 on lo mpls 100 onlink
        BGP.next_hop: 10.10.0.1
        BGP.mpls_label_stack: 100
```

The important part is:

```text
via 192.0.2.1 on lo mpls 100 onlink
```

This appears to mean that the L3VPN route can resolve over an IP-only
recursive
NEXT_HOP and produce a nexthop carrying only the VPN service label.

## Version Tested

Reproduced against:

```text
BIRD version 2.19.0+branch.master.880200b1f94c
```

The PoC intentionally refuses to run against versions older than 2.19.0.

## Why I am asking

I understand that a single-label L3VPN packet is not always wrong. For
example,
it may be valid when the remote PE is directly adjacent, when PHP removes
the
transport label before the final hop, or when another explicitly configured
tunnel or encapsulation mechanism is used.

The case I am concerned about is a multi-hop MPLS L3VPN topology where the
BGP
NEXT_HOP is not the directly attached IGP next hop and no
GRE/SR/static-tunnel
exception is configured. In such a topology, an IP-only recursive route to
the
remote PE can cause BIRD to install/export a VPN-label-only nexthop. An
ordinary
P router would not know how to forward a packet that carries only the remote
VPN service label.

## Expected Behavior or Documentation

For MPLS L3VPN deployments that require a label-switched transport path to
the
remote PE, I expected BIRD either to verify that recursive NEXT_HOP
resolution
provides a transport label, or to clearly document that IP-only recursive
resolution is accepted and operators must ensure the required
transport/tunnel
by other means.

In other words, if the current behavior is intentional, it would be helpful
for
the documentation to say that `vpn4 mpls` routes may be considered reachable
through an unlabeled recursive IP nexthop, and that BIRD may emit only the
VPN
service label unless the underlay route itself provides transport labels.

## Control-Plane Reproduction

The attached PoC starts a private BIRD instance, installs an unlabeled
underlay
route to a remote PE address, and injects a VPNv4/MPLS route.

Run:

```sh
python3 poc_bird_l3vpn_labeled_nexthop.py --keep-workdir
```

Reproduced signal:

```text
RESULT: BIRD accepted the VPN route as unicast using only the VPN service
label.
```

The script exits with status `2` when the behavior is reproduced.

## Dataplane Verification

I also verified the dataplane consequence by reproducing the resulting Linux
route shape in an isolated user/network namespace:

```text
203.0.113.0/24 encap mpls 100 via 192.0.2.1 dev v0
```

The dataplane helper creates a veth pair, installs the MPLS route above,
sends
one packet to `203.0.113.1`, and parses the Ethernet/MPLS frame observed on
the
peer veth.

Run:

```sh
python3 dataplane_linux_mpls_label_stack.py
```

Observed output:

```text
===== dataplane route =====
203.0.113.0/24  encap mpls  100 via 192.0.2.1 dev v0
===== captured frame =====
ETH_TYPE: 0x8847
LABEL_STACK: [{'label': 100, 'exp': 0, 'bos': 1, 'ttl': 64}]
RESULT: reproduced VPN-label-only dataplane frame with a single MPLS label
100.
```

This confirms that a kernel route equivalent to BIRD's resolved nexthop
emits a
VPN-label-only MPLS frame on the dataplane.

## Source-Level Analysis

BIRD correctly parses the VPN label from the VPNv4 NLRI and applies it to
the
route nexthop.

In `proto/bgp/packets.c`, `bgp_decode_mpls_labels()` attaches the MPLS label
stack and then calls `bgp_apply_mpls_labels()`:

```c
bgp_set_attr_ptr(&(a->eattrs), s->pool, BA_MPLS_LABEL_STACK, 0, s->
mpls_labels);
...
bgp_apply_mpls_labels(s, a, labels, lnum);
```

For recursive BGP NEXT_HOPs, `bgp_apply_next_hop()` gets a hostentry from
the
IGP table:

```c
s->hostentry = rt_get_hostentry(tab, gw, lla, c->c.table);

if (!s->mpls)
  rta_apply_hostentry(a, s->hostentry, NULL);

/* With MPLS, hostentry is applied later in bgp_apply_mpls_labels() */
```

Then `bgp_apply_mpls_labels()` combines the recursive hostentry nexthop
with the
VPN label:

```c
mpls_label_stack ms;

ms.len = lnum;
memcpy(ms.stack, labels, 4*lnum);
rta_apply_hostentry(a, s->hostentry, &ms);
```

In `nest/rt-table.c`, `rta_apply_hostentry()` prepends any labels from the
recursive underlay nexthop and appends the VPN labels:

```c
nhp->labels = nh->labels + mls->len;
nhp->labels_orig = mls->len;
memcpy(nhp->label, nh->label, nh->labels * sizeof(u32));
memcpy(&(nhp->label[nh->labels]), mls->stack, mls->len * sizeof(u32));
```

This works when the recursive underlay route has transport labels. If
`nh->labels == 0`, the resulting nexthop still contains the VPN label only.

BIRD's generic BGP route resolvability check appears broader:

```c
static inline int
rte_resolvable(rte *rt)
{
  return rt->attrs->dest != RTD_UNREACHABLE;
}
```

I did not find an L3VPN-specific check that requires a labeled recursive
nexthop
before treating a VPN route as forwardable in deployments that depend on
MPLS
transport labels.

## Comparison with FRR

FRR appears to distinguish ordinary IP reachability from labeled-nexthop
reachability for L3VPN.

In `bgpd/bgp_nht.c`, FRR documents the condition:

```c
/*
 * In case of unicast routes that were imported from vpn
 * and that have labels, they are valid only if there are
 * nexthops with labels
 */
```

The L3VPN nexthop check requires one of the accepted MPLS-forwarding
conditions:

```c
return (bnc && (bnc->nexthop_num > 0 &&
        (CHECK_FLAG(path->flags, BGP_PATH_ACCEPT_OWN) ||
         CHECK_FLAG(bnc->flags, BGP_NEXTHOP_LABELED_VALID) ||
         bgp_isvalid_nexthop_for_ebgp(bnc, path) ||
         bgp_isvalid_nexthop_for_mplsovergre(bnc, path))));
```

`BGP_NEXTHOP_LABELED_VALID` is set only when the recursive nexthop received
from
zebra contains labels:

```c
if (nexthop->nh_label && nexthop->nh_label->num_labels) {
  SET_FLAG(bnc->flags, BGP_NEXTHOP_LABELED_VALID);
}
```

I am not suggesting that BIRD must copy FRR's behavior exactly. I am using
this
as a comparison point because FRR makes the transport-label requirement
explicit
for VPN-imported labeled routes, with documented exceptions.

## Impact

In a multi-hop MPLS L3VPN topology where the BGP NEXT_HOP is not the
directly
attached IGP next hop and no GRE/SR/static-tunnel exception is configured,
BIRD
may install or export an L3VPN route whose outgoing label stack contains
only
the VPN service label.

Potential consequences:

1. VPN traffic may be blackholed in the MPLS underlay.
2. Operators may see the route as installed and reachable even though MPLS
   forwarding cannot deliver it correctly in that topology.
3. BIRD behavior may surprise operators coming from implementations that
   explicitly require a labeled transport path for MPLS VPN routes.

The impact is configuration-dependent. It may not apply to directly adjacent
L3VPN deployments, PHP-at-final-hop cases, or deployments that explicitly
use
another encapsulation model.

## Suggested Fix or Clarification

If this behavior is unintentional, BIRD could add an L3VPN-specific
forwardability check before treating VPN routes as resolvable in recursive
MPLS-underlay deployments:

1. When a VPN route has a service label and recursive NEXT_HOP resolution is
   used, verify that the resolved underlay nexthop carries at least one
   transport label.
2. If the recursive underlay route is IP-only and no accepted exception
applies,
   mark the VPN route unreachable or avoid exporting/installing it as an
   MPLS-forwardable L3VPN route.
3. Consider explicit exceptions for directly connected L3VPN, GRE/tunnel
   forwarding, SR, PHP-sensitive cases, or other documented encapsulation
modes.

If the current behavior is intentional, documenting it would help operators
understand that BIRD may resolve VPN routes over unlabeled IP underlay
routes
and emit only the VPN service label unless they ensure the transport path
separately.

Thank you for taking a look.

Reply via email to