-
Notifications
You must be signed in to change notification settings - Fork 17.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
x/net/quic: add QUIC implementation #58547
Comments
I generally like the use of Confusingly, it probably does not make sense for Edit: Random thought: Might it make sense to introduce new interfaces for the |
Author of quic-go here 👋 If there's interest from the Go team's side, I'd be happy to talk about what would be needed to get (parts of?) quic-go merged into the standard library. |
It probably does make sense for I don't think it's useful for anything in a QUIC implementation to implement If someone does need to use a |
Related: #32204 (net/http: support HTTP/3) |
Thank you for sharing this!
Does an endpoint that acts as a client need to set a certificate? If so, I'd guess that it's even OK to call
These methods don't allow using normal io packages in a Context-aware way; anything that takes an What are the tradeoffs of this versus a method like This revision of the API doesn't give visibility into flow control. How much is required to build a reliable QPACK implementation? From https://www.rfc-editor.org/rfc/rfc9204.html#section-2.1.3
Only because you mentioned future possibility of Early Data: Would Dialing with Early Data need a separate |
@neild I just want to make sure @marten-seemann's comment above was seen. Given the years of effort that have already been spent implementing and optimizing QUIC in Go, it would probably make sense to take advantage of that rather than start over from scratch, even if the exported APIs are a little different. Please consider using quic-go as the basis for this effort. |
@mholt quic-go never tried to integrate with stdlib net packages in a natural way. while I'm sure some of the internal structures might be reusable. the overall library itself wasn't terribly appealing to me personally due to the incompatibilities with the wider ecosystem. edit: as a result I strongly recommend against using quic-go as a base because of that decision. quic-go focused on getting quic http support available. golang's stdlib implementation should be focused on compatibility with the wide ecosystem at the transport level. the fundmental driving forces for the API design are very different. |
@paralin I think the adapter code all of them had to implement to handle quic-go speaks for itself. golang stdlib needs to figure out how to interopt at the transport level. similar to the packet conn vs stream conns it already has. if I have a stream oriented protocol I shouldn't care if I receive a quic, tcp, or unix transport. this is the problem we need to resolve which quic-go explicitly decided to ignore |
This is an active proposal and no decisions have been made as of yet. Please hold off on speculation about the implementation, and take conversations about quic-go elsewhere. |
@mdlayher agreed, but my points about the interopt in stdlib for quic stand. they're important even if we ignore quic-go. |
It is good for proposals to focus on API, but implementation is explicitly on topic at least for large proposals, given that it's one of the sections listed in the design doc template. As for quic-go, I completely agree that it would be good to take advantage of the expertise that @marten-seemann has built up over his years of development of quic-go. We would certainly welcome his help. At the same time, reusing quic-go directly is probably not the right path forward, for a few reasons:
For all these reasons, the path forward is almost certainly not to adopt quic-go directly. @marten-seemann, as I said before, we certainly appreciate your work implementing QUIC to date as well as the expertise you have amassed, and if you would be interested to share that with us in the development and review of a fresh implementation, you'd certainly be welcome. On the other hand, if you would rather focus on quic-go and not a different implementation, we'd understand that too. |
main things I'm interested in w/ a quic implementation are exposing ALPN and seamless interopt w/ other standard transports. aka shouldn't have to make extra calls to server http over a quic transport. just setup the quic listener and pass it to http.Serve. when the quic implementation gets to a workable state I'm 100% down to start using it in some of my applications and provide feedback on the API. |
@rsc, thank you for your detailed post. It seems like a decision has already been made, but nevertheless, here are my 2c. Happy to share some of my insights from almost 8 years of developing / maintaining a QUIC stack and from having been a member of the IETF QUIC working group since the very beginning. Building a performant QUIC stack is an absolutely massive endeavor. Getting the handshake to work is nothing more than a tiny first step. When we started the project, it only took us one or two weeks to download a small file from a quic-go server using Chrome via what was back then called H2/QUIC, and most of that time was spent on implementing the bespoke QUIC Crypto. This results in a spec-compliant QUIC stack, but in no way an optimized / performant one. You'd probably want to implement
Maybe not absolutely necessary, but highly desirable:
Aside from all of these features above, we've spent significant engineering efforts on performance optimizations (e.g. reducing the number of allocs) and DoS defense (you're keeping track of a lot of things, e.g. sent and received frames, sent and received packets, etc., and all of these data structures are potentially attackable). As I see it, there's little point in just providing a spec-compliant QUIC implementation, if it can't (at the very least) compete with TCP's performance. And performance work on quic-go is far from done at this point. Let me briefly comment on the points you made.
Not sure in what sense quic-go is too low-level. However, quic-go is still on a v0.x version, and we're happy to consider well-motivated API changes. That statement stands independent of the discussion on this issue, and we're happy about proposals how to make the quic-go API work better (please open an issue in quic-go, happy to discuss there!).
We've removed support for QUIC crypto a long time ago. The only compatibility code that we're still maintaining is for draft-29, which we're planning to remove some time in summer this year. The additional code for draft-29 is pretty limited to begin with (mostly just using different labels in the various HKDF expansions), and with one tiny exception doesn't leak beyond the handshake package. One thing I'm really happy about is finally cleaning up the API between quic-go and crypto/tls. I can't wait to start using the new API (I already have a branch). The API that my crypto/tls fork has accumulated over the years is indeed suboptimal (to say the least). Cleaning it up was always complicated by the fact that I had to maintain two separate forks (for the most recent 2 Go versions) at the same time. Other than that, I don't think there's any code around that only exists for historical reasons. While minimizing the LOC was never a design target (code clarity and testability was), I don't think there's a lot you can remove without removing features or sacrificing performance.
Indeed. In hindsight, that was a bad decision we made when we started the project. Migrating would probably take somewhere around 2 weeks of work to rewrite the tests. Pretty sure that this would still be orders of magnitude less work than rewriting an implementation from scratch.
All code has been reviewed by a Googler, @lucas-clemente (not on the Go team though). I don't mean to be nit-picky here, but it's just 24,000 LOC if you exclude tests (and 63,000 if you don't) (counted using I'd also like to point out that quic-go is widely used in production, for example by Caddy (using the HTTP/3 implementation it comes with) and accounts for ~80-90% of all connections in the IPFS network (using just the quic package, without HTTP/3). See here for a (very much incomplete) list of other projects that use it. It's also tested against a long list of other QUIC implementations using the QUIC Interop Runner which we built a few years ago to facilitate automated interop testing in the QUIC working group.
Happy to help, in one way or the other. You know where to find me :) |
No; fixed the documentation for
There are three types of API for cancellable read/write operations in common use that I know of:
I believe we need to support the first one for compatibility with I don't have a strong opinion about which of the latter two options is best ( Another possibility if #57928 is accepted (not strictly necessary, but necessary to implement this efficiently) might be:
This is an excellent question. I left flow control out of the initial proposal because I'm not completely satisfied with any of the ideas I've had so far. My current inclination is to have a per-stream configuration option that makes writes to the stream effectively atomic--a write will block until flow control is available to send the entire write, sending either the entire chunk of data or none of it.
I'd be interested to hear other ideas.
To close a connection with an error and wait for the peer to acknowledge:
I think you might be right that combining Abort and Close into a single
I think we can do Early Data almost entirely within the proposed API.
But I haven't tried to implement this, and might be missing something. |
Change https://go.dev/cl/475435 mentions this issue: |
Change https://go.dev/cl/468402 mentions this issue: |
Change https://go.dev/cl/475437 mentions this issue: |
Change https://go.dev/cl/475436 mentions this issue: |
Change https://go.dev/cl/475438 mentions this issue: |
To be clear: This will not work. QUIC is not TCP. A In addition, while it would be possible to run HTTP/1 over QUIC streams, nobody (so far as I know) does this. HTTP/3 uses QUIC as an underlying transport, but HTTP/3 is not just HTTP/1 with TCP swapped out for QUIC. |
Yes, The adaptor is simple to write in either direction (2 to 3 or 3 to 2). The It seems that API 1 can also be built into API 3, where the value that For what it's worth, I'd also expect multiple calls to I agree though that it's not clear which of these APIs is best. Most of all I'd like one that aligns with performance: where it's easy to implement and use in an efficient way and hard to end up with a bunch of great code that can't be made to go fast.
That sounds pretty simple to use, nice! If it allows writes that are larger than a single packet, and some pacing is active, and the user cancels the write via
IIUC there are three protocol-level limits that determine whether a particular STREAM frame is allowed on the wire: stream-level flow control, connection-level flow control, and connection-level congestion control (which seems closely related to any pacing in place). It looks like QPACK specifies a need for interacting with the first two. I expect it's unusual to need to interact with the third, but I have an application that takes advantage of visibility into that: it prioritizes which data to send, and sometimes whether to send any data at all, based on how soon it expects the QUIC stack will be able to send it to the peer. (Maybe some of that is better left to an integration with the packet scheduler.) I wonder if there's room for an API along the lines of https://pkg.go.dev/golang.org/x/time/rate#Limiter.ReserveN that gives an app a higher level of visibility into and control of those windows, through an API that returns a struct with methods that allow inspecting and manipulating (and canceling) the reservation. It's definitely an "other idea"; I don't know whether it's better than your
|
For golang/go#58547 Change-Id: I49a27ab82781c817511c6f7da0268529abc3f27f Reviewed-on: https://go-review.googlesource.com/c/net/+/564015 Reviewed-by: Jonathan Amsterdam <jba@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Log events for various congestion control and loss recovery metrics. For golang/go#58547 Change-Id: Ife3b3897f6ca731049c78b934a7123aa1ed4aee2 Reviewed-on: https://go-review.googlesource.com/c/net/+/564016 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Jonathan Amsterdam <jba@google.com>
Log unparsable or otherwise discarded packets. For golang/go#58547 Change-Id: Ief64174d91c93691bd524515aa6518e487543ced Reviewed-on: https://go-review.googlesource.com/c/net/+/564017 Reviewed-by: Jonathan Amsterdam <jba@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Change https://go.dev/cl/564477 mentions this issue: |
Change https://go.dev/cl/564476 mentions this issue: |
Change https://go.dev/cl/564475 mentions this issue: |
For golang/go#58547 Change-Id: Ie62fcf596bf020bda5a167f7a0d3d95bac9e591a Reviewed-on: https://go-review.googlesource.com/c/net/+/564475 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Jonathan Amsterdam <jba@google.com>
Change https://go.dev/cl/564495 mentions this issue: |
Change https://go.dev/cl/564496 mentions this issue: |
RFC 9000 recommends sending an ack for every second ack-eliciting packet received. This frequency is high enough to have a noticeable impact on performance. Follow the approach used by Google QUICHE: Ack every other packet for the first 100 packets, and then switch to acking every 10th packet. (Various other implementations also use a reduced ack frequency; see Custura et al., 2022.) For golang/go#58547 Change-Id: Idc7051cec23c279811030eb555bc49bb888d6795 Reviewed-on: https://go-review.googlesource.com/c/net/+/564476 Reviewed-by: Jonathan Amsterdam <jba@google.com> Auto-Submit: Damien Neil <dneil@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Currently unoptimized and slow. Adding along with a benchmark to compare to the fast-path followup. For golang/go#58547 Change-Id: If02b65e6e7cfc770d3f949e5fb9fbb9d8a765a90 Reviewed-on: https://go-review.googlesource.com/c/net/+/564477 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Jonathan Amsterdam <jba@google.com>
Change https://go.dev/cl/565255 mentions this issue: |
Keep a reference to the next chunk of bytes available for reading in an unsynchronized buffer. Read and ReadByte calls read from this buffer when possible, avoiding the need to lock the stream. This change makes it unnecessary to wrap a stream in a *bytes.Buffer when making small reads, at the expense of making reads concurrency-unsafe. Since the quic package is a low-level one and this lets us avoid an extra buffer in the HTTP/3 implementation, the tradeoff seems worthwhile. For golang/go#58547 Change-Id: Ib3ca446311974571c2367295b302f36a6349b00d Reviewed-on: https://go-review.googlesource.com/c/net/+/564495 Reviewed-by: Jonathan Amsterdam <jba@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Similar to the fast-path for reads, writes are buffered in an unsynchronized []byte allowing for lock-free small writes. For golang/go#58547 Change-Id: I305cb5f91eff662a473f44a4bc051acc7c213e4c Reviewed-on: https://go-review.googlesource.com/c/net/+/564496 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Jonathan Amsterdam <jba@google.com>
Change https://go.dev/cl/565795 mentions this issue: |
Change https://go.dev/cl/565796 mentions this issue: |
Change https://go.dev/cl/565797 mentions this issue: |
Make the abstraction over UDP connections higher level, and add support for setting the source address and ECN bits in sent packets, and receving the destination address and ECN bits in received packets. There is no good way that I can find to identify the source IP address of packets we send. Look up the destination IP address of the first packet received on each connection, and use this as the source address for all future packets we send. This avoids unexpected path migration, where the address we send from changes without our knowing it. Reject received packets sent from an unexpected peer address. In the future, when we support path migration, we will want to relax these restrictions. ECN bits may be used to detect network congestion. We don't make use of them at this time, but this CL adds the necessary UDP layer support to do so in the future. This CL also lays the groundwork for using more efficient platform APIs to send/receive packets in the future. (sendmmsg/recvmmsg/GSO/GRO) These features require platform-specific APIs. Add support for Darwin and Linux to start with, with a graceful fallback on other OSs. For golang/go#58547 Change-Id: I1c97cc0d3e52fff18e724feaaac4a50d3df671bc Reviewed-on: https://go-review.googlesource.com/c/net/+/565255 Reviewed-by: Jonathan Amsterdam <jba@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
We do not support path migration yet, and will ignore packets sent from anything other than the peer's original address. Handle PATH_CHALLENGE frames by sending a PATH_RESPONSE. Handle PATH_RESPONSE frames by closing the connection (since we never send a challenge to respond to). For golang/go#58547 Change-Id: I828b9dcb23e17f5edf3d605b8f04efdafb392807 Reviewed-on: https://go-review.googlesource.com/c/net/+/565795 Reviewed-by: Jonathan Amsterdam <jba@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Client connections must set tls.Config.ServerName to authenticate the identity of the server. (RFC 9001, Section 4.4.) Previously, we specified a single tls.Config per Endpoint. Change the Config passed to Listen to only apply to client connections accepted by the endpoint. Add a Config parameter to Listener.Dial to allow specifying a separate config per outbound connection, allowing the user to set the ServerName field. When the user does not set ServerName, set it ourselves. For golang/go#58547 Change-Id: Ie2500ae7c7a85400e6cc1c10cefa2bd4c746e313 Reviewed-on: https://go-review.googlesource.com/c/net/+/565796 Reviewed-by: Jonathan Amsterdam <jba@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
For golang/go#58547 Change-Id: Ie5dd0ed383ea7a5b3a45103cb730ff62792f62e1 Reviewed-on: https://go-review.googlesource.com/c/net/+/565797 Reviewed-by: Jonathan Amsterdam <jba@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Change https://go.dev/cl/566295 mentions this issue: |
I plan on submitting https://go.dev/cl/566295 shortly, which will move the in-development internal package to This implementation is still a work in progress, but it's complete enough for people to look it over and kick the tires. You can create QUIC connections, streams, and send and receive data on streams. It has not undergone any form of real-world usage, and there are doubtless many bugs. I would not recommend trying to use this in production yet. This package is experimental and not yet bound by the Go compatibility promise. The API may change as we gain experience with it. HTTP/3 is not yet implemented. I intend to do that next. 0-RTT, path migration, and the unreliable datagram extension are also not yet implemented. These should be straightforward to implement, but I don't know when I'll get to them. A number of performance-critical features are missing, notably GRO, GSO, and PMTUD. Expect poor performance until those are completed. In my tests, performance is currently mostly limited by our ability to pass datagrams through the kernel. There are some differences in the API compared to the initial proposal in this issue. Consult the package godoc for details. A quick overview of significant differences:
|
For golang/go#58547 Change-Id: I119d820824f82bfdd236c6826f960d0c934745ca Reviewed-on: https://go-review.googlesource.com/c/net/+/566295 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Jonathan Amsterdam <jba@google.com>
@neild great news. I'll see about playing with it soonish I hope in one of my projects that would benefit dramatically from having quic transport ALPN and both datagram and stream connections and will provide some feedback. |
I propose adding an implementation of the QUIC transport protocol (RFC 9000) in
golang.org/x/net/quic
. QUIC is the protocol underlying HTTP/3, and QUIC support is a necessary prerequisite for HTTP/3 support.The proposed API is in https://go.dev/cl/468575. This API does not include support for Early Data, but does not preclude adding that support at a later time.
RFC 9000 does not define a QUIC API, but it does define a set of operations that can be performed on a QUIC connection or stream.
A QUIC connection is shared state between a client and server.
open a [client] connection:
listen for incoming connections:
A QUIC stream is an ordered, reliable byte stream. A connection may have many streams. (A QUIC stream is loosely analogous to a TCP connection.)
create streams:
accept streams created by the peer:
read from, write to, and close streams:
stream operations also have Context-aware variants:
data written to streams is buffered, and may be explicitly flushed:
See https://go.dev/cl/468575 for the detailed API.
The text was updated successfully, but these errors were encountered: