Project

General

Profile

Actions

Bug #9232

open
JS OD

SNI not evaluated for a second QUIC connection that reuses the UDP source port

Bug #9232: SNI not evaluated for a second QUIC connection that reuses the UDP source port

Added by Jaspreet Singh 6 days ago. Updated 3 days ago.

Status:
New
Priority:
Normal
Assignee:
Target version:
Affected Versions:
Effort:
low
Difficulty:
Label:

Description

SNI not evaluated for a second QUIC connection that reuses the UDP source port (same flow)

Summary

In QUIC parsing, once Suricata has seen the first ClientHello on a flow it stops inspecting that flow's client Initials for the rest of the flow's life. When a client opens a second QUIC connection from the same UDP source port (so the packets land on the same flow / 4-tuple), the second connection's ClientHello is never parsed and its quic.sni is never evaluated, even though that second connection has a different DCID and its Initial is independently decryptable.

Net effect: a quic.sni rule that should match the second connection does not fire. There is no alert, no anomaly event, and no decrypt-failure event for the second connection. An observer can hide a flagged SNI behind an earlier, benign connection simply by reusing the source port.

Steps to reproduce

Three client to server QUIC connections to the same server, captured on the server side. Each ClientHello carries a distinct DCID:

conn SNI UDP src port
1 allowed.example 45555
2 blocked.example 45555 (reused)
3 blocked.example 45556

Rule:

alert quic any any -> any 443 (msg:"blocked sni"; quic.sni; content:"blocked.example"; sid:1000001; rev:1;)

Run offline against the capture (-k none because actions do not apply to offline pcaps; stock config, QUIC app-layer enabled by default):

suricata -c suricata.yaml -S test.rules -r reuse.pcap -l out -k none

Expected behaviour

Two alerts for sid:1000001, one for conn 2 (blocked.example on src port 45555) and one for conn 3 (blocked.example on src port 45556):

  • event_type: alert, alert.signature_id: 1000001, src_port: 45555
  • event_type: alert, alert.signature_id: 1000001, src_port: 45556
  • a quic event for src port 45555 recording quic.sni: "blocked.example"

Actual behaviour

Only one alert fires, for conn 3 (src port 45556). Conn 2 on the reused port is not evaluated:

  • sid:1000001 fires once, only src_port: 45556.
  • For src_port: 45555 the only quic records are conn 1's quic.sni: "allowed.example" and a later record with quic.sni: null; the blocked.example ClientHello on the reused port is never parsed.
  • Both connections on 45555 are folded into a single flow (one flow record).
  • No alert, no anomaly event, and no FailedDecrypt/ErrorOnData event for conn 2.

The two connections differ only in that conn 2 reuses conn 1's source port. The second connection does not need to wait for the first to close; the client controls the source port and can start the second connection on it at any time.

Controls (to show the result is sound)

  • Blocked SNI alone on a fresh port: 1 alert (detection works).
  • Blocked then allowed on one port: 1 alert, on the blocked connection.
  • The same reuse capture, but with the second connection shifted past the UDP flow timeout (default flow-timeouts.udp established: 300; shifted +600 s so the first flow is aged out): 2 alerts. The alert returns once the connections are on separate flows, which pins the cause to flow reuse.
  • Allowed SNI twice on one port: 0 alerts (no false positives).
  • A retransmitted ClientHello (same DCID, duplicated Initial): still 1 alert (no duplicate), i.e. the gap is specific to a new connection on the reused flow, not to any repeated Initial.

Root cause

rust/src/quic/quic.rs, QuicState::parse():

1. Once the first ClientHello is seen, hello_ts / hello_tc is set, and the next client packet returns early:

if (to_server && self.hello_ts) || (!to_server && self.hello_tc) {
    // payload is encrypted, stop parsing here
    return true;
}

so a later ClientHello on the same flow is never parsed.

2. Initial keys are derived only once:

if self.keys.is_none() && header.ty == QuicType::Initial {
    self.keys = quic_keys_initial(u32::from(header.version), &header.dcid);
}

The second connection has a different DCID, so the retained keys would not decrypt it even if it were reached.

There is already a precedent in the same function for resetting handshake state: the Retry branch (8.0.x) sets hello_ts = false; keys = None so the client's re-sent Initial is parsed. A new client DCID on an existing flow could be handled the same way.

Affected versions

Reproduced on 7.0.8 and 8.0.6


Related issues 1 (1 open — 0 closed)

Related to Suricata - Bug #8776: quic: SNI detection evasion when a new connection reuses an existing UDP five-tupleIn ReviewActions

JI Updated by Jason Ish 3 days ago Actions #1

  • Assignee set to OISF Dev

JI Updated by Jason Ish 3 days ago Actions #2

  • Related to Bug #8776: quic: SNI detection evasion when a new connection reuses an existing UDP five-tuple added

VJ Updated by Victor Julien 3 days ago Actions #3

Is this a duplicate of #8776 or is it a similar but different issue?

Actions

Also available in: PDF Atom