diff mbox

[ovs-dev,05/14] Add OpenFlow support for new "arp" action.

Message ID 1449623297-31060-6-git-send-email-blp@ovn.org
State Superseded
Headers show

Commit Message

Ben Pfaff Dec. 9, 2015, 1:08 a.m. UTC
This is useful for generating an ARP request from the OpenFlow flow table,
which in turn is useful for implementing a router.  An upcoming commit
will use this feature to implement ARP request generationg in the OVN L3
logical router.

Signed-off-by: Ben Pfaff <blp@ovn.org>
---
 NEWS                                              |   2 +
 datapath/linux/compat/include/linux/openvswitch.h |   5 +-
 lib/dpif-netdev.c                                 |   3 +-
 lib/dpif.c                                        |   1 +
 lib/odp-execute.c                                 |  26 +++++
 lib/odp-util.c                                    |  12 ++
 lib/ofp-actions.c                                 | 129 +++++++++++++++++++++-
 lib/ofp-actions.h                                 |   3 +-
 lib/ofp-errors.h                                  |   4 +
 lib/packets.c                                     |  36 +++---
 lib/packets.h                                     |   1 +
 ofproto/ofproto-dpif-sflow.c                      |   4 +
 ofproto/ofproto-dpif-xlate.c                      |  70 ++++++++++++
 tests/ofproto-dpif.at                             |  65 +++++++++++
 utilities/ovs-ofctl.8.in                          |  26 +++++
 15 files changed, 370 insertions(+), 17 deletions(-)

Comments

Justin Pettit Dec. 10, 2015, 1:57 a.m. UTC | #1
> On Dec 8, 2015, at 5:08 PM, Ben Pfaff <blp@ovn.org> wrote:
> 
> --- a/NEWS
> +++ b/NEWS
> @@ -1,5 +1,7 @@
> Post-v2.5.0
> ---------------------
> +   - OpenFlow:
> +     * New "arp" OpenFlow extension action for generating ARP packets.

Once you have a design for packet injection from a controller, I think we should revisit this patch.  My initial reaction is that I'd prefer to not introduce changes to OVS unless they're really needed, and it's not clear to me whether that's the case here.  We're going to be handling the IPv6 equivalent from the controller, so there'd be some parity.

> + * @OVS_ACTION_ATTR_ARP: Transform IPv4 packet into ARP packet, execute nested
> + * actions, restore IPv4 packet.

Another advantage of initiating the ARP in the controller is that we can buffer the packet, since this means that an ARP miss will always require a retransmission of the original packet.  We'd also have the ability to rate-limit ARP generation from the controller.

> +/* Parses 'arg' as the argument to a "arp" action, and appends such an
> + * action to 'ofpacts'.

s/a "arp"/an "arp"/

> --- a/lib/packets.h
> +++ b/lib/packets.h
> @@ -1041,6 +1041,7 @@ void packet_set_nd(struct dp_packet *, const ovs_be32 target[4],
> 
> void packet_format_tcp_flags(struct ds *, uint16_t);
> const char *packet_tcp_flag_to_string(uint32_t flag);
> +void compose_arp__(struct dp_packet *);

Somehow it seems kind of strange to have a "__" function defined in a header file for external users.

Acked-by: Justin Pettit <jpettit@ovn.org>

--Justin
Ben Pfaff Dec. 16, 2015, 12:18 p.m. UTC | #2
On Wed, Dec 09, 2015 at 05:57:47PM -0800, Justin Pettit wrote:
> 
> > On Dec 8, 2015, at 5:08 PM, Ben Pfaff <blp@ovn.org> wrote:
> > 
> > --- a/NEWS
> > +++ b/NEWS
> > @@ -1,5 +1,7 @@
> > Post-v2.5.0
> > ---------------------
> > +   - OpenFlow:
> > +     * New "arp" OpenFlow extension action for generating ARP packets.
> 
> Once you have a design for packet injection from a controller, I think
> we should revisit this patch.  My initial reaction is that I'd prefer
> to not introduce changes to OVS unless they're really needed, and it's
> not clear to me whether that's the case here.  We're going to be
> handling the IPv6 equivalent from the controller, so there'd be some
> parity.

That's true.

> > + * @OVS_ACTION_ATTR_ARP: Transform IPv4 packet into ARP packet, execute nested
> > + * actions, restore IPv4 packet.
> 
> Another advantage of initiating the ARP in the controller is that we
> can buffer the packet, since this means that an ARP miss will always
> require a retransmission of the original packet.  We'd also have the
> ability to rate-limit ARP generation from the controller.

I want to equivocate here.

Rate-limiting always works better the closer you get to the source.  If
we rate-limit at the controller, then we're not rate-limiting the
packets sent to the controller by the switch.  That makes it hard to
achieve fairness.  That is, if we send all the packets that need an ARP
to the controller, and there's a big stream of those that the controller
is going to drop, and some packet slips in that won't be dropped, then
the socket buffer between the switch and controller is what's actually
going to drop them, and statistically the fairness there isn't going to
be good.

Therefore, I don't think we should rate-limit from the controller.  We
do have some mechanisms to rate-limit from the switch.  One way we can
do it today is to "learn" a flow that drops packets with a suitable
(e.g. 1-second) hard timeout; I don't know whether this is the best way
though.

I don't think that ARP generation from the controller is mutually
exclusive with buffering packets.

> > +/* Parses 'arg' as the argument to a "arp" action, and appends such an
> > + * action to 'ofpacts'.
> 
> s/a "arp"/an "arp"/

Thanks, fixed.

> > --- a/lib/packets.h
> > +++ b/lib/packets.h
> > @@ -1041,6 +1041,7 @@ void packet_set_nd(struct dp_packet *, const ovs_be32 target[4],
> > 
> > void packet_format_tcp_flags(struct ds *, uint16_t);
> > const char *packet_tcp_flag_to_string(uint32_t flag);
> > +void compose_arp__(struct dp_packet *);
> 
> Somehow it seems kind of strange to have a "__" function defined in a
> header file for external users.

Sorry.

> Acked-by: Justin Pettit <jpettit@ovn.org>

I'm going to hold off on pushing this one for the moment.
diff mbox

Patch

diff --git a/NEWS b/NEWS
index a6b1599..578171d 100644
--- a/NEWS
+++ b/NEWS
@@ -1,5 +1,7 @@ 
 Post-v2.5.0
 ---------------------
+   - OpenFlow:
+     * New "arp" OpenFlow extension action for generating ARP packets.
 
 
 v2.5.0 - xx xxx xxxx
diff --git a/datapath/linux/compat/include/linux/openvswitch.h b/datapath/linux/compat/include/linux/openvswitch.h
index 3b39ebb..5acf539 100644
--- a/datapath/linux/compat/include/linux/openvswitch.h
+++ b/datapath/linux/compat/include/linux/openvswitch.h
@@ -1,5 +1,5 @@ 
 /*
- * Copyright (c) 2007-2014 Nicira, Inc.
+ * Copyright (c) 2007-2015 Nicira, Inc.
  *
  * This file is offered under your choice of two licenses: Apache 2.0 or GNU
  * GPL 2.0 or later.  The permission statements for each of these licenses is
@@ -783,6 +783,8 @@  enum ovs_nat_attr {
  * ovs_action_push_tnl.
  * @OVS_ACTION_ATTR_TUNNEL_POP: Lookup tunnel port by port-no passed and pop
  * tunnel header.
+ * @OVS_ACTION_ATTR_ARP: Transform IPv4 packet into ARP packet, execute nested
+ * actions, restore IPv4 packet.
  */
 
 enum ovs_action_attr {
@@ -806,6 +808,7 @@  enum ovs_action_attr {
 #ifndef __KERNEL__
 	OVS_ACTION_ATTR_TUNNEL_PUSH,   /* struct ovs_action_push_tnl*/
 	OVS_ACTION_ATTR_TUNNEL_POP,    /* u32 port number. */
+	OVS_ACTION_ATTR_ARP,	       /* Nested OVS_ACTION_ATTR_*. */
 #endif
 	__OVS_ACTION_ATTR_MAX,	      /* Nothing past this will be accepted
 				       * from userspace. */
diff --git a/lib/dpif-netdev.c b/lib/dpif-netdev.c
index a67ef05..d382fde 100644
--- a/lib/dpif-netdev.c
+++ b/lib/dpif-netdev.c
@@ -1,5 +1,5 @@ 
 /*
- * Copyright (c) 2009, 2010, 2011, 2012, 2013, 2014 Nicira, Inc.
+ * Copyright (c) 2009, 2010, 2011, 2012, 2013, 2014, 2015 Nicira, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -3627,6 +3627,7 @@  dp_execute_cb(void *aux_, struct dp_packet **packets, int cnt,
     case OVS_ACTION_ATTR_SET_MASKED:
     case OVS_ACTION_ATTR_SAMPLE:
     case OVS_ACTION_ATTR_HASH:
+    case OVS_ACTION_ATTR_ARP:
     case OVS_ACTION_ATTR_UNSPEC:
     case __OVS_ACTION_ATTR_MAX:
         OVS_NOT_REACHED();
diff --git a/lib/dpif.c b/lib/dpif.c
index 38e40ba..2926668 100644
--- a/lib/dpif.c
+++ b/lib/dpif.c
@@ -1146,6 +1146,7 @@  dpif_execute_helper_cb(void *aux_, struct dp_packet **packets, int cnt,
     case OVS_ACTION_ATTR_SET:
     case OVS_ACTION_ATTR_SET_MASKED:
     case OVS_ACTION_ATTR_SAMPLE:
+    case OVS_ACTION_ATTR_ARP:
     case OVS_ACTION_ATTR_UNSPEC:
     case __OVS_ACTION_ATTR_MAX:
         OVS_NOT_REACHED();
diff --git a/lib/odp-execute.c b/lib/odp-execute.c
index b5204b2..5e5c3ca 100644
--- a/lib/odp-execute.c
+++ b/lib/odp-execute.c
@@ -34,6 +34,8 @@ 
 #include "unaligned.h"
 #include "util.h"
 
+#include "openvswitch/vlog.h"
+
 /* Masked copy of an ethernet address. 'src' is already properly masked. */
 static void
 ether_addr_copy_masked(struct eth_addr *dst, const struct eth_addr src,
@@ -480,6 +482,23 @@  odp_execute_sample(void *dp, struct dp_packet *packet, bool steal,
                         nl_attr_get_size(subactions), dp_execute_action);
 }
 
+static void
+odp_execute_arp(void *dp, struct dp_packet *ip,
+                const struct nlattr *action, odp_execute_cb dp_execute_action)
+{
+    uint64_t stub[DIV_ROUND_UP(2 + ETH_HEADER_LEN + VLAN_HEADER_LEN +
+                               ARP_ETH_HEADER_LEN, 8)];
+    struct dp_packet arp;
+    dp_packet_use_stub(&arp, stub, sizeof stub);
+    compose_arp__(&arp);
+    arp.md = ip->md;
+
+    struct dp_packet *arpp = &arp;
+    odp_execute_actions(dp, &arpp, 1, false, nl_attr_get(action),
+                        nl_attr_get_size(action), dp_execute_action);
+    dp_packet_uninit(&arp);
+}
+
 static bool
 requires_datapath_assistance(const struct nlattr *a)
 {
@@ -503,6 +522,7 @@  requires_datapath_assistance(const struct nlattr *a)
     case OVS_ACTION_ATTR_HASH:
     case OVS_ACTION_ATTR_PUSH_MPLS:
     case OVS_ACTION_ATTR_POP_MPLS:
+    case OVS_ACTION_ATTR_ARP:
         return false;
 
     case OVS_ACTION_ATTR_UNSPEC:
@@ -623,6 +643,12 @@  odp_execute_actions(void *dp, struct dp_packet **packets, int cnt, bool steal,
             }
             break;
 
+        case OVS_ACTION_ATTR_ARP:
+            for (i = 0; i < cnt; i++) {
+                odp_execute_arp(dp, packets[i], a, dp_execute_action);
+            }
+            break;
+
         case OVS_ACTION_ATTR_OUTPUT:
         case OVS_ACTION_ATTR_TUNNEL_PUSH:
         case OVS_ACTION_ATTR_TUNNEL_POP:
diff --git a/lib/odp-util.c b/lib/odp-util.c
index 90942c7..8fe14c5 100644
--- a/lib/odp-util.c
+++ b/lib/odp-util.c
@@ -120,6 +120,7 @@  odp_action_len(uint16_t type)
     case OVS_ACTION_ATTR_SET_MASKED: return ATTR_LEN_VARIABLE;
     case OVS_ACTION_ATTR_SAMPLE: return ATTR_LEN_VARIABLE;
     case OVS_ACTION_ATTR_CT: return ATTR_LEN_VARIABLE;
+    case OVS_ACTION_ATTR_ARP: return ATTR_LEN_VARIABLE;
 
     case OVS_ACTION_ATTR_UNSPEC:
     case __OVS_ACTION_ATTR_MAX:
@@ -766,6 +767,14 @@  format_odp_conntrack_action(struct ds *ds, const struct nlattr *attr)
 }
 
 static void
+format_odp_arp_action(struct ds *ds, const struct nlattr *attr)
+{
+    ds_put_cstr(ds, "arp(");
+    format_odp_actions(ds, nl_attr_get(attr), nl_attr_get_size(attr));
+    ds_put_char(ds, ')');
+}
+
+static void
 format_odp_action(struct ds *ds, const struct nlattr *a)
 {
     int expected_len;
@@ -858,6 +867,9 @@  format_odp_action(struct ds *ds, const struct nlattr *a)
     case OVS_ACTION_ATTR_CT:
         format_odp_conntrack_action(ds, a);
         break;
+    case OVS_ACTION_ATTR_ARP:
+        format_odp_arp_action(ds, a);
+        break;
     case OVS_ACTION_ATTR_UNSPEC:
     case __OVS_ACTION_ATTR_MAX:
     default:
diff --git a/lib/ofp-actions.c b/lib/ofp-actions.c
index 354c768..92b296b 100644
--- a/lib/ofp-actions.c
+++ b/lib/ofp-actions.c
@@ -295,6 +295,9 @@  enum ofp_raw_action_type {
     /* NX1.0+(36): struct nx_action_nat, ... */
     NXAST_RAW_NAT,
 
+    /* NX1.0+(37): struct nx_action_arp, ... */
+    NXAST_RAW_ARP,
+
 /* ## ------------------ ## */
 /* ## Debugging actions. ## */
 /* ## ------------------ ## */
@@ -423,6 +426,9 @@  ofpact_next_flattened(const struct ofpact *ofpact)
     case OFPACT_CT:
         return ofpact_get_CT(ofpact)->actions;
 
+    case OFPACT_ARP:
+        return ofpact_get_ARP(ofpact)->actions;
+
     case OFPACT_WRITE_ACTIONS:
         return ofpact_get_WRITE_ACTIONS(ofpact)->actions;
     }
@@ -5349,7 +5355,108 @@  parse_NAT(char *arg, struct ofpbuf *ofpacts,
     }
     return NULL;
 }
+
+/* Action structure for NXAST_ARP.
+ *
+ * The only content is a series of nested OpenFlow actions to act on the ARP
+ * packet.
+ */
+struct nx_action_arp {
+    ovs_be16 type;              /* OFPAT_VENDOR. */
+    ovs_be16 len;               /* At least 16. */
+    ovs_be32 vendor;            /* NX_VENDOR_ID. */
+    ovs_be16 subtype;           /* NXAST_ARP. */
+    uint8_t zero[6];            /* Must be zero. */
+    /* Followed by a sequence of zero or more OpenFlow actions. The length of
+     * these is included in 'len'. */
+};
+OFP_ASSERT(sizeof(struct nx_action_arp) == 16);
+
+static enum ofperr
+decode_NXAST_RAW_ARP(const struct nx_action_arp *naa,
+                     enum ofp_version ofp_version, struct ofpbuf *out)
+{
+    const size_t arp_offset = ofpacts_pull(out);
+    struct ofpact_nest *arp;
+    struct ofpbuf openflow;
+    int error = 0;
+
+    if (!is_all_zeros(naa->zero, sizeof naa->zero)) {
+        return OFPERR_NXBAC_MUST_BE_ZERO;
+    }
+
+    arp = ofpact_put_ARP(out);
+    ofpbuf_pull(out, sizeof *arp);
+
+    ofpbuf_use_const(&openflow, naa + 1, ntohs(naa->len) - sizeof *naa);
+    error = ofpacts_pull_openflow_actions__(
+        &openflow, openflow.size, ofp_version,
+        1u << OVSINST_OFPIT11_APPLY_ACTIONS, out, OFPACT_ARP);
+    if (error) {
+        goto out;
+    }
+
+    arp = ofpbuf_push_uninit(out, sizeof *arp);
+    out->header = &arp->ofpact;
+    ofpact_update_len(out, &arp->ofpact);
+
+out:
+    ofpbuf_push_uninit(out, arp_offset);
+    return error;
+}
+
+static void
+encode_ARP(const struct ofpact_nest *arp, enum ofp_version ofp_version,
+           struct ofpbuf *out)
+{
+    struct nx_action_arp *naa;
+    const size_t ofs = out->size;
+    size_t len;
+
+    naa = put_NXAST_ARP(out);
+
+    len = ofpacts_put_openflow_actions(arp->actions,
+                                       ofpact_nest_get_action_len(arp),
+                                       out, ofp_version);
+    len += sizeof *naa;
+    naa = ofpbuf_at(out, ofs, sizeof *naa);
+    naa->len = htons(len);
+}
+
+/* Parses 'arg' as the argument to a "arp" action, and appends such an
+ * action to 'ofpacts'.
+ *
+ * Returns NULL if successful, otherwise a malloc()'d string describing the
+ * error.  The caller is responsible for freeing the returned string. */
+static char * OVS_WARN_UNUSED_RESULT
+parse_ARP(char *arg, struct ofpbuf *ofpacts,
+         enum ofputil_protocol *usable_protocols)
+{
+    const size_t arp_offset = ofpacts_pull(ofpacts);
+    struct ofpact_nest *arp = ofpact_put_ARP(ofpacts);
+
+    /* Hide existing actions from ofpacts_parse_copy(), so the
+     * nesting can be handled transparently. */
+    ofpbuf_pull(ofpacts, sizeof *arp);
+    char *error = ofpacts_parse_copy(arg, ofpacts, usable_protocols, false,
+                                     OFPACT_ARP);
+    ofpact_pad(ofpacts);
+    ofpacts->header = ofpbuf_push_uninit(ofpacts, sizeof *arp);
+    arp = ofpacts->header;
+
+    ofpact_update_len(ofpacts, &arp->ofpact);
+    ofpbuf_push_uninit(ofpacts, arp_offset);
+
+    return error;
+}
 
+static void
+format_ARP(const struct ofpact_nest *a, struct ds *s)
+{
+    ds_put_cstr(s, "arp(");
+    ofpacts_format(a->actions, ofpact_nest_get_action_len(a), s);
+    ds_put_char(s, ')');
+}
 
 /* Meter instruction. */
 
@@ -5749,6 +5856,7 @@  ofpact_is_set_or_move_action(const struct ofpact *a)
     case OFPACT_OUTPUT:
     case OFPACT_OUTPUT_REG:
     case OFPACT_POP_MPLS:
+    case OFPACT_ARP:
     case OFPACT_POP_QUEUE:
     case OFPACT_PUSH_MPLS:
     case OFPACT_PUSH_VLAN:
@@ -5822,6 +5930,7 @@  ofpact_is_allowed_in_actions_set(const struct ofpact *a)
     case OFPACT_SAMPLE:
     case OFPACT_STACK_POP:
     case OFPACT_STACK_PUSH:
+    case OFPACT_ARP:
     case OFPACT_DEBUG_RECIRC:
 
     /* The action set may only include actions and thus
@@ -6024,6 +6133,7 @@  ovs_instruction_type_from_ofpact_type(enum ofpact_type type)
     case OFPACT_DEC_MPLS_TTL:
     case OFPACT_PUSH_MPLS:
     case OFPACT_POP_MPLS:
+    case OFPACT_ARP:
     case OFPACT_SET_TUNNEL:
     case OFPACT_SET_QUEUE:
     case OFPACT_POP_QUEUE:
@@ -6592,6 +6702,21 @@  ofpact_check__(enum ofputil_protocol *usable_protocols, struct ofpact *a,
         flow->dl_type = ofpact_get_POP_MPLS(a)->ethertype;
         return 0;
 
+    case OFPACT_ARP: {
+        if (flow->dl_type != htons(ETH_TYPE_IP)) {
+            return OFPERR_NXBAC_ARP_REQUIRES_IPV4;
+        }
+
+        flow->dl_type = htons(ETH_TYPE_ARP);
+        struct ofpact_nest *oa = ofpact_get_ARP(a);
+        enum ofperr error = ofpacts_check(oa->actions,
+                                          ofpact_nest_get_action_len(oa),
+                                          flow, max_ports, table_id, n_tables,
+                                          usable_protocols);
+        flow->dl_type = htons(ETH_TYPE_IP);
+        return error;
+    }
+
     case OFPACT_SAMPLE:
         return 0;
 
@@ -6784,7 +6909,8 @@  ofpacts_verify_nested(const struct ofpact *a, enum ofpact_type outer_action)
 
     if (outer_action) {
         ovs_assert(outer_action == OFPACT_WRITE_ACTIONS
-                   || outer_action == OFPACT_CT);
+                   || outer_action == OFPACT_CT
+                   || outer_action == OFPACT_ARP);
 
         if (outer_action == OFPACT_CT) {
             if (!field) {
@@ -7130,6 +7256,7 @@  ofpact_outputs_to_port(const struct ofpact *ofpact, ofp_port_t port)
     case OFPACT_SET_MPLS_TC:
     case OFPACT_SET_MPLS_TTL:
     case OFPACT_DEC_MPLS_TTL:
+    case OFPACT_ARP:
     case OFPACT_SET_TUNNEL:
     case OFPACT_WRITE_METADATA:
     case OFPACT_SET_QUEUE:
diff --git a/lib/ofp-actions.h b/lib/ofp-actions.h
index 18c7395..66b82ed 100644
--- a/lib/ofp-actions.h
+++ b/lib/ofp-actions.h
@@ -86,6 +86,7 @@ 
     OFPACT(DEC_MPLS_TTL,    ofpact_null,        ofpact, "dec_mpls_ttl") \
     OFPACT(PUSH_MPLS,       ofpact_push_mpls,   ofpact, "push_mpls")    \
     OFPACT(POP_MPLS,        ofpact_pop_mpls,    ofpact, "pop_mpls")     \
+    OFPACT(ARP,             ofpact_nest,        ofpact, "arp")          \
                                                                         \
     /* Metadata. */                                                     \
     OFPACT(SET_TUNNEL,      ofpact_tunnel,      ofpact, "set_tunnel")   \
@@ -475,7 +476,7 @@  struct ofpact_meter {
 
 /* OFPACT_WRITE_ACTIONS.
  *
- * Used for OFPIT11_WRITE_ACTIONS. */
+ * Used for OFPIT11_WRITE_ACTIONS and NXAST_ARP. */
 struct ofpact_nest {
     struct ofpact ofpact;
     uint8_t pad[PAD_SIZE(sizeof(struct ofpact), OFPACT_ALIGNTO)];
diff --git a/lib/ofp-errors.h b/lib/ofp-errors.h
index 9c108d0..f40faf7 100644
--- a/lib/ofp-errors.h
+++ b/lib/ofp-errors.h
@@ -269,6 +269,10 @@  enum ofperr {
      * 64. */
     OFPERR_NXBAC_BAD_CONJUNCTION,
 
+    /* NX1.0-1.1(2,527), NX1.2+(23).  ARP is allowed only in a flow that
+     * matches on Ethertype 0x800. */
+    OFPERR_NXBAC_ARP_REQUIRES_IPV4,
+
 /* ## --------------------- ## */
 /* ## OFPET_BAD_INSTRUCTION ## */
 /* ## --------------------- ## */
diff --git a/lib/packets.c b/lib/packets.c
index d82341d..386a6a4 100644
--- a/lib/packets.c
+++ b/lib/packets.c
@@ -1191,35 +1191,45 @@  packet_format_tcp_flags(struct ds *s, uint16_t tcp_flags)
  * 'arp_op', 'arp_sha', 'arp_tha', 'arp_spa', and 'arp_tpa'.  The outer
  * Ethernet frame is initialized with Ethernet source 'arp_sha' and destination
  * 'arp_tha', except that destination ff:ff:ff:ff:ff:ff is used instead if
- * 'broadcast' is true. */
+ * 'broadcast' is true.  Points the L3 header to the ARP header. */
 void
 compose_arp(struct dp_packet *b, uint16_t arp_op,
             const struct eth_addr arp_sha, const struct eth_addr arp_tha,
             bool broadcast, ovs_be32 arp_spa, ovs_be32 arp_tpa)
 {
-    struct eth_header *eth;
-    struct arp_eth_header *arp;
+    compose_arp__(b);
+
+    struct eth_header *eth = dp_packet_l2(b);
+    eth->eth_dst = broadcast ? eth_addr_broadcast : arp_tha;
+    eth->eth_src = arp_sha;
+
+    struct arp_eth_header *arp = dp_packet_l3(b);
+    arp->ar_op = htons(arp_op);
+    arp->ar_sha = arp_sha;
+    arp->ar_tha = arp_tha;
+    put_16aligned_be32(&arp->ar_spa, arp_spa);
+    put_16aligned_be32(&arp->ar_tpa, arp_tpa);
+}
 
+/* Clears 'b' and replaces its contents by an ARP frame.  Sets the fields in
+ * the Ethernet and ARP headers that are fixed for ARP frames to those fixed
+ * values, and zeroes the other fields.  Points the L3 header to the ARP
+ * header. */
+void
+compose_arp__(struct dp_packet *b)
+{
     dp_packet_clear(b);
     dp_packet_prealloc_tailroom(b, ARP_PACKET_SIZE);
     dp_packet_reserve(b, 2 + VLAN_HEADER_LEN);
 
-    eth = dp_packet_put_uninit(b, sizeof *eth);
-    eth->eth_dst = broadcast ? eth_addr_broadcast : arp_tha;
-    eth->eth_src = arp_sha;
+    struct eth_header *eth = dp_packet_put_zeros(b, sizeof *eth);
     eth->eth_type = htons(ETH_TYPE_ARP);
 
-    arp = dp_packet_put_uninit(b, sizeof *arp);
+    struct arp_eth_header *arp = dp_packet_put_zeros(b, sizeof *arp);
     arp->ar_hrd = htons(ARP_HRD_ETHERNET);
     arp->ar_pro = htons(ARP_PRO_IP);
     arp->ar_hln = sizeof arp->ar_sha;
     arp->ar_pln = sizeof arp->ar_spa;
-    arp->ar_op = htons(arp_op);
-    arp->ar_sha = arp_sha;
-    arp->ar_tha = arp_tha;
-
-    put_16aligned_be32(&arp->ar_spa, arp_spa);
-    put_16aligned_be32(&arp->ar_tpa, arp_tpa);
 
     dp_packet_reset_offsets(b);
     dp_packet_set_l3(b, arp);
diff --git a/lib/packets.h b/lib/packets.h
index eaa329f..cde0743 100644
--- a/lib/packets.h
+++ b/lib/packets.h
@@ -1041,6 +1041,7 @@  void packet_set_nd(struct dp_packet *, const ovs_be32 target[4],
 
 void packet_format_tcp_flags(struct ds *, uint16_t);
 const char *packet_tcp_flag_to_string(uint32_t flag);
+void compose_arp__(struct dp_packet *);
 void compose_arp(struct dp_packet *, uint16_t arp_op,
                  const struct eth_addr arp_sha,
                  const struct eth_addr arp_tha, bool broadcast,
diff --git a/ofproto/ofproto-dpif-sflow.c b/ofproto/ofproto-dpif-sflow.c
index f11699c..4db3357 100644
--- a/ofproto/ofproto-dpif-sflow.c
+++ b/ofproto/ofproto-dpif-sflow.c
@@ -1138,6 +1138,10 @@  dpif_sflow_read_actions(const struct flow *flow,
 	    }
 	    break;
 
+    case OVS_ACTION_ATTR_ARP:
+        /* XXX Do not handle this for now. */
+        break;
+
 	case OVS_ACTION_ATTR_USERSPACE:
 	case OVS_ACTION_ATTR_RECIRC:
 	case OVS_ACTION_ATTR_HASH:
diff --git a/ofproto/ofproto-dpif-xlate.c b/ofproto/ofproto-dpif-xlate.c
index cf184e4..c1ae417 100644
--- a/ofproto/ofproto-dpif-xlate.c
+++ b/ofproto/ofproto-dpif-xlate.c
@@ -4163,6 +4163,7 @@  recirc_unroll_actions(const struct ofpact *ofpacts, size_t ofpacts_len,
         case OFPACT_CONTROLLER:
         case OFPACT_DEC_MPLS_TTL:
         case OFPACT_DEC_TTL:
+        case OFPACT_ARP:
             recirc_put_unroll_xlate(ctx);
             break;
 
@@ -4387,6 +4388,71 @@  compose_conntrack_action(struct xlate_ctx *ctx, struct ofpact_conntrack *ofc)
 }
 
 static void
+execute_arp_action(struct xlate_ctx *ctx, const struct ofpact_nest *arp)
+{
+    ctx->xout->slow |= SLOW_ACTION;
+
+    /* Commit the current flow and save it.
+     *
+     * (We don't save ctx->base_flow separately because the commit should
+     * ensure that it is identical.) */
+    xlate_commit_actions(ctx);
+    struct flow old_flow = ctx->xin->flow;
+
+    /* Transform ctx->in->flow to ARP. */
+    ctx->xin->flow.dl_type = htons(ETH_TYPE_ARP);
+    ctx->xin->flow.nw_proto = ARP_OP_REQUEST;
+    ctx->xin->flow.arp_sha = ctx->xin->flow.dl_src;
+    ctx->xin->flow.arp_tha = ctx->xin->flow.dl_dst;
+
+    /* This introduces a read dependency on dl_src and dl_dst, so mark them in
+     * the wildcard mask.  We also have to mark the ARP fields; otherwise at
+     * commit time we won't even consider them. */
+    memset(&ctx->wc->masks.dl_src, 0xff, sizeof ctx->wc->masks.dl_src);
+    memset(&ctx->wc->masks.dl_dst, 0xff, sizeof ctx->wc->masks.dl_dst);
+    memset(&ctx->wc->masks.nw_src, 0xff, sizeof ctx->wc->masks.nw_src);
+    memset(&ctx->wc->masks.nw_dst, 0xff, sizeof ctx->wc->masks.nw_dst);
+    memset(&ctx->wc->masks.nw_proto, 0xff, sizeof ctx->wc->masks.nw_proto);
+    memset(&ctx->wc->masks.arp_sha, 0xff, sizeof ctx->wc->masks.arp_sha);
+    memset(&ctx->wc->masks.arp_tha, 0xff, sizeof ctx->wc->masks.arp_tha);
+
+    /* Transform ctx->base_flow to ARP.  This differs from ctx->xin->flow
+     * because the datapath ARP action zeroes all field values. */
+    ctx->base_flow = ctx->xin->flow;
+    ctx->base_flow.vlan_tci = 0;
+    ctx->base_flow.dl_src = eth_addr_zero;
+    ctx->base_flow.dl_dst = eth_addr_zero;
+    ctx->base_flow.nw_src = 0;  /* arp_spa */
+    ctx->base_flow.nw_dst = 0;  /* arp_tpa */
+    ctx->base_flow.arp_sha = eth_addr_zero;
+    ctx->base_flow.arp_tha = eth_addr_zero;
+
+    /* Save ctx->xin->packet.  Then, if it was nonnull, replace it by a
+     * synthesized ARP packet. */
+    const struct dp_packet *old_packet = ctx->xin->packet;
+    uint64_t stub[DIV_ROUND_UP(2 + ETH_HEADER_LEN + VLAN_HEADER_LEN +
+                               ARP_ETH_HEADER_LEN, 8)];
+    struct dp_packet packet;
+    if (old_packet) {
+        dp_packet_use_stub(&packet, stub, sizeof stub);
+        compose_arp__(&packet);
+        packet.md = old_packet->md;
+        ctx->xin->packet = &packet;
+    }
+
+    size_t start = nl_msg_start_nested(ctx->odp_actions, OVS_ACTION_ATTR_ARP);
+    do_xlate_actions(arp->actions, ofpact_nest_get_action_len(arp), ctx);
+    nl_msg_end_nested(ctx->odp_actions, start);
+
+    /* Restore old flow and base_flow.  Restore old packet. */
+    ctx->base_flow = ctx->xin->flow = old_flow;
+    if (old_packet) {
+        dp_packet_uninit(&packet);
+        ctx->xin->packet = old_packet;
+    }
+}
+
+static void
 do_xlate_actions(const struct ofpact *ofpacts, size_t ofpacts_len,
                  struct xlate_ctx *ctx)
 {
@@ -4634,6 +4700,10 @@  do_xlate_actions(const struct ofpact *ofpacts, size_t ofpacts_len,
             compose_mpls_pop_action(ctx, ofpact_get_POP_MPLS(a)->ethertype);
             break;
 
+        case OFPACT_ARP:
+            execute_arp_action(ctx, ofpact_get_ARP(a));
+            break;
+
         case OFPACT_SET_MPLS_LABEL:
             CHECK_MPLS_RECIRCULATION();
             compose_set_mpls_label_action(
diff --git a/tests/ofproto-dpif.at b/tests/ofproto-dpif.at
index 60734db..73f7641 100644
--- a/tests/ofproto-dpif.at
+++ b/tests/ofproto-dpif.at
@@ -3044,6 +3044,71 @@  arp,in_port=ANY,vlan_tci=0x0000,dl_src=80:88:88:88:88:88,dl_dst=ff:ff:ff:ff:ff:f
 OVS_VSWITCHD_STOP
 AT_CLEANUP
 
+AT_SETUP([ofproto-dpif - IP to ARP transformation slow-path])
+OVS_VSWITCHD_START
+
+# Add pairs of ports for input and output.
+ADD_OF_PORTS([br0], [1], [2], [3], [4], [11], [12], [13], [14])
+for i in 11 12 13 14; do
+    ovs-vsctl -- set Interface p$i type=dummy options:pcap=p$i.pcap
+done
+
+# Add flows that do various kind of "arp" action transformations
+# from port 1 to 11, 2 to 12, 3 to 13, 4 to 14.
+AT_DATA([flows.txt], [dnl
+in_port=1,ip actions=arp(11),arp(set_field:2->arp_op,11),11
+in_port=2,ip actions=mod_vlan_vid:123,arp(12),arp(set_field:2->arp_op,12),12
+in_port=3,ip actions=arp(mod_vlan_vid:123,13),arp(set_field:2->arp_op,13),13
+in_port=4,ip actions=arp(14),strip_vlan,arp(set_field:2->arp_op,14),14
+])
+AT_CHECK([ovs-ofctl add-flows br0 flows.txt])
+
+# Input two packets (one without a VLAN, one with VLAN 99) on each input port.
+for i in 1 2 3 4; do
+    ovs-appctl netdev-dummy/receive p$i "in_port($i),eth(src=80:88:88:88:88:88,dst=10:20:30:40:50:60),eth_type(0x0800),ipv4(src=192.168.0.1,dst=192.168.1.2,proto=17,tos=0,ttl=64,frag=no),udp(src=8,dst=10)"
+    ovs-appctl netdev-dummy/receive p$i "in_port($i),eth(src=80:88:88:88:88:88,dst=10:20:30:40:50:60),eth_type(0x8100),vlan(vid=99,pcp=7),encap(eth_type(0x0800),ipv4(src=192.168.0.1,dst=192.168.1.2,proto=17,tos=0,ttl=64,frag=no),udp(src=8,dst=10))"
+done
+AT_CHECK([ovs-appctl time/warp 5000], [0], [ignore])
+
+# Check the packets that were output.
+AT_CHECK([ovs-ofctl parse-pcap p11.pcap], [0], [dnl
+arp,in_port=ANY,vlan_tci=0x0000,dl_src=80:88:88:88:88:88,dl_dst=10:20:30:40:50:60,arp_spa=192.168.0.1,arp_tpa=192.168.1.2,arp_op=1,arp_sha=80:88:88:88:88:88,arp_tha=10:20:30:40:50:60
+arp,in_port=ANY,vlan_tci=0x0000,dl_src=80:88:88:88:88:88,dl_dst=10:20:30:40:50:60,arp_spa=192.168.0.1,arp_tpa=192.168.1.2,arp_op=2,arp_sha=80:88:88:88:88:88,arp_tha=10:20:30:40:50:60
+udp,in_port=ANY,vlan_tci=0x0000,dl_src=80:88:88:88:88:88,dl_dst=10:20:30:40:50:60,nw_src=192.168.0.1,nw_dst=192.168.1.2,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=8,tp_dst=10
+arp,in_port=ANY,dl_vlan=99,dl_vlan_pcp=7,dl_src=80:88:88:88:88:88,dl_dst=10:20:30:40:50:60,arp_spa=192.168.0.1,arp_tpa=192.168.1.2,arp_op=1,arp_sha=80:88:88:88:88:88,arp_tha=10:20:30:40:50:60
+arp,in_port=ANY,dl_vlan=99,dl_vlan_pcp=7,dl_src=80:88:88:88:88:88,dl_dst=10:20:30:40:50:60,arp_spa=192.168.0.1,arp_tpa=192.168.1.2,arp_op=2,arp_sha=80:88:88:88:88:88,arp_tha=10:20:30:40:50:60
+udp,in_port=ANY,dl_vlan=99,dl_vlan_pcp=7,dl_src=80:88:88:88:88:88,dl_dst=10:20:30:40:50:60,nw_src=192.168.0.1,nw_dst=192.168.1.2,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=8,tp_dst=10
+])
+
+AT_CHECK([ovs-ofctl parse-pcap p12.pcap], [0], [dnl
+arp,in_port=ANY,dl_vlan=123,dl_vlan_pcp=0,dl_src=80:88:88:88:88:88,dl_dst=10:20:30:40:50:60,arp_spa=192.168.0.1,arp_tpa=192.168.1.2,arp_op=1,arp_sha=80:88:88:88:88:88,arp_tha=10:20:30:40:50:60
+arp,in_port=ANY,dl_vlan=123,dl_vlan_pcp=0,dl_src=80:88:88:88:88:88,dl_dst=10:20:30:40:50:60,arp_spa=192.168.0.1,arp_tpa=192.168.1.2,arp_op=2,arp_sha=80:88:88:88:88:88,arp_tha=10:20:30:40:50:60
+udp,in_port=ANY,dl_vlan=123,dl_vlan_pcp=0,dl_src=80:88:88:88:88:88,dl_dst=10:20:30:40:50:60,nw_src=192.168.0.1,nw_dst=192.168.1.2,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=8,tp_dst=10
+arp,in_port=ANY,dl_vlan=123,dl_vlan_pcp=7,dl_src=80:88:88:88:88:88,dl_dst=10:20:30:40:50:60,arp_spa=192.168.0.1,arp_tpa=192.168.1.2,arp_op=1,arp_sha=80:88:88:88:88:88,arp_tha=10:20:30:40:50:60
+arp,in_port=ANY,dl_vlan=123,dl_vlan_pcp=7,dl_src=80:88:88:88:88:88,dl_dst=10:20:30:40:50:60,arp_spa=192.168.0.1,arp_tpa=192.168.1.2,arp_op=2,arp_sha=80:88:88:88:88:88,arp_tha=10:20:30:40:50:60
+udp,in_port=ANY,dl_vlan=123,dl_vlan_pcp=7,dl_src=80:88:88:88:88:88,dl_dst=10:20:30:40:50:60,nw_src=192.168.0.1,nw_dst=192.168.1.2,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=8,tp_dst=10
+])
+
+AT_CHECK([ovs-ofctl parse-pcap p13.pcap], [0], [dnl
+arp,in_port=ANY,dl_vlan=123,dl_vlan_pcp=0,dl_src=80:88:88:88:88:88,dl_dst=10:20:30:40:50:60,arp_spa=192.168.0.1,arp_tpa=192.168.1.2,arp_op=1,arp_sha=80:88:88:88:88:88,arp_tha=10:20:30:40:50:60
+arp,in_port=ANY,vlan_tci=0x0000,dl_src=80:88:88:88:88:88,dl_dst=10:20:30:40:50:60,arp_spa=192.168.0.1,arp_tpa=192.168.1.2,arp_op=2,arp_sha=80:88:88:88:88:88,arp_tha=10:20:30:40:50:60
+udp,in_port=ANY,vlan_tci=0x0000,dl_src=80:88:88:88:88:88,dl_dst=10:20:30:40:50:60,nw_src=192.168.0.1,nw_dst=192.168.1.2,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=8,tp_dst=10
+arp,in_port=ANY,dl_vlan=123,dl_vlan_pcp=7,dl_src=80:88:88:88:88:88,dl_dst=10:20:30:40:50:60,arp_spa=192.168.0.1,arp_tpa=192.168.1.2,arp_op=1,arp_sha=80:88:88:88:88:88,arp_tha=10:20:30:40:50:60
+arp,in_port=ANY,dl_vlan=99,dl_vlan_pcp=7,dl_src=80:88:88:88:88:88,dl_dst=10:20:30:40:50:60,arp_spa=192.168.0.1,arp_tpa=192.168.1.2,arp_op=2,arp_sha=80:88:88:88:88:88,arp_tha=10:20:30:40:50:60
+udp,in_port=ANY,dl_vlan=99,dl_vlan_pcp=7,dl_src=80:88:88:88:88:88,dl_dst=10:20:30:40:50:60,nw_src=192.168.0.1,nw_dst=192.168.1.2,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=8,tp_dst=10
+])
+
+AT_CHECK([ovs-ofctl parse-pcap p14.pcap], [0], [dnl
+arp,in_port=ANY,vlan_tci=0x0000,dl_src=80:88:88:88:88:88,dl_dst=10:20:30:40:50:60,arp_spa=192.168.0.1,arp_tpa=192.168.1.2,arp_op=1,arp_sha=80:88:88:88:88:88,arp_tha=10:20:30:40:50:60
+arp,in_port=ANY,vlan_tci=0x0000,dl_src=80:88:88:88:88:88,dl_dst=10:20:30:40:50:60,arp_spa=192.168.0.1,arp_tpa=192.168.1.2,arp_op=2,arp_sha=80:88:88:88:88:88,arp_tha=10:20:30:40:50:60
+udp,in_port=ANY,vlan_tci=0x0000,dl_src=80:88:88:88:88:88,dl_dst=10:20:30:40:50:60,nw_src=192.168.0.1,nw_dst=192.168.1.2,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=8,tp_dst=10
+arp,in_port=ANY,dl_vlan=99,dl_vlan_pcp=7,dl_src=80:88:88:88:88:88,dl_dst=10:20:30:40:50:60,arp_spa=192.168.0.1,arp_tpa=192.168.1.2,arp_op=1,arp_sha=80:88:88:88:88:88,arp_tha=10:20:30:40:50:60
+arp,in_port=ANY,vlan_tci=0x0000,dl_src=80:88:88:88:88:88,dl_dst=10:20:30:40:50:60,arp_spa=192.168.0.1,arp_tpa=192.168.1.2,arp_op=2,arp_sha=80:88:88:88:88:88,arp_tha=10:20:30:40:50:60
+udp,in_port=ANY,vlan_tci=0x0000,dl_src=80:88:88:88:88:88,dl_dst=10:20:30:40:50:60,nw_src=192.168.0.1,nw_dst=192.168.1.2,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=8,tp_dst=10
+])
+OVS_VSWITCHD_STOP
+AT_CLEANUP
+
 AT_SETUP([ofproto-dpif - VLAN handling])
 OVS_VSWITCHD_START(
   [set Bridge br0 fail-mode=standalone -- \
diff --git a/utilities/ovs-ofctl.8.in b/utilities/ovs-ofctl.8.in
index c37ced3..dc704b6 100644
--- a/utilities/ovs-ofctl.8.in
+++ b/utilities/ovs-ofctl.8.in
@@ -1740,6 +1740,32 @@  nf_conntrack module loaded.
 .
 .RE
 .
+.IP \fBarp(\fIaction\fR...\fB)\fR
+Transforms an IPv4 packet into an ARP packet, then executes each
+nested \fIaction\fR on the ARP packet.  Actions following the
+\fBarp\fR action apply to the original packet, not the ARP packet,
+discarding any changes that the nested actions made to packet data or
+metadata.
+.
+.IP
+The initial values of fields in the ARP packet are derived as follows:
+.RS
+.IP "\fBdl_src\fR and \fBdl_dst\fR"
+Unchanged from the IPv4 packet.
+.IP "\fBdl_type\fR"
+0x0806 (ARP)
+.IP "\fBnw_proto\fR (ARP opcode)"
+1 (ARP request)
+.IP "\fBarp_sha\fR"
+Copied from the Ethernet source address.
+.IP "\fBarp_spa\fR"
+Copied from the IP source address.
+.IP "\fBarp_tha\fR"
+Copied from the Ethernet destination address.
+.IP "\fBarp_tpa\fR"
+Copied from the IP destination address.
+.RE
+.
 .IP \fBdec_ttl\fR
 .IQ \fBdec_ttl(\fIid1\fR[\fB,\fIid2\fR]...\fB)\fR
 Decrement TTL of IPv4 packet or hop limit of IPv6 packet.  If the