Skip to main content

Module gate

Module gate 

Source
Expand description

The SNI gate: which handshakes are allowed to reach quinn at all.

A UDP port that answers anything is a port a scan finds. quinn’s endpoint is conservative but not silent: a long-header packet naming a version it does not speak draws a Version Negotiation packet, an Initial whose Destination Connection ID is shorter than the eight bytes RFC 9000 §7.2 requires draws a CONNECTION_CLOSE, and a short-header packet for no live connection can draw a Stateless Reset. Each of those is a correct answer to a QUIC peer and an answer to a scanner as well.

This module puts a filter under quinn, at the socket, so that a datagram quinn would answer never reaches it. It is off unless [security] expected_sni names at least one host: an empty list is the shipped default and every datagram passes through untouched.

§What passes

With the gate open, a received datagram is judged by its first QUIC packet. One packet is enough to judge the whole datagram because RFC 9000 §12.2 says “Receivers MAY route based on the information in the first packet contained in a UDP datagram” and, in the same paragraph, “Senders MUST NOT coalesce QUIC packets with different connection IDs into a single UDP datagram”:

  • Short header — passes. It belongs to a connection this endpoint may already hold, and the gate keeps no connection state to judge it with; see What is left uncovered below.
  • Long header, version not 1 — refused, whatever the version is. What that removes depends on the version. quinn’s default EndpointConfig, which crate::quic::Server::bind takes unchanged, supports seven: version 1 and the six draft numbers 0xff00001d to 0xff000022 (quinn-proto’s DEFAULT_SUPPORTED_VERSIONS). For a version outside that list quinn would send a Version Negotiation packet, and refusing removes the reply. For one of the six drafts quinn would open a connection, and refusing removes the service. Nothing this server interoperates with is affected, because Surge speaks version 1.
  • Long header, not an Initial — passes. quinn drops a Handshake or 0-RTT packet for an unknown connection without a word.
  • An Initial in a datagram below 1200 bytes — passes, because RFC 9000 §14.1 has the server discard it and quinn does exactly that, silently.
  • An Initial whose Destination Connection ID is shorter than eight bytes — refused, on the header alone and whether or not the packet authenticates. RFC 9000 §7.2 gives a client’s first Initial a floor of eight bytes. Every later Initial of an admitted handshake is addressed by the eight bytes this endpoint chose, and so is the one a Retry supplies, so nothing legitimate is this shape. quinn’s own rule is ahead of its decryption as well, in early_validate_first_packet, and it answers this shape with the CONNECTION_CLOSE named above before it reads a frame.
  • An Initial this server cannot open — passes. Only a client’s first Initial packets are keyed by their own Destination Connection ID: once the server has answered, the client addresses it by the connection ID the server chose (RFC 9000 §7.2) while the Initial keys stay the ones derived from the first packet (RFC 9001 §5.2), so the acknowledgements and later fragments of a handshake already admitted here cannot be opened without its state. They belong to a flight that was judged when it started; a forgery that is not one of them fails quinn’s own decryption and is dropped there without a reply.
  • An Initial with no CRYPTO frame at offset 0 — passes. It is an acknowledgement or a later fragment of a handshake already in progress, and the flight it belongs to was judged when it started.
  • An Initial whose ClientHello is cut short before its extensions — passes. A ClientHello may span several Initial packets, and a gate that guessed here would refuse a legitimate client whose first flight is large; the certificate resolver in crate::tls is the second gate that catches what this one lets through.
  • An Initial carrying a complete ClientHello — refused unless its server_name extension (RFC 6066 §3) names a host in the configured list.

§How a datagram is refused

Not by removing it from the batch. A single recvmmsg buffer can hold a whole GRO run of datagrams described by one stride, and cutting one out of the middle would mean rewriting the run. Instead the refused datagram is left in place with its Destination Connection ID length byte overwritten by a value no connection ID may have (RFC 9000 §17.2 caps it at 20 bytes), which is a header quinn’s own decoder rejects before it looks at the version, the type or anything else — and rejects by returning, not by answering.

The obvious alternative, clearing the fixed bit, does not work here: quinn accepts either value for it unless EndpointConfig::grease_quic_bit is turned off, and it defaults to on.

§What is left uncovered

An Initial the gate passes because it can read no name out of it still reaches quinn, and quinn acknowledges it if anything in it is ack-eliciting: a PING and nothing else, a ClientHello cut short before its extensions, a lone CRYPTO frame at a non-zero offset. Each draws an acknowledgement with no name anywhere in it. The second and the third are the fail-open branch above, and refusing them is refusing the large first flight this module deliberately lets through; the first could be refused on its own, and doing so would leave the other two answering, so it would change nothing about what somebody able to build an Initial packet can learn here. All three cost that somebody a QUIC v1 Initial keyed the way RFC 9001 §5.2 says, which is a probe aimed at this server rather than the scan of an address the gate is for.

A short-header packet for a connection this endpoint does not hold can still draw a Stateless Reset (RFC 9000 §10.3), and this module deliberately does not try to stop it: telling a live connection’s packets from a stranger’s needs the set of live connection IDs, which lives inside quinn, and filtering on the four-tuple instead would break the connection migration a mobile client behind a relay’s NAT depends on. What it costs is small enough to state exactly: quinn answers only a packet of at least 22 bytes whose first two bits are 01, and only when the eight bytes after them pass the endpoint’s own keyed connection-ID check, which random payload does with probability 2^-40. A scanner’s empty or fixed probe gets nothing.

Nor is any of this traffic obfuscation. The gate hides that a service is here from somebody who does not know the name to ask for; it does nothing about what a connection looks like once one is open.

Structs§

Expected
The list the gate is enforcing right now.
Gate
A UDP socket that hands quinn only the datagrams the gate admits.
Names
The hosts a handshake may name, compared the way a name is compared.