Files
x/internal/util/masque/conn.go
T
David Manouchehri c5f91232cb Add TCP CONNECT support to MASQUE (RFC 9114)
Extend MASQUE implementation to support TCP tunneling via standard HTTP/3
CONNECT method, in addition to existing UDP support via CONNECT-UDP:

- Add StreamConn type for TCP data transfer over HTTP/3 stream body
- Update handler to dispatch based on :protocol pseudo-header:
  - "connect-udp" for UDP (RFC 9298)
  - Empty/"HTTP/3.0" for TCP (RFC 9114)
- Add handleConnectTCP() using bidirectional stream relay
- Update connector to support both TCP and UDP networks
- Add connectTCP() for standard CONNECT requests

TCP uses :authority for target address and stream body for data.
UDP uses path template and HTTP/3 datagrams for data.
2025-12-28 23:47:51 +00:00

279 lines
7.2 KiB
Go

package masque
import (
"context"
"io"
"net"
"sync"
"time"
"github.com/quic-go/quic-go/http3"
)
// DatagramStreamer is an interface for sending and receiving HTTP/3 datagrams.
// Both http3.Stream and http3.RequestStream implement this interface.
type DatagramStreamer interface {
SendDatagram(b []byte) error
ReceiveDatagram(ctx context.Context) ([]byte, error)
}
// DatagramConn wraps an HTTP/3 stream's datagram methods as net.PacketConn.
// This allows UDP packets to be tunneled over HTTP/3 datagrams per RFC 9297/9298.
type DatagramConn struct {
stream DatagramStreamer
closer io.Closer // Optional closer for the underlying stream
localAddr net.Addr
remoteAddr net.Addr
closed chan struct{}
closeOnce sync.Once
mu sync.RWMutex
readDeadline time.Time
}
// NewDatagramConn creates a new DatagramConn wrapping an HTTP/3 stream.
func NewDatagramConn(stream *http3.Stream, laddr, raddr net.Addr) *DatagramConn {
return &DatagramConn{
stream: stream,
localAddr: laddr,
remoteAddr: raddr,
closed: make(chan struct{}),
}
}
// NewDatagramConnFromRequestStream creates a new DatagramConn wrapping an HTTP/3 request stream.
// This is used by the client-side connector. The stream will be closed when Close() is called.
func NewDatagramConnFromRequestStream(stream *http3.RequestStream, laddr, raddr net.Addr) *DatagramConn {
return &DatagramConn{
stream: stream,
closer: stream, // RequestStream implements io.Closer
localAddr: laddr,
remoteAddr: raddr,
closed: make(chan struct{}),
}
}
// ReadFrom reads a UDP datagram from the HTTP/3 stream.
// Per RFC 9297, HTTP datagrams have a context ID prefix.
// For CONNECT-UDP (RFC 9298), the context ID is 0.
func (c *DatagramConn) ReadFrom(b []byte) (n int, addr net.Addr, err error) {
select {
case <-c.closed:
return 0, nil, net.ErrClosed
default:
}
ctx := context.Background()
c.mu.RLock()
deadline := c.readDeadline
c.mu.RUnlock()
if !deadline.IsZero() {
var cancel context.CancelFunc
ctx, cancel = context.WithDeadline(ctx, deadline)
defer cancel()
}
data, err := c.stream.ReceiveDatagram(ctx)
if err != nil {
return 0, nil, err
}
if len(data) == 0 {
return 0, c.remoteAddr, nil
}
// Per RFC 9297: datagram format is context-id (varint) + payload
// For CONNECT-UDP with context ID 0, the first byte is 0x00
// We strip the context ID prefix
if data[0] == 0x00 {
data = data[1:]
}
n = copy(b, data)
return n, c.remoteAddr, nil
}
// WriteTo sends a UDP datagram via the HTTP/3 stream.
// Per RFC 9297, we prepend the context ID (0x00 for CONNECT-UDP).
func (c *DatagramConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
select {
case <-c.closed:
return 0, net.ErrClosed
default:
}
// Prepend context ID (0x00 for context ID 0)
datagram := make([]byte, 1+len(b))
datagram[0] = 0x00 // Context ID = 0
copy(datagram[1:], b)
if err := c.stream.SendDatagram(datagram); err != nil {
return 0, err
}
return len(b), nil
}
// Read reads data from the connection (net.Conn interface).
func (c *DatagramConn) Read(b []byte) (n int, err error) {
n, _, err = c.ReadFrom(b)
return
}
// Write writes data to the connection (net.Conn interface).
func (c *DatagramConn) Write(b []byte) (n int, err error) {
return c.WriteTo(b, c.remoteAddr)
}
// Close closes the datagram connection and the underlying stream.
func (c *DatagramConn) Close() error {
var err error
c.closeOnce.Do(func() {
close(c.closed)
if c.closer != nil {
err = c.closer.Close()
}
})
return err
}
// LocalAddr returns the local network address.
func (c *DatagramConn) LocalAddr() net.Addr {
return c.localAddr
}
// RemoteAddr returns the remote network address.
func (c *DatagramConn) RemoteAddr() net.Addr {
return c.remoteAddr
}
// SetDeadline sets the read and write deadlines.
func (c *DatagramConn) SetDeadline(t time.Time) error {
c.SetReadDeadline(t)
c.SetWriteDeadline(t)
return nil
}
// SetReadDeadline sets the read deadline.
func (c *DatagramConn) SetReadDeadline(t time.Time) error {
c.mu.Lock()
c.readDeadline = t
c.mu.Unlock()
return nil
}
// SetWriteDeadline sets the write deadline.
// Note: Write deadlines are not used for HTTP/3 datagrams as SendDatagram is non-blocking.
func (c *DatagramConn) SetWriteDeadline(t time.Time) error {
return nil
}
// Ensure DatagramConn implements both net.Conn and net.PacketConn
var (
_ net.Conn = (*DatagramConn)(nil)
_ net.PacketConn = (*DatagramConn)(nil)
)
// StreamReadWriter is an interface for reading and writing to HTTP/3 streams.
// Both http3.Stream and http3.RequestStream implement this interface.
type StreamReadWriter interface {
io.Reader
io.Writer
io.Closer
SetDeadline(t time.Time) error
SetReadDeadline(t time.Time) error
SetWriteDeadline(t time.Time) error
}
// StreamConn wraps an HTTP/3 stream as net.Conn for TCP tunneling (RFC 9114).
// Unlike DatagramConn which uses HTTP/3 datagrams, StreamConn reads/writes
// directly to the HTTP/3 stream body for reliable, ordered byte streams.
type StreamConn struct {
stream StreamReadWriter
localAddr net.Addr
remoteAddr net.Addr
closed chan struct{}
closeOnce sync.Once
}
// NewStreamConn creates a new StreamConn wrapping an HTTP/3 stream (server-side).
// This is used for TCP CONNECT tunneling where data flows through the stream body.
func NewStreamConn(stream *http3.Stream, laddr, raddr net.Addr) *StreamConn {
return &StreamConn{
stream: stream,
localAddr: laddr,
remoteAddr: raddr,
closed: make(chan struct{}),
}
}
// NewStreamConnFromRequestStream creates a new StreamConn wrapping an HTTP/3 request stream (client-side).
// This is used for TCP CONNECT tunneling where data flows through the stream body.
func NewStreamConnFromRequestStream(stream *http3.RequestStream, laddr, raddr net.Addr) *StreamConn {
return &StreamConn{
stream: stream,
localAddr: laddr,
remoteAddr: raddr,
closed: make(chan struct{}),
}
}
// Read reads data from the HTTP/3 stream body.
func (c *StreamConn) Read(b []byte) (n int, err error) {
select {
case <-c.closed:
return 0, net.ErrClosed
default:
}
return c.stream.Read(b)
}
// Write writes data to the HTTP/3 stream body.
func (c *StreamConn) Write(b []byte) (n int, err error) {
select {
case <-c.closed:
return 0, net.ErrClosed
default:
}
return c.stream.Write(b)
}
// Close closes the stream connection.
func (c *StreamConn) Close() error {
var err error
c.closeOnce.Do(func() {
close(c.closed)
err = c.stream.Close()
})
return err
}
// LocalAddr returns the local network address.
func (c *StreamConn) LocalAddr() net.Addr {
return c.localAddr
}
// RemoteAddr returns the remote network address.
func (c *StreamConn) RemoteAddr() net.Addr {
return c.remoteAddr
}
// SetDeadline sets the read and write deadlines.
func (c *StreamConn) SetDeadline(t time.Time) error {
return c.stream.SetDeadline(t)
}
// SetReadDeadline sets the read deadline.
func (c *StreamConn) SetReadDeadline(t time.Time) error {
return c.stream.SetReadDeadline(t)
}
// SetWriteDeadline sets the write deadline.
func (c *StreamConn) SetWriteDeadline(t time.Time) error {
return c.stream.SetWriteDeadline(t)
}
// Ensure StreamConn implements net.Conn
var _ net.Conn = (*StreamConn)(nil)