Skip to content

Commit

Permalink
add documentation for masque-go's CONNECT-UDP implementation (#71)
Browse files Browse the repository at this point in the history
* add documentation for masque-go's CONNECT-UDP implementation

* start the HTTP/3 server

* add a high-level protocol overview

* add a Mermaid diagram

* add masque-go to README

* improve the proxy section

* use uritemplate.MustNew

* minor changes to the client page
  • Loading branch information
marten-seemann authored Sep 19, 2024
1 parent 0729886 commit c492d8d
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 2 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
This is documentation for the [quic-go.net](https://quic-go.net), the documentation website for the quic-go project, covering:
* [quic-go](https://github.com/quic-go/quic-go) (both the QUIC and HTTP/3 layer)
* [webtransport-go](https://github.com/quic-go/webtransport-go)
* [masque-go](https://github.com/quic-go/masque-go)

## Version Compatibility

This documentation is focused **exclusively** on the most recent releases of quic-go and webtransport-go.
This documentation is focused **exclusively** on the most recent releases of the libraries.
No effort is made to document older versions.

## Running locally
Expand Down
23 changes: 22 additions & 1 deletion content/docs/masque/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,25 @@ toc: true
weight: 4
---

CONNECT-UDP ([RFC 9298](https://datatracker.ietf.org/doc/html/rfc9298)) is being implemented in [masque-go](https://github.com/quic-go/masque-go). Documentation will be added once the API has become stable.
CONNECT-UDP ([RFC 9298](https://datatracker.ietf.org/doc/html/rfc9298)) enables the proxying of UDP packets over HTTP/3. It is implemented in [masque-go](https://github.com/quic-go/masque-go).

## Protocol Overview

A client establishes an HTTP/3 connection to a proxy. It requests the proxying of UDP packets to a remote server by sending an Extended CONNECT ([RFC 9220](https://datatracker.ietf.org/doc/html/rfc9220)) HTTP request. If the proxy accepts the proxying request, it opens a UDP socket to the target and forwards UDP packets between the client and the target. Between the client and the proxy, UDP datagrams are sent on the QUIC connection using HTTP Datagrams ([RFC 9279](https://datatracker.ietf.org/doc/html/rfc9279)).

```mermaid
sequenceDiagram
participant Client
participant Proxy
participant Server as Target Server
Client->>Proxy: Extended CONNECT Proxying Request
Proxy-->>Proxy: Open UDP Socket to Server
loop UDP Proxying
Client->>Proxy: UDP payload sent in HTTP Datagram
Proxy->>Server: UDP Packet
Server-->>Proxy: UDP Packet
Proxy->>Client: UDP payload sent in HTTP Datagram
end
```
48 changes: 48 additions & 0 deletions content/docs/masque/client.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
title: Running a Client
toc: true
weight: 2
---

## Setting up a Proxied Connection

A client needs to be configured with the same URI template as the proxy. For more information on URI templates, see [URI Templates]({{< relref "proxy#uri-templates" >}}).

```go
t := uritemplate.MustNew("https://example.org:4443/masque?h={target_host}&p={target_port}")
cl := masque.Client{
Template: t,
}
```

`Client.DialAddr` can then be used establish proxied connections to servers by hostname.
In this case, DNS resolution is handled by the proxy:
```go
// dial a target with a hostname
conn, rsp, err := cl.DialAddr(ctx, "quic-go.net:443")
```

`Client.Dial` can be used to establish proxied connections to servers by IP address:
```go
conn, rsp, err := cl.Dial(ctx, <*net.UDPAddr>)
```

The `net.PacketConn` returned from these methods is only non-nil if the proxy accepted the proxying request.
This is the case if the HTTP status code is in the 2xx range:
```go
conn, rsp, err := cl.DialAddr(ctx, "quic-go.net:443")
// ... handle error ...
if rsp.StatusCode < 200 && rsp.StatusCode > 299 {
// proxying request rejected
// The response status code and body might contain more information.
return
}
// use conn to send and receive UDP datagrams to the target
```

Multiple UDP flows can be proxied over the same QUIC connection to the proxy by calling `DialAddr` and / or `Dial` multiple times on the same `Client`.

## 📝 Future Work

* Logging / Tracing: [#59](https://github.com/quic-go/masque-go/issues/59)
* Proxying IP packets over HTTP ([RFC 9484](https://datatracker.ietf.org/doc/html/rfc9484)): [#63](https://github.com/quic-go/masque-go/issues/63)
108 changes: 108 additions & 0 deletions content/docs/masque/proxy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
---
title: Running a Proxy
toc: true
weight: 1
---

To create a MASQUE proxy server, the following steps are necessary:

1. Set up an HTTP/3 server that defines an `http.Handler` for the URI template.
2. Decode the client's request and create a socket to the target.
3. Use the `masque.Proxy` to handle proxying UDP of the UDP packet flow.

## URI Templates

HTTP clients are configured to use a CONNECT-UDP proxy with a URI Template ([RFC 6570](https://datatracker.ietf.org/doc/html/rfc6570)).
This URI template encodes the target host and port number.

For example, for a proxy running on `https://proxy.example.com`, these are possible URI templates:
* `https://proxy.example.org:4443/masque?h={target_host}&p={target_port}`
* `https://proxy.example.org:4443/masque/{target_host}/{target_port}`

The `target_host` can either be a hostname or an IP address. In case a hostname is used, DNS resolution is handled by the proxy.

When receiving a request at the specified HTTP handler, the server decodes the URI template and opens a UDP socket to the requested target.

## Handling Proxying Requests

To run a CONNECT-UDP proxy on `https://example.org:4443` with the URI template `https://example.org:4443/masque?h={target_host}&p={target_port}`:

```go
t := uritemplate.MustNew("https://example.org:4443/masque?h={target_host}&p={target_port}")
// ... error handling
var proxy masque.Proxy
http.Handle("/masque", func(w http.ResponseWriter, r *http.Request) {
// parse the UDP proxying request
mreq, err := masque.ParseRequest(r, t)
if err != nil {
var perr *masque.RequestParseError
if errors.As(err, &perr) {
w.WriteHeader(perr.HTTPStatus)
return
}
w.WriteHeader(http.StatusBadRequest)
return
}

// optional: whitelisting / blacklisting logic

// start proxying UDP datagrams back and forth
err = proxy.Proxy(w, mreq)
// ... error handling
}

// set up HTTP/3 server on :4443
s := http3.Server{Addr: ":4443"}
s.ListenAndServeTLS(<certfile>, <keyfile>)
```

`masque.ParseRequest` parses the Extended CONNECT request, and extracts the target host and port from the URI template. If parsing of the request fails, it returns a `masque.RequestParseError`. This struct contains a field 'HTTPStatus', allowing the application to reject invalid requests with the correct HTTP status code.

The `masque Request.Target` contains the requested target encoded as `{target_host}:{target_port}`. Applications can implement custom logic to decide which proxying requests are permissible.

{{< callout type="warning" >}}
Applications may add custom header fields to the response header, but must not call `WriteHeader` on the `http.ResponseWriter`
The header is sent when `Proxy.Proxy` is called.
{{< /callout >}}

For more details on how to set up and configure an HTTP/3 server, see [Serving HTTP/3]({{< relref "../http3/server.md" >}}).


## Managing UDP Sockets

The `proxy.Proxy` function used above creates a new connected UDP socket on `:0` to send UDP datagrams to the target.

An application that wishes a more fine-grained control over the socket can instead use `Proxy.ProxyConnectedSocket`:
```go
http.Handle("/masque", func(w http.ResponseWriter, r *http.Request) {
// parse the UDP proxying request
mreq, err := masque.ParseRequest(r, t)
// ... handle error, as above ...

// custom logic to resolve and create a UDP socket
addr, err := net.ResolveUDPAddr("udp", mreq.Target)
// ... handle error ...
conn, err := net.DialUDP("udp", addr)
// ... handle error ...

err = proxy.ProxyConnectedSocket(w, mreq, conn)
// ... handle error ...
}
```

The `net.UDPConn` passed to `ProxyConnectedSocket` is closed by the proxy after proxying is done.

{{< callout type="warning" >}}
Note that it is currently not possible to use unconnected UDP sockets (issue [#3](https://github.com/quic-go/masque-go/issues/3)).
It is invalid to pass an unconnected socket to `ProxyConnectedSocket`.
{{< /callout >}}



## 📝 Future Work

* Unconnected UDP sockets: [#3](https://github.com/quic-go/masque-go/issues/3)
* Use the Proxy-Status HTTP header ([RFC 9209](https://datatracker.ietf.org/doc/html/rfc9209)) to communicate failures: [#2](https://github.com/quic-go/masque-go/issues/2)
* Use GSO and GRO to speed up UDP packet processing: [#31](https://github.com/quic-go/masque-go/issues/31) and [#32](https://github.com/quic-go/masque-go/issues/32)
* Logging / Tracing: [#59](https://github.com/quic-go/masque-go/issues/59)
* Proxying IP packets over HTTP ([RFC 9484](https://datatracker.ietf.org/doc/html/rfc9484)): [#63](https://github.com/quic-go/masque-go/issues/63)

0 comments on commit c492d8d

Please sign in to comment.