ftwcloudd serves QUIC on UDP 443 beside TCP 443, with the same
certificate:
- HTTP/3 (
h3) for the routes that are plain HTTPS:/v1/app/once,/v1/now,/healthzand/source. TCP responses carryAlt-Svc: h3=":443"; ma=86400, except a WebSocket upgrade's. - The uplink (
ftw-cloud-box/1) and the app session (ftw-cloud-app/1), with the WebSockets' handlers and messages (srcfl/ftwdocs/protocol-cloud.md, "QUIC"). Boxes and phones race QUIC against the WebSocket and fall back to it on their own, so shutting UDP never cuts anyone off; it only makes them slower.
What it buys, measured by scripts/netem-once.sh (a watch's one-shot read,
40 ms round trip): a read on a new connection takes two round trips instead
of three. The pull request that added it has the table.
Open the port before the image that listens on it runs, and shut it after
the image stops saying so. A client that heard Alt-Svc races QUIC against
TCP for up to a day; with UDP shut it loses the race every time and gains
nothing.
In a terminal signed in to the FTW Cloud account (976075258475):
aws ec2 authorize-security-group-ingress --region eu-north-1 --group-id sg-098992ad382ecf94a \
--ip-permissions 'IpProtocol=udp,FromPort=443,ToPort=443,IpRanges=[{CidrIp=0.0.0.0/0,Description=HTTP/3}]' \
--tag-specifications 'ResourceType=security-group-rule,Tags=[{Key=Project,Value=ftw-cloud}]'If the TCP rule also has ::/0, add Ipv6Ranges=[{CidrIpv6=::/0,Description=HTTP/3}]
to match it. Check that the group now lets in 443/tcp and 443/udp and
nothing else:
aws ec2 describe-security-group-rules --region eu-north-1 \
--filters Name=group-id,Values=sg-098992ad382ecf94a \
--query 'SecurityGroupRules[?!IsEgress].[IpProtocol,FromPort,ToPort,CidrIpv4,CidrIpv6]' --output tableIt now raises the socket buffer ceiling to 7.5 MB, which quic-go asks for
(net.core.rmem_max and net.core.wmem_max). Without it HTTP/3 still works
and quic-go logs once that its buffers are small. Run it as under "Bringing
it up" in README.md.
Deploy the image built from the commit that added HTTP/3, or any later one,
as under "Updating" in README.md. Its compose.yml maps
443:443/udp. No flag is needed: HTTP/3 is on by default.
From any machine with Go and a checkout of this repository:
go run ./cmd/ftwcloudd health https://cloud.ftw.energy/healthzIt prints HTTP/1.1 ok and HTTP/3.0 ok, each with its time, and fails if
either fails. A failing HTTP/3 line from outside, with a healthy container,
means the security group or a network in between drops UDP. curl --http3
does the same where curl was built with HTTP/3 (macOS's own curl is not).
The container checks itself the same way every minute, against its own
port, and docker compose ps on the host shows healthy or unhealthy.
Nothing restarts on it; it is there to look at after a deploy.
Without a release: on the host, add FTW_CLOUD_H3=false,
FTW_CLOUD_QUIC_SESSIONS=false or both to /etc/ftw-cloud/compose.env and
run systemctl restart ftw-cloud.service. ftw-cloud-deploy keeps those
lines. With both off the service stops listening on UDP and stops sending
Alt-Svc. Wait a day, the ma in Alt-Svc, before you shut UDP 443 in
the security group:
aws ec2 revoke-security-group-ingress --region eu-north-1 --group-id sg-098992ad382ecf94a \
--ip-permissions 'IpProtocol=udp,FromPort=443,ToPort=443,IpRanges=[{CidrIp=0.0.0.0/0}]'- TLS still ends in
ftwcloudd. Over HTTP/3,r.TLSis the service's own TLS 1.3 session with the client, and its exporter is the client's (TestTheExporterOverHTTP3IsTheClientsOwn). A UDP load balancer that forwards packets is fine; anything that ends QUIC is not. - Restarts. When the process goes away, a TCP client hears from the
kernel, but a QUIC client hears nothing. So
ftwclouddkeeps a stateless reset key inquic-reset.keyin the state directory, and the next process answers an old connection's next packet with a reset: the client knows at once, not after its idle timeout (TestARestartResetsTheOldProcesssConnections). Keep the file with the rest of/state; a new key only costs one idle timeout per client. On SIGTERM the door stops taking connections, and on the way out it ends each live session with 1001, going away (TestCloseEndsEachSessionWithGoingAway). The drain, which closes them first with 4503 and a spread, goes between the two. - Floods. Past 200 new connections a second the door asks each to prove its address with a Retry, which costs a real client one round trip and a spoofed one everything. A handshake gets 5 s, and the door holds at most a set number of handshakes and connections at once. A UDP port that will not bind turns QUIC off and leaves TCP serving.
- 0-RTT. The sessions take early data under the protocol's rules: a
box's target may get its
challengebefore the handshake ends, and a phone's session waits for the handshake, so a replayed flight changes nothing. A ticket allows early data for a day. HTTP/3 requests in early data wait for the handshake. A restart draws new ticket keys. - A UDP load balancer in front would have to keep each QUIC connection on one host by its connection id; with one host there is none.
- No DNS record for HTTP/3. An
HTTPSrecord withalpn=h3would tell every client, the sockets' too, to try QUIC on the first connection.Alt-Svcreaches only clients of the plain routes. - Apple's URLSession speaks HTTP/3 to this service. With
assumesHTTP3Capableit tries HTTP/3 on the first request, and with UDP refused it fell back to TCP in the same time as without the flag (macOS, loopback; a network that drops UDP silently is not measured). A WebSocket in a session that already holds an HTTP/3 connection still opened over HTTP/1.1. On a port other than 443 URLSession did not move to HTTP/3 fromAlt-Svcalone. So the watch's direct path should setassumesHTTP3Capableand keep one URLSession for its requests. - Logs. Neither quic-go nor its HTTP/3 server logs anything here; both would name the peer's address.