Skip to content

QUIC: bind more than one port per server #189

Description

@MDA2AV

What

QuicOptions.Port is a single ushort, so a server binds exactly one QUIC listener. Everything else about a server is multi-port — TcpOptions has ExtraPorts, UdpOptions.Ports is already an array — but HTTP/3 is limited to one endpoint.

Why it matters

It surfaced building the GenHTTP ioxide engine, where protocols are configured per port. Every combination works on one host except a second HTTP/3 port:

8080  Http1           ok
8081  Http2           ok
8082  Http1AndHttp2   ok
8443  All             ok   <- the only endpoint that can carry HTTP/3
8444  Http1AndHttp3   needs a second host
8445  Http2AndHttp3   needs a third
8446  Http3           needs a fourth

A second endpoint asking for HTTP/3 has to be refused at startup, so the engine throws and the sample either drops those combinations or runs a host per combination. More practically, it means one process cannot serve HTTP/3 for two sites on different ports, or run a public and an mTLS-protected QUIC endpoint side by side, without a server each.

Why it looks tractable

Most of the machinery is already multi-port:

  • UdpOptions.Ports is an array, and Reactor.Udp binds every one of them per reactor via SO_REUSEPORT.
  • The QUIC port is appended to that same list — "The QUIC port is a UDP socket like any other; only completion routing differs" (Reactor.Udp.cs).
  • UdpDatagram already carries LocalPort, so a datagram knows which endpoint it arrived on.
  • The connection factory is invoked at exactly one place, in QuicDispatchDatagram:
QuicConnection? freshQuicConnection = _quicOptions?.ConnectionFactory?.Invoke(this, in datagram, in dcid);

So the shape is roughly: QuicOptions carries several ports each with its own factory, OpenUdpSockets binds them all, and dispatch selects the factory by datagram.LocalPort. Existing CID routing is unaffected — locally-minted CIDs are already unique per engine, and short-header packets keep resolving through _quicConns without needing the port at all.

Design questions

  • One factory per port, or one for all of them? Per port is what makes this worth doing: endpoints differ by certificate, ALPN and client-CA, and each of those lives in a QuicEngine. A single shared factory would only let you vary behaviour by inspecting the datagram, which is the wrong seam.
  • LocalCidLength. Currently one value on QuicOptions, and the demux slices exactly that many bytes from short-header packets before it knows which endpoint the packet belongs to. So either every endpoint keeps the same CID length (simplest, and they all default to 8), or the demux has to resolve the port first. The first is probably right, with validation rather than silent breakage if they disagree.
  • Backwards compatibility. Port + ConnectionFactory can stay as the single-endpoint spelling and populate the map, so nothing existing changes.

Not urgent

There is a workaround — one server per HTTP/3 endpoint, which costs a set of reactors each and works fine. This is about making the QUIC side as multi-port as the rest of the transport already is.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions