diff mbox series

[ovs-dev] conntrack: extract l4 information for SCTP

Message ID 162620994395.706194.9943828179088324976.stgit@fed.void
State Superseded
Headers show
Series [ovs-dev] conntrack: extract l4 information for SCTP | expand

Checks

Context Check Description
ovsrobot/apply-robot success apply and check: success
ovsrobot/github-robot success github build: passed

Commit Message

Paolo Valerio July 13, 2021, 8:59 p.m. UTC
since a27d70a89 ("conntrack: add generic IP protocol support") all
the unrecognized IP protocols get handled using ct_proto_other ops
and are managed as L3 using 3 tuples.

This patch stores L4 information for SCTP in the conn_key so that
multiple conn instances, instead of one with ports zeroed, will be
created when there are multiple SCTP connections between two hosts.
It also performs crc32c check when not offloaded, and adds SCTP to
pat_enabled.

With this patch, given two SCTP association between two hosts, and
given for example the following rules:

in_port=tap0,ip,action=ct(commit,zone=1,nat(src=10.1.1.240:12345-12346)),tap1
in_port=tap1,ct_state=-trk,ip,action=ct(table=0,zone=1,nat)
in_port=tap1,ct_state=+trk,ct_zone=1,ip,action=tap0

the following entries will be created:

sctp,orig=(src=192.168.100.100,dst=10.1.1.1,sport=55884,dport=5201),reply=(src=10.1.1.1,dst=10.1.1.240,sport=5201,dport=12345),zone=1
sctp,orig=(src=192.168.100.100,dst=10.1.1.1,sport=59874,dport=5202),reply=(src=10.1.1.1,dst=10.1.1.240,sport=5202,dport=12346),zone=1

instead of:

sctp,orig=(src=192.168.100.100,dst=10.1.1.1,sport=0,dport=0),reply=(src=10.1.1.1,dst=10.1.1.240,sport=0,dport=0),zone=1

Signed-off-by: Paolo Valerio <pvalerio@redhat.com>
---
 lib/conntrack.c                  |   94 ++++++++++++++++++++++++++++++++++++++
 lib/packets.h                    |   18 +++++++
 tests/system-kmod-macros.at      |   11 ++++
 tests/system-traffic.at          |   80 ++++++++++++++++++++++++++++++++
 tests/system-userspace-macros.at |    7 +++
 5 files changed, 209 insertions(+), 1 deletion(-)

Comments

Gaetan Rivet Aug. 3, 2021, 11:15 p.m. UTC | #1
On Tue, Jul 13, 2021, at 22:59, Paolo Valerio wrote:
> since a27d70a89 ("conntrack: add generic IP protocol support") all
> the unrecognized IP protocols get handled using ct_proto_other ops
> and are managed as L3 using 3 tuples.
> 
> This patch stores L4 information for SCTP in the conn_key so that
> multiple conn instances, instead of one with ports zeroed, will be
> created when there are multiple SCTP connections between two hosts.
> It also performs crc32c check when not offloaded, and adds SCTP to
> pat_enabled.
> 
> With this patch, given two SCTP association between two hosts, and
> given for example the following rules:
> 
> in_port=tap0,ip,action=ct(commit,zone=1,nat(src=10.1.1.240:12345-12346)),tap1
> in_port=tap1,ct_state=-trk,ip,action=ct(table=0,zone=1,nat)
> in_port=tap1,ct_state=+trk,ct_zone=1,ip,action=tap0
> 
> the following entries will be created:
> 
> sctp,orig=(src=192.168.100.100,dst=10.1.1.1,sport=55884,dport=5201),reply=(src=10.1.1.1,dst=10.1.1.240,sport=5201,dport=12345),zone=1
> sctp,orig=(src=192.168.100.100,dst=10.1.1.1,sport=59874,dport=5202),reply=(src=10.1.1.1,dst=10.1.1.240,sport=5202,dport=12346),zone=1
> 
> instead of:
> 
> sctp,orig=(src=192.168.100.100,dst=10.1.1.1,sport=0,dport=0),reply=(src=10.1.1.1,dst=10.1.1.240,sport=0,dport=0),zone=1
> 
> Signed-off-by: Paolo Valerio <pvalerio@redhat.com>
> ---
>  lib/conntrack.c                  |   94 ++++++++++++++++++++++++++++++++++++++
>  lib/packets.h                    |   18 +++++++
>  tests/system-kmod-macros.at      |   11 ++++
>  tests/system-traffic.at          |   80 ++++++++++++++++++++++++++++++++
>  tests/system-userspace-macros.at |    7 +++
>  5 files changed, 209 insertions(+), 1 deletion(-)
> 
> diff --git a/lib/conntrack.c b/lib/conntrack.c
> index 551c2061a..9c628c052 100644
> --- a/lib/conntrack.c
> +++ b/lib/conntrack.c
> @@ -28,8 +28,10 @@
>  #include "conntrack-tp.h"
>  #include "coverage.h"
>  #include "csum.h"
> +#include "crc32c.h"
>  #include "ct-dpif.h"
>  #include "dp-packet.h"
> +#include "unaligned.h"

Hello Paolo,

The code looks good to me and the test runs properly.

I have only a small question. The "unaligned.h" include above is
not sorted alphabetically. Is there a reason for this that forces
to put it here?

With either a comment explaining this constraint or the include line moved,
Acked-by: Gaetan Rivet <grive@u256.net>

Thanks!
Paolo Valerio Aug. 4, 2021, 10:47 a.m. UTC | #2
Hello Gaëtan,

Gaëtan Rivet <grive@u256.net> writes:

> On Tue, Jul 13, 2021, at 22:59, Paolo Valerio wrote:
>> since a27d70a89 ("conntrack: add generic IP protocol support") all
>> the unrecognized IP protocols get handled using ct_proto_other ops
>> and are managed as L3 using 3 tuples.
>> 
>> This patch stores L4 information for SCTP in the conn_key so that
>> multiple conn instances, instead of one with ports zeroed, will be
>> created when there are multiple SCTP connections between two hosts.
>> It also performs crc32c check when not offloaded, and adds SCTP to
>> pat_enabled.
>> 
>> With this patch, given two SCTP association between two hosts, and
>> given for example the following rules:
>> 
>> in_port=tap0,ip,action=ct(commit,zone=1,nat(src=10.1.1.240:12345-12346)),tap1
>> in_port=tap1,ct_state=-trk,ip,action=ct(table=0,zone=1,nat)
>> in_port=tap1,ct_state=+trk,ct_zone=1,ip,action=tap0
>> 
>> the following entries will be created:
>> 
>> sctp,orig=(src=192.168.100.100,dst=10.1.1.1,sport=55884,dport=5201),reply=(src=10.1.1.1,dst=10.1.1.240,sport=5201,dport=12345),zone=1
>> sctp,orig=(src=192.168.100.100,dst=10.1.1.1,sport=59874,dport=5202),reply=(src=10.1.1.1,dst=10.1.1.240,sport=5202,dport=12346),zone=1
>> 
>> instead of:
>> 
>> sctp,orig=(src=192.168.100.100,dst=10.1.1.1,sport=0,dport=0),reply=(src=10.1.1.1,dst=10.1.1.240,sport=0,dport=0),zone=1
>> 
>> Signed-off-by: Paolo Valerio <pvalerio@redhat.com>
>> ---
>>  lib/conntrack.c                  |   94 ++++++++++++++++++++++++++++++++++++++
>>  lib/packets.h                    |   18 +++++++
>>  tests/system-kmod-macros.at      |   11 ++++
>>  tests/system-traffic.at          |   80 ++++++++++++++++++++++++++++++++
>>  tests/system-userspace-macros.at |    7 +++
>>  5 files changed, 209 insertions(+), 1 deletion(-)
>> 
>> diff --git a/lib/conntrack.c b/lib/conntrack.c
>> index 551c2061a..9c628c052 100644
>> --- a/lib/conntrack.c
>> +++ b/lib/conntrack.c
>> @@ -28,8 +28,10 @@
>>  #include "conntrack-tp.h"
>>  #include "coverage.h"
>>  #include "csum.h"
>> +#include "crc32c.h"
>>  #include "ct-dpif.h"
>>  #include "dp-packet.h"
>> +#include "unaligned.h"
>
> Hello Paolo,
>
> The code looks good to me and the test runs properly.
>

thanks for the review.

> I have only a small question. The "unaligned.h" include above is
> not sorted alphabetically. Is there a reason for this that forces
> to put it here?
>

not really, most probably I forgot to reorder :)
Thanks for spotting this. I'll send a v2.

> With either a comment explaining this constraint or the include line moved,
> Acked-by: Gaetan Rivet <grive@u256.net>
>
> Thanks!
> -- 
> Gaetan Rivet
diff mbox series

Patch

diff --git a/lib/conntrack.c b/lib/conntrack.c
index 551c2061a..9c628c052 100644
--- a/lib/conntrack.c
+++ b/lib/conntrack.c
@@ -28,8 +28,10 @@ 
 #include "conntrack-tp.h"
 #include "coverage.h"
 #include "csum.h"
+#include "crc32c.h"
 #include "ct-dpif.h"
 #include "dp-packet.h"
+#include "unaligned.h"
 #include "flow.h"
 #include "netdev.h"
 #include "odp-netlink.h"
@@ -731,6 +733,9 @@  pat_packet(struct dp_packet *pkt, const struct conn *conn)
         } else if (conn->key.nw_proto == IPPROTO_UDP) {
             struct udp_header *uh = dp_packet_l4(pkt);
             packet_set_udp_port(pkt, conn->rev_key.dst.port, uh->udp_dst);
+        } else if (conn->key.nw_proto == IPPROTO_SCTP) {
+            struct sctp_header *sh = dp_packet_l4(pkt);
+            packet_set_sctp_port(pkt, conn->rev_key.dst.port, sh->sctp_dst);
         }
     } else if (conn->nat_info->nat_action & NAT_ACTION_DST) {
         if (conn->key.nw_proto == IPPROTO_TCP) {
@@ -739,6 +744,9 @@  pat_packet(struct dp_packet *pkt, const struct conn *conn)
         } else if (conn->key.nw_proto == IPPROTO_UDP) {
             packet_set_udp_port(pkt, conn->rev_key.dst.port,
                                 conn->rev_key.src.port);
+        } else if (conn->key.nw_proto == IPPROTO_SCTP) {
+            packet_set_sctp_port(pkt, conn->rev_key.dst.port,
+                                 conn->rev_key.src.port);
         }
     }
 }
@@ -789,12 +797,17 @@  un_pat_packet(struct dp_packet *pkt, const struct conn *conn)
         } else if (conn->key.nw_proto == IPPROTO_UDP) {
             struct udp_header *uh = dp_packet_l4(pkt);
             packet_set_udp_port(pkt, uh->udp_src, conn->key.src.port);
+        } else if (conn->key.nw_proto == IPPROTO_SCTP) {
+            struct sctp_header *sh = dp_packet_l4(pkt);
+            packet_set_sctp_port(pkt, sh->sctp_src, conn->key.src.port);
         }
     } else if (conn->nat_info->nat_action & NAT_ACTION_DST) {
         if (conn->key.nw_proto == IPPROTO_TCP) {
             packet_set_tcp_port(pkt, conn->key.dst.port, conn->key.src.port);
         } else if (conn->key.nw_proto == IPPROTO_UDP) {
             packet_set_udp_port(pkt, conn->key.dst.port, conn->key.src.port);
+        } else if (conn->key.nw_proto == IPPROTO_SCTP) {
+            packet_set_sctp_port(pkt, conn->key.dst.port, conn->key.src.port);
         }
     }
 }
@@ -811,6 +824,10 @@  reverse_pat_packet(struct dp_packet *pkt, const struct conn *conn)
             struct udp_header *uh_in = dp_packet_l4(pkt);
             packet_set_udp_port(pkt, conn->key.src.port,
                                 uh_in->udp_dst);
+        } else if (conn->key.nw_proto == IPPROTO_SCTP) {
+            struct sctp_header *sh_in = dp_packet_l4(pkt);
+            packet_set_sctp_port(pkt, conn->key.src.port,
+                                 sh_in->sctp_dst);
         }
     } else if (conn->nat_info->nat_action & NAT_ACTION_DST) {
         if (conn->key.nw_proto == IPPROTO_TCP) {
@@ -819,6 +836,9 @@  reverse_pat_packet(struct dp_packet *pkt, const struct conn *conn)
         } else if (conn->key.nw_proto == IPPROTO_UDP) {
             packet_set_udp_port(pkt, conn->key.src.port,
                                 conn->key.dst.port);
+        } else if (conn->key.nw_proto == IPPROTO_SCTP) {
+            packet_set_sctp_port(pkt, conn->key.src.port,
+                                 conn->key.dst.port);
         }
     }
 }
@@ -1719,6 +1739,26 @@  checksum_valid(const struct conn_key *key, const void *data, size_t size,
     return valid;
 }
 
+static inline bool
+sctp_checksum_valid(const void *data, size_t size)
+{
+    struct sctp_header *sctp = (struct sctp_header *) data;
+    ovs_be32 rcvd_csum, csum;
+    bool ret;
+
+    rcvd_csum = get_16aligned_be32(&sctp->sctp_csum);
+    put_16aligned_be32(&sctp->sctp_csum, 0);
+    csum = crc32c(data, size);
+    put_16aligned_be32(&sctp->sctp_csum, rcvd_csum);
+
+    ret = (rcvd_csum == csum);
+    if (!ret) {
+        COVERAGE_INC(conntrack_l4csum_err);
+    }
+
+    return ret;
+}
+
 static inline bool
 check_l4_tcp(const struct conn_key *key, const void *data, size_t size,
              const void *l3, bool validate_checksum)
@@ -1755,6 +1795,39 @@  check_l4_udp(const struct conn_key *key, const void *data, size_t size,
            || (validate_checksum ? checksum_valid(key, data, size, l3) : true);
 }
 
+static inline bool
+sctp_check_len(const struct sctp_header *sh, size_t size)
+{
+    const struct sctp_chunk_header *sch;
+    size_t s;
+
+    if (size < SCTP_HEADER_LEN) {
+        return false;
+    }
+
+    FOR_EACH_SCTP_CHUNK(sh, sch, s, size) {
+        /* This value represents the size of the chunk in bytes, including
+         * the Chunk Type, Chunk Flags, Chunk Length, and Chunk Value fields.
+         * Therefore, if the Chunk Value field is zero-length, the Length
+         * field will be set to 4. */
+        if (ntohs(sch->length) < sizeof(*sch)) {
+            return false;
+        }
+    }
+
+    return (s == size);
+}
+
+static inline bool
+check_l4_sctp(const void *data, size_t size, bool validate_checksum)
+{
+    if (OVS_UNLIKELY(!sctp_check_len(data, size))) {
+        return false;
+    }
+
+    return validate_checksum ? sctp_checksum_valid(data, size) : true;
+}
+
 static inline bool
 check_l4_icmp(const void *data, size_t size, bool validate_checksum)
 {
@@ -1805,6 +1878,21 @@  extract_l4_udp(struct conn_key *key, const void *data, size_t size,
     return key->src.port && key->dst.port;
 }
 
+static inline bool
+extract_l4_sctp(struct conn_key *key, const void *data, size_t size,
+                size_t *chk_len)
+{
+    if (OVS_UNLIKELY(size < (chk_len ? *chk_len : SCTP_HEADER_LEN))) {
+        return false;
+    }
+
+    const struct sctp_header *sctp = data;
+    key->src.port = sctp->sctp_src;
+    key->dst.port = sctp->sctp_dst;
+
+    return key->src.port && key->dst.port;
+}
+
 static inline bool extract_l4(struct conn_key *key, const void *data,
                               size_t size, bool *related, const void *l3,
                               bool validate_checksum, size_t *chk_len);
@@ -2020,6 +2108,9 @@  extract_l4(struct conn_key *key, const void *data, size_t size, bool *related,
         return (!related || check_l4_udp(key, data, size, l3,
                 validate_checksum))
                && extract_l4_udp(key, data, size, chk_len);
+    } else if (key->nw_proto == IPPROTO_SCTP) {
+        return (!related || check_l4_sctp(data, size, validate_checksum))
+               && extract_l4_sctp(key, data, size, chk_len);
     } else if (key->dl_type == htons(ETH_TYPE_IP)
                && key->nw_proto == IPPROTO_ICMP) {
         return (!related || check_l4_icmp(data, size, validate_checksum))
@@ -2411,7 +2502,8 @@  nat_get_unique_tuple(struct conntrack *ct, const struct conn *conn,
                   guard_addr = {0};
     uint32_t hash = nat_range_hash(conn, ct->hash_basis);
     bool pat_proto = conn->key.nw_proto == IPPROTO_TCP ||
-                     conn->key.nw_proto == IPPROTO_UDP;
+                     conn->key.nw_proto == IPPROTO_UDP ||
+                     conn->key.nw_proto == IPPROTO_SCTP;
     uint16_t min_dport, max_dport, curr_dport;
     uint16_t min_sport, max_sport, curr_sport;
 
diff --git a/lib/packets.h b/lib/packets.h
index 515bb59b1..a6b7a5330 100644
--- a/lib/packets.h
+++ b/lib/packets.h
@@ -848,6 +848,24 @@  struct sctp_header {
 };
 BUILD_ASSERT_DECL(SCTP_HEADER_LEN == sizeof(struct sctp_header));
 
+#define SCTP_CHUNK_HEADER_LEN 4
+struct sctp_chunk_header {
+    uint8_t type;
+    uint8_t flags;
+    ovs_be16 length;
+};
+BUILD_ASSERT_DECL(SCTP_CHUNK_HEADER_LEN == sizeof(struct sctp_chunk_header));
+
+#define SCTP_NEXT_CHUNK(sh, off) \
+    ALIGNED_CAST(struct sctp_chunk_header *, (uint8_t *) sh + off)
+
+#define FOR_EACH_SCTP_CHUNK(sh, sch, offset, size) \
+    for (offset = sizeof(struct sctp_header), \
+         sch = SCTP_NEXT_CHUNK(sh, offset); \
+         offset < size; \
+         offset += ROUND_UP(ntohs(sch->length), 4), \
+         sch = SCTP_NEXT_CHUNK(sh, offset))
+
 #define UDP_HEADER_LEN 8
 struct udp_header {
     ovs_be16 udp_src;
diff --git a/tests/system-kmod-macros.at b/tests/system-kmod-macros.at
index 86d633ac4..070fd4899 100644
--- a/tests/system-kmod-macros.at
+++ b/tests/system-kmod-macros.at
@@ -110,6 +110,17 @@  m4_define([CHECK_CONNTRACK_ZEROIP_SNAT],
     AT_SKIP_IF([test "$IS_WIN32" = "yes"])
 ])
 
+# CHECK_CONNTRACK_SCTP()
+#
+# Perform requirements checks for running conntrack SCTP. The kernel
+# optionally support nf proto sctp.
+#
+m4_define([CHECK_CONNTRACK_SCTP],
+[
+   AT_SKIP_IF([test "$IS_WIN32" = "yes"])
+   AT_SKIP_IF([! test -e /proc/sys/net/netfilter/nf_conntrack_sctp_timeout_closed])
+])
+
 # CHECK_CONNTRACK_TIMEOUT()
 #
 # Perform requirements checks for running conntrack customized timeout tests.
diff --git a/tests/system-traffic.at b/tests/system-traffic.at
index f400cfabc..65e4ffc08 100644
--- a/tests/system-traffic.at
+++ b/tests/system-traffic.at
@@ -3451,6 +3451,86 @@  udp,orig=(src=10.1.1.1,dst=10.1.1.2,sport=<cleared>,dport=<cleared>),reply=(src=
 OVS_TRAFFIC_VSWITCHD_STOP
 AT_CLEANUP
 
+AT_SETUP([conntrack - SCTP SNAT with port range])
+CHECK_CONNTRACK()
+CHECK_CONNTRACK_SCTP()
+OVS_TRAFFIC_VSWITCHD_START()
+
+ADD_NAMESPACES(at_ns0, at_ns1)
+
+ADD_VETH(p0, at_ns0, br0, "10.1.1.1/24")
+ADD_VETH(p1, at_ns1, br0, "10.1.1.2/24")
+NS_CHECK_EXEC([at_ns0], [ip link set dev p0 address e6:66:c1:11:11:11])
+NS_CHECK_EXEC([at_ns1], [ip link set dev p1 address e6:66:c1:22:22:22])
+
+dnl Allow any traffic from ns0->ns1. Only allow return traffic from ns1->ns0.
+AT_DATA([flows.txt], [dnl
+table=0,priority=100,in_port=1,sctp,action=ct(commit,zone=1,nat(src=10.1.1.240:34567)),controller
+table=0,priority=100,in_port=2,ct_state=-trk,sctp,tp_dst=34567,action=ct(table=1,zone=1,nat)
+table=0,priority=0,action=drop
+table=1,priority=100,in_port=2,ct_state=+trk+rpl,ct_zone=1,sctp,action=controller
+table=1,priority=0,action=drop
+])
+
+AT_CHECK([ovs-ofctl --bundle add-flows br0 flows.txt])
+
+AT_CAPTURE_FILE([ofctl_monitor.log])
+AT_CHECK([ovs-ofctl monitor br0 65534 invalid_ttl --detach --no-chdir --pidfile 2> ofctl_monitor.log])
+
+dnl Simple SCTP association local and remote single homing
+dnl Send INIT.
+AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=1 packet=e666c1222222e666c111111108004502004400004000408424300a0101010a010102d6b9303900000000c5cc426b0100002470e18ccc0001a000000affff7ae1c142000c00060005000080000004c0000004 actions=resubmit(,0)"])
+dnl Reply INIT_ACK.
+AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=2 packet=e666c1111111e666c122222208004502012400004000408422610a0101020a0101f03039870770e18ccc97abd49a0200010425bb9dfa0001a000000a000abb90fba5000700e827a048cd1474b111490710816ec95cfc501126b200000000000000000000000000000000fa9dbb25cc8ce17000000000000000002b953b0e1d346d160a000a00a5fb90bb020087070a0101f00000000000000000000000000000000000000000393001000000000080020024fbb82eae13af8d70329bc42bb7cd7e6458d60ff1a181e9b41167c2cab54471bf0000000000000000000000000000000000000000000000000000000000000000000000000100002470e18ccc0001a000000affff7ae1c142000c00060005000080000004c00000040000000000000000000000000000000080000004c0000004 actions=resubmit(,0)"])
+dnl Send COOKIE_ECHO.
+AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=1 packet=e666c1222222e666c1111111080045020108000040004084236c0a0101010a010102d6b9303925bb9dfaf2c860300a0000e827a048cd1474b111490710816ec95cfc501126b200000000000000000000000000000000fa9dbb25cc8ce17000000000000000002b953b0e1d346d160a000a00a5fb90bb020087070a0101f00000000000000000000000000000000000000000393001000000000080020024fbb82eae13af8d70329bc42bb7cd7e6458d60ff1a181e9b41167c2cab54471bf0000000000000000000000000000000000000000000000000000000000000000000000000100002470e18ccc0001a000000affff7ae1c142000c00060005000080000004c000000400000000000000000000000000000000 actions=resubmit(,0)"])
+dnl Reply COOKIE_ACK.
+AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=2 packet=e666c1111111e666c122222208004502002400004000408423610a0101020a0101f03039870770e18ccc0391398b0b000004 actions=resubmit(,0)"])
+dnl Send DATA.
+AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=1 packet=e666c1222222e666c1111111080045020034000140004084243f0a0101010a010102d6b9303925bb9dfabc366345000300147ae1c1420000000000000000666f6f0a actions=resubmit(,0)"])
+dnl Reply SACK.
+AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=2 packet=e666c1111111e666c122222208004502003042c840004084e08c0a0101020a0101f03039870770e18ccc6a990714030000107ae1c14200019ffc00000000 actions=resubmit(,0)"])
+dnl Send SHUTDOWN the association.
+AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=1 packet=e666c1222222e666c1111111080045020028000240004084244a0a0101010a010102d6b9303925bb9dfad2fc7dd707000008bb90fba4 actions=resubmit(,0)"])
+dnl Reply SHUTDOWN_ACK.
+AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=2 packet=e666c1111111e666c122222208004502002442c940004084e0970a0101020a0101f03039870770e18ccc3a181be908000004 actions=resubmit(,0)"])
+dnl Send SHUTDOWN_COMPLETE.
+AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=1 packet=e666c1222222e666c1111111080045020024000340004084244d0a0101010a010102d6b9303925bb9dfa05db68ce0e000004 actions=resubmit(,0)"])
+
+AT_CHECK([ovs-appctl revalidator/purge], [0])
+
+OVS_APP_EXIT_AND_WAIT([ovs-ofctl])
+
+AT_CHECK([cat ofctl_monitor.log], [0], [dnl
+NXT_PACKET_IN2 (xid=0x0): cookie=0x0 total_len=82 in_port=1 (via action) data_len=82 (unbuffered)
+sctp,vlan_tci=0x0000,dl_src=e6:66:c1:11:11:11,dl_dst=e6:66:c1:22:22:22,nw_src=10.1.1.240,nw_dst=10.1.1.2,nw_tos=0,nw_ecn=2,nw_ttl=64,tp_src=34567,tp_dst=12345 sctp_csum:9670267b
+NXT_PACKET_IN2 (xid=0x0): table_id=1 cookie=0x0 total_len=306 ct_state=est|rpl|trk|dnat,ct_zone=1,ct_nw_src=10.1.1.1,ct_nw_dst=10.1.1.2,ct_nw_proto=132,ct_tp_src=54969,ct_tp_dst=12345,ip,in_port=2 (via action) data_len=306 (unbuffered)
+sctp,vlan_tci=0x0000,dl_src=e6:66:c1:22:22:22,dl_dst=e6:66:c1:11:11:11,nw_src=10.1.1.2,nw_dst=10.1.1.1,nw_tos=0,nw_ecn=2,nw_ttl=64,tp_src=12345,tp_dst=54969 sctp_csum:49864886
+NXT_PACKET_IN2 (xid=0x0): cookie=0x0 total_len=278 in_port=1 (via action) data_len=278 (unbuffered)
+sctp,vlan_tci=0x0000,dl_src=e6:66:c1:11:11:11,dl_dst=e6:66:c1:22:22:22,nw_src=10.1.1.240,nw_dst=10.1.1.2,nw_tos=0,nw_ecn=2,nw_ttl=64,tp_src=34567,tp_dst=12345 sctp_csum:8c816918
+NXT_PACKET_IN2 (xid=0x0): table_id=1 cookie=0x0 total_len=50 ct_state=est|rpl|trk|dnat,ct_zone=1,ct_nw_src=10.1.1.1,ct_nw_dst=10.1.1.2,ct_nw_proto=132,ct_tp_src=54969,ct_tp_dst=12345,ip,in_port=2 (via action) data_len=50 (unbuffered)
+sctp,vlan_tci=0x0000,dl_src=e6:66:c1:22:22:22,dl_dst=e6:66:c1:11:11:11,nw_src=10.1.1.2,nw_dst=10.1.1.1,nw_tos=0,nw_ecn=2,nw_ttl=64,tp_src=12345,tp_dst=54969 sctp_csum:ef4749fc
+NXT_PACKET_IN2 (xid=0x0): cookie=0x0 total_len=66 in_port=1 (via action) data_len=66 (unbuffered)
+sctp,vlan_tci=0x0000,dl_src=e6:66:c1:11:11:11,dl_dst=e6:66:c1:22:22:22,nw_src=10.1.1.240,nw_dst=10.1.1.2,nw_tos=0,nw_ecn=2,nw_ttl=64,tp_src=34567,tp_dst=12345 sctp_csum:eb2b2c17
+NXT_PACKET_IN2 (xid=0x0): table_id=1 cookie=0x0 total_len=62 ct_state=est|rpl|trk|dnat,ct_zone=1,ct_nw_src=10.1.1.1,ct_nw_dst=10.1.1.2,ct_nw_proto=132,ct_tp_src=54969,ct_tp_dst=12345,ip,in_port=2 (via action) data_len=62 (unbuffered)
+sctp,vlan_tci=0x0000,dl_src=e6:66:c1:22:22:22,dl_dst=e6:66:c1:11:11:11,nw_src=10.1.1.2,nw_dst=10.1.1.1,nw_tos=0,nw_ecn=2,nw_ttl=64,tp_src=12345,tp_dst=54969 sctp_csum:9b67e853
+NXT_PACKET_IN2 (xid=0x0): cookie=0x0 total_len=54 in_port=1 (via action) data_len=54 (unbuffered)
+sctp,vlan_tci=0x0000,dl_src=e6:66:c1:11:11:11,dl_dst=e6:66:c1:22:22:22,nw_src=10.1.1.240,nw_dst=10.1.1.2,nw_tos=0,nw_ecn=2,nw_ttl=64,tp_src=34567,tp_dst=12345 sctp_csum:bc0e5463
+NXT_PACKET_IN2 (xid=0x0): table_id=1 cookie=0x0 total_len=50 ct_state=est|rpl|trk|dnat,ct_zone=1,ct_nw_src=10.1.1.1,ct_nw_dst=10.1.1.2,ct_nw_proto=132,ct_tp_src=54969,ct_tp_dst=12345,ip,in_port=2 (via action) data_len=50 (unbuffered)
+sctp,vlan_tci=0x0000,dl_src=e6:66:c1:22:22:22,dl_dst=e6:66:c1:11:11:11,nw_src=10.1.1.2,nw_dst=10.1.1.1,nw_tos=0,nw_ecn=2,nw_ttl=64,tp_src=12345,tp_dst=54969 sctp_csum:d6ce6b9e
+NXT_PACKET_IN2 (xid=0x0): cookie=0x0 total_len=50 in_port=1 (via action) data_len=50 (unbuffered)
+sctp,vlan_tci=0x0000,dl_src=e6:66:c1:11:11:11,dl_dst=e6:66:c1:22:22:22,nw_src=10.1.1.240,nw_dst=10.1.1.2,nw_tos=0,nw_ecn=2,nw_ttl=64,tp_src=34567,tp_dst=12345 sctp_csum:add7db93
+])
+
+dnl Check the ct entry
+dnl protoinfo has to be removed in order to normalize the current difference between user and kernel output
+AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(10.1.1.2) | sed 's/,protoinfo=.*$//' ], [], [dnl
+sctp,orig=(src=10.1.1.1,dst=10.1.1.2,sport=<cleared>,dport=<cleared>),reply=(src=10.1.1.2,dst=10.1.1.240,sport=<cleared>,dport=<cleared>),zone=1
+])
+
+OVS_TRAFFIC_VSWITCHD_STOP
+AT_CLEANUP
+
 dnl Check kernel datapath to make sure conntrack fills in L3 and L4
 dnl protocol information
 AT_SETUP([conntrack - fragment reassembly with L3 L4 protocol information])
diff --git a/tests/system-userspace-macros.at b/tests/system-userspace-macros.at
index f639ba53a..fd6307745 100644
--- a/tests/system-userspace-macros.at
+++ b/tests/system-userspace-macros.at
@@ -104,6 +104,13 @@  m4_define([CHECK_CONNTRACK_NAT])
 #
 m4_define([CHECK_CONNTRACK_ZEROIP_SNAT])
 
+# CHECK_CONNTRACK_SCTP()
+#
+# Perform requirements checks for running conntrack SCTP. The userspace
+# datapath has no dependency, so no check is required.
+#
+m4_define([CHECK_CONNTRACK_SCTP])
+
 # CHECK_CONNTRACK_TIMEOUT()
 #
 # Perform requirements checks for running conntrack customized timeout tests.