Repository navigation
Conversation
The watch's one-shot read, the widgets' /v1/now, /healthz and /source now also answer over HTTP/3, with the same certificate and handler. The sockets, /v1/box and /v1/app, stay WebSockets over TCP. - internal/quicdoor accepts QUIC on the listen address over UDP and hands each connection to the protocol its ALPN named; today only h3. 0-RTT is off, extended CONNECT never reaches a route, and nothing logs. - TCP responses carry Alt-Svc: h3=":443"; ma=86400, except a WebSocket upgrade's. - r.TLS over HTTP/3 is the service's own TLS 1.3 session: its exporter is the client's (TestTheExporterOverHTTP3IsTheClientsOwn). - ftwcloudd health checks /healthz over TCP and HTTP/3; compose maps 443/udp and runs it as the container's health check. - deploy/http3.md: the security group's UDP 443, the order, turning it off, and the UDP buffer sysctl bootstrap.sh now sets. - scripts/netem-once.sh times a one-shot read over HTTP/1.1, HTTP/2 and HTTP/3 under netem. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A box dials ftw-cloud-box/1 and a phone ftw-cloud-app/1 on UDP 443, beside HTTP/3 on the same port. internal/quicdoor carries the WebSocket's messages on a control stream that starts with the target, a bulk stream each way for the tunnel's lane 1, and QUIC DATAGRAMs for hb and live without sources (srcfl/ftw docs/protocol-cloud.md, "QUIC"). - internal/wire is what the handlers need of a connection; a WebSocket is one as it is. uplink.ServeConn and appsession.ServeConn are the hooks the door calls. - The uplink refuses a datagram that is not newest-wins and anything but tun on the bulk stream, and drops an hb or live older than one it holds. - store.Session.Tun carries whether a tun came as bulk, so the phone's goes on its bulk stream too. - A refusal crosses as the close code; a phone that moves to a new address keeps its box session with no handshake. - -quic-sessions turns the sessions on, beside -h3. go/protocol pinned to srcfl/ftw@ec122827 (feat/cloud-quic). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The door now takes early data. A phone's Noise message 1 in early data is answered once per ephemeral key; a second copy waits for a handshake its sender cannot finish. Tickets allow early data for a day. HTTP/3 requests wait for the handshake. A box sends only its target early. TestAReplayedEarlyFlightChangesNothing records a phone's first flight (two Initials with the ClientHello, and the 0-RTT packet with message 1 and the box session's message 1), replays it later from another address, and shows the door holds it back, the box hears no handshake and no watch, and the phone's live session goes on. quicdoor.Dial sends early data with a ticket and sends it again when the service rejects it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ch for each Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
quic-go packs DATAGRAM frames ahead of stream data, so with a snapshot that spans packets an hb could reach a phone before message 2, and the phone read it as message 2: one failed reconnect in 30 under netem. The service now sends a phone everything on the control stream until the phone's first transport message shows it has message 2. The door also refuses a bad home in the target at once, before it waits for message 1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 137f261651
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if !c.bulkIn || gotBulk { | ||
| c.closeWith(cloudwire.CloseProtocol, "") | ||
| return |
There was a problem hiding this comment.
Accept the phone's inbound bulk stream
A phone connection is constructed with bulkIn == false, so this branch closes it as soon as the phone sends a lane-1 tunnel message on its QUIC bulk stream. The new protocol advertises a bulk stream each way, but only box-to-phone bulk currently works; phone-to-box bulk traffic therefore terminates the app session instead of reaching the box, and the app handler would also need to accept and preserve wire.Bulk for tun messages.
Useful? React with 👍 / 👎.
| select { | ||
| case <-c.resumed: | ||
| continue | ||
| case <-ctx.Done(): | ||
| return |
There was a problem hiding this comment.
Keep reading after rejected early data
When the server rejects 0-RTT—for example after the ticket's early-data window expires—the original QUIC connection context is canceled with Err0RTTRejected, so this select can return on ctx.Done() instead of waiting for afterEarly to install and replay onto NextConnection. The control reader then exits without reporting an error, leaving the resumed app session unable to receive its response and eventually timing out; wait for the replacement signal and bind subsequent readers to the replacement connection rather than the original context.
Useful? React with 👍 / 👎.
A restart or crash sends a QUIC client nothing, so it waited out its idle timeout before dialling again, where a TCP client hears from the kernel at once. ftwcloudd now keeps a reset key in quic-reset.key in the state directory, and the next process resets an old connection's next packet. The test ends the old connection in about 10 ms with the key; without it the connection still stands after 3 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Serve closed the transport when its context ended, and quic-go destroys a transport's connections without a word, so every QUIC box and phone waited out its idle timeout after a restart. Now SIGTERM only stops the door taking connections. Door.Close, on the way out of main, ends each live session with 1001 and then closes the transport, and a drain that closes them first with its own code fits between the two. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
QUIC, steps 2 and 3, the service's side:
/v1/boxand/v1/appover QUIC on UDP 443, with the WebSockets' handlers, and 0-RTT that a replay cannot use. It is stacked on #16 (HTTP/3) and paired with srcfl/ftw#1653 (protocol and box) and srcfl/ftw-app#74 (iPhone).go/protocolis pinned to srcfl/ftwec122827onfeat/cloud-quic; I move the pin to the merge commit once srcfl/ftw#1653 lands.What changes
internal/quicdoornow also takesftw-cloud-box/1andftw-cloud-app/1on the same UDP port as HTTP/3.Conncarries the WebSockets' messages:hband a table-lessliveas datagrams.QUIC's keepalive and idle timeout stand in for the ping. Close codes are the QUIC application error. A second stream of either kind closes the connection with 4400.
internal/wireis the WebSocket's method set, which a*websocket.Connalready has. The hooks in the packages cloud-scale owns are small:uplink.ServeConnandappsession.ServeConn;conn wire.Connin place of*websocket.Conn;parseUplinktakes the new kinds;store.Session.Tun()carriesBulk, so a bulktunfrom the box goes on the phone's bulk stream.I told that session.
The uplink closes on a datagram that is not newest-wins, and on anything but
tunon the bulk stream. It drops anhborlivewhoseseqis not above the newest of its class on the connection.A phone gets everything on the control stream until its first transport message shows that it holds message 2.
0-RTT.
WrapSession).EarlyRepeatscounts them).-quic-sessions(andFTW_CLOUD_QUIC_SESSIONSin compose) turns the sessions off without a release, beside-h3.deploy/http3.mdcovers both. The security group change from HTTP/3 for the plain HTTPS routes, on UDP 443 beside TCP (QUIC step 1) #16 is all the sessions need.Restarts. quic-go destroys a closed transport's connections without a word, so a restart would leave every QUIC box and phone to its 45 s idle timeout. Now SIGTERM only stops the door taking connections, and
Door.Close, on the way out ofmain, ends each live session with 1001 before it closes the transport. feat: a seam for shards, a quicker hot path, metrics, deploys without a storm, and a load harness #17's drain, which closes them first with 4503 and a spread, goes between the two. From HTTP/3 for the plain HTTPS routes, on UDP 443 beside TCP (QUIC step 1) #16, a stateless reset key in/statecovers a crash: the next process resets an old connection's next packet.The tests that show it
TestACommandCrossesQUIC: a command over QUIC on both legs, and over QUIC on one leg with the WebSocket on the other.TestBulkTravelsOnTheBulkStream,TestHbAndLiveTravelAsDatagrams(with "nothing but the stream before the phone speaks"),TestALateDatagramIsDropped.TestHelloOverQUICIsBoundToItsTLSSession: hello carries the exporter of the TLS session inside QUIC; one from another session gets nohello_ok.TestARefusalOverQUICCarriesItsCode: 4403 and 4400 cross as application errors.TestThePhoneSessionSurvivesANetworkSwitch: mid-session the phone moves to a new UDP socket, a new source address, by probing and switching the path. The next command gets its ack and result. The box runs no new handshake, and the local address is the new one.TestCloseEndsEachSessionWithGoingAway: after the context ends, the door takes no new connection and the live session still carries messages;Closethen ends it with 1001.TestARestartResetsTheOldProcesssConnections(HTTP/3 for the plain HTTPS routes, on UDP 443 beside TCP (QUIC step 1) #16): the old connection ends about 10 ms after the restart with the key, and still stands after 3 s without it.TestAReplayedEarlyFlightChangesNothing:Used0RTT, and the command works.watch, and the live session's next command works.Numbers: today's WebSocket against QUIC
A real
ftwcloudd, a box running Core's owncloudlink,appuplinkand command handler, and phones that pair with it and speak the app protocol inside the tunnel. The WebSocket rows are today's path on both legs (box and phone); the QUIC rows are QUIC on both. Everything runs in one Linux container whose loopback carries netem: 20 ms each way, so every leg has a 40 ms round trip and the loss applies to each packet on both legs. The readable copy is on. 30 connects and 30 taps per row, 90 s of frames.What it says: QUIC saves round trips where a connection starts, and it keeps loss on one stream from holding up the others. Cold, it is two round trips less (the TCP handshake and the WebSocket upgrade). On a reconnect, 0-RTT saves one more. Under loss the tails shrink everywhere, and a network switch costs no new session. With no loss, a frame or a tap takes one round trip either way, as physics says.
now.tun), the box session's handshake andhellowith its subscription, to the snapshot.cmd.ack.live, from the box'swallMsto the phone. Over QUIC it goes as a datagram; the counts say how many of the 90 the box sent arrived. A lost one shows as a gap in the freshness rules, never as a live value.On the phone: FTWKit in the iOS simulator
LiveQUICTests(in srcfl/ftw-app#74) runs the app's own stack (AppModel,PairModel,CloudCarrier, the tunnel and the session) against a realftwclouddand a real Core box with the relay unreachable. It runs in an iPhone 17 simulator. The service and the box run in a container whose netem shapes only the phone's leg, 20 ms each way plus OrbStack's own few ms (ping 56–100 ms); the box reaches the cloud unshaped. The readable copy is off. Each row is one pairing, 30 taps and 6 returns from the background.The Mac's own
swift testgave the same at 40 ms: 305 against 180 ms from scan to stream, and 269 against 184 ms back from the background. In an earlier 5 % run the race gave one session to the WebSocket, because one lost QUIC packet made QUIC miss its 250 ms lead. A QUIC dial that loses now goes on to learn, so that one loss no longer keeps QUIC second for a day.Keeping an idle box linked
The box uplink stayed idle for 180 s with the readable copy off, at 40 ms, counted packet by packet on the loopback (IP bytes):
In the first two minutes, both ends also send path MTU probes of about 1.4 KB beside each keepalive. Over the 180 s window that came to 10 KB, a rate of 4.9 MB a day that stops once discovery ends. A PING every 15 s keeps a 30 s NAT mapping open.
Not checked
localhostand a pinned root (DidResumefalse), so the iPhone's reconnect is a full handshake here. The box and the Go phone use 0-RTT. The next try is a device, or the simulator with the root in its trust store.The rig is not in any repository. It is the box as a test binary in Core's
appuplink(with Core's own stubs), a phone in Go overgo/protocolandquicdoor.Dial,run.sh, and netem in an Alpine container on OrbStack. Ask, and I will put it in a PR as a reproducible check.Checks
go vet ./...,go test -race ./...,go test -tags ftwclouddev ./...,scripts/check-imports.sh: pass on a laptop. The replay test passed 5 of 5 under-race, and the network switch 3 of 3.TestPastTheTunLimitsTheServiceDrops(uplink, untouched) failed once in 10 under load and passed 10 of 10 alone. Its writes block on the test box, so the bucket's refill races the burst. It takes 10 s onmaintoo.Size
About 1,110 lines of service Go beside #16, about 1,375 with it.
quicdooris 1,060 lines in all; about 150 of them are the client side that tests and measurements dial with.wireis 65, and the hooks are the rest. The service was past its 3,000-line budget before this.🤖 Generated with Claude Code