Developer’s Corner: QUIC Kista Interim Day 1
June 6, 2018
The IETF QUIC Working Group has come together for another Interim meeting. This time, the meeting sponsor is Ericsson: they are providing a conference room and networking at their office in Kista, Sweden. I am participating remotely, which means that I woke up at 3 o’clock this morning to be ready for the 9:30 am meeting held across the ocean.
Every item on the agenda is pressing, as the Working Group is trying to make the November deadline. The way QUIC transport layer interacts with the TLS stack is changing. The HTTP/QUIC draft is still quite raw (if it were a steak, you’d send it back). We are trying to reach consensus on as many issues as possible and nail down more stuff than we will add this time around, thereby making progress.
Martin Thomson’s report was optimistic. Since our last meeting at the IETF 101, the editors closed many issues in GitHub and marked others v2. The latter means that these issues are postponed until the second version of IETF QUIC, whenever that turns out to be.
Stream 0 Redesign
Eric “EKR” Recorla’s DTLS presentation at the IETF 101 created a lot of discussion. EKR showed that the current QUIC drafts have a fundamental flaw: the transport part of QUIC depends on the crypto layer and the crypto layer depends on the QUIC transport. This makes several things quite painful. Eric’s own DTLS proposal, while not accepted by the Working Group at the time, resulted in the Chairs (Mark Nottingham and Lars Eggert) empaneling a special Stream 0 Design Team. This team, led by EKR and Ian Swett, worked out a novel, consensus approach to address the stream 0 mess.
Bye, Stream 0!
Remember the “CREAMs and CRACKs?” The new design uses dedicated frame types (instead of a special QUIC stream) to carry handshake messages. This allows for significant layering simplification, going from this:
(Both diagrams are borrowed from the design document linked above.)
Besides the new CRYPTO_HS frame type (I guess they did not have the chutzpah to call the new type CREAM), we see that the TLS record layer is missing. This is the novel part of the design. Since QUIC frames are themselves a type of a record — in the sense that they have boundaries — the TLS record structure can be skipped and TLS messages are carried directly in the new QUIC frames.
The risk of this approach is dependency on TLS libraries exposing a new API to be able to access TLS messages outside of records. The good news is that OpenSSL and BoringSSL have agreed to accommodate us, while a few smaller implementations have already added such support.
Packet Number Spaces
Another interesting part of the Design Team’s proposal is that each encryption level (there are three) gets its own packet numbers. This is something new and was met with scepticism by some.
The proposal — or rather, its direction — was adopted with strong support, modulo the packet number discussion, which is to be revisited.
The state of HTTP over QUIC Internet Draft was presented by Mike Bishop, the draft’s author. Mike covered three topics: changes to HTTP frame types, self-describing streams, and stream priority trees.
Frame Type Changes
Out of all HTTP frame types in HTTP over QUIC (HQ), only the PRIORITY frame uses flags. Thus, the flags field is removed from the generic HTTP frame.
HQ uses three special unidirectional streams:
- Control stream;
- QPACK encoder stream; and
- QPACK decoder stream.
Multiply this by two (client and server) and you get six streams that an HQ implementation deals with in a special manner. Mike’s proposal is to have unidirectional streams in HQ have a leading type byte. Based on its value, the application will know what stream type it is: a control stream, a decoder stream, and so on. This would make it, in the presenter’s own words, simpler. This is to satisfy the principle that
Application layer shouldn’t need to “grab” a particular stream by ID.
I don’t know whence comes this principle. It sounds rather meritless to me. I had to speak up. My argument against this proposal went as follows.
The HQ layer installs different stream handlers based on stream type. The control stream does one thing, the encoder stream another, while the regular HTTP message streams are handled in yet another way. A special stream ID — 2, 3, 6, 7, 10, or 11 — allows to initialize a stream with the correct handler as soon as the first stream frame arrives. On the other hand, having to examine the first byte to initialize the stream forces one to handle the case when the first STREAM frame for a particular stream does not have the first byte. In other words, a stream is instantiated, but has to be finalized later. This is certainly not simpler.
Martin Thomson replied that it made his code simpler. I don’t know about Martin’s code, but it would not make our code simpler. A bit later in the discussion, I offered the following analogy:
Think of a networked computer. Port 80 is HTTP; port 22 is SSH; and so on. The kernel does not examine the first byte of an incoming TCP connection to see what service it wants. Instead, the application is allowed to use port numbers. TCP connections have ports; QUIC streams have IDs. We can use them.
The API problem Marten describes seems like a non-issue: There are two sets of APIs: - User code / HQ; and - HQ / QUIC. Stream IDs do not have to be exposed in the former: The user does not care about them, as these things are just your regular HTTP messages. In the HQ / QUIC API, on the other hand, expose away! Streams have IDs, the QUIC transport draft talks about them, the IDs have meanings -- it only makes sense that the layer that uses the QUIC objects directly can use Stream IDs. Why not?!
Why do some people high on the IETF QUIC totem pole advocate for not using stream IDs in HQ without giving a good technical reason? I can only guess. (A related issue — in the sense that it is similarly groundless — is the desire to mandate that streams be opened in sequence. This discussion is being kept alive and tomorrow will be revisited again. Watch this space.)
The “no-grab” principle above is fine and dandy, but QUIC streams do not have intrinsic types. Using the first payload byte as a substitute is a kludge.
Roberto Peon pointed out that control streams without fixed IDs would make debugging difficult. Certainly, the proposal’s momentum was slowed, but the consensus was to “continue discussing” it. I suppose it means I have to remain vigilant!
HQ has inherited the HTTP/2 stream priority model… and associated problems. The short version is that streams can depend on one another and just because a stream is finished does not mean that it can be removed from the dependency tree. In some scenarios, this could mean that the dependency tree keeps on growing. (Few HTTP/2 implementations do stream dependencies properly. See Are HTTP/2 Servers Ready Yet? Salient quote: “the priority mechanism has not been well designed and deployed.”)
A few ideas were tossed around:
- Is this really our problem? Since HQ uses HTTP/2 semantics, shouldn’t we keep priorities as-is?
- Should we drop support for priorities altogether?
- Should priorities only affect idle streams?
Ultimately, the Working Group resolved to come back to this issue later.
Alan Frindell, the editor of the QPACK draft, described what the current version of QPACK looks like. Other than being broken by design, QPACK is almost complete. So much so that the last slide of Alan’s presentation talked about an interop.
I objected to using framing on the QPACK encoder stream, as it renders some optimizations impossible. For example, why can the encoder not write to the packet directly? If one has to know the length, it means that the encoder instructions must first be buffered. The reason for framing, Alan explained, is to aid the decoder. Some HPACK decoders do not deal well with incomplete instructions; they do not know how to resume.
I think that this is not applicable in this case. The table update stream processed by a QPACK library only affects a single entity, the dynamic table, and can save the state easily. It is the processing of partial HEADERS block that HPACK decoders have historically had trouble with. These blocks are framed and carried on other, regular, streams. (I will likely present this argument to the QPACK editor and authors.)
While Alan proceeded to demonstrate a simple QPACK session walk-through, Martin Thomson filed a bug titled “Encoder stream can deadlock.” And so it can! Due to framing. Thanks, Martin.
The second part of the Interim debates is tomorrow. Stay tuned!