From 82d28a59eee78cfc6d42713cb65452548b9dfbac Mon Sep 17 00:00:00 2001 From: Marten Seemann Date: Thu, 19 Sep 2024 16:54:08 +0800 Subject: [PATCH] add documentation for masque-go's CONNECT-UDP implementation --- content/docs/masque/_index.md | 6 ++- content/docs/masque/client.md | 47 ++++++++++++++++++ content/docs/masque/proxy.md | 91 +++++++++++++++++++++++++++++++++++ 3 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 content/docs/masque/client.md create mode 100644 content/docs/masque/proxy.md diff --git a/content/docs/masque/_index.md b/content/docs/masque/_index.md index 746ba7d..994a222 100644 --- a/content/docs/masque/_index.md +++ b/content/docs/masque/_index.md @@ -4,4 +4,8 @@ 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 being implemented in [masque-go](https://github.com/quic-go/masque-go). + +A CONNECT-UDP client establishes an HTTP/3 connection to a proxy and requests the proxying to a remote server. UDP datagrams are then sent using HTTP Datagrams ([RFC 9279](https://datatracker.ietf.org/doc/html/rfc9298)). + + diff --git a/content/docs/masque/client.md b/content/docs/masque/client.md new file mode 100644 index 0000000..1bbadf6 --- /dev/null +++ b/content/docs/masque/client.md @@ -0,0 +1,47 @@ +--- +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 the [URI Templates]({{< relref "proxy#uri-templates" >}}) section on the proxy page. + +```go +t, err := uritemplate.New("https://example.org:4443/masque?h={target_host}&p={target_port}") +// ... handle error ... +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 packets to the target +``` + +## 📝 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) diff --git a/content/docs/masque/proxy.md b/content/docs/masque/proxy.md new file mode 100644 index 0000000..a619a22 --- /dev/null +++ b/content/docs/masque/proxy.md @@ -0,0 +1,91 @@ +--- +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 of the UDP packet flow. + +## URI Templates + +HTTP clients are configured to use a 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, err := uritemplate.New("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. + err = proxy.Proxy(w, mreq) + // ... error handling +} +``` + +The error returned from `masque ParseRequest` is a `masque.RequestParseError`, which contains a field 'HTTPStatus'. This allows the proxy to reject +invalid requests with the correct HTTP status code. + +The `masque Request.Target` contains the requested target as `{target_host}:{target_port}`. Proxies 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 >}} + +## Controlling the Socket + +`proxy.Proxy` 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 use `Proxy.ProxyConnectedSocket` instead of `Proxy.Proxy`: +```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 ... +} +``` + +## 📝 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)