01a2bbaa11
- Return errors from MasqueConn.Read/Write instead of silently succeeding (prevents infinite busy-loops and silent data loss if these methods are accidentally called). - Set recorder Network field in protocol dispatch switch rather than hardcoding "udp", so early errors don't misreport type. - Clarify that DatagramConn context-ID handling only supports context ID 0 (sufficient for CONNECT-UDP per RFC 9298).
152 lines
4.4 KiB
Go
152 lines
4.4 KiB
Go
package masque
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"errors"
|
|
"net"
|
|
"time"
|
|
|
|
"github.com/go-gost/core/logger"
|
|
"github.com/quic-go/quic-go"
|
|
"github.com/quic-go/quic-go/http3"
|
|
)
|
|
|
|
var errReadWriteNotSupported = errors.New("masque: direct Read/Write not supported on MasqueConn, use GetRequestStream")
|
|
|
|
// 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 or the request stream.
|
|
func (c *MasqueConn) Read(b []byte) (n int, err error) {
|
|
return 0, errReadWriteNotSupported
|
|
}
|
|
|
|
// Write implements net.Conn but is not used for MASQUE.
|
|
// The actual data transfer happens via datagrams or the request stream.
|
|
func (c *MasqueConn) Write(b []byte) (n int, err error) {
|
|
return 0, errReadWriteNotSupported
|
|
}
|
|
|
|
// 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
|
|
}
|