quic-go / quic-go

A QUIC implementation in pure Go
https://quic-go.net
MIT License
10.04k stars 1.31k forks source link

RoundTripper and Proxy #3370

Open arcadi4n opened 2 years ago

arcadi4n commented 2 years ago

Hello,

Thank you for your great project!

I wrote a client that will use the roundtripper and works fine. However, the computer on which I want to run the client has a system proxy configured. I tried something similar to how I would normally do it using the "net/http" library but I get an error that http3.roundTripper struct does't support the Proxy field.

Is there any way to do this?

marten-seemann commented 2 years ago

What would the Proxy field do?

arcadi4n commented 2 years ago

Thank you for your response!

I tried to do something like the following:

proxyUrl, _ := url.Parse("https://10.1.1.10:8080")

roundTripper := &http3.RoundTripper{
        TLSClientConfig: &tls.Config{
            InsecureSkipVerify: true,
        },
        Proxy: http.ProxyURL(proxyUrl),
    }

My goal was was to use it similarly to the "net/http" library. Is there any way to achieve this?

ja3abuser commented 2 years ago

any update?

pteichman commented 2 years ago

A note for others in here: some of this can be done with the current API (though it's a little cumbersome). Dropping a little extra info in here in case it can inform an implementation:

My motivation is that I need to use HTTP3 to create a CONNECT style proxy to another host. The behavior is described in section 4.4 of RFC9114: the CONNECT is sent over HTTP3 (and quic) and the proxy creates a TCP connection to the requested host and port, which can be used via the quic stream from the CONNECT request. None of that is important from the HTTP3 client perspective, just a little background.

I can do this for stdlib http requests by setting the Proxy and ProxyConnectHeader (for auth) fields; with those set, http.Transport will automatically issue the CONNECT request as it dials.

With quic-go/http3 you can do something similar by writing a new http.RoundTripper. Its RoundTrip needs to:

My use case would be satisfied by adding a stdlib compatible Proxy and ProxyConnectHeader field to http3.RoundTripper, but all of the above would need to be added with them; likely in its Dial.

Fundamentally though this is a way to round trip an HTTP2 or 1.1 request using the HTTP3 connection; it's more of a compatibility bridge between net/http and quic-go/http3 than a new way to get an HTTP3 request through. CONNECT-UDP may be an answer for that, but that's outside of my use case.

kokes commented 2 years ago

Thanks, @pteichman, this helped me get closer to CONNECT proxy, just a few extra notes for whoever might come across this:

While my approach does reimplement lots of http3 from this module, there's that bit of extra control over individual streams (and that setupConn issue I just couldn't overcome) that appealed to me.

paramaw commented 1 year ago

hi @kokes, do you have any code samples of your implementation? I'm looking to do the same thing.

kokes commented 1 year ago

Hi, snippets are as follows:

I create a bidirectional stream

stream, err := quicConn.OpenStreamSync(...) // I have a pre-existing context

I then craft my headers and use qpack.NewEncoder from qpack to encode this into a byte buffer.

headers := []qpack.HeaderField{
    {Name: ":method", Value: method}, // CONNECT, most likely
    {Name: ":authority", Value: myurl.Host},
    // + auth
}

I then take this byte buffer and write a HEADERS frame as specified in the HTTP/3 RFC.

ln := len(payload) // payload being those qpacked headers

sett := bytes.NewBuffer(nil)
quicvarint.Write(sett, uint64(ftype)) // HEADER frameType = 0x01
quicvarint.Write(sett, uint64(ln))

stream.Write(append(sett.Bytes(), payload...)) // or write the payload into the buffer and then io.Copy from the buffer to the stream

I then read from the stream - first I expect HEADERS back, so I read the (quicvarint'ed) frame type. This should be HEADERS (0x01). Then it's a varint'ed length and then the byte array itself (of said length). I use qpack.Decoder to make sense of the contents.

ftype, err := quicvarint.Read(r)

Once that's done, all the frames I'm receiving within this stream should be DATA (which you should check, anyway). It's again frame type, length and then the byte array of raw data being passed from the origin by your proxy (assuming it works as I expect it to).

You can either handle these DATA frames in a loop or create a struct that wraps your stream and implements io.ReadWriter by writing simple Read and Write methods that (un)pack those three parts of each frame - type, length, data. After all it's implemented in this very repo.

If you also add methods for LocalAddr, RemoteAddr and a few deadline methods, you can satisfy the net.Conn interface and thus pass this struct around, specifically to tls.Client

hostname := req.URL.Hostname()
decipheredWire := tls.Client(rawWire, &tls.Config{ServerName: hostname})

And once that is done, you can simply req.Write(buffer) and send this buffer into the deciphered wire and read the response using http.ReadResponse.

It's all a bit cumbersome, but doable. I implemented the same for an http2 CONNECT proxy, but didn't have to do any of these shenanigans, a simple io.Pipe worked fine over there, because we didn't have that control stream issue like we do here. Without that, the whole implementation would be super simple, just like Peter alluded to above.

I hope this helps.

RithvikChuppala commented 7 months ago

For anyone wondering, this can also be used to create a CONNECT-UDP request to proxy QUIC to a remote server via the initial HTTP3/QUIC connection with a few extra steps (granted it does not follow RFC 9298 exactly since it does not use HTTP3 Datagrams for the initial HTTP connection but that is beyond my use case).

Follow the same initial steps to extract the quic stream from the CONNECT-UDP HTTP connection on the client side and wrap it in a struct that stubs several functions to create a net.Conn as Peter mentioned above - I referred to https://github.com/quic-go/quic-go/pull/3997 to get a few things working.

In addition to what was mentioned above, stub out a few more methods to extend your net.Conn stream struct to a net.PacketConn (ReadFrom, WriteTo, etc); once you have done that on the client side, you can pass the net.PacketConn stream struct to quic.Dial and "dial" the remote QUIC server (it does not directly dial the remote server; rather, it sends the dial packets to the proxy through the extracted stream).

On the proxy end, extract the quic Stream from the client-to-proxy CONNECT-UDP HTTP3 stream and create a net.Conn stream struct (it should be the same process as the client side). Next, create a UDP connection to the remote QUIC server's address (net.DialUDP) and then io.Copy the net.Conn stream struct and the new UDP net.Conn both ways (remember to run the two io.Copies in separate go routines). This extracts all the end-to-end data from the proxy tunnel and packetizes it in UDP that is exchanged with the remote QUIC server (which can now interpret this as a regular Dial coming from the client).

Once the proxy plugs the two connections together, the quic Dial from the client will tunnel through the HTTP3 stream, reach the remote QUIC server (via the proxy's UDP connection), engage in the handshake, and establish an end-to-end QUIC connection. You can then utilize this connection as any regular QUIC connection (create streams, send data, send datagrams, etc)