Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 41 additions & 14 deletions content/docs/connect-udp/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
20 changes: 10 additions & 10 deletions content/docs/connect-udp/proxy.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
}

Expand All @@ -55,9 +55,9 @@ 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.
`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`
Expand All @@ -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 ...
}
```
Expand All @@ -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)
Loading