mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-09-27 15:14:26 +00:00
287 lines
9 KiB
Go
287 lines
9 KiB
Go
package main
|
|
|
|
import (
|
|
std_bufio "bufio"
|
|
"context"
|
|
"crypto/tls"
|
|
"encoding/base64"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/netip"
|
|
"strconv"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
C "github.com/sagernet/sing-box/constant"
|
|
"github.com/sagernet/sing-box/option"
|
|
"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"
|
|
)
|
|
|
|
const (
|
|
masqueServerAddress = "10.8.0.1"
|
|
masqueSiteAddress = "10.9.0.5"
|
|
)
|
|
|
|
type masqueEnvironment struct {
|
|
serverPort uint16
|
|
serverProxyPort uint16
|
|
clientProxyPort uint16
|
|
}
|
|
|
|
func startMASQUE(t *testing.T, serverVersions []int, clientVersion int, disableVersionFallback bool, mtu uint32, serverRoutes []netip.Prefix, clientRoutes []netip.Prefix) masqueEnvironment {
|
|
t.Helper()
|
|
environment := masqueEnvironment{
|
|
serverProxyPort: reserveOpenVPNTCPPort(t),
|
|
clientProxyPort: reserveOpenVPNTCPPort(t),
|
|
}
|
|
masquePort := reserveOpenVPNEchoPort(t)
|
|
environment.serverPort = masquePort
|
|
_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
|
|
users := []auth.User{{Username: "sekai", Password: "password"}}
|
|
startInstance(t, masqueInstanceOptions(C.TypeMASQUEServer, &option.MASQUEServerEndpointOptions{
|
|
ListenOptions: option.ListenOptions{
|
|
Listen: common.Ptr(badoption.Addr(netip.MustParseAddr("127.0.0.1"))),
|
|
ListenPort: masquePort,
|
|
},
|
|
MASQUEEndpointOptions: option.MASQUEEndpointOptions{MTU: mtu},
|
|
Users: users,
|
|
Version: serverVersions,
|
|
InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{
|
|
TLS: &option.InboundTLSOptions{
|
|
Enabled: true,
|
|
ServerName: "example.org",
|
|
CertificatePath: certPem,
|
|
KeyPath: keyPem,
|
|
},
|
|
},
|
|
Address: []netip.Prefix{netip.MustParsePrefix(masqueServerAddress + "/24")},
|
|
AdvertiseRoutes: serverRoutes,
|
|
}, environment.serverProxyPort, ""))
|
|
startInstance(t, masqueInstanceOptions(C.TypeMASQUEClient, &option.MASQUEClientEndpointOptions{
|
|
ServerOptions: option.ServerOptions{
|
|
Server: "127.0.0.1",
|
|
ServerPort: masquePort,
|
|
},
|
|
MASQUEEndpointOptions: option.MASQUEEndpointOptions{MTU: mtu},
|
|
Username: users[0].Username,
|
|
Password: users[0].Password,
|
|
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
|
TLS: &option.OutboundTLSOptions{
|
|
Enabled: true,
|
|
ServerName: "example.org",
|
|
CertificatePath: certPem,
|
|
},
|
|
},
|
|
Version: clientVersion,
|
|
DisableVersionFallback: disableVersionFallback,
|
|
AdvertiseRoutes: clientRoutes,
|
|
}, environment.clientProxyPort, "127.0.0.1"))
|
|
waitForOpenVPNClientReady(t, environment.clientProxyPort, reserveOpenVPNEchoPort(t), masqueServerAddress)
|
|
return environment
|
|
}
|
|
|
|
func masqueInstanceOptions(endpointType string, endpointOptions any, proxyPort uint16, overrideAddress string) option.Options {
|
|
return option.Options{
|
|
Endpoints: []option.Endpoint{
|
|
{
|
|
Type: endpointType,
|
|
Tag: "masque",
|
|
Options: endpointOptions,
|
|
},
|
|
},
|
|
Inbounds: []option.Inbound{
|
|
{
|
|
Type: C.TypeSOCKS,
|
|
Tag: "socks-in",
|
|
Options: &option.SocksInboundOptions{
|
|
ListenOptions: option.ListenOptions{
|
|
Listen: common.Ptr(badoption.Addr(netip.MustParseAddr("127.0.0.1"))),
|
|
ListenPort: proxyPort,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Outbounds: []option.Outbound{
|
|
{
|
|
Type: C.TypeDirect,
|
|
Tag: "direct",
|
|
},
|
|
},
|
|
Route: &option.RouteOptions{
|
|
Rules: []option.Rule{
|
|
{
|
|
Type: C.RuleTypeDefault,
|
|
DefaultOptions: option.DefaultRule{
|
|
RawDefaultRule: option.RawDefaultRule{
|
|
Inbound: []string{"socks-in"},
|
|
},
|
|
RuleAction: option.RuleAction{
|
|
Action: C.RuleActionTypeRoute,
|
|
RouteOptions: option.RouteActionOptions{
|
|
Outbound: "masque",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Type: C.RuleTypeDefault,
|
|
DefaultOptions: option.DefaultRule{
|
|
RawDefaultRule: option.RawDefaultRule{
|
|
Inbound: []string{"masque"},
|
|
},
|
|
RuleAction: option.RuleAction{
|
|
Action: C.RuleActionTypeRoute,
|
|
RouteOptions: option.RouteActionOptions{
|
|
Outbound: "direct",
|
|
RawRouteOptionsActionOptions: option.RawRouteOptionsActionOptions{
|
|
OverrideAddress: overrideAddress,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestMASQUESelfToSelf(t *testing.T) {
|
|
for _, testCase := range []struct {
|
|
name string
|
|
version int
|
|
}{
|
|
{"HTTP3", 3},
|
|
{"HTTP2", 2},
|
|
{"HTTP1", 1},
|
|
} {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
environment := startMASQUE(t, nil, testCase.version, true, 0, nil, nil)
|
|
testSuitOpenVPN(t, environment.clientProxyPort, reserveOpenVPNEchoPort(t), masqueServerAddress)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMASQUEVersionFallback(t *testing.T) {
|
|
environment := startMASQUE(t, []int{1, 2}, 0, false, 0, nil, nil)
|
|
testSuitOpenVPN(t, environment.clientProxyPort, reserveOpenVPNEchoPort(t), masqueServerAddress)
|
|
}
|
|
|
|
func TestMASQUEAdvertiseRoutes(t *testing.T) {
|
|
environment := startMASQUE(t, nil, 0, false, 0, nil, []netip.Prefix{netip.MustParsePrefix("10.9.0.0/24")})
|
|
readinessPort := reserveOpenVPNEchoPort(t)
|
|
closeEcho := startOpenVPNReadinessEcho(t, readinessPort)
|
|
waitForOpenVPNRemoteReady(t, environment.serverProxyPort, masqueSiteAddress, readinessPort, 30*time.Second)
|
|
closeEcho()
|
|
testSuitOpenVPN(t, environment.serverProxyPort, reserveOpenVPNEchoPort(t), masqueSiteAddress)
|
|
}
|
|
|
|
func TestMASQUENoRoute(t *testing.T) {
|
|
environment := startMASQUE(t, nil, 0, false, 0, []netip.Prefix{netip.MustParsePrefix("10.9.0.0/24")}, nil)
|
|
for _, proxyPort := range []uint16{environment.clientProxyPort, environment.serverProxyPort} {
|
|
dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", proxyPort), socks.Version5, "", "")
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
conn, err := dialer.DialContext(ctx, N.NetworkTCP, M.ParseSocksaddrHostPort("10.10.0.1", 80))
|
|
if err == nil {
|
|
conn.Close()
|
|
}
|
|
require.Error(t, err)
|
|
require.NoError(t, ctx.Err())
|
|
cancel()
|
|
}
|
|
}
|
|
|
|
func TestMASQUEPacketTooBig(t *testing.T) {
|
|
environment := startMASQUE(t, nil, 3, true, 1500, nil, nil)
|
|
testSuitOpenVPN(t, environment.clientProxyPort, reserveOpenVPNEchoPort(t), masqueServerAddress)
|
|
}
|
|
|
|
func TestMASQUEReconnectBackoff(t *testing.T) {
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
var accepted atomic.Int32
|
|
go func() {
|
|
for {
|
|
conn, acceptErr := listener.Accept()
|
|
if acceptErr != nil {
|
|
return
|
|
}
|
|
accepted.Add(1)
|
|
conn.Close()
|
|
}
|
|
}()
|
|
t.Cleanup(func() {
|
|
listener.Close()
|
|
})
|
|
startInstance(t, masqueInstanceOptions(C.TypeMASQUEClient, &option.MASQUEClientEndpointOptions{
|
|
ServerOptions: option.ServerOptions{
|
|
Server: "127.0.0.1",
|
|
ServerPort: uint16(listener.Addr().(*net.TCPAddr).Port),
|
|
},
|
|
Version: 1,
|
|
}, reserveOpenVPNTCPPort(t), ""))
|
|
time.Sleep(2500 * time.Millisecond)
|
|
require.Positive(t, accepted.Load())
|
|
require.LessOrEqual(t, accepted.Load(), int32(3))
|
|
}
|
|
|
|
func TestMASQUEStuckClient(t *testing.T) {
|
|
environment := startMASQUE(t, nil, 0, false, 0, nil, nil)
|
|
stuckConn, err := tls.Dial("tcp", "127.0.0.1:"+strconv.Itoa(int(environment.serverPort)), &tls.Config{
|
|
ServerName: "example.org",
|
|
InsecureSkipVerify: true,
|
|
NextProtos: []string{"http/1.1"},
|
|
})
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
stuckConn.Close()
|
|
})
|
|
_, err = stuckConn.Write([]byte("GET /.well-known/masque/ip/*/*/ HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: Upgrade\r\nUpgrade: connect-ip\r\nCapsule-Protocol: ?1\r\nAuthorization: Basic " + base64.StdEncoding.EncodeToString([]byte("sekai:password")) + "\r\n\r\n"))
|
|
require.NoError(t, err)
|
|
reader := std_bufio.NewReader(stuckConn)
|
|
response, err := http.ReadResponse(reader, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, http.StatusSwitchingProtocols, response.StatusCode)
|
|
require.Equal(t, uint64(1), readVarint(t, reader))
|
|
capsuleLength := readVarint(t, reader)
|
|
capsule := make([]byte, capsuleLength)
|
|
_, err = io.ReadFull(reader, capsule)
|
|
require.NoError(t, err)
|
|
require.Equal(t, byte(4), capsule[1])
|
|
stuckAddress := netip.AddrFrom4([4]byte(capsule[2:6]))
|
|
|
|
dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", environment.serverProxyPort), socks.Version5, "", "")
|
|
floodConn, err := dialer.DialContext(context.Background(), N.NetworkUDP, M.SocksaddrFrom(stuckAddress, 9))
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
floodConn.Close()
|
|
})
|
|
floodDone := make(chan struct{})
|
|
go func() {
|
|
defer close(floodDone)
|
|
payload := make([]byte, 1200)
|
|
for range 4096 {
|
|
_, writeErr := floodConn.Write(payload)
|
|
if writeErr != nil {
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
select {
|
|
case <-floodDone:
|
|
case <-time.After(10 * time.Second):
|
|
}
|
|
|
|
echoPort := reserveOpenVPNEchoPort(t)
|
|
closeEcho := startOpenVPNReadinessEcho(t, echoPort)
|
|
defer closeEcho()
|
|
require.NoError(t, probeOpenVPNTCPWithTimeout(environment.clientProxyPort, masqueServerAddress, echoPort, 5*time.Second))
|
|
}
|