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.