sing-box/test/http_h3_test.go
2026-09-27 23:02:03 +08:00

373 lines
12 KiB
Go

//go:build with_quic
package main
import (
std_bufio "bufio"
"context"
"crypto/tls"
"io"
"net"
"net/http"
"net/netip"
"net/url"
"strconv"
"testing"
"time"
"github.com/sagernet/quic-go"
"github.com/sagernet/quic-go/http3"
sTLS "github.com/sagernet/sing-box/common/tls"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
sHTTP "github.com/sagernet/sing-box/transport/http"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/auth"
"github.com/sagernet/sing/common/json/badoption"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/protocol/socks"
"github.com/stretchr/testify/require"
)
func dialHTTP3Proxy(t *testing.T, port uint16, enableDatagrams bool) *http3.ClientConn {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
quicConn, err := quic.DialAddrEarly(ctx, "127.0.0.1:"+strconv.Itoa(int(port)), &tls.Config{
ServerName: "example.org",
InsecureSkipVerify: true,
NextProtos: []string{http3.NextProtoH3},
}, &quic.Config{EnableDatagrams: enableDatagrams})
require.NoError(t, err)
transport := &http3.Transport{EnableDatagrams: enableDatagrams}
clientConn := transport.NewClientConn(quicConn)
t.Cleanup(func() {
clientConn.CloseWithError(0, "")
transport.Close()
})
return clientConn
}
func TestHTTPInboundHTTP3(t *testing.T) {
_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
startTLSHTTPInbound(t, certPem, keyPem, []int{1, 2, 3}, nil)
origin := newForwardOrigin(t)
clientConn := dialHTTP3Proxy(t, serverPort, true)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
stream, err := clientConn.OpenRequestStream(ctx)
require.NoError(t, err)
err = stream.SendRequestHeader(&http.Request{
Method: http.MethodConnect,
URL: &url.URL{Host: origin.host()},
Host: origin.host(),
Header: http.Header{"Proxy-Authorization": []string{proxyAuthorization}},
})
require.NoError(t, err)
response, err := stream.ReadResponse()
require.NoError(t, err)
require.Equal(t, http.StatusOK, response.StatusCode)
_, err = stream.Write([]byte("GET /hello HTTP/1.1\r\nHost: " + origin.host() + "\r\n\r\n"))
require.NoError(t, err)
originResponse, err := http.ReadResponse(std_bufio.NewReader(stream), nil)
require.NoError(t, err)
body, err := io.ReadAll(originResponse.Body)
require.NoError(t, err)
require.Equal(t, "hello", string(body))
stream.Close()
forward, err := clientConn.OpenRequestStream(ctx)
require.NoError(t, err)
err = forward.SendRequestHeader(&http.Request{
Method: http.MethodGet,
URL: &url.URL{Scheme: "http", Host: origin.host(), Path: "/hello"},
Host: origin.host(),
Header: http.Header{
"Proxy-Authorization": []string{proxyAuthorization},
"User-Agent": nil,
},
})
require.NoError(t, err)
require.NoError(t, forward.Close())
forwardResponse, err := forward.ReadResponse()
require.NoError(t, err)
body, err = io.ReadAll(forwardResponse.Body)
require.NoError(t, err)
require.Equal(t, http.StatusOK, forwardResponse.StatusCode)
require.Equal(t, "hello", string(body))
forward.Close()
rejected, err := clientConn.OpenRequestStream(ctx)
require.NoError(t, err)
err = rejected.SendRequestHeader(&http.Request{
Method: http.MethodGet,
URL: &url.URL{Scheme: "https", Host: origin.host(), Path: "/hello"},
Host: origin.host(),
Header: http.Header{"Proxy-Authorization": []string{proxyAuthorization}},
})
require.NoError(t, err)
require.NoError(t, rejected.Close())
rejectedResponse, err := rejected.ReadResponse()
require.NoError(t, err)
require.Equal(t, http.StatusBadRequest, rejectedResponse.StatusCode)
}
func openHTTP3ConnectUDP(t *testing.T, ctx context.Context, clientConn *http3.ClientConn, path string) *http3.RequestStream {
stream, err := clientConn.OpenRequestStream(ctx)
require.NoError(t, err)
err = stream.SendRequestHeader(&http.Request{
Method: http.MethodConnect,
Proto: "connect-udp",
URL: &url.URL{
Scheme: "https",
Host: "example.org",
Path: path,
},
Host: "example.org",
Header: http.Header{
"Capsule-Protocol": []string{"?1"},
"Proxy-Authorization": []string{proxyAuthorization},
},
})
require.NoError(t, err)
response, err := stream.ReadResponse()
require.NoError(t, err)
require.Equal(t, http.StatusOK, response.StatusCode)
return stream
}
func TestHTTPInboundConnectUDPHTTP3(t *testing.T) {
_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
startTLSHTTPInbound(t, certPem, keyPem, []int{1, 2, 3}, nil)
echo := startUDPEcho(t)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
clientConn := dialHTTP3Proxy(t, serverPort, true)
stream := openHTTP3ConnectUDP(t, ctx, clientConn, connectUDPPath(echo))
for i := 0; i < 3; i++ {
err := stream.SendDatagram(append([]byte{0}, []byte("ping")...))
require.NoError(t, err)
datagram, err := stream.ReceiveDatagram(ctx)
require.NoError(t, err)
require.Equal(t, append([]byte{0}, []byte("ping")...), datagram)
}
writeDatagramCapsule(t, stream, []byte("capsule"))
datagram, err := stream.ReceiveDatagram(ctx)
require.NoError(t, err)
require.Equal(t, append([]byte{0}, []byte("capsule")...), datagram)
err = stream.SendDatagram(append([]byte{0x40, 0x00}, []byte("varint")...))
require.NoError(t, err)
datagram, err = stream.ReceiveDatagram(ctx)
require.NoError(t, err)
require.Equal(t, append([]byte{0}, []byte("varint")...), datagram)
stream.Close()
capsuleConn := dialHTTP3Proxy(t, serverPort, false)
capsuleStream := openHTTP3ConnectUDP(t, ctx, capsuleConn, connectUDPPath(echo))
reader := std_bufio.NewReader(capsuleStream)
for i := 0; i < 3; i++ {
writeDatagramCapsule(t, capsuleStream, []byte("capsule"))
require.Equal(t, "capsule", string(readDatagramCapsule(t, reader)))
}
capsuleStream.Close()
}
func startHTTP3OnlyInbound(t *testing.T, certPem string, keyPem string) {
startInstance(t, option.Options{
Inbounds: []option.Inbound{
{
Type: C.TypeHTTP,
Options: &option.HTTPInboundOptions{
ListenOptions: option.ListenOptions{
Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())),
ListenPort: serverPort,
},
Version: []int{3},
Users: []auth.User{{Username: "sekai", Password: "password"}},
InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{
TLS: &option.InboundTLSOptions{
Enabled: true,
ServerName: "example.org",
CertificatePath: certPem,
KeyPath: keyPem,
},
},
},
},
},
Outbounds: []option.Outbound{{Type: C.TypeDirect}},
})
}
func startHTTP3Outbound(t *testing.T, certPem string, disableFallback bool) {
startInstance(t, option.Options{
Inbounds: []option.Inbound{
{
Type: C.TypeMixed,
Options: &option.HTTPMixedInboundOptions{
ListenOptions: option.ListenOptions{
Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())),
ListenPort: clientPort,
},
},
},
},
Outbounds: []option.Outbound{
{
Type: C.TypeHTTP,
Options: &option.HTTPOutboundOptions{
ServerOptions: option.ServerOptions{
Server: "127.0.0.1",
ServerPort: serverPort,
},
Username: "sekai",
Password: "password",
Version: 3,
DisableVersionFallback: disableFallback,
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
TLS: &option.OutboundTLSOptions{
Enabled: true,
ServerName: "example.org",
CertificatePath: certPem,
},
},
},
},
},
})
}
func TestHTTPOutboundHTTP3(t *testing.T) {
_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
startHTTP3OnlyInbound(t, certPem, keyPem)
_, err := net.DialTimeout("tcp", "127.0.0.1:"+strconv.Itoa(int(serverPort)), time.Second)
require.Error(t, err)
startHTTP3Outbound(t, certPem, true)
origin := newForwardOrigin(t)
client := proxyClient(t, clientPort)
for i := 0; i < 3; i++ {
request, err := http.NewRequest(http.MethodGet, origin.url("/hello"), nil)
require.NoError(t, err)
request.Header.Set("User-Agent", "")
response, err := client.Do(request)
require.NoError(t, err)
body, err := io.ReadAll(response.Body)
response.Body.Close()
require.NoError(t, err)
require.Equal(t, "hello", string(body))
}
dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", clientPort), socks.Version5, "", "")
dialUDP := func() (net.PacketConn, error) {
return dialer.ListenPacket(context.Background(), M.ParseSocksaddrHostPort("127.0.0.1", testPort))
}
require.NoError(t, testPingPongWithPacketConn(t, testPort, dialUDP))
require.NoError(t, testLargeDataWithPacketConn(t, testPort, dialUDP))
}
func TestHTTPOutboundHTTP3Fallback(t *testing.T) {
_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
startInstance(t, option.Options{
Inbounds: []option.Inbound{
{
Type: C.TypeHTTP,
Options: &option.HTTPInboundOptions{
ListenOptions: option.ListenOptions{
Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())),
ListenPort: serverPort,
},
Version: []int{1, 2},
Users: []auth.User{{Username: "sekai", Password: "password"}},
InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{
TLS: &option.InboundTLSOptions{
Enabled: true,
ServerName: "example.org",
CertificatePath: certPem,
KeyPath: keyPem,
},
},
},
},
},
Outbounds: []option.Outbound{{Type: C.TypeDirect}},
})
startHTTP3Outbound(t, certPem, false)
origin := newForwardOrigin(t)
client := proxyClient(t, clientPort)
request, err := http.NewRequest(http.MethodGet, origin.url("/hello"), nil)
require.NoError(t, err)
request.Header.Set("User-Agent", "")
response, err := client.Do(request)
require.NoError(t, err)
body, err := io.ReadAll(response.Body)
response.Body.Close()
require.NoError(t, err)
require.Equal(t, "hello", string(body))
}
func TestHTTPOutboundHTTP3NoFallback(t *testing.T) {
_, certPem, _ := createSelfSignedCertificate(t, "example.org")
startHTTP3Outbound(t, certPem, true)
origin := newForwardOrigin(t)
client := proxyClient(t, clientPort)
request, err := http.NewRequest(http.MethodGet, origin.url("/hello"), nil)
require.NoError(t, err)
response, err := client.Do(request)
require.NoError(t, err)
response.Body.Close()
require.Equal(t, http.StatusBadGateway, response.StatusCode)
}
func TestHTTPOutboundHTTP3Cancel(t *testing.T) {
_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
certificate, err := tls.LoadX509KeyPair(certPem, keyPem)
require.NoError(t, err)
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)})
require.NoError(t, err)
quicListener, err := quic.ListenEarly(udpConn, &tls.Config{
Certificates: []tls.Certificate{certificate},
NextProtos: []string{http3.NextProtoH3},
}, &quic.Config{EnableDatagrams: true})
require.NoError(t, err)
server := &http3.Server{
EnableDatagrams: true,
Handler: http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
<-request.Context().Done()
}),
}
go server.ServeListener(quicListener)
t.Cleanup(func() {
server.Close()
udpConn.Close()
})
serverAddress := M.SocksaddrFromNet(udpConn.LocalAddr())
tlsConfig, err := sTLS.NewClient(context.Background(), log.NewNOPFactory().Logger(), serverAddress.AddrString(), option.OutboundTLSOptions{
Enabled: true,
ServerName: "example.org",
CertificatePath: certPem,
ALPN: []string{http3.NextProtoH3},
})
require.NoError(t, err)
client, err := sHTTP.NewClient(sHTTP.ClientOptions{
RawDialer: N.SystemDialer,
TLSConfig: tlsConfig,
Server: serverAddress,
Version: 3,
DisableVersionFallback: true,
})
require.NoError(t, err)
defer client.Close()
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
startTime := time.Now()
_, err = client.DialContext(ctx, N.NetworkTCP, M.ParseSocksaddrHostPort("127.0.0.1", testPort))
require.ErrorIs(t, err, context.DeadlineExceeded)
require.Less(t, time.Since(startTime), 5*time.Second)
packetCtx, packetCancel := context.WithTimeout(context.Background(), time.Second)
defer packetCancel()
_, err = client.ListenPacket(packetCtx, M.ParseSocksaddrHostPort("127.0.0.1", testPort))
require.ErrorIs(t, err, context.DeadlineExceeded)
}