mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-25 15:25:01 +03:00
a69ae2b0ea
The route marker took a free TCP port and bound UDP on the same number, which fails whenever that UDP port is in use or reserved (Windows excludes port ranges per protocol), so the TUIC routing tests failed at random on such hosts.
569 lines
20 KiB
Go
569 lines
20 KiB
Go
package service
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/quic-go/quic-go"
|
|
_ "github.com/xtls/xray-core/app/commander"
|
|
_ "github.com/xtls/xray-core/app/dispatcher"
|
|
_ "github.com/xtls/xray-core/app/proxyman/inbound"
|
|
_ "github.com/xtls/xray-core/app/proxyman/outbound"
|
|
"github.com/xtls/xray-core/core"
|
|
"github.com/xtls/xray-core/features/stats"
|
|
"github.com/xtls/xray-core/infra/conf"
|
|
_ "github.com/xtls/xray-core/proxy/blackhole"
|
|
_ "github.com/xtls/xray-core/proxy/dokodemo"
|
|
_ "github.com/xtls/xray-core/proxy/freedom"
|
|
_ "github.com/xtls/xray-core/proxy/socks"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/tuic"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/xray"
|
|
)
|
|
|
|
const tuicRoutingProbe = "tuic-route-probe"
|
|
|
|
func TestTuicUserRoutingDoesNotDoubleCountTraffic(t *testing.T) {
|
|
setupBulkDB(t)
|
|
cert, key := writeTestCertificate(t)
|
|
udp, err := net.ListenPacket("udp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
port := udp.LocalAddr().(*net.UDPAddr).Port
|
|
_ = udp.Close()
|
|
clients := []model.Client{
|
|
{Email: "клиент@test", ID: uuid.NewString(), Password: "specific-password", Enable: true},
|
|
{Email: "user@test", ID: uuid.NewString(), Password: "user-password", Enable: true},
|
|
{Email: "fallback@test", ID: uuid.NewString(), Password: "fallback-password", Enable: true},
|
|
}
|
|
settings := map[string]any{"certificate": cert, "private_key": key, "clients": clients, "alpn": []string{"h3"}}
|
|
ib := mkInbound(t, port, model.TUIC, string(mustJSON(t, settings)))
|
|
ib.Listen = "127.0.0.1"
|
|
if err := database.GetDB().Save(ib).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := (&ClientService{}).SyncInbound(nil, ib.Id, clients); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, c := range clients {
|
|
if err := (&InboundService{}).AddClientStat(database.GetDB(), ib.Id, &c); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
outbounds := []any{map[string]any{"tag": "blocked", "protocol": "blackhole"}}
|
|
for _, tag := range []string{"specific", "user", "fallback"} {
|
|
addr := startTuicRouteMarker(t, tag)
|
|
outbounds = append(outbounds, map[string]any{"tag": tag, "protocol": "freedom", "settings": map[string]any{"redirect": addr}})
|
|
}
|
|
apiPort := freeLoopbackPort(t)
|
|
template := map[string]any{
|
|
"api": map[string]any{"tag": "api", "services": []string{"HandlerService"}},
|
|
"inbounds": []any{map[string]any{"tag": "api", "listen": "127.0.0.1", "port": apiPort, "protocol": "tunnel", "settings": map[string]any{"address": "127.0.0.1"}}},
|
|
"log": map[string]any{"loglevel": "error", "access": "none"}, "stats": map[string]any{},
|
|
"outbounds": outbounds,
|
|
"policy": map[string]any{
|
|
"levels": map[string]any{
|
|
"0": map[string]any{"statsUserUplink": true, "statsUserDownlink": true, "connIdle": 123},
|
|
"4294967295": map[string]any{"statsUserUplink": true, "statsUserDownlink": true},
|
|
},
|
|
"system": map[string]any{"statsInboundUplink": true, "statsInboundDownlink": true},
|
|
},
|
|
"routing": map[string]any{"rules": []any{
|
|
map[string]any{"type": "field", "inboundTag": []string{"api"}, "outboundTag": "api"},
|
|
map[string]any{"type": "field", "inboundTag": []string{ib.Tag}, "user": []string{clients[0].Email}, "outboundTag": "specific"},
|
|
map[string]any{"type": "field", "user": []string{clients[0].Email, clients[1].Email}, "outboundTag": "user"},
|
|
map[string]any{"type": "field", "inboundTag": []string{ib.Tag}, "outboundTag": "fallback"},
|
|
}},
|
|
}
|
|
if err := (&SettingService{}).saveSetting("xrayTemplateConfig", string(mustJSON(t, template))); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg, err := (&XrayService{}).GetXrayConfig()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ordinaryPort := freeLoopbackPort(t)
|
|
cfg.InboundConfigs = append(cfg.InboundConfigs, xray.InboundConfig{
|
|
Listen: mustJSON(t, "127.0.0.1"), Port: ordinaryPort, Protocol: "socks", Tag: "ordinary",
|
|
Settings: mustJSON(t, map[string]any{"auth": "password", "accounts": []any{map[string]any{"user": clients[1].Email, "pass": "ordinary-password"}}}),
|
|
})
|
|
var parsed conf.Config
|
|
if err := json.Unmarshal(mustJSON(t, cfg), &parsed); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var relaySettings struct {
|
|
UserLevel uint32 `json:"userLevel"`
|
|
}
|
|
var relayInbound xray.InboundConfig
|
|
for _, candidate := range cfg.InboundConfigs {
|
|
if candidate.Tag == ib.Tag {
|
|
relayInbound = candidate
|
|
}
|
|
}
|
|
if err := json.Unmarshal(relayInbound.Settings, &relaySettings); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
relayPolicy := parsed.Policy.Levels[relaySettings.UserLevel]
|
|
if relaySettings.UserLevel == 0 || relaySettings.UserLevel == ^uint32(0) || relayPolicy == nil ||
|
|
relayPolicy.ConnectionIdle == nil || *relayPolicy.ConnectionIdle != 123 ||
|
|
relayPolicy.StatsUserUplink || relayPolicy.StatsUserDownlink || relayPolicy.StatsUserOnline {
|
|
t.Fatalf("relay policy lost session limits, collided with a custom level, or enabled duplicate stats: level=%d policy=%+v", relaySettings.UserLevel, relayPolicy)
|
|
}
|
|
if !parsed.Policy.Levels[0].StatsUserUplink || !parsed.Policy.Levels[^uint32(0)].StatsUserUplink {
|
|
t.Fatal("TUIC relay altered existing Xray user policy levels")
|
|
}
|
|
built, err := parsed.Build()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
x, err := core.New(built)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = x.Close() })
|
|
if err := x.Start(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := publishTuicRelayReadiness(cfg); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
counters := x.GetFeature(stats.ManagerType()).(stats.Manager)
|
|
instances, err := (&InboundService{}).DesiredTuicInstances()
|
|
if err != nil || len(instances) != 1 {
|
|
t.Fatalf("desired TUIC instances = %v, error %v", instances, err)
|
|
}
|
|
server, err := tuic.NewServer(instances[0], &tuic.SocksRelay{
|
|
Gate: tuic.ManagedRelayGate(tuic.SOCKSPortForInbound(ib.Id)), InboundID: ib.Id,
|
|
Addr: fmt.Sprintf("127.0.0.1:%d", tuic.SOCKSPortForInbound(ib.Id)), Password: tuic.SocksPassword(),
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := server.Start(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = server.Close() })
|
|
for _, credentials := range []struct{ email, password string }{
|
|
{"unknown@test", tuic.SocksPassword()}, {clients[0].Email, "wrong-password"},
|
|
} {
|
|
relay := &tuic.SocksRelay{Addr: fmt.Sprintf("127.0.0.1:%d", tuic.SOCKSPortForInbound(ib.Id)), Password: credentials.password}
|
|
conn, err := relay.DialTCP(context.Background(), credentials.email, &tuic.Address{Type: tuic.AddrTypeIPv4, IP: net.IPv4(192, 0, 2, 1), Port: 6697})
|
|
if conn != nil {
|
|
_ = conn.Close()
|
|
}
|
|
if err == nil || err.Error() != "tuic socks: auth rejected (code 255)" {
|
|
t.Fatalf("invalid relay credentials were not rejected: %v", err)
|
|
}
|
|
}
|
|
for i, route := range []string{"specific", "user", "fallback"} {
|
|
t.Run(route, func(t *testing.T) {
|
|
client := connectTuicRoutingClient(t, instances[0], clients[i])
|
|
for _, mode := range []string{"tcp", "native", "quic"} {
|
|
t.Run(mode, func(t *testing.T) { probeTuicRoute(t, client, mode, route+":"+tuicRoutingProbe) })
|
|
}
|
|
up, down := int64(3*len(tuicRoutingProbe)), int64(3*len(route+":"+tuicRoutingProbe))
|
|
var gotUp, gotDown int64
|
|
deadline := time.Now().Add(3 * time.Second)
|
|
for gotUp < up || gotDown < down {
|
|
for _, delta := range server.CollectClientTraffic() {
|
|
if delta.Email != clients[i].Email {
|
|
t.Fatalf("unexpected native traffic identity: %+v", delta)
|
|
}
|
|
gotUp += delta.Up
|
|
gotDown += delta.Down
|
|
}
|
|
if time.Now().After(deadline) {
|
|
t.Fatalf("native traffic = %d/%d, want %d/%d", gotUp, gotDown, up, down)
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
if gotUp != up || gotDown != down {
|
|
t.Fatalf("native traffic = %d/%d, want %d/%d", gotUp, gotDown, up, down)
|
|
}
|
|
if _, _, err := (&InboundService{}).AddTraffic(nil, []*xray.ClientTraffic{{Email: clients[i].Email, Up: gotUp, Down: gotDown}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var billed xray.ClientTraffic
|
|
if err := database.GetDB().Where("email = ?", clients[i].Email).First(&billed).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if billed.Up != up || billed.Down != down {
|
|
t.Fatalf("persisted TUIC usage = %d/%d, want %d/%d", billed.Up, billed.Down, up, down)
|
|
}
|
|
for _, direction := range []string{"uplink", "downlink"} {
|
|
if counter := counters.GetCounter("user>>>" + clients[i].Email + ">>>traffic>>>" + direction); counter != nil {
|
|
t.Fatalf("TUIC relay created duplicate Xray %s counter: %d", direction, counter.Value())
|
|
}
|
|
}
|
|
if online := counters.GetOnlineMap("user>>>" + clients[i].Email + ">>>online"); online != nil {
|
|
t.Fatal("TUIC relay exposed loopback addresses as user online IPs")
|
|
}
|
|
if counter := counters.GetCounter("inbound>>>" + ib.Tag + ">>>traffic>>>uplink"); counter == nil || counter.Value() < up {
|
|
t.Fatal("TUIC inbound totals were lost while disabling duplicate per-user counters")
|
|
}
|
|
})
|
|
}
|
|
for _, reset := range []string{"telegram", "scheduled"} {
|
|
t.Run("restore-"+reset, func(t *testing.T) {
|
|
if err := database.GetDB().Model(&xray.ClientTraffic{}).Where("email = ?", clients[0].Email).Update("enable", false).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
server.UpdateUsers(nil)
|
|
var resetErr error
|
|
if reset == "telegram" {
|
|
resetErr = (&InboundService{}).ResetClientTrafficByEmail(clients[0].Email)
|
|
} else {
|
|
_, resetErr = (&ClientService{}).ResetAllClientTraffics(&InboundService{}, ib.Id)
|
|
}
|
|
if resetErr != nil {
|
|
t.Fatal(resetErr)
|
|
}
|
|
renewed, err := (&InboundService{}).DesiredTuicInstances()
|
|
if err != nil || len(renewed) != 1 {
|
|
t.Fatalf("renewal missing: %+v %v", renewed, err)
|
|
}
|
|
server.UpdateUsers(renewed[0].Clients)
|
|
peer := connectTuicRoutingClient(t, renewed[0], clients[0])
|
|
for _, mode := range []string{"tcp", "native", "quic"} {
|
|
probeTuicRoute(t, peer, mode, "specific:"+tuicRoutingProbe)
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("replacement-window", func(t *testing.T) { verifyTuicReplacementWindow(t, cfg, apiPort, ib, server, clients) })
|
|
|
|
verifyTuicHotUserUpdates(t, cfg, apiPort, ib, server, clients, counters)
|
|
|
|
priorOrdinary := int64(0)
|
|
if counter := counters.GetCounter("user>>>" + clients[1].Email + ">>>traffic>>>uplink"); counter != nil {
|
|
priorOrdinary = counter.Value()
|
|
}
|
|
|
|
relay := &tuic.SocksRelay{Addr: fmt.Sprintf("127.0.0.1:%d", ordinaryPort), Password: "ordinary-password"}
|
|
conn, err := relay.DialTCP(context.Background(), clients[1].Email, &tuic.Address{Type: tuic.AddrTypeIPv4, IP: net.IPv4(192, 0, 2, 1), Port: 6697})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer conn.Close()
|
|
_ = conn.SetDeadline(time.Now().Add(3 * time.Second))
|
|
if _, err := conn.Write([]byte(tuicRoutingProbe)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
response := make([]byte, len("user:"+tuicRoutingProbe))
|
|
if _, err := io.ReadFull(conn, response); err != nil || string(response) != "user:"+tuicRoutingProbe {
|
|
t.Fatalf("ordinary SOCKS route = %q, error %v", response, err)
|
|
}
|
|
counter := counters.GetCounter("user>>>" + clients[1].Email + ">>>traffic>>>uplink")
|
|
if counter == nil || counter.Value()-priorOrdinary != int64(len(tuicRoutingProbe)) {
|
|
t.Fatal("ordinary Xray user traffic must still be counted independently of TUIC")
|
|
}
|
|
}
|
|
|
|
// listenTCPAndUDPOnOnePort retries because a free TCP port may be taken or reserved for UDP
|
|
// (Windows excludes port ranges per protocol).
|
|
func listenTCPAndUDPOnOnePort(t *testing.T) (net.Listener, net.PacketConn) {
|
|
t.Helper()
|
|
for range 20 {
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if packet, err := net.ListenPacket("udp", listener.Addr().String()); err == nil {
|
|
return listener, packet
|
|
}
|
|
_ = listener.Close()
|
|
}
|
|
t.Fatal("no port free for both TCP and UDP")
|
|
return nil, nil
|
|
}
|
|
|
|
func startTuicRouteMarker(t *testing.T, marker string) string {
|
|
t.Helper()
|
|
listener, packet := listenTCPAndUDPOnOnePort(t)
|
|
t.Cleanup(func() { _ = listener.Close() })
|
|
t.Cleanup(func() { _ = packet.Close() })
|
|
go func() {
|
|
for {
|
|
conn, err := listener.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
go func() {
|
|
defer conn.Close()
|
|
_ = conn.SetDeadline(time.Now().Add(30 * time.Second))
|
|
body := make([]byte, len(tuicRoutingProbe))
|
|
for {
|
|
if _, err := io.ReadFull(conn, body); err != nil {
|
|
return
|
|
}
|
|
if _, err := conn.Write(append([]byte(marker+":"), body...)); err != nil {
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
}()
|
|
go func() {
|
|
buf := make([]byte, 2048)
|
|
for {
|
|
n, addr, err := packet.ReadFrom(buf)
|
|
if err != nil {
|
|
return
|
|
}
|
|
_, _ = packet.WriteTo(append([]byte(marker+":"), buf[:n]...), addr)
|
|
}
|
|
}()
|
|
return listener.Addr().String()
|
|
}
|
|
|
|
func verifyTuicHotUserUpdates(t *testing.T, initial *xray.Config, apiPort int, inbound *model.Inbound, server *tuic.Server, clients []model.Client, counters stats.Manager) {
|
|
t.Helper()
|
|
svc := &XrayService{}
|
|
if err := (&SettingService{}).SetRestartXrayOnClientDisable(true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
api := &xray.XrayAPI{}
|
|
if err := api.Init(apiPort); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer api.Close()
|
|
ordinary := initial.InboundConfigs[len(initial.InboundConfigs)-1]
|
|
otherRelay := &tuic.SocksRelay{Addr: fmt.Sprintf("127.0.0.1:%d", ordinary.Port), Password: "ordinary-password"}
|
|
other, err := otherRelay.DialTCP(context.Background(), clients[1].Email, &tuic.Address{Type: tuic.AddrTypeIPv4, IP: net.IPv4(192, 0, 2, 1), Port: 6697})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer other.Close()
|
|
assertOtherSurvives := func() {
|
|
t.Helper()
|
|
_ = other.SetDeadline(time.Now().Add(3 * time.Second))
|
|
if _, err := other.Write([]byte(tuicRoutingProbe)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
response := make([]byte, len("user:"+tuicRoutingProbe))
|
|
if _, err := io.ReadFull(other, response); err != nil || string(response) != "user:"+tuicRoutingProbe {
|
|
t.Fatalf("TUIC account edit interrupted an unrelated live connection: %q, %v", response, err)
|
|
}
|
|
}
|
|
assertOtherSurvives()
|
|
instances, err := (&InboundService{}).DesiredTuicInstances()
|
|
if err != nil || len(instances) != 1 {
|
|
t.Fatalf("missing native instance: %+v, %v", instances, err)
|
|
}
|
|
persistent := connectTuicRoutingClient(t, instances[0], clients[1])
|
|
assertNativeSurvives := func() {
|
|
t.Helper()
|
|
for _, mode := range []string{"native", "quic"} {
|
|
probeTuicRoute(t, persistent, mode, "user:"+tuicRoutingProbe)
|
|
}
|
|
}
|
|
assertNativeSurvives()
|
|
t.Run("hot-rollback", func(t *testing.T) {
|
|
var previous xray.InboundConfig
|
|
for _, candidate := range initial.InboundConfigs {
|
|
if candidate.TuicRelay {
|
|
previous = candidate
|
|
}
|
|
}
|
|
invalid := previous
|
|
var settings map[string]any
|
|
if err := json.Unmarshal(previous.Settings, &settings); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
settings["accounts"] = []relayAccount{{User: strings.Repeat("x", 256), Pass: "secret"}}
|
|
invalid.Settings = mustJSON(t, settings)
|
|
err := applyTuicRelayReplacements(api, []xray.InboundReplacement{{Previous: &previous, Next: &invalid}})
|
|
if err == nil || !strings.Contains(err.Error(), "credentials exceed SOCKS5 bounds") {
|
|
t.Fatalf("invalid replacement was not refused: %v", err)
|
|
}
|
|
assertOtherSurvives()
|
|
assertNativeSurvives()
|
|
fresh := connectTuicRoutingClient(t, instances[0], clients[0])
|
|
for _, mode := range []string{"tcp", "native", "quic"} {
|
|
probeTuicRoute(t, fresh, mode, "specific:"+tuicRoutingProbe)
|
|
}
|
|
})
|
|
added := model.Client{Email: "добавлен@test", ID: uuid.NewString(), Password: "new-password", Enable: true}
|
|
before := initial
|
|
for _, step := range []string{"add", "delete", "rename", "empty"} {
|
|
t.Run("hot-"+step, func(t *testing.T) {
|
|
switch step {
|
|
case "add":
|
|
clients = append(clients, added)
|
|
case "delete":
|
|
clients = clients[1:]
|
|
case "rename":
|
|
added.Email = "переименован@test"
|
|
clients[len(clients)-1] = added
|
|
case "empty":
|
|
clients = nil
|
|
}
|
|
var settings map[string]any
|
|
if err := json.Unmarshal([]byte(inbound.Settings), &settings); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
settings["clients"] = clients
|
|
if err := database.GetDB().Model(inbound).Update("settings", string(mustJSON(t, settings))).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
after, err := svc.GetXrayConfig()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
after.InboundConfigs = append(after.InboundConfigs, ordinary)
|
|
diff, hot := xray.ComputeHotDiff(before, after)
|
|
if !hot || svc.restartToDropClients(diff) || applyTuicRelayReplacements(api, diff.TuicRelayChanges) != nil || !applyXrayHotDiff(api, diff) {
|
|
t.Fatalf("account update requires a core restart: hot=%t diff=%+v", hot, diff)
|
|
}
|
|
native, err := (&InboundService{}).DesiredTuicInstances()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(native) > 0 {
|
|
server.UpdateUsers(native[0].Clients)
|
|
peer := connectTuicRoutingClient(t, native[0], added)
|
|
for _, mode := range []string{"tcp", "native", "quic"} {
|
|
probeTuicRoute(t, peer, mode, "fallback:"+tuicRoutingProbe)
|
|
}
|
|
if counter := counters.GetCounter("user>>>" + added.Email + ">>>traffic>>>uplink"); counter != nil {
|
|
t.Fatalf("hot-added user lost relay policy and was billed twice: %d", counter.Value())
|
|
}
|
|
} else {
|
|
server.UpdateUsers(nil)
|
|
}
|
|
if step != "empty" {
|
|
assertNativeSurvives()
|
|
}
|
|
assertOtherSurvives()
|
|
before = after
|
|
})
|
|
}
|
|
}
|
|
|
|
func connectTuicRoutingClient(t *testing.T, inst tuic.Instance, user model.Client) *quic.Conn {
|
|
t.Helper()
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
client, err := quic.DialAddr(ctx, inst.BindTo(), &tls.Config{InsecureSkipVerify: true, NextProtos: inst.ALPN}, &quic.Config{EnableDatagrams: true})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = client.CloseWithError(0, "complete") })
|
|
id := uuid.MustParse(user.ID)
|
|
tlsState := client.ConnectionState().TLS
|
|
token, err := tlsState.ExportKeyingMaterial(string(id[:]), []byte(user.Password), 32)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stream, err := client.OpenUniStreamSync(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := stream.Write(append(append([]byte{tuic.ProtocolVersion, tuic.CmdAuthenticate}, id[:]...), token...)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_ = stream.Close()
|
|
return client
|
|
}
|
|
|
|
func probeTuicRoute(t *testing.T, client *quic.Conn, mode, want string) {
|
|
t.Helper()
|
|
if err := exchangeTuicRoute(client, mode, want); err != nil {
|
|
t.Fatalf("%s relay request failed: %v", mode, err)
|
|
}
|
|
}
|
|
|
|
func exchangeTuicRoute(client *quic.Conn, mode, want string) error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
target := &tuic.Address{Type: tuic.AddrTypeIPv4, IP: net.IPv4(192, 0, 2, 1), Port: 6697}
|
|
var frame bytes.Buffer
|
|
var reader io.Reader
|
|
switch mode {
|
|
case "tcp":
|
|
stream, err := client.OpenStreamSync(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer stream.Close()
|
|
_ = stream.SetDeadline(time.Now().Add(5 * time.Second))
|
|
frame.Write([]byte{tuic.ProtocolVersion, tuic.CmdConnect})
|
|
if err := tuic.WriteAddress(&frame, target); err != nil {
|
|
return err
|
|
}
|
|
frame.WriteString(tuicRoutingProbe)
|
|
if _, err := stream.Write(frame.Bytes()); err != nil {
|
|
return err
|
|
}
|
|
reader = stream
|
|
case "native", "quic":
|
|
association := uint16(1)
|
|
if mode == "quic" {
|
|
association = 2
|
|
}
|
|
if err := tuic.WritePacket(&frame, association, 1, 1, 0, target, []byte(tuicRoutingProbe)); err != nil {
|
|
return err
|
|
}
|
|
if mode == "native" {
|
|
if err := client.SendDatagram(frame.Bytes()); err != nil {
|
|
return err
|
|
}
|
|
response, err := client.ReceiveDatagram(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
reader = bytes.NewReader(response)
|
|
} else {
|
|
stream, err := client.OpenUniStreamSync(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := stream.Write(frame.Bytes()); err != nil {
|
|
return err
|
|
}
|
|
_ = stream.Close()
|
|
response, err := client.AcceptUniStream(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_ = response.SetReadDeadline(time.Now().Add(5 * time.Second))
|
|
reader = response
|
|
}
|
|
_, cmd, err := tuic.ReadCommand(reader)
|
|
if err != nil {
|
|
return fmt.Errorf("read UDP response command: %w", err)
|
|
}
|
|
if cmd != tuic.CmdPacket {
|
|
return fmt.Errorf("UDP response command = %d, want Packet", cmd)
|
|
}
|
|
hdr, err := tuic.ReadPacketHeader(reader)
|
|
if err != nil {
|
|
return fmt.Errorf("read UDP response header: %w", err)
|
|
}
|
|
if hdr.AssocID != association || hdr.Size != uint16(len(want)) {
|
|
return fmt.Errorf("UDP response header = %+v, want association %d and size %d", hdr, association, len(want))
|
|
}
|
|
}
|
|
response := make([]byte, len(want))
|
|
if _, err := io.ReadFull(reader, response); err != nil {
|
|
return err
|
|
}
|
|
if string(response) != want {
|
|
return fmt.Errorf("%s route returned %q, want %q", mode, response, want)
|
|
}
|
|
return nil
|
|
}
|