I'm marking this as "Changes Requested" since there is an unexpected test failure happening.
On Mon, Oct 13, 2025 at 4:26 PM Mark Michelson via dev <[email protected]> wrote: > > By default, northd does not install any responder flows for unicast ARP. > These are intended to be forwarded to the destination so that the > destination can respond appropriately. For VIF ports, this tends to work > as expected in most scenarios. > > When proxy ARP is configured on a logical switch port conntected to a > logical router, then we install low priority flows to ensure that > ARPs for the configured proxy addresses is responded to by the logical > switch. These proxy ARP flows are hit when unicast ARP requests are sent > for the VIF ports on the logical switch. We therefore end up responding > incorrectly to unicast ARP requests with the proxy ARP MAC instead of > forwarding the ARP request to the proper VIF port. > > This commit fixes the issue by installing explicit "next;" actions for > unicast ARP requests directed towards VIFs so that the proxy ARP flows > will not be hit. These flows are only installed if proxy ARP is > configured, since they are unnecessary otherwise. > > Reported-at: https://issues.redhat.com/browse/FDP-1646 > Signed-off-by: Mark Michelson <[email protected]> > --- > v2: > * This version also fixes the problem with IPv6 ND_NS packets. The > tests have been updated to test for IPv6. > * This version only installs unicast ARP/ND_NS flows if the LSP > address overlaps with the configured proxy_arp addresses. The > ovn-northd test has been updated to ensure that this is correct. The > code that does this is pretty gnarly, since it's four levels of > for loops. But it works! > * Fixed nits from Xavier (removed ofport request, removed unnecessary > lflow-list, etc.). > --- > northd/northd.c | 104 ++++++++++++++++++++++++++++++++++++++------ > northd/northd.h | 7 +++ > tests/ovn-northd.at | 93 +++++++++++++++++++++++++++++++++++++++ > tests/ovn.at | 76 ++++++++++++++++++++++++++++++++ > 4 files changed, 266 insertions(+), 14 deletions(-) > > diff --git a/northd/northd.c b/northd/northd.c > index 3b1d3eba7..3dbd7f2b0 100644 > --- a/northd/northd.c > +++ b/northd/northd.c > @@ -557,6 +557,7 @@ ovn_datapath_create(struct hmap *datapaths, const struct > uuid *key, > od->tunnel_key = sdp->sb_dp->tunnel_key; > init_mcast_info_for_datapath(od); > od->datapath_lflows = lflow_ref_create(); > + od->proxy_arp_addrs = VECTOR_EMPTY_INITIALIZER(struct lport_addresses); > return od; > } > > @@ -586,6 +587,11 @@ ovn_datapath_destroy(struct ovn_datapath *od) > destroy_ports_for_datapath(od); > sset_destroy(&od->router_ips); > lflow_ref_destroy(od->datapath_lflows); > + /* The ovn_ports own the proxy_arp_addresses, so we do not > + * need to call destroy_lport_addresses() on the components > + * of the vector > + */ > + vector_destroy(&od->proxy_arp_addrs); > free(od); > } > } > @@ -1876,6 +1882,7 @@ join_logical_ports(const struct > sbrec_port_binding_table *sbrec_pb_table, > if (extract_addresses(arp_proxy, &op->proxy_arp_addrs, &ofs) > || > extract_ip_addresses(arp_proxy, &op->proxy_arp_addrs)) { > op->od->has_arp_proxy_port = true; > + vector_push(&op->od->proxy_arp_addrs, > &op->proxy_arp_addrs); > } else { > static struct vlog_rate_limit rl = > VLOG_RATE_LIMIT_INIT(1, 5); > @@ -9836,6 +9843,85 @@ build_lswitch_arp_nd_responder_skip_local(struct > ovn_port *op, > &op->nbsp->header_, op->lflow_ref); > } > > +static void > +build_arp_match(struct ds *match, const char *ipv4_addr, const char *mac) > +{ > + ds_put_format(match, "arp.tpa == %s && arp.op == 1 && eth.dst == %s", > + ipv4_addr, mac); > +} > + > +static void > +build_nd_match(struct ds *match, const struct ipv6_netaddr *ipv6_addr, > + bool is_multicast) > +{ > + if (is_multicast) { > + ds_put_format(match, "nd_ns_mcast && ip6.dst == %s && nd.target == > %s", > + ipv6_addr->sn_addr_s, ipv6_addr->addr_s); > + } else { > + ds_put_format(match, "nd_ns && ip6.dst == %s && nd.target == %s", > + ipv6_addr->addr_s, ipv6_addr->addr_s); > + } > +} > + > +static void > +build_lswitch_arp_nd_unicast_flows(struct ovn_port *op, > + struct lflow_table *lflows, > + struct ds *match) > +{ > + /* Typically, we don't need to build any unicast flows for ARP or ND > + * since the natural switching behavior of the logical switch will > + * get the packet to its intended destination. However, if proxy ARP > + * is configured, then we may need to install flows to ensure that > + * we do not incorrectly respond to unicast ARPs destined to a known > + * IP with the proxy ARP instead. > + */ > + if (!op->od->nbs || !op->od->has_arp_proxy_port) { > + return; > + } > + > + struct lport_addresses *proxy_arp_addrs; > + VECTOR_FOR_EACH_PTR(&op->od->proxy_arp_addrs, proxy_arp_addrs) { > + for (size_t i = 0; i < op->n_lsp_addrs; i++) { > + for (size_t j = 0; j < proxy_arp_addrs->n_ipv4_addrs; j++) { > + for (size_t k = 0; k < op->lsp_addrs[i].n_ipv4_addrs; k++) { > + struct ipv4_netaddr *ipv4_addr = > &op->lsp_addrs[i].ipv4_addrs[k]; > + ovs_be32 proxy_arp_mask = > proxy_arp_addrs->ipv4_addrs[j].mask; > + ovs_be32 proxy_arp_network = > proxy_arp_addrs->ipv4_addrs[j].network; > + if ((ipv4_addr->addr & proxy_arp_mask) != > proxy_arp_network) { > + continue; > + } > + ds_clear(match); > + build_arp_match(match, ipv4_addr->addr_s, > + op->lsp_addrs[i].ea_s); > + ovn_lflow_add_with_hint(lflows, op->od, > + S_SWITCH_IN_ARP_ND_RSP, 50, > + ds_cstr(match), > + "next;", &op->nbsp->header_, > + op->lflow_ref); > + } > + } > + for (size_t j = 0; j < proxy_arp_addrs->n_ipv6_addrs; j++) { > + for (size_t k = 0; k < op->lsp_addrs[j].n_ipv6_addrs; k++) { > + struct ipv6_netaddr *ipv6_addr = > &op->lsp_addrs[i].ipv6_addrs[k]; > + struct in6_addr *proxy_arp_mask = > &proxy_arp_addrs->ipv6_addrs[j].mask; > + struct in6_addr *proxy_arp_network = > &proxy_arp_addrs->ipv6_addrs[j].network; > + struct in6_addr network = > ipv6_addr_bitand(&ipv6_addr->addr, proxy_arp_mask); > + if (!ipv6_addr_equals(proxy_arp_network, &network)) { > + continue; > + } > + ds_clear(match); > + build_nd_match(match, ipv6_addr, false); > + ovn_lflow_add_with_hint(lflows, op->od, > + S_SWITCH_IN_ARP_ND_RSP, 50, > + ds_cstr(match), > + "next;", &op->nbsp->header_, > + op->lflow_ref); > + } > + } > + } > + } > +} > + > /* Ingress table 24: ARP/ND responder, reply for known IPs. > * (priority 50). */ > static void > @@ -9956,15 +10042,8 @@ build_lswitch_arp_nd_responder_known_ips(struct > ovn_port *op, > for (size_t i = 0; i < op->n_lsp_addrs; i++) { > for (size_t j = 0; j < op->lsp_addrs[i].n_ipv4_addrs; j++) { > ds_clear(match); > - /* Do not reply on unicast ARPs, forward them to the target > - * to have ability to monitor target liveness via unicast > - * ARP requests. > - */ > - ds_put_format(match, > - "arp.tpa == %s && " > - "arp.op == 1 && " > - "eth.dst == ff:ff:ff:ff:ff:ff", > - op->lsp_addrs[i].ipv4_addrs[j].addr_s); > + build_arp_match(match, op->lsp_addrs[i].ipv4_addrs[j].addr_s, > + "ff:ff:ff:ff:ff:ff"); > ds_clear(actions); > ds_put_format(actions, > "eth.dst = eth.src; " > @@ -10017,11 +10096,7 @@ build_lswitch_arp_nd_responder_known_ips(struct > ovn_port *op, > */ > for (size_t j = 0; j < op->lsp_addrs[i].n_ipv6_addrs; j++) { > ds_clear(match); > - ds_put_format( > - match, > - "nd_ns_mcast && ip6.dst == %s && nd.target == %s", > - op->lsp_addrs[i].ipv6_addrs[j].sn_addr_s, > - op->lsp_addrs[i].ipv6_addrs[j].addr_s); > + build_nd_match(match, &op->lsp_addrs[i].ipv6_addrs[j], true); > > ds_clear(actions); > ds_put_format(actions, > @@ -10061,6 +10136,7 @@ build_lswitch_arp_nd_responder_known_ips(struct > ovn_port *op, > op->lflow_ref); > } > } > + build_lswitch_arp_nd_unicast_flows(op, lflows, match); > } > if (op->proxy_arp_addrs.n_ipv4_addrs || > op->proxy_arp_addrs.n_ipv6_addrs) { > diff --git a/northd/northd.h b/northd/northd.h > index cdc532954..dc863ec48 100644 > --- a/northd/northd.h > +++ b/northd/northd.h > @@ -471,6 +471,13 @@ struct ovn_datapath { > /* Reference to the lflows belonging to this datapath currently router > * only lflows. */ > struct lflow_ref *datapath_lflows; > + > + /* This vector contains pointers to struct lport_addresses. These are > + * the configured "arp_proxy" addresses of all logical switch ports on > + * this datapath. The ovn_ports own these addresses, so we should not > + * free them when destroying the ovn_datapath. > + */ > + struct vector proxy_arp_addrs; > }; > > const struct ovn_datapath *ovn_datapath_find(const struct hmap *datapaths, > diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at > index 07fb57bd6..6b978bd6a 100644 > --- a/tests/ovn-northd.at > +++ b/tests/ovn-northd.at > @@ -18716,3 +18716,96 @@ AT_CHECK( > > AT_CLEANUP > ]) > + > +OVN_FOR_EACH_NORTHD_NO_HV([ > +AT_SETUP([Unicast ARP flows]) > +ovn_start > + > +# Typically on logical switches, the ARP responder stage installs nothing > +# for unicast ARPs towards VIF MACs. Instead, we rely on the default priority > +# 1 "next;" action to move these ARPs through the pipeline, eventually > resulting > +# in the ARP reaching the destination. When proxy ARP is configured, we need > +# to install explicit flows for the unicast ARPs at a higher priority. > Otherwise > +# the proxy ARP responder may respond with incorrect information. > +# > +# In this test, we are ensuring that the unicast flows in the ARP responder > stage > +# are only installed when proxy ARP is enabled. > + > +check ovn-nbctl ls-add ls1 > +check ovn-nbctl lsp-add ls1 vm1 -- \ > + lsp-set-addresses vm1 "00:00:00:00:00:02 192.168.0.2 fd01::2" > + > +check ovn-nbctl lr-add lr1 > +check ovn-nbctl lrp-add lr1 lr1-ls1 00:00:00:00:00:01 192.168.0.1 fd01::1 > +check ovn-nbctl lsp-add ls1 ls1-lr1 -- \ > + lsp-set-addresses ls1-lr1 router -- \ > + lsp-set-type ls1-lr1 router -- \ > + lsp-set-options ls1-lr1 router-port=lr1-ls1 > + > +check ovn-nbctl --wait=sb sync > + > +# If we check the ARP responder flows in ls1, we should not see any unicast > +# flows for vm1 (00:00:00:00:00:02). We also should not see any unicast > +# IPv6 ND flows for vm1 (fd01::2) > +AT_CHECK([ovn-sbctl lflow-list ls1 | grep ls_in_arp_rsp | grep "eth.dst == > 00:00:00:00:00:02" | ovn_strip_lflows], [0], [dnl > +]) > +AT_CHECK([ovn-sbctl lflow-list ls1 | grep ls_in_arp_rsp | grep "ip6.dst == > fd01::2" | ovn_strip_lflows], [0], [dnl > +]) > + > +AT_CHECK([ovn-sbctl lflow-list ls1 | grep ls_in_arp_rsp | grep "eth.dst == > 00:00:00:00:00:01" | ovn_strip_lflows], [0], [dnl > +]) > +AT_CHECK([ovn-sbctl lflow-list ls1 | grep ls_in_arp_rsp | grep "ip6.dst == > fd01::1" | ovn_strip_lflows], [0], [dnl > +]) > + > +# Add ARP proxy configuration on the router port. > +check ovn-nbctl set logical_switch_port ls1-lr1 > options:arp_proxy="192.168.0.0/24 fd01::/64" > +check ovn-nbctl --wait=sb sync > + > +# Now that we have ARP proxy configured, we should see flows for > +# vm1's MAC. > +AT_CHECK([ovn-sbctl lflow-list ls1 | grep ls_in_arp_rsp | grep "eth.dst == > 00:00:00:00:00:02" | ovn_strip_lflows], [0], [dnl > + table=??(ls_in_arp_rsp ), priority=50 , match=(arp.tpa == > 192.168.0.2 && arp.op == 1 && eth.dst == 00:00:00:00:00:02), action=(next;) > +]) > +# And we should see unicast ND flows for vm1's IP address > +AT_CHECK([ovn-sbctl lflow-list ls1 | grep ls_in_arp_rsp | grep "ip6.dst == > fd01::2" | ovn_strip_lflows], [0], [dnl > + table=??(ls_in_arp_rsp ), priority=50 , match=(nd_ns && ip6.dst == > fd01::2 && nd.target == fd01::2), action=(next;) > +]) > + > +# We should also see an ARP flow for ls1-lr1's MAC. > +AT_CHECK([ovn-sbctl lflow-list ls1 | grep ls_in_arp_rsp | grep "eth.dst == > 00:00:00:00:00:01" | ovn_strip_lflows], [0], [dnl > + table=??(ls_in_arp_rsp ), priority=50 , match=(arp.tpa == > 192.168.0.1 && arp.op == 1 && eth.dst == 00:00:00:00:00:01), action=(next;) > +]) > +# And we should see an ND flow for ls1-lr1's IPv6 address. > +AT_CHECK([ovn-sbctl lflow-list ls1 | grep ls_in_arp_rsp | grep "ip6.dst == > fd01::1" | ovn_strip_lflows], [0], [dnl > + table=??(ls_in_arp_rsp ), priority=50 , match=(nd_ns && ip6.dst == > fd01::1 && nd.target == fd01::1), action=(next;) > +]) > + > +check ovn-nbctl remove logical_switch_port ls1-lr1 options arp_proxy > +check ovn-nbctl --wait=sb sync > + > +# We have removed ARP proxy, so we should no longer see a unicast ARP or ND > flow. > +AT_CHECK([ovn-sbctl lflow-list ls1 | grep ls_in_arp_rsp | grep "eth.dst == > 00:00:00:00:00:02" | ovn_strip_lflows], [0], [dnl > +]) > +AT_CHECK([ovn-sbctl lflow-list ls1 | grep ls_in_arp_rsp | grep "ip6.dst == > fd01::2" | ovn_strip_lflows], [0], [dnl > +]) > +AT_CHECK([ovn-sbctl lflow-list ls1 | grep ls_in_arp_rsp | grep "eth.dst == > 00:00:00:00:00:01" | ovn_strip_lflows], [0], [dnl > +]) > +AT_CHECK([ovn-sbctl lflow-list ls1 | grep ls_in_arp_rsp | grep "ip6.dst == > fd01::1" | ovn_strip_lflows], [0], [dnl > +]) > + > +# Now we'll set up ARP proxy flows for networks that are different from our > +# LSPs. This should result in no unicast ARP/ND flows being installed. > +check ovn-nbctl set logical_switch_port ls1-lr1 > options:arp_proxy="193.168.0.0/24 fd11::/64" > +check ovn-nbctl --wait=sb sync > + > +AT_CHECK([ovn-sbctl lflow-list ls1 | grep ls_in_arp_rsp | grep "eth.dst == > 00:00:00:00:00:02" | ovn_strip_lflows], [0], [dnl > +]) > +AT_CHECK([ovn-sbctl lflow-list ls1 | grep ls_in_arp_rsp | grep "ip6.dst == > fd01::2" | ovn_strip_lflows], [0], [dnl > +]) > +AT_CHECK([ovn-sbctl lflow-list ls1 | grep ls_in_arp_rsp | grep "eth.dst == > 00:00:00:00:00:01" | ovn_strip_lflows], [0], [dnl > +]) > +AT_CHECK([ovn-sbctl lflow-list ls1 | grep ls_in_arp_rsp | grep "ip6.dst == > fd01::1" | ovn_strip_lflows], [0], [dnl > +]) > + > +AT_CLEANUP > +]) > diff --git a/tests/ovn.at b/tests/ovn.at > index 3f340e7b3..0783376bc 100644 > --- a/tests/ovn.at > +++ b/tests/ovn.at > @@ -44371,3 +44371,79 @@ check ovn-nbctl --wait=hv sync > OVN_CLEANUP([hv1],[hv2],[hv3]) > AT_CLEANUP > ]) > + > +OVN_FOR_EACH_NORTHD([ > +AT_SETUP([Unicast ARP when proxy ARP is configured]) > +ovn_start > + > +check ovn-nbctl ls-add ls1 > +check ovn-nbctl lsp-add ls1 vm1 -- \ > + lsp-set-addresses vm1 "00:00:00:00:00:02 10.0.0.2 fd01::2" > +check ovn-nbctl lsp-add ls1 vm2 -- \ > + lsp-set-addresses vm2 "00:00:00:00:00:03 10.0.0.3 fd01::3" > + > +check ovn-nbctl lr-add lr1 > +check ovn-nbctl lrp-add lr1 lr1-ls1 00:00:00:00:00:01 10.0.0.1 fd01::1 > +check ovn-nbctl lsp-add ls1 ls1-lr1 -- \ > + lsp-set-addresses ls1-lr1 router -- \ > + lsp-set-type ls1-lr1 router -- \ > + lsp-set-options ls1-lr1 router-port=lr1-ls1 > + > +check ovn-nbctl --wait=hv sync > + > +net_add n1 > +sim_add hv1 > +as hv1 > +ovs-vsctl add-br br-phys > +ovn_attach n1 br-phys 192.168.0.1 > +ovs-vsctl add-port br-int vif1 -- set Interface vif1 > external-ids:iface-id=vm1 \ > + options:tx_pcap=hv1/vif1-tx.pcap \ > + options:rxq_pcap=hv1/vif1-rx.pcap > +ovs-vsctl add-port br-int vif2 -- set Interface vif2 > external-ids:iface-id=vm2 \ > + options:tx_pcap=hv1/vif2-tx.pcap \ > + options:rxq_pcap=hv1/vif2-rx.pcap > + > +OVN_POPULATE_ARP > + > +wait_for_ports_up > + > +arp=$(fmt_pkt "Ether(dst='00:00:00:00:00:02', src='00:00:00:00:00:03')/ \ > + ARP(hwsrc='00:00:00:00:00:03', hwdst='00:00:00:00:00:02', > + psrc='10.0.0.3', pdst='10.0.0.2')") > +nd_ns=$(fmt_pkt "Ether(dst='00:00:00:00:00:02', src='00:00:00:00:00:03')/ \ > + IPv6(src='fd01::3', dst='fd01::2')/ \ > + ICMPv6ND_NS(tgt='fd01::2')") > + > +# If we send a unicast ARP from vm2 towards vm1, the ARP should be forwarded > +# to vm1 by ls1. > +as hv1 ovs-appctl netdev-dummy/receive vif2 $arp > +echo $arp > expected > +OVN_CHECK_PACKETS([hv1/vif1-tx.pcap], expected) > + > +# And if we send a unicast ND_NS from vm2 towards vm1, the ND_NS should be > +# forwarded to vm1 by ls1. > +as hv1 ovs-appctl netdev-dummy/receive vif2 $nd_ns > +echo $nd_ns >> expected > +OVN_CHECK_PACKETS([hv1/vif1-tx.pcap], expected) > + > + > +# Add ARP proxy configuration on the router port. The subnet for the ARP > proxy > +# addresses overlaps with the VIF addresses for vm1 and vm2. > +check ovn-nbctl set logical_switch_port ls1-lr1 > options:arp_proxy="10.0.0.0/8 fd01::/64" > +check ovn-nbctl --wait=hv sync > + > +# Sending a unicast ARP from vm2 towards vm1 should still result in the ARP > being > +# forwarded to vm1 by ls1. > +as hv1 ovs-appctl netdev-dummy/receive vif2 $arp > +echo $arp >> expected > +OVN_CHECK_PACKETS([hv1/vif1-tx.pcap], expected) > + > +# Sending a unicast ND_NS from vm2 towards vm1 should still result in the > ND_NS being > +# forwarded to vm1 by ls1. > +as hv1 ovs-appctl netdev-dummy/receive vif2 $nd_ns > +echo $nd_ns >> expected > +OVN_CHECK_PACKETS([hv1/vif1-tx.pcap], expected) > + > +OVN_CLEANUP([hv1]) > +AT_CLEANUP > +]) > -- > 2.50.1 > > _______________________________________________ > dev mailing list > [email protected] > https://mail.openvswitch.org/mailman/listinfo/ovs-dev > _______________________________________________ dev mailing list [email protected] https://mail.openvswitch.org/mailman/listinfo/ovs-dev
