1752b29df9
Implement UDP tunneling over HTTP/3 using HTTP Datagrams (RFC 9297): - Add masque handler for server-side CONNECT-UDP - Add masque connector and h3-masque dialer for client-side - Add enableDatagrams option to HTTP/3 listener - Add shared utilities for datagram connections and path parsing Resource management and connection caching: - Add deferred stream cleanup in connector on error paths - Add IsClosed() and Close() methods to Client for proper session management - Clean up stale cached clients in dialer before reuse - Close underlying stream when DatagramConn is closed - Move RequestStream opening from connector to dialer to enable dead connection detection and cache invalidation (follows QUIC dialer pattern)
151 lines
4.3 KiB
Go
151 lines
4.3 KiB
Go
package masque
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"net"
|
|
"time"
|
|
|
|
"github.com/go-gost/core/logger"
|
|
"github.com/quic-go/quic-go"
|
|
"github.com/quic-go/quic-go/http3"
|
|
)
|
|
|
|
// Client manages HTTP/3 connections for MASQUE proxying.
|
|
// It wraps a QUIC connection and HTTP/3 client, following the session pattern
|
|
// used by other gost dialers (ssh, quic, mws, etc.).
|
|
type Client struct {
|
|
host string
|
|
addr string
|
|
transport *http3.Transport
|
|
quicConn *quic.Conn
|
|
clientConn *http3.ClientConn
|
|
dialer func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (*quic.Conn, error)
|
|
log logger.Logger
|
|
}
|
|
|
|
// IsClosed returns true if the underlying connection is closed.
|
|
// This follows the pattern used by other gost session wrappers.
|
|
func (c *Client) IsClosed() bool {
|
|
if c.clientConn == nil {
|
|
return true
|
|
}
|
|
select {
|
|
case <-c.clientConn.Context().Done():
|
|
return true
|
|
default:
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Close closes the client connection.
|
|
func (c *Client) Close() error {
|
|
if c.quicConn != nil {
|
|
return (*c.quicConn).CloseWithError(0, "closed")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Dial establishes an HTTP/3 connection to the proxy server.
|
|
// It returns a MasqueConn that can be used by the MASQUE connector.
|
|
// Following the QUIC dialer pattern, this opens a request stream immediately
|
|
// to detect dead connections and allow cache invalidation.
|
|
func (c *Client) Dial(ctx context.Context, addr string) (net.Conn, error) {
|
|
// Create connection if we don't have one (caller checks IsClosed first)
|
|
if c.clientConn == nil {
|
|
// Use the transport's Dial function to establish QUIC connection
|
|
quicConn, err := c.dialer(ctx, addr, c.transport.TLSClientConfig, c.transport.QUICConfig)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c.quicConn = quicConn
|
|
|
|
// Create HTTP/3 client connection on top of QUIC
|
|
c.clientConn = c.transport.NewClientConn(quicConn)
|
|
}
|
|
|
|
// Open request stream NOW (following QUIC dialer pattern).
|
|
// This allows dead connection detection - if this fails, caller can invalidate cache.
|
|
reqStream, err := c.clientConn.OpenRequestStream(ctx)
|
|
if err != nil {
|
|
// Connection is dead - clear our state so next dial creates fresh connection
|
|
c.clientConn = nil
|
|
c.quicConn = nil
|
|
return nil, err
|
|
}
|
|
|
|
// Return a connection wrapper with pre-opened stream
|
|
return &MasqueConn{
|
|
clientConn: c.clientConn,
|
|
reqStream: reqStream,
|
|
host: c.host,
|
|
log: c.log,
|
|
}, nil
|
|
}
|
|
|
|
// MasqueConn wraps an HTTP/3 client connection for use with the MASQUE connector.
|
|
// It implements net.Conn for compatibility with the gost connector interface,
|
|
// but the actual data transfer happens via datagrams through the pre-opened RequestStream.
|
|
type MasqueConn struct {
|
|
clientConn *http3.ClientConn
|
|
reqStream *http3.RequestStream // Pre-opened stream for CONNECT-UDP
|
|
host string
|
|
log logger.Logger
|
|
}
|
|
|
|
// GetRequestStream returns the pre-opened HTTP/3 request stream.
|
|
// The connector uses this for the CONNECT-UDP handshake.
|
|
func (c *MasqueConn) GetRequestStream() *http3.RequestStream {
|
|
return c.reqStream
|
|
}
|
|
|
|
// GetHost returns the proxy host.
|
|
func (c *MasqueConn) GetHost() string {
|
|
return c.host
|
|
}
|
|
|
|
// Read implements net.Conn but is not used for MASQUE.
|
|
// The actual data transfer happens via datagrams.
|
|
func (c *MasqueConn) Read(b []byte) (n int, err error) {
|
|
// This should not be called - datagrams are used for data transfer
|
|
return 0, nil
|
|
}
|
|
|
|
// Write implements net.Conn but is not used for MASQUE.
|
|
// The actual data transfer happens via datagrams.
|
|
func (c *MasqueConn) Write(b []byte) (n int, err error) {
|
|
// This should not be called - datagrams are used for data transfer
|
|
return len(b), nil
|
|
}
|
|
|
|
// Close closes the connection.
|
|
func (c *MasqueConn) Close() error {
|
|
// Don't close the underlying clientConn as it's shared/pooled
|
|
return nil
|
|
}
|
|
|
|
// LocalAddr returns the local network address.
|
|
func (c *MasqueConn) LocalAddr() net.Addr {
|
|
return &net.UDPAddr{}
|
|
}
|
|
|
|
// RemoteAddr returns the remote network address.
|
|
func (c *MasqueConn) RemoteAddr() net.Addr {
|
|
return &net.UDPAddr{}
|
|
}
|
|
|
|
// SetDeadline sets the read and write deadlines.
|
|
func (c *MasqueConn) SetDeadline(t time.Time) error {
|
|
return nil
|
|
}
|
|
|
|
// SetReadDeadline sets the read deadline.
|
|
func (c *MasqueConn) SetReadDeadline(t time.Time) error {
|
|
return nil
|
|
}
|
|
|
|
// SetWriteDeadline sets the write deadline.
|
|
func (c *MasqueConn) SetWriteDeadline(t time.Time) error {
|
|
return nil
|
|
}
|