From dc47702535c94c32e0578e5f2b02d8bb8c1edb7d Mon Sep 17 00:00:00 2001 From: Marten Seemann Date: Sun, 5 Jul 2026 18:36:19 +0800 Subject: [PATCH] update masque-go documentation to the v0.4.0 release --- content/docs/connect-udp/client.md | 55 ++++++++++++++++++++++-------- content/docs/connect-udp/proxy.md | 20 +++++------ 2 files changed, 51 insertions(+), 24 deletions(-) diff --git a/content/docs/connect-udp/client.md b/content/docs/connect-udp/client.md index 036acbf..bcd8ff6 100644 --- a/content/docs/connect-udp/client.md +++ b/content/docs/connect-udp/client.md @@ -12,33 +12,60 @@ A client needs to be configured with the same URI template as the proxy. For mor template := uritemplate.MustNew("https://example.org:4443/masque?h={target_host}&p={target_port}") ``` -`Client.DialAddr` can then be used establish proxied connections to servers by hostname. -In this case, DNS resolution is handled by the proxy: +Create a CONNECT-UDP request for the target, then use a `masque.Transport` to dial it. +The target is passed as `host:port`; when the host is a name, DNS resolution is handled by the proxy: ```go -cl := masque.Client{} -// dial a target with a hostname -conn, rsp, err := cl.DialAddr(ctx, template, "quic-go.net:443") +req, err := masque.NewRequest(ctx, template, "quic-go.net:443") +// ... handle error ... + +tr := masque.Transport{} +conn, rsp, err := tr.Dial(req) ``` -`Client.Dial` can be used to establish proxied connections to servers by IP address: +For IP addresses, pass the address in the same `host:port` format: ```go -conn, rsp, err := cl.Dial(ctx, template,<*net.UDPAddr>) +req, err := masque.NewRequest(ctx, template, "203.0.113.1:443") +// ... handle error ... + +conn, rsp, err := tr.Dial(req) ``` -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: +It is possible to add HTTP headers to the CONNECT-UDP request before dialing: ```go -conn, rsp, err := cl.DialAddr(ctx, template, "quic-go.net:443") +req, err := masque.NewRequest(ctx, template, "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. + +req.Header().Set("Authorization", "Bearer token") +conn, rsp, err := tr.Dial(req) +``` + +The `*masque.Conn` returned from `Dial` is only non-nil if the proxy accepted the proxying request. +For a non-2xx response, `Dial` returns an error and the HTTP response: +```go +conn, rsp, err := tr.Dial(req) +if err != nil { + if rsp != nil { + // 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`. +Multiple UDP flows can be proxied over the same QUIC connection to the proxy by creating a `ClientConn` from an established QUIC connection: +```go +qconn, err := quic.DialAddr(ctx, "example.org:4443", tlsConf, &quic.Config{EnableDatagrams: true}) +// ... handle error ... + +cconn, err := tr.NewClientConn(qconn) +// ... handle error ... + +req, err := masque.NewRequest(ctx, template, "quic-go.net:443") +// ... handle error ... + +conn, rsp, err := cconn.Dial(req) +``` ## 📝 Future Work diff --git a/content/docs/connect-udp/proxy.md b/content/docs/connect-udp/proxy.md index 21eb2e9..6c7a28f 100644 --- a/content/docs/connect-udp/proxy.md +++ b/content/docs/connect-udp/proxy.md @@ -33,9 +33,9 @@ t := uritemplate.MustNew("https://example.org:4443/masque?h={target_host}&p={tar 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) + preq, err := masque.ParseProxyRequest(r, t) if err != nil { - if perr, ok := errors.AsType[*masque.RequestParseError](err); ok { + if perr, ok := errors.AsType[*masque.ProxyRequestParseError](err); ok { w.WriteHeader(perr.HTTPStatus) return } @@ -46,7 +46,7 @@ http.Handle("/masque", func(w http.ResponseWriter, r *http.Request) { // optional: whitelisting / blacklisting logic // start proxying UDP datagrams back and forth - err = proxy.Proxy(w, mreq) + err = proxy.Proxy(w, preq) // ... error handling } @@ -55,9 +55,9 @@ s := http3.Server{Addr: ":4443"} s.ListenAndServeTLS(, ) ``` -`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. +`masque.ParseProxyRequest` 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.ProxyRequestParseError`. 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. +The `masque.ProxyRequest.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` @@ -70,21 +70,22 @@ For more details on how to set up and configure an HTTP/3 server, see [Serving H ## Managing UDP Sockets The `proxy.Proxy` function used above creates a new connected UDP socket on `:0` to send UDP datagrams to the target. +When it creates the socket, it also sends a Proxy-Status response header ([RFC 9209](https://datatracker.ietf.org/doc/html/rfc9209)) with details such as DNS errors or the resolved next hop. 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) + preq, err := masque.ParseProxyRequest(r, t) // ... handle error, as above ... // custom logic to resolve and create a UDP socket - addr, err := net.ResolveUDPAddr("udp", mreq.Target) + addr, err := net.ResolveUDPAddr("udp", preq.Target) // ... handle error ... - conn, err := net.DialUDP("udp", addr) + conn, err := net.DialUDP("udp", nil, addr) // ... handle error ... - err = proxy.ProxyConnectedSocket(w, mreq, conn) + err = proxy.ProxyConnectedSocket(w, preq, conn) // ... handle error ... } ``` @@ -101,7 +102,6 @@ The `net.UDPConn` passed to `ProxyConnectedSocket` is closed by the proxy after ## 📝 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 a UDP Listener: [#64](https://github.com/quic-go/masque-go/issues/64)