masque-go

repository·master·Indexed 42 days ago

https://github.com/quic-go/masque-go

A 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.

Tokens
4.9K
Snippets
18
Records
24
Agent score
53%

What's inside masque-go

  1. Use the Proxy type to implement RFC 9298 CONNECT-UDP

    master

    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:

    1. 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.
    2. 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:

    • When implementing the handler, you MUST NOT call 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.
    • The 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
        }
    }
    ```埋
  2. Proxy the high-level request with Proxy()

    master

    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:

    • Resolves r.Target using net.ResolveUDPAddr.
    • If resolution fails (e.g., DNS error), it adds details to the Proxy-Status header and returns an appropriate HTTP error status (e.g., http.StatusBadGateway for DNS errors, http.StatusGatewayTimeout for timeouts).
    • Dials the target via net.DialUDP.
    • If dialing fails, it adds destination_ip_unroutable to the Proxy-Status and returns an error.
    • If successful, it calls ProxyConnectedSocket internally.
    err := proxy.Proxy(w, proxyRequest)
  3. Dial a proxied connection with Transport.Dial

    master

    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:

    • The *Request.URL must contain a host.
    • If no port is specified in the URL, it defaults to 443.
    • The underlying QUICConfig must have EnableDatagrams set to true.
    pconn, rsp, err := transport.Dial(req)
    if err != nil {
    	// handle error
    }
    defer pconn.Close()
    // use pconn...
  4. Manage deadlines on a masque.Conn

    master

    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{})
  5. Parse CONNECT-UDP requests with ParseProxyRequest

    master

    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 connection
  6. Use Transport.NewClientConn for multiple proxied connections

    master

    For 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:

    • The provided *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 })
  7. Establish proxied connections with ClientConn.Dial

    master

    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:

    • The Request must have a non-nil URL.
    • The Request must have a host specified.
    • The proxy server must have enabled 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 needed
  8. Access and modify headers on a masque.Request

    master

    The 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")
  9. Proxy using an existing socket with ProxyConnectedSocket()

    master

    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:

    • Requires the http.ResponseWriter to implement the http3.HTTPStreamer interface to access the underlying QUIC stream.
    • Sets the Capsule-Protocol header on the response.
    • Writes http.StatusOK to the response.
    • Spawns two goroutines to handle bidirectional data flow:
      • 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.
    • Note: This method closes the provided conn before returning.
    • Note: It discards any capsules sent on the request stream.
    // Assuming 'conn' is an existing *net.UDPConn
    err := proxy.ProxyConnectedSocket(w, proxyRequest, conn)
  10. Close a masque.Conn

    master

    Calling Close() on a masque.Conn performs the following actions:

    1. Marks the connection as closed.
    2. Cancels any pending reads on the underlying HTTP/3 stream.
    3. Closes the underlying HTTP/3 stream.
    4. Waits for the internal read goroutine to finish.
    5. Cancels the read context.
    6. Stops any active read deadline timers.
    7. If the connection was created via Transport.Dial, it also executes the internal closeConn function to clean up the QUIC connection.
  11. Get local and remote addresses from masque.Conn

    master

    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.