From b578d9cc6e55b54288adec9e73196042a16823e3 Mon Sep 17 00:00:00 2001 From: Numan Siddique Date: Fri, 8 May 2026 20:44:28 -0400 Subject: [PATCH] pinctrl: Use correct IPv6 dst for nd_ns action for IPv4 over v6. If a logical router with dynamic-routing enabled learns a route whose nexthop is an IPv6 address, then ovn-northd adds a logical flow in the lr_in_ip_routing stage, but it doesn't add the priority-200 logical flow in lr_in_arp_request to resolve the mac of the nexthop. The below prioroty-100 logical flow gets invoked instead. table=27(lr_in_arp_request ), priority=100 , match=(eth.dst == 00:00:00:00:00:00 && reg9[9] == 0), action=(nd_ns { nd.target = xxreg0; output; }; next;) And this doesn't set the correct Solicit IPv6 destination address. This patch fixes the issue in pinctrl handler function for nd_ns action by using the IPv6 destination address stored in xxreg0. This action assumes that xxreg0 is populated with the proper address. Other alternative fix is for ovn-northd to add a logical flow in lr_in_arp_request just like it adds for static routes. I think its better to fix it in pinctrl instead. Found this issue while testing dynamic routing feature. Fixes: bcba1b74c82c ("pinctrl: Handle arp/nd for other address families."). Assisted-by: Claude Opus 4.7, Claude Code Signed-off-by: Numan Siddique --- controller/pinctrl.c | 26 ++++++++++++------ tests/ovn.at | 63 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 8 deletions(-) diff --git a/controller/pinctrl.c b/controller/pinctrl.c index 9e1b11d814..4473ec668d 100644 --- a/controller/pinctrl.c +++ b/controller/pinctrl.c @@ -6541,17 +6541,27 @@ pinctrl_handle_nd_ns(struct rconn *swconn, const struct flow *ip_flow, /* We might be here without actually currently handling an IPv6 packet. * This can happen in the case where we route IPv4 packets over an IPv6 - * link. - * In these cases we have no destination IPv6 address from the packet that - * we can reuse. But we receive the actual destination IPv6 address via - * userdata anyway, so what we pass to compose_nd_ns is irrelevant. - * This is just a hope since we do not parse the userdata. If we land here - * for whatever reason without being an IPv6 packet and without userdata we - * will send out a wrong packet. - */ + * link (e.g. RFC 5549 / BGP unnumbered, where an IPv4 destination is + * resolved via an IPv6 link-local nexthop). + * + * In that case we have no destination IPv6 address in the trigger packet + * to reuse. compose_nd_ns() needs a valid destination so it can derive + * the correct solicited-node multicast (ff02::1:ff{addr[13:16]}) for + * eth.dst and ip6.dst -- userdata only sets nd.target on the new packet + * and does not rewrite ip6.dst, so a wrong ipv6_dst here egresses on the + * wire as-is. + * + * The fallback nd_ns logical flow in S_ROUTER_IN_ARP_REQUEST stores the + * actual IPv6 nexthop in xxreg0 (REG_NEXT_HOP_IPV6) before invoking the + * nd_ns action, so for the IPv4-over-IPv6 case read xxreg0 from the + * trigger packet's flow metadata. */ struct in6_addr ipv6_dst = IN6ADDR_EXACT_INIT; if (get_dl_type(ip_flow) == htons(ETH_TYPE_IPV6)) { ipv6_dst = ip_flow->ipv6_dst; + } else { + ovs_be128 nexthop_be = + hton128(flow_get_xxreg(&pin->flow_metadata.flow, 0)); + memcpy(&ipv6_dst, &nexthop_be, sizeof ipv6_dst); } compose_nd_ns(&packet, ip_flow->dl_src, &ipv6_src, &ipv6_dst); diff --git a/tests/ovn.at b/tests/ovn.at index fbaa63d99c..3126fec320 100644 --- a/tests/ovn.at +++ b/tests/ovn.at @@ -46214,3 +46214,66 @@ AT_CHECK([grep -q "WARN.*dynamic-routing" hv/ovn-controller.log], [1]) OVN_CLEANUP([hv]) AT_CLEANUP ]) + +OVN_FOR_EACH_NORTHD([ +AT_SETUP([IPv4 over v6 Neigh solicitation test]) +ovn_start + +net_add n1 +sim_add hv +ovs-vsctl add-br br-phys +ovn_attach n1 br-phys 192.168.0.1 +ovs-vsctl set open . external-ids:ovn-bridge-mappings=phys:br-phys + +check ovn-nbctl ls-add sw0 +check ovn-nbctl lsp-add sw0 sw0-port1 +check ovn-nbctl lsp-set-addresses sw0-port1 "50:54:00:00:00:01 10.0.0.3" + +check ovn-nbctl lr-add lr0 +check ovn-nbctl lrp-add lr0 lr0-sw0 00:00:00:00:ff:01 10.0.0.1/24 1000::1/64 +check ovn-nbctl lsp-add-router-port sw0 sw0-lr0 lr0-sw0 + +check ovn-nbctl ls-add public +check ovn-nbctl lrp-add lr0 lr0-public 00:00:20:20:12:13 +check ovn-nbctl lsp-add-router-port public public-lr0 lr0-public + +# localnet port +check ovn-nbctl lsp-add-localnet-port public ln-public phys + +check ovn-nbctl lrp-set-gateway-chassis lr0-public hv 20 +check ovn-nbctl lr-nat-add lr0 dnat_and_snat 172.168.0.110 10.0.0.3 + +ovs-vsctl -- add-port br-int vif1 -- \ + set interface vif1 external-ids:iface-id=sw0-port1 \ + options:tx_pcap=hv/vif1-tx.pcap \ + options:rxq_pcap=hv/vif1-rx.pcap \ + ofport-request=1 + +wait_for_ports_up + +# Add a learnt route manually +dp_uuid=$(fetch_column datapath _uuid external_ids:name=lr0) +lrp_uuid=$(fetch_column port_binding _uuid logical_port=lr0-public) + +check_uuid ovn-sbctl create learned_route datapath=$dp_uuid logical_port=$lrp_uuid \ +ip_prefix=0.0.0.0/0 nexthop='"fe80::42:ff:fe00:1ff"' + +check ovn-nbctl --wait=hv sync + +# Send an IPv4 packet from sw0-port1 destined to outside +packet=$(fmt_pkt "Ether(dst='00:00:00:00:ff:01', src='50:54:00:00:00:01')/ \ + IP(dst='8.8.8.8', src='10.0.0.3')/ICMP()") +check as hv ovs-appctl netdev-dummy/receive vif1 $packet + +nd_ns=$(fmt_pkt "Ether(dst='33:33:ff:00:01:ff', src='00:00:20:20:12:13')/ \ + IPv6(src='fe80::200:20ff:fe20:1213', \ + dst='ff02::1:ff00:1ff')/ICMPv6ND_NS(tgt='fe80::42:ff:fe00:1ff')/\ + ICMPv6NDOptSrcLLAddr(lladdr='00:00:20:20:12:13')") + +echo $nd_ns > expected_nd_ns +OVN_CHECK_PACKETS_CONTAIN([hv/br-phys_n1-tx.pcap], [expected_nd_ns]) + +OVN_CLEANUP([hv]) +AT_CLEANUP +]) +