]> git.ipfire.org Git - thirdparty/suricata.git/commitdiff
stream: SYN queue support 8569/head
authorVictor Julien <vjulien@oisf.net>
Thu, 9 Feb 2023 16:11:21 +0000 (17:11 +0100)
committerVictor Julien <vjulien@oisf.net>
Thu, 2 Mar 2023 17:00:33 +0000 (18:00 +0100)
Support case where there are multiple SYN retransmits, where
each has a new timestamp.

Before this patch, Suricata would only accept a SYN/ACK that
matches the last timestamp. However, observed behavior is that
the server may choose to only respond to the first. In IPS mode
this could lead to a connection timing out as Suricata drops
the SYN/ACK it considers wrong, and the server continues to
retransmit it.

This patch reuses the SYN/ACK queuing logic to keep a list
of SYN packets and their window, timestamp, wscale and sackok
settings. Then when the SYN/ACK arrives, it is first evaluated
against the normal session state. But if it fails due to a
timestamp mismatch, it will look for queued SYN's and see if
any of them match the timestamp. If one does, the ssn is updated
to use that SYN and the SYN/ACK is accepted.

Bug: #5856.
(cherry picked from commit 7bfee147ef6caefe0dd4444a088f451188108e0a)

rules/stream-events.rules
src/decode-events.c
src/decode-events.h
src/stream-tcp-private.h
src/stream-tcp.c

index 205a137df4b1baaad6cc686864692898e963afe8..cb1ae245e7ba126bc76f8566f180ea144a8071b5 100644 (file)
@@ -10,11 +10,12 @@ alert tcp any any -> any any (msg:"SURICATA STREAM 3way handshake SYNACK resend
 alert tcp any any -> any any (msg:"SURICATA STREAM 3way handshake SYNACK resend with different seq"; stream-event:3whs_synack_resend_with_diff_seq; classtype:protocol-command-decode; sid:2210005; rev:2;)
 alert tcp any any -> any any (msg:"SURICATA STREAM 3way handshake SYNACK to server on SYN recv"; stream-event:3whs_synack_toserver_on_syn_recv; classtype:protocol-command-decode; sid:2210006; rev:2;)
 alert tcp any any -> any any (msg:"SURICATA STREAM 3way handshake SYNACK with wrong ack"; stream-event:3whs_synack_with_wrong_ack; classtype:protocol-command-decode; sid:2210007; rev:2;)
-# Excessive SYN/ACKs within a session. Limit is set in stream engine, "stream.max-synack-queued".
+# Excessive SYNs or SYN/ACKs within a session. Limit is set in stream engine, "stream.max-synack-queued".
 alert tcp any any -> any any (msg:"SURICATA STREAM 3way handshake excessive different SYN/ACKs"; stream-event:3whs_synack_flood; classtype:protocol-command-decode; sid:2210055; rev:2;)
 # Client sent an SYN packet with TCP fast open and data, but the server only ACK'd
 # the SYN, not the data, while still supporting TFO.
 alert tcp any any -> any any (msg:"SURICATA STREAM 3way handshake SYN/ACK ignored TFO data"; stream-event:3whs_synack_tfo_data_ignored; classtype:protocol-command-decode; sid:2210064; rev:1;)
+alert tcp any any -> any any (msg:"SURICATA STREAM 3way handshake excessive different SYNs"; stream-event:3whs_syn_flood; classtype:protocol-command-decode; sid:2210063; rev:1;)
 alert tcp any any -> any any (msg:"SURICATA STREAM 3way handshake SYN resend different seq on SYN recv"; stream-event:3whs_syn_resend_diff_seq_on_syn_recv; classtype:protocol-command-decode; sid:2210008; rev:2;)
 alert tcp any any -> any any (msg:"SURICATA STREAM 3way handshake SYN to client on SYN recv"; stream-event:3whs_syn_toclient_on_syn_recv; classtype:protocol-command-decode; sid:2210009; rev:2;)
 alert tcp any any -> any any (msg:"SURICATA STREAM 3way handshake wrong seq wrong ack"; stream-event:3whs_wrong_seq_wrong_ack; classtype:protocol-command-decode; sid:2210010; rev:2;)
@@ -98,4 +99,3 @@ alert tcp any any -> any any (msg:"SURICATA STREAM pkt seen on wrong thread"; st
 alert tcp any any -> any any (msg:"SURICATA STREAM FIN SYN reuse"; stream-event:fin_syn; classtype:protocol-command-decode; sid:2210060; rev:1;)
 
 # next sid 2210065
-
index 79659e866a35869aeee28bfcb57f56f571040422..78c3cd9acff05bcd12552546d7090b845870b67a 100644 (file)
@@ -607,6 +607,10 @@ const struct DecodeEvents_ DEvents[] = {
             "stream.3whs_syn_toclient_on_syn_recv",
             STREAM_3WHS_SYN_TOCLIENT_ON_SYN_RECV,
     },
+    {
+            "stream.3whs_syn_flood",
+            STREAM_3WHS_SYN_FLOOD,
+    },
     {
             "stream.3whs_wrong_seq_wrong_ack",
             STREAM_3WHS_WRONG_SEQ_WRONG_ACK,
index e5f0ae968549fff0b83f2ee5ccedb6365c835f3d..e42afafae72256045a3cd7a97b9a90b3cc745519 100644 (file)
@@ -225,6 +225,7 @@ enum {
     STREAM_3WHS_SYNACK_TFO_DATA_IGNORED,
     STREAM_3WHS_SYN_RESEND_DIFF_SEQ_ON_SYN_RECV,
     STREAM_3WHS_SYN_TOCLIENT_ON_SYN_RECV,
+    STREAM_3WHS_SYN_FLOOD,
     STREAM_3WHS_WRONG_SEQ_WRONG_ACK,
     STREAM_3WHS_ACK_DATA_INJECT,
     STREAM_4WHS_SYNACK_WITH_WRONG_ACK,
index b229edfae800a2df37b7eebc88b7b5136088ab1d..57f8c33b42c1e2b19984539a2a70f5f22bab5359 100644 (file)
@@ -34,7 +34,7 @@
 #define STREAMTCP_QUEUE_FLAG_WS     0x02
 #define STREAMTCP_QUEUE_FLAG_SACK   0x04
 
-/** currently only SYN/ACK */
+/** Tracking SYNs and SYN/ACKs */
 typedef struct TcpStateQueue_ {
     uint8_t flags;
     uint8_t wscale;
index d2ab48924aa01ff8bbe755108269a9e36274f980..8c7f2e21f5fde1d781a778c7ae7ba1b3efd5a561 100644 (file)
@@ -1460,6 +1460,170 @@ static inline bool StateSynSentValidateTimestamp(TcpSession *ssn, Packet *p)
     return true;
 }
 
+static void TcpStateQueueInitFromSsnSyn(const TcpSession *ssn, TcpStateQueue *q)
+{
+    BUG_ON(ssn->state != TCP_SYN_SENT); // TODO
+    memset(q, 0, sizeof(*q));
+
+    /* SYN won't use wscale yet. So window should be limited to 16 bits. */
+    DEBUG_VALIDATE_BUG_ON(ssn->server.window > UINT16_MAX);
+    q->win = (uint16_t)ssn->server.window;
+
+    q->pkt_ts = ssn->client.last_pkt_ts;
+
+    if (ssn->flags & STREAMTCP_FLAG_CLIENT_SACKOK) {
+        q->flags |= STREAMTCP_QUEUE_FLAG_SACK;
+    }
+    if (ssn->flags & STREAMTCP_FLAG_SERVER_WSCALE) {
+        q->flags |= STREAMTCP_QUEUE_FLAG_WS;
+        q->wscale = ssn->server.wscale;
+    }
+    if (ssn->client.flags & STREAMTCP_STREAM_FLAG_TIMESTAMP) {
+        q->flags |= STREAMTCP_QUEUE_FLAG_TS;
+        q->ts = ssn->client.last_ts;
+    }
+
+    SCLogDebug("ssn %p: state:%p, isn:%u/win:%u/has_ts:%s/tsval:%u", ssn, q, q->seq, q->win,
+            BOOL2STR(q->flags & STREAMTCP_QUEUE_FLAG_TS), q->ts);
+}
+
+static void TcpStateQueueInitFromPktSyn(const Packet *p, TcpStateQueue *q)
+{
+#if defined(DEBUG_VALIDATION) || defined(DEBUG)
+    const TcpSession *ssn = p->flow->protoctx;
+    BUG_ON(ssn->state != TCP_SYN_SENT);
+#endif
+    memset(q, 0, sizeof(*q));
+
+    q->win = TCP_GET_WINDOW(p);
+    q->pkt_ts = p->ts.tv_sec;
+
+    if (TCP_GET_SACKOK(p) == 1) {
+        q->flags |= STREAMTCP_QUEUE_FLAG_SACK;
+    }
+    if (TCP_HAS_WSCALE(p)) {
+        q->flags |= STREAMTCP_QUEUE_FLAG_WS;
+        q->wscale = TCP_GET_WSCALE(p);
+    }
+    if (TCP_HAS_TS(p)) {
+        q->flags |= STREAMTCP_QUEUE_FLAG_TS;
+        q->ts = TCP_GET_TSVAL(p);
+    }
+
+#if defined(DEBUG)
+    SCLogDebug("ssn %p: state:%p, isn:%u/win:%u/has_ts:%s/tsval:%u", ssn, q, q->seq, q->win,
+            BOOL2STR(q->flags & STREAMTCP_QUEUE_FLAG_TS), q->ts);
+#endif
+}
+
+static void TcpStateQueueInitFromPktSynAck(const Packet *p, TcpStateQueue *q)
+{
+#if defined(DEBUG_VALIDATION) || defined(DEBUG)
+    const TcpSession *ssn = p->flow->protoctx;
+    BUG_ON(ssn->state != TCP_SYN_SENT);
+#endif
+    memset(q, 0, sizeof(*q));
+
+    q->win = TCP_GET_WINDOW(p);
+    q->pkt_ts = p->ts.tv_sec;
+
+    if (TCP_GET_SACKOK(p) == 1) {
+        q->flags |= STREAMTCP_QUEUE_FLAG_SACK;
+    }
+    if (TCP_HAS_WSCALE(p)) {
+        q->flags |= STREAMTCP_QUEUE_FLAG_WS;
+        q->wscale = TCP_GET_WSCALE(p);
+    }
+    if (TCP_HAS_TS(p)) {
+        q->flags |= STREAMTCP_QUEUE_FLAG_TS;
+        q->ts = TCP_GET_TSECR(p);
+    }
+
+#if defined(DEBUG)
+    SCLogDebug("ssn %p: state:%p, isn:%u/win:%u/has_ts:%s/tsval:%u", ssn, q, q->seq, q->win,
+            BOOL2STR(q->flags & STREAMTCP_QUEUE_FLAG_TS), q->ts);
+#endif
+}
+
+/** \internal
+ *  \brief Find the Queued SYN that is the same as this SYN/ACK
+ *  \retval q or NULL */
+static const TcpStateQueue *StreamTcp3whsFindSyn(const TcpSession *ssn, TcpStateQueue *s)
+{
+    SCLogDebug("ssn %p: search state:%p, isn:%u/win:%u/has_ts:%s/tsval:%u", ssn, s, s->seq, s->win,
+            BOOL2STR(s->flags & STREAMTCP_QUEUE_FLAG_TS), s->ts);
+
+    for (const TcpStateQueue *q = ssn->queue; q != NULL; q = q->next) {
+        SCLogDebug("ssn %p: queue state:%p, isn:%u/win:%u/has_ts:%s/tsval:%u", ssn, q, q->seq,
+                q->win, BOOL2STR(q->flags & STREAMTCP_QUEUE_FLAG_TS), q->ts);
+        if ((s->flags & STREAMTCP_QUEUE_FLAG_TS) == (q->flags & STREAMTCP_QUEUE_FLAG_TS) &&
+                s->ts == q->ts) {
+            return q;
+        }
+    }
+    return NULL;
+}
+
+/** \note the SEQ values *must* be the same */
+static int StreamTcp3whsStoreSyn(TcpSession *ssn, Packet *p)
+{
+    TcpStateQueue search;
+    TcpStateQueueInitFromSsnSyn(ssn, &search);
+
+    /* first see if this is already in our list */
+    if (ssn->queue != NULL && StreamTcp3whsFindSyn(ssn, &search) != NULL)
+        return 0;
+
+    if (ssn->queue_len == stream_config.max_synack_queued) { // TODO
+        SCLogDebug("ssn %p: =~ SYN queue limit reached", ssn);
+        StreamTcpSetEvent(p, STREAM_3WHS_SYN_FLOOD);
+        return -1;
+    }
+
+    if (StreamTcpCheckMemcap((uint32_t)sizeof(TcpStateQueue)) == 0) {
+        SCLogDebug("ssn %p: =~ SYN queue failed: stream memcap reached", ssn);
+        return -1;
+    }
+
+    TcpStateQueue *q = SCCalloc(1, sizeof(*q));
+    if (unlikely(q == NULL)) {
+        SCLogDebug("ssn %p: =~ SYN queue failed: alloc failed", ssn);
+        return -1;
+    }
+    StreamTcpIncrMemuse((uint64_t)sizeof(TcpStateQueue));
+
+    *q = search;
+    /* put in list */
+    q->next = ssn->queue;
+    ssn->queue = q;
+    ssn->queue_len++;
+    return 0;
+}
+
+static inline void StreamTcp3whsStoreSynApplyToSsn(TcpSession *ssn, const TcpStateQueue *q)
+{
+    if (q->flags & STREAMTCP_QUEUE_FLAG_TS) {
+        ssn->client.last_pkt_ts = q->pkt_ts;
+        ssn->client.last_ts = q->ts;
+        ssn->client.flags |= STREAMTCP_STREAM_FLAG_TIMESTAMP;
+        SCLogDebug("ssn: %p client.last_ts updated to %u", ssn, ssn->client.last_ts);
+    }
+    if (q->flags & STREAMTCP_QUEUE_FLAG_WS) {
+        ssn->flags |= STREAMTCP_FLAG_SERVER_WSCALE;
+        ssn->server.wscale = q->wscale;
+    } else {
+        ssn->flags &= STREAMTCP_FLAG_SERVER_WSCALE;
+        ssn->server.wscale = 0;
+    }
+    ssn->server.window = q->win;
+
+    if (q->flags & STREAMTCP_QUEUE_FLAG_SACK) {
+        ssn->flags |= STREAMTCP_FLAG_CLIENT_SACKOK;
+    } else {
+        ssn->flags &= ~STREAMTCP_FLAG_CLIENT_SACKOK;
+    }
+}
+
 /**
  *  \brief  Function to handle the TCP_SYN_SENT state. The function handles
  *          SYN, SYN/ACK, RST packets and correspondingly changes the connection
@@ -1477,41 +1641,10 @@ static int StreamTcpPacketStateSynSent(ThreadVars *tv, Packet *p,
     if (ssn == NULL)
         return -1;
 
-    SCLogDebug("ssn %p: pkt received: %s", ssn, PKT_IS_TOCLIENT(p) ?
-               "toclient":"toserver");
-
-    /* check for bad responses */
-    if (StateSynSentValidateTimestamp(ssn, p) == false) {
-        StreamTcpSetEvent(p, STREAM_PKT_INVALID_TIMESTAMP);
-        return -1;
-    }
-
-    /* RST */
-    if (p->tcph->th_flags & TH_RST) {
-        if (!StreamTcpValidateRst(ssn, p))
-            return -1;
-
-        if (PKT_IS_TOSERVER(p)) {
-            if (SEQ_EQ(TCP_GET_SEQ(p), ssn->client.isn) &&
-                    SEQ_EQ(TCP_GET_WINDOW(p), 0) &&
-                    SEQ_EQ(TCP_GET_ACK(p), (ssn->client.isn + 1)))
-            {
-                SCLogDebug("ssn->server.flags |= STREAMTCP_STREAM_FLAG_RST_RECV");
-                ssn->server.flags |= STREAMTCP_STREAM_FLAG_RST_RECV;
-                StreamTcpCloseSsnWithReset(p, ssn);
-            }
-        } else {
-            ssn->client.flags |= STREAMTCP_STREAM_FLAG_RST_RECV;
-            SCLogDebug("ssn->client.flags |= STREAMTCP_STREAM_FLAG_RST_RECV");
-            StreamTcpCloseSsnWithReset(p, ssn);
-        }
-
-    /* FIN */
-    } else if (p->tcph->th_flags & TH_FIN) {
-        /** \todo */
+    SCLogDebug("ssn %p: pkt received: %s", ssn, PKT_IS_TOCLIENT(p) ? "toclient" : "toserver");
 
     /* SYN/ACK */
-    } else if ((p->tcph->th_flags & (TH_SYN|TH_ACK)) == (TH_SYN|TH_ACK)) {
+    if ((p->tcph->th_flags & (TH_SYN | TH_ACK)) == (TH_SYN | TH_ACK)) {
         if ((ssn->flags & STREAMTCP_FLAG_4WHS) && PKT_IS_TOSERVER(p)) {
             SCLogDebug("ssn %p: SYN/ACK received on 4WHS session", ssn);
 
@@ -1644,7 +1777,68 @@ static int StreamTcpPacketStateSynSent(ThreadVars *tv, Packet *p,
             ssn->flags |= STREAMTCP_FLAG_TCP_FAST_OPEN;
             StreamTcpPacketSetState(p, ssn, TCP_ESTABLISHED);
         }
+
+        const bool ts_mismatch = !StateSynSentValidateTimestamp(ssn, p);
+        if (ts_mismatch) {
+            SCLogDebug("ssn %p: ts_mismatch:%s", ssn, BOOL2STR(ts_mismatch));
+            if (ssn->queue) {
+                TcpStateQueue search;
+                TcpStateQueueInitFromPktSynAck(p, &search);
+
+                const TcpStateQueue *q = StreamTcp3whsFindSyn(ssn, &search);
+                if (q == NULL) {
+                    SCLogDebug("not found: mismatch");
+                    StreamTcpSetEvent(p, STREAM_PKT_INVALID_TIMESTAMP);
+                    return -1;
+                }
+                SCLogDebug("ssn %p: found queued SYN state:%p, isn:%u/win:%u/has_ts:%s/tsval:%u",
+                        ssn, q, q->seq, q->win, BOOL2STR(q->flags & STREAMTCP_QUEUE_FLAG_TS),
+                        q->ts);
+
+                StreamTcp3whsStoreSynApplyToSsn(ssn, q);
+
+            } else {
+                SCLogDebug("not found: no queue");
+                StreamTcpSetEvent(p, STREAM_PKT_INVALID_TIMESTAMP);
+                return -1;
+            }
+        }
+
+        /* clear ssn->queue on state change: TcpSession can be reused by SYN/ACK */
+        StreamTcp3wsFreeQueue(ssn);
+
         StreamTcp3whsSynAckUpdate(ssn, p, /* no queue override */NULL);
+        return 0;
+    }
+
+    /* check for bad responses */
+    if (StateSynSentValidateTimestamp(ssn, p) == false) {
+        StreamTcpSetEvent(p, STREAM_PKT_INVALID_TIMESTAMP);
+        return -1;
+    }
+
+    /* RST */
+    if (p->tcph->th_flags & TH_RST) {
+
+        if (!StreamTcpValidateRst(ssn, p))
+            return -1;
+
+        if (PKT_IS_TOSERVER(p)) {
+            if (SEQ_EQ(TCP_GET_SEQ(p), ssn->client.isn) && SEQ_EQ(TCP_GET_WINDOW(p), 0) &&
+                    SEQ_EQ(TCP_GET_ACK(p), (ssn->client.isn + 1))) {
+                SCLogDebug("ssn->server.flags |= STREAMTCP_STREAM_FLAG_RST_RECV");
+                ssn->server.flags |= STREAMTCP_STREAM_FLAG_RST_RECV;
+                StreamTcpCloseSsnWithReset(p, ssn);
+            }
+        } else {
+            ssn->client.flags |= STREAMTCP_STREAM_FLAG_RST_RECV;
+            SCLogDebug("ssn->client.flags |= STREAMTCP_STREAM_FLAG_RST_RECV");
+            StreamTcpCloseSsnWithReset(p, ssn);
+        }
+
+        /* FIN */
+    } else if (p->tcph->th_flags & TH_FIN) {
+        /** \todo */
 
     } else if (p->tcph->th_flags & TH_SYN) {
         SCLogDebug("ssn %p: SYN packet on state SYN_SENT... resent", ssn);
@@ -1710,27 +1904,23 @@ static int StreamTcpPacketStateSynSent(ThreadVars *tv, Packet *p,
                     ssn->client.isn, ssn->client.next_seq,
                     ssn->client.last_ack);
         } else if (PKT_IS_TOSERVER(p)) {
-            /*
-             * On retransmitted SYN packets, the timestamp value must be updated,
-             * to avoid dropping any SYN+ACK packets that respond to a retransmitted SYN
-             * with an updated timestamp in StateSynSentValidateTimestamp.
-             */
-            if ((ssn->client.flags & STREAMTCP_STREAM_FLAG_TIMESTAMP) && TCP_HAS_TS(p)) {
-                uint32_t ts_val = TCP_GET_TSVAL(p);
+            /* on a SYN resend we queue up the SYN's until a SYN/ACK moves the state
+             * to SYN_RECV. We update the ssn to the most recent, as it is most likely
+             * to be correct. */
 
-                // Check whether packets have been received in the correct order (only ever update)
-                if (ssn->client.last_ts < ts_val) {
-                    ssn->client.last_ts = ts_val;
-                    ssn->client.last_pkt_ts = p->ts.tv_sec;
-                }
+            TcpStateQueue syn_pkt, syn_ssn;
+            TcpStateQueueInitFromPktSyn(p, &syn_pkt);
+            TcpStateQueueInitFromSsnSyn(ssn, &syn_ssn);
 
-                SCLogDebug("ssn %p: Retransmitted SYN. Updated timestamp from packet %" PRIu64, ssn,
-                        p->pcap_cnt);
+            if (memcmp(&syn_pkt, &syn_ssn, sizeof(TcpStateQueue)) != 0) {
+                /* store the old session settings */
+                StreamTcp3whsStoreSyn(ssn, p);
+                SCLogDebug("ssn %p: Retransmitted SYN. Updating ssn from packet %" PRIu64
+                           ". Stored previous state",
+                        ssn, p->pcap_cnt);
             }
+            StreamTcp3whsStoreSynApplyToSsn(ssn, &syn_pkt);
         }
-
-        /** \todo check if it's correct or set event */
-
     } else if (p->tcph->th_flags & TH_ACK) {
         /* Handle the asynchronous stream, when we receive a  SYN packet
            and now istead of receving a SYN/ACK we receive a ACK from the