diff --git a/controller/lflow.c b/controller/lflow.c
index d0335a83a..45c4d725a 100644
--- a/controller/lflow.c
+++ b/controller/lflow.c
@@ -687,6 +687,7 @@ consider_logical_flow(
         .egress_ptable = OFTABLE_LOG_EGRESS_PIPELINE,
         .output_ptable = output_ptable,
         .mac_bind_ptable = OFTABLE_MAC_BINDING,
+        .mac_lookup_ptable = OFTABLE_MAC_LOOKUP,
     };
     ovnacts_encode(ovnacts.data, ovnacts.size, &ep, &ofpacts);
     ovnacts_free(ovnacts.data, ovnacts.size);
@@ -777,7 +778,9 @@ consider_neighbor_flow(struct ovsdb_idl_index *sbrec_port_binding_by_name,
         return;
     }
 
-    struct match match = MATCH_CATCHALL_INITIALIZER;
+    struct match get_arp_match = MATCH_CATCHALL_INITIALIZER;
+    struct match lookup_arp_match = MATCH_CATCHALL_INITIALIZER;
+
     if (strchr(b->ip, '.')) {
         ovs_be32 ip;
         if (!ip_parse(b->ip, &ip)) {
@@ -785,7 +788,9 @@ consider_neighbor_flow(struct ovsdb_idl_index *sbrec_port_binding_by_name,
             VLOG_WARN_RL(&rl, "bad 'ip' %s", b->ip);
             return;
         }
-        match_set_reg(&match, 0, ntohl(ip));
+        match_set_reg(&get_arp_match, 0, ntohl(ip));
+        match_set_reg(&lookup_arp_match, 0, ntohl(ip));
+        match_set_dl_type(&lookup_arp_match, htons(ETH_TYPE_ARP));
     } else {
         struct in6_addr ip6;
         if (!ipv6_parse(b->ip, &ip6)) {
@@ -795,17 +800,31 @@ consider_neighbor_flow(struct ovsdb_idl_index *sbrec_port_binding_by_name,
         }
         ovs_be128 value;
         memcpy(&value, &ip6, sizeof(value));
-        match_set_xxreg(&match, 0, ntoh128(value));
+        match_set_xxreg(&get_arp_match, 0, ntoh128(value));
+
+        match_set_xxreg(&lookup_arp_match, 0, ntoh128(value));
+        match_set_dl_type(&lookup_arp_match, htons(ETH_TYPE_IPV6));
+        match_set_nw_proto(&lookup_arp_match, 58);
+        match_set_icmp_code(&lookup_arp_match, 0);
     }
 
-    match_set_metadata(&match, htonll(pb->datapath->tunnel_key));
-    match_set_reg(&match, MFF_LOG_OUTPORT - MFF_REG0, pb->tunnel_key);
+    match_set_metadata(&get_arp_match, htonll(pb->datapath->tunnel_key));
+    match_set_reg(&get_arp_match, MFF_LOG_OUTPORT - MFF_REG0, pb->tunnel_key);
+
+    match_set_metadata(&lookup_arp_match, htonll(pb->datapath->tunnel_key));
+    match_set_reg(&lookup_arp_match, MFF_LOG_INPORT - MFF_REG0,
+                  pb->tunnel_key);
 
     uint64_t stub[1024 / 8];
     struct ofpbuf ofpacts = OFPBUF_STUB_INITIALIZER(stub);
     put_load(mac.ea, sizeof mac.ea, MFF_ETH_DST, 0, 48, &ofpacts);
-    ofctrl_add_flow(flow_table, OFTABLE_MAC_BINDING, 100, 0, &match, &ofpacts,
-                    &b->header_.uuid);
+    ofctrl_add_flow(flow_table, OFTABLE_MAC_BINDING, 100, 0, &get_arp_match,
+                    &ofpacts, &b->header_.uuid);
+
+    match_set_dl_src(&lookup_arp_match, mac);
+    ofctrl_add_flow(flow_table, OFTABLE_MAC_LOOKUP, 100, 0, &lookup_arp_match,
+                    &ofpacts, &b->header_.uuid);
+
     ofpbuf_uninit(&ofpacts);
 }
 
diff --git a/controller/lflow.h b/controller/lflow.h
index 54da00b49..d6d18978a 100644
--- a/controller/lflow.h
+++ b/controller/lflow.h
@@ -58,6 +58,7 @@ struct uuid;
  * you make any changes. */
 #define OFTABLE_PHY_TO_LOG            0
 #define OFTABLE_LOG_INGRESS_PIPELINE  8 /* First of LOG_PIPELINE_LEN tables. */
+#define OFTABLE_MAC_LOOKUP           31
 #define OFTABLE_REMOTE_OUTPUT        32
 #define OFTABLE_LOCAL_OUTPUT         33
 #define OFTABLE_CHECK_LOOPBACK       34
diff --git a/include/ovn/actions.h b/include/ovn/actions.h
index 145f27f25..37f74e281 100644
--- a/include/ovn/actions.h
+++ b/include/ovn/actions.h
@@ -73,8 +73,10 @@ struct ovn_extend_table;
     OVNACT(ND_NA_ROUTER,      ovnact_nest)            \
     OVNACT(GET_ARP,           ovnact_get_mac_bind)    \
     OVNACT(PUT_ARP,           ovnact_put_mac_bind)    \
+    OVNACT(LOOKUP_ARP,        ovnact_lookup_mac_bind) \
     OVNACT(GET_ND,            ovnact_get_mac_bind)    \
     OVNACT(PUT_ND,            ovnact_put_mac_bind)    \
+    OVNACT(LOOKUP_ND,         ovnact_lookup_mac_bind) \
     OVNACT(PUT_DHCPV4_OPTS,   ovnact_put_opts)        \
     OVNACT(PUT_DHCPV6_OPTS,   ovnact_put_opts)        \
     OVNACT(SET_QUEUE,         ovnact_set_queue)       \
@@ -266,6 +268,14 @@ struct ovnact_put_mac_bind {
     struct expr_field mac;      /* 48-bit Ethernet address. */
 };
 
+/* OVNACT_LOOKUP_ARP, OVNACT_LOOKUP_ND. */
+struct ovnact_lookup_mac_bind {
+    struct ovnact ovnact;
+    struct expr_field port;     /* Logical port name. */
+    struct expr_field ip;       /* 32-bit or 128-bit IP address. */
+    struct expr_field mac;      /* 48-bit Ethernet address. */
+};
+
 struct ovnact_gen_option {
     const struct gen_opts_map *option;
     struct expr_constant_set value;
@@ -628,6 +638,8 @@ struct ovnact_encode_params {
     uint8_t output_ptable;      /* OpenFlow table for 'output' to resubmit. */
     uint8_t mac_bind_ptable;    /* OpenFlow table for 'get_arp'/'get_nd' to
                                    resubmit. */
+    uint8_t mac_lookup_ptable;  /* OpenFlow table for
+                                   'lookup_arp'/'lookup_nd' to resubmit. */
 };
 
 void ovnacts_encode(const struct ovnact[], size_t ovnacts_len,
diff --git a/lib/actions.c b/lib/actions.c
index 6a5907e1b..9cb05e34c 100644
--- a/lib/actions.c
+++ b/lib/actions.c
@@ -1607,6 +1607,89 @@ ovnact_put_mac_bind_free(struct ovnact_put_mac_bind *put_mac OVS_UNUSED)
 {
 }
 
+static void format_lookup_mac(const struct ovnact_lookup_mac_bind *lookup_mac,
+                              struct ds *s, const char *name)
+{
+    ds_put_format(s, "%s(", name);
+    expr_field_format(&lookup_mac->port, s);
+    ds_put_cstr(s, ", ");
+    expr_field_format(&lookup_mac->ip, s);
+    ds_put_cstr(s, ", ");
+    expr_field_format(&lookup_mac->mac, s);
+    ds_put_cstr(s, ");");
+}
+
+static void
+format_LOOKUP_ARP(const struct ovnact_lookup_mac_bind *lookup_mac,
+                         struct ds *s)
+{
+    format_lookup_mac(lookup_mac, s, "lookup_arp");
+}
+
+static void
+format_LOOKUP_ND(const struct ovnact_lookup_mac_bind *lookup_mac,
+                        struct ds *s)
+{
+    format_lookup_mac(lookup_mac, s, "lookup_nd");
+}
+
+static void
+encode_lookup_mac(const struct ovnact_lookup_mac_bind *lookup_mac,
+                  enum mf_field_id ip_field,
+                  const struct ovnact_encode_params *ep,
+                  struct ofpbuf *ofpacts)
+{
+    const struct arg args[] = {
+        { expr_resolve_field(&lookup_mac->port), MFF_LOG_INPORT },
+        { expr_resolve_field(&lookup_mac->ip), ip_field },
+        { expr_resolve_field(&lookup_mac->mac),  MFF_ETH_SRC},
+    };
+
+    encode_setup_args(args, ARRAY_SIZE(args), ofpacts);
+
+    put_load(0, MFF_ETH_DST, 0, 48, ofpacts);
+    emit_resubmit(ofpacts, ep->mac_lookup_ptable);
+
+    encode_restore_args(args, ARRAY_SIZE(args), ofpacts);
+}
+
+static void
+encode_LOOKUP_ARP(const struct ovnact_lookup_mac_bind *lookup_mac,
+                  const struct ovnact_encode_params *ep,
+                  struct ofpbuf *ofpacts)
+{
+    encode_lookup_mac(lookup_mac, MFF_REG0, ep, ofpacts);
+}
+
+static void
+encode_LOOKUP_ND(const struct ovnact_lookup_mac_bind *lookup_mac,
+                        const struct ovnact_encode_params *ep,
+                        struct ofpbuf *ofpacts)
+{
+    encode_lookup_mac(lookup_mac, MFF_XXREG0, ep, ofpacts);
+}
+
+static void
+parse_lookup_mac_bind(struct action_context *ctx, int width,
+                      struct ovnact_lookup_mac_bind *lookup_mac)
+{
+    lexer_force_match(ctx->lexer, LEX_T_LPAREN);
+    action_parse_field(ctx, 0, false, &lookup_mac->port);
+    lexer_force_match(ctx->lexer, LEX_T_COMMA);
+    action_parse_field(ctx, width, false, &lookup_mac->ip);
+    lexer_force_match(ctx->lexer, LEX_T_COMMA);
+    action_parse_field(ctx, 48, false, &lookup_mac->mac);
+    lexer_force_match(ctx->lexer, LEX_T_RPAREN);
+}
+
+static void
+ovnact_lookup_mac_bind_free(
+    struct ovnact_lookup_mac_bind *lookup_mac OVS_UNUSED)
+{
+
+}
+
+
 static void
 parse_gen_opt(struct action_context *ctx, struct ovnact_gen_option *o,
               const struct hmap *gen_opts, const char *opts_type)
@@ -2784,10 +2867,14 @@ parse_action(struct action_context *ctx)
         parse_get_mac_bind(ctx, 32, ovnact_put_GET_ARP(ctx->ovnacts));
     } else if (lexer_match_id(ctx->lexer, "put_arp")) {
         parse_put_mac_bind(ctx, 32, ovnact_put_PUT_ARP(ctx->ovnacts));
+    } else if (lexer_match_id(ctx->lexer, "lookup_arp")) {
+        parse_lookup_mac_bind(ctx, 32, ovnact_put_LOOKUP_ARP(ctx->ovnacts));
     } else if (lexer_match_id(ctx->lexer, "get_nd")) {
         parse_get_mac_bind(ctx, 128, ovnact_put_GET_ND(ctx->ovnacts));
     } else if (lexer_match_id(ctx->lexer, "put_nd")) {
         parse_put_mac_bind(ctx, 128, ovnact_put_PUT_ND(ctx->ovnacts));
+    } else if (lexer_match_id(ctx->lexer, "lookup_nd")) {
+        parse_lookup_mac_bind(ctx, 128, ovnact_put_LOOKUP_ND(ctx->ovnacts));
     } else if (lexer_match_id(ctx->lexer, "set_queue")) {
         parse_SET_QUEUE(ctx);
     } else if (lexer_match_id(ctx->lexer, "log")) {
diff --git a/northd/ovn-northd.8.xml b/northd/ovn-northd.8.xml
index b34ef687a..e411705cd 100644
--- a/northd/ovn-northd.8.xml
+++ b/northd/ovn-northd.8.xml
@@ -1218,7 +1218,174 @@ output;
       Other packets are implicitly dropped.
     </p>
 
-    <h3>Ingress Table 1: IP Input</h3>
+    <h3>Ingress Table 1: ARP/ND lookup</h3>
+
+    <p>
+      For ARP and Neighbor Discovery packets, this table looks into the
+      <ref db="OVN_Southbound" table="MAC_Binding"/> records to determine
+      if OVN needs to learn the mac bindings. Following flows are added:
+    </p>
+
+    <ul>
+      <li>
+        <p>
+          A priority-100 flow which matches on IPv4 ARP packet and applies
+          the actions:
+        </p>
+
+        <pre>
+xxreg1[0..47] = eth.dst;;
+lookup_arp(inport, arp.spa, arp.sha);
+xxreg0[0..47] = eth.dst;
+eth.dst = xxreg1[0..47];
+next;
+        </pre>
+      </li>
+
+      <li>
+        <p>
+          A priority-100 flow which matches on IPv6 Neighbor Discovery
+          advertisement packet and applies the actions:
+        </p>
+
+        <pre>
+xxreg1[0..47] = eth.dst;;
+lookup_nd(inport, nd.target, nd.tll);
+xxreg0[0..47] = eth.dst;
+eth.dst = xxreg1[0..47];
+next;
+        </pre>
+      </li>
+
+      <li>
+        <p>
+          A priority-100 flow which matches on IPv6 Neighbor Discovery
+          solicitation packet and applies the actions:
+        </p>
+
+        <pre>
+xxreg1[0..47] = eth.dst;;
+lookup_nd(inport, ip6.src, nd.sll);
+xxreg0[0..47] = eth.dst;
+eth.dst = xxreg1[0..47];
+next;
+        </pre>
+      </li>
+
+      <li>
+        A priority-0 fallback flow that matches all packets
+        and advances to the next table.
+      </li>
+    </ul>
+
+    <h3>Ingress Table 2: MAC learning</h3>
+
+    <p>
+      This table adds flows to learn the mac bindings from the ARP and
+      IPv6 Neighbor Solicitation/Advertisement packets if ARP/ND lookup
+      failed in the previous table.
+    </p>
+
+    <p>
+      xxreg0[0..47] will be <code>00:00:00:00:00:00</code> if the
+      <code>lookup_arp/lookup_nd</code> in the previous table failed the
+      lookup in the mac binding table.
+    </p>
+
+    <ul>
+      <li>
+        A priority-100 flow with the match <code>arp.op == 2 &amp;&amp;
+        xxreg0[0..47] == 00:00:00:00:00:00</code> and applies the
+        action <code>put_arp(inport, arp.spa, arp.sha);</code>
+      </li>
+
+      <li>
+        A priority-90 flow with the match <code>arp.op == 2</code> and
+        applies the action <code>drop;</code>
+      </li>
+
+      <li>
+        <p>
+          MAC learning from ARP requests.
+        </p>
+
+        <p>
+          These flows populates the mac binding table of the logical router
+          port from the ARP request packets for the router's own IP address.
+          The ARP requests are handled only if the requestor's IP belongs
+          to the same subnets of the logical router port.
+          For each router port <var>P</var> that owns IP address <var>A</var>,
+          which belongs to subnet <var>S</var> with prefix length <var>L</var>,
+          and Ethernet address <var>E</var>, a priority-90 flow matches
+          <code>inport == <var>P</var> &amp;&amp;
+          arp.spa == <var>S</var>/<var>L</var> &amp;&amp; arp.op == 1
+          &amp;&amp; arp.tpa == <var>A</var> &amp;&amp;
+          xxreg0[0..47] == 00:00:00:00:00:00</code> (ARP request) with the
+          following actions:
+        </p>
+
+        <pre>
+put_arp(inport, arp.spa, arp.sha);
+next;
+        </pre>
+      </li>
+
+      <li>
+        <p>
+          MAC learning from ARP requests not redirected to router IPs.
+        </p>
+
+        <p>
+          For each router port <var>P</var> that owns IP address
+          <var>A</var>, which belongs to subnet <var>S</var> with prefix length
+          <var>L</var>, and Ethernet address <var>E</var>, a priority-90 flow
+          matches <code>inport == <var>P</var> &amp;&amp;
+          arp.spa == <var>S</var>/<var>L</var> &amp;&amp; arp.op == 1
+          &amp;&amp; xxreg0[0..47] == 00:00:00:00:00:00</code> (ARP request)
+          with the action <code>put_arp(inport, arp.spa, arp.sha);</code>.
+        </p>
+
+        <p>
+          If the logical router port <var>P</var> is a distributed gateway
+          router port, additional match
+          <code>is_chassis_resident(cr-<var>P</var>)</code> is added so that
+          the resident gateway chassis handles such ARP packets.
+        </p>
+      </li>
+
+      <li>
+        <p>
+          MAC learning from IPv6 Neighbor Solicitation packets.
+        </p>
+
+        <p>
+          A priority-100 flow with the match <code>nd_ns &amp;&amp;
+          xxreg0[0..47] == 00:00:00:00:00:00</code> and applies the
+          below actions and advancing the packet to the next table.
+        </p>
+
+        <pre>
+put_nd(inport, ip6.src, nd.sll);
+next;
+        </pre>
+      </li>
+
+      <li>
+        <p>
+          MAC learning from IPv6 Neighbor Advertisement packets.
+          This flow uses Neighbor Advertisements to populate the
+          logical router's mac binding table.
+        </p>
+
+        <p>
+          A priority-100 flow with the match <code>nd_na &amp;&amp;
+          xxreg0[0..47] == 00:00:00:00:00:00</code> and applies the
+          action <code>put_nd(inport, nd.target, nd.tll);</code>
+        </p>
+      </li>
+    </ul>
+
+    <h3>Ingress Table 3: IP Input</h3>
 
     <p>
       This table is the core of the logical router datapath functionality.  It
@@ -1315,8 +1482,7 @@ next;
         </p>
 
         <p>
-          These flows reply to ARP requests for the router's own IP address
-          and populates mac binding table of the logical router port.
+          These flows reply to ARP requests for the router's own IP address.
           The ARP requests are handled only if the requestor's IP belongs
           to the same subnets of the logical router port.
           For each router port <var>P</var> that owns IP address <var>A</var>,
@@ -1329,7 +1495,6 @@ next;
         </p>
 
         <pre>
-put_arp(inport, arp.spa, arp.sha);
 eth.dst = eth.src;
 eth.src = <var>E</var>;
 arp.op = 2; /* ARP reply. */
@@ -1365,17 +1530,6 @@ output;
         </p>
       </li>
 
-      <li>
-        <p>
-          These flows handles ARP requests not for router's own IP address.
-          They use the SPA and SHA to populate the logical router port's
-          mac binding table, with priority 80.  The typical use case of
-          these flows are GARP requests handling.  For the gateway port
-          on a distributed logical router, these flows are only programmed
-          on the gateway port instance on the <code>redirect-chassis</code>.
-        </p>
-      </li>
-
       <li>
         <p>
           These flows reply to ARP requests for the virtual IP addresses
@@ -1446,36 +1600,6 @@ arp.sha = <var>external_mac</var>;
         </ul>
       </li>
 
-      <li>
-        <p>
-          ARP reply handling.  Following flows are added to handle ARP replies.
-        </p>
-
-        <p>
-          For each distributed gateway logical router port a priority-92 flow
-          with match <code>inport == <var>P</var> &amp;&amp;
-          is_chassis_resident(cr-<var>P</var>) &amp;&amp; eth.bcast &amp;&amp;
-          arp.op == 2 &amp;&amp; arp.spa == <var>I</var></code> with the
-          action <code>put_arp(inport, arp.spa, arp.sha);</code> so that the
-          resident gateway chassis can learn the GARP reply, where
-          <var>P</var> is the distributed gateway router port name,
-          <var>I</var> is the logical router port's network address.
-        </p>
-
-        <p>
-          For each distributed gateway logical router port a priority-92 flow
-          with match <code>inport == <var>P</var> &amp;&amp;
-          !is_chassis_resident(cr-<var>P</var>) &amp;&amp; eth.bcast &amp;&amp;
-          arp.op == 2 &amp;&amp; arp.spa == <var>I</var></code> with the action
-          <code>drop;</code> so that other chassis drop this packet.
-        </p>
-
-        <p>
-          A priority-90 flow with match <code>arp.op == 2</code> has actions
-          <code>put_arp(inport, arp.spa, arp.sha);</code>.
-        </p>
-      </li>
-
       <li>
         <p>
           Reply to IPv6 Neighbor Solicitations.  These flows reply to
@@ -1494,7 +1618,6 @@ arp.sha = <var>external_mac</var>;
         </p>
 
         <pre>
-put_nd(inport, ip6.src, nd.sll);
 nd_na_router {
     eth.src = <var>E</var>;
     ip6.src = <var>A</var>;
@@ -1516,7 +1639,6 @@ nd_na_router {
         </p>
 
         <pre>
-put_nd(inport, ip6.src, nd.sll);
 nd_na {
     eth.src = <var>E</var>;
     ip6.src = <var>A</var>;
@@ -1540,23 +1662,6 @@ nd_na {
         </p>
       </li>
 
-      <li>
-        IPv6 neighbor advertisement handling.  This flow uses neighbor
-        advertisements to populate the logical router's mac binding
-        table.  A priority-90 flow with match <code>nd_na</code>
-        has actions <code>put_nd(inport, nd.target, nd.tll);</code>.
-      </li>
-
-      <li>
-        IPv6 neighbor solicitation for non-hosted addresses handling.
-        This flow uses neighbor solicitations to populate the logical
-        router's mac binding table (ones that were directed at the
-        logical router would have matched the priority-90 neighbor
-        solicitation flow already).  A priority-80 flow with match
-        <code>nd_ns</code> has actions
-        <code>put_nd(inport, ip6.src, nd.sll);</code>.
-      </li>
-
       <li>
         <p>
           UDP port unreachable.  Priority-80 flows generate ICMP port
@@ -1670,7 +1775,7 @@ icmp6 {
       </li>
     </ul>
 
-    <h3>Ingress Table 2: DEFRAG</h3>
+    <h3>Ingress Table 4: DEFRAG</h3>
 
     <p>
       This is to send packets to connection tracker for tracking and
@@ -1728,7 +1833,7 @@ icmp6 {
       </li>
     </ul>
 
-    <p>Ingress Table 3: UNSNAT on Distributed Routers</p>
+    <p>Ingress Table 5: UNSNAT on Distributed Routers</p>
 
     <ul>
       <li>
@@ -1767,7 +1872,7 @@ icmp6 {
       </li>
     </ul>
 
-    <h3>Ingress Table 4: DNAT</h3>
+    <h3>Ingress Table 6: DNAT</h3>
 
     <p>
       Packets enter the pipeline with destination IP address that needs to
@@ -1775,7 +1880,7 @@ icmp6 {
       in the reverse direction needs to be unDNATed.
     </p>
 
-    <p>Ingress Table 4: Load balancing DNAT rules</p>
+    <p>Ingress Table 6: Load balancing DNAT rules</p>
 
     <p>
       Following load balancing DNAT flows are added for Gateway router or
@@ -1836,7 +1941,7 @@ icmp6 {
       </li>
     </ul>
 
-    <p>Ingress Table 4: DNAT on Gateway Routers</p>
+    <p>Ingress Table 6: DNAT on Gateway Routers</p>
 
     <ul>
       <li>
@@ -1862,7 +1967,7 @@ icmp6 {
       </li>
     </ul>
 
-    <p>Ingress Table 4: DNAT on Distributed Routers</p>
+    <p>Ingress Table 6: DNAT on Distributed Routers</p>
 
     <p>
       On distributed routers, the DNAT table only handles packets
@@ -1909,7 +2014,7 @@ icmp6 {
       </li>
     </ul>
 
-    <h3>Ingress Table 5: IPv6 ND RA option processing</h3>
+    <h3>Ingress Table 7: IPv6 ND RA option processing</h3>
 
     <ul>
       <li>
@@ -1939,7 +2044,7 @@ reg0[5] = put_nd_ra_opts(<var>options</var>);next;
       </li>
     </ul>
 
-    <h3>Ingress Table 6: IPv6 ND RA responder</h3>
+    <h3>Ingress Table 8: IPv6 ND RA responder</h3>
 
     <p>
       This table implements IPv6 ND RA responder for the IPv6 ND RA replies
@@ -1984,7 +2089,7 @@ output;
       </li>
     </ul>
 
-    <h3>Ingress Table 7: IP Routing</h3>
+    <h3>Ingress Table 9: IP Routing</h3>
 
     <p>
       A packet that arrives at this table is an IP packet that should be
@@ -2134,7 +2239,7 @@ next;
       </li>
     </ul>
 
-    <h3>Ingress Table 8: ARP/ND Resolution</h3>
+    <h3>Ingress Table 10: ARP/ND Resolution</h3>
 
     <p>
       Any packet that reaches this table is an IP packet whose next-hop
@@ -2281,7 +2386,7 @@ next;
 
     </ul>
 
-    <h3>Ingress Table 9: Check packet length</h3>
+    <h3>Ingress Table 11: Check packet length</h3>
 
     <p>
       For distributed logical routers with distributed gateway port configured
@@ -2311,7 +2416,7 @@ REGBIT_PKT_LARGER = check_pkt_larger(<var>L</var>); next;
       and advances to the next table.
     </p>
 
-    <h3>Ingress Table 10: Handle larger packets</h3>
+    <h3>Ingress Table 12: Handle larger packets</h3>
 
     <p>
       For distributed logical routers with distributed gateway port configured
@@ -2360,7 +2465,7 @@ icmp4 {
       and advances to the next table.
     </p>
 
-    <h3>Ingress Table 11: Gateway Redirect</h3>
+    <h3>Ingress Table 13: Gateway Redirect</h3>
 
     <p>
       For distributed logical routers where one of the logical router
@@ -2422,7 +2527,7 @@ icmp4 {
       </li>
     </ul>
 
-    <h3>Ingress Table 12: ARP Request</h3>
+    <h3>Ingress Table 14: ARP Request</h3>
 
     <p>
       In the common case where the Ethernet destination has been resolved, this
diff --git a/northd/ovn-northd.c b/northd/ovn-northd.c
index c24e4d864..9100d6719 100644
--- a/northd/ovn-northd.c
+++ b/northd/ovn-northd.c
@@ -145,19 +145,21 @@ enum ovn_stage {
                                                                       \
     /* Logical router ingress stages. */                              \
     PIPELINE_STAGE(ROUTER, IN,  ADMISSION,      0, "lr_in_admission")    \
-    PIPELINE_STAGE(ROUTER, IN,  IP_INPUT,       1, "lr_in_ip_input")     \
-    PIPELINE_STAGE(ROUTER, IN,  DEFRAG,         2, "lr_in_defrag")       \
-    PIPELINE_STAGE(ROUTER, IN,  UNSNAT,         3, "lr_in_unsnat")       \
-    PIPELINE_STAGE(ROUTER, IN,  DNAT,           4, "lr_in_dnat")         \
-    PIPELINE_STAGE(ROUTER, IN,  ND_RA_OPTIONS,  5, "lr_in_nd_ra_options") \
-    PIPELINE_STAGE(ROUTER, IN,  ND_RA_RESPONSE, 6, "lr_in_nd_ra_response") \
-    PIPELINE_STAGE(ROUTER, IN,  IP_ROUTING,     7, "lr_in_ip_routing")   \
-    PIPELINE_STAGE(ROUTER, IN,  POLICY,         8, "lr_in_policy")       \
-    PIPELINE_STAGE(ROUTER, IN,  ARP_RESOLVE,    9, "lr_in_arp_resolve")  \
-    PIPELINE_STAGE(ROUTER, IN,  CHK_PKT_LEN   , 10, "lr_in_chk_pkt_len")   \
-    PIPELINE_STAGE(ROUTER, IN,  LARGER_PKTS,    11,"lr_in_larger_pkts")   \
-    PIPELINE_STAGE(ROUTER, IN,  GW_REDIRECT,    12, "lr_in_gw_redirect")  \
-    PIPELINE_STAGE(ROUTER, IN,  ARP_REQUEST,    13, "lr_in_arp_request")  \
+    PIPELINE_STAGE(ROUTER, IN,  LOOKUP_ARP,     1, "lr_in_lookup_arp") \
+    PIPELINE_STAGE(ROUTER, IN,  PUT_ARP,        2, "lr_in_put_arp") \
+    PIPELINE_STAGE(ROUTER, IN,  IP_INPUT,       3, "lr_in_ip_input")     \
+    PIPELINE_STAGE(ROUTER, IN,  DEFRAG,         4, "lr_in_defrag")       \
+    PIPELINE_STAGE(ROUTER, IN,  UNSNAT,         5, "lr_in_unsnat")       \
+    PIPELINE_STAGE(ROUTER, IN,  DNAT,           6, "lr_in_dnat")         \
+    PIPELINE_STAGE(ROUTER, IN,  ND_RA_OPTIONS,  7, "lr_in_nd_ra_options") \
+    PIPELINE_STAGE(ROUTER, IN,  ND_RA_RESPONSE, 8, "lr_in_nd_ra_response") \
+    PIPELINE_STAGE(ROUTER, IN,  IP_ROUTING,     9, "lr_in_ip_routing")   \
+    PIPELINE_STAGE(ROUTER, IN,  POLICY,         10, "lr_in_policy")       \
+    PIPELINE_STAGE(ROUTER, IN,  ARP_RESOLVE,    11, "lr_in_arp_resolve")  \
+    PIPELINE_STAGE(ROUTER, IN,  CHK_PKT_LEN   , 12, "lr_in_chk_pkt_len")   \
+    PIPELINE_STAGE(ROUTER, IN,  LARGER_PKTS,    13,"lr_in_larger_pkts")   \
+    PIPELINE_STAGE(ROUTER, IN,  GW_REDIRECT,    14, "lr_in_gw_redirect")  \
+    PIPELINE_STAGE(ROUTER, IN,  ARP_REQUEST,    15, "lr_in_arp_request")  \
                                                                       \
     /* Logical router egress stages. */                               \
     PIPELINE_STAGE(ROUTER, OUT, UNDNAT,    0, "lr_out_undnat")        \
@@ -6367,7 +6369,111 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
                       ds_cstr(&match), "next;");
     }
 
-    /* Logical router ingress table 1: IP Input. */
+    /* Logical router ingress table 1: LOOKUP_ARP and table 2: PUT_ARP. */
+    HMAP_FOR_EACH (od, key_node, datapaths) {
+        if (!od->nbr) {
+            continue;
+        }
+
+        /* Learn from ARP requests and ARP replies. A typical
+         * use case is GARP request handling.
+         * Table LOOKUP_ARP does a lookup for the (arp.spa, arp.sha)
+         * in the mac binding table using the 'lookup_arp' action.
+         * If it is present, then this action stores the mac in the eth.dst
+         * of the packet. Before calling 'lookup_arp' we store
+         * eth.dst in xxreg1. After 'lookup_arp' action is applied
+         * we store the searched mac - eth.dst in xxreg0 and restore
+         * eth.dst to its original value.
+         *
+         * Table PUT_ARP learns the mac using the action - 'put_arp'
+         * only if xxreg0 is 00:00:00:00:00:00. There is no need to learn
+         * the mac otherwise.
+         *
+         * The same thing will be done for IPv6 ND/NS packets.
+         * */
+        ovn_lflow_add(lflows, od, S_ROUTER_IN_LOOKUP_ARP, 100, "arp",
+                      "xxreg1[0..47] = eth.dst; "
+                      "lookup_arp(inport, arp.spa, arp.sha); "
+                      "xxreg0[0..47] = eth.dst; "
+                      "eth.dst = xxreg1[0..47]; next;");
+
+        ovn_lflow_add(lflows, od, S_ROUTER_IN_PUT_ARP, 100,
+                      "arp.op == 2 && xxreg0[0..47] == 00:00:00:00:00:00",
+                      "put_arp(inport, arp.spa, arp.sha);");
+
+        ovn_lflow_add(lflows, od, S_ROUTER_IN_PUT_ARP, 90, "arp.op == 2",
+                      "drop;");
+
+        /* IPv6 ND/NS handling. */
+        ovn_lflow_add(lflows, od, S_ROUTER_IN_LOOKUP_ARP, 100, "nd_na",
+                      "xxreg1[0..47] = eth.dst; "
+                      "lookup_nd(inport, nd.target, nd.tll); "
+                      "xxreg0[0..47] = eth.dst; "
+                      "eth.dst = xxreg1[0..47]; next;");
+
+        ovn_lflow_add(lflows, od, S_ROUTER_IN_LOOKUP_ARP, 100, "nd_ns",
+                      "xxreg1[0..47] = eth.dst; "
+                      "lookup_nd(inport, ip6.src, nd.sll); "
+                      "xxreg0[0..47] = eth.dst; "
+                      "eth.dst = xxreg1[0..47]; next;");
+
+        ovn_lflow_add(lflows, od, S_ROUTER_IN_PUT_ARP, 100,
+                      "nd_na && xxreg0[0..47] == 00:00:00:00:00:00",
+                      "put_nd(inport, nd.target, nd.tll);");
+
+        ovn_lflow_add(lflows, od, S_ROUTER_IN_PUT_ARP, 100,
+                      "nd_ns && xxreg0[0..47] == 00:00:00:00:00:00",
+                      "put_nd(inport, ip6.src, nd.sll); next;");
+
+        /* Pass other traffic not already handled to the next table for
+         * routing. */
+        ovn_lflow_add(lflows, od, S_ROUTER_IN_LOOKUP_ARP, 0, "1", "next;");
+        ovn_lflow_add(lflows, od, S_ROUTER_IN_PUT_ARP, 0, "1", "next;");
+    }
+
+    HMAP_FOR_EACH (op, key_node, ports) {
+        if (!op->nbrp) {
+            continue;
+        }
+
+        for (int i = 0; i < op->lrp_networks.n_ipv4_addrs; i++) {
+            ds_clear(&match);
+            ds_put_format(&match,
+                          "inport == %s && arp.spa == %s/%u && arp.tpa == %s"
+                          " && arp.op == 1 && "
+                          "xxreg0[0..47] == 00:00:00:00:00:00",
+                          op->json_key,
+                          op->lrp_networks.ipv4_addrs[i].network_s,
+                          op->lrp_networks.ipv4_addrs[i].plen,
+                          op->lrp_networks.ipv4_addrs[i].addr_s);
+            ovn_lflow_add(lflows, op->od, S_ROUTER_IN_PUT_ARP, 100,
+                          ds_cstr(&match),
+                          "put_arp(inport, arp.spa, arp.sha); next; ");
+        }
+
+        /* Learn from ARP requests that were not directed at us. A typical
+         * use case is GARP request handling.  (A priority-90 flow will
+         * respond to request to us and learn the sender's mac address.) */
+        for (int i = 0; i < op->lrp_networks.n_ipv4_addrs; i++) {
+            ds_clear(&match);
+            ds_put_format(&match,
+                          "inport == %s && arp.spa == %s/%u && arp.op == 1 && "
+                          "xxreg0[0..47] == 00:00:00:00:00:00",
+                          op->json_key,
+                          op->lrp_networks.ipv4_addrs[i].network_s,
+                          op->lrp_networks.ipv4_addrs[i].plen);
+            if (op->od->l3dgw_port && op == op->od->l3dgw_port
+                && op->od->l3redirect_port) {
+                ds_put_format(&match, " && is_chassis_resident(%s)",
+                              op->od->l3redirect_port->json_key);
+            }
+            ovn_lflow_add(lflows, op->od, S_ROUTER_IN_PUT_ARP, 90,
+                          ds_cstr(&match),
+                          "put_arp(inport, arp.spa, arp.sha);");
+        }
+    }
+
+    /* Logical router ingress table 3: IP Input. */
     HMAP_FOR_EACH (od, key_node, datapaths) {
         if (!od->nbr) {
             continue;
@@ -6389,11 +6495,6 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
         ovn_lflow_add(lflows, od, S_ROUTER_IN_IP_INPUT, 95, "ip4.mcast",
                       od->mcast_info.rtr.relay ? "next;" : "drop;");
 
-        /* ARP reply handling.  Use ARP replies to populate the logical
-         * router's ARP table. */
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_IP_INPUT, 90, "arp.op == 2",
-                      "put_arp(inport, arp.spa, arp.sha);");
-
         /* Drop Ethernet local broadcast.  By definition this traffic should
          * not be forwarded.*/
         ovn_lflow_add(lflows, od, S_ROUTER_IN_IP_INPUT, 50,
@@ -6405,23 +6506,12 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
         ovn_lflow_add(lflows, od, S_ROUTER_IN_IP_INPUT, 30,
                       ds_cstr(&match), "drop;");
 
-        /* ND advertisement handling.  Use advertisements to populate
-         * the logical router's ARP/ND table. */
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_IP_INPUT, 90, "nd_na",
-                      "put_nd(inport, nd.target, nd.tll);");
-
-        /* Lean from neighbor solicitations that were not directed at
-         * us.  (A priority-90 flow will respond to requests to us and
-         * learn the sender's mac address. */
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_IP_INPUT, 80, "nd_ns",
-                      "put_nd(inport, ip6.src, nd.sll);");
-
         /* Pass other traffic not already handled to the next table for
          * routing. */
         ovn_lflow_add(lflows, od, S_ROUTER_IN_IP_INPUT, 0, "1", "next;");
     }
 
-    /* Logical router ingress table 1: IP Input for IPv4. */
+    /* Logical router ingress table 4: IP Input for IPv4. */
     HMAP_FOR_EACH (op, key_node, ports) {
         if (!op->nbrp) {
             continue;
@@ -6531,7 +6621,6 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
 
             ds_clear(&actions);
             ds_put_format(&actions,
-                "put_arp(inport, arp.spa, arp.sha); "
                 "eth.dst = eth.src; "
                 "eth.src = %s; "
                 "arp.op = 2; /* ARP reply */ "
@@ -6550,62 +6639,6 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
                           ds_cstr(&match), ds_cstr(&actions));
         }
 
-        /* Learn from ARP requests that were not directed at us. A typical
-         * use case is GARP request handling.  (A priority-90 flow will
-         * respond to request to us and learn the sender's mac address.) */
-        for (int i = 0; i < op->lrp_networks.n_ipv4_addrs; i++) {
-            ds_clear(&match);
-            ds_put_format(&match,
-                          "inport == %s && arp.spa == %s/%u && arp.op == 1",
-                          op->json_key,
-                          op->lrp_networks.ipv4_addrs[i].network_s,
-                          op->lrp_networks.ipv4_addrs[i].plen);
-            if (op->od->l3dgw_port && op == op->od->l3dgw_port
-                && op->od->l3redirect_port) {
-                ds_put_format(&match, " && is_chassis_resident(%s)",
-                              op->od->l3redirect_port->json_key);
-            }
-            ovn_lflow_add(lflows, op->od, S_ROUTER_IN_IP_INPUT, 80,
-                          ds_cstr(&match),
-                          "put_arp(inport, arp.spa, arp.sha);");
-
-        }
-
-        /* Handle GARP reply packets received on a distributed router gateway
-         * port. GARP reply broadcast packets could be sent by external
-         * switches. We don't want them to be handled by all the
-         * ovn-controllers if they receive it. So add a priority-92 flow to
-         * apply the put_arp action on a redirect chassis and drop it on
-         * other chassis.
-         * Note that we are already adding a priority-90 logical flow in the
-         * table S_ROUTER_IN_IP_INPUT to apply the put_arp action if
-         * arp.op == 2.
-         * */
-        if (op->od->l3dgw_port && op == op->od->l3dgw_port
-                && op->od->l3redirect_port) {
-            for (int i = 0; i < op->lrp_networks.n_ipv4_addrs; i++) {
-                ds_clear(&match);
-                ds_put_format(&match,
-                              "inport == %s && is_chassis_resident(%s) && "
-                              "eth.bcast && arp.op == 2 && arp.spa == %s/%u",
-                              op->json_key, op->od->l3redirect_port->json_key,
-                              op->lrp_networks.ipv4_addrs[i].network_s,
-                              op->lrp_networks.ipv4_addrs[i].plen);
-                ovn_lflow_add(lflows, op->od, S_ROUTER_IN_IP_INPUT, 92,
-                              ds_cstr(&match),
-                              "put_arp(inport, arp.spa, arp.sha);");
-                ds_clear(&match);
-                ds_put_format(&match,
-                              "inport == %s && !is_chassis_resident(%s) && "
-                              "eth.bcast && arp.op == 2 && arp.spa == %s/%u",
-                              op->json_key, op->od->l3redirect_port->json_key,
-                              op->lrp_networks.ipv4_addrs[i].network_s,
-                              op->lrp_networks.ipv4_addrs[i].plen);
-                ovn_lflow_add(lflows, op->od, S_ROUTER_IN_IP_INPUT, 92,
-                              ds_cstr(&match), "drop;");
-            }
-        }
-
         /* A set to hold all load-balancer vips that need ARP responses. */
         struct sset all_ips = SSET_INITIALIZER(&all_ips);
         int addr_family;
@@ -6916,7 +6949,6 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
 
             ds_clear(&actions);
             ds_put_format(&actions,
-                          "put_nd(inport, ip6.src, nd.sll); "
                           "nd_na_router { "
                           "eth.src = %s; "
                           "ip6.src = %s; "
diff --git a/ovn-sb.xml b/ovn-sb.xml
index 477e7bc7a..dd733c8df 100644
--- a/ovn-sb.xml
+++ b/ovn-sb.xml
@@ -1397,6 +1397,29 @@
           <p><b>Example:</b> <code>put_arp(inport, arp.spa, arp.sha);</code></p>
         </dd>
 
+        <dt>
+          <code>lookup_arp(<var>P</var>, <var>A</var>, <var>M</var>);</code>
+        </dt>
+
+        <dd>
+          <p>
+            <b>Parameters</b>: logical port string field <var>P</var>, 32-bit
+            IP address field <var>A</var>, 48-bit MAC address field
+            <var>M</var>.
+          </p>
+
+          <p>
+            Looks up <var>A</var> and <var>M</var> in <var>P</var>'s mac
+            binding table. If an entry is found, stores <var>M</var> in
+            <code>eth.dst</code>, otherwise stores
+            <code>00:00:00:00:00:00</code> in <code>eth.dst</code>.
+          </p>
+
+          <p>
+            <b>Example:</b> <code>lookup_arp(inport, arp.spa, arp.sha);</code>
+          </p>
+        </dd>
+
         <dt><code>nd_ns { <var>action</var>; </code>...<code> };</code></dt>
         <dd>
           <p>
@@ -1553,6 +1576,29 @@
           <p><b>Example:</b> <code>put_nd(inport, nd.target, nd.tll);</code></p>
         </dd>
 
+        <dt>
+          <code>lookup_nd(<var>P</var>, <var>A</var>, <var>M</var>);</code>
+        </dt>
+
+        <dd>
+          <p>
+            <b>Parameters</b>: logical port string field <var>P</var>, 128-bit
+            IP address field <var>A</var>, 48-bit MAC address field
+            <var>M</var>.
+          </p>
+
+          <p>
+            Looks up <var>A</var> and <var>M</var> in <var>P</var>'s mac
+            binding table. If an entry is found, stores <var>M</var> in
+            <code>eth.dst</code>, otherwise stores
+            <code>00:00:00:00:00:00</code> in <code>eth.dst</code>.
+          </p>
+
+          <p>
+            <b>Example:</b> <code>lookup_nd(inport, ip6.src, eth.src);</code>
+          </p>
+        </dd>
+
         <dt>
           <code><var>R</var> = put_dhcp_opts(<var>D1</var> = <var>V1</var>, <var>D2</var> = <var>V2</var>, ..., <var>Dn</var> = <var>Vn</var>);</code>
         </dt>
diff --git a/tests/ovn.at b/tests/ovn.at
index 2a35b4e15..6db63b595 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -1117,6 +1117,31 @@ put_arp(inport, arp.spa, arp.sha);
     encodes as push:NXM_NX_REG0[],push:NXM_OF_ETH_SRC[],push:NXM_NX_ARP_SHA[],push:NXM_OF_ARP_SPA[],pop:NXM_NX_REG0[],pop:NXM_OF_ETH_SRC[],controller(userdata=00.00.00.01.00.00.00.00),pop:NXM_OF_ETH_SRC[],pop:NXM_NX_REG0[]
     has prereqs eth.type == 0x806 && eth.type == 0x806
 
+# lookup_arp
+lookup_arp(inport, ip4.dst, eth.src);
+    encodes as push:NXM_NX_REG0[],push:NXM_OF_IP_DST[],pop:NXM_NX_REG0[],set_field:00:00:00:00:00:00->eth_dst,resubmit(,31),pop:NXM_NX_REG0[]
+    has prereqs eth.type == 0x800
+lookup_arp(inport, arp.spa, arp.sha);
+    encodes as push:NXM_NX_REG0[],push:NXM_OF_ETH_SRC[],push:NXM_NX_ARP_SHA[],push:NXM_OF_ARP_SPA[],pop:NXM_NX_REG0[],pop:NXM_OF_ETH_SRC[],set_field:00:00:00:00:00:00->eth_dst,resubmit(,31),pop:NXM_OF_ETH_SRC[],pop:NXM_NX_REG0[]
+    has prereqs eth.type == 0x806 && eth.type == 0x806
+
+lookup_arp;
+    Syntax error at `;' expecting `('.
+lookup_arp();
+    Syntax error at `)' expecting field name.
+lookup_arp(inport);
+    Syntax error at `)' expecting `,'.
+lookup_arp(inport ip4.dst);
+    Syntax error at `ip4.dst' expecting `,'.
+lookup_arp(inport, ip4.dst;
+    Syntax error at `;' expecting `,'.
+lookup_arp(inport, ip4.dst, eth.src;
+    Syntax error at `;' expecting `)'.
+lookup_arp(inport, eth.dst);
+    Cannot use 48-bit field eth.dst[0..47] where 32-bit field is required.
+lookup_arp(inport, ip4.src, ip4.dst);
+    Cannot use 32-bit field ip4.dst[0..31] where 48-bit field is required.
+
 # put_dhcp_opts
 reg1[0] = put_dhcp_opts(offerip = 1.2.3.4, router = 10.0.0.1);
     encodes as controller(userdata=00.00.00.02.00.00.00.00.00.01.de.10.00.00.00.40.01.02.03.04.03.04.0a.00.00.01,pause)
@@ -1217,6 +1242,33 @@ reg1[0] = put_dhcpv6_opts(ia_addr="ae70::4");
 reg1[0] = put_dhcpv6_opts(ia_addr=ae70::4, domain_search=ae70::1);
     DHCPv6 option domain_search requires string value.
 
+# lookup_nd
+lookup_nd(inport, ip6.dst, eth.src);
+    encodes as push:NXM_NX_XXREG0[],push:NXM_NX_IPV6_DST[],pop:NXM_NX_XXREG0[],set_field:00:00:00:00:00:00->eth_dst,resubmit(,31),pop:NXM_NX_XXREG0[]
+    has prereqs eth.type == 0x86dd
+lookup_nd(inport, nd.target, nd.tll);
+    encodes as push:NXM_NX_XXREG0[],push:NXM_OF_ETH_SRC[],push:NXM_NX_ND_TLL[],push:NXM_NX_ND_TARGET[],pop:NXM_NX_XXREG0[],pop:NXM_OF_ETH_SRC[],set_field:00:00:00:00:00:00->eth_dst,resubmit(,31),pop:NXM_OF_ETH_SRC[],pop:NXM_NX_XXREG0[]
+    has prereqs (icmp6.type == 0x87 || icmp6.type == 0x88) && eth.type == 0x86dd && ip.proto == 0x3a && (eth.type == 0x800 || eth.type == 0x86dd) && icmp6.code == 0 && eth.type == 0x86dd && ip.proto == 0x3a && (eth.type == 0x800 || eth.type == 0x86dd) && ip.ttl == 0xff && (eth.type == 0x800 || eth.type == 0x86dd) && icmp6.type == 0x88 && eth.type == 0x86dd && ip.proto == 0x3a && (eth.type == 0x800 || eth.type == 0x86dd) && icmp6.code == 0 && eth.type == 0x86dd && ip.proto == 0x3a && (eth.type == 0x800 || eth.type == 0x86dd) && ip.ttl == 0xff && (eth.type == 0x800 || eth.type == 0x86dd)
+
+lookup_nd;
+    Syntax error at `;' expecting `('.
+lookup_nd();
+    Syntax error at `)' expecting field name.
+lookup_nd(inport);
+    Syntax error at `)' expecting `,'.
+lookup_nd(inport ip6.dst);
+    Syntax error at `ip6.dst' expecting `,'.
+lookup_nd(inport, ip6.dst;
+    Syntax error at `;' expecting `,'.
+lookup_nd(inport, ip6.dst, eth.src;
+    Syntax error at `;' expecting `)'.
+lookup_nd(inport, eth.dst);
+    Cannot use 48-bit field eth.dst[0..47] where 128-bit field is required.
+lookup_nd(inport, ip4.src, ip4.dst);
+    Cannot use 32-bit field ip4.src[0..31] where 128-bit field is required.
+lookup_nd(inport, ip6.src, ip6.dst);
+    Cannot use 128-bit field ip6.dst[0..127] where 48-bit field is required.
+
 # set_queue
 set_queue(0);
     encodes as set_queue:0
@@ -14502,7 +14554,7 @@ ovn-sbctl dump-flows lr0 | grep lr_in_arp_resolve | grep "reg0 == 10.0.0.10" \
 # Since the sw0-vir is not claimed by any chassis, eth.dst should be set to
 # zero if the ip4.dst is the virtual ip in the router pipeline.
 AT_CHECK([cat lflows.txt], [0], [dnl
-  table=9 (lr_in_arp_resolve  ), priority=100  , match=(outport == "lr0-sw0" && reg0 == 10.0.0.10), action=(eth.dst = 00:00:00:00:00:00; next;)
+  table=11(lr_in_arp_resolve  ), priority=100  , match=(outport == "lr0-sw0" && reg0 == 10.0.0.10), action=(eth.dst = 00:00:00:00:00:00; next;)
 ])
 
 ip_to_hex() {
@@ -14538,7 +14590,7 @@ ovn-sbctl dump-flows lr0 | grep lr_in_arp_resolve | grep "reg0 == 10.0.0.10" \
 # There should be an arp resolve flow to resolve the virtual_ip with the
 # sw0-p1's MAC.
 AT_CHECK([cat lflows.txt], [0], [dnl
-  table=9 (lr_in_arp_resolve  ), priority=100  , match=(outport == "lr0-sw0" && reg0 == 10.0.0.10), action=(eth.dst = 50:54:00:00:00:03; next;)
+  table=11(lr_in_arp_resolve  ), priority=100  , match=(outport == "lr0-sw0" && reg0 == 10.0.0.10), action=(eth.dst = 50:54:00:00:00:03; next;)
 ])
 
 # send the garp from sw0-p2 (in hv2). hv2 should claim sw0-vir
@@ -14561,7 +14613,7 @@ ovn-sbctl dump-flows lr0 | grep lr_in_arp_resolve | grep "reg0 == 10.0.0.10" \
 # There should be an arp resolve flow to resolve the virtual_ip with the
 # sw0-p2's MAC.
 AT_CHECK([cat lflows.txt], [0], [dnl
-  table=9 (lr_in_arp_resolve  ), priority=100  , match=(outport == "lr0-sw0" && reg0 == 10.0.0.10), action=(eth.dst = 50:54:00:00:00:04; next;)
+  table=11(lr_in_arp_resolve  ), priority=100  , match=(outport == "lr0-sw0" && reg0 == 10.0.0.10), action=(eth.dst = 50:54:00:00:00:04; next;)
 ])
 
 # Now send arp reply from sw0-p1. hv1 should claim sw0-vir
@@ -14582,7 +14634,7 @@ ovn-sbctl dump-flows lr0 | grep lr_in_arp_resolve | grep "reg0 == 10.0.0.10" \
 > lflows.txt
 
 AT_CHECK([cat lflows.txt], [0], [dnl
-  table=9 (lr_in_arp_resolve  ), priority=100  , match=(outport == "lr0-sw0" && reg0 == 10.0.0.10), action=(eth.dst = 50:54:00:00:00:03; next;)
+  table=11(lr_in_arp_resolve  ), priority=100  , match=(outport == "lr0-sw0" && reg0 == 10.0.0.10), action=(eth.dst = 50:54:00:00:00:03; next;)
 ])
 
 # Delete hv1-vif1 port. hv1 should release sw0-vir
@@ -14600,7 +14652,7 @@ ovn-sbctl dump-flows lr0 | grep lr_in_arp_resolve | grep "reg0 == 10.0.0.10" \
 > lflows.txt
 
 AT_CHECK([cat lflows.txt], [0], [dnl
-  table=9 (lr_in_arp_resolve  ), priority=100  , match=(outport == "lr0-sw0" && reg0 == 10.0.0.10), action=(eth.dst = 00:00:00:00:00:00; next;)
+  table=11(lr_in_arp_resolve  ), priority=100  , match=(outport == "lr0-sw0" && reg0 == 10.0.0.10), action=(eth.dst = 00:00:00:00:00:00; next;)
 ])
 
 # Now send arp reply from sw0-p2. hv2 should claim sw0-vir
@@ -14621,7 +14673,7 @@ ovn-sbctl dump-flows lr0 | grep lr_in_arp_resolve | grep "reg0 == 10.0.0.10" \
 > lflows.txt
 
 AT_CHECK([cat lflows.txt], [0], [dnl
-  table=9 (lr_in_arp_resolve  ), priority=100  , match=(outport == "lr0-sw0" && reg0 == 10.0.0.10), action=(eth.dst = 50:54:00:00:00:04; next;)
+  table=11(lr_in_arp_resolve  ), priority=100  , match=(outport == "lr0-sw0" && reg0 == 10.0.0.10), action=(eth.dst = 50:54:00:00:00:04; next;)
 ])
 
 # Delete sw0-p2 logical port
@@ -15811,3 +15863,225 @@ as hv4 ovs-appctl fdb/show br-phys
 OVN_CLEANUP([hv1],[hv2],[hv3],[hv4])
 
 AT_CLEANUP
+
+AT_SETUP([ovn -- ARP lookup before learning])
+AT_KEYWORDS([virtual ports])
+AT_SKIP_IF([test $HAVE_PYTHON = no])
+ovn_start
+
+send_garp() {
+    local hv=$1 inport=$2 eth_src=$3 eth_dst=$4 spa=$5 tpa=$6
+    local request=${eth_dst}${eth_src}08060001080006040001${eth_src}${spa}${eth_dst}${tpa}
+    as hv$hv ovs-appctl netdev-dummy/receive hv${hv}-vif$inport $request
+}
+
+send_arp_reply() {
+    local hv=$1 inport=$2 eth_src=$3 eth_dst=$4 spa=$5 tpa=$6
+    local request=${eth_dst}${eth_src}08060001080006040002${eth_src}${spa}${eth_dst}${tpa}
+    as hv$hv ovs-appctl netdev-dummy/receive hv${hv}-vif$inport $request
+}
+
+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 hv1-vif1 -- \
+    set interface hv1-vif1 external-ids:iface-id=sw0-p1 \
+    options:tx_pcap=hv1/vif1-tx.pcap \
+    options:rxq_pcap=hv1/vif1-rx.pcap \
+    ofport-request=1
+ovs-vsctl -- add-port br-int hv1-vif2 -- \
+    set interface hv1-vif2 external-ids:iface-id=sw0-p3 \
+    options:tx_pcap=hv1/vif2-tx.pcap \
+    options:rxq_pcap=hv1/vif2-rx.pcap \
+    ofport-request=2
+
+sim_add hv2
+as hv2
+ovs-vsctl add-br br-phys
+ovn_attach n1 br-phys 192.168.0.2
+ovs-vsctl -- add-port br-int hv2-vif1 -- \
+    set interface hv2-vif1 external-ids:iface-id=sw1-p1 \
+    options:tx_pcap=hv2/vif1-tx.pcap \
+    options:rxq_pcap=hv2/vif1-rx.pcap \
+    ofport-request=1
+
+ovn-nbctl ls-add sw0
+
+ovn-nbctl lsp-add sw0 sw0-p1
+ovn-nbctl lsp-set-addresses sw0-p1 "50:54:00:00:00:03"
+
+# Create the second logical switch with one port
+ovn-nbctl ls-add sw1
+ovn-nbctl lsp-add sw1 sw1-p1
+ovn-nbctl lsp-set-addresses sw1-p1 "40:54:00:00:00:03 20.0.0.3"
+ovn-nbctl lsp-set-port-security sw1-p1 "40:54:00:00:00:03 20.0.0.3"
+
+# Create a logical router and attach both logical switches
+ovn-nbctl lr-add lr0
+ovn-nbctl lrp-add lr0 lr0-sw0 00:00:00:00:ff:01 10.0.0.1/24
+ovn-nbctl lsp-add sw0 sw0-lr0
+ovn-nbctl lsp-set-type sw0-lr0 router
+ovn-nbctl lsp-set-addresses sw0-lr0 00:00:00:00:ff:01
+ovn-nbctl lsp-set-options sw0-lr0 router-port=lr0-sw0
+
+ovn-nbctl lrp-add lr0 lr0-sw1 00:00:00:00:ff:02 20.0.0.1/24
+ovn-nbctl lsp-add sw1 sw1-lr0
+ovn-nbctl lsp-set-type sw1-lr0 router
+ovn-nbctl lsp-set-addresses sw1-lr0 00:00:00:00:ff:02
+ovn-nbctl lsp-set-options sw1-lr0 router-port=lr0-sw1
+
+OVN_POPULATE_ARP
+ovn-nbctl --wait=hv sync
+
+as hv1 ovs-appctl -t ovn-controller vlog/set dbg
+
+ip_to_hex() {
+    printf "%02x%02x%02x%02x" "$@"
+}
+
+# From sw0-p1 send GARP for 10.0.0.30.
+# ovn-controller should learn the
+#   mac_binding entry
+#     port - lr0-sw0
+#     ip - 10.0.0.30
+#     mac - 50:54:00:00:00:03
+
+AT_CHECK([test 0 = `ovn-sbctl list mac_binding | wc -l`])
+eth_src=505400000003
+eth_dst=ffffffffffff
+spa=$(ip_to_hex 10 0 0 30)
+tpa=$(ip_to_hex 10 0 0 30)
+send_garp 1 1 $eth_src $eth_dst $spa $tpa
+
+OVS_WAIT_UNTIL([test 1 = `ovn-sbctl --bare --columns _uuid list mac_binding | wc -l`])
+
+AT_CHECK([ovn-sbctl --format=csv --bare --columns logical_port,ip,mac \
+list mac_binding], [0], [lr0-sw0
+10.0.0.30
+50:54:00:00:00:03
+])
+
+AT_CHECK([test 1 = `cat hv1/ovn-controller.log | grep NXT_PACKET_IN2 | wc -l`])
+AT_CHECK([test 1 = `as hv1 ovs-ofctl dump-flows br-int table=10 | grep arp | \
+grep controller | grep -v n_packets=0 | wc -l`])
+
+# Wait for an entry in table=31
+OVS_WAIT_UNTIL(
+    [test 1 = `as hv1 ovs-ofctl dump-flows br-int table=31 | grep n_packets=0 \
+| wc -l`]
+)
+
+# Send garp again. This time the packet should not be sent to ovn-controller.
+send_garp 1 1 $eth_src $eth_dst $spa $tpa
+# Wait for an entry in table=31
+OVS_WAIT_UNTIL([test 1 = `as hv1 ovs-ofctl dump-flows br-int table=31 | grep n_packets=1 | wc -l`])
+
+# The packet should not be sent to ovn-controller. The packet
+count should be 1 only.
+AT_CHECK([test 1 = `cat hv1/ovn-controller.log | grep NXT_PACKET_IN2 | wc -l`])
+AT_CHECK([test 1 = `as hv1 ovs-ofctl dump-flows br-int table=10 | grep arp | \
+grep controller | grep -v n_packets=0 | wc -l`])
+
+# Now send garp packet with different mac.
+eth_src=505400000013
+eth_dst=ffffffffffff
+spa=$(ip_to_hex 10 0 0 30)
+tpa=$(ip_to_hex 10 0 0 30)
+send_garp 1 1 $eth_src $eth_dst $spa $tpa
+
+# The garp packet should be sent to ovn-controller and the mac_binding entry
+# should be updated.
+OVS_WAIT_UNTIL([test 2 = `cat hv1/ovn-controller.log | grep NXT_PACKET_IN2 | wc -l`])
+
+AT_CHECK([test 1 = `ovn-sbctl --bare --columns _uuid list mac_binding | wc -l`])
+
+AT_CHECK([ovn-sbctl --format=csv --bare --columns logical_port,ip,mac \
+list mac_binding], [0], [lr0-sw0
+10.0.0.30
+50:54:00:00:00:13
+])
+
+# Send ARP request to lrp - lr0-sw1 (20.0.0.1) using src mac 50:54:00:00:00:33
+# and src ip - 10.0.0.50.from sw0-p1.
+# ovn-controller should add the mac_binding entry
+#   logical_port - lr0
+#   IP           - 10.0.0.50
+#   MAC          - 50:54:00:00:00:33
+eth_src=505400000033
+eth_dst=ffffffffffff
+spa=$(ip_to_hex 10 0 0 50)
+tpa=$(ip_to_hex 20 0 0 1)
+
+send_garp 1 1 $eth_src $eth_dst $spa $tpa
+
+# The garp packet should be sent to ovn-controller and the mac_binding entry
+# should be updated.
+OVS_WAIT_UNTIL([test 3 = `cat hv1/ovn-controller.log | grep NXT_PACKET_IN2 | wc -l`])
+
+OVS_WAIT_UNTIL(
+    [test 1 = `as hv1 ovs-ofctl dump-flows br-int table=31 | grep dl_src=50:54:00:00:00:33 \
+| wc -l`]
+)
+
+AT_CHECK([ovn-sbctl --format=csv --bare --columns logical_port,ip,mac \
+find mac_binding ip=10.0.0.50], [0], [lr0-sw0
+10.0.0.50
+50:54:00:00:00:33
+])
+
+# Send the same packet again.
+send_garp 1 1 $eth_src $eth_dst $spa $tpa
+
+OVS_WAIT_UNTIL(
+    [test 1 = `as hv1 ovs-ofctl dump-flows br-int table=31 | grep dl_src=50:54:00:00:00:33 \
+| grep n_packets=1 | wc -l`]
+)
+
+AT_CHECK([test 3 = `cat hv1/ovn-controller.log | grep NXT_PACKET_IN2 | wc -l`])
+
+# Now send ARP reply packet with IP - 10.0.0.40 and mac 505400000023
+eth_src=505400000023
+eth_dst=ffffffffffff
+spa=$(ip_to_hex 10 0 0 40)
+tpa=$(ip_to_hex 10 0 0 50)
+send_arp_reply 1 1 $eth_src $eth_dst $spa $tpa
+
+# ovn-controller should add the
+#   mac_binding entry
+#     port - lr0-sw0
+#     ip - 10.0.0.40
+#     mac - 50:54:00:00:00:23
+
+# The garp packet should be sent to ovn-controller and the mac_binding entry
+# should be updated.
+OVS_WAIT_UNTIL([test 4 = `cat hv1/ovn-controller.log | grep NXT_PACKET_IN2 | wc -l`])
+
+# Wait for an entry in table=31 for the learnt mac_binding entry.
+
+OVS_WAIT_UNTIL(
+    [test 1 = `as hv1 ovs-ofctl dump-flows br-int table=31 | grep dl_src=50:54:00:00:00:23 \
+| wc -l`]
+)
+
+# Send the same garp reply. This time it should not be sent to ovn-controller.
+send_arp_reply 1 1 $eth_src $eth_dst $spa $tpa
+OVS_WAIT_UNTIL(
+    [test 1 = `as hv1 ovs-ofctl dump-flows br-int table=31 | grep dl_src=50:54:00:00:00:23 \
+| grep n_packets=1 | wc -l`]
+)
+
+AT_CHECK([test 4 = `cat hv1/ovn-controller.log | grep NXT_PACKET_IN2 | wc -l`])
+
+send_arp_reply 1 1 $eth_src $eth_dst $spa $tpa
+OVS_WAIT_UNTIL(
+    [test 1 = `as hv1 ovs-ofctl dump-flows br-int table=31 | grep dl_src=50:54:00:00:00:23 \
+| grep n_packets=2 | wc -l`]
+)
+
+AT_CHECK([test 4 = `cat hv1/ovn-controller.log | grep NXT_PACKET_IN2 | wc -l`])
+
+OVN_CLEANUP([hv1], [hv2])
+AT_CLEANUP
diff --git a/tests/test-ovn.c b/tests/test-ovn.c
index 8462c21b6..e96321bd6 100644
--- a/tests/test-ovn.c
+++ b/tests/test-ovn.c
@@ -1297,6 +1297,7 @@ test_parse_actions(struct ovs_cmdl_context *ctx OVS_UNUSED)
                 .egress_ptable = 40,
                 .output_ptable = 64,
                 .mac_bind_ptable = 65,
+                .mac_lookup_ptable = 31,
             };
             struct ofpbuf ofpacts;
             ofpbuf_init(&ofpacts, 0);
diff --git a/utilities/ovn-trace.c b/utilities/ovn-trace.c
index 0583610b9..2bd0f906b 100644
--- a/utilities/ovn-trace.c
+++ b/utilities/ovn-trace.c
@@ -556,6 +556,22 @@ ovntrace_mac_binding_find(const struct ovntrace_datapath *dp,
     return NULL;
 }
 
+static const struct ovntrace_mac_binding *
+ovntrace_mac_binding_find_mac_ip(const struct ovntrace_datapath *dp,
+                                 uint16_t port_key, const struct in6_addr *ip,
+                                 struct eth_addr mac)
+{
+    const struct ovntrace_mac_binding *bind;
+    HMAP_FOR_EACH_WITH_HASH (bind, node, hash_mac_binding(port_key, ip),
+                             &dp->mac_bindings) {
+        if (bind->port_key == port_key && ipv6_addr_equals(ip, &bind->ip)
+            && eth_addr_equals(bind->mac, mac)) {
+            return bind;
+        }
+    }
+    return NULL;
+}
+
 /* If 's' ends with a UUID, returns a copy of it with the UUID truncated to
  * just the first 6 characters; otherwise, returns a copy of 's'. */
 static char *
@@ -1704,6 +1720,49 @@ execute_get_mac_bind(const struct ovnact_get_mac_bind *bind,
                          ETH_ADDR_ARGS(uflow->dl_dst));
 }
 
+static void
+execute_lookup_mac(const struct ovnact_lookup_mac_bind *bind OVS_UNUSED,
+                   const struct ovntrace_datapath *dp OVS_UNUSED,
+                   struct flow *uflow OVS_UNUSED,
+                   struct ovs_list *super OVS_UNUSED)
+{
+    /* Get logical port number.*/
+    struct mf_subfield port_sf = expr_resolve_field(&bind->port);
+    ovs_assert(port_sf.n_bits == 32);
+    uint32_t port_key = mf_get_subfield(&port_sf, uflow);
+
+    /* Get IP address. */
+    struct mf_subfield ip_sf = expr_resolve_field(&bind->ip);
+    ovs_assert(ip_sf.n_bits == 32 || ip_sf.n_bits == 128);
+    union mf_subvalue ip_sv;
+    mf_read_subfield(&ip_sf, uflow, &ip_sv);
+    struct in6_addr ip = (ip_sf.n_bits == 32
+                          ? in6_addr_mapped_ipv4(ip_sv.ipv4)
+                          : ip_sv.ipv6);
+
+    /* Get MAC. */
+    struct mf_subfield mac_sf = expr_resolve_field(&bind->mac);
+    ovs_assert(mac_sf.n_bits == 48);
+    union mf_subvalue mac_sv;
+    mf_read_subfield(&mac_sf, uflow, &mac_sv);
+
+    const struct ovntrace_mac_binding *binding
+        = ovntrace_mac_binding_find_mac_ip(dp, port_key, &ip, mac_sv.mac);
+
+    uflow->dl_dst = binding ? binding->mac : eth_addr_zero;
+    if (binding) {
+        ovntrace_node_append(super, OVNTRACE_NODE_ACTION,
+                             "/* MAC binding to "ETH_ADDR_FMT" found. */",
+                             ETH_ADDR_ARGS(uflow->dl_dst));
+    } else {
+        ovntrace_node_append(super, OVNTRACE_NODE_ACTION,
+                             "/* lookup failed - No MAC binding. */");
+    }
+    ovntrace_node_append(super, OVNTRACE_NODE_MODIFY,
+                         "eth.dst = "ETH_ADDR_FMT,
+                         ETH_ADDR_ARGS(uflow->dl_dst));
+}
+
 static void
 execute_put_opts(const struct ovnact_put_opts *po,
                  const char *name, struct flow *uflow,
@@ -2072,6 +2131,14 @@ trace_actions(const struct ovnact *ovnacts, size_t ovnacts_len,
             /* Nothing to do for tracing. */
             break;
 
+        case OVNACT_LOOKUP_ARP:
+            execute_lookup_mac(ovnact_get_LOOKUP_ARP(a), dp, uflow, super);
+            break;
+
+        case OVNACT_LOOKUP_ND:
+            execute_lookup_mac(ovnact_get_LOOKUP_ND(a), dp, uflow, super);
+            break;
+
         case OVNACT_PUT_DHCPV4_OPTS:
             execute_put_dhcp_opts(ovnact_get_PUT_DHCPV4_OPTS(a),
                                   "put_dhcp_opts", uflow, super);
