With 'options:esp_udp_encap=true', an esp tunnel carries ESP in UDP as
described in RFC 3948, so that it can cross network address
translation.  Packets are sent from and to port 4500, or
'options:esp_udp_port', with a zero UDP checksum over IPv4 and a
computed one over IPv6.

Such a tunnel uses a datapath port of its own, 'esp_sys_4500', which
listens on the UDP port instead of IP protocol 50, as VXLAN and Geneve
tunnels do.  On reception, NAT keepalives and IKE packets, which start
with a zero non-ESP marker, are dropped and counted rather than
decrypted.  An IKE daemon on the tunnel's local address therefore
cannot use the same port yet.

The translation now takes the outer IP protocol of an esp tunnel from
the header it pushes, since it is either ESP or UDP.

Tests:
 - tunnel-push-pop: encapsulation, decapsulation of a packet encrypted
   by an independent implementation, IKE and keepalive packets.
 - system-userspace: ping and ping6 with the Linux kernel's ESP in UDP.

Assisted-by: claude-opus-5-5, Claude Code
Signed-off-by: Thilak Raj Surendra Babu <[email protected]>
---
 Documentation/faq/configuration.rst |  5 +-
 NEWS                                |  2 +
 lib/esp.h                           |  3 +
 lib/netdev-native-tnl.c             | 93 ++++++++++++++++++++++++++---
 lib/netdev-vport.c                  | 32 +++++++++-
 lib/odp-util.c                      | 22 +++++++
 lib/tnl-ports.c                     |  7 +++
 ofproto/ofproto-dpif-xlate.c        | 22 +++++--
 tests/odp.at                        |  1 +
 tests/system-traffic.at             | 24 +++++++-
 tests/tunnel-push-pop.at            | 91 ++++++++++++++++++++++++++++
 vswitchd/vswitch.xml                | 27 +++++++++
 12 files changed, 311 insertions(+), 18 deletions(-)

diff --git a/Documentation/faq/configuration.rst 
b/Documentation/faq/configuration.rst
index 0369877b4..313099f44 100644
--- a/Documentation/faq/configuration.rst
+++ b/Documentation/faq/configuration.rst
@@ -294,7 +294,10 @@ Q: Does the userspace datapath, for example with DPDK, 
support IPsec?
 
     * Keys are configured manually.  Key exchange (IKE) is not supported yet.
 
-    * Only tunnel mode, without UDP encapsulation for NAT traversal.
+    * Only tunnel mode.  ESP can be carried in UDP for NAT traversal
+      (``options:esp_udp_encap=true``), but the peer's UDP port is not
+      learned from received packets, and an IKE daemon on the same address
+      cannot use the same port.
 
     * Encapsulated packets are not fragmented, and received ESP packets that
       are fragments are not reassembled, so the MTU of the inner interfaces
diff --git a/NEWS b/NEWS
index a5fbd8a75..0fdb75fe9 100644
--- a/NEWS
+++ b/NEWS
@@ -12,6 +12,8 @@ Post-v4.0.0
        The keys are configured manually for now.
      * New 'esp/show' unixctl command to display the security associations
        of 'esp' tunnels and their statistics.
+     * 'esp' tunnels support UDP encapsulation for NAT traversal, and TCP
+       segmentation in software when userspace TSO is enabled.
 
 
 v4.0.0 - 17 Aug 2026
diff --git a/lib/esp.h b/lib/esp.h
index 5dfb362ab..8a6362b7a 100644
--- a/lib/esp.h
+++ b/lib/esp.h
@@ -55,6 +55,9 @@
 /* An AES-256 key followed by the 4-byte salt, as in RFC 4106. */
 #define ESP_MAX_KEY_LEN (32 + ESP_SALT_LEN)
 
+/* UDP port for ESP in UDP, for NAT traversal (RFC 3948). */
+#define ESP_UDP_ENCAP_PORT 4500
+
 #define ESP_DEFAULT_REPLAY_WINDOW 64
 #define ESP_MAX_REPLAY_WINDOW     4096
 
diff --git a/lib/netdev-native-tnl.c b/lib/netdev-native-tnl.c
index aacea1b4f..0579a069f 100644
--- a/lib/netdev-native-tnl.c
+++ b/lib/netdev-native-tnl.c
@@ -56,6 +56,8 @@ COVERAGE_DEFINE(native_tnl_l3csum_checked);
 COVERAGE_DEFINE(native_tnl_l3csum_err);
 COVERAGE_DEFINE(native_tnl_l4csum_checked);
 COVERAGE_DEFINE(native_tnl_l4csum_err);
+COVERAGE_DEFINE(native_tnl_esp_natt_keepalive);
+COVERAGE_DEFINE(native_tnl_esp_natt_non_esp);
 
 #define VXLAN_HLEN   (sizeof(struct udp_header) +         \
                       sizeof(struct vxlanhdr))
@@ -1121,10 +1123,24 @@ netdev_esp_build_header(const struct netdev *netdev,
     const struct netdev_tunnel_config *tnl_cfg;
     struct esp_header *esp;
 
+    tnl_cfg = netdev_get_tunnel_config(netdev);
+    if (tnl_cfg->dst_port) {
+        /* ESP in UDP, for NAT traversal (RFC 3948).  The source port is the
+         * same as the destination port, and the checksum is zero for IPv4
+         * and computed later for IPv6. */
+        struct udp_header *udp;
+
+        udp = netdev_tnl_ip_build_header(data, params, IPPROTO_UDP, 0);
+        udp->udp_src = tnl_cfg->dst_port;
+        udp->udp_dst = tnl_cfg->dst_port;
+        data->header_len += sizeof *udp;
+        esp = (struct esp_header *) (udp + 1);
+    } else {
+        esp = netdev_tnl_ip_build_header(data, params, IPPROTO_ESP, 0);
+    }
+
     /* netdev_esp_push_header() finds the outbound SA from this SPI and the
      * destination address, and fills in the rest of the ESP header. */
-    tnl_cfg = netdev_get_tunnel_config(netdev);
-    esp = netdev_tnl_ip_build_header(data, params, IPPROTO_ESP, 0);
     put_16aligned_be32(&esp->spi, tnl_cfg->esp_out_spi);
     put_16aligned_be32(&esp->seq_no, 0);
     memset(esp + 1, 0, ESP_IV_LEN);
@@ -1146,7 +1162,9 @@ netdev_esp_push_header(const struct netdev *netdev,
     size_t payload_len;
     struct esp_sa *sa;
     uint8_t next_hdr;
+    uint8_t nw_proto;
     int ip_tot_size;
+    void *l4;
 
     if (packet->packet_type == htonl(PT_IPV4)) {
         next_hdr = IPPROTO_IPIP;
@@ -1163,8 +1181,8 @@ netdev_esp_push_header(const struct netdev *netdev,
     dp_packet_set_size(packet, payload_len);
 
     dp_packet_put_uninit(packet, esp_trailer_len(payload_len));
-    esp = netdev_tnl_push_ip_header(packet, data->header, data->header_len,
-                                    &ip_tot_size, 0);
+    l4 = netdev_tnl_push_ip_header(packet, data->header, data->header_len,
+                                   &ip_tot_size, 0);
 
     /* 'netdev' is the datapath port shared by all esp tunnels, so find the
      * outbound SA from the SPI and destination in the header. */
@@ -1173,12 +1191,32 @@ netdev_esp_push_header(const struct netdev *netdev,
             = netdev_tnl_ipv6_hdr(CONST_CAST(void *, data->header));
 
         memcpy(&dst, &ip6->ip6_dst, sizeof dst);
+        nw_proto = ip6->ip6_nxt;
     } else {
         const struct ip_header *ip
             = netdev_tnl_ip_hdr(CONST_CAST(void *, data->header));
 
         in6_addr_set_mapped_ipv4(&dst, get_16aligned_be32(&ip->ip_dst));
+        nw_proto = ip->ip_proto;
+    }
+
+    if (nw_proto == IPPROTO_UDP) {
+        struct udp_header *udp = l4;
+
+        udp->udp_len = htons(ip_tot_size);
+        dp_packet_l4_proto_set_udp(packet);
+        if (netdev_tnl_is_header_ipv6(data->header)) {
+            /* A zero checksum is not allowed over IPv6.  It is computed
+             * when the packet is sent, after encryption. */
+            dp_packet_l4_checksum_set_partial(packet);
+        } else {
+            dp_packet_l4_checksum_set_good(packet);
+        }
+        esp = (struct esp_header *) (udp + 1);
+    } else {
+        esp = l4;
     }
+
     sa = esp_sad_lookup(get_16aligned_be32(&esp->spi), &dst);
     if (OVS_UNLIKELY(!sa)) {
         return ENOENT;
@@ -1196,15 +1234,56 @@ netdev_esp_pop_header(struct dp_packet *packet)
     struct esp_header *esp;
     unsigned int hlen;
     uint8_t next_hdr;
+    uint8_t nw_proto;
     ovs_be32 spi;
+    void *l4;
 
     pkt_metadata_init_tnl(md);
-    esp = ip_extract_tnl_md(packet, tnl, &hlen);
-    if (!esp) {
+    l4 = ip_extract_tnl_md(packet, tnl, &hlen);
+    if (!l4) {
         goto err;
     }
-
     esp_len = dp_packet_size(packet) - dp_packet_l2_pad_size(packet) - hlen;
+
+    if (netdev_tnl_is_header_ipv6(dp_packet_data(packet))) {
+        nw_proto = ((struct ovs_16aligned_ip6_hdr *) dp_packet_l3(packet))
+                   ->ip6_nxt;
+    } else {
+        nw_proto = ((struct ip_header *) dp_packet_l3(packet))->ip_proto;
+    }
+
+    if (nw_proto == IPPROTO_UDP) {
+        /* ESP in UDP (RFC 3948). */
+        struct udp_header *udp = l4;
+        const uint8_t *payload = (const uint8_t *) (udp + 1);
+
+        if (esp_len < sizeof *udp) {
+            goto err;
+        }
+        esp_len -= sizeof *udp;
+        hlen += sizeof *udp;
+        tnl->tp_src = udp->udp_src;
+        tnl->tp_dst = udp->udp_dst;
+
+        if (esp_len == 1 && payload[0] == 0xff) {
+            /* NAT keepalive (RFC 3948 section 2.3). */
+            COVERAGE_INC(native_tnl_esp_natt_keepalive);
+            goto err;
+        }
+        static const uint8_t non_esp_marker[4];
+
+        if (esp_len >= sizeof non_esp_marker
+            && !memcmp(payload, non_esp_marker, sizeof non_esp_marker)) {
+            /* Non-ESP marker: an IKE packet (RFC 3948 section 2.2), which
+             * is not for the datapath. */
+            COVERAGE_INC(native_tnl_esp_natt_non_esp);
+            goto err;
+        }
+        esp = (struct esp_header *) (udp + 1);
+    } else {
+        esp = l4;
+    }
+
     if (esp_open(esp, esp_len, &spi, &next_hdr, &payload_len)) {
         goto err;
     }
diff --git a/lib/netdev-vport.c b/lib/netdev-vport.c
index c5b03dab5..6407b07e4 100644
--- a/lib/netdev-vport.c
+++ b/lib/netdev-vport.c
@@ -118,7 +118,9 @@ netdev_vport_needs_dst_port(const struct netdev *dev)
 
     return (class->get_config == get_tunnel_config &&
             (!strcmp("geneve", type) || !strcmp("vxlan", type) ||
-             !strcmp("gtpu", type) || !strcmp("bareudp",type)));
+             !strcmp("gtpu", type) || !strcmp("bareudp",type) ||
+             (!strcmp("esp", type)
+              && netdev_get_tunnel_config(dev)->dst_port)));
 }
 
 const char *
@@ -226,6 +228,9 @@ netdev_vport_construct(struct netdev *netdev_)
         tnl_cfg->dst_port = port ? htons(port) : htons(GTPU_DST_PORT);
     } else if (!strcmp(type, "bareudp")) {
         tnl_cfg->dst_port = htons(port);
+    } else if (!strcmp(type, "esp")) {
+        /* ESP in UDP, for NAT traversal, if a port is specified. */
+        tnl_cfg->dst_port = htons(port);
     }
 
     tnl_cfg->dont_fragment = true;
@@ -637,7 +642,8 @@ is_esp_option(const char *key)
 {
     return (!strcmp(key, "esp_in_spi") || !strcmp(key, "esp_out_spi")
             || !strcmp(key, "esp_in_key") || !strcmp(key, "esp_out_key")
-            || !strcmp(key, "esp_esn") || !strcmp(key, "esp_replay_window"));
+            || !strcmp(key, "esp_esn") || !strcmp(key, "esp_replay_window")
+            || !strcmp(key, "esp_udp_encap") || !strcmp(key, "esp_udp_port"));
 }
 
 /* Parses the ESP options in 'args' for ESP tunnel 'dev', checks them against
@@ -710,6 +716,18 @@ esp_tunnel_config(struct netdev_vport *dev, const struct 
smap *args,
         return EINVAL;
     }
 
+    if (smap_get_bool(args, "esp_udp_encap", false)) {
+        unsigned int port = smap_get_uint(args, "esp_udp_port",
+                                          ESP_UDP_ENCAP_PORT);
+
+        if (!port || port > UINT16_MAX) {
+            ds_put_format(errors, "%s: 'esp_udp_port' must be between 1 "
+                          "and 65535\n", name);
+            return EINVAL;
+        }
+        tnl_cfg->dst_port = htons(port);
+    }
+
     in.esn = out.esn = smap_get_bool(args, "esp_esn", false);
     replay_window = smap_get_uint(args, "esp_replay_window",
                                   ESP_DEFAULT_REPLAY_WINDOW);
@@ -921,7 +939,8 @@ set_tunnel_config(struct netdev *dev_, const struct smap 
*args, char **errp)
         tnl_cfg.dst_port = htons(GTPU_DST_PORT);
     }
 
-    needs_dst_port = netdev_vport_needs_dst_port(dev_);
+    /* For esp tunnels, the UDP port is set with 'esp_udp_port'. */
+    needs_dst_port = netdev_vport_needs_dst_port(dev_) && !is_esp;
     tnl_cfg.dont_fragment = true;
 
     SMAP_FOR_EACH (node, args) {
@@ -1294,6 +1313,13 @@ get_tunnel_config(const struct netdev *dev, struct smap 
*args)
             smap_add_format(args, "esp_replay_window", "%"PRIu16,
                             tnl_cfg->esp_replay_window);
         }
+        if (tnl_cfg->dst_port) {
+            smap_add(args, "esp_udp_encap", "true");
+            if (ntohs(tnl_cfg->dst_port) != ESP_UDP_ENCAP_PORT) {
+                smap_add_format(args, "esp_udp_port", "%"PRIu16,
+                                ntohs(tnl_cfg->dst_port));
+            }
+        }
     } else if (tnl_cfg->in_key_flow && tnl_cfg->out_key_flow) {
         smap_add(args, "key", "flow");
     } else if (tnl_cfg->in_key_present && tnl_cfg->out_key_present
diff --git a/lib/odp-util.c b/lib/odp-util.c
index 6a7ffeb02..8d4b61516 100644
--- a/lib/odp-util.c
+++ b/lib/odp-util.c
@@ -906,6 +906,19 @@ format_odp_tnl_push_header(struct ds *ds, struct 
ovs_action_push_tnl *data)
                       ntohl(get_16aligned_be32(&gtph->teid)));
     } else if (data->tnl_type == OVS_VPORT_TYPE_ESP) {
         const struct esp_header *esp = l4;
+        uint8_t nw_proto = (eth->eth_type == htons(ETH_TYPE_IP)
+                            ? ((const struct ip_header *) l3)->ip_proto
+                            : ((const struct ovs_16aligned_ip6_hdr *) l3)
+                              ->ip6_nxt);
+
+        if (nw_proto == IPPROTO_UDP) {
+            /* ESP in UDP, for NAT traversal. */
+            esp = format_udp_tnl_push_header(ds, udp, bytes_left);
+            if (!esp) {
+                return;
+            }
+            bytes_left -= sizeof *udp;
+        }
 
         if (bytes_left < sizeof *esp) {
             ds_put_cstr(ds, "esp(truncated header))");
@@ -1855,6 +1868,15 @@ ovs_parse_tnl_push(const char *s, struct 
ovs_action_push_tnl *data)
             gnh->proto_type = htons(ETH_TYPE_TEB);
             put_16aligned_be32(&gnh->vni, htonl(vni << 8));
             tnl_type = OVS_VPORT_TYPE_GENEVE;
+        } else if (ovs_scan_len(s, &n, "esp(spi=0x%"SCNx32"))", &spi)) {
+            /* ESP in UDP, for NAT traversal. */
+            struct esp_header *esp = (struct esp_header *) (udp + 1);
+
+            put_16aligned_be32(&esp->spi, htonl(spi));
+            put_16aligned_be32(&esp->seq_no, 0);
+            memset(esp + 1, 0, ESP_IV_LEN);
+            tnl_type = OVS_VPORT_TYPE_ESP;
+            header_len = sizeof *eth + ip_len + sizeof *udp + ESP_PREFIX_LEN;
         } else {
             return -EINVAL;
         }
diff --git a/lib/tnl-ports.c b/lib/tnl-ports.c
index 3e7acd018..806cfd031 100644
--- a/lib/tnl-ports.c
+++ b/lib/tnl-ports.c
@@ -176,7 +176,9 @@ tnl_type_to_nw_proto(const char type[], uint8_t 
nw_protos[2])
         nw_protos[0] = IPPROTO_IPIP;
         nw_protos[1] = IPPROTO_IPV6;
     } else if (!strcmp(type, "esp")) {
+        /* ESP, or ESP in UDP for NAT traversal. */
         nw_protos[0] = IPPROTO_ESP;
+        nw_protos[1] = IPPROTO_UDP;
     }
 }
 
@@ -219,6 +221,11 @@ tnl_port_map_insert(odp_port_t port, ovs_be16 tp_port,
     int i;
 
     tnl_type_to_nw_proto(type, nw_protos);
+    if (!strcmp(type, "esp")) {
+        /* An esp tunnel port uses UDP only if it has a UDP port. */
+        nw_protos[0] = tp_port ? IPPROTO_UDP : IPPROTO_ESP;
+        nw_protos[1] = 0;
+    }
 
     for (i = 0; i < 2; i++) {
         if (nw_protos[i]) {
diff --git a/ofproto/ofproto-dpif-xlate.c b/ofproto/ofproto-dpif-xlate.c
index cf9f8467d..d09332aee 100644
--- a/ofproto/ofproto-dpif-xlate.c
+++ b/ofproto/ofproto-dpif-xlate.c
@@ -3859,8 +3859,9 @@ static void
 propagate_tunnel_data_to_flow(struct xlate_ctx *ctx, struct eth_addr dmac,
                               struct eth_addr smac,   struct in6_addr s_ip6,
                               ovs_be32 s_ip, bool is_tnl_ipv6,
-                              enum ovs_vport_type tnl_type)
+                              const struct ovs_action_push_tnl *tnl_push_data)
 {
+    enum ovs_vport_type tnl_type = tnl_push_data->tnl_type;
     struct flow *base_flow, *flow;
     flow = &ctx->xin->flow;
     base_flow = &ctx->base_flow;
@@ -3883,9 +3884,21 @@ propagate_tunnel_data_to_flow(struct xlate_ctx *ctx, 
struct eth_addr dmac,
         nw_proto = (flow->dl_type == htons(ETH_TYPE_IP))
                    ? IPPROTO_IPIP : IPPROTO_IPV6;
         break;
-    case OVS_VPORT_TYPE_ESP:
-        nw_proto = IPPROTO_ESP;
+    case OVS_VPORT_TYPE_ESP: {
+        /* ESP, or ESP in UDP for NAT traversal, as in the pushed header. */
+        const struct eth_header *eth = (const void *) tnl_push_data->header;
+
+        if (is_tnl_ipv6) {
+            const struct ovs_16aligned_ip6_hdr *ip6 = (const void *) (eth + 1);
+
+            nw_proto = ip6->ip6_nxt;
+        } else {
+            const struct ip_header *ip = (const void *) (eth + 1);
+
+            nw_proto = ip->ip_proto;
+        }
         break;
+    }
     case OVS_VPORT_TYPE_UNSPEC:
     case OVS_VPORT_TYPE_NETDEV:
     case OVS_VPORT_TYPE_INTERNAL:
@@ -4035,8 +4048,7 @@ native_tunnel_output(struct xlate_ctx *ctx, const struct 
xport *xport,
      * any more when sending packet to tunnel. */
 
     propagate_tunnel_data_to_flow(ctx, dmac, smac, s_ip6,
-                                  s_ip, tnl_params.is_ipv6,
-                                  tnl_push_data.tnl_type);
+                                  s_ip, tnl_params.is_ipv6, &tnl_push_data);
 
     size_t offset;
     size_t push_action_size;
diff --git a/tests/odp.at b/tests/odp.at
index a5ce6c9f2..7364ff489 100644
--- a/tests/odp.at
+++ b/tests/odp.at
@@ -346,6 +346,7 @@ 
tnl_push(tnl_port(6),header(size=78,type=112,eth(dst=f8:bc:12:44:34:b6,src=f8:bc
 
tnl_push(tnl_port(6),header(size=110,type=112,eth(dst=f8:bc:12:44:34:b6,src=f8:bc:12:46:58:e0,dl_type=0x86dd),ipv6(src=2001:cafe::88,dst=2001:cafe::92,label=0,proto=43,tclass=0x0,hlimit=64),srv6(segments_left=2,segs(2001:cafe::90,2001:cafe::91,2001:cafe::92))),out_port(1))
 
tnl_push(tnl_port(7),header(size=50,type=113,eth(dst=f8:bc:12:44:34:b6,src=f8:bc:12:46:58:e0,dl_type=0x0800),ipv4(src=1.1.2.88,dst=1.1.2.92,proto=50,tos=0,ttl=64,frag=0x4000),esp(spi=0x1000)),out_port(1))
 
tnl_push(tnl_port(7),header(size=70,type=113,eth(dst=f8:bc:12:44:34:b6,src=f8:bc:12:46:58:e0,dl_type=0x86dd),ipv6(src=2001:cafe::88,dst=2001:cafe::92,label=0,proto=50,tclass=0x0,hlimit=64),esp(spi=0xfffffffe)),out_port(1))
+tnl_push(tnl_port(8),header(size=58,type=113,eth(dst=f8:bc:12:44:34:b6,src=f8:bc:12:46:58:e0,dl_type=0x0800),ipv4(src=1.1.2.88,dst=1.1.2.92,proto=17,tos=0,ttl=64,frag=0x4000),udp(src=4500,dst=4500,csum=0x0),esp(spi=0x1000)),out_port(1))
 ct
 ct(commit)
 ct(commit,zone=5)
diff --git a/tests/system-traffic.at b/tests/system-traffic.at
index 2dea939d0..b8d629754 100644
--- a/tests/system-traffic.at
+++ b/tests/system-traffic.at
@@ -1703,10 +1703,11 @@ NS_CHECK_EXEC([at_ns1], [ping -q -c 3 -i 0.3 -W 2 
10.100.100.100 | FORMAT_PING],
 OVS_TRAFFIC_VSWITCHD_STOP
 AT_CLEANUP
 
-# ESP_TUNNEL_TEST([esp-options], [xfrm-state-options])
+# ESP_TUNNEL_TEST([esp-options], [xfrm-state-options], [ns-setup])
 #
 # Checks IPv4 and IPv6 traffic through an ESP tunnel between OVS and the
-# kernel's IPsec implementation in the namespace 'at_ns0'.
+# kernel's IPsec implementation in the namespace 'at_ns0'.  'ns-setup' runs
+# after the kernel's security associations and policies are configured.
 m4_define([ESP_TUNNEL_TEST], [
 OVS_CHECK_ESP()
 OVS_TRAFFIC_VSWITCHD_START()
@@ -1757,6 +1758,7 @@ for inner in "10.1.1.1/32 10.1.1.100/32" "fd00::1/128 
fd00::100/128"; do
     NS_CHECK_EXEC([at_ns0], [ip xfrm policy add src $[]2 dst $[]1 dir in dnl
                     tmpl src 172.31.1.100 dst 172.31.1.1 proto esp mode 
tunnel])
 done
+$3
 
 dnl First, check the underlay.
 NS_CHECK_EXEC([at_ns0], [ping -q -c 3 -i 0.3 -W 2 172.31.1.100 | FORMAT_PING], 
[0], [dnl
@@ -1791,6 +1793,24 @@ AT_SETUP([datapath - ping over esp tunnel with extended 
sequence numbers])
 ESP_TUNNEL_TEST([options:esp_esn=true], [flag esn replay-window 64])
 AT_CLEANUP
 
+AT_SETUP([datapath - ping over esp tunnel in udp])
+dnl The kernel only decapsulates ESP in UDP received on a socket that has the
+dnl UDP_ENCAP_ESPINUDP option, which an IKE daemon would usually open.
+AT_DATA([espinudp.py], [import socket
+import time
+
+s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+s.bind(("0.0.0.0", 4500))
+s.setsockopt(socket.IPPROTO_UDP, 100, 2)  # UDP_ENCAP, UDP_ENCAP_ESPINUDP
+time.sleep(3600)
+])
+ESP_TUNNEL_TEST([options:esp_udp_encap=true],
+                [encap espinudp 4500 4500 0.0.0.0 replay-window 64],
+                [NETNS_DAEMONIZE([at_ns0], [python3 espinudp.py],
+                                 [espinudp.pid])
+                 OVS_WAIT_UNTIL([ip netns exec at_ns0 ss -uln | grep -q 
:4500])])
+AT_CLEANUP
+
 AT_SETUP([datapath - ping6 over srv6 tunnel])
 OVS_CHECK_SRV6()
 
diff --git a/tests/tunnel-push-pop.at b/tests/tunnel-push-pop.at
index aa887340b..a2cb54b4f 100644
--- a/tests/tunnel-push-pop.at
+++ b/tests/tunnel-push-pop.at
@@ -1783,3 +1783,94 @@ AT_CHECK([ovs-appctl coverage/read-counter 
netdev_push_header_drops], [0], [0
 
 OVS_VSWITCHD_STOP
 AT_CLEANUP
+
+AT_SETUP([tunnel_push_pop - esp in udp])
+AT_KEYWORDS([ipsec])
+AT_SKIP_IF([test "$HAVE_OPENSSL" = no])
+
+OVS_VSWITCHD_START([add-port br0 p0 -- set Interface p0 type=dummy dnl
+                    ofport_request=1 other-config:hwaddr=aa:55:aa:55:00:00 dnl
+                    options:tx_pcap=p0.pcap])
+AT_CHECK([ovs-vsctl add-br int-br -- set bridge int-br datapath_type=dummy])
+AT_CHECK([ovs-vsctl add-port int-br t1 -- set Interface t1 type=esp dnl
+            ofport_request=2 options:remote_ip=1.1.2.92 dnl
+            options:esp_udp_encap=true dnl
+            options:esp_in_spi=4096 options:esp_out_spi=8192 dnl
+            options:esp_in_key=0x101112131415161718191a1b1c1d1e1fcafebabe dnl
+            options:esp_out_key=0x202122232425262728292a2b2c2d2e2fdeadbeef dnl
+            -- add-port int-br p1 -- set Interface p1 type=dummy dnl
+            ofport_request=3 options:tx_pcap=p1.pcap])
+
+AT_CHECK([ovs-appctl dpif/show | grep t1 | sed 's|2/[[0-9]]*|2/X|'], [0], [dnl
+    t1 2/X: (esp: esp_in_spi=4096, esp_out_spi=8192, esp_udp_encap=true, 
remote_ip=1.1.2.92)
+])
+
+AT_CHECK([ovs-appctl netdev-dummy/ip4addr br0 1.1.2.88/24], [0], [OK
+])
+AT_CHECK([ovs-appctl tnl/neigh/set br0 1.1.2.92 aa:55:aa:55:00:01], [0], [OK
+])
+AT_CHECK([ovs-ofctl add-flow br0 action=normal])
+AT_CHECK([ovs-ofctl add-flow int-br in_port=t1,action=p1])
+AT_CHECK([ovs-ofctl add-flow int-br in_port=p1,action=t1])
+
+dnl The tunnel listens on UDP port 4500 instead of IP protocol 50.
+AT_CHECK([ovs-appctl tnl/ports/show -v | grep esp_sys | sed 
's/([[0-9]]*)/(X)/'],
+         [0], [dnl
+esp_sys_4500 (X) : 
eth(dst=aa:55:aa:55:00:00),eth_type(0x0800),ipv4(dst=1.1.2.88,proto=17,frag=no),udp(dst=4500)
+])
+
+dnl Encapsulation.
+AT_CHECK([ovs-appctl ofproto/trace --names int-br dnl
+            in_port=p1,dl_src=f8:bc:12:44:34:b6,dl_dst=aa:55:aa:55:00:00,dnl
+udp,nw_src=10.0.0.1,nw_dst=10.0.0.2,nw_ttl=64,udp_src=100,udp_dst=200],
+         [0], [stdout])
+AT_CHECK([tail -1 stdout], [0], [dnl
+Datapath actions: 
pop_eth,tnl_push(tnl_port(esp_sys_4500),header(size=58,type=113,eth(dst=aa:55:aa:55:00:01,src=aa:55:aa:55:00:00,dl_type=0x0800),ipv4(src=1.1.2.88,dst=1.1.2.92,proto=17,tos=0,ttl=64,frag=0x4000),udp(src=4500,dst=4500,csum=0x0),esp(spi=0x2000)),out_port(br0)),p0
+])
+
+AT_CHECK([ovs-appctl netdev-dummy/receive p1 dnl
+  
'eth(src=f8:bc:12:44:34:b6,dst=aa:55:aa:55:00:00),eth_type(0x0800),ipv4(src=10.0.0.1,dst=10.0.0.2,proto=17,tos=0,ttl=64,frag=no),udp(src=100,dst=200)'])
+OVS_WAIT_UNTIL([test $(ovs-pcap p0.pcap | wc -l) -ge 1])
+
+dnl Outer IPv4 and UDP headers, then the SPI and sequence number 1.  The UDP
+dnl length is 8 + 16 + 92 + 2 (padding) + 2 + 16 = 136, and its checksum is 0.
+AT_CHECK([ovs-pcap p0.pcap | cut -c1-100], [0], [dnl
+aa55aa550001aa55aa55000008004500009c000040004011339c010102580101025c11941194008800000000200000000001
+])
+
+dnl Decapsulation of a packet encrypted by an independent implementation.
+m4_define([ESP_IN_UDP], 
[aa55aa550000aa55aa55000108004500006800004000401133d00101025c01010258119411940054000000001000000000010102030405060708319779ecb7385859a48de241704608a2b76f467926ada1846103e4f0c011b294f83a80b9ba98e1cfbfd7b9421e33c5e1a7d8e84de7b0d2a39cfc4887])
+m4_define([IKE], 
[aa55aa550000aa55aa55000108004500003c00004000401133fc0101025c01010258119411940028000000000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b])
+m4_define([KEEPALIVE], 
[aa55aa550000aa55aa55000108004500001d000040004011341b0101025c010102581194119400090000ff])
+
+AT_CHECK([ovs-appctl netdev-dummy/receive p0 ESP_IN_UDP])
+OVS_WAIT_UNTIL([test $(ovs-pcap p1.pcap | wc -l) -ge 1])
+AT_CHECK([ovs-pcap p1.pcap], [0], [dnl
+00000000000000000000000008004500002800004000401126c30a0000020a00000100c80064001400006f76732d6573702d74657374
+])
+
+dnl IKE packets, which start with a zero non-ESP marker, and NAT keepalives
+dnl are not decrypted.
+AT_CHECK([ovs-appctl netdev-dummy/receive p0 IKE])
+AT_CHECK([ovs-appctl netdev-dummy/receive p0 KEEPALIVE])
+OVS_WAIT_UNTIL([test $(ovs-appctl coverage/read-counter 
native_tnl_esp_natt_keepalive) = 1])
+AT_CHECK([ovs-appctl coverage/read-counter native_tnl_esp_natt_non_esp], [0], 
[1
+])
+AT_CHECK([ovs-pcap p1.pcap | wc -l], [0], [1
+])
+AT_CHECK([ovs-appctl esp/show t1 | sed -n 4,6p], [0], [dnl
+  inbound : spi 0x00001000, aes128-gcm16, esn off, replay window 64
+    packets 1, bytes 40, highest seq 1
+    replayed 0, auth failed 0, malformed 0
+])
+
+dnl Without 'esp_udp_encap', the tunnel uses IP protocol 50 again.
+AT_CHECK([ovs-vsctl remove Interface t1 options esp_udp_encap])
+AT_CHECK([ovs-appctl tnl/ports/show | grep esp_sys | sed 's/([[0-9]]*)/(X)/'],
+         [0], [dnl
+esp_sys (X) ref_cnt=1
+])
+
+dnl A datapath flow from the old port may be revalidated meanwhile.
+OVS_VSWITCHD_STOP(["/receive tunnel port not found/d"])
+AT_CLEANUP
diff --git a/vswitchd/vswitch.xml b/vswitchd/vswitch.xml
index 9a4bd662d..5dd30d690 100644
--- a/vswitchd/vswitch.xml
+++ b/vswitchd/vswitch.xml
@@ -3452,6 +3452,33 @@
           to 64.
         </p>
       </column>
+
+      <column name="options" key="esp_udp_encap" type='{"type": "boolean"}'>
+        <p>
+          Optional.  If <code>true</code>, ESP packets are carried in UDP, as
+          described in RFC 3948, so that the tunnel can cross network address
+          translation (NAT).  Packets are sent from and to <ref
+          column="options" key="esp_udp_port"/>, with a zero UDP checksum over
+          IPv4, and the tunnel receives ESP in UDP on that port instead of
+          plain ESP.  The peer must be configured the same way, e.g. with
+          <code>encap espinudp 4500 4500 0.0.0.0</code> for Linux.  Defaults to
+          <code>false</code>.
+        </p>
+        <p>
+          The datapath then receives every UDP packet to that port and to the
+          tunnel's local address.  It drops NAT keepalives and IKE packets,
+          which start with a zero non-ESP marker, so an IKE daemon on the same
+          address cannot use that port.
+        </p>
+      </column>
+
+      <column name="options" key="esp_udp_port"
+              type='{"type": "integer", "minInteger": 1, "maxInteger": 65535}'>
+        <p>
+          Optional.  The UDP port for <ref column="options"
+          key="esp_udp_encap"/>.  Defaults to 4500.
+        </p>
+      </column>
     </group>
 
     <group title="Patch Options">
-- 
2.43.0

_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to