Overview of masque-go
masterquic-go and provides both client and proxy implementations for tunneling UDP traffic over QUIC.repository·master·Indexed 42 days ago
https://github.com/quic-go/masque-goA Go library providing client and proxy implementations of the CONNECT-UDP protocol (RFC 9298) using QUIC as the transport layer. Built on top of quic-go, it enables tunneling UDP traffic over HTTP/3, featuring a net.PacketConn implementation for proxied UDP and tools for managing proxy server connections and request parsing.
quic-go and provides both client and proxy implementations for tunneling UDP traffic over QUIC.The Proxy type implements an RFC 9298 CONNECT-UDP proxy. It provides methods to proxy UDP traffic over HTTP/3 streams.
There are two primary ways to use it:
Proxy(w http.ResponseWriter, r *ProxyRequest): This is the high-level method. It resolves the target address from r.Target, dials a new UDP connection, and then handles the proxying. It automatically manages the lifecycle of the UDP connection.ProxyConnectedSocket(w http.ResponseWriter, r *ProxyRequest, conn *net.UDPConn): This provides more control. You provide an already established *net.UDPConn. This method is responsible for closing the connection before returning.Important Constraints:
w.WriteHeader() on the http.ResponseWriter yourself if you want the Proxy methods to manage the response status and headers (like Proxy-Status and Capsule-Protocol). The Proxy methods handle writing the status code and headers.Proxy methods will set the Capsule-Protocol header and write http.StatusOK automatically.```go
// Example conceptual usage within an HTTP/3 handler
func (s *Server) HandleProxyRequest(w http.ResponseWriter, r *http.Request) {
// r would be cast or treated as a *ProxyRequest
proxyReq := r.(*ProxyRequest)
// Use the Proxy instance to handle the connection
err := s.proxy.Proxy(w, proxyReq)
if err != nil {
// Handle error
}
}
```埋The Proxy method handles the full lifecycle of a CONNECT-UDP request: resolving the target address, dialing the UDP connection, and initiating the proxying process.
Signature:
func (s *Proxy) Proxy(w http.ResponseWriter, r *ProxyRequest) error
Behavior:
r.Target using net.ResolveUDPAddr.Proxy-Status header and returns an appropriate HTTP error status (e.g., http.StatusBadGateway for DNS errors, http.StatusGatewayTimeout for timeouts).net.DialUDP.destination_ip_unroutable to the Proxy-Status and returns an error.ProxyConnectedSocket internally.err := proxy.Proxy(w, proxyRequest)The Dial method is a shortcut for opening a new QUIC connection to the proxy and then immediately dialing a proxied connection.
Lifecycle: Closing the returned *Conn also closes the underlying QUIC connection to the proxy.
Requirements:
*Request.URL must contain a host.443.QUICConfig must have EnableDatagrams set to true.pconn, rsp, err := transport.Dial(req)
if err != nil {
// handle error
}
defer pconn.Close()
// use pconn...The masque.Conn type allows you to set I/O deadlines to prevent blocking indefinitely.
SetReadDeadline(t time.Time): Sets the time when future ReadFrom calls will fail with os.ErrDeadlineExceeded. If t is zero, no deadline is set.SetWriteDeadline(t time.Time): Currently a no-op (returns nil) due to pending upstream changes in quic-go.SetDeadline(t time.Time): Sets both read and write deadlines to the same time.// Set a read deadline for 5 seconds from now
err := conn.SetReadDeadline(time.Now().Add(5 * time.Second))
if err != nil {
// handle error
}
// Clear the deadline
err = conn.SetReadDeadline(time.Time{})Use ParseProxyRequest to parse incoming HTTP CONNECT requests intended for a UDP proxy. This function validates the request method, protocol, and host against a provided URI template.
If parsing fails, it returns a *ProxyRequestParseError. It is recommended to reject the request using the HTTPStatus field provided in the error to ensure the client receives the correct HTTP response code (e.g., 405 Method Not Allowed or 400 Bad Request).
Successful parsing returns a *ProxyRequest containing the Target (the destination host:port) and the original Host.
import (
"net/http"
"github.com/yosida95/uritemplate/v3"
"github.com/your-project/masque"
)
// Assuming 'template' is a pre-configured *uritemplate.Template
proxyReq, err := masque.ParseProxyRequest(httpRequest, template)
if err != nil {
if parseErr, ok := err.(*masque.ProxyRequestParseError); ok {
// Use parseErr.HTTPStatus to respond to the client
// e.g., http.Error(w, parseErr.Error(), parseErr.HTTPStatus)
}
}
// Use proxyReq.Target to establish the UDP connectionFor advanced use cases where you want to multiplex multiple proxied connections over a single established QUIC connection to the proxy, use NewClientConn instead of Dial.
Lifecycle: The caller owns the provided *quic.Conn and is responsible for closing it. NewClientConn returns a *ClientConn which can then be used to dial multiple proxied connections.
Requirements:
*quic.Conn must have negotiated Datagram support (both local and remote).// 1. Establish the QUIC connection to the proxy manually
quicConn, err := quic.DialAddr(ctx, proxyAddr, tlsConf, quicConf)
if err != nil {
return err
}
// 2. Wrap it in a masque ClientConn
clientConn, err := transport.NewClientConn(quicConn)
if err != nil {
return err
}
// 3. Use clientConn to dial multiple proxied connections
pconn, rsp, err := clientConn.dial(req, func() error { return nil })The ClientConn type represents a single connection to a proxy server. You can use it to establish multiple proxied connections (CONNECT-UDP) over that single proxy connection using the Dial method.
To use Dial, you must provide a *Request object containing the target destination. The method returns a *Conn (the proxied connection), an *http.Response (the server's response to the CONNECT request), and an error.
Requirements for successful Dialing:
Request must have a non-nil URL.Request must have a host specified.Extended CONNECT and Datagrams in its settings.If the server responds with a status code outside the 200-299 range, Dial will return an error along with the response.
// Assuming c is an initialized *masque.ClientConn and req is a valid *masque.Request
conn, resp, err := c.Dial(req)
if err != nil {
// Handle error
}
// Use conn for proxied communication
// Use resp to inspect proxy-specific headers if neededThe Header() method returns the http.Header associated with a masque.Request. You can use this to add custom headers to the request before it is sent/dialed.
// Get the header map
header := req.Header()
// Add a custom header
header.Set("X-Custom-Header", "value")The ProxyConnectedSocket method allows you to proxy traffic using a UDP connection that you have already established or configured.
Signature:
func (s *Proxy) ProxyConnectedSocket(w http.ResponseWriter, _ *ProxyRequest, conn *net.UDPConn) error
Behavior:
http.ResponseWriter to implement the http3.HTTPStreamer interface to access the underlying QUIC stream.Capsule-Protocol header on the response.http.StatusOK to the response.proxyConnSend: Reads datagrams from the QUIC stream and writes them to the UDP connection. It expects datagrams to potentially contain a contextID (via quicvarint), but currently only proxies the raw UDP payload if the contextID is 0.proxyConnReceive: Reads from the UDP connection and sends datagrams back through the QUIC stream.conn before returning.// Assuming 'conn' is an existing *net.UDPConn
err := proxy.ProxyConnectedSocket(w, proxyRequest, conn)Calling Close() on a masque.Conn performs the following actions:
Transport.Dial, it also executes the internal closeConn function to clean up the QUIC connection.You can retrieve the network addresses associated with a masque.Conn using the following methods:
LocalAddr() net.Addr: Returns the local address of the connection.RemoteAddr() net.Addr: Returns the remote address of the connection.