6VPE (VPNv6 over IPv4 MPLS): IPv6 route with IPv4 next-hop rejected by kernel — root cause found, patch attached
Hello, While preparing a question about this symptom, I found the root cause and put together a working, tested patch against sysdep/linux/netlink.c. Posting the fix directly rather than just the question, since a patch is more actionable than a symptom report. Happy to open this as a GitLab MR if that's useful — wanted to get feedback from the list / maintainers first, since I'm not fully sure this is the right layer to fix it at. == Environment == BIRD: 3.3.1 (bird3 3.3.1-cznic.1~trixie, Debian 13 test box) Kernel: 6.12.86 (Debian test), production runs Gentoo Role: egress PE, seamless-MPLS BGP L3VPN (AS 197296), IPv4-only underlay (OSPFv2 + BGP-LU), adding IPv6 as 6VPE (RFC 4659) over the IPv4 transport, extended next hop capability enabled on the vpn6 channel, no IPv6 IGP / LDPv6 anywhere in the underlay. == Symptom (unchanged from the original report) == Control plane resolution is complete and correct — BIRD resolves the VPNv6 route recursively via the IPv4 labeled table and builds the correct double MPLS label stack: show route table vpntab6 all 2a02:d280:201::/64 mpls 1207 unicast [proto_bgp_rr] * (100/?) [?] via 172.21.1.1 on lan mpls 24482/16 hostentry: via 100.127.1.26 table master4_lu mpls 16 But kernel export fails for these routes: show route table table_vpn6_overlay 2a02:d280:201::/64 unicast [l3_overlay] ! (80/?) via 172.21.1.1 on lan mpls 24482/16 <bird>: Netlink: Invalid argument (repeated, once per scan cycle) == Root cause == Confirmed by manual reproduction outside BIRD: # ip -6 route add 2a02:d280:300::/40 encap mpls 24481/1033 via inet 172.21.1.1 dev lan table 20 Error: IPv6 does not support RTA_VIA attribute. net/ipv6/route.c in mainline Linux does not accept RTA_VIA (the netlink attribute for a cross-family nexthop) on an IPv6 route, full stop — regardless of whether the address is a raw IPv4 address or an IPv4-mapped IPv6 address. Since our underlay is IPv4-only, every VPNv6 route we import resolves (via the IPv4-labeled IGP table) to a physically IPv4-family adjacency, so every such route hits this kernel limitation. In sysdep/linux/netlink.c, nl_add_nexthop() is exactly where this happens: if (ipa_nonzero(nh->gw)) { if (af == (ipa_is_ip4(nh->gw) ? AF_INET : AF_INET6)) nl_add_attr_ipa(h, bufsize, RTA_GATEWAY, nh->gw); /* same family, OK */ else nl_add_attr_via(h, bufsize, nh->gw); /* cross-family -> rejected for v6 */ } The MPLS encap (nl_add_attr_mpls_encap(), a few lines above) is completely independent of this gateway-family branch — it's already correctly built from the hostentry-resolved label stack before the gateway is even considered. So fixing the gateway encoding does not touch the label stack at all. This looks like the mirror case of RFC 8950 (v4-via-v6, "IPv6 next hop for IPv4 NLRI", which the kernel does support): what we need here is the opposite direction, v6-via-v4, for which the kernel intentionally has no FIB representation. For an MPLS-encapsulated route, though, the gateway is only used to pick the L2 adjacency (the packet leaves as an MPLS frame, so the address family of the nexthop is otherwise irrelevant to forwarding), which means we don't actually need the kernel to support v6-via-v4 in general — we just need *some* valid on-link IPv6 address for that neighbor. == Two things I ruled out first (documenting so nobody re-treads this) == 1. Overriding gw in a kernel export filter (`if ifname = "lan" then gw = fe80::1;`): the route becomes installable, but overriding gw forces BIRD to recompute the nexthop as non-recursive/explicit, which drops the label stack from the hostentry resolution. Installable but non-forwarding. 2. BGP-side "next hop address" / extended-nexthop config changes: these only affect what *we* advertise for routes *we* originate. They have no effect on how imported routes (from the RR, next-hop = remote PE loopback) get resolved — that resolution goes through our IPv4-only IGP table (master4_lu) regardless of how our own next-hop attribute is encoded. Also, disabling extended-nexthop entirely makes BIRD reject the config outright ("Mismatched IGP table type") since it then requires the next-hop family to match the IGP table family. Verified both variants live against production traffic; neither changes the kernel-export outcome. == The patch == Confirmed the neighboring router already sends IPv6 Router Advertisements on the segment (independent of any IPv6 IGP — RA is link-scoped), so the kernel already keeps a working ND cache entry for it: ip -6 neigh show dev lan fe80::1 lladdr dc:68:0c:32:ed:b2 router STALE The attached patch adds a small on-link-router cache to netlink.c (mirrors the existing AF_BRIDGE FDB cache/parsing that's already in the file, just generalized to AF_INET6 neighbors flagged NTF_ROUTER), refreshed once per KRT scan cycle. In nl_add_nexthop(), when exporting an IPv6 route whose resolved gateway is IPv4-family, it looks up this cache by egress interface and substitutes the neighbor's real link-local address for RTA_GATEWAY instead of building the (rejected) RTA_VIA — the MPLS encap is untouched. No interface name or address is hardcoded anywhere; it's fully derived from what the kernel already knows via ND. If the cache has no entry yet for that interface, behavior is unchanged from today (route stays uninstalled, self-heals on the next scan) — no regression. Diff attached: 0001-netlink-ipv6-onlink-router-cache-for-6vpe-mpls-nexthop.diff (against v3.3.1, 4 hunks, all confined to sysdep/linux/netlink.c). == Test results (live, on a production-representative PE) == show route table table_vpn6_overlay -> routes now "*" (were "!") ip -6 route show table 20 2a02:d280:201::/64 encap mpls 24482/16 via fe80::1 dev lan proto bird ip -6 route get 2a02:d280:200::1 ... encap mpls 24481/1032 via fe80::1 dev lan ... (correct FIB lookup) ip -6 neigh show dev lan fe80::1 lladdr dc:68:0c:32:ed:b2 router REACHABLE journalctl -u bird -> no more "Netlink: Invalid argument" End-to-end ping from a live CPE through the fixed path: 171/171 received, 0% loss. == Questions for the list / maintainers == 1. Is this an acceptable place/approach to fix this, or would you rather see it handled differently (e.g. in rt-table.c nexthop resolution itself, or as an explicit config knob rather than automatic ND-based discovery)? 2. Known edge case: if a segment ever has more than one NTF_ROUTER neighbor on the same interface, the cache currently keeps whichever one was last seen in the dump (non-deterministic ordering). Not an issue in our topology (always exactly one router per segment), but worth flagging. 3. Cache refresh is tied to the existing KRT scan interval rather than the async netlink notification path, to avoid touching the shared AF_BRIDGE dispatch code. Happy to change this if an event-driven refresh is preferred upstream. Willing to open this as a merge request, test further, or provide more captures/config context — just let me know the preferred path. Thanks a lot for BIRD. Aleš Kopecký AS 197296 ales.kopecky@u-n.cz Attachments: 0001-netlink-ipv6-onlink-router-cache-for-6vpe-mpls-nexthop.diff
Hello Aleš, On Fri, Jul 10, 2026 at 02:20:42PM +0200, Aleš Kopecký via Bird-users wrote:
[...]
Role: egress PE, seamless-MPLS BGP L3VPN (AS 197296), IPv4-only underlay (OSPFv2 + BGP-LU), adding IPv6 as 6VPE (RFC 4659) over the IPv4 transport, extended next hop capability enabled on the vpn6 channel, no IPv6 IGP / LDPv6 anywhere in the underlay.
This looks a little bit cursed in itself but whatever. There are probably legitimate uses of an IPv4-only underlay.
== Symptom (unchanged from the original report) == Control plane resolution is complete and correct — BIRD resolves the VPNv6 route recursively via the IPv4 labeled table and builds the correct double MPLS label stack:
show route table vpntab6 all 2a02:d280:201::/64 mpls 1207 unicast [proto_bgp_rr] * (100/?) [?] via 172.21.1.1 on lan mpls 24482/16 hostentry: via 100.127.1.26 table master4_lu mpls 16
But kernel export fails for these routes:
show route table table_vpn6_overlay 2a02:d280:201::/64 unicast [l3_overlay] ! (80/?) via 172.21.1.1 on lan mpls 24482/16
<bird>: Netlink: Invalid argument (repeated, once per scan cycle)
== Root cause == Confirmed by manual reproduction outside BIRD:
# ip -6 route add 2a02:d280:300::/40 encap mpls 24481/1033 via inet 172.21.1.1 dev lan table 20 Error: IPv6 does not support RTA_VIA attribute.
net/ipv6/route.c in mainline Linux does not accept RTA_VIA (the netlink attribute for a cross-family nexthop) on an IPv6 route, full stop — regardless of whether the address is a raw IPv4 address or an IPv4-mapped IPv6 address. Since our underlay is IPv4-only, every VPNv6 route we import resolves (via the IPv4-labeled IGP table) to a physically IPv4-family adjacency, so every such route hits this kernel limitation.
My primary thought would be to push this into the kernel, or to deploy an IPv6 underlay. Do I assume correctly that your whole network has been IPv4-only until now, and you are trying to get IPv6 working, which means that deploying an IPv6 overlay would be way harder than doing this kind of hack?
[...]
== The patch == Confirmed the neighboring router already sends IPv6 Router Advertisements on the segment (independent of any IPv6 IGP — RA is link-scoped), so the kernel already keeps a working ND cache entry for it:
ip -6 neigh show dev lan fe80::1 lladdr dc:68:0c:32:ed:b2 router STALE
Well, this is in itself kinda fragile, as we can't really take for granted that an RA would arrive.
The attached patch adds a small on-link-router cache to netlink.c (mirrors the existing AF_BRIDGE FDB cache/parsing that's already in the file, just generalized to AF_INET6 neighbors flagged NTF_ROUTER), refreshed once per KRT scan cycle. In nl_add_nexthop(), when exporting an IPv6 route whose resolved gateway is IPv4-family, it looks up this cache by egress interface and substitutes the neighbor's real link-local address for RTA_GATEWAY instead of building the (rejected) RTA_VIA — the MPLS encap is untouched. No interface name or address is hardcoded anywhere; it's fully derived from what the kernel already knows via ND. If the cache has no entry yet for that interface, behavior is unchanged from today (route stays uninstalled, self-heals on the next scan) — no regression.
I'm not reading the patch in full but it looks like you expect that the link is point-to-point, i.e. there is only one remote router per link. That is generally not true. You would actually need a full translation table between IPv4 and IPv6 next hop, populated by reading both ARP and ND cache and matching the entries. And even that needs some caution.
[...]
== Questions for the list / maintainers == 1. Is this an acceptable place/approach to fix this, or would you rather see it handled differently (e.g. in rt-table.c nexthop resolution itself, or as an explicit config knob rather than automatic ND-based discovery)?
It looks like a crude hack with no inspectability. We generally prefer tables to be accessible by users from CLI.
2. Known edge case: if a segment ever has more than one NTF_ROUTER neighbor on the same interface, the cache currently keeps whichever one was last seen in the dump (non-deterministic ordering). Not an issue in our topology (always exactly one router per segment), but worth flagging.
This is not an edge case, this is a blocker for upstreaming.
3. Cache refresh is tied to the existing KRT scan interval rather than the async netlink notification path, to avoid touching the shared AF_BRIDGE dispatch code. Happy to change this if an event-driven refresh is preferred upstream.
I would expect that a reasonable ARP-ND cache would be actually tied to both, as other tables are. This whole topic will probably need a senior meeting to discus possible caveats in the implementation, so that BIRD stays maintainable and well-performing into future. But before I put this into our backlog, there may be some tricks hidden in the hostentry mechanism and autopeering, which could resolve your whole problem way easier. Or not, I don't have enough information. We can look into your problem best effort if you send us your config and tell us more about how the network actually looks like, or it can be done faster if you include bird-partner@nic.cz in the loop and get [BIRD Support](https://bird.nic.cz/commercial-services/#deployments-isp). I hope this helps. Note: I see various indications that the e-mail I'm replying to was written by Claude Sonnet 5, and I would appreciate disclosing such information. We are experimenting with using LLMs to aid development, currently still with miserable results, and knowing which LLMs generate which kind of output, would be nice, to better calibrate our approach to LLMs. -- Maria Matejka (she/her) | BIRD Team Leader | CZ.NIC, z.s.p.o.
Hi Maria, Thanks for the quick and substantive reply and honestly for the criticism of the patch. I really appreciate it. I´m sending my LAB bird.conf and network description. If you can solve my problem more clearly than my patch that would be great. We are a local ISP with multiple vendors access PEs. It is not realistic to switch the underlay to IPv6 for now. Here are my answers in the text: Hello Aleš, On Fri, Jul 10, 2026 at 02:20:42PM +0200, Aleš Kopecký via Bird-users wrote: […] Role: egress PE, seamless-MPLS BGP L3VPN (AS 197296), IPv4-only underlay (OSPFv2 + BGP-LU), adding IPv6 as 6VPE (RFC 4659) over the IPv4 transport, extended next hop capability enabled on the vpn6 channel, no IPv6 IGP / LDPv6 anywhere in the underlay. This looks a little bit cursed in itself but whatever. There are probably legitimate uses of an IPv4-only underlay. Seamless because BIRD has no MPLS LDP. I read that it will be in the feature, but now I solved it by importing/exporting BGP-LU in the core switch HPE5940. == Symptom (unchanged from the original report) == Control plane resolution is complete and correct — BIRD resolves the VPNv6 route recursively via the IPv4 labeled table and builds the correct double MPLS label stack: show route table vpntab6 all 2a02:d280:201::/64 mpls 1207 unicast [proto_bgp_rr] * (100/?) [?] via 172.21.1.1 on lan mpls 24482/16 hostentry: via 100.127.1.26 table master4_lu mpls 16 But kernel export fails for these routes: show route table table_vpn6_overlay 2a02:d280:201::/64 unicast [l3_overlay] ! (80/?) via 172.21.1.1 on lan mpls 24482/16 : Netlink: Invalid argument (repeated, once per scan cycle) == Root cause == Confirmed by manual reproduction outside BIRD: # ip -6 route add 2a02:d280:300::/40 encap mpls 24481/1033 via inet 172.21.1.1 dev lan table 20 Error: IPv6 does not support RTA_VIA attribute. net/ipv6/route.c in mainline Linux does not accept RTA_VIA (the netlink attribute for a cross-family nexthop) on an IPv6 route, full stop — regardless of whether the address is a raw IPv4 address or an IPv4-mapped IPv6 address. Since our underlay is IPv4-only, every VPNv6 route we import resolves (via the IPv4-labeled IGP table) to a physically IPv4-family adjacency, so every such route hits this kernel limitation. My primary thought would be to push this into the kernel, or to deploy an IPv6 underlay. Do I assume correctly that your whole network has been IPv4-only until now, and you are trying to get IPv6 working, which means that deploying an IPv6 overlay would be way harder than doing this kind of hack? We actually have IPv4 only and dual stack in some localities. But we have some troubles with memory leak since deploying OSPFv3 on boxes. So this is another reason we want to drop the dual stack. And this hardware also has no LDPv6 or any other MPLS label distribution protocol like SR. So we are looking for a sustainable solution. 6VPE over IPv4 works nice on those boxes although it's 10 years old HW. It's not real to change for a new HW fast and good price. […] == The patch == Confirmed the neighboring router already sends IPv6 Router Advertisements on the segment (independent of any IPv6 IGP — RA is link-scoped), so the kernel already keeps a working ND cache entry for it: ip -6 neigh show dev lan fe80::1 lladdr dc:68:0c:32:ed:b2 router STALE Well, this is in itself kinda fragile, as we can’t really take for granted that an RA would arrive. In my setup I have only a core L3 switch and one Linux ISP GW (shaper) link in the broadcast domain. I have it in LAB and I have static IP addresses configuration so it works fine. I tried to switch it to auto link-local address configuration and my patch doesn't work until I send RA from peer. This confirms your point: relying on the NTF_ROUTER flag is fragile because RA isn't guaranteed (peer doesn't send RA by default). This points to what may be the cleaner approach you hinted at: BIRD already knows the exact IPv4 adjacency it resolved through (access PE in the hostentry above). If that specific adjacency could be translated to its IPv6 link-local counterpart at export time — instead of scanning the ND cache for a router flag — it would remove both the RA dependency and the multi-router ambiguity in one go. Is that what the hostentry mechanism could give us? The attached patch adds a small on-link-router cache to netlink.c (mirrors the existing AF_BRIDGE FDB cache/parsing that’s already in the file, just generalized to AF_INET6 neighbors flagged NTF_ROUTER), refreshed once per KRT scan cycle. In nl_add_nexthop(), when exporting an IPv6 route whose resolved gateway is IPv4-family, it looks up this cache by egress interface and substitutes the neighbor’s real link-local address for RTA_GATEWAY instead of building the (rejected) RTA_VIA — the MPLS encap is untouched. No interface name or address is hardcoded anywhere; it’s fully derived from what the kernel already knows via ND. If the cache has no entry yet for that interface, behavior is unchanged from today (route stays uninstalled, self-heals on the next scan) — no regression. I’m not reading the patch in full but it looks like you expect that the link is point-to-point, i.e. there is only one remote router per link. That is generally not true. You would actually need a full translation table between IPv4 and IPv6 next hop, populated by reading both ARP and ND cache and matching the entries. And even that needs some caution. In my setup the link between Linux and peer L3 switch is always ptp (exactly 2 routers in the broadcast domain). But you are right, it is not generally true. […] == Questions for the list / maintainers == 1. Is this an acceptable place/approach to fix this, or would you rather see it handled differently (e.g. in rt-table.c nexthop resolution itself, or as an explicit config knob rather than automatic ND-based discovery)? It looks like a crude hack with no inspectability. We generally prefer tables to be accessible by users from CLI. I understand. My solution isn' t transparent. *root@bird-dev:/etc/network# birdc show route table table_vpn6_overlayBIRD 3.3.1 ready.Table table_vpn6_overlay:::/0 unicast [static_wan6_overlay 16:19:01.992] * (200) via 2a02:d280:10:0:46:23:58:137 on wan2a02:d280:201::/64 unicast [l3_overlay 16:19:03.850] * (80/?) via 172.21.1.1 on lan mpls 24480/162a02:d280:300::/40 unicast [l3_overlay 16:19:03.850] * (80/?) via 172.21.1.1 on lan mpls 24464/10322a02:d280:200::/64 unicast [l3_overlay 16:19:03.850] * (80/?) via 172.21.1.1 on lan mpls 24464/1033root@bird-dev:/etc/network# ip -6 route show table 202a02:d280:200::/64 encap mpls 24464/1033 via fe80::de68:cff:fe32:edb2 dev lan proto bird metric 32 pref medium2a02:d280:201::/64 encap mpls 24480/16 via fe80::de68:cff:fe32:edb2 dev lan proto bird metric 32 pref medium2a02:d280:300::/40 encap mpls 24464/1032 via fe80::de68:cff:fe32:edb2 dev lan proto bird metric 32 pref mediumdefault via 2a02:d280:10:0:46:23:58:137 dev wan proto bird metric 32 pref medium* 2. Known edge case: if a segment ever has more than one NTF_ROUTER neighbor on the same interface, the cache currently keeps whichever one was last seen in the dump (non-deterministic ordering). Not an issue in our topology (always exactly one router per segment), but worth flagging. This is not an edge case, this is a blocker for upstreaming. 3. Cache refresh is tied to the existing KRT scan interval rather than the async netlink notification path, to avoid touching the shared AF_BRIDGE dispatch code. Happy to change this if an event-driven refresh is preferred upstream. I would expect that a reasonable ARP-ND cache would be actually tied to both, as other tables are. This whole topic will probably need a senior meeting to discus possible caveats in the implementation, so that BIRD stays maintainable and well-performing into future. But before I put this into our backlog, there may be some tricks hidden in the hostentry mechanism and autopeering, which could resolve your whole problem way easier. Or not, I don’t have enough information. We can look into your problem best effort if you send us your config and tell us more about how the network actually looks like, or it can be done faster if you include bird-partner@nic.cz in the loop and get BIRD Support <https://bird.nic.cz/commercial-services/#deployments-isp>. I hope this helps. Note: I see various indications that the e-mail I’m replying to was written by Claude Sonnet 5, and I would appreciate disclosing such information. We are experimenting with using LLMs to aid development, currently still with miserable results, and knowing which LLMs generate which kind of output, would be nice, to better calibrate our approach to LLMs. YES. I admit it was written by AI Claude Sonnet 5 in VS Code. And the patch was fully made by it. If you can solve my problem more efficiently, I'll throw my patch in the trash. I use AI to generate and validate configuration and I'm checking it. This is my first report to the BIRD community. We use BIRD to OSPFv2 and OSPFv3 for more than 10 years in one layer. But not for MPLS and BGP and more layers. I´m sending the BIRD config. If you want more information I'd be happy to send it. Note: I also have a similar class of problem with Mikrotik - v4-mapped next-hop for VPE6 over IPv4 underlay - but on a different layer. Mikrotik RouterOS has its own closed control plane resolver, not the Linux FIB, so it is not the same root cause as mine. Mentioning it only as context, not asking you to solve that one. *https://forum.mikrotik.com/t/feature-request-6vpe-vpnv6-ipv6-address-family/... <https://forum.mikrotik.com/t/feature-request-6vpe-vpnv6-ipv6-address-family/82034/40>* Your email helps me a lot and if I can I would switch underlay to IPv6 but I have no appropriate HW. Aleš Kopecký *United Networks SE*Nepomucká 1232/215 32600 Plzeň IČ: 03579051 DIČ: CZ03579051 [image: icon] +420 373 705 705 [image: icon] www.unitednetworks.cz [image: icon] ales.kopecky@u-n.cz [image: icon] Naše pobočky: Blatná, Domažlice, Klatovy, Plzeň, Nepomuk, Strakonice a Sušice *Jste s námi spokojení? Ohodnoťte nás. *🥇 Facebook <https://www.facebook.com/UnitedNetworksAitexKonetMaxtel/reviews> Google <https://g.page/r/CdY7LY81QtagEBM/review> Seznam <https://www.firmy.cz/detail/13113409-united-networks-se-plzen-cernice.html> *United Networks SE* Nepomucká 1232/215 323 00 Plzeň IČ: 03579051 DIČ: CZ03579051 INFOLINKA: +420 373 705 705 Web: www.u-n.cz <http://www.u-n.cz/> pá 10. 7. 2026 v 17:14 odesílatel Maria Matejka via Bird-users < bird-users@network.cz> napsal:
Hello Aleš,
On Fri, Jul 10, 2026 at 02:20:42PM +0200, Aleš Kopecký via Bird-users wrote:
[…]
Role: egress PE, seamless-MPLS BGP L3VPN (AS 197296), IPv4-only underlay (OSPFv2 + BGP-LU), adding IPv6 as 6VPE (RFC 4659) over the IPv4 transport, extended next hop capability enabled on the vpn6 channel, no IPv6 IGP / LDPv6 anywhere in the underlay.
This looks a little bit cursed in itself but whatever. There are probably legitimate uses of an IPv4-only underlay.
== Symptom (unchanged from the original report) == Control plane resolution is complete and correct — BIRD resolves the VPNv6 route recursively via the IPv4 labeled table and builds the correct double MPLS label stack:
show route table vpntab6 all 2a02:d280:201::/64 mpls 1207 unicast [proto_bgp_rr] * (100/?) [?] via 172.21.1.1 on lan mpls 24482/16 hostentry: via 100.127.1.26 table master4_lu mpls 16
But kernel export fails for these routes:
show route table table_vpn6_overlay 2a02:d280:201::/64 unicast [l3_overlay] ! (80/?) via 172.21.1.1 on lan mpls 24482/16
: Netlink: Invalid argument (repeated, once per scan cycle)
== Root cause == Confirmed by manual reproduction outside BIRD:
# ip -6 route add 2a02:d280:300::/40 encap mpls 24481/1033 via inet 172.21.1.1 dev lan table 20 Error: IPv6 does not support RTA_VIA attribute.
net/ipv6/route.c in mainline Linux does not accept RTA_VIA (the netlink attribute for a cross-family nexthop) on an IPv6 route, full stop — regardless of whether the address is a raw IPv4 address or an IPv4-mapped IPv6 address. Since our underlay is IPv4-only, every VPNv6 route we import resolves (via the IPv4-labeled IGP table) to a physically IPv4-family adjacency, so every such route hits this kernel limitation.
My primary thought would be to push this into the kernel, or to deploy an IPv6 underlay. Do I assume correctly that your whole network has been IPv4-only until now, and you are trying to get IPv6 working, which means that deploying an IPv6 overlay would be way harder than doing this kind of hack?
[…]
== The patch == Confirmed the neighboring router already sends IPv6 Router Advertisements on the segment (independent of any IPv6 IGP — RA is link-scoped), so the kernel already keeps a working ND cache entry for it:
ip -6 neigh show dev lan fe80::1 lladdr dc:68:0c:32:ed:b2 router STALE
Well, this is in itself kinda fragile, as we can’t really take for granted that an RA would arrive.
The attached patch adds a small on-link-router cache to netlink.c (mirrors the existing AF_BRIDGE FDB cache/parsing that’s already in the file, just generalized to AF_INET6 neighbors flagged NTF_ROUTER), refreshed once per KRT scan cycle. In nl_add_nexthop(), when exporting an IPv6 route whose resolved gateway is IPv4-family, it looks up this cache by egress interface and substitutes the neighbor’s real link-local address for RTA_GATEWAY instead of building the (rejected) RTA_VIA — the MPLS encap is untouched. No interface name or address is hardcoded anywhere; it’s fully derived from what the kernel already knows via ND. If the cache has no entry yet for that interface, behavior is unchanged from today (route stays uninstalled, self-heals on the next scan) — no regression.
I’m not reading the patch in full but it looks like you expect that the link is point-to-point, i.e. there is only one remote router per link. That is generally not true.
You would actually need a full translation table between IPv4 and IPv6 next hop, populated by reading both ARP and ND cache and matching the entries. And even that needs some caution.
[…]
== Questions for the list / maintainers == 1. Is this an acceptable place/approach to fix this, or would you rather see it handled differently (e.g. in rt-table.c nexthop resolution itself, or as an explicit config knob rather than automatic ND-based discovery)?
It looks like a crude hack with no inspectability. We generally prefer tables to be accessible by users from CLI.
2. Known edge case: if a segment ever has more than one NTF_ROUTER neighbor on the same interface, the cache currently keeps whichever one was last seen in the dump (non-deterministic ordering). Not an issue in our topology (always exactly one router per segment), but worth flagging.
This is not an edge case, this is a blocker for upstreaming.
3. Cache refresh is tied to the existing KRT scan interval rather than the async netlink notification path, to avoid touching the shared AF_BRIDGE dispatch code. Happy to change this if an event-driven refresh is preferred upstream.
I would expect that a reasonable ARP-ND cache would be actually tied to both, as other tables are.
This whole topic will probably need a senior meeting to discus possible caveats in the implementation, so that BIRD stays maintainable and well-performing into future.
But before I put this into our backlog, there may be some tricks hidden in the hostentry mechanism and autopeering, which could resolve your whole problem way easier. Or not, I don’t have enough information.
We can look into your problem best effort if you send us your config and tell us more about how the network actually looks like, or it can be done faster if you include bird-partner@nic.cz in the loop and get BIRD Support <https://bird.nic.cz/commercial-services/#deployments-isp>.
I hope this helps.
Note: I see various indications that the e-mail I’m replying to was written by Claude Sonnet 5, and I would appreciate disclosing such information. We are experimenting with using LLMs to aid development, currently still with miserable results, and knowing which LLMs generate which kind of output, would be nice, to better calibrate our approach to LLMs.
– Maria Matejka (she/her) | BIRD Team Leader | CZ.NIC, z.s.p.o.
participants (2)
-
Aleš Kopecký -
Maria Matejka