Proxy: Add MASQUE inbound (IETF CONNECT-IP server, RFC 9484) (#6844)

Completes https://github.com/XTLS/Xray-core/pull/6807 and https://github.com/XTLS/Xray-core/pull/6810
This commit is contained in:
Cluvex authored and GitHub committed 2026-09-27 19:16:54 +00:00
1 parent 65e853ed84
commit 7a018833ec
22 files changed
+3339 -91

No files matched your search

+12 -12
View File
@@ -15,7 +15,7 @@ import (
"github.com/apernet/quic-go"
)
const maxBufferedRequestBody = 32 << 10
const maxStreamBuffer = 32 << 10
type HTTP2ClientConn struct {
roundTripper http.RoundTripper
@@ -37,7 +37,7 @@ func (c *HTTP2ClientConn) Dial(req *Request) (*Conn, *http.Response, error) {
ctx := httpReq.Context()
streamCtx, cancel := context.WithCancel(context.WithoutCancel(ctx))
stop := context.AfterFunc(ctx, cancel)
body := newRequestBody()
body := NewStreamBuffer()
r := httpReq.Clone(streamCtx)
r.Header[":protocol"] = []string{requestProtocol}
r.Body = body
@@ -67,7 +67,7 @@ func (c *HTTP2ClientConn) Dial(req *Request) (*Conn, *http.Response, error) {
type http2Stream struct {
reader *bufio.Reader
body *requestBody
body *StreamBuffer
rsp io.Closer
cancel context.CancelFunc
}
@@ -86,7 +86,7 @@ func (s *http2Stream) abort() {
s.rsp.Close()
}
type requestBody struct {
type StreamBuffer struct {
mu sync.Mutex
cond sync.Cond
buf []byte
@@ -95,13 +95,13 @@ type requestBody struct {
deadline time.Time
}
func newRequestBody() *requestBody {
b := &requestBody{}
func NewStreamBuffer() *StreamBuffer {
b := &StreamBuffer{}
b.cond.L = &b.mu
return b
}
func (b *requestBody) Read(p []byte) (int, error) {
func (b *StreamBuffer) Read(p []byte) (int, error) {
b.mu.Lock()
defer b.mu.Unlock()
for len(b.buf) == 0 && !b.closed && b.err == nil {
@@ -119,7 +119,7 @@ func (b *requestBody) Read(p []byte) (int, error) {
return n, nil
}
func (b *requestBody) Write(p []byte) (int, error) {
func (b *StreamBuffer) Write(p []byte) (int, error) {
b.mu.Lock()
defer b.mu.Unlock()
for {
@@ -130,7 +130,7 @@ func (b *requestBody) Write(p []byte) (int, error) {
return 0, io.ErrClosedPipe
case !b.deadline.IsZero() && !time.Now().Before(b.deadline):
return 0, os.ErrDeadlineExceeded
case len(b.buf) < maxBufferedRequestBody:
case len(b.buf) < maxStreamBuffer:
b.buf = append(b.buf, p...)
b.cond.Broadcast()
return len(p), nil
@@ -139,7 +139,7 @@ func (b *requestBody) Write(p []byte) (int, error) {
}
}
func (b *requestBody) Close() error {
func (b *StreamBuffer) Close() error {
b.mu.Lock()
b.closed = true
b.cond.Broadcast()
@@ -147,7 +147,7 @@ func (b *requestBody) Close() error {
return nil
}
func (b *requestBody) CloseWithError(err error) {
func (b *StreamBuffer) CloseWithError(err error) {
b.mu.Lock()
if b.err == nil {
b.err = err
@@ -157,7 +157,7 @@ func (b *requestBody) CloseWithError(err error) {
b.mu.Unlock()
}
func (b *requestBody) SetWriteDeadline(t time.Time) error {
func (b *StreamBuffer) SetWriteDeadline(t time.Time) error {
b.mu.Lock()
b.deadline = t
b.cond.Broadcast()
@@ -109,7 +109,7 @@ func setupHTTP2Conns(t *testing.T) (client, server *Conn) {
func newTestHTTP2Stream() (*http2Stream, *io.PipeWriter) {
pr, pw := io.Pipe()
return &http2Stream{reader: bufio.NewReader(pr), body: newRequestBody(), rsp: pr, cancel: func() {}}, pw
return &http2Stream{reader: bufio.NewReader(pr), body: NewStreamBuffer(), rsp: pr, cancel: func() {}}, pw
}
func TestHTTP2Request(t *testing.T) {
@@ -343,7 +343,7 @@ func TestHTTP2CloseUnblocksWrites(t *testing.T) {
require.Eventually(t, func() bool {
str.body.mu.Lock()
defer str.body.mu.Unlock()
return len(str.body.buf) >= maxBufferedRequestBody
return len(str.body.buf) >= maxStreamBuffer
}, 5*time.Second, time.Millisecond)
closed := make(chan error, 1)
@@ -416,7 +416,7 @@ func TestHTTP2WritesDatagramCapsules(t *testing.T) {
func TestRequestBody(t *testing.T) {
t.Run("coalesces writes", func(t *testing.T) {
b := newRequestBody()
b := NewStreamBuffer()
for _, s := range []string{"foo", "bar", "baz"} {
_, err := b.Write([]byte(s))
require.NoError(t, err)
@@ -428,8 +428,8 @@ func TestRequestBody(t *testing.T) {
})
t.Run("blocks writes while full", func(t *testing.T) {
b := newRequestBody()
_, err := b.Write(make([]byte, maxBufferedRequestBody))
b := NewStreamBuffer()
_, err := b.Write(make([]byte, maxStreamBuffer))
require.NoError(t, err)
written := make(chan struct{})
go func() {
@@ -441,7 +441,7 @@ func TestRequestBody(t *testing.T) {
t.Fatal("write did not block")
case <-time.After(50 * time.Millisecond):
}
_, err = b.Read(make([]byte, maxBufferedRequestBody))
_, err = b.Read(make([]byte, maxStreamBuffer))
require.NoError(t, err)
select {
case <-written:
@@ -451,7 +451,7 @@ func TestRequestBody(t *testing.T) {
})
t.Run("close", func(t *testing.T) {
b := newRequestBody()
b := NewStreamBuffer()
_, err := b.Write([]byte("foo"))
require.NoError(t, err)
require.NoError(t, b.Close())
@@ -463,8 +463,8 @@ func TestRequestBody(t *testing.T) {
})
t.Run("write deadline", func(t *testing.T) {
b := newRequestBody()
_, err := b.Write(make([]byte, maxBufferedRequestBody))
b := NewStreamBuffer()
_, err := b.Write(make([]byte, maxStreamBuffer))
require.NoError(t, err)
writeErr := make(chan error, 1)
go func() {
@@ -481,11 +481,11 @@ func TestRequestBody(t *testing.T) {
require.NoError(t, b.Close())
data, err := io.ReadAll(b)
require.NoError(t, err)
require.Len(t, data, maxBufferedRequestBody)
require.Len(t, data, maxStreamBuffer)
})
t.Run("close with error", func(t *testing.T) {
b := newRequestBody()
b := NewStreamBuffer()
_, err := b.Write([]byte("foo"))
require.NoError(t, err)
b.CloseWithError(net.ErrClosed)
+680
View File
@@ -0,0 +1,680 @@
package masque
import (
"bufio"
"bytes"
"context"
go_errors "errors"
"io"
"maps"
"math"
"net"
"net/http"
"net/url"
"slices"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/xtls/xray-core/transport/internet/masque/connectip"
"golang.org/x/net/http2"
"golang.org/x/net/http2/hpack"
)
const (
http2MaxConcurrentStreams = 100
http2HandshakeTimeout = 10 * time.Second
http2DefaultHeaderTable = 4096
)
var (
errHTTP2BadPreface = go_errors.New("http2: invalid connection preface")
errHTTP2BadRequest = go_errors.New("http2: malformed request")
errHTTP2StreamClosed = go_errors.New("http2: stream closed")
errHTTP2RequestBodyClosed = go_errors.New("http2: request body closed")
)
type connAddrsKey struct{}
type connAddrs struct {
local net.Addr
remote net.Addr
}
type http2ServerConn struct {
conn net.Conn
handler http.Handler
ctx context.Context
cancel context.CancelFunc
wmu sync.Mutex
bw *bufio.Writer
fr *http2.Framer
hbuf bytes.Buffer
henc *hpack.Encoder
lastFrame atomic.Int64
mu sync.Mutex
cond sync.Cond
err error
maxFrameSize uint32
initialWindow int64
connSendWindow int64
connRecvWindow int64
connRecvUnacked int64
streams map[uint32]*http2ServerStream
lastStreamID uint32
}
type http2ServerStream struct {
c *http2ServerConn
id uint32
ctx context.Context
cancel context.CancelFunc
header http.Header
out *connectip.StreamBuffer
sent chan struct{}
sendWindow int64
recvWindow int64
recvUnacked int64
recv bytes.Buffer
recvEnd bool
wroteHeader bool
sentEnd bool
resetErr error
}
func serveHTTP2(ctx context.Context, conn net.Conn, handler http.Handler) {
ctx, cancel := context.WithCancel(ctx)
c := &http2ServerConn{
conn: conn,
handler: handler,
ctx: context.WithValue(ctx, connAddrsKey{}, connAddrs{local: conn.LocalAddr(), remote: conn.RemoteAddr()}),
cancel: cancel,
bw: bufio.NewWriter(conn),
maxFrameSize: http2DefaultFrameSize,
initialWindow: http2DefaultWindow,
connSendWindow: http2DefaultWindow,
connRecvWindow: http2ConnectionWindow,
streams: make(map[uint32]*http2ServerStream),
}
c.cond.L = &c.mu
c.henc = hpack.NewEncoder(&c.hbuf)
c.henc.SetMaxDynamicTableSizeLimit(0)
c.lastFrame.Store(time.Now().UnixNano())
br := bufio.NewReader(conn)
preface := make([]byte, len(http2.ClientPreface))
conn.SetReadDeadline(time.Now().Add(http2HandshakeTimeout))
if _, err := io.ReadFull(br, preface); err != nil || string(preface) != http2.ClientPreface {
c.fail(errHTTP2BadPreface)
return
}
conn.SetReadDeadline(time.Time{})
c.fr = http2.NewFramer(c.bw, br)
c.fr.SetMaxReadFrameSize(http2DefaultFrameSize)
c.fr.ReadMetaHeaders = hpack.NewDecoder(http2DefaultHeaderTable, nil)
c.fr.MaxHeaderListSize = http2MaxHeaderListSize
if err := c.write(func(fr *http2.Framer) error {
if err := fr.WriteSettings(
http2.Setting{ID: http2.SettingMaxConcurrentStreams, Val: http2MaxConcurrentStreams},
http2.Setting{ID: http2.SettingInitialWindowSize, Val: http2StreamWindow},
http2.Setting{ID: http2.SettingMaxHeaderListSize, Val: http2MaxHeaderListSize},
http2.Setting{ID: http2.SettingEnableConnectProtocol, Val: 1},
); err != nil {
return err
}
return fr.WriteWindowUpdate(0, http2ConnectionWindow-http2DefaultWindow)
}); err != nil {
c.fail(err)
return
}
go c.keepAlive()
c.readLoop()
}
func (c *http2ServerConn) write(f func(*http2.Framer) error) error {
c.wmu.Lock()
defer c.wmu.Unlock()
if err := f(c.fr); err != nil {
return err
}
return c.bw.Flush()
}
func (c *http2ServerConn) fail(err error) {
c.mu.Lock()
if c.err == nil {
c.err = err
}
streams := slices.Collect(maps.Values(c.streams))
c.cond.Broadcast()
c.mu.Unlock()
for _, st := range streams {
st.out.CloseWithError(err)
st.cancel()
}
c.cancel()
c.conn.Close()
}
func (c *http2ServerConn) keepAlive() {
ticker := time.NewTicker(http2KeepAlivePeriod)
defer ticker.Stop()
for {
select {
case <-c.ctx.Done():
return
case <-ticker.C:
}
idle := time.Since(time.Unix(0, c.lastFrame.Load()))
if idle >= http2IdleTimeout {
c.fail(errHTTP2IdleTimeout)
return
}
if idle >= http2KeepAlivePeriod {
go c.write(func(fr *http2.Framer) error {
return fr.WritePing(false, [8]byte{})
})
}
}
}
func (c *http2ServerConn) readLoop() {
for {
f, err := c.fr.ReadFrame()
if err != nil {
var streamErr http2.StreamError
if go_errors.As(err, &streamErr) {
if err := c.resetStream(streamErr); err != nil {
c.fail(err)
return
}
continue
}
c.fail(err)
return
}
c.lastFrame.Store(time.Now().UnixNano())
if err := c.handleFrame(f); err != nil {
c.fail(err)
return
}
}
}
func (c *http2ServerConn) handleFrame(f http2.Frame) error {
switch f := f.(type) {
case *http2.SettingsFrame:
if f.IsAck() {
return nil
}
if err := c.applySettings(f); err != nil {
return err
}
return c.write((*http2.Framer).WriteSettingsAck)
case *http2.PingFrame:
if f.IsAck() {
return nil
}
return c.write(func(fr *http2.Framer) error {
return fr.WritePing(true, f.Data)
})
case *http2.WindowUpdateFrame:
return c.handleWindowUpdate(f)
case *http2.MetaHeadersFrame:
return c.handleHeaders(f)
case *http2.DataFrame:
return c.handleData(f)
case *http2.RSTStreamFrame:
c.abortStream(f.StreamID, http2.StreamError{StreamID: f.StreamID, Code: f.ErrCode}, false)
case *http2.PushPromiseFrame:
return http2.ConnectionError(http2.ErrCodeProtocol)
}
return nil
}
func (c *http2ServerConn) applySettings(f *http2.SettingsFrame) error {
c.mu.Lock()
defer c.mu.Unlock()
defer c.cond.Broadcast()
return f.ForeachSetting(func(s http2.Setting) error {
if err := s.Valid(); err != nil {
return err
}
switch s.ID {
case http2.SettingMaxFrameSize:
c.maxFrameSize = s.Val
case http2.SettingInitialWindowSize:
delta := int64(s.Val) - c.initialWindow
c.initialWindow = int64(s.Val)
for _, st := range c.streams {
st.sendWindow += delta
if st.sendWindow > math.MaxInt32 {
return http2.ConnectionError(http2.ErrCodeFlowControl)
}
}
}
return nil
})
}
func (c *http2ServerConn) handleWindowUpdate(f *http2.WindowUpdateFrame) error {
c.mu.Lock()
defer c.mu.Unlock()
if f.StreamID == 0 {
c.connSendWindow += int64(f.Increment)
if c.connSendWindow > math.MaxInt32 {
return http2.ConnectionError(http2.ErrCodeFlowControl)
}
} else if st := c.streams[f.StreamID]; st != nil {
st.sendWindow += int64(f.Increment)
if st.sendWindow > math.MaxInt32 {
return http2.ConnectionError(http2.ErrCodeFlowControl)
}
}
c.cond.Broadcast()
return nil
}
func (c *http2ServerConn) handleHeaders(f *http2.MetaHeadersFrame) error {
c.mu.Lock()
if st := c.streams[f.StreamID]; st != nil {
if !f.StreamEnded() {
c.mu.Unlock()
return http2.ConnectionError(http2.ErrCodeProtocol)
}
st.recvEnd = true
c.cond.Broadcast()
c.mu.Unlock()
return nil
}
if f.StreamID%2 == 0 || f.StreamID <= c.lastStreamID {
c.mu.Unlock()
return http2.ConnectionError(http2.ErrCodeProtocol)
}
c.lastStreamID = f.StreamID
refused := len(c.streams) >= http2MaxConcurrentStreams
c.mu.Unlock()
if refused {
return c.write(func(fr *http2.Framer) error {
return fr.WriteRSTStream(f.StreamID, http2.ErrCodeRefusedStream)
})
}
req, err := newHTTP2Request(f)
if err != nil {
return c.write(func(fr *http2.Framer) error {
return fr.WriteRSTStream(f.StreamID, http2.ErrCodeProtocol)
})
}
ctx, cancel := context.WithCancel(c.ctx)
st := &http2ServerStream{
c: c,
id: f.StreamID,
ctx: ctx,
cancel: cancel,
header: make(http.Header),
out: connectip.NewStreamBuffer(),
sent: make(chan struct{}),
recvWindow: http2StreamWindow,
recvEnd: f.StreamEnded(),
}
req = req.WithContext(ctx)
req.RemoteAddr = c.conn.RemoteAddr().String()
if st.recvEnd {
req.Body = http.NoBody
req.ContentLength = 0
} else {
req.Body = &http2RequestBody{st: st}
req.ContentLength = -1
}
c.mu.Lock()
if c.err != nil {
err := c.err
c.mu.Unlock()
cancel()
return err
}
st.sendWindow = c.initialWindow
c.streams[f.StreamID] = st
c.mu.Unlock()
go c.serveStream(st, req)
return nil
}
func newHTTP2Request(f *http2.MetaHeadersFrame) (*http.Request, error) {
method := f.PseudoValue("method")
scheme := f.PseudoValue("scheme")
authority := f.PseudoValue("authority")
path := f.PseudoValue("path")
protocol := f.PseudoValue("protocol")
if method == "" || (protocol != "" && method != http.MethodConnect) {
return nil, errHTTP2BadRequest
}
var u *url.URL
if method == http.MethodConnect && protocol == "" {
if authority == "" || path != "" || scheme != "" {
return nil, errHTTP2BadRequest
}
u = &url.URL{Host: authority}
} else {
if path == "" || scheme == "" {
return nil, errHTTP2BadRequest
}
var err error
if u, err = url.ParseRequestURI(path); err != nil {
return nil, errHTTP2BadRequest
}
}
header := make(http.Header)
for _, hf := range f.RegularFields() {
header.Add(hf.Name, hf.Value)
}
if protocol != "" {
header.Set(":protocol", protocol)
}
if authority == "" {
authority = header.Get("Host")
}
return &http.Request{
Method: method,
URL: u,
Proto: "HTTP/2.0",
ProtoMajor: 2,
Header: header,
Host: authority,
RequestURI: path,
}, nil
}
func (c *http2ServerConn) handleData(f *http2.DataFrame) error {
size := int64(f.Length)
c.mu.Lock()
c.connRecvWindow -= size
if c.connRecvWindow < 0 {
c.mu.Unlock()
return http2.ConnectionError(http2.ErrCodeFlowControl)
}
st := c.streams[f.StreamID]
if st == nil || st.resetErr != nil || st.recvEnd {
idle := st == nil && f.StreamID > c.lastStreamID
c.connRecvWindow += size
c.mu.Unlock()
if idle {
return http2.ConnectionError(http2.ErrCodeProtocol)
}
if size == 0 {
return nil
}
return c.write(func(fr *http2.Framer) error {
return fr.WriteWindowUpdate(0, uint32(size))
})
}
st.recvWindow -= size
if st.recvWindow < 0 {
c.mu.Unlock()
return http2.ConnectionError(http2.ErrCodeFlowControl)
}
st.recv.Write(f.Data())
padding := size - int64(len(f.Data()))
st.recvUnacked += padding
c.connRecvUnacked += padding
if f.StreamEnded() {
st.recvEnd = true
}
c.cond.Broadcast()
c.mu.Unlock()
return nil
}
func (c *http2ServerConn) resetStream(streamErr http2.StreamError) error {
c.mu.Lock()
if streamErr.StreamID%2 == 1 && streamErr.StreamID > c.lastStreamID {
c.lastStreamID = streamErr.StreamID
}
c.mu.Unlock()
c.abortStream(streamErr.StreamID, streamErr, false)
return c.write(func(fr *http2.Framer) error {
return fr.WriteRSTStream(streamErr.StreamID, streamErr.Code)
})
}
func (c *http2ServerConn) abortStream(id uint32, err error, reset bool) {
c.mu.Lock()
st := c.streams[id]
if st == nil || st.resetErr != nil {
c.mu.Unlock()
return
}
st.resetErr = err
c.cond.Broadcast()
c.mu.Unlock()
st.out.CloseWithError(err)
st.cancel()
if reset {
go c.write(func(fr *http2.Framer) error {
return fr.WriteRSTStream(id, http2.ErrCodeCancel)
})
}
}
func (c *http2ServerConn) serveStream(st *http2ServerStream, req *http.Request) {
go st.sendLoop()
c.handler.ServeHTTP(&http2ResponseWriter{st: st}, req)
st.writeHeader(http.StatusOK)
st.out.Close()
<-st.sent
c.mu.Lock()
finish := st.resetErr == nil && !st.sentEnd && c.err == nil
refuse := finish && !st.recvEnd
st.sentEnd = true
delete(c.streams, st.id)
c.cond.Broadcast()
c.mu.Unlock()
st.cancel()
if finish {
if err := c.write(func(fr *http2.Framer) error {
if err := fr.WriteData(st.id, true, nil); err != nil {
return err
}
if refuse {
return fr.WriteRSTStream(st.id, http2.ErrCodeNo)
}
return nil
}); err != nil {
c.fail(err)
}
}
}
func (st *http2ServerStream) writeHeader(code int) {
c := st.c
c.mu.Lock()
if st.wroteHeader || st.resetErr != nil || c.err != nil {
c.mu.Unlock()
return
}
st.wroteHeader = true
header := st.header.Clone()
maxFrameSize := int(c.maxFrameSize)
c.mu.Unlock()
if err := c.write(func(fr *http2.Framer) error {
c.hbuf.Reset()
c.henc.WriteField(hpack.HeaderField{Name: ":status", Value: strconv.Itoa(code)})
for _, k := range slices.Sorted(maps.Keys(header)) {
name := strings.ToLower(k)
switch name {
case "connection", "proxy-connection", "keep-alive", "transfer-encoding", "upgrade":
continue
}
for _, v := range header[k] {
c.henc.WriteField(hpack.HeaderField{Name: name, Value: v})
}
}
block := c.hbuf.Bytes()
for first := true; first || len(block) > 0; first = false {
chunk := block[:min(len(block), maxFrameSize)]
block = block[len(chunk):]
var err error
if first {
err = fr.WriteHeaders(http2.HeadersFrameParam{StreamID: st.id, BlockFragment: chunk, EndHeaders: len(block) == 0})
} else {
err = fr.WriteContinuation(st.id, len(block) == 0, chunk)
}
if err != nil {
return err
}
}
return nil
}); err != nil {
c.fail(err)
}
}
func (st *http2ServerStream) sendLoop() {
defer close(st.sent)
c := st.c
buf := make([]byte, http2DefaultFrameSize)
for {
n, err := st.out.Read(buf)
for data := buf[:n]; len(data) > 0; {
allowed, err := st.awaitSendWindow(len(data))
if err != nil {
st.out.CloseWithError(err)
return
}
if err := c.write(func(fr *http2.Framer) error {
return fr.WriteData(st.id, false, data[:allowed])
}); err != nil {
c.fail(err)
return
}
data = data[allowed:]
}
if err != nil {
return
}
}
}
func (st *http2ServerStream) awaitSendWindow(n int) (int, error) {
c := st.c
c.mu.Lock()
defer c.mu.Unlock()
for {
switch {
case st.resetErr != nil:
return 0, st.resetErr
case c.err != nil:
return 0, c.err
case st.sentEnd:
return 0, errHTTP2StreamClosed
}
if window := min(c.connSendWindow, st.sendWindow); window > 0 {
n = int(min(int64(n), window, int64(c.maxFrameSize)))
c.connSendWindow -= int64(n)
st.sendWindow -= int64(n)
return n, nil
}
c.cond.Wait()
}
}
type http2ResponseWriter struct {
st *http2ServerStream
}
func (w *http2ResponseWriter) Header() http.Header { return w.st.header }
func (w *http2ResponseWriter) WriteHeader(code int) { w.st.writeHeader(code) }
func (w *http2ResponseWriter) Write(p []byte) (int, error) {
w.st.writeHeader(http.StatusOK)
return w.st.out.Write(p)
}
func (w *http2ResponseWriter) Flush() { w.st.writeHeader(http.StatusOK) }
func (w *http2ResponseWriter) SetWriteDeadline(t time.Time) error {
return w.st.out.SetWriteDeadline(t)
}
type http2RequestBody struct {
st *http2ServerStream
}
func (b *http2RequestBody) Read(p []byte) (int, error) {
st := b.st
c := st.c
c.mu.Lock()
for st.recv.Len() == 0 && !st.recvEnd && st.resetErr == nil && c.err == nil {
c.cond.Wait()
}
if st.recv.Len() == 0 {
err := io.EOF
switch {
case st.resetErr != nil:
err = st.resetErr
case c.err != nil && !st.recvEnd:
err = c.err
}
c.mu.Unlock()
return 0, err
}
n, _ := st.recv.Read(p)
st.recvUnacked += int64(n)
c.connRecvUnacked += int64(n)
var streamUpdate, connUpdate int64
if st.recvUnacked >= http2WindowUpdateSize && !st.recvEnd {
streamUpdate = st.recvUnacked
st.recvUnacked = 0
st.recvWindow += streamUpdate
}
if c.connRecvUnacked >= http2WindowUpdateSize {
connUpdate = c.connRecvUnacked
c.connRecvUnacked = 0
c.connRecvWindow += connUpdate
}
c.mu.Unlock()
if streamUpdate > 0 || connUpdate > 0 {
if err := c.write(func(fr *http2.Framer) error {
if connUpdate > 0 {
if err := fr.WriteWindowUpdate(0, uint32(connUpdate)); err != nil {
return err
}
}
if streamUpdate > 0 {
return fr.WriteWindowUpdate(st.id, uint32(streamUpdate))
}
return nil
}); err != nil {
c.fail(err)
}
}
return n, nil
}
func (b *http2RequestBody) Close() error {
st := b.st
c := st.c
c.mu.Lock()
done := st.recvEnd
c.mu.Unlock()
if !done {
c.abortStream(st.id, errHTTP2RequestBodyClosed, true)
}
return nil
}
@@ -0,0 +1,281 @@
package masque
import (
"bytes"
"context"
"crypto/rand"
"crypto/sha256"
"io"
"net"
"net/http"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/net/http2"
"golang.org/x/net/http2/hpack"
)
func serveHTTP2Pipe(t *testing.T, handler http.Handler) net.Conn {
t.Helper()
client, server := tcpPipe(t)
done := make(chan struct{})
go func() {
serveHTTP2(context.Background(), server, handler)
close(done)
}()
t.Cleanup(func() {
client.Close()
server.Close()
<-done
})
return client
}
type http2ClientPeer struct {
t *testing.T
conn net.Conn
fr *http2.Framer
hbuf bytes.Buffer
henc *hpack.Encoder
}
func newHTTP2ClientPeer(t *testing.T, handler http.Handler) (*http2ClientPeer, []http2.Setting) {
t.Helper()
conn := serveHTTP2Pipe(t, handler)
p := &http2ClientPeer{t: t, conn: conn, fr: http2.NewFramer(conn, conn)}
p.henc = hpack.NewEncoder(&p.hbuf)
p.fr.ReadMetaHeaders = hpack.NewDecoder(4096, nil)
_, err := io.WriteString(conn, http2.ClientPreface)
require.NoError(t, err)
require.NoError(t, p.fr.WriteSettings())
f := p.readFrame()
require.IsType(t, &http2.SettingsFrame{}, f)
var settings []http2.Setting
f.(*http2.SettingsFrame).ForeachSetting(func(s http2.Setting) error {
settings = append(settings, s)
return nil
})
f = p.readFrame()
require.IsType(t, &http2.WindowUpdateFrame{}, f)
require.Equal(t, uint32(http2ConnectionWindow-http2DefaultWindow), f.(*http2.WindowUpdateFrame).Increment)
f = p.readFrame()
require.True(t, f.(*http2.SettingsFrame).IsAck())
require.NoError(t, p.fr.WriteSettingsAck())
return p, settings
}
func (p *http2ClientPeer) readFrame() http2.Frame {
p.t.Helper()
p.conn.SetReadDeadline(time.Now().Add(5 * time.Second))
f, err := p.fr.ReadFrame()
require.NoError(p.t, err)
return f
}
func (p *http2ClientPeer) writeHeaders(streamID uint32, endStream bool, fields ...string) {
p.t.Helper()
p.hbuf.Reset()
for i := 0; i < len(fields); i += 2 {
require.NoError(p.t, p.henc.WriteField(hpack.HeaderField{Name: fields[i], Value: fields[i+1]}))
}
require.NoError(p.t, p.fr.WriteHeaders(http2.HeadersFrameParam{
StreamID: streamID,
BlockFragment: p.hbuf.Bytes(),
EndHeaders: true,
EndStream: endStream,
}))
}
func (p *http2ClientPeer) writeConnect(streamID uint32) {
p.t.Helper()
p.writeHeaders(streamID, false,
":method", "CONNECT",
":protocol", "connect-ip",
":scheme", "https",
":authority", "proxy.example",
":path", "/.well-known/masque/ip/*/*/",
"capsule-protocol", "?1",
)
}
func TestHTTP2ServerSettings(t *testing.T) {
_, settings := newHTTP2ClientPeer(t, http.NotFoundHandler())
require.Equal(t, []http2.Setting{
{ID: http2.SettingMaxConcurrentStreams, Val: http2MaxConcurrentStreams},
{ID: http2.SettingInitialWindowSize, Val: http2StreamWindow},
{ID: http2.SettingMaxHeaderListSize, Val: http2MaxHeaderListSize},
{ID: http2.SettingEnableConnectProtocol, Val: 1},
}, settings)
}
func TestHTTP2ServerRoundTrip(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodConnect, r.Method)
assert.Equal(t, "connect-ip", r.Header.Get(":protocol"))
assert.Equal(t, "?1", r.Header.Get("Capsule-Protocol"))
assert.Equal(t, "Basic dTpw", r.Header.Get("Authorization"))
assert.Equal(t, 2, r.ProtoMajor)
assert.Equal(t, "proxy.example", r.Host)
assert.Equal(t, "/.well-known/masque/ip/*/*/", r.URL.Path)
w.Header().Set("Capsule-Protocol", "?1")
w.WriteHeader(http.StatusOK)
assert.NoError(t, http.NewResponseController(w).Flush())
_, err := io.Copy(w, r.Body)
assert.NoError(t, err)
})
cc, err := newHTTP2ClientConn(serveHTTP2Pipe(t, handler))
require.NoError(t, err)
defer cc.Close()
pr, pw := io.Pipe()
rsp, err := cc.RoundTrip(connectRequest(t, context.Background(), pr))
require.NoError(t, err)
require.Equal(t, http.StatusOK, rsp.StatusCode)
require.Equal(t, "?1", rsp.Header.Get("Capsule-Protocol"))
payload := make([]byte, 3*http2ConnectionWindow/2)
rand.Read(payload)
go func() {
pw.Write(payload)
pw.Close()
}()
echoed := sha256.New()
n, err := io.Copy(echoed, rsp.Body)
require.NoError(t, err)
require.Equal(t, int64(len(payload)), n)
require.Equal(t, sha256.Sum256(payload), [32]byte(echoed.Sum(nil)))
}
func TestHTTP2ServerStatus(t *testing.T) {
p, _ := newHTTP2ClientPeer(t, http.NotFoundHandler())
p.writeConnect(1)
f := p.readFrame()
require.IsType(t, &http2.MetaHeadersFrame{}, f)
require.Equal(t, "404", f.(*http2.MetaHeadersFrame).PseudoValue("status"))
var body []byte
for {
f = p.readFrame()
require.IsType(t, &http2.DataFrame{}, f)
body = append(body, f.(*http2.DataFrame).Data()...)
if f.(*http2.DataFrame).StreamEnded() {
break
}
}
require.Equal(t, "404 page not found\n", string(body))
f = p.readFrame()
require.IsType(t, &http2.RSTStreamFrame{}, f)
require.Equal(t, http2.ErrCodeNo, f.(*http2.RSTStreamFrame).ErrCode)
}
func TestHTTP2ServerMalformedRequests(t *testing.T) {
for _, tc := range []struct {
name string
fields []string
}{
{"no method", []string{":scheme", "https", ":path", "/", ":authority", "proxy.example"}},
{"no path", []string{":method", "GET", ":scheme", "https", ":authority", "proxy.example"}},
{"protocol without CONNECT", []string{":method", "GET", ":protocol", "connect-ip", ":scheme", "https", ":path", "/", ":authority", "proxy.example"}},
{"plain CONNECT with a path", []string{":method", "CONNECT", ":path", "/", ":authority", "proxy.example"}},
} {
t.Run(tc.name, func(t *testing.T) {
p, _ := newHTTP2ClientPeer(t, http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
t.Error("the handler saw a malformed request")
}))
p.writeHeaders(1, false, tc.fields...)
f := p.readFrame()
require.IsType(t, &http2.RSTStreamFrame{}, f)
require.Equal(t, http2.ErrCodeProtocol, f.(*http2.RSTStreamFrame).ErrCode)
})
}
}
func TestHTTP2ServerRefusesExtraStreams(t *testing.T) {
release := make(chan struct{})
var started sync.WaitGroup
started.Add(http2MaxConcurrentStreams)
p, _ := newHTTP2ClientPeer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
started.Done()
<-release
}))
defer close(release)
for i := range http2MaxConcurrentStreams {
p.writeConnect(uint32(2*i + 1))
}
started.Wait()
p.writeConnect(2*http2MaxConcurrentStreams + 1)
f := p.readFrame()
require.IsType(t, &http2.RSTStreamFrame{}, f)
require.Equal(t, uint32(2*http2MaxConcurrentStreams+1), f.Header().StreamID)
require.Equal(t, http2.ErrCodeRefusedStream, f.(*http2.RSTStreamFrame).ErrCode)
}
func TestHTTP2ServerClientReset(t *testing.T) {
readErr := make(chan error, 1)
canceled := make(chan struct{})
p, _ := newHTTP2ClientPeer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, err := r.Body.Read(make([]byte, 1))
readErr <- err
<-r.Context().Done()
close(canceled)
}))
p.writeConnect(1)
f := p.readFrame()
require.Equal(t, "200", f.(*http2.MetaHeadersFrame).PseudoValue("status"))
require.NoError(t, p.fr.WriteRSTStream(1, http2.ErrCodeCancel))
select {
case err := <-readErr:
require.Equal(t, http2.StreamError{StreamID: 1, Code: http2.ErrCodeCancel}, err)
case <-time.After(5 * time.Second):
t.Fatal("the body read did not fail after RST_STREAM")
}
select {
case <-canceled:
case <-time.After(5 * time.Second):
t.Fatal("the request context was not canceled")
}
}
func TestHTTP2ServerAnswersPings(t *testing.T) {
p, _ := newHTTP2ClientPeer(t, http.NotFoundHandler())
data := [8]byte{8, 7, 6, 5, 4, 3, 2, 1}
require.NoError(t, p.fr.WritePing(false, data))
f := p.readFrame()
require.IsType(t, &http2.PingFrame{}, f)
require.True(t, f.(*http2.PingFrame).IsAck())
require.Equal(t, data, f.(*http2.PingFrame).Data)
}
func TestHTTP2ServerRejectsOverflow(t *testing.T) {
p, _ := newHTTP2ClientPeer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
<-r.Context().Done()
}))
p.writeConnect(1)
p.readFrame()
chunk := make([]byte, http2DefaultFrameSize)
go func() {
for range http2StreamWindow/len(chunk) + 1 {
if p.fr.WriteData(1, false, chunk) != nil {
return
}
}
}()
p.conn.SetReadDeadline(time.Now().Add(5 * time.Second))
_, err := io.Copy(io.Discard, p.conn)
require.NoError(t, err)
}
func TestHTTP2ServerBadPreface(t *testing.T) {
conn := serveHTTP2Pipe(t, http.NotFoundHandler())
_, err := io.WriteString(conn, "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
require.NoError(t, err)
conn.SetReadDeadline(time.Now().Add(5 * time.Second))
n, err := io.Copy(io.Discard, conn)
require.NoError(t, err)
require.Zero(t, n)
}
+422
View File
@@ -0,0 +1,422 @@
package masque
import (
"context"
"crypto/rand"
gotls "crypto/tls"
go_errors "errors"
"io"
"maps"
"net/http"
"net/url"
"runtime"
"slices"
"strings"
"sync"
"time"
"github.com/apernet/quic-go"
"github.com/apernet/quic-go/http3"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/hysteria/congestion"
"github.com/xtls/xray-core/transport/internet/hysteria/congestion/bbr"
"github.com/xtls/xray-core/transport/internet/masque/connectip"
"github.com/xtls/xray-core/transport/internet/tls"
"golang.org/x/net/http2"
)
type Listener struct {
path pathMatcher
addConn internet.ConnHandler
ctx context.Context
cancel context.CancelFunc
quicServer *http3.Server
quicListener *quic.Listener
transport *quic.Transport
pktConn net.PacketConn
tcpListener net.Listener
mu sync.Mutex
conns map[net.Conn]struct{}
}
func serverVersions(config *tls.Config) (h2, h3 bool) {
h2 = slices.Contains(config.NextProtocol, http2.NextProtoTLS)
h3 = slices.Contains(config.NextProtocol, http3.NextProtoH3) || !h2
return h2, h3
}
func Listen(ctx context.Context, address net.Address, port net.Port, streamSettings *internet.MemoryStreamConfig, handler internet.ConnHandler) (internet.Listener, error) {
if address.Family().IsDomain() {
return nil, errors.New("address is domain")
}
tlsConfig := tls.ConfigFromStreamSettings(streamSettings)
if tlsConfig == nil {
return nil, errors.New("tls config is nil")
}
config := streamSettings.ProtocolSettings.(*Config)
path, err := newPathMatcher(config.Path)
if err != nil {
return nil, err
}
l := &Listener{
path: path,
addConn: handler,
conns: make(map[net.Conn]struct{}),
}
l.ctx, l.cancel = context.WithCancel(context.Background())
h2, h3 := serverVersions(tlsConfig)
if h3 {
if err := l.listenHTTP3(address, port, streamSettings, tlsConfig); err != nil {
l.Close()
return nil, err
}
errors.LogInfo(ctx, "listening UDP for MASQUE over HTTP/3 on ", address, ":", port)
}
if h2 {
if err := l.listenHTTP2(ctx, address, port, streamSettings, tlsConfig); err != nil {
l.Close()
return nil, err
}
errors.LogInfo(ctx, "listening TCP for MASQUE over HTTP/2 on ", address, ":", port)
}
return l, nil
}
func (l *Listener) listenHTTP3(address net.Address, port net.Port, streamSettings *internet.MemoryStreamConfig, tlsConfig *tls.Config) error {
quicParams := streamSettings.QuicParams
if quicParams == nil {
quicParams = &internet.QuicParams{
BbrProfile: string(bbr.ProfileStandard),
}
}
switch quicParams.Congestion {
case "", "reno", "bbr", "brutal", "force-brutal":
default:
return errors.New("unknown congestion control: ", quicParams.Congestion)
}
quicConfig := &quic.Config{
InitialStreamReceiveWindow: quicParams.InitStreamReceiveWindow,
MaxStreamReceiveWindow: quicParams.MaxStreamReceiveWindow,
InitialConnectionReceiveWindow: quicParams.InitConnReceiveWindow,
MaxConnectionReceiveWindow: quicParams.MaxConnReceiveWindow,
MaxIdleTimeout: time.Duration(quicParams.MaxIdleTimeout) * time.Second,
KeepAlivePeriod: time.Duration(quicParams.KeepAlivePeriod) * time.Second,
MaxIncomingStreams: quicParams.MaxIncomingStreams,
InitialPacketSize: initialPacketSize,
DisablePathMTUDiscovery: quicParams.DisablePathMtuDiscovery || (runtime.GOOS != "linux" && runtime.GOOS != "windows" && runtime.GOOS != "darwin"),
EnableDatagrams: true,
DisablePathManager: true,
}
if quicParams.MaxIdleTimeout == 0 {
quicConfig.MaxIdleTimeout = 30 * time.Second
}
udpAddr := &net.UDPAddr{IP: address.IP(), Port: int(port)}
var err error
if streamSettings.FinalMask != nil {
l.pktConn, err = streamSettings.FinalMask.ListenPacket(context.Background(), udpAddr)
} else {
l.pktConn, err = internet.ListenSystemPacket(context.Background(), udpAddr, streamSettings.SocketSettings)
}
if err != nil {
return errors.New("failed to listen UDP on ", address, ":", port).Base(err)
}
var resetKey *quic.StatelessResetKey
if !quicParams.DisableStatelessReset {
resetKey = &quic.StatelessResetKey{}
common.Must2(rand.Read(resetKey[:]))
}
l.transport = &quic.Transport{Conn: l.pktConn, DisableGSO: quicParams.DisableGSO, StatelessResetKey: resetKey}
gotlsConfig := tlsConfig.GetTLSConfig()
gotlsConfig.NextProtos = []string{http3.NextProtoH3}
l.quicListener, err = l.transport.Listen(gotlsConfig, quicConfig)
if err != nil {
return err
}
l.quicServer = &http3.Server{
Handler: l,
EnableDatagrams: true,
ConnContext: func(ctx context.Context, conn *quic.Conn) context.Context {
switch quicParams.Congestion {
case "reno":
case "", "bbr", "brutal":
congestion.UseBBR(conn, bbr.Profile(quicParams.BbrProfile))
case "force-brutal":
congestion.UseBrutal(conn, quicParams.BrutalUp, quicParams.BrutalDisableLossCompensation)
}
return context.WithValue(ctx, connAddrsKey{}, connAddrs{local: conn.LocalAddr(), remote: conn.RemoteAddr()})
},
}
go func() {
if err := l.quicServer.ServeListener(l.quicListener); err != nil && !go_errors.Is(err, quic.ErrServerClosed) && !go_errors.Is(err, http.ErrServerClosed) {
errors.LogErrorInner(context.Background(), err, "failed to serve MASQUE over HTTP/3")
}
}()
return nil
}
func (l *Listener) listenHTTP2(ctx context.Context, address net.Address, port net.Port, streamSettings *internet.MemoryStreamConfig, tlsConfig *tls.Config) error {
tcpAddr := &net.TCPAddr{IP: address.IP(), Port: int(port)}
var err error
if streamSettings.FinalMask != nil {
l.tcpListener, err = streamSettings.FinalMask.Listen(ctx, tcpAddr)
} else {
l.tcpListener, err = internet.ListenSystem(ctx, tcpAddr, streamSettings.SocketSettings)
}
if err != nil {
return errors.New("failed to listen TCP on ", address, ":", port).Base(err)
}
gotlsConfig := tlsConfig.GetTLSConfig()
gotlsConfig.NextProtos = []string{http2.NextProtoTLS}
go l.acceptHTTP2(gotlsConfig)
return nil
}
func (l *Listener) acceptHTTP2(config *gotls.Config) {
for {
conn, err := l.tcpListener.Accept()
if err != nil {
if l.ctx.Err() != nil || strings.Contains(err.Error(), "closed") {
return
}
errors.LogWarningInner(context.Background(), err, "failed to accept MASQUE connections")
if strings.Contains(err.Error(), "too many") {
time.Sleep(500 * time.Millisecond)
}
continue
}
go l.serveHTTP2Conn(conn, config)
}
}
func (l *Listener) serveHTTP2Conn(conn net.Conn, config *gotls.Config) {
tlsConn := tls.Server(conn, config).(*tls.Conn)
if !l.track(tlsConn, true) {
tlsConn.Close()
return
}
defer l.track(tlsConn, false)
ctx, cancel := context.WithTimeout(l.ctx, http2HandshakeTimeout)
err := tlsConn.HandshakeContext(ctx)
cancel()
if err != nil {
errors.LogDebugInner(context.Background(), err, "MASQUE: TLS handshake failed")
tlsConn.Close()
return
}
if protocol := tlsConn.NegotiatedProtocol(); protocol != http2.NextProtoTLS {
errors.LogDebug(context.Background(), "MASQUE: the client negotiated ", protocol, " instead of h2")
tlsConn.Close()
return
}
serveHTTP2(l.ctx, tlsConn, l)
}
func (l *Listener) track(conn net.Conn, add bool) bool {
l.mu.Lock()
defer l.mu.Unlock()
if !add {
delete(l.conns, conn)
return true
}
if l.ctx.Err() != nil {
return false
}
l.conns[conn] = struct{}{}
return true
}
func (l *Listener) Addr() net.Addr {
if l.tcpListener != nil {
return l.tcpListener.Addr()
}
return l.quicListener.Addr()
}
func (l *Listener) Close() error {
l.cancel()
var errs []error
if l.quicServer != nil {
errs = append(errs, l.quicServer.Close())
}
if l.quicListener != nil {
errs = append(errs, l.quicListener.Close())
}
if l.transport != nil {
errs = append(errs, l.transport.Close())
}
if l.pktConn != nil {
errs = append(errs, l.pktConn.Close())
}
if l.tcpListener != nil {
errs = append(errs, l.tcpListener.Close())
}
l.mu.Lock()
for conn := range l.conns {
conn.Close()
}
l.mu.Unlock()
return errors.Combine(errs...)
}
func (l *Listener) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if !l.path.match(r.URL) {
w.WriteHeader(http.StatusNotFound)
return
}
request, err := connectip.ParseProxyRequest(r)
if err != nil {
status := http.StatusBadRequest
if perr, ok := go_errors.AsType[*connectip.ProxyRequestParseError](err); ok {
status = perr.HTTPStatus
}
w.WriteHeader(status)
return
}
addrs, _ := r.Context().Value(connAddrsKey{}).(connAddrs)
conn := &ServerConn{
w: w,
request: r,
proxyRequest: request,
local: addrs.local,
remote: addrs.remote,
done: make(chan struct{}),
}
l.addConn(conn)
select {
case <-conn.done:
case <-l.ctx.Done():
conn.Close()
}
}
type pathMatcher struct {
path string
query url.Values
}
func newPathMatcher(path string) (pathMatcher, error) {
u, err := url.ParseRequestURI(path)
if err != nil || !strings.HasPrefix(u.Path, "/") {
return pathMatcher{}, errors.New("invalid path: ", path)
}
return pathMatcher{path: u.Path, query: u.Query()}, nil
}
func (m pathMatcher) match(u *url.URL) bool {
return u.Path == m.path && maps.EqualFunc(u.Query(), m.query, slices.Equal[[]string])
}
type ServerConn struct {
w http.ResponseWriter
request *http.Request
proxyRequest *connectip.ProxyRequest
local net.Addr
remote net.Addr
answer sync.Once
mu sync.Mutex
ipConn *connectip.Conn
done chan struct{}
closeOnce sync.Once
}
func (c *ServerConn) Request() *http.Request {
return c.request
}
func (c *ServerConn) Accept() (*connectip.Conn, error) {
var err error = errors.New("the request was already answered")
c.answer.Do(func() {
var ipConn *connectip.Conn
ipConn, err = (&connectip.Proxy{}).Proxy(c.w, c.proxyRequest)
c.mu.Lock()
c.ipConn = ipConn
c.mu.Unlock()
})
if err != nil {
return nil, err
}
return c.ipConn, nil
}
func (c *ServerConn) Reject(status int, header http.Header) {
c.answer.Do(func() {
for k, vv := range header {
for _, v := range vv {
c.w.Header().Add(k, v)
}
}
c.w.WriteHeader(status)
})
}
func (c *ServerConn) tunnel() *connectip.Conn {
c.mu.Lock()
defer c.mu.Unlock()
return c.ipConn
}
func (c *ServerConn) Read(b []byte) (int, error) {
ipConn := c.tunnel()
if ipConn == nil {
return 0, io.ErrClosedPipe
}
return ipConn.ReadPacket(b)
}
func (c *ServerConn) Write(b []byte) (int, error) {
ipConn := c.tunnel()
if ipConn == nil {
return 0, io.ErrClosedPipe
}
icmp, err := ipConn.WritePacket(b)
if err != nil {
return 0, err
}
if len(icmp) > 0 {
return 0, &PacketTooBigError{ICMP: icmp}
}
return len(b), nil
}
func (c *ServerConn) Close() error {
c.closeOnce.Do(func() {
c.Reject(http.StatusInternalServerError, nil)
if ipConn := c.tunnel(); ipConn != nil {
ipConn.Close()
}
close(c.done)
})
return nil
}
func (c *ServerConn) LocalAddr() net.Addr {
return c.local
}
func (c *ServerConn) RemoteAddr() net.Addr {
return c.remote
}
func (c *ServerConn) SetDeadline(time.Time) error {
return nil
}
func (c *ServerConn) SetReadDeadline(time.Time) error {
return nil
}
func (c *ServerConn) SetWriteDeadline(time.Time) error {
return nil
}
func init() {
common.Must(internet.RegisterTransportListener(protocolName, Listen))
}
+150
View File
@@ -0,0 +1,150 @@
package masque
import (
"context"
"io"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/xtls/xray-core/transport/internet/stat"
"github.com/xtls/xray-core/transport/internet/tls"
)
func TestServerVersions(t *testing.T) {
for _, c := range []struct {
alpn []string
h2, h3 bool
}{
{alpn: nil, h3: true},
{alpn: []string{"h3"}, h3: true},
{alpn: []string{"h2"}, h2: true},
{alpn: []string{"h2", "http/1.1"}, h2: true},
{alpn: []string{"h3", "h2"}, h2: true, h3: true},
{alpn: []string{"http/1.1"}, h3: true},
} {
h2, h3 := serverVersions(&tls.Config{NextProtocol: c.alpn})
if h2 != c.h2 || h3 != c.h3 {
t.Errorf("serverVersions(%q) = %v, %v, want %v, %v", c.alpn, h2, h3, c.h2, c.h3)
}
}
}
func TestPathMatcher(t *testing.T) {
for _, c := range []struct {
path string
request string
want bool
}{
{DefaultPath, "/.well-known/masque/ip/*/*/", true},
{DefaultPath, "/.well-known/masque/ip/%2A/%2A/", true},
{DefaultPath, "/.well-known/masque/ip/*/*", false},
{DefaultPath, "/.well-known/masque/ip/*/*/?x=1", false},
{DefaultPath, "/.well-known/masque/ip/192.0.2.1/6/", false},
{"/masque?target=*&ipproto=*", "/masque?ipproto=*&target=*", true},
{"/masque?target=*&ipproto=*", "/masque?target=*", false},
} {
m, err := newPathMatcher(c.path)
require.NoError(t, err)
u, err := url.ParseRequestURI(c.request)
require.NoError(t, err)
if got := m.match(u); got != c.want {
t.Errorf("path %q matching %q = %v, want %v", c.path, c.request, got, c.want)
}
}
_, err := newPathMatcher("masque")
require.Error(t, err)
}
func connectIPRequest(target string) *http.Request {
r := httptest.NewRequest(http.MethodGet, "https://proxy.example"+target, nil)
r.Method = http.MethodConnect
r.Proto, r.ProtoMajor, r.ProtoMinor = "HTTP/2.0", 2, 0
r.Header.Set(":protocol", "connect-ip")
r.Header.Set("Capsule-Protocol", "?1")
r.Body = io.NopCloser(&blockingReader{})
return r
}
type blockingReader struct{}
func (*blockingReader) Read([]byte) (int, error) {
time.Sleep(time.Hour)
return 0, io.EOF
}
func serve(t *testing.T, r *http.Request, handle func(*ServerConn)) *httptest.ResponseRecorder {
t.Helper()
path, err := newPathMatcher(DefaultPath)
require.NoError(t, err)
l := &Listener{path: path, addConn: func(conn stat.Connection) {
go func() {
handle(conn.(*ServerConn))
conn.Close()
}()
}}
l.ctx, l.cancel = context.WithCancel(context.Background())
defer l.cancel()
w := httptest.NewRecorder()
done := make(chan struct{})
go func() {
l.ServeHTTP(w, r)
close(done)
}()
select {
case <-done:
case <-time.After(5 * time.Second):
t.Fatal("ServeHTTP did not return")
}
return w
}
func TestListenerRejectsOtherRequests(t *testing.T) {
unexpected := func(*ServerConn) { t.Error("an invalid request reached the proxy") }
require.Equal(t, http.StatusNotFound, serve(t, connectIPRequest("/other"), unexpected).Code)
get := connectIPRequest(DefaultPath)
get.Method = http.MethodGet
require.Equal(t, http.StatusMethodNotAllowed, serve(t, get, unexpected).Code)
websocket := connectIPRequest(DefaultPath)
websocket.Header.Set(":protocol", "websocket")
require.Equal(t, http.StatusNotImplemented, serve(t, websocket, unexpected).Code)
noCapsules := connectIPRequest(DefaultPath)
noCapsules.Header.Del("Capsule-Protocol")
require.Equal(t, http.StatusBadRequest, serve(t, noCapsules, unexpected).Code)
}
func TestServerConnAnswers(t *testing.T) {
t.Run("reject", func(t *testing.T) {
w := serve(t, connectIPRequest(DefaultPath), func(conn *ServerConn) {
require.Equal(t, "connect-ip", conn.Request().Header.Get(":protocol"))
conn.Reject(http.StatusUnauthorized, http.Header{"WWW-Authenticate": {"Basic"}})
})
require.Equal(t, http.StatusUnauthorized, w.Code)
require.Equal(t, "Basic", w.Header().Get("WWW-Authenticate"))
})
t.Run("no answer", func(t *testing.T) {
w := serve(t, connectIPRequest(DefaultPath), func(*ServerConn) {})
require.Equal(t, http.StatusInternalServerError, w.Code)
})
t.Run("accept", func(t *testing.T) {
w := serve(t, connectIPRequest(DefaultPath), func(conn *ServerConn) {
ipConn, err := conn.Accept()
require.NoError(t, err)
require.NotNil(t, ipConn)
_, err = conn.Accept()
require.Error(t, err)
conn.Reject(http.StatusForbidden, nil)
})
require.Equal(t, http.StatusOK, w.Code)
require.Equal(t, "?1", w.Header().Get("Capsule-Protocol"))
})
}