diff --git a/Documentation/faq/configuration.rst b/Documentation/faq/configuration.rst
index dc6c92446..6a53d369c 100644
--- a/Documentation/faq/configuration.rst
+++ b/Documentation/faq/configuration.rst
@@ -238,6 +238,27 @@ Q: Does Open vSwitch support GTP-U?
                 set int gtpu0 type=gtpu options:key=<teid> \
                 options:remote_ip=172.31.1.1
 
+Q: Does Open vSwitch support SRv6?
+
+    A: Yes. Starting with version 3.1, the Open vSwitch userspace
+    datapath supports SRv6 (Segment Routing over IPv6). The following
+    example shows tunneling to fc00:300::1 via fc00:100::1 and fc00:200::1.
+    In the current implementation, if "IPv6 in IPv6" or "IPv4 in IPv6" packets
+    are routed to this interface, and these packets are not SRv6 packets, they
+    may be dropped, so be careful in workloads with a mix of these tunnels.
+    Also note the following restrictions:
+
+    * Segment list length is limited to 6.
+    * SRv6 packets with other than segments_left = 0 are simply dropped.
+
+    ::
+
+        $ ovs-vsctl add-br br0
+        $ ovs-vsctl add-port br0 srv6_0 -- \
+                set int srv6_0 type=srv6  \
+                options:remote_ip=fc00:300::1 \
+                options:srv6_segs="fc00:100::1,fc00:200::1,fc00:300::1"
+
 Q: How do I connect two bridges?
 
     A: First, why do you want to do this?  Two connected bridges are not much
diff --git a/Documentation/faq/releases.rst b/Documentation/faq/releases.rst
index 9e1b42262..6b4f55d0e 100644
--- a/Documentation/faq/releases.rst
+++ b/Documentation/faq/releases.rst
@@ -151,6 +151,7 @@ Q: Are all features available with all datapaths?
     Tunnel - ERSPAN                 4.18           2.10         2.10     NO
     Tunnel - ERSPAN-IPv6            4.18           2.10         2.10     NO
     Tunnel - GTP-U                  NO             NO           2.14     NO
+    Tunnel - SRv6                   NO             NO           3.1      NO
     Tunnel - Bareudp                5.7            NO           NO       NO
     QoS - Policing                  YES            1.1          2.6      NO
     QoS - Shaping                   YES            1.1          NO       NO
diff --git a/NEWS b/NEWS
index 83c126b00..91d9ee9f1 100644
--- a/NEWS
+++ b/NEWS
@@ -46,6 +46,8 @@ v3.1.0 - xx xxx xxxx
      * Add new experimental PMD load based sleeping feature. PMD threads can
        request to sleep up to a user configured 'pmd-maxsleep' value under
        low load conditions.
+   - SRv6 Tunnel Protocol
+     * Only support for userspace datapath.
 
 
 v3.0.0 - 15 Aug 2022
diff --git a/include/linux/openvswitch.h b/include/linux/openvswitch.h
index 8bb5abdc8..987b5cbf7 100644
--- a/include/linux/openvswitch.h
+++ b/include/linux/openvswitch.h
@@ -254,6 +254,7 @@ enum ovs_vport_type {
 	OVS_VPORT_TYPE_IP6GRE = 109,
 	OVS_VPORT_TYPE_GTPU = 110,
 	OVS_VPORT_TYPE_BAREUDP = 111,  /* Bareudp tunnel. */
+	OVS_VPORT_TYPE_SRV6 = 112,  /* SRv6 tunnel. */
 	__OVS_VPORT_TYPE_MAX
 };
 
diff --git a/lib/conntrack.c b/lib/conntrack.c
index 550b2be9b..88d61aad2 100644
--- a/lib/conntrack.c
+++ b/lib/conntrack.c
@@ -1692,7 +1692,9 @@ extract_l3_ipv6(struct conn_key *key, const void *data, size_t size,
     uint8_t nw_frag = 0;
 
     const struct ovs_16aligned_ip6_frag *frag_hdr;
-    if (!parse_ipv6_ext_hdrs(&data, &size, &nw_proto, &nw_frag, &frag_hdr)) {
+    const struct ip6_rt_hdr *rt_hdr;
+    if (!parse_ipv6_ext_hdrs(&data, &size, &nw_proto, &nw_frag, &frag_hdr,
+                             &rt_hdr)) {
         return false;
     }
 
diff --git a/lib/dpif-netlink-rtnl.c b/lib/dpif-netlink-rtnl.c
index 4fc42daed..5788294ae 100644
--- a/lib/dpif-netlink-rtnl.c
+++ b/lib/dpif-netlink-rtnl.c
@@ -129,6 +129,8 @@ vport_type_to_kind(enum ovs_vport_type type,
         }
     case OVS_VPORT_TYPE_GTPU:
         return NULL;
+    case OVS_VPORT_TYPE_SRV6:
+        return "srv6";
     case OVS_VPORT_TYPE_BAREUDP:
         return "bareudp";
     case OVS_VPORT_TYPE_NETDEV:
@@ -319,6 +321,7 @@ dpif_netlink_rtnl_verify(const struct netdev_tunnel_config *tnl_cfg,
     case OVS_VPORT_TYPE_LISP:
     case OVS_VPORT_TYPE_STT:
     case OVS_VPORT_TYPE_GTPU:
+    case OVS_VPORT_TYPE_SRV6:
     case OVS_VPORT_TYPE_UNSPEC:
     case __OVS_VPORT_TYPE_MAX:
     default:
@@ -411,6 +414,7 @@ dpif_netlink_rtnl_create(const struct netdev_tunnel_config *tnl_cfg,
     case OVS_VPORT_TYPE_LISP:
     case OVS_VPORT_TYPE_STT:
     case OVS_VPORT_TYPE_GTPU:
+    case OVS_VPORT_TYPE_SRV6:
     case OVS_VPORT_TYPE_UNSPEC:
     case __OVS_VPORT_TYPE_MAX:
     default:
@@ -519,6 +523,7 @@ dpif_netlink_rtnl_port_destroy(const char *name, const char *type)
     case OVS_VPORT_TYPE_ERSPAN:
     case OVS_VPORT_TYPE_IP6ERSPAN:
     case OVS_VPORT_TYPE_IP6GRE:
+    case OVS_VPORT_TYPE_SRV6:
     case OVS_VPORT_TYPE_BAREUDP:
         return dpif_netlink_rtnl_destroy(name);
     case OVS_VPORT_TYPE_NETDEV:
diff --git a/lib/dpif-netlink.c b/lib/dpif-netlink.c
index 026b0daa8..f90a675e0 100644
--- a/lib/dpif-netlink.c
+++ b/lib/dpif-netlink.c
@@ -919,6 +919,9 @@ get_vport_type(const struct dpif_netlink_vport *vport)
     case OVS_VPORT_TYPE_GTPU:
         return "gtpu";
 
+    case OVS_VPORT_TYPE_SRV6:
+        return "srv6";
+
     case OVS_VPORT_TYPE_BAREUDP:
         return "bareudp";
 
@@ -957,6 +960,8 @@ netdev_to_ovs_vport_type(const char *type)
         return OVS_VPORT_TYPE_GRE;
     } else if (!strcmp(type, "gtpu")) {
         return OVS_VPORT_TYPE_GTPU;
+    } else if (!strcmp(type, "srv6")) {
+        return OVS_VPORT_TYPE_SRV6;
     } else if (!strcmp(type, "bareudp")) {
         return OVS_VPORT_TYPE_BAREUDP;
     } else {
diff --git a/lib/flow.c b/lib/flow.c
index c3a3aa3ce..b26334385 100644
--- a/lib/flow.c
+++ b/lib/flow.c
@@ -479,9 +479,11 @@ invalid:
 static inline bool
 parse_ipv6_ext_hdrs__(const void **datap, size_t *sizep, uint8_t *nw_proto,
                       uint8_t *nw_frag,
-                      const struct ovs_16aligned_ip6_frag **frag_hdr)
+                      const struct ovs_16aligned_ip6_frag **frag_hdr,
+                      const struct ip6_rt_hdr **rt_hdr)
 {
     *frag_hdr = NULL;
+    *rt_hdr = NULL;
     while (1) {
         if (OVS_LIKELY((*nw_proto != IPPROTO_HOPOPTS)
                        && (*nw_proto != IPPROTO_ROUTING)
@@ -504,7 +506,6 @@ parse_ipv6_ext_hdrs__(const void **datap, size_t *sizep, uint8_t *nw_proto,
         }
 
         if ((*nw_proto == IPPROTO_HOPOPTS)
-            || (*nw_proto == IPPROTO_ROUTING)
             || (*nw_proto == IPPROTO_DSTOPTS)) {
             /* These headers, while different, have the fields we care
              * about in the same location and with the same
@@ -515,6 +516,13 @@ parse_ipv6_ext_hdrs__(const void **datap, size_t *sizep, uint8_t *nw_proto,
                                             (ext_hdr->ip6e_len + 1) * 8))) {
                 return false;
             }
+        } else if (*nw_proto == IPPROTO_ROUTING) {
+            *rt_hdr = *datap;
+            *nw_proto = (*rt_hdr)->nexthdr;
+            if (OVS_UNLIKELY(!data_try_pull(datap, sizep,
+                                            ((*rt_hdr)->hdrlen + 1) * 8))) {
+                return false;
+            }
         } else if (*nw_proto == IPPROTO_AH) {
             /* A standard AH definition isn't available, but the fields
              * we care about are in the same location as the generic
@@ -561,15 +569,19 @@ parse_ipv6_ext_hdrs__(const void **datap, size_t *sizep, uint8_t *nw_proto,
  * has FLOW_NW_FRAG_LATER set.  Both first and later fragments have
  * FLOW_NW_FRAG_ANY set in 'nw_frag'.
  *
+ * If a routing header is found, '*rt_hdr' is set to the routing
+ * header and otherwise set to NULL.
+ *
  * A return value of false indicates that there was a problem parsing
  * the extension headers.*/
 bool
 parse_ipv6_ext_hdrs(const void **datap, size_t *sizep, uint8_t *nw_proto,
                     uint8_t *nw_frag,
-                    const struct ovs_16aligned_ip6_frag **frag_hdr)
+                    const struct ovs_16aligned_ip6_frag **frag_hdr,
+                    const struct ip6_rt_hdr **rt_hdr)
 {
     return parse_ipv6_ext_hdrs__(datap, sizep, nw_proto, nw_frag,
-                                 frag_hdr);
+                                 frag_hdr, rt_hdr);
 }
 
 bool
@@ -946,8 +958,9 @@ miniflow_extract(struct dp_packet *packet, struct miniflow *dst)
         nw_proto = nh->ip6_nxt;
 
         const struct ovs_16aligned_ip6_frag *frag_hdr;
+        const struct ip6_rt_hdr *rt_hdr;
         if (!parse_ipv6_ext_hdrs__(&data, &size, &nw_proto, &nw_frag,
-                                   &frag_hdr)) {
+                                   &frag_hdr, &rt_hdr)) {
             goto out;
         }
 
@@ -1201,9 +1214,10 @@ parse_tcp_flags(struct dp_packet *packet,
         dp_packet_set_l2_pad_size(packet, size - plen);
         size = plen;
         const struct ovs_16aligned_ip6_frag *frag_hdr;
+        const struct ip6_rt_hdr *rt_hdr;
         nw_proto = nh->ip6_nxt;
         if (!parse_ipv6_ext_hdrs__(&data, &size, &nw_proto, &nw_frag,
-            &frag_hdr)) {
+            &frag_hdr, &rt_hdr)) {
             return 0;
         }
     } else {
diff --git a/lib/flow.h b/lib/flow.h
index c647ad83c..a9d026e1c 100644
--- a/lib/flow.h
+++ b/lib/flow.h
@@ -132,7 +132,8 @@ void packet_expand(struct dp_packet *, const struct flow *, size_t size);
 
 bool parse_ipv6_ext_hdrs(const void **datap, size_t *sizep, uint8_t *nw_proto,
                          uint8_t *nw_frag,
-                         const struct ovs_16aligned_ip6_frag **frag_hdr);
+                         const struct ovs_16aligned_ip6_frag **frag_hdr,
+                         const struct ip6_rt_hdr **rt_hdr);
 bool parse_nsh(const void **datap, size_t *sizep, struct ovs_key_nsh *key);
 uint16_t parse_tcp_flags(struct dp_packet *packet, ovs_be16 *dl_type_p,
                          uint8_t *nw_frag_p, ovs_be16 *first_vlan_tci_p);
diff --git a/lib/ipf.c b/lib/ipf.c
index d45266374..fae2b2d72 100644
--- a/lib/ipf.c
+++ b/lib/ipf.c
@@ -486,8 +486,9 @@ ipf_reassemble_v6_frags(struct ipf_list *ipf_list)
     size_t datasize = pl;
 
     const struct ovs_16aligned_ip6_frag *frag_hdr = NULL;
-    if (!parse_ipv6_ext_hdrs(&data, &datasize, &nw_proto, &nw_frag, &frag_hdr)
-        || !nw_frag || !frag_hdr) {
+    const struct ip6_rt_hdr *rt_hdr = NULL;
+    if (!parse_ipv6_ext_hdrs(&data, &datasize, &nw_proto, &nw_frag, &frag_hdr,
+                             &rt_hdr) || !nw_frag || !frag_hdr) {
 
         ipf_print_reass_packet("Unparsed reassembled v6 packet; v6 hdr:", l3);
         dp_packet_delete(pkt);
@@ -679,8 +680,9 @@ ipf_is_valid_v6_frag(struct ipf *ipf, struct dp_packet *pkt)
     const void *data = l3 + 1;
     size_t datasize = l3_size - l3_hdr_size;
     const struct ovs_16aligned_ip6_frag *frag_hdr = NULL;
+    const struct ip6_rt_hdr *rt_hdr = NULL;
     if (!parse_ipv6_ext_hdrs(&data, &datasize, &nw_proto, &nw_frag,
-                             &frag_hdr) || !nw_frag || !frag_hdr) {
+                             &frag_hdr, &rt_hdr) || !nw_frag || !frag_hdr) {
         return false;
     }
 
@@ -722,8 +724,10 @@ ipf_v6_key_extract(struct dp_packet *pkt, ovs_be16 dl_type, uint16_t zone,
     const void *data = l3 + 1;
     size_t datasize = dp_packet_l3_size(pkt) - sizeof *l3;
     const struct ovs_16aligned_ip6_frag *frag_hdr = NULL;
+    const struct ip6_rt_hdr *rt_hdr = NULL;
 
-    parse_ipv6_ext_hdrs(&data, &datasize, &nw_proto, &nw_frag, &frag_hdr);
+    parse_ipv6_ext_hdrs(&data, &datasize, &nw_proto, &nw_frag, &frag_hdr,
+                        &rt_hdr);
     ovs_assert(nw_frag && frag_hdr);
     ovs_be16 ip6f_offlg = frag_hdr->ip6f_offlg;
     *start_data_byte = ntohs(ip6f_offlg & IP6F_OFF_MASK) +
diff --git a/lib/netdev-native-tnl.c b/lib/netdev-native-tnl.c
index b89dfdd52..550dfb5e0 100644
--- a/lib/netdev-native-tnl.c
+++ b/lib/netdev-native-tnl.c
@@ -356,6 +356,17 @@ gre_header_len(ovs_be16 flags)
     return hlen;
 }
 
+static int
+parse_srv6_header(struct dp_packet *packet,
+                 struct flow_tnl *tnl)
+{
+    unsigned int ulen;
+
+    netdev_tnl_ip_extract_tnl_md(packet, tnl, &ulen);
+
+    return ulen;
+}
+
 static int
 parse_gre_header(struct dp_packet *packet,
                  struct flow_tnl *tnl)
@@ -845,6 +856,158 @@ netdev_gtpu_build_header(const struct netdev *netdev,
     return 0;
 }
 
+static void
+srv6_build_header(struct ovs_action_push_tnl *data,
+                  const struct netdev_tnl_build_header_params *params,
+                  int nr_segs, const struct in6_addr *segs)
+{
+    struct ovs_16aligned_ip6_hdr *nh6;
+    struct srv6_base_hdr *srh;
+    struct in6_addr *s;
+    ovs_be16 dl_type;
+    unsigned int hlen;
+    int i;
+
+    ovs_assert(nr_segs > 0);
+
+    nh6 = (struct ovs_16aligned_ip6_hdr *) eth_build_header(data, params);
+    put_16aligned_be32(&nh6->ip6_flow, htonl(6 << 28) |
+                       htonl(params->flow->tunnel.ip_tos << 20));
+    nh6->ip6_hlim = params->flow->tunnel.ip_ttl;
+    nh6->ip6_nxt = IPPROTO_ROUTING;
+    memcpy(&nh6->ip6_src, params->s_ip, sizeof(ovs_be32[4]));
+    memcpy(&nh6->ip6_dst, &segs[0], sizeof(ovs_be32[4]));
+
+
+    srh = (struct srv6_base_hdr *) (nh6 + 1);
+    dl_type = params->flow->dl_type;
+    if (dl_type == htons(ETH_TYPE_IP)) {
+        srh->rt_hdr.nexthdr = IPPROTO_IPIP;
+    } else if (dl_type == htons(ETH_TYPE_IPV6)) {
+        srh->rt_hdr.nexthdr = IPPROTO_IPV6;
+    }
+    srh->rt_hdr.type = IPV6_SRCRT_TYPE_4;
+    srh->rt_hdr.hdrlen = 2 * nr_segs;
+    srh->rt_hdr.segments_left = nr_segs - 1;
+    srh->last_entry = nr_segs - 1;
+    srh->flags = 0;
+    srh->tag = 0;
+
+    s = ALIGNED_CAST(struct in6_addr *,
+                     (char *) srh + sizeof(struct srv6_base_hdr));
+    for (i = 0; i < nr_segs; i++) {
+        /* Segment list is written to the header in reverse order. */
+        memcpy(s, &segs[nr_segs - i - 1], sizeof(ovs_be32[4]));
+        s++;
+    }
+
+    hlen = IPV6_HEADER_LEN + sizeof(struct srv6_base_hdr) +
+           8 * srh->rt_hdr.hdrlen;
+
+    data->header_len += hlen;
+    data->tnl_type = OVS_VPORT_TYPE_SRV6;
+}
+
+int
+netdev_srv6_build_header(const struct netdev *netdev,
+                         struct ovs_action_push_tnl *data,
+                         const struct netdev_tnl_build_header_params *params)
+{
+    struct netdev_vport *dev = netdev_vport_cast(netdev);
+    struct netdev_tunnel_config *tnl_cfg;
+
+    ovs_mutex_lock(&dev->mutex);
+    tnl_cfg = &dev->tnl_cfg;
+
+    if (tnl_cfg->srv6_num_segs) {
+        srv6_build_header(data, params,
+                          tnl_cfg->srv6_num_segs, tnl_cfg->srv6_segs);
+    } else {
+        /*
+         * If explicit segment list setting is omitted, tunnel destination
+         * is considered to be the first segment list.
+         */
+        srv6_build_header(data, params,
+                          1, &params->flow->tunnel.ipv6_dst);
+    }
+
+    ovs_mutex_unlock(&dev->mutex);
+
+    return 0;
+}
+
+void
+netdev_srv6_push_header(const struct netdev *netdev OVS_UNUSED,
+                        struct dp_packet *packet OVS_UNUSED,
+                        const struct ovs_action_push_tnl *data OVS_UNUSED)
+{
+    int ip_tot_size;
+
+    netdev_tnl_push_ip_header(packet, data->header,
+                              data->header_len, &ip_tot_size);
+}
+
+struct dp_packet *
+netdev_srv6_pop_header(struct dp_packet *packet)
+{
+    struct pkt_metadata *md = &packet->md;
+    struct flow_tnl *tnl = &md->tunnel;
+    const struct ovs_16aligned_ip6_hdr *nh = dp_packet_l3(packet);
+    int hlen;
+
+    size_t size = dp_packet_l3_size(packet) - IPV6_HEADER_LEN;
+    const void *data = nh + 1;
+
+    uint8_t nw_frag = 0;
+    uint8_t nw_proto = nh->ip6_nxt;
+    const struct ovs_16aligned_ip6_frag *frag_hdr = NULL;
+    const struct ip6_rt_hdr *rt_hdr = NULL;
+
+    /*
+     * Verifies that the routing header is present in the IPv6
+     * extension headers and that its type is SRv6.
+     * */
+    if (!parse_ipv6_ext_hdrs(&data, &size, &nw_proto, &nw_frag,
+                             &frag_hdr, &rt_hdr)) {
+        goto err;
+    }
+
+    if (!rt_hdr) {
+        goto err;
+    }
+
+    if (rt_hdr->type != IPV6_SRCRT_TYPE_4) {
+        goto err;
+    }
+
+    if (rt_hdr->segments_left > 0) {
+        VLOG_WARN_RL(&err_rl, "invalid srv6 segments_left=%d\n",
+                     rt_hdr->segments_left);
+        goto err;
+    }
+
+    if (rt_hdr->nexthdr == IPPROTO_IPIP) {
+        packet->packet_type = htonl(PT_IPV4);
+    } else if (rt_hdr->nexthdr == IPPROTO_IPV6) {
+        packet->packet_type = htonl(PT_IPV6);
+    } else {
+        goto err;
+    }
+
+    pkt_metadata_init_tnl(md);
+
+    hlen = parse_srv6_header(packet, tnl);
+
+    dp_packet_reset_packet(packet, hlen);
+
+    return packet;
+
+err:
+    dp_packet_delete(packet);
+
+    return NULL;
+}
+
 struct dp_packet *
 netdev_vxlan_pop_header(struct dp_packet *packet)
 {
diff --git a/lib/netdev-native-tnl.h b/lib/netdev-native-tnl.h
index 22ae2ce53..07dae2797 100644
--- a/lib/netdev-native-tnl.h
+++ b/lib/netdev-native-tnl.h
@@ -65,6 +65,16 @@ netdev_gtpu_build_header(const struct netdev *netdev,
                          struct ovs_action_push_tnl *data,
                          const struct netdev_tnl_build_header_params *p);
 
+struct dp_packet *netdev_srv6_pop_header(struct dp_packet *packet);
+
+void netdev_srv6_push_header(const struct netdev *netdev,
+                             struct dp_packet *packet,
+                             const struct ovs_action_push_tnl *data);
+
+int netdev_srv6_build_header(const struct netdev *netdev,
+                             struct ovs_action_push_tnl *data,
+                             const struct netdev_tnl_build_header_params *p);
+
 void
 netdev_tnl_push_udp_header(const struct netdev *netdev,
                            struct dp_packet *packet,
diff --git a/lib/netdev-vport.c b/lib/netdev-vport.c
index 3b3927865..663ee8606 100644
--- a/lib/netdev-vport.c
+++ b/lib/netdev-vport.c
@@ -424,6 +424,35 @@ parse_tunnel_ip(const char *value, bool accept_mcast, bool *flow,
     return 0;
 }
 
+static int
+parse_srv6_segs(char *s, struct in6_addr *segs, uint8_t *num_segs)
+{
+    char *save_ptr = NULL;
+    char *token;
+
+    if (!s) {
+        return EINVAL;
+    }
+
+    *num_segs = 0;
+
+    while ((token = strtok_r(s, ",", &save_ptr)) != NULL) {
+        if (*num_segs == SRV6_MAX_SEGS) {
+            return EINVAL;
+        }
+
+        if (inet_pton(AF_INET6, token, segs) != 1) {
+            return EINVAL;
+        }
+
+        segs++;
+        (*num_segs)++;
+        s = NULL;
+    }
+
+    return 0;
+}
+
 enum tunnel_layers {
     TNL_L2 = 1 << 0,       /* 1 if a tunnel type can carry Ethernet traffic. */
     TNL_L3 = 1 << 1        /* 1 if a tunnel type can carry L3 traffic. */
@@ -443,6 +472,8 @@ tunnel_supported_layers(const char *type,
         return TNL_L3;
     } else if (!strcmp(type, "bareudp")) {
         return TNL_L3;
+    } else if (!strcmp(type, "srv6")) {
+        return TNL_L3;
     } else {
         return TNL_L2;
     }
@@ -750,6 +781,17 @@ set_tunnel_config(struct netdev *dev_, const struct smap *args, char **errp)
                     goto out;
                 }
             }
+        } else if (!strcmp(node->key, "srv6_segs")) {
+            err = parse_srv6_segs(node->value,
+                                  tnl_cfg.srv6_segs,
+                                  &tnl_cfg.srv6_num_segs);
+
+            switch (err) {
+            case EINVAL:
+                ds_put_format(&errors, "%s: bad %s 'srv6_segs'\n",
+                              name, node->value);
+                break;
+            }
         } else if (!strcmp(node->key, "payload_type")) {
             if (!strcmp(node->value, "mpls")) {
                  tnl_cfg.payload_ethertype = htons(ETH_TYPE_MPLS);
@@ -1290,6 +1332,17 @@ netdev_vport_tunnel_register(void)
           },
           {{NULL, NULL, 0, 0}}
         },
+        { "srv6_sys",
+          {
+              TUNNEL_FUNCTIONS_COMMON,
+              .type = "srv6",
+              .build_header = netdev_srv6_build_header,
+              .push_header = netdev_srv6_push_header,
+              .pop_header = netdev_srv6_pop_header,
+              .get_ifindex = NETDEV_VPORT_GET_IFINDEX,
+          },
+          {{NULL, NULL, 0, 0}}
+        },
 
     };
     static struct ovsthread_once once = OVSTHREAD_ONCE_INITIALIZER;
diff --git a/lib/netdev.h b/lib/netdev.h
index acf174927..ff207f56c 100644
--- a/lib/netdev.h
+++ b/lib/netdev.h
@@ -140,6 +140,10 @@ struct netdev_tunnel_config {
     bool erspan_idx_flow;
     bool erspan_dir_flow;
     bool erspan_hwid_flow;
+
+    uint8_t srv6_num_segs;
+    #define SRV6_MAX_SEGS 6
+    struct in6_addr srv6_segs[SRV6_MAX_SEGS];
 };
 
 void netdev_run(void);
diff --git a/lib/odp-util.c b/lib/odp-util.c
index 5fc312f8c..851fd44d3 100644
--- a/lib/odp-util.c
+++ b/lib/odp-util.c
@@ -714,6 +714,24 @@ format_odp_tnl_push_header(struct ds *ds, struct ovs_action_push_tnl *data)
             ds_put_char(ds, ')');
         }
 
+        ds_put_char(ds, ')');
+    } else if (data->tnl_type == OVS_VPORT_TYPE_SRV6) {
+        const struct srv6_base_hdr *srh;
+        struct in6_addr *segs;
+        int i;
+        int nr_segs;
+
+        srh = (const struct srv6_base_hdr *) l4;
+        segs = ALIGNED_CAST(struct in6_addr *, srh + 1);
+        nr_segs = srh->last_entry + 1;
+
+        ds_put_format(ds, "srv6(");
+        ds_put_format(ds, "segments_left=%d", srh->rt_hdr.segments_left);
+        ds_put_format(ds, ",segs=");
+        for (i = 0; i < nr_segs; i++) {
+            ds_put_format(ds, i > 0 ? "," : "");
+            ipv6_format_addr(&segs[nr_segs - i - 1], ds);
+        }
         ds_put_char(ds, ')');
     } else if (data->tnl_type == OVS_VPORT_TYPE_GRE ||
                data->tnl_type == OVS_VPORT_TYPE_IP6GRE) {
@@ -1534,6 +1552,7 @@ ovs_parse_tnl_push(const char *s, struct ovs_action_push_tnl *data)
     uint8_t hwid, dir;
     uint32_t teid;
     uint8_t gtpu_flags, gtpu_msgtype;
+    uint8_t segments_left;
 
     if (!ovs_scan_len(s, &n, "tnl_push(tnl_port(%"SCNi32"),", &data->tnl_port)) {
         return -EINVAL;
@@ -1775,6 +1794,43 @@ ovs_parse_tnl_push(const char *s, struct ovs_action_push_tnl *data)
         tnl_type = OVS_VPORT_TYPE_GTPU;
         header_len = sizeof *eth + ip_len +
                      sizeof *udp + sizeof *gtph;
+    } else if (ovs_scan_len(s, &n, "srv6(segments_left=%"SCNu8,
+                            &segments_left)) {
+        char seg_s[IPV6_SCAN_LEN + 1];
+        struct in6_addr seg;
+        struct in6_addr *segs;
+        struct srv6_base_hdr *srh = (struct srv6_base_hdr *) (ip6 + 1);
+        uint8_t n_segs = segments_left + 1;
+
+        ip6->ip6_nxt = IPPROTO_ROUTING;
+
+        srh->rt_hdr.type = IPV6_SRCRT_TYPE_4;
+        srh->rt_hdr.segments_left = segments_left;
+        srh->rt_hdr.hdrlen = 2 * n_segs;
+        srh->last_entry = n_segs - 1;
+
+        tnl_type = OVS_VPORT_TYPE_SRV6;
+        header_len = sizeof *eth + ip_len +
+                     sizeof *srh + 8 * srh->rt_hdr.hdrlen;
+
+        /* Parse segment list */
+        if (!ovs_scan_len(s, &n, ",segs="IPV6_SCAN_FMT, seg_s)
+            || inet_pton(AF_INET6, seg_s, &seg) != 1) {
+            return -EINVAL;
+        }
+
+        segs = ALIGNED_CAST(struct in6_addr *, srh + 1);
+        segs += n_segs - 1;
+        memcpy(segs--, &seg, sizeof(struct in6_addr));
+
+        while (ovs_scan_len(s, &n, ","IPV6_SCAN_FMT, seg_s)
+               && inet_pton(AF_INET6, seg_s, &seg) == 1) {
+            memcpy(segs--, &seg, sizeof(struct in_addr));
+        }
+
+        if (!ovs_scan_len(s, &n, "))")) {
+            return -EINVAL;
+        }
     } else {
         return -EINVAL;
     }
diff --git a/lib/packets.h b/lib/packets.h
index 8626aac8d..312e849f9 100644
--- a/lib/packets.h
+++ b/lib/packets.h
@@ -710,6 +710,10 @@ char *ip_parse_cidr_len(const char *s, int *n, ovs_be32 *ip,
 #define IPPROTO_UDPLITE 136
 #endif
 
+#ifndef IPPROTO_IPIP
+#define IPPROTO_IPIP 4
+#endif
+
 /* TOS fields. */
 #define IP_ECN_NOT_ECT 0x0
 #define IP_ECN_ECT_1 0x01
@@ -988,6 +992,15 @@ struct ovs_16aligned_ip6_frag {
     ovs_16aligned_be32 ip6f_ident;
 };
 
+#define IP6_RT_HDR_LEN 4
+struct ip6_rt_hdr {
+    uint8_t nexthdr;
+    uint8_t hdrlen;
+    uint8_t type;
+    uint8_t segments_left;
+};
+BUILD_ASSERT_DECL(IP6_RT_HDR_LEN == sizeof(struct ip6_rt_hdr));
+
 #define ICMP6_HEADER_LEN 4
 struct icmp6_header {
     uint8_t icmp6_type;
@@ -1514,6 +1527,17 @@ BUILD_ASSERT_DECL(sizeof(struct vxlanhdr) == 8);
 #define VXLAN_F_GPE  0x4000
 #define VXLAN_HF_GPE 0x04000000
 
+/* SRv6 protocol header */
+#define IPV6_SRCRT_TYPE_4 4
+#define SRV6_BASE_HDR_LEN 8
+struct srv6_base_hdr {
+    struct ip6_rt_hdr rt_hdr;
+    uint8_t last_entry;
+    uint8_t flags;
+    ovs_be16 tag;
+};
+BUILD_ASSERT_DECL(sizeof(struct srv6_base_hdr) == SRV6_BASE_HDR_LEN);
+
 /* Input values for PACKET_TYPE macros have to be in host byte order.
  * The _BE postfix indicates result is in network byte order. Otherwise result
  * is in host byte order. */
diff --git a/lib/tnl-ports.c b/lib/tnl-ports.c
index 050eafa6b..e920d38a4 100644
--- a/lib/tnl-ports.c
+++ b/lib/tnl-ports.c
@@ -126,7 +126,7 @@ map_insert(odp_port_t port, struct eth_addr mac, struct in6_addr *addr,
          /* XXX: No fragments support. */
         match.wc.masks.nw_frag = FLOW_NW_FRAG_MASK;
 
-        /* 'tp_port' is zero for GRE tunnels. In this case it
+        /* 'tp_port' is zero for GRE and SRv6 tunnels. In this case it
          * doesn't make sense to match on UDP port numbers. */
         if (tp_port) {
             match.wc.masks.tp_dst = OVS_BE16_MAX;
@@ -161,40 +161,39 @@ map_insert_ipdev__(struct ip_device *ip_dev, char dev_name[],
     }
 }
 
-static uint8_t
-tnl_type_to_nw_proto(const char type[])
+static void
+tnl_type_to_nw_proto(const char type[], uint8_t nw_protos[2])
 {
+    nw_protos[1] = 0;
+
     if (!strcmp(type, "geneve")) {
-        return IPPROTO_UDP;
+        nw_protos[0] = IPPROTO_UDP;
     }
     if (!strcmp(type, "stt")) {
-        return IPPROTO_TCP;
+        nw_protos[0] = IPPROTO_TCP;
     }
     if (!strcmp(type, "gre") || !strcmp(type, "erspan") ||
         !strcmp(type, "ip6erspan") || !strcmp(type, "ip6gre")) {
-        return IPPROTO_GRE;
+        nw_protos[0] = IPPROTO_GRE;
     }
     if (!strcmp(type, "vxlan")) {
-        return IPPROTO_UDP;
+        nw_protos[0] = IPPROTO_UDP;
     }
     if (!strcmp(type, "gtpu")) {
-        return IPPROTO_UDP;
+        nw_protos[0] = IPPROTO_UDP;
+    }
+    if (!strcmp(type, "srv6")) {
+        nw_protos[0] = IPPROTO_IPIP;
+        nw_protos[1] = IPPROTO_IPV6;
     }
-    return 0;
 }
 
-void
-tnl_port_map_insert(odp_port_t port, ovs_be16 tp_port,
-                    const char dev_name[], const char type[])
+static void
+tnl_port_map_insert__(odp_port_t port, ovs_be16 tp_port,
+                      const char dev_name[], uint8_t nw_proto)
 {
     struct tnl_port *p;
     struct ip_device *ip_dev;
-    uint8_t nw_proto;
-
-    nw_proto = tnl_type_to_nw_proto(type);
-    if (!nw_proto) {
-        return;
-    }
 
     ovs_mutex_lock(&mutex);
     LIST_FOR_EACH(p, node, &port_list) {
@@ -220,6 +219,22 @@ out:
     ovs_mutex_unlock(&mutex);
 }
 
+void
+tnl_port_map_insert(odp_port_t port, ovs_be16 tp_port,
+                    const char dev_name[], const char type[])
+{
+    uint8_t nw_protos[2];
+    int i;
+
+    tnl_type_to_nw_proto(type, nw_protos);
+
+    for (i = 0; i < 2; i++) {
+        if (nw_protos[i]) {
+            tnl_port_map_insert__(port, tp_port, dev_name, nw_protos[i]);
+        }
+    }
+}
+
 static void
 tnl_port_unref(const struct cls_rule *cr)
 {
@@ -256,14 +271,11 @@ ipdev_map_delete(struct ip_device *ip_dev, ovs_be16 tp_port, uint8_t nw_proto)
     }
 }
 
-void
-tnl_port_map_delete(odp_port_t port, const char type[])
+static void
+tnl_port_map_delete__(odp_port_t port, uint8_t nw_proto)
 {
     struct tnl_port *p;
     struct ip_device *ip_dev;
-    uint8_t nw_proto;
-
-    nw_proto = tnl_type_to_nw_proto(type);
 
     ovs_mutex_lock(&mutex);
     LIST_FOR_EACH_SAFE (p, node, &port_list) {
@@ -280,6 +292,21 @@ tnl_port_map_delete(odp_port_t port, const char type[])
     ovs_mutex_unlock(&mutex);
 }
 
+void
+tnl_port_map_delete(odp_port_t port, const char type[])
+{
+    uint8_t nw_protos[2];
+    int i;
+
+    tnl_type_to_nw_proto(type, nw_protos);
+
+    for (i = 0; i < 2; i++) {
+        if (nw_protos[i]) {
+            tnl_port_map_delete__(port, nw_protos[i]);
+        }
+    }
+}
+
 /* 'flow' is non-const to allow for temporary modifications during the lookup.
  * Any changes are restored before returning. */
 odp_port_t
diff --git a/ofproto/ofproto-dpif-xlate.c b/ofproto/ofproto-dpif-xlate.c
index a9cf3cbee..1c15800d9 100644
--- a/ofproto/ofproto-dpif-xlate.c
+++ b/ofproto/ofproto-dpif-xlate.c
@@ -3617,20 +3617,24 @@ propagate_tunnel_data_to_flow(struct xlate_ctx *ctx, struct eth_addr dmac,
     struct flow *base_flow, *flow;
     flow = &ctx->xin->flow;
     base_flow = &ctx->base_flow;
-    uint8_t nw_proto = 0;
+    uint8_t nw_protos[2] = {0};
 
     switch (tnl_type) {
     case OVS_VPORT_TYPE_GRE:
     case OVS_VPORT_TYPE_ERSPAN:
     case OVS_VPORT_TYPE_IP6ERSPAN:
     case OVS_VPORT_TYPE_IP6GRE:
-        nw_proto = IPPROTO_GRE;
+        nw_protos[0] = IPPROTO_GRE;
         break;
     case OVS_VPORT_TYPE_VXLAN:
     case OVS_VPORT_TYPE_GENEVE:
     case OVS_VPORT_TYPE_GTPU:
     case OVS_VPORT_TYPE_BAREUDP:
-        nw_proto = IPPROTO_UDP;
+        nw_protos[0] = IPPROTO_UDP;
+        break;
+    case OVS_VPORT_TYPE_SRV6:
+        nw_protos[0] = IPPROTO_IPIP;
+        nw_protos[1] = IPPROTO_IPV6;
         break;
     case OVS_VPORT_TYPE_LISP:
     case OVS_VPORT_TYPE_STT:
@@ -3645,10 +3649,10 @@ propagate_tunnel_data_to_flow(struct xlate_ctx *ctx, struct eth_addr dmac,
      * Update base_flow first followed by flow as the dst_flow gets modified
      * in the function.
      */
-    propagate_tunnel_data_to_flow__(base_flow, flow, dmac, smac, s_ip6, s_ip,
-                                    is_tnl_ipv6, nw_proto);
-    propagate_tunnel_data_to_flow__(flow, flow, dmac, smac, s_ip6, s_ip,
-                                    is_tnl_ipv6, nw_proto);
+    propagate_tunnel_data_to_flow__(base_flow, flow, dmac, smac, s_ip6,
+                                    s_ip, is_tnl_ipv6, nw_protos[0]);
+    propagate_tunnel_data_to_flow__(flow, flow, dmac, smac, s_ip6,
+                                    s_ip, is_tnl_ipv6, nw_protos[0]);
 }
 
 static int
diff --git a/python/ovs/flow/odp.py b/python/ovs/flow/odp.py
index db63afc8d..88aee17fb 100644
--- a/python/ovs/flow/odp.py
+++ b/python/ovs/flow/odp.py
@@ -474,6 +474,14 @@ class ODPFlow(Flow):
                                             }
                                         )
                                     ),
+                                    "srv6": nested_kv_decoder(
+                                        KVDecoders(
+                                            {
+                                                "segments_left": decode_int,
+                                                "segs": decode_default,
+                                            }
+                                        )
+                                    ),
                                 }
                             )
                         ),
diff --git a/python/ovs/tests/test_odp.py b/python/ovs/tests/test_odp.py
index f8017ca8a..d648b0e5e 100644
--- a/python/ovs/tests/test_odp.py
+++ b/python/ovs/tests/test_odp.py
@@ -452,6 +452,22 @@ def test_odp_fields(input_string, expected):
                 ),
             ],
         ),
+        (
+            "actions:tnl_push(header(srv6(segments_left=0,segs=2001:cafe::92)))",
+            [
+                KeyValue(
+                    "tnl_push",
+                    {
+                        "header": {
+                            "srv6": {
+                                "segments_left": 0,
+                                "segs": "2001:cafe::92",
+                            }
+                        }
+                    },
+                ),
+            ],
+        ),
         (
             "actions:clone(1),clone(clone(push_vlan(vid=12,pcp=0),2),1)",
             [
diff --git a/tests/automake.mk b/tests/automake.mk
index c8de3fe28..4f540179b 100644
--- a/tests/automake.mk
+++ b/tests/automake.mk
@@ -163,6 +163,7 @@ SYSTEM_USERSPACE_TESTSUITE_AT = \
 	tests/system-userspace-testsuite.at \
 	tests/system-userspace-macros.at \
 	tests/system-userspace-packet-type-aware.at \
+	tests/system-userspace-traffic.at \
 	tests/system-route.at
 
 SYSTEM_TSO_TESTSUITE_AT = \
diff --git a/tests/odp.at b/tests/odp.at
index 41eb726e9..54ae7b704 100644
--- a/tests/odp.at
+++ b/tests/odp.at
@@ -346,6 +346,7 @@ tnl_push(tnl_port(6),header(size=70,type=4,eth(dst=f8:bc:12:44:34:b6,src=f8:bc:1
 tnl_push(tnl_port(6),header(size=70,type=5,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=17,tclass=0x0,hlimit=64),udp(src=0,dst=6081,csum=0x0),geneve(oam,vni=0x1c7)),out_port(1))
 tnl_push(tnl_port(6),header(size=78,type=5,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=17,tclass=0x0,hlimit=64),udp(src=0,dst=6081,csum=0x0),geneve(crit,vni=0x1c7,options({class=0xffff,type=0x80,len=4,0xa}))),out_port(1))
 tnl_push(tnl_port(6),header(size=70,type=5,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=17,tclass=0x0,hlimit=64),udp(src=0,dst=6081,csum=0xffff),geneve(vni=0x1c7)),out_port(1))
+tnl_push(tnl_port(6),header(size=78,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=0,segs=2001:cafe::92)),out_port(1))
 ct
 ct(commit)
 ct(commit,zone=5)
diff --git a/tests/system-common-macros.at b/tests/system-common-macros.at
index 8b9f5c752..8a6689e86 100644
--- a/tests/system-common-macros.at
+++ b/tests/system-common-macros.at
@@ -126,6 +126,22 @@ m4_define([ADD_VETH_BOND],
     ]
 )
 
+# ADD_VETH_NS([ns1], [port1], [ip_addr1], [ns2], [port2], [ip_addr2])
+#
+# Add a pair of veth ports in 'ns1' and 'ns2'. The port names are 'port1'
+# and 'port2' respectively, and the IP addresses 'ip_addr1' and 'ip_addr2'
+# are assigned to each port.
+m4_define([ADD_VETH_NS],
+    [ AT_CHECK([ip link add $2 type veth peer name $5]),
+      AT_CHECK([ip link set $2 netns $1])
+      AT_CHECK([ip link set $5 netns $4])
+      NS_CHECK_EXEC([$1], [ip link set $2 up])
+      NS_CHECK_EXEC([$4], [ip link set $5 up])
+      NS_CHECK_EXEC([$1], [ip addr add $3 dev $2])
+      NS_CHECK_EXEC([$4], [ip addr add $6 dev $5])
+    ]
+)
+
 # ADD_VLAN([port], [namespace], [vlan-id], [ip-addr])
 #
 # Add a VLAN device named 'port' within 'namespace'. It will be configured
@@ -331,6 +347,12 @@ m4_define([OVS_CHECK_GENEVE_UDP6ZEROCSUM],
     [AT_SKIP_IF([! ip link add foo type geneve help 2>&1 | grep udp6zerocsum >/dev/null])
      OVS_CHECK_FIREWALL()])
 
+# OVS_CHECK_SRV6()
+m4_define([OVS_CHECK_SRV6],
+    [AT_SKIP_IF([! ip -6 route add fc00::1/96 encap seg6 mode encap dev lo 2>&1 >/dev/null])
+     AT_CHECK([ip -6 route del fc00::1/96 2>&1 >/dev/null])
+     OVS_CHECK_FIREWALL()])
+
 # OVS_CHECK_8021AD()
 m4_define([OVS_CHECK_8021AD],
     [AT_SKIP_IF([! grep -q "VLAN header stack length probed as" ovs-vswitchd.log])
diff --git a/tests/system-userspace-testsuite.at b/tests/system-userspace-testsuite.at
index 2e9659a67..43a081cda 100644
--- a/tests/system-userspace-testsuite.at
+++ b/tests/system-userspace-testsuite.at
@@ -26,4 +26,5 @@ m4_include([tests/system-traffic.at])
 m4_include([tests/system-layer3-tunnels.at])
 m4_include([tests/system-interface.at])
 m4_include([tests/system-userspace-packet-type-aware.at])
+m4_include([tests/system-userspace-traffic.at])
 m4_include([tests/system-route.at])
diff --git a/tests/system-userspace-traffic.at b/tests/system-userspace-traffic.at
new file mode 100644
index 000000000..468a016c0
--- /dev/null
+++ b/tests/system-userspace-traffic.at
@@ -0,0 +1,120 @@
+AT_BANNER([userspace-datapath-sanity])
+
+AT_SETUP([datapath - ping over srv6 tunnel])
+OVS_CHECK_TUNNEL_TSO()
+OVS_CHECK_SRV6()
+
+OVS_TRAFFIC_VSWITCHD_START()
+
+ADD_NAMESPACES(at_ns0)
+ADD_NAMESPACES(at_ns1)
+NS_EXEC([at_ns0], [sysctl -w net.ipv6.conf.default.seg6_enabled=1])
+NS_EXEC([at_ns0], [sysctl -w net.ipv4.conf.default.forwarding=1])
+NS_EXEC([at_ns0], [sysctl -w net.ipv6.conf.default.forwarding=1])
+NS_EXEC([at_ns0], [sysctl -w net.ipv6.conf.all.seg6_enabled=1])
+NS_EXEC([at_ns0], [sysctl -w net.ipv4.conf.all.forwarding=1])
+NS_EXEC([at_ns0], [sysctl -w net.ipv6.conf.all.forwarding=1])
+
+dnl Set up underlay link from host into the namespace 'at_ns0'
+dnl using veth pair. Kernel side tunnel endpoint (SID) is
+dnl 'fc00:a::1/128', so add it to the route.
+ADD_BR([br-underlay])
+ADD_VETH(p0, at_ns0, br-underlay, "fc00::1/64", [], [], "nodad")
+AT_CHECK([ovs-ofctl add-flow br-underlay "actions=normal"])
+AT_CHECK([ip addr add dev br-underlay "fc00::100/64" nodad])
+AT_CHECK([ip link set dev br-underlay up])
+AT_CHECK([ip route add fc00:a::1/128 dev br-underlay via fc00::1])
+
+dnl Set up tunnel endpoints on OVS outside the namespace.
+ADD_OVS_TUNNEL6([srv6], [br0], [at_srv6], [fc00:a::1], [10.100.100.100/24])
+AT_CHECK([ovs-vsctl set bridge br0 other_config:hwaddr=aa:55:aa:55:00:00])
+AT_CHECK([ip route add 10.1.1.0/24 dev br0 via 10.100.100.1])
+AT_CHECK([arp -s 10.100.100.1 aa:55:aa:55:00:01])
+AT_CHECK([ovs-ofctl add-flow br0 in_port=LOCAL,actions=output:at_srv6])
+AT_CHECK([ovs-ofctl add-flow br0 in_port=at_srv6,actions=mod_dl_dst:aa:55:aa:55:00:00,output:LOCAL])
+
+dnl Set up tunnel endpoints on the namespace 'at_ns0',
+dnl and overlay port on the namespace 'at_ns1'
+ADD_VETH_NS([at_ns0], [veth0], [10.1.1.2/24], [at_ns1], [veth1], [10.1.1.1/24])
+NS_CHECK_EXEC([at_ns0], [ip sr tunsrc set fc00:a::1])
+NS_CHECK_EXEC([at_ns0], [ip route add 10.100.100.0/24 encap seg6 mode encap segs fc00::100 dev p0])
+NS_CHECK_EXEC([at_ns0], [ip -6 route add fc00:a::1 encap seg6local action End.DX4 nh4 0.0.0.0 dev veth0])
+NS_CHECK_EXEC([at_ns1], [ip route add 10.100.100.0/24 via 10.1.1.2 dev veth1])
+
+dnl Linux seems to take a little time to get its IPv6 stack in order. Without
+dnl waiting, we get occasional failures due to the following error:
+dnl "connect: Cannot assign requested address"
+OVS_WAIT_UNTIL([ip netns exec at_ns0 ping6 -c 1 fc00::100])
+
+dnl First, check the underlay.
+NS_CHECK_EXEC([at_ns0], [ping6 -q -c 3 -i 0.3 -w 2 fc00::100 | FORMAT_PING], [0], [dnl
+3 packets transmitted, 3 received, 0% packet loss, time 0ms
+])
+
+dnl Okay, now check the overlay.
+NS_CHECK_EXEC([at_ns1], [ping -q -c 3 -i 0.3 -w 2 10.100.100.100 | FORMAT_PING], [0], [dnl
+3 packets transmitted, 3 received, 0% packet loss, time 0ms
+])
+
+OVS_TRAFFIC_VSWITCHD_STOP
+AT_CLEANUP
+
+AT_SETUP([datapath - ping6 over srv6 tunnel])
+OVS_CHECK_TUNNEL_TSO()
+OVS_CHECK_SRV6()
+
+OVS_TRAFFIC_VSWITCHD_START()
+
+ADD_NAMESPACES(at_ns0)
+ADD_NAMESPACES(at_ns1)
+NS_EXEC([at_ns0], [sysctl -w net.ipv6.conf.default.seg6_enabled=1])
+NS_EXEC([at_ns0], [sysctl -w net.ipv6.conf.default.forwarding=1])
+NS_EXEC([at_ns0], [sysctl -w net.ipv6.conf.all.seg6_enabled=1])
+NS_EXEC([at_ns0], [sysctl -w net.ipv6.conf.all.forwarding=1])
+
+dnl Set up underlay link from host into the namespace 'at_ns0'
+dnl using veth pair. Kernel side tunnel endpoint (SID) is
+dnl 'fc00:a::1/128', so add it to the route.
+ADD_BR([br-underlay])
+ADD_VETH(p0, at_ns0, br-underlay, "fc00::1/64", [], [], "nodad")
+AT_CHECK([ovs-ofctl add-flow br-underlay "actions=normal"])
+AT_CHECK([ip addr add dev br-underlay "fc00::100/64" nodad])
+AT_CHECK([ip link set dev br-underlay up])
+AT_CHECK([ip -6 route add fc00:a::1/128 dev br-underlay via fc00::1])
+
+dnl Set up tunnel endpoints on OVS outside the namespace.
+ADD_OVS_TUNNEL6([srv6], [br0], [at_srv6], [fc00:a::1], [fc00:100::100/64])
+AT_CHECK([ovs-vsctl set bridge br0 other_config:hwaddr=aa:55:aa:55:00:00])
+dnl [sleep infinity]
+AT_CHECK([ip addr add dev br0 fc00:100::100/64])
+AT_CHECK([ip -6 route add fc00:1::1/128 dev br0 via fc00:100::1])
+AT_CHECK([ip -6 neigh add fc00:100::1 lladdr aa:55:aa:55:00:01 dev br0])
+AT_CHECK([ovs-ofctl add-flow br0 in_port=LOCAL,actions=output:at_srv6])
+AT_CHECK([ovs-ofctl add-flow br0 in_port=at_srv6,actions=mod_dl_dst:aa:55:aa:55:00:00,output:LOCAL])
+
+dnl Set up tunnel endpoints on the namespace 'at_ns0',
+dnl and overlay port on the namespace 'at_ns1'
+ADD_VETH_NS([at_ns0], [veth0], [fc00:1::2/64], [at_ns1], [veth1], [fc00:1::1/64])
+NS_CHECK_EXEC([at_ns0], [ip sr tunsrc set fc00:a::1])
+NS_CHECK_EXEC([at_ns0], [ip -6 route add fc00:100::0/64 encap seg6 mode encap segs fc00::100 dev p0])
+NS_CHECK_EXEC([at_ns0], [ip -6 route add fc00:a::1 encap seg6local action End.DX6 nh6 :: dev veth0])
+NS_CHECK_EXEC([at_ns1], [ip -6 route add fc00:100::/64 via fc00:1::2 dev veth1])
+
+dnl Linux seems to take a little time to get its IPv6 stack in order. Without
+dnl waiting, we get occasional failures due to the following error:
+dnl "connect: Cannot assign requested address"
+OVS_WAIT_UNTIL([ip netns exec at_ns0 ping6 -c 1 fc00::100])
+OVS_WAIT_UNTIL([ip netns exec at_ns1 ping6 -c 1 fc00:100::100])
+
+dnl First, check the underlay.
+NS_CHECK_EXEC([at_ns0], [ping6 -q -c 3 -i 0.3 -w 2 fc00::100 | FORMAT_PING], [0], [dnl
+3 packets transmitted, 3 received, 0% packet loss, time 0ms
+])
+
+dnl Okay, now check the overlay.
+NS_CHECK_EXEC([at_ns1], [ping6 -q -c 3 -i 0.3 -w 2 fc00:100::100 | FORMAT_PING], [0], [dnl
+3 packets transmitted, 3 received, 0% packet loss, time 0ms
+])
+
+OVS_TRAFFIC_VSWITCHD_STOP
+AT_CLEANUP
diff --git a/tests/tunnel-push-pop-ipv6.at b/tests/tunnel-push-pop-ipv6.at
index c96b77cd1..249c3bf1e 100644
--- a/tests/tunnel-push-pop-ipv6.at
+++ b/tests/tunnel-push-pop-ipv6.at
@@ -202,6 +202,8 @@ AT_CHECK([ovs-vsctl add-port int-br t2 -- set Interface t2 type=vxlan \
                        options:remote_ip=flow options:key=123 ofport_request=5\
                     -- add-port int-br t5 -- set Interface t5 type=gre \
                        options:remote_ip=2001:cafe::92 options:key=455 options:packet_type=legacy_l3 ofport_request=6\
+                    -- add-port int-br t6 -- set Interface t6 type=srv6 \
+                       options:remote_ip=2001:cafe::92 ofport_request=7\
                        ], [0])
 
 AT_CHECK([ovs-appctl dpif/show], [0], [dnl
@@ -216,12 +218,15 @@ dummy@ovs-dummy: hit:0 missed:0
     t3 4/4789: (vxlan: csum=true, out_key=flow, remote_ip=2001:cafe::93)
     t4 5/6081: (geneve: key=123, remote_ip=flow)
     t5 6/3: (gre: key=455, packet_type=legacy_l3, remote_ip=2001:cafe::92)
+    t6 7/6: (srv6: remote_ip=2001:cafe::92)
 ])
 
 AT_CHECK([ovs-appctl tnl/ports/show |sort], [0], [dnl
 Listening ports:
 genev_sys_6081 (6081) ref_cnt=1
 gre_sys (3) ref_cnt=2
+srv6_sys (6) ref_cnt=1
+srv6_sys (6) ref_cnt=1
 vxlan_sys_4789 (4789) ref_cnt=2
 ])
 
@@ -363,6 +368,8 @@ AT_CHECK([ovs-appctl tnl/ports/show |sort], [0], [dnl
 Listening ports:
 genev_sys_6081 (6081) ref_cnt=1
 gre_sys (3) ref_cnt=2
+srv6_sys (6) ref_cnt=1
+srv6_sys (6) ref_cnt=1
 vxlan_sys_4789 (4789) ref_cnt=2
 ])
 
@@ -384,6 +391,12 @@ AT_CHECK([tail -1 stdout], [0],
   [Datapath actions: tnl_pop(6081)
 ])
 
+dnl Check SRv6 tunnel pop
+AT_CHECK([ovs-appctl ofproto/trace ovs-dummy 'in_port(1),eth(src=f8:bc:12:44:34:b6,dst=aa:55:aa:55:00:00),eth_type(0x86dd),ipv6(src=2001:cafe::92,dst=2001:cafe::88,label=0,proto=4,tclass=0x0,hlimit=64)'], [0], [stdout])
+AT_CHECK([tail -1 stdout], [0],
+  [Datapath actions: tnl_pop(6)
+])
+
 dnl Check VXLAN tunnel push
 AT_CHECK([ovs-ofctl add-flow int-br action=2])
 AT_CHECK([ovs-appctl ofproto/trace ovs-dummy 'in_port(2),eth(src=f8:bc:12:44:34:b6,dst=aa:55:aa:55:00:01),eth_type(0x0800),ipv4(src=1.1.3.88,dst=1.1.3.112,proto=47,tos=0,ttl=64,frag=no)'], [0], [stdout])
@@ -405,6 +418,13 @@ AT_CHECK([tail -1 stdout], [0],
   [Datapath actions: tnl_push(tnl_port(3),header(size=62,type=109,eth(dst=f8:bc:12:44:34:b6,src=aa:55:aa:55:00:00,dl_type=0x86dd),ipv6(src=2001:cafe::88,dst=2001:cafe::92,label=0,proto=47,tclass=0x0,hlimit=64),gre((flags=0x2000,proto=0x6558),key=0x1c8)),out_port(100)),1
 ])
 
+dnl Check SRv6 tunnel push
+AT_CHECK([ovs-ofctl add-flow int-br action=7])
+AT_CHECK([ovs-appctl ofproto/trace ovs-dummy 'in_port(2),eth(src=f8:bc:12:44:34:b6,dst=aa:55:aa:55:00:01),eth_type(0x0800),ipv4(src=1.1.3.88,dst=1.1.3.112,proto=47,tos=0,ttl=64,frag=no)'], [0], [stdout])
+AT_CHECK([tail -1 stdout], [0],
+  [Datapath actions: pop_eth,tnl_push(tnl_port(6),header(size=78,type=112,eth(dst=f8:bc:12:44:34:b6,src=aa:55:aa:55:00:00,dl_type=0x86dd),ipv6(src=2001:cafe::88,dst=2001:cafe::92,label=0,proto=43,tclass=0x0,hlimit=64),srv6(segments_left=0,segs=2001:cafe::92)),out_port(100)),1
+])
+
 dnl Check Geneve tunnel push
 AT_CHECK([ovs-ofctl add-flow int-br "actions=set_field:2001:cafe::92->tun_ipv6_dst,5"])
 AT_CHECK([ovs-appctl ofproto/trace ovs-dummy 'in_port(2),eth(src=f8:bc:12:44:34:b6,dst=aa:55:aa:55:00:01),eth_type(0x0800),ipv4(src=1.1.3.88,dst=1.1.3.112,proto=47,tos=0,ttl=64,frag=no)'], [0], [stdout])
@@ -510,6 +530,8 @@ AT_CHECK([ovs-appctl tnl/ports/show |sort], [0], [dnl
 Listening ports:
 genev_sys_6081 (6081) ref_cnt=1
 gre_sys (3) ref_cnt=1
+srv6_sys (6) ref_cnt=1
+srv6_sys (6) ref_cnt=1
 vxlan_sys_4789 (4789) ref_cnt=1
 vxlan_sys_4790 (4790) ref_cnt=1
 ])
@@ -518,6 +540,7 @@ AT_CHECK([ovs-vsctl del-port int-br t1 \
                     -- del-port int-br t2 \
                     -- del-port int-br t4 \
                     -- del-port int-br t5 \
+                    -- del-port int-br t6 \
                        ], [0])
 
 dnl Check tunnel lookup entries after deleting all remaining tunnel ports
diff --git a/tests/tunnel.at b/tests/tunnel.at
index 037b4c390..4964987f5 100644
--- a/tests/tunnel.at
+++ b/tests/tunnel.at
@@ -1223,3 +1223,59 @@ AT_CHECK([ovs-vsctl add-port br0 p1 -- set int p1 type=dummy])
 OVS_APP_EXIT_AND_WAIT([ovs-vswitchd])
 OVS_APP_EXIT_AND_WAIT([ovsdb-server])]
 AT_CLEANUP
+
+AT_SETUP([tunnel - SRV6 basic])
+OVS_VSWITCHD_START([add-port br0 p1 -- set Interface p1 type=dummy \
+                    ofport_request=1 \
+                    -- add-port br0 p2 -- set Interface p2 type=srv6 \
+                    options:remote_ip=flow \
+                    ofport_request=2])
+OVS_VSWITCHD_DISABLE_TUNNEL_PUSH_POP
+
+dnl First setup dummy interface IP address, then add the route
+dnl so that tnl-port table can get valid IP address for the device.
+AT_CHECK([ovs-appctl netdev-dummy/ip6addr br0 fc00::1/64], [0], [OK
+])
+AT_CHECK([ovs-appctl ovs/route/add fc00::0/64 br0], [0], [OK
+])
+AT_CHECK([ovs-appctl ovs/route/show], [0], [dnl
+Route Table:
+User: fc00::/64 dev br0 SRC fc00::1
+])
+
+AT_DATA([flows.txt], [dnl
+in_port=1,actions=set_field:fc00::2->tun_ipv6_dst,output:2
+in_port=2,actions=1
+])
+AT_CHECK([ovs-ofctl add-flows br0 flows.txt])
+
+AT_CHECK([ovs-appctl dpif/show | tail -n +3], [0], [dnl
+    br0 65534/100: (dummy-internal)
+    p1 1/1: (dummy)
+    p2 2/6: (srv6: remote_ip=flow)
+])
+
+AT_CHECK([ovs-appctl tnl/ports/show |sort], [0], [dnl
+Listening ports:
+srv6_sys (6) ref_cnt=1
+srv6_sys (6) ref_cnt=1
+])
+
+AT_CHECK([ovs-appctl ofproto/list-tunnels], [0], [dnl
+port 6: p2 (srv6: ::->flow, key=0, legacy_l3, dp port=6, ttl=64)
+])
+
+dnl Encap: ipv4 inner packet
+AT_CHECK([ovs-appctl ofproto/trace ovs-dummy 'in_port(1),eth(src=50:54:00:00:00:05,dst=50:54:00:00:00:07),eth_type(0x0800),ipv4(src=192.168.0.1,dst=192.168.0.2,proto=6,tos=4,ttl=128,frag=no),tcp(src=8,dst=9)'], [0], [stdout])
+AT_CHECK([tail -1 stdout], [0],
+  [Datapath actions: set(tunnel(ipv6_dst=fc00::2,ttl=64,flags(df))),pop_eth,6
+])
+
+dnl Encap: ipv6 inner packet
+AT_CHECK([ovs-appctl ofproto/trace ovs-dummy 'in_port(1),eth(src=50:54:00:00:00:05,dst=50:54:00:00:00:07),eth_type(0x86dd),ipv6(src=2001:cafe::92,dst=2001:cafe::88,label=0,proto=47,tclass=0x0,hlimit=64)'], [0], [stdout])
+AT_CHECK([tail -1 stdout], [0],
+  [Datapath actions: set(tunnel(ipv6_dst=fc00::2,ttl=64,flags(df))),pop_eth,6
+])
+
+OVS_VSWITCHD_STOP
+AT_CLEANUP
