Compare commits

..
1 Commits
Author SHA1 Message Date
Fangliding 8ec957cc72 refactor 2026-10-27 16:03:46 +08:00
81 changed files with 895 additions and 5575 deletions
+1 -1
View File
@@ -82,7 +82,7 @@ jobs:
uses: docker/setup-buildx-action@v4 uses: docker/setup-buildx-action@v4
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v4.7.0 uses: docker/login-action@v4.6.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
+18 -3
View File
@@ -35,7 +35,24 @@ jobs:
if: steps.check-assets.outputs.missing == 'true' if: steps.check-assets.outputs.missing == 'true'
run: sleep 90 run: sleep 90
check-format-and-proto: check-proto:
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
steps:
- name: Checkout codebase
uses: actions/checkout@v7
- name: Check Proto Version Header
run: |
head -n 4 core/config.pb.go > ref.txt
find . -name "*.pb.go" ! -name "*_grpc.pb.go" -print0 | while IFS= read -r -d '' file; do
if ! cmp -s ref.txt <(head -n 4 "$file"); then
echo "Error: Header mismatch in $file"
head -n 4 "$file"
exit 1
fi
done
check-format:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
permissions: permissions:
@@ -51,8 +68,6 @@ jobs:
cache: false cache: false
- name: Check Format - name: Check Format
run: go run ./infra/vformat/main.go -mode check -pwd ./ run: go run ./infra/vformat/main.go -mode check -pwd ./
- name: Check Protobuf
run: go run ./infra/vprotogen/main.go -mode check -pwd ./
test: test:
needs: check-assets needs: check-assets
+36 -16
View File
@@ -21,10 +21,7 @@ import (
"golang.org/x/net/http2" "golang.org/x/net/http2"
) )
const ( const idleTimeout = 30 * time.Second
idleTimeout = 30 * time.Second
maxDownloadSize int64 = 256 * 1024 * 1024
)
// DO NOT EDIT. Ignored by geodata, rejected by Lua. // DO NOT EDIT. Ignored by geodata, rejected by Lua.
const luaGuardHeader = "\x12\x01\x00" const luaGuardHeader = "\x12\x01\x00"
@@ -36,6 +33,7 @@ type stage struct {
type downloader struct { type downloader struct {
ctx context.Context ctx context.Context
httpClient *http.Client
httpsClient *http.Client httpsClient *http.Client
} }
@@ -63,11 +61,12 @@ func (c *idleConn) Write(b []byte) (int, error) {
func newDownloader(ctx context.Context, dispatcher routing.Dispatcher, outbound string) *downloader { func newDownloader(ctx context.Context, dispatcher routing.Dispatcher, outbound string) *downloader {
return &downloader{ return &downloader{
ctx: ctx, ctx: ctx,
httpsClient: newClient(ctx, dispatcher, outbound), httpClient: newClient(ctx, dispatcher, outbound, false),
httpsClient: newClient(ctx, dispatcher, outbound, true),
} }
} }
func newClient(baseCtx context.Context, dispatcher routing.Dispatcher, outbound string) *http.Client { func newClient(baseCtx context.Context, dispatcher routing.Dispatcher, outbound string, isHTTPS bool) *http.Client {
dial := func(ctx context.Context, network, address string) (net.Conn, error) { dial := func(ctx context.Context, network, address string) (net.Conn, error) {
var conn net.Conn var conn net.Conn
err := task.Run(ctx, func() error { err := task.Run(ctx, func() error {
@@ -92,6 +91,7 @@ func newClient(baseCtx context.Context, dispatcher routing.Dispatcher, outbound
Conn: conn, Conn: conn,
}, nil }, nil
} }
if isHTTPS {
return &http.Client{ return &http.Client{
Transport: &http2.Transport{ Transport: &http2.Transport{
DialTLSContext: func(ctx context.Context, network string, address string, cfg *tls.Config) (net.Conn, error) { DialTLSContext: func(ctx context.Context, network string, address string, cfg *tls.Config) (net.Conn, error) {
@@ -114,12 +114,31 @@ func newClient(baseCtx context.Context, dispatcher routing.Dispatcher, outbound
if req.URL.Scheme != "https" { if req.URL.Scheme != "https" {
return errors.New("redirected to non-https URL: ", req.URL.String()) return errors.New("redirected to non-https URL: ", req.URL.String())
} }
if len(via) >= 20 { if len(via) >= 10 {
return errors.New("stopped after 20 redirects") return errors.New("stopped after 10 redirects")
} }
return nil return nil
}, },
} }
} else {
return &http.Client{
Transport: &http.Transport{
Proxy: nil,
DisableKeepAlives: true,
DialContext: dial,
ResponseHeaderTimeout: idleTimeout,
},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if req.URL.Scheme != "https" {
return errors.New("redirected to non-https URL: ", req.URL.String())
}
if len(via) >= 10 {
return errors.New("stopped after 10 redirects")
}
return nil
},
}
}
} }
func (d *downloader) download(assets []*Asset) ([]stage, error) { func (d *downloader) download(assets []*Asset) ([]stage, error) {
@@ -184,26 +203,27 @@ func (d *downloader) fetch(rawURL string, writer io.Writer) error {
} }
utils.TryDefaultHeadersWith(req.Header, "nav") utils.TryDefaultHeadersWith(req.Header, "nav")
resp, err := d.httpsClient.Do(req) var client *http.Client
if req.URL.Scheme == "https" {
client = d.httpsClient
} else {
client = d.httpClient
}
resp, err := client.Do(req)
if err != nil { if err != nil {
return err return err
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
io.Copy(io.Discard, resp.Body)
return errors.New("unexpected status code: ", resp.StatusCode) return errors.New("unexpected status code: ", resp.StatusCode)
} }
if resp.ContentLength > maxDownloadSize {
return errors.New("geodata download exceeds 256 MiB")
}
n, err := io.Copy(writer, io.LimitReader(resp.Body, maxDownloadSize+1)) n, err := io.Copy(writer, resp.Body)
if err != nil { if err != nil {
return err return err
} }
if n > maxDownloadSize {
return errors.New("geodata download exceeds 256 MiB")
}
if n == 0 { if n == 0 {
return errors.New("empty response body") return errors.New("empty response body")
} }
+2 -55
View File
@@ -7,13 +7,10 @@ import (
"net/http/httptest" "net/http/httptest"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
commongeodata "github.com/xtls/xray-core/common/geodata" commongeodata "github.com/xtls/xray-core/common/geodata"
xlua "github.com/xtls/xray-core/common/lua" xlua "github.com/xtls/xray-core/common/lua"
"github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/core"
"github.com/yuin/gopher-lua/parse" "github.com/yuin/gopher-lua/parse"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
) )
@@ -34,11 +31,11 @@ func TestDownloadOneLuaGuard(t *testing.T) {
if err := os.WriteFile(filepath.Join(dir, "asset.lua"), nil, 0o600); err != nil { if err := os.WriteFile(filepath.Join(dir, "asset.lua"), nil, 0o600); err != nil {
t.Fatal(err) t.Fatal(err)
} }
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(tc.body)) _, _ = w.Write([]byte(tc.body))
})) }))
defer server.Close() defer server.Close()
d := &downloader{ctx: context.Background(), httpsClient: server.Client()} d := &downloader{ctx: context.Background(), httpClient: server.Client()}
staged, err := d.downloadOne(&Asset{Url: server.URL, File: "asset.lua"}) staged, err := d.downloadOne(&Asset{Url: server.URL, File: "asset.lua"})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -74,53 +71,3 @@ func TestDownloadOneLuaGuard(t *testing.T) {
}) })
} }
} }
func TestProtobufConfigValidation(t *testing.T) {
dir := t.TempDir()
t.Setenv("xray.location.asset", dir)
for _, name := range []string{"asset.dat", "asset.DAT"} {
if err := os.WriteFile(filepath.Join(dir, name), nil, 0o600); err != nil {
t.Fatal(err)
}
}
if err := os.Mkdir(filepath.Join(dir, "directory.dat"), 0o700); err != nil {
t.Fatal(err)
}
server, err := core.New(&core.Config{})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = server.Close() })
for _, tc := range []struct {
name string
config *Config
wantError string
}{
{"minutely cron", &Config{Cron: "* * * * *"}, ""},
{"minimum interval", &Config{Cron: "@every 1m"}, ""},
{"short interval", &Config{Cron: "@every 59s"}, "interval must be at least 1 minute"},
{"fractional interval", &Config{Cron: "@every 59.999s"}, "interval must be at least 1 minute"},
{"invalid cron", &Config{Cron: "invalid"}, "invalid geodata cron"},
{"dat extension", &Config{Cron: "@daily", Assets: []*Asset{{Url: "https://example.com/asset.dat", File: "asset.dat"}}}, ""},
{"uppercase extension", &Config{Cron: "@daily", Assets: []*Asset{{Url: "https://example.com/asset.dat", File: "asset.DAT"}}}, ""},
{"invalid extension", &Config{Cron: "@daily", Assets: []*Asset{{Url: "https://example.com/asset.dat", File: "asset.lua"}}}, "extension must be .dat"},
{"missing extension", &Config{Cron: "@daily", Assets: []*Asset{{Url: "https://example.com/asset.dat", File: "asset"}}}, "extension must be .dat"},
{"missing file", &Config{Cron: "@daily", Assets: []*Asset{{Url: "https://example.com/asset.dat", File: "missing.dat"}}}, "invalid geodata asset file"},
{"directory", &Config{Cron: "@daily", Assets: []*Asset{{Url: "https://example.com/asset.dat", File: "directory.dat"}}}, "asset is not a regular file"},
} {
t.Run(tc.name, func(t *testing.T) {
config, err := serial.ToTypedMessage(tc.config).GetInstance()
if err != nil {
t.Fatal(err)
}
_, err = core.CreateObject(server, config)
if tc.wantError == "" {
if err != nil {
t.Fatal(err)
}
} else if err == nil || !strings.Contains(err.Error(), tc.wantError) {
t.Fatalf("CreateObject() error = %v; want %q", err, tc.wantError)
}
})
}
}
+3 -23
View File
@@ -2,16 +2,12 @@ package geodata
import ( import (
"context" "context"
"path/filepath"
"strings"
"sync" "sync"
"time"
"github.com/robfig/cron/v3" "github.com/robfig/cron/v3"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
commongeodata "github.com/xtls/xray-core/common/geodata" commongeodata "github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/platform/filesystem"
"github.com/xtls/xray-core/core" "github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/features/routing" "github.com/xtls/xray-core/features/routing"
) )
@@ -29,24 +25,6 @@ func New(ctx context.Context, config *Config) (*Instance, error) {
if config.Cron == "" { if config.Cron == "" {
return &Instance{}, nil return &Instance{}, nil
} }
if len(config.Assets) > 100 {
return nil, errors.New("geodata supports at most 100 assets")
}
for _, asset := range config.Assets {
if !strings.EqualFold(filepath.Ext(asset.GetFile()), ".dat") {
return nil, errors.New("geodata asset file extension must be .dat")
}
if _, err := filesystem.ResolveAsset(asset.GetFile()); err != nil {
return nil, errors.New("invalid geodata asset file: ", asset.GetFile()).Base(err)
}
}
schedule, err := cron.ParseStandard(config.Cron)
if err != nil {
return nil, errors.New("invalid geodata cron").Base(err)
}
if interval, ok := schedule.(cron.ConstantDelaySchedule); ok && interval.Delay < time.Minute {
return nil, errors.New("geodata cron interval must be at least 1 minute")
}
g := &Instance{ g := &Instance{
assets: config.Assets, assets: config.Assets,
@@ -66,7 +44,9 @@ func New(ctx context.Context, config *Config) (*Instance, error) {
cron.WithChain(cron.SkipIfStillRunning(cron.DiscardLogger)), cron.WithChain(cron.SkipIfStillRunning(cron.DiscardLogger)),
cron.WithLogger(cron.DiscardLogger), cron.WithLogger(cron.DiscardLogger),
) )
g.tasker.Schedule(schedule, cron.FuncJob(g.execute)) if _, err := g.tasker.AddFunc(config.Cron, g.execute); err != nil {
return nil, errors.New("invalid geodata cron").Base(err)
}
errors.LogInfo(ctx, "scheduled geodata reload with cron: ", config.Cron) errors.LogInfo(ctx, "scheduled geodata reload with cron: ", config.Cron)
return g, nil return g, nil
-1
View File
@@ -46,7 +46,6 @@ message RoutingContext {
// - attributes: Select connection's additional attributes. // - attributes: Select connection's additional attributes.
// - outbound: Equivalent as "outbound" and "outbound_group", select both // - outbound: Equivalent as "outbound" and "outbound_group", select both
// outbound tag and outbound group tags. // outbound tag and outbound group tags.
//
// * If FieldSelectors is left empty, all fields will be returned. // * If FieldSelectors is left empty, all fields will be returned.
message SubscribeRoutingStatsRequest { message SubscribeRoutingStatsRequest {
repeated string FieldSelectors = 1; repeated string FieldSelectors = 1;
+17 -59
View File
@@ -8,65 +8,23 @@ import (
var privateIPMatcher = sync.OnceValue(func() IPMatcher { var privateIPMatcher = sync.OnceValue(func() IPMatcher {
return common.Must2(IPReg.BuildIPMatcher(common.Must2(ParseIPRules([]string{ return common.Must2(IPReg.BuildIPMatcher(common.Must2(ParseIPRules([]string{
// https://www.iana.org/assignments/iana-ipv4-special-registry "0.0.0.0/8",
"0.0.0.0/8", // "This network" "10.0.0.0/8",
"0.0.0.0/32", // "This host on this network" "100.64.0.0/10",
"10.0.0.0/8", // Private-Use "127.0.0.0/8",
"100.64.0.0/10", // Shared Address Space "169.254.0.0/16",
"127.0.0.0/8", // Loopback "172.16.0.0/12",
"169.254.0.0/16", // Link Local "192.0.0.0/24",
"172.16.0.0/12", // Private-Use "192.0.2.0/24",
// "192.0.0.0/24", // IETF Protocol Assignments "192.88.99.0/24",
"192.0.0.0/29", // IPv4 Service Continuity Prefix "192.168.0.0/16",
"192.0.0.8/32", // IPv4 dummy address "198.18.0.0/15",
"192.0.0.9/32", // Port Control Protocol Anycast "198.51.100.0/24",
"192.0.0.10/32", // Traversal Using Relays around NAT Anycast "203.0.113.0/24",
"192.0.0.170/32", // NAT64/DNS64 Discovery "224.0.0.0/3",
"192.0.0.171/32", // NAT64/DNS64 Discovery "::/127",
"192.0.2.0/24", // Documentation (TEST-NET-1) "fc00::/7",
// "192.31.196.0/24", // AS112-v4 "fe80::/10",
"192.52.193.0/24", // AMT
// "192.88.99.0/24", // Deprecated (6to4 Relay Anycast)
"192.88.99.2/32", // 6a44-relay anycast address
"192.168.0.0/16", // Private-Use
// "192.175.48.0/24", // Direct Delegation AS112 Service
"198.18.0.0/15", // Benchmarking
"198.51.100.0/24", // Documentation (TEST-NET-2)
"203.0.113.0/24", // Documentation (TEST-NET-3)
"240.0.0.0/4", // Reserved
"255.255.255.255/32", // Limited Broadcast
// https://www.iana.org/assignments/multicast-addresses
"224.0.0.0/4",
// https://www.iana.org/assignments/iana-ipv6-special-registry
"::1/128", // Loopback Address
"::/128", // Unspecified Address
// "::ffff:0:0/96", // IPv4-mapped Address
"64:ff9b::/96", // IPv4-IPv6 Translat.
"64:ff9b:1::/48", // IPv4-IPv6 Translat.
"100::/64", // Discard-Only Address Block
"100:0:0:1::/64", // Dummy IPv6 Prefix
// "2001::/23", // IETF Protocol Assignments
"2001::/32", // TEREDO
"2001:1::1/128", // Port Control Protocol Anycast
"2001:1::2/128", // Traversal Using Relays around NAT Anycast
"2001:1::3/128", // DNS-SD Service Registration Protocol Anycast
"2001:2::/48", // Benchmarking
"2001:3::/32", // AMT
// "2001:4:112::/48", // AS112-v6
// "2001:10::/28", // Deprecated (previously ORCHID)
"2001:20::/28", // ORCHIDv2
"2001:30::/28", // Drone Remote ID Protocol Entity Tags (DETs) Prefix
"2001:db8::/32", // Documentation
"2002::/16", // 6to4
// "2620:4f:8000::/48", // Direct Delegation AS112 Service
"3fff::/20", // Documentation
"5f00::/16", // Segment Routing (SRv6) SIDs
"fc00::/7", // Unique-Local
"fe80::/10", // Link-Local Unicast
// https://www.iana.org/assignments/ipv6-multicast-addresses
"ff00::/8", "ff00::/8",
})))) }))))
}) })
+2 -2
View File
@@ -11,7 +11,7 @@ import (
"github.com/xtls/xray-core/common/base64" "github.com/xtls/xray-core/common/base64"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/platform/filesystem" "github.com/xtls/xray-core/common/platform"
glua "github.com/yuin/gopher-lua" glua "github.com/yuin/gopher-lua"
"github.com/yuin/gopher-lua/parse" "github.com/yuin/gopher-lua/parse"
) )
@@ -36,7 +36,7 @@ func Compile(script string) (*Program, error) {
// TODO: Restore inline script support and re-enable the related tests. // TODO: Restore inline script support and re-enable the related tests.
return nil, errors.New("currently unavailable") return nil, errors.New("currently unavailable")
code, err = base64.Decode(script) code, err = base64.Decode(script)
} else if name, err = filesystem.ResolveLuaFile(script); err == nil { } else if name, err = platform.ResolveLuaFile(script); err == nil {
code, err = os.ReadFile(name) code, err = os.ReadFile(name)
// TODO: Restore privileged signature authorization and re-enable the related tests and CLI entries. // TODO: Restore privileged signature authorization and re-enable the related tests and CLI entries.
// if err == nil { // if err == nil {
+11 -8
View File
@@ -187,20 +187,19 @@ func TestCompileInlineDiagnostics(t *testing.T) {
*/ */
func TestCompileFileExtension(t *testing.T) { func TestCompileFileExtension(t *testing.T) {
for _, name := range []string{"script", "script.dat", "script.lua.txt"} { for _, name := range []string{"script", "script.dat", "script.lua.txt", "script.LUA"} {
if program, err := Compile(name); program != nil || err == nil || !strings.Contains(err.Error(), "Lua file extension must be .lua") {
t.Fatalf("Compile(%q) = %v, %v; want nil program and .lua extension error", name, program, err)
}
}
for _, name := range []string{"script.lua", "script.LUA", "script.Lua", "script.lUa"} {
path := filepath.Join(t.TempDir(), name) path := filepath.Join(t.TempDir(), name)
if err := os.WriteFile(path, []byte("value = 42"), 0o600); err != nil { if err := os.WriteFile(path, []byte("value = 42"), 0o600); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if program, err := Compile(path); program == nil || err != nil { if program, err := Compile(path); program != nil || err == nil || !strings.Contains(err.Error(), "Lua file extension must be .lua") {
t.Fatalf("Compile(%q) = %v, %v; want successful compilation", path, program, err) t.Fatalf("Compile(%q) = %v, %v; want nil program and .lua extension error", path, program, err)
} }
} }
path := writeTestScript(t, "value = 42") + string(filepath.Separator) + "."
if program, err := Compile(path); program == nil || err != nil {
t.Fatalf("Compile(%q) = %v, %v; want acceptance of the resolved .lua path", path, program, err)
}
} }
func TestCompileRejectsReadFailure(t *testing.T) { func TestCompileRejectsReadFailure(t *testing.T) {
@@ -235,6 +234,10 @@ func writeTestScript(t *testing.T, code string) string {
if err := os.WriteFile(path, []byte(code), 0o600); err != nil { if err := os.WriteFile(path, []byte(code), 0o600); err != nil {
t.Fatal(err) t.Fatal(err)
} }
path, err := filepath.EvalSymlinks(path)
if err != nil {
t.Fatal(err)
}
return path return path
} }
+18 -12
View File
@@ -4,7 +4,7 @@ package net
import ( import (
"bufio" "bufio"
"encoding/binary" "encoding/hex"
"fmt" "fmt"
"net" "net"
"os" "os"
@@ -45,7 +45,10 @@ func FindProcess(network, srcIP string, srcPort uint16, destIP string, destPort
panic("Unsupported network type for process lookup.") panic("Unsupported network type for process lookup.")
} }
targetHexAddr := formatProcNetAddress(net.ParseIP(srcIP), Port(srcPort)) targetHexAddr, err := formatLittleEndianString(net.ParseIP(srcIP), Port(srcPort))
if err != nil {
return 0, "", "", errors.New("failed to format address: ", err)
}
portFallback := "" portFallback := ""
if network == "udp" { if network == "udp" {
@@ -84,21 +87,24 @@ func FindProcess(network, srcIP string, srcPort uint16, destIP string, destPort
return pid, procName, absPath, nil return pid, procName, absPath, nil
} }
func formatProcNetAddress(addr net.IP, port Port) string { func formatLittleEndianString(addr net.IP, port Port) (string, error) {
ip := addr
var ipBytes []byte var ipBytes []byte
if ipv4 := addr.To4(); ipv4 != nil { if ip.To4() != nil {
ipBytes = ipv4 ipBytes = ip.To4()
} else { } else {
ipBytes = addr.To16() ipBytes = ip.To16()
}
if ipBytes == nil {
return "", errors.New("invalid IP format for ", addr, ": ", ip)
} }
var formatted strings.Builder for i, j := 0, len(ipBytes)-1; i < j; i, j = i+1, j-1 {
formatted.Grow(len(ipBytes)*2 + 5) ipBytes[i], ipBytes[j] = ipBytes[j], ipBytes[i]
for i := 0; i < len(ipBytes); i += 4 {
fmt.Fprintf(&formatted, "%08X", binary.NativeEndian.Uint32(ipBytes[i:i+4]))
} }
fmt.Fprintf(&formatted, ":%04X", uint16(port)) portHex := fmt.Sprintf("%04X", uint16(port))
return formatted.String() ipHex := strings.ToUpper(hex.EncodeToString(ipBytes))
return fmt.Sprintf("%s:%s", ipHex, portHex), nil
} }
func findInodeInFiles(filePaths []string, targetHexAddr, portFallback string) (string, error) { func findInodeInFiles(filePaths []string, targetHexAddr, portFallback string) (string, error) {
+24 -24
View File
@@ -5,7 +5,6 @@ import (
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/platform" "github.com/xtls/xray-core/common/platform"
@@ -27,6 +26,14 @@ func ReadFile(path string) ([]byte, error) {
return buf.ReadAllToBytes(reader) return buf.ReadAllToBytes(reader)
} }
func ReadAsset(file string) ([]byte, error) {
path, _, err := getAssetFileLocation(file)
if err != nil {
return nil, err
}
return ReadFile(path)
}
func OpenAsset(file string) (io.ReadCloser, int64, error) { func OpenAsset(file string) (io.ReadCloser, int64, error) {
path, info, err := getAssetFileLocation(file) path, info, err := getAssetFileLocation(file)
if err != nil { if err != nil {
@@ -36,6 +43,11 @@ func OpenAsset(file string) (io.ReadCloser, int64, error) {
return r, info.Size(), err return r, info.Size(), err
} }
func StatAsset(file string) (os.FileInfo, error) {
_, info, err := getAssetFileLocation(file)
return info, err
}
func ResolveAsset(file string) (string, error) { func ResolveAsset(file string) (string, error) {
path, _, err := getAssetFileLocation(file) path, _, err := getAssetFileLocation(file)
return path, err return path, err
@@ -67,29 +79,17 @@ func ReadCert(file string) ([]byte, error) {
return ReadFile(platform.GetCertLocation(file)) return ReadFile(platform.GetCertLocation(file))
} }
// ResolveLuaFile finds a local .lua script and returns its absolute path. func CopyFile(dst string, src string) error {
// Relative paths: XRAY_LOCATION_CONFDIR > XRAY_LOCATION_CONFIG > working dir > executable dir. bytes, err := ReadFile(src)
func ResolveLuaFile(path string) (string, error) { if err != nil {
if !strings.EqualFold(filepath.Ext(path), ".lua") { return err
return "", errors.New("Lua file extension must be .lua")
} }
if filepath.IsAbs(path) { f, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY, 0o644)
return path, nil if err != nil {
return err
} }
for _, dir := range []string{ defer f.Close()
platform.GetConfDirPath(),
platform.NewEnvFlag(platform.ConfigLocation).GetValue(func() string { return "" }), _, err = f.Write(bytes)
".", return err
platform.GetExecutableDir(),
} {
if dir != "" {
resolved := filepath.Join(dir, path)
if _, err := os.Stat(resolved); err == nil {
return filepath.Abs(resolved)
} else if !os.IsNotExist(err) {
return "", err
}
}
}
return "", os.ErrNotExist
} }
+2 -47
View File
@@ -1,17 +1,13 @@
package filesystem_test package filesystem_test
import ( import (
"errors"
"os"
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/platform"
. "github.com/xtls/xray-core/common/platform/filesystem" . "github.com/xtls/xray-core/common/platform/filesystem"
) )
func TestResolveAssetRejectsInvalidPath(t *testing.T) { func TestStatAssetRejectsInvalidPath(t *testing.T) {
for _, file := range []string{ for _, file := range []string{
"", "",
".", ".",
@@ -29,49 +25,8 @@ func TestResolveAssetRejectsInvalidPath(t *testing.T) {
`nested\..\geoip.dat`, `nested\..\geoip.dat`,
filepath.Join(t.TempDir(), "geoip.dat"), filepath.Join(t.TempDir(), "geoip.dat"),
} { } {
if _, err := ResolveAsset(file); err == nil { if _, err := StatAsset(file); err == nil {
t.Fatalf("expected error for %q", file) t.Fatalf("expected error for %q", file)
} }
} }
} }
func TestResolveLuaFile(t *testing.T) {
t.Chdir(t.TempDir())
workingDir, err := os.Getwd()
common.Must(err)
executable, err := os.Executable()
common.Must(err)
file, err := os.CreateTemp(filepath.Dir(executable), "lua-*.lua")
common.Must(err)
common.Must(file.Close())
defer os.Remove(file.Name())
name := filepath.Base(file.Name())
paths := []string{
filepath.Join(t.TempDir(), name),
filepath.Join(t.TempDir(), name),
filepath.Join(workingDir, name),
file.Name(),
}
t.Setenv(platform.ConfdirLocation, filepath.Dir(paths[0]))
t.Setenv(platform.ConfigLocation, filepath.Dir(paths[1]))
for _, path := range paths[:3] {
common.Must(os.WriteFile(path, nil, 0o600))
}
if got, err := ResolveLuaFile(paths[2]); err != nil || got != paths[2] {
t.Fatalf("absolute path = %q, %v; want %q", got, err, paths[2])
}
for i, want := range paths {
if i == 2 {
t.Setenv(platform.ConfdirLocation, "")
t.Setenv(platform.ConfigLocation, "")
}
if got, err := ResolveLuaFile(name); err != nil || got != want {
t.Fatalf("resolved path = %q, %v; want %q", got, err, want)
}
common.Must(os.Remove(want))
}
if _, err := ResolveLuaFile(name); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("missing file error = %v", err)
}
}
+2 -2
View File
@@ -14,7 +14,7 @@ func LineSeparator() string {
// GetAssetLocation searches for `file` in the env dir, the executable dir, and certain locations // GetAssetLocation searches for `file` in the env dir, the executable dir, and certain locations
func GetAssetLocation(file string) string { func GetAssetLocation(file string) string {
assetPath := NewEnvFlag(AssetLocation).GetValue(GetExecutableDir) assetPath := NewEnvFlag(AssetLocation).GetValue(getExecutableDir)
defPath := filepath.Join(assetPath, file) defPath := filepath.Join(assetPath, file)
for _, p := range []string{ for _, p := range []string{
defPath, defPath,
@@ -36,6 +36,6 @@ func GetAssetLocation(file string) string {
// GetCertLocation searches for `file` in the env dir and the executable dir // GetCertLocation searches for `file` in the env dir and the executable dir
func GetCertLocation(file string) string { func GetCertLocation(file string) string {
certPath := NewEnvFlag(CertLocation).GetValue(GetExecutableDir) certPath := NewEnvFlag(CertLocation).GetValue(getExecutableDir)
return filepath.Join(certPath, file) return filepath.Join(certPath, file)
} }
+65 -2
View File
@@ -1,6 +1,8 @@
package platform // import "github.com/xtls/xray-core/common/platform" package platform // import "github.com/xtls/xray-core/common/platform"
import ( import (
"errors"
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
@@ -72,7 +74,7 @@ func NormalizeEnvName(name string) string {
return strings.ReplaceAll(strings.ToUpper(strings.TrimSpace(name)), ".", "_") return strings.ReplaceAll(strings.ToUpper(strings.TrimSpace(name)), ".", "_")
} }
func GetExecutableDir() string { func getExecutableDir() string {
exec, err := os.Executable() exec, err := os.Executable()
if err != nil { if err != nil {
return "" return ""
@@ -81,7 +83,7 @@ func GetExecutableDir() string {
} }
func GetConfigurationPath() string { func GetConfigurationPath() string {
configPath := NewEnvFlag(ConfigLocation).GetValue(GetExecutableDir) configPath := NewEnvFlag(ConfigLocation).GetValue(getExecutableDir)
return filepath.Join(configPath, "config.json") return filepath.Join(configPath, "config.json")
} }
@@ -90,3 +92,64 @@ func GetConfDirPath() string {
configPath := NewEnvFlag(ConfdirLocation).GetValue(func() string { return "" }) configPath := NewEnvFlag(ConfdirLocation).GetValue(func() string { return "" })
return configPath return configPath
} }
// ResolveLuaFile finds a local .lua script and returns its resolved absolute path.
// Relative paths: XRAY_LOCATION_CONFDIR > XRAY_LOCATION_CONFIG > working dir > executable dir.
func ResolveLuaFile(path string) (string, error) {
if path == "" {
return "", errors.New("Lua file path is empty")
}
paths := []string{path}
if !filepath.IsAbs(path) {
paths = nil
for _, dir := range []string{
GetConfDirPath(),
NewEnvFlag(ConfigLocation).GetValue(func() string { return "" }),
".",
getExecutableDir(),
} {
if dir != "" {
paths = append(paths, filepath.Join(dir, path))
}
}
}
path, err := resolveFile(paths)
if err != nil {
return "", err
}
return resolveLuaTarget(path)
}
func resolveLuaTarget(path string) (string, error) {
path, err := filepath.EvalSymlinks(path)
if err != nil {
return "", fmt.Errorf("failed to resolve Lua file symlinks: %w", err)
}
if filepath.Ext(path) != ".lua" {
return "", errors.New("Lua file extension must be .lua")
}
return path, nil
}
func resolveFile(paths []string) (string, error) {
var tried []string
for _, path := range paths {
path, err := filepath.Abs(path)
if err != nil {
return "", fmt.Errorf("failed to resolve file path: %w", err)
}
tried = append(tried, path)
info, err := os.Stat(path)
if errors.Is(err, os.ErrNotExist) {
continue
}
if err != nil {
return "", fmt.Errorf("failed to inspect file %q: %w", path, err)
}
if !info.Mode().IsRegular() {
return "", fmt.Errorf("file is not a regular file: %s", path)
}
return path, nil
}
return "", fmt.Errorf("file not found; tried %q: %w", tried, os.ErrNotExist)
}
+88
View File
@@ -1,9 +1,11 @@
package platform_test package platform_test
import ( import (
"errors"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings"
"testing" "testing"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
@@ -64,3 +66,89 @@ func TestGetAssetLocation(t *testing.T) {
} }
} }
} }
func TestResolveLuaFile(t *testing.T) {
workingDir := t.TempDir()
t.Chdir(workingDir)
executable, err := os.Executable()
common.Must(err)
file, err := os.CreateTemp(filepath.Dir(executable), "lua-*.lua")
common.Must(err)
common.Must(file.Close())
defer os.Remove(file.Name())
name := filepath.Base(file.Name())
paths := []string{
filepath.Join(t.TempDir(), name),
filepath.Join(t.TempDir(), name),
filepath.Join(workingDir, name),
file.Name(),
}
t.Setenv(ConfdirLocation, filepath.Dir(paths[0]))
t.Setenv(ConfigLocation, filepath.Dir(paths[1]))
for _, path := range paths[:3] {
common.Must(os.WriteFile(path, nil, 0o600))
}
for i, path := range paths {
paths[i], err = filepath.EvalSymlinks(path)
common.Must(err)
}
if got, err := ResolveLuaFile(paths[2]); err != nil || got != paths[2] {
t.Fatalf("absolute path = %q, %v; want %q", got, err, paths[2])
}
for i, want := range paths {
if i == 2 {
t.Setenv(ConfdirLocation, "")
t.Setenv(ConfigLocation, "")
}
if got, err := ResolveLuaFile(name); err != nil || got != want {
t.Fatalf("resolved path = %q, %v; want %q", got, err, want)
}
common.Must(os.Remove(want))
}
if _, err := ResolveLuaFile(name); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("missing file error = %v", err)
}
t.Setenv(ConfdirLocation, filepath.Dir(paths[0]))
t.Setenv(ConfigLocation, filepath.Dir(paths[1]))
common.Must(os.Mkdir(paths[0], 0o700))
common.Must(os.WriteFile(paths[1], nil, 0o600))
for _, path := range []string{"", name, filepath.Join(t.TempDir(), name)} {
if _, err := ResolveLuaFile(path); err == nil {
t.Fatalf("accepted invalid path %q", path)
}
}
}
func TestResolveLuaFileSymlink(t *testing.T) {
for _, tc := range []struct {
link, target string
valid bool
}{
{"alias.lua", "data.dat", false},
{"alias.dat", "script.lua", true},
} {
t.Run(tc.link, func(t *testing.T) {
dir, err := filepath.EvalSymlinks(t.TempDir())
common.Must(err)
target := filepath.Join(dir, tc.target)
common.Must(os.WriteFile(target, nil, 0o600))
link := filepath.Join(dir, tc.link)
if err := os.Symlink(tc.target, link); err != nil {
if runtime.GOOS == "windows" {
t.Skipf("cannot create symlinks: %v", err)
}
t.Fatal(err)
}
got, err := ResolveLuaFile(link)
if tc.valid {
if err != nil || got != target {
t.Fatalf("ResolveLuaFile(%q) = %q, %v; want %q", link, got, err, target)
}
} else if err == nil || got != "" || !strings.Contains(err.Error(), "Lua file extension must be .lua") {
t.Fatalf("ResolveLuaFile(%q) = %q, %v; want .lua extension error", link, got, err)
}
})
}
}
+2 -2
View File
@@ -11,12 +11,12 @@ func LineSeparator() string {
// GetAssetLocation searches for `file` in the env dir and the executable dir // GetAssetLocation searches for `file` in the env dir and the executable dir
func GetAssetLocation(file string) string { func GetAssetLocation(file string) string {
assetPath := NewEnvFlag(AssetLocation).GetValue(GetExecutableDir) assetPath := NewEnvFlag(AssetLocation).GetValue(getExecutableDir)
return filepath.Join(assetPath, file) return filepath.Join(assetPath, file)
} }
// GetCertLocation searches for `file` in the env dir and the executable dir // GetCertLocation searches for `file` in the env dir and the executable dir
func GetCertLocation(file string) string { func GetCertLocation(file string) string {
certPath := NewEnvFlag(CertLocation).GetValue(GetExecutableDir) certPath := NewEnvFlag(CertLocation).GetValue(getExecutableDir)
return filepath.Join(certPath, file) return filepath.Join(certPath, file)
} }
+30 -29
View File
@@ -3,11 +3,7 @@ package signal
import ( import (
"context" "context"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/task"
) )
type ActivityUpdater interface { type ActivityUpdater interface {
@@ -17,10 +13,10 @@ type ActivityUpdater interface {
type ActivityTimer struct { type ActivityTimer struct {
mu sync.RWMutex mu sync.RWMutex
updated chan struct{} updated chan struct{}
checkTask *task.Periodic checker *time.Timer
timeout time.Duration
onTimeout func() onTimeout func()
consumed atomic.Bool consumed bool
once sync.Once
} }
func (t *ActivityTimer) Update() { func (t *ActivityTimer) Update() {
@@ -30,30 +26,29 @@ func (t *ActivityTimer) Update() {
} }
} }
func (t *ActivityTimer) check() error { func (t *ActivityTimer) check() bool {
select { select {
case <-t.updated: case <-t.updated:
return true
default: default:
t.finish() t.finish()
return false
} }
return nil
} }
func (t *ActivityTimer) finish() { func (t *ActivityTimer) finish() {
t.once.Do(func() {
t.consumed.Store(true)
t.mu.Lock() t.mu.Lock()
defer t.mu.Unlock() if t.consumed {
t.mu.Unlock()
common.CloseIfExists(t.checkTask) return
}
t.checker.Stop()
t.consumed = true
t.mu.Unlock()
t.onTimeout() t.onTimeout()
})
} }
func (t *ActivityTimer) SetTimeout(timeout time.Duration) { func (t *ActivityTimer) SetTimeout(timeout time.Duration) {
if t.consumed.Load() {
return
}
if timeout == 0 { if timeout == 0 {
t.finish() t.finish()
return return
@@ -61,25 +56,31 @@ func (t *ActivityTimer) SetTimeout(timeout time.Duration) {
t.mu.Lock() t.mu.Lock()
defer t.mu.Unlock() defer t.mu.Unlock()
// double check, just in case if t.consumed {
if t.consumed.Load() {
return return
} }
newCheckTask := &task.Periodic{ t.timeout = timeout
Interval: timeout, t.checker.Reset(timeout)
Execute: t.check,
}
common.CloseIfExists(t.checkTask)
t.checkTask = newCheckTask
t.Update()
common.Must(newCheckTask.Start())
} }
func CancelAfterInactivity(ctx context.Context, cancel context.CancelFunc, timeout time.Duration) *ActivityTimer { func CancelAfterInactivity(ctx context.Context, cancel context.CancelFunc, timeout time.Duration) *ActivityTimer {
timer := &ActivityTimer{ timer := &ActivityTimer{
updated: make(chan struct{}, 1), updated: make(chan struct{}, 1),
onTimeout: cancel, onTimeout: cancel,
timeout: timeout,
}
timer.checker = time.AfterFunc(timeout, func() {
if timer.check() {
timer.mu.Lock()
defer timer.mu.Unlock()
if timer.consumed {
return
}
timer.checker.Reset(timer.timeout)
}
})
if timeout == 0 {
timer.SetTimeout(0)
} }
timer.SetTimeout(timeout)
return timer return timer
} }
+1 -1
View File
@@ -20,7 +20,7 @@ import (
var ( var (
Version_x byte = 26 Version_x byte = 26
Version_y byte = 10 Version_y byte = 10
Version_z byte = 29 Version_z byte = 17
) )
var ( var (
+2
View File
@@ -1,3 +1,5 @@
package core package core
//go:generate go install -v google.golang.org/protobuf/cmd/protoc-gen-go@latest
//go:generate go install -v google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
//go:generate go run ../infra/vprotogen/main.go -pwd ./.. //go:generate go run ../infra/vprotogen/main.go -pwd ./..
-10
View File
@@ -2,7 +2,6 @@ package localdns
import ( import (
"context" "context"
"net/netip"
"syscall" "syscall"
"time" "time"
@@ -101,15 +100,6 @@ func New() *Client {
if internet.IsSkippedDNSServer(address) { if internet.IsSkippedDNSServer(address) {
return nil, errors.New("skipped DNS server ", address) return nil, errors.New("skipped DNS server ", address)
} }
// A socket of the server's IP version only needs that version bound
// to the interface, which a dual-stack one would need both of.
if ap, err := netip.ParseAddrPort(address); err == nil && (network == "udp" || network == "tcp") {
if ap.Addr().Unmap().Is4() {
network += "4"
} else {
network += "6"
}
}
return d.DialContext(ctx, network, address) return d.DialContext(ctx, network, address)
}, },
} }
+2 -9
View File
@@ -4,7 +4,6 @@ go 1.27
require ( require (
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e
github.com/bufbuild/protocompile v0.14.1
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344
github.com/golang/mock v1.7.0-rc.1 github.com/golang/mock v1.7.0-rc.1
github.com/google/go-cmp v0.7.0 github.com/google/go-cmp v0.7.0
@@ -15,7 +14,7 @@ require (
github.com/miekg/dns v1.1.73 github.com/miekg/dns v1.1.73
github.com/pelletier/go-toml v1.9.5 github.com/pelletier/go-toml v1.9.5
github.com/pion/stun/v3 v3.1.7 github.com/pion/stun/v3 v3.1.7
github.com/pires/go-proxyproto v0.16.0 github.com/pires/go-proxyproto v0.15.0
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/stretchr/testify v1.12.1 github.com/stretchr/testify v1.12.1
@@ -33,7 +32,7 @@ require (
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
golang.zx2c4.com/wireguard/windows v1.1.1 golang.zx2c4.com/wireguard/windows v1.1.1
google.golang.org/grpc v1.84.0 google.golang.org/grpc v1.84.0
google.golang.org/protobuf v1.36.11 google.golang.org/protobuf v1.36.12
h12.io/socks v1.0.3 h12.io/socks v1.0.3
layeh.com/gopher-luar v1.0.11 layeh.com/gopher-luar v1.0.11
lukechampine.com/blake3 v1.4.1 lukechampine.com/blake3 v1.4.1
@@ -59,12 +58,6 @@ require (
golang.org/x/text v0.43.0 // indirect golang.org/x/text v0.43.0 // indirect
golang.org/x/tools v0.51.0 // indirect golang.org/x/tools v0.51.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 // indirect gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 // indirect
) )
tool (
google.golang.org/grpc/cmd/protoc-gen-go-grpc
google.golang.org/protobuf/cmd/protoc-gen-go
)
+2 -8
View File
@@ -2,8 +2,6 @@ github.com/andybalholm/brotli v1.2.3 h1:8H1qwOkl2LPfjf3YezB90JnCliZb6SInJ/OJkEbA
github.com/andybalholm/brotli v1.2.3/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/andybalholm/brotli v1.2.3/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e h1:5mgtR5gwIgBKMiGI1QdXldZZ+SNor06Nbu1wCBulQBg= github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e h1:5mgtR5gwIgBKMiGI1QdXldZZ+SNor06Nbu1wCBulQBg=
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e/go.mod h1:x7qxEvX6MCVtDuBKHj3E+88+BtrbEMuAL5qGUKItjW8= github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e/go.mod h1:x7qxEvX6MCVtDuBKHj3E+88+BtrbEMuAL5qGUKItjW8=
github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@@ -61,8 +59,8 @@ github.com/pion/stun/v3 v3.1.7 h1:uRXMTlGLf89WgItGNyZ6aR5jMTX0NBbybXADpQCzn+E=
github.com/pion/stun/v3 v3.1.7/go.mod h1:Nq77RW4aRrSNrltf2ksUJLjxWeipj4lnlgdsYIxC8g8= github.com/pion/stun/v3 v3.1.7/go.mod h1:Nq77RW4aRrSNrltf2ksUJLjxWeipj4lnlgdsYIxC8g8=
github.com/pion/transport/v4 v4.1.0 h1:8S+nF2reM2cJuqC6g78OVy2BBgmbdns+acx3jA97BvQ= github.com/pion/transport/v4 v4.1.0 h1:8S+nF2reM2cJuqC6g78OVy2BBgmbdns+acx3jA97BvQ=
github.com/pion/transport/v4 v4.1.0/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM= github.com/pion/transport/v4 v4.1.0/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM=
github.com/pires/go-proxyproto v0.16.0 h1:9KYT/fM3wpjT8bdMEj/7FBcoiUG1KyzEjRYnPu+vz7A= github.com/pires/go-proxyproto v0.15.0 h1:dTshmNbFm/D+0+sbrxUuddPOZ5Y0B7c5NhtsBkm6LqI=
github.com/pires/go-proxyproto v0.16.0/go.mod h1:n7yhg8yIfNl3KgYDCwBzmeUHNpKTVnUhcWpc7rV4RW4= github.com/pires/go-proxyproto v0.15.0/go.mod h1:OXsCrKwrK2tXS9YrI5tkHx5xaQlO8FH3lFW76orFh24=
github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0= github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0=
github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk= github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
@@ -154,10 +152,6 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 h1:
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.84.0 h1:soMyaPJ8pAak5PIQ0DGBUir0XRo2fRoMqhNWMLlLxO0= google.golang.org/grpc v1.84.0 h1:soMyaPJ8pAak5PIQ0DGBUir0XRo2fRoMqhNWMLlLxO0=
google.golang.org/grpc v1.84.0/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0= google.golang.org/grpc v1.84.0/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.0 h1:6Al3kEFFP9VJhRz3DID6quisgPnTeZVr4lep9kkxdPA=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.0/go.mod h1:QLvsjh0OIR0TYBeiu2bkWGTJBUNQ64st52iWj/yA93I=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-2
View File
@@ -91,7 +91,6 @@ func (c *FreedomConfig) Build() (proto.Message, error) {
} }
if c.Fragment != nil { if c.Fragment != nil {
errors.PrintDeprecatedFeatureWarning(`"fragment" in Freedom`, `"fragment" in "streamSettings.finalmask.tcp"`)
config.Fragment = new(freedom.Fragment) config.Fragment = new(freedom.Fragment)
switch strings.ToLower(c.Fragment.Packets) { switch strings.ToLower(c.Fragment.Packets) {
@@ -148,7 +147,6 @@ func (c *FreedomConfig) Build() (proto.Message, error) {
} }
if c.Noises != nil { if c.Noises != nil {
errors.PrintDeprecatedFeatureWarning(`"noises" in Freedom`, `"noise" in "streamSettings.finalmask.udp"`)
for _, n := range c.Noises { for _, n := range c.Noises {
NConfig, err := ParseNoise(n) NConfig, err := ParseNoise(n)
if err != nil { if err != nil {
+4
View File
@@ -6,6 +6,7 @@ import (
"github.com/robfig/cron/v3" "github.com/robfig/cron/v3"
"github.com/xtls/xray-core/app/geodata" "github.com/xtls/xray-core/app/geodata"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/platform/filesystem"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
) )
@@ -18,6 +19,9 @@ func (c *GeodataAssetConfig) Build() (*geodata.Asset, error) {
if err := validateHTTPS(c.URL); err != nil { if err := validateHTTPS(c.URL); err != nil {
return nil, errors.New("invalid geodata asset url: ", c.URL).Base(err) return nil, errors.New("invalid geodata asset url: ", c.URL).Base(err)
} }
if _, err := filesystem.StatAsset(c.File); err != nil {
return nil, errors.New("invalid geodata asset file: ", c.File).Base(err)
}
return &geodata.Asset{ return &geodata.Asset{
Url: c.URL, Url: c.URL,
File: c.File, File: c.File,
+23
View File
@@ -1,6 +1,7 @@
package conf_test package conf_test
import ( import (
"path/filepath"
"testing" "testing"
"github.com/xtls/xray-core/app/geodata" "github.com/xtls/xray-core/app/geodata"
@@ -8,6 +9,8 @@ import (
) )
func TestGeodataConfig(t *testing.T) { func TestGeodataConfig(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
creator := func() Buildable { creator := func() Buildable {
return new(GeodataConfig) return new(GeodataConfig)
} }
@@ -35,7 +38,27 @@ func TestGeodataConfig(t *testing.T) {
}) })
} }
func TestGeodataAssetConfig(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
if _, err := (&GeodataAssetConfig{
URL: "https://example.com/geoip.dat",
File: "geoip.dat",
}).Build(); err != nil {
t.Fatal(err)
}
if _, err := (&GeodataAssetConfig{
URL: "https://example.com/geoip.dat",
File: "missing.dat",
}).Build(); err == nil {
t.Fatal("expected error")
}
}
func TestGeodataAssetConfigInvalidURL(t *testing.T) { func TestGeodataAssetConfigInvalidURL(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
for _, rawURL := range []string{ for _, rawURL := range []string{
"", "",
"http://example.com/geoip.dat", "http://example.com/geoip.dat",
-21
View File
@@ -15,11 +15,9 @@ import (
googleuuid "github.com/google/uuid" googleuuid "github.com/google/uuid"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/geodata"
xlua "github.com/xtls/xray-core/common/lua" xlua "github.com/xtls/xray-core/common/lua"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/finalmask/filter"
"github.com/xtls/xray-core/transport/internet/finalmask/fragment" "github.com/xtls/xray-core/transport/internet/finalmask/fragment"
"github.com/xtls/xray-core/transport/internet/finalmask/header/custom" "github.com/xtls/xray-core/transport/internet/finalmask/header/custom"
"github.com/xtls/xray-core/transport/internet/finalmask/mkcp/aes128gcm" "github.com/xtls/xray-core/transport/internet/finalmask/mkcp/aes128gcm"
@@ -76,7 +74,6 @@ var (
customVarNamePattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`) customVarNamePattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
tcpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{ tcpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{
"filter": func() interface{} { return new(Filter) },
"header-custom": func() interface{} { return new(HeaderCustomTCP) }, "header-custom": func() interface{} { return new(HeaderCustomTCP) },
"fragment": func() interface{} { return new(FragmentMask) }, "fragment": func() interface{} { return new(FragmentMask) },
"sudoku": func() interface{} { return new(Sudoku) }, "sudoku": func() interface{} { return new(Sudoku) },
@@ -84,7 +81,6 @@ var (
}, "type", "settings") }, "type", "settings")
udpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{ udpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{
"filter": func() interface{} { return new(Filter) },
"header-custom": func() interface{} { return new(HeaderCustomUDP) }, "header-custom": func() interface{} { return new(HeaderCustomUDP) },
"mkcp-legacy": func() interface{} { return new(MkcpLegacy) }, "mkcp-legacy": func() interface{} { return new(MkcpLegacy) },
"noise": func() interface{} { return new(NoiseMask) }, "noise": func() interface{} { return new(NoiseMask) },
@@ -97,23 +93,6 @@ var (
}, "type", "settings") }, "type", "settings")
) )
type Filter struct {
BlockedSourceIPs []string `json:"blockedSourceIPs"`
AllowedSourceIPs []string `json:"allowedSourceIPs"`
}
func (c *Filter) Build() (proto.Message, error) {
rules, err := geodata.ParseIPRules(c.BlockedSourceIPs)
if err != nil {
return nil, errors.New(`invalid "blockedSourceIPs"`).Base(err)
}
allowed, err := geodata.ParseIPRules(c.AllowedSourceIPs)
if err != nil {
return nil, errors.New(`invalid "allowedSourceIPs"`).Base(err)
}
return &filter.Config{BlockedSourceIps: rules, AllowedSourceIps: allowed}, nil
}
type TCPItem struct { type TCPItem struct {
Delay Int32Range `json:"delay"` Delay Int32Range `json:"delay"`
Rand int32 `json:"rand"` Rand int32 `json:"rand"`
@@ -1,79 +0,0 @@
package conf_test
import (
"encoding/json"
"fmt"
"net"
"strings"
"testing"
"github.com/xtls/xray-core/common/geodata"
. "github.com/xtls/xray-core/infra/conf"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/finalmask/filter"
)
func TestFinalMaskFilter(t *testing.T) {
for _, network := range []string{"tcp", "udp"} {
for _, test := range []struct {
name, settings string
blocked, allowed int
}{
{"blacklist", `{"blockedSourceIPs":["192.0.2.0/24"]}`, 1, 0},
{"whitelist", `{"allowedSourceIPs":["192.0.2.0/24"]}`, 0, 1},
{"both", `{"blockedSourceIPs":["192.0.2.0/24"],"allowedSourceIPs":["192.0.2.0/24"]}`, 1, 1},
{"empty_whitelist", `{"allowedSourceIPs":[]}`, 0, 0},
{"empty_blacklist", `{"blockedSourceIPs":[]}`, 0, 0},
} {
t.Run(network+"/"+test.name, func(t *testing.T) {
input := fmt.Sprintf(`{"finalmask":{"%s":[{"type":"filter","settings":%s}]}}`, network, test.settings)
var config StreamConfig
if err := json.Unmarshal([]byte(input), &config); err != nil {
t.Fatal(err)
}
built, err := config.Build()
if err != nil {
t.Fatal(err)
}
masks := built.Tcpmasks
if network == "udp" {
masks = built.Udpmasks
}
if len(masks) != 1 {
t.Fatalf("expected one mask, got %d", len(masks))
}
instance, err := masks[0].GetInstance()
if err != nil {
t.Fatal(err)
}
filter, ok := instance.(*filter.Config)
if !ok {
t.Fatalf("unexpected mask type: %T", instance)
}
if len(filter.BlockedSourceIps) != test.blocked || len(filter.AllowedSourceIps) != test.allowed {
t.Fatalf("unexpected filter policy: %v", filter)
}
for _, rules := range [][]*geodata.IPRule{filter.BlockedSourceIps, filter.AllowedSourceIps} {
for _, rule := range rules {
cidr := rule.GetCustom().GetCidr()
if cidr.GetPrefix() != 24 || !net.IP(cidr.GetIp()).Equal(net.IPv4(192, 0, 2, 0)) {
t.Fatalf("unexpected IP rule: %v", cidr)
}
}
}
if _, err := internet.ToMemoryStreamConfig(built); err != nil {
t.Fatalf("failed to initialize runtime finalmask: %v", err)
}
})
}
}
for _, field := range []string{"blockedSourceIPs", "allowedSourceIPs"} {
var config Filter
if err := json.Unmarshal([]byte(fmt.Sprintf(`{"%s":["192.0.2.0/33"]}`, field)), &config); err != nil {
t.Fatal(err)
}
if _, err := config.Build(); err == nil || !strings.Contains(err.Error(), field) {
t.Errorf("expected invalid %s, got %v", field, err)
}
}
}
-18
View File
@@ -90,7 +90,6 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
config.ProtocolName = protocol config.ProtocolName = protocol
} }
var xdriveH3 bool
switch strings.ToLower(c.Security) { switch strings.ToLower(c.Security) {
case "", "none": case "", "none":
case "tls": case "tls":
@@ -105,7 +104,6 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
tm := serial.ToTypedMessage(ts) tm := serial.ToTypedMessage(ts)
config.SecuritySettings = append(config.SecuritySettings, tm) config.SecuritySettings = append(config.SecuritySettings, tm)
config.SecurityType = tm.Type config.SecurityType = tm.Type
xdriveH3 = config.ProtocolName == "xdrive" && tlsSettings.ALPN != nil && slices.Equal(*tlsSettings.ALPN, StringList{"h3"})
if config.ProtocolName == "xdrive" && tlsSettings.ALPN != nil && slices.Equal(*tlsSettings.ALPN, StringList{"http/1.1"}) && tlsSettings.ECHConfigList == "" { if config.ProtocolName == "xdrive" && tlsSettings.ALPN != nil && slices.Equal(*tlsSettings.ALPN, StringList{"http/1.1"}) && tlsSettings.ECHConfigList == "" {
// a fingerprint puts its own ALPN into the hello, except Go TLS and the // a fingerprint puts its own ALPN into the hello, except Go TLS and the
// randomized ones, which take it from the config or send none // randomized ones, which take it from the config or send none
@@ -221,7 +219,6 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
}) })
} }
if c.XDRIVESettings != nil { if c.XDRIVESettings != nil {
c.XDRIVESettings.h3 = xdriveH3
xs, err := c.XDRIVESettings.Build() xs, err := c.XDRIVESettings.Build()
if err != nil { if err != nil {
return nil, errors.New("Failed to build XDRIVE config.").Base(err) return nil, errors.New("Failed to build XDRIVE config.").Base(err)
@@ -263,20 +260,5 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
} }
} }
if xdriveH3 && c.FinalMask != nil {
if c.XDRIVESettings != nil && c.XDRIVESettings.Service == "google-drive" {
// noise only sends datagrams of its own ahead of the QUIC ones, the
// other masks change those or need a peer that undoes them
for _, mask := range c.FinalMask.Udp {
if strings.ToLower(mask.Type) != "noise" {
return nil, errors.New(`XDRIVE: over h3 to Google Drive the only "udp" mask that works is "noise", not "`, mask.Type, `"`)
}
}
}
if config.QuicParams != nil && (config.QuicParams.Congestion == "brutal" || config.QuicParams.Congestion == "force-brutal") {
return nil, errors.New(`XDRIVE: brutal is for Hysteria servers, over h3 "congestion" is "bbr" or "reno", not "`, config.QuicParams.Congestion, `"`)
}
}
return config, nil return config, nil
} }
+1 -51
View File
@@ -997,18 +997,6 @@ type XDriveConfig struct {
EagerWindowMs uint32 `json:"eagerWindowMs"` EagerWindowMs uint32 `json:"eagerWindowMs"`
HoleTimeoutMs uint32 `json:"holeTimeoutMs"` HoleTimeoutMs uint32 `json:"holeTimeoutMs"`
Template json.RawMessage `json:"template"` Template json.RawMessage `json:"template"`
S3 *XDriveS3Config `json:"s3"`
Xmux XmuxConfig `json:"xmux"`
// set by StreamConfig when "alpn" is ["h3"]
h3 bool
}
type XDriveS3Config struct {
Endpoint string `json:"endpoint"`
Region string `json:"region"`
Bucket string `json:"bucket"`
PathStyle *bool `json:"pathStyle"`
} }
// Build implements Buildable. // Build implements Buildable.
@@ -1027,27 +1015,8 @@ func (c *XDriveConfig) Build() (proto.Message, error) {
if len(c.Template) == 0 { if len(c.Template) == 0 {
return nil, errors.New(`service "template" needs a "template" object`) return nil, errors.New(`service "template" needs a "template" object`)
} }
case "s3":
if c.S3 == nil {
return nil, errors.New(`service "s3" needs an "s3" object`)
}
default: default:
return nil, errors.New(`unknown "service": "`, c.Service, `", use "google-drive", "template", "s3" or "local"`) return nil, errors.New(`unknown "service": "`, c.Service, `", use "google-drive", "template" or "local"`)
}
if c.Xmux.MaxConnections.To > 0 && c.Xmux.MaxConcurrency.To > 0 {
return nil, errors.New("maxConnections cannot be specified together with maxConcurrency")
}
if c.Xmux == (XmuxConfig{}) {
c.Xmux.MaxConnections.From = 1
c.Xmux.MaxConnections.To = 1
if c.Service == "google-drive" && !c.h3 {
// Google gives an h2 connection 1 MiB of window, which a few sessions on
// a long rtt would share, an h3 connection has no such limit
c.Xmux.MaxConnections.From = 4
c.Xmux.MaxConnections.To = 4
}
c.Xmux.HMaxReusableSecs.From = 1800
c.Xmux.HMaxReusableSecs.To = 3000
} }
config := &xdrive.Config{ config := &xdrive.Config{
RemoteFolder: c.RemoteFolder, RemoteFolder: c.RemoteFolder,
@@ -1062,25 +1031,6 @@ func (c *XDriveConfig) Build() (proto.Message, error) {
EagerWindowMs: c.EagerWindowMs, EagerWindowMs: c.EagerWindowMs,
HoleTimeoutMs: c.HoleTimeoutMs, HoleTimeoutMs: c.HoleTimeoutMs,
Template: string(c.Template), Template: string(c.Template),
Xmux: &splithttp.XmuxConfig{
MaxConcurrency: newRangeConfig(c.Xmux.MaxConcurrency),
MaxConnections: newRangeConfig(c.Xmux.MaxConnections),
CMaxReuseTimes: newRangeConfig(c.Xmux.CMaxReuseTimes),
HMaxRequestTimes: newRangeConfig(c.Xmux.HMaxRequestTimes),
HMaxReusableSecs: newRangeConfig(c.Xmux.HMaxReusableSecs),
HKeepAlivePeriod: c.Xmux.HKeepAlivePeriod,
},
}
if c.Service == "s3" {
config.S3 = &xdrive.S3Config{
Endpoint: c.S3.Endpoint,
Region: c.S3.Region,
Bucket: c.S3.Bucket,
PathStyle: c.S3.PathStyle == nil || *c.S3.PathStyle,
}
if err := xdrive.ValidateS3Config(config); err != nil {
return nil, err
}
} }
return config, nil return config, nil
} }
+1 -124
View File
@@ -9,7 +9,6 @@ import (
. "github.com/xtls/xray-core/infra/conf" . "github.com/xtls/xray-core/infra/conf"
"github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet"
finalmaskcustom "github.com/xtls/xray-core/transport/internet/finalmask/header/custom" finalmaskcustom "github.com/xtls/xray-core/transport/internet/finalmask/header/custom"
"github.com/xtls/xray-core/transport/internet/splithttp"
"github.com/xtls/xray-core/transport/internet/xdrive" "github.com/xtls/xray-core/transport/internet/xdrive"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
) )
@@ -324,7 +323,7 @@ func TestXDriveRejectsUnknownService(t *testing.T) {
if err := json.Unmarshal([]byte(`{"remoteFolder": "/tmp/xdrive", "service": "Dropbox"}`), config); err != nil { if err := json.Unmarshal([]byte(`{"remoteFolder": "/tmp/xdrive", "service": "Dropbox"}`), config); err != nil {
t.Fatalf("Unmarshal: %v", err) t.Fatalf("Unmarshal: %v", err)
} }
if _, err := config.Build(); err == nil || !strings.Contains(err.Error(), `use "google-drive", "template", "s3" or "local"`) { if _, err := config.Build(); err == nil || !strings.Contains(err.Error(), `use "google-drive", "template" or "local"`) {
t.Fatalf("expected an unknown service error, got %v", err) t.Fatalf("expected an unknown service error, got %v", err)
} }
} }
@@ -425,81 +424,6 @@ func TestXDriveHTTP1ALPNWarning(t *testing.T) {
} }
} }
// over h3 Google Drive takes only the noise mask, and no service takes brutal
func TestXDriveH3Checks(t *testing.T) {
const (
drive = `"method": "xdrive", "xdriveSettings": {"remoteFolder": "f", "service": "google-drive", "secrets": ["a", "b", "c"]}`
local = `"method": "xdrive", "xdriveSettings": {"remoteFolder": "/tmp/xdrive", "service": "local"}`
noise = `{"type": "Noise", "settings": {"noise": [{"rand": "50-100"}]}}`
salamander = `{"type": "salamander", "settings": {"password": "p"}}`
forceBrutal = `{"quicParams": {"congestion": "force-brutal", "brutalUp": "10 mbps"}}`
)
for _, c := range []struct {
alpn, transport, finalmask string
ok bool
}{
{`["h3"]`, drive, `{"udp": [` + noise + `]}`, true},
{`["h3"]`, drive, `{"udp": [` + salamander + `]}`, false},
{`["h3"]`, drive, `{"udp": [` + noise + `, ` + salamander + `]}`, false},
{`["h3"]`, local, `{"udp": [` + salamander + `]}`, true},
{`["h3"]`, `"method": "xdrive"`, `{"udp": [` + salamander + `]}`, true},
{`["h2"]`, drive, `{"udp": [` + salamander + `]}`, true},
{`null`, drive, `{"udp": [` + salamander + `]}`, true},
{`["h3"]`, drive, `null`, true},
{`["h3"]`, drive, `{"quicParams": {"congestion": "bbr"}}`, true},
{`["h3"]`, drive, `{"quicParams": {"congestion": "brutal"}}`, false},
{`["h3"]`, local, forceBrutal, false},
{`["h3", "h2"]`, drive, `{"quicParams": {"congestion": "brutal"}}`, true},
{`["h3"]`, `"method": "xhttp"`, forceBrutal, true},
} {
config := new(StreamConfig)
if err := json.Unmarshal([]byte(`{
`+c.transport+`,
"security": "tls",
"tlsSettings": {"alpn": `+c.alpn+`},
"finalmask": `+c.finalmask+`
}`), config); err != nil {
t.Fatalf("Unmarshal: %v", err)
}
if _, err := config.Build(); (err == nil) != c.ok {
t.Errorf("alpn %s, %s, finalmask %s: %v", c.alpn, c.transport, c.finalmask, err)
}
}
}
// one h3 connection is not held back by a window like h2, so h3 takes one by default
func TestXDriveH3Connections(t *testing.T) {
for _, c := range []struct {
alpn string
want int32
}{
{`["h3"]`, 1},
{`["h2"]`, 4},
{`null`, 4},
} {
config := new(StreamConfig)
if err := json.Unmarshal([]byte(`{
"method": "xdrive",
"xdriveSettings": {"remoteFolder": "f", "service": "google-drive", "secrets": ["a", "b", "c"]},
"security": "tls",
"tlsSettings": {"alpn": `+c.alpn+`}
}`), config); err != nil {
t.Fatalf("Unmarshal: %v", err)
}
built, err := config.Build()
if err != nil {
t.Fatalf("alpn %s: %v", c.alpn, err)
}
xs, err := built.TransportSettings[0].Settings.GetInstance()
if err != nil {
t.Fatalf("alpn %s: %v", c.alpn, err)
}
if got := xs.(*xdrive.Config).Xmux.MaxConnections; got.From != c.want || got.To != c.want {
t.Errorf("alpn %s: maxConnections is %d-%d, want %d", c.alpn, got.From, got.To, c.want)
}
}
}
func TestXDriveTemplateNeedsTemplate(t *testing.T) { func TestXDriveTemplateNeedsTemplate(t *testing.T) {
config := new(XDriveConfig) config := new(XDriveConfig)
if err := json.Unmarshal([]byte(`{"remoteFolder": "f", "service": "template"}`), config); err != nil { if err := json.Unmarshal([]byte(`{"remoteFolder": "f", "service": "template"}`), config); err != nil {
@@ -589,50 +513,3 @@ func TestXDriveInboundConfig(t *testing.T) {
} }
} }
} }
func TestXDriveXmux(t *testing.T) {
defaults := func(connections int32) *splithttp.XmuxConfig {
return &splithttp.XmuxConfig{
MaxConcurrency: &splithttp.RangeConfig{},
MaxConnections: &splithttp.RangeConfig{From: connections, To: connections},
CMaxReuseTimes: &splithttp.RangeConfig{},
HMaxRequestTimes: &splithttp.RangeConfig{},
HMaxReusableSecs: &splithttp.RangeConfig{From: 1800, To: 3000},
}
}
for _, c := range []struct {
input string
want *splithttp.XmuxConfig
}{
{`{"service": "google-drive", "secrets": ["a", "b", "c"]}`, defaults(4)},
{`{"service": "template", "template": {}}`, defaults(1)},
{`{"service": "google-drive", "secrets": ["a", "b", "c"], "xmux": {"maxConcurrency": "1-2", "hMaxRequestTimes": 100, "hKeepAlivePeriod": -1}}`, &splithttp.XmuxConfig{
MaxConcurrency: &splithttp.RangeConfig{From: 1, To: 2},
MaxConnections: &splithttp.RangeConfig{},
CMaxReuseTimes: &splithttp.RangeConfig{},
HMaxRequestTimes: &splithttp.RangeConfig{From: 100, To: 100},
HMaxReusableSecs: &splithttp.RangeConfig{},
HKeepAlivePeriod: -1,
}},
} {
config := new(XDriveConfig)
if err := json.Unmarshal([]byte(c.input), config); err != nil {
t.Fatalf("%s: Unmarshal: %v", c.input, err)
}
built, err := config.Build()
if err != nil {
t.Fatalf("%s: Build: %v", c.input, err)
}
if got := built.(*xdrive.Config).Xmux; !proto.Equal(got, c.want) {
t.Errorf("%s: xmux is %v, want %v", c.input, got, c.want)
}
}
config := new(XDriveConfig)
if err := json.Unmarshal([]byte(`{"service": "template", "template": {}, "xmux": {"maxConnections": 2, "maxConcurrency": 8}}`), config); err != nil {
t.Fatalf("Unmarshal: %v", err)
}
if _, err := config.Build(); err == nil {
t.Error("Build accepted maxConnections with maxConcurrency")
}
}
+3 -6
View File
@@ -237,11 +237,10 @@ type OutboundDetourConfig struct {
func requiresTransportSecurity(address *Address) bool { func requiresTransportSecurity(address *Address) bool {
if address == nil || address.Address == nil { if address == nil || address.Address == nil {
return true return false
} }
if address.Family().IsIP() { if address.Family().IsIP() {
ip := address.IP() return !geodata.GetPrivateIPMatcher().Match(address.IP())
return !ip.IsPrivate() && !ip.IsLoopback() && !ip.IsLinkLocalUnicast()
} }
domain := strings.TrimSuffix(strings.ToLower(address.Domain()), ".") domain := strings.TrimSuffix(strings.ToLower(address.Domain()), ".")
return !geodata.GetPrivateDomainMatcher().MatchAny(domain) return !geodata.GetPrivateDomainMatcher().MatchAny(domain)
@@ -609,6 +608,7 @@ func (c *Config) Build() (*core.Config, error) {
config := &core.Config{ config := &core.Config{
App: []*serial.TypedMessage{ App: []*serial.TypedMessage{
serial.ToTypedMessage(&dispatcher.Config{}), serial.ToTypedMessage(&dispatcher.Config{}),
serial.ToTypedMessage(&proxyman.InboundConfig{}),
serial.ToTypedMessage(&proxyman.OutboundConfig{}), serial.ToTypedMessage(&proxyman.OutboundConfig{}),
}, },
} }
@@ -718,9 +718,6 @@ func (c *Config) Build() (*core.Config, error) {
config.App = append(config.App, serial.ToTypedMessage(r)) config.App = append(config.App, serial.ToTypedMessage(r))
} }
// Initialize the inbound module last to ensure inbound connections are handled correctly.
config.App = append(config.App, serial.ToTypedMessage(&proxyman.InboundConfig{}))
var inbounds []InboundDetourConfig var inbounds []InboundDetourConfig
if len(c.InboundConfigs) > 0 { if len(c.InboundConfigs) > 0 {
+1 -1
View File
@@ -90,6 +90,7 @@ func TestXrayConfig(t *testing.T) {
AccessLogPath: "/var/log/xray/access.log", AccessLogPath: "/var/log/xray/access.log",
}), }),
serial.ToTypedMessage(&dispatcher.Config{}), serial.ToTypedMessage(&dispatcher.Config{}),
serial.ToTypedMessage(&proxyman.InboundConfig{}),
serial.ToTypedMessage(&proxyman.OutboundConfig{}), serial.ToTypedMessage(&proxyman.OutboundConfig{}),
serial.ToTypedMessage(&router.Config{ serial.ToTypedMessage(&router.Config{
DomainStrategy: router.Config_AsIs, DomainStrategy: router.Config_AsIs,
@@ -110,7 +111,6 @@ func TestXrayConfig(t *testing.T) {
}, },
}, },
}), }),
serial.ToTypedMessage(&proxyman.InboundConfig{}),
}, },
Inbound: []*core.InboundHandlerConfig{ Inbound: []*core.InboundHandlerConfig{
{ {
+201 -180
View File
@@ -1,87 +1,149 @@
package main package main
import ( import (
"bytes" "errors"
"context"
"flag" "flag"
"fmt" "fmt"
"io/fs" "go/build"
"io"
"net/http"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"regexp"
"runtime"
"strconv"
"strings" "strings"
"github.com/bufbuild/protocompile"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protodesc"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/descriptorpb"
"google.golang.org/protobuf/types/pluginpb"
) )
var ( var directory = flag.String("pwd", "", "Working directory of Xray vprotogen.")
directory = flag.String("pwd", "", "Working directory of Xray vprotogen.")
mode = flag.String("mode", "generate", "Execution mode. 'generate' (default) compiles and saves changes. 'check' reports out-of-date files and exits with non-zero.")
)
// topoSort sorts file descriptors so each dependency appears before files importing it. // envFile returns the name of the Go environment configuration file.
func topoSort(files []protoreflect.FileDescriptor) []*descriptorpb.FileDescriptorProto { // Copy from https://github.com/golang/go/blob/c4f2a9788a7be04daf931ac54382fbe2cb754938/src/cmd/go/internal/cfg/cfg.go#L150-L166
fileMap := make(map[string]protoreflect.FileDescriptor) func envFile() (string, error) {
for _, f := range files { if file := os.Getenv("GOENV"); file != "" {
fileMap[f.Path()] = f if file == "off" {
return "", errors.New("GOENV=off")
} }
return file, nil
visited := make(map[string]bool)
var sorted []*descriptorpb.FileDescriptorProto
var visit func(fd protoreflect.FileDescriptor)
visit = func(fd protoreflect.FileDescriptor) {
if visited[fd.Path()] {
return
} }
visited[fd.Path()] = true dir, err := os.UserConfigDir()
imports := fd.Imports()
for i := 0; i < imports.Len(); i++ {
if dep, ok := fileMap[imports.Get(i).Path()]; ok {
visit(dep)
}
}
sorted = append(sorted, protodesc.ToFileDescriptorProto(fd))
}
for _, f := range files {
visit(f)
}
return sorted
}
func runPlugin(pluginName, pwd string, req *pluginpb.CodeGeneratorRequest) (*pluginpb.CodeGeneratorResponse, error) {
reqBytes, err := proto.Marshal(req)
if err != nil { if err != nil {
return nil, err return "", err
}
if dir == "" {
return "", errors.New("missing user-config dir")
}
return filepath.Join(dir, "go", "env"), nil
} }
cmd := exec.Command("go", "tool", pluginName) // GetRuntimeEnv returns the value of runtime environment variable,
cmd.Dir = pwd // that is set by running following command: `go env -w key=value`.
cmd.Stdin = bytes.NewReader(reqBytes) func GetRuntimeEnv(key string) (string, error) {
cmd.Env = os.Environ() file, err := envFile()
var stdout, stderr bytes.Buffer if err != nil {
cmd.Stdout = &stdout return "", err
cmd.Stderr = &stderr }
if file == "" {
if err := cmd.Run(); err != nil { return "", errors.New("missing runtime env file")
return nil, fmt.Errorf("%v: %s", err, stderr.String()) }
var data []byte
var runtimeEnv string
data, readErr := os.ReadFile(file)
if readErr != nil {
return "", readErr
}
envStrings := strings.Split(string(data), "\n")
for _, envItem := range envStrings {
envItem = strings.TrimSuffix(envItem, "\r")
envKeyValue := strings.Split(envItem, "=")
if strings.EqualFold(strings.TrimSpace(envKeyValue[0]), key) {
runtimeEnv = strings.TrimSpace(envKeyValue[1])
}
}
return runtimeEnv, nil
} }
var resp pluginpb.CodeGeneratorResponse // GetGOBIN returns GOBIN environment variable as a string. It will NOT be empty.
if err := proto.Unmarshal(stdout.Bytes(), &resp); err != nil { func GetGOBIN() string {
return nil, fmt.Errorf("failed to unmarshal response: %w", err) // The one set by user explicitly by `export GOBIN=/path` or `env GOBIN=/path command`
GOBIN := os.Getenv("GOBIN")
if GOBIN == "" {
var err error
// The one set by user by running `go env -w GOBIN=/path`
GOBIN, err = GetRuntimeEnv("GOBIN")
if err != nil {
// The default one that Golang uses
return filepath.Join(build.Default.GOPATH, "bin")
} }
if resp.GetError() != "" { if GOBIN == "" {
return nil, fmt.Errorf("%s", resp.GetError()) return filepath.Join(build.Default.GOPATH, "bin")
} }
return &resp, nil return GOBIN
}
return GOBIN
}
func whichProtoc(suffix, targetedVersion string) (string, error) {
protoc := "protoc" + suffix
path, err := exec.LookPath(protoc)
if err != nil {
return "", fmt.Errorf(`
Command "%s" not found.
Make sure that %s is in your system path or current path.
Download %s v%s or later from https://github.com/protocolbuffers/protobuf/releases
`, protoc, protoc, protoc, targetedVersion)
}
return path, nil
}
func getProjectProtocVersion(url string) (string, error) {
resp, err := http.Get(url)
if err != nil {
return "", errors.New("can not get the version of protobuf used in xray project")
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", errors.New("can not read from body")
}
versionRegexp := regexp.MustCompile(`\/\/\s*protoc\s*v\d+\.(\d+\.\d+)`)
matched := versionRegexp.FindStringSubmatch(string(body))
return matched[1], nil
}
func getInstalledProtocVersion(protocPath string) (string, error) {
cmd := exec.Command(protocPath, "--version")
cmd.Env = append(cmd.Env, os.Environ()...)
output, cmdErr := cmd.CombinedOutput()
if cmdErr != nil {
return "", cmdErr
}
versionRegexp := regexp.MustCompile(`protoc\s*(\d+\.\d+)`)
matched := versionRegexp.FindStringSubmatch(string(output))
return matched[1], nil
}
func parseVersion(s string, width int) int64 {
strList := strings.Split(s, ".")
format := fmt.Sprintf("%%s%%0%ds", width)
v := ""
for _, value := range strList {
v = fmt.Sprintf(format, v, value)
}
var result int64
var err error
if result, err = strconv.ParseInt(v, 10, 64); err != nil {
return 0
}
return result
}
func needToUpdate(targetedVersion, installedVersion string) bool {
vt := parseVersion(targetedVersion, 4)
vi := parseVersion(installedVersion, 4)
return vt > vi
} }
func main() { func main() {
@@ -92,147 +154,106 @@ func main() {
flag.Parse() flag.Parse()
if !filepath.IsAbs(*directory) { if !filepath.IsAbs(*directory) {
pwd, err := os.Getwd() pwd, wdErr := os.Getwd()
if err != nil { if wdErr != nil {
fmt.Println("Can not get current working directory.") fmt.Println("Can not get current working directory.")
os.Exit(1) os.Exit(1)
} }
*directory = filepath.Join(pwd, *directory) *directory = filepath.Join(pwd, *directory)
} }
pwd := *directory
isCheck := false pwd := *directory
switch *mode { GOBIN := GetGOBIN()
case "generate", "": binPath := os.Getenv("PATH")
isCheck = false pathSlice := []string{pwd, GOBIN, binPath}
case "check": binPath = strings.Join(pathSlice, string(os.PathListSeparator))
isCheck = true os.Setenv("PATH", binPath)
default:
fmt.Fprintf(os.Stderr, "Unrecognized 'mode': %s. Use 'generate' or 'check'.\n", *mode) suffix := ""
if runtime.GOOS == "windows" {
suffix = ".exe"
}
/*
targetedVersion, err := getProjectProtocVersion("https://raw.githubusercontent.com/XTLS/Xray-core/HEAD/core/config.pb.go")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
*/
targetedVersion := ""
protoc, err := whichProtoc(suffix, targetedVersion)
if err != nil {
fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
if isCheck { installedVersion, err := getInstalledProtocVersion(protoc)
fmt.Println("Checking for out-of-date Protobuf generated files...") if err != nil {
fmt.Println(err)
os.Exit(1)
} }
var protoFiles []string if needToUpdate(targetedVersion, installedVersion) {
var pbCount int fmt.Printf(`
err := filepath.WalkDir(pwd, func(path string, d fs.DirEntry, err error) error { You are using an old protobuf version, please update to v%s or later.
if err != nil || d.IsDir() { Download it from https://github.com/protocolbuffers/protobuf/releases
* Protobuf version used in xray project: v%s
* Protobuf version you have installed: v%s
`, targetedVersion, targetedVersion, installedVersion)
os.Exit(1)
}
protoFilesMap := make(map[string][]string)
walkErr := filepath.Walk(pwd, func(path string, info os.FileInfo, err error) error {
if err != nil {
fmt.Println(err)
return err return err
} }
if strings.HasSuffix(d.Name(), ".proto") {
rel, _ := filepath.Rel(pwd, path) if info.IsDir() {
protoFiles = append(protoFiles, filepath.ToSlash(rel)) return nil
} }
if strings.HasSuffix(d.Name(), ".pb.go") {
pbCount++ dir := filepath.Dir(path)
filename := filepath.Base(path)
if strings.HasSuffix(filename, ".proto") {
path = path[len(pwd)+1:]
protoFilesMap[dir] = append(protoFilesMap[dir], path)
} }
return nil return nil
}) })
if err != nil { if walkErr != nil {
fmt.Println("Walk error:", err) fmt.Println(walkErr)
os.Exit(1)
}
if len(protoFiles) == 0 {
if isCheck && pbCount > 0 {
fmt.Fprintf(os.Stderr, "Found %d .pb.go files without any .proto files. Remove these orphaned files.\n", pbCount)
os.Exit(1)
}
return
}
compiler := protocompile.Compiler{
Resolver: protocompile.WithStandardImports(&protocompile.SourceResolver{
ImportPaths: []string{pwd},
}),
SourceInfoMode: protocompile.SourceInfoStandard,
}
linkedFiles, err := compiler.Compile(context.Background(), protoFiles...)
if err != nil {
fmt.Fprintf(os.Stderr, "Protobuf compilation failed:\n%v\n", err)
os.Exit(1) os.Exit(1)
} }
var fdList []protoreflect.FileDescriptor for _, files := range protoFilesMap {
for _, f := range linkedFiles { for _, relProtoFile := range files {
fdList = append(fdList, f) args := []string{
"--go_out", pwd,
"--go_opt", "paths=source_relative",
"--go-grpc_out", pwd,
"--go-grpc_opt", "paths=source_relative",
"--plugin", "protoc-gen-go=" + filepath.Join(GOBIN, "protoc-gen-go"+suffix),
"--plugin", "protoc-gen-go-grpc=" + filepath.Join(GOBIN, "protoc-gen-go-grpc"+suffix),
} }
args = append(args, relProtoFile)
req := &pluginpb.CodeGeneratorRequest{ cmd := exec.Command(protoc, args...)
FileToGenerate: protoFiles, cmd.Env = append(cmd.Env, os.Environ()...)
Parameter: proto.String("paths=source_relative"), cmd.Dir = pwd
ProtoFile: topoSort(fdList), output, cmdErr := cmd.CombinedOutput()
// TODO: replace with unknown in the future if len(output) > 0 {
CompilerVersion: &pluginpb.Version{ fmt.Println(string(output))
Major: proto.Int32(6),
Minor: proto.Int32(33),
Patch: proto.Int32(5),
},
} }
if cmdErr != nil {
var diffCount int fmt.Println(cmdErr)
generatedFiles := make(map[string]struct{})
for _, plugin := range []string{"protoc-gen-go", "protoc-gen-go-grpc"} {
resp, err := runPlugin(plugin, pwd, req)
if err != nil {
fmt.Fprintf(os.Stderr, "%s failed: %v\n", plugin, err)
os.Exit(1)
}
for _, f := range resp.File {
targetPath := filepath.Join(pwd, filepath.FromSlash(f.GetName()))
generatedFiles[targetPath] = struct{}{}
content := []byte(f.GetContent())
hasDiff := false
existing, err := os.ReadFile(targetPath)
if err != nil {
hasDiff = true
} else {
oldNorm := bytes.ReplaceAll(existing, []byte("\r\n"), []byte("\n"))
newNorm := bytes.ReplaceAll(content, []byte("\r\n"), []byte("\n"))
if !bytes.Equal(oldNorm, newNorm) {
hasDiff = true
}
}
if hasDiff {
diffCount++
fmt.Println(targetPath)
if isCheck {
continue
}
if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
fmt.Fprintf(os.Stderr, "Error creating directory: %v\n", err)
os.Exit(1)
}
mode := os.FileMode(0o644)
if stat, err := os.Stat(targetPath); err == nil {
mode = stat.Mode().Perm()
}
if err := os.WriteFile(targetPath, content, mode); err != nil {
fmt.Fprintf(os.Stderr, "Error writing %s: %v\n", targetPath, err)
os.Exit(1) os.Exit(1)
} }
} }
} }
} }
if isCheck {
if pbCount != len(generatedFiles) {
fmt.Printf("Protobuf file count mismatch: found %d, expected %d. Remove orphaned files and regenerate.\n", pbCount, len(generatedFiles))
diffCount++
}
if diffCount > 0 {
fmt.Println("Protobuf generated file problem(s) found.")
fmt.Println("Please run 'go run ./infra/vprotogen' to regenerate them.")
os.Exit(1)
}
fmt.Println("All Protobuf generated file check has been passed.")
}
}
-2
View File
@@ -92,8 +92,6 @@ func executeAddRules(cmd *base.Command, args []string) {
if err != nil { if err != nil {
base.Fatalf("failed to perform AddRule: %s", err) base.Fatalf("failed to perform AddRule: %s", err)
} }
// if multiple files are specified, use append to merge them instead of overriding previous one
shouldAppend = true
showJSONResponse(resp) showJSONResponse(resp)
} }
} }
+11 -4
View File
@@ -3,7 +3,6 @@ package lua
import ( import (
"os" "os"
"path/filepath" "path/filepath"
"strings"
xlua "github.com/xtls/xray-core/common/lua" xlua "github.com/xtls/xray-core/common/lua"
"github.com/xtls/xray-core/main/commands/base" "github.com/xtls/xray-core/main/commands/base"
@@ -33,13 +32,14 @@ func executeSign(cmd *base.Command, args []string) {
cmd.Usage() cmd.Usage()
} }
if !strings.EqualFold(filepath.Ext(args[0]), ".lua") {
base.Fatalf("Lua file extension must be .lua")
}
info, err := os.Stat(args[0]) info, err := os.Stat(args[0])
if err != nil { if err != nil {
base.Fatalf("failed to inspect Lua file: %s", err) base.Fatalf("failed to inspect Lua file: %s", err)
} }
if !info.Mode().IsRegular() {
base.Fatalf("Lua input is not a regular file: %s", args[0])
}
code, err := os.ReadFile(args[0]) code, err := os.ReadFile(args[0])
if err != nil { if err != nil {
base.Fatalf("failed to read Lua file: %s", err) base.Fatalf("failed to read Lua file: %s", err)
@@ -67,7 +67,14 @@ func executeSign(cmd *base.Command, args []string) {
func writeSignedFile(path string, code []byte, mode os.FileMode) error { func writeSignedFile(path string, code []byte, mode os.FileMode) error {
if info, err := os.Stat(path); err == nil { if info, err := os.Stat(path); err == nil {
if !info.Mode().IsRegular() {
return &os.PathError{Op: "write", Path: path, Err: os.ErrInvalid}
}
mode = info.Mode().Perm() mode = info.Mode().Perm()
path, err = filepath.EvalSymlinks(path)
if err != nil {
return err
}
} else if !os.IsNotExist(err) { } else if !os.IsNotExist(err) {
return err return err
} }
+10 -2
View File
@@ -58,10 +58,18 @@ func readConfigFile(file *os.File) ([]byte, error) {
return nil, err return nil, err
} }
if info.Mode().IsRegular() { if info.Mode().IsRegular() {
switch strings.ToLower(filepath.Ext(file.Name())) { path, err := filepath.Abs(file.Name())
if err != nil {
return nil, err
}
path, err = filepath.EvalSymlinks(path)
if err != nil {
return nil, err
}
switch strings.ToLower(filepath.Ext(path)) {
case ".jsonc", ".json", ".yml", ".yaml", ".toml", ".pb", ".protobuf": case ".jsonc", ".json", ".yml", ".yaml", ".toml", ".pb", ".protobuf":
default: default:
return nil, errors.New("unsupported config file extension: ", file.Name()) return nil, errors.New("unsupported config file extension: ", path)
} }
} }
return io.ReadAll(file) return io.ReadAll(file)
-7
View File
@@ -381,13 +381,6 @@ func (h *Handler) handleIPQuery(id uint16, qType dnsmessage.Type, domain string,
errors.LogInfoInner(context.Background(), err, "pack message") errors.LogInfoInner(context.Background(), err, "pack message")
b.Release() b.Release()
timer.SetTimeout(0) timer.SetTimeout(0)
return
}
if len(msgBytes) > buf.Size {
b.Release()
timer.SetTimeout(0)
errors.LogError(context.Background(), "DNS response message too large")
return
} }
b.Resize(0, int32(len(msgBytes))) b.Resize(0, int32(len(msgBytes)))
+17 -17
View File
@@ -24,11 +24,11 @@ const (
type Config struct { type Config struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
// List of networks that the Dokodemo accepts.
AllowedNetworks []net.Network `protobuf:"varint,7,rep,packed,name=allowed_networks,json=allowedNetworks,proto3,enum=xray.common.net.Network" json:"allowed_networks,omitempty"`
RewriteAddress *net.IPOrDomain `protobuf:"bytes,1,opt,name=rewrite_address,json=rewriteAddress,proto3" json:"rewrite_address,omitempty"` RewriteAddress *net.IPOrDomain `protobuf:"bytes,1,opt,name=rewrite_address,json=rewriteAddress,proto3" json:"rewrite_address,omitempty"`
RewritePort uint32 `protobuf:"varint,2,opt,name=rewrite_port,json=rewritePort,proto3" json:"rewrite_port,omitempty"` RewritePort uint32 `protobuf:"varint,2,opt,name=rewrite_port,json=rewritePort,proto3" json:"rewrite_port,omitempty"`
PortMap map[string]string `protobuf:"bytes,3,rep,name=port_map,json=portMap,proto3" json:"port_map,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` PortMap map[string]string `protobuf:"bytes,3,rep,name=port_map,json=portMap,proto3" json:"port_map,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
// List of networks that the Dokodemo accepts.
AllowedNetworks []net.Network `protobuf:"varint,7,rep,packed,name=allowed_networks,json=allowedNetworks,proto3,enum=xray.common.net.Network" json:"allowed_networks,omitempty"`
FollowRedirect bool `protobuf:"varint,5,opt,name=follow_redirect,json=followRedirect,proto3" json:"follow_redirect,omitempty"` FollowRedirect bool `protobuf:"varint,5,opt,name=follow_redirect,json=followRedirect,proto3" json:"follow_redirect,omitempty"`
UserLevel uint32 `protobuf:"varint,6,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"` UserLevel uint32 `protobuf:"varint,6,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
@@ -65,13 +65,6 @@ func (*Config) Descriptor() ([]byte, []int) {
return file_proxy_dokodemo_config_proto_rawDescGZIP(), []int{0} return file_proxy_dokodemo_config_proto_rawDescGZIP(), []int{0}
} }
func (x *Config) GetAllowedNetworks() []net.Network {
if x != nil {
return x.AllowedNetworks
}
return nil
}
func (x *Config) GetRewriteAddress() *net.IPOrDomain { func (x *Config) GetRewriteAddress() *net.IPOrDomain {
if x != nil { if x != nil {
return x.RewriteAddress return x.RewriteAddress
@@ -93,6 +86,13 @@ func (x *Config) GetPortMap() map[string]string {
return nil return nil
} }
func (x *Config) GetAllowedNetworks() []net.Network {
if x != nil {
return x.AllowedNetworks
}
return nil
}
func (x *Config) GetFollowRedirect() bool { func (x *Config) GetFollowRedirect() bool {
if x != nil { if x != nil {
return x.FollowRedirect return x.FollowRedirect
@@ -112,11 +112,11 @@ var File_proxy_dokodemo_config_proto protoreflect.FileDescriptor
const file_proxy_dokodemo_config_proto_rawDesc = "" + const file_proxy_dokodemo_config_proto_rawDesc = "" +
"\n" + "\n" +
"\x1bproxy/dokodemo/config.proto\x12\x13xray.proxy.dokodemo\x1a\x18common/net/address.proto\x1a\x18common/net/network.proto\"\xff\x02\n" + "\x1bproxy/dokodemo/config.proto\x12\x13xray.proxy.dokodemo\x1a\x18common/net/address.proto\x1a\x18common/net/network.proto\"\xff\x02\n" +
"\x06Config\x12C\n" + "\x06Config\x12D\n" +
"\x10allowed_networks\x18\a \x03(\x0e2\x18.xray.common.net.NetworkR\x0fallowedNetworks\x12D\n" +
"\x0frewrite_address\x18\x01 \x01(\v2\x1b.xray.common.net.IPOrDomainR\x0erewriteAddress\x12!\n" + "\x0frewrite_address\x18\x01 \x01(\v2\x1b.xray.common.net.IPOrDomainR\x0erewriteAddress\x12!\n" +
"\frewrite_port\x18\x02 \x01(\rR\vrewritePort\x12C\n" + "\frewrite_port\x18\x02 \x01(\rR\vrewritePort\x12C\n" +
"\bport_map\x18\x03 \x03(\v2(.xray.proxy.dokodemo.Config.PortMapEntryR\aportMap\x12'\n" + "\bport_map\x18\x03 \x03(\v2(.xray.proxy.dokodemo.Config.PortMapEntryR\aportMap\x12C\n" +
"\x10allowed_networks\x18\a \x03(\x0e2\x18.xray.common.net.NetworkR\x0fallowedNetworks\x12'\n" +
"\x0ffollow_redirect\x18\x05 \x01(\bR\x0efollowRedirect\x12\x1d\n" + "\x0ffollow_redirect\x18\x05 \x01(\bR\x0efollowRedirect\x12\x1d\n" +
"\n" + "\n" +
"user_level\x18\x06 \x01(\rR\tuserLevel\x1a:\n" + "user_level\x18\x06 \x01(\rR\tuserLevel\x1a:\n" +
@@ -141,13 +141,13 @@ var file_proxy_dokodemo_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_proxy_dokodemo_config_proto_goTypes = []any{ var file_proxy_dokodemo_config_proto_goTypes = []any{
(*Config)(nil), // 0: xray.proxy.dokodemo.Config (*Config)(nil), // 0: xray.proxy.dokodemo.Config
nil, // 1: xray.proxy.dokodemo.Config.PortMapEntry nil, // 1: xray.proxy.dokodemo.Config.PortMapEntry
(net.Network)(0), // 2: xray.common.net.Network (*net.IPOrDomain)(nil), // 2: xray.common.net.IPOrDomain
(*net.IPOrDomain)(nil), // 3: xray.common.net.IPOrDomain (net.Network)(0), // 3: xray.common.net.Network
} }
var file_proxy_dokodemo_config_proto_depIdxs = []int32{ var file_proxy_dokodemo_config_proto_depIdxs = []int32{
2, // 0: xray.proxy.dokodemo.Config.allowed_networks:type_name -> xray.common.net.Network 2, // 0: xray.proxy.dokodemo.Config.rewrite_address:type_name -> xray.common.net.IPOrDomain
3, // 1: xray.proxy.dokodemo.Config.rewrite_address:type_name -> xray.common.net.IPOrDomain 1, // 1: xray.proxy.dokodemo.Config.port_map:type_name -> xray.proxy.dokodemo.Config.PortMapEntry
1, // 2: xray.proxy.dokodemo.Config.port_map:type_name -> xray.proxy.dokodemo.Config.PortMapEntry 3, // 2: xray.proxy.dokodemo.Config.allowed_networks:type_name -> xray.common.net.Network
3, // [3:3] is the sub-list for method output_type 3, // [3:3] is the sub-list for method output_type
3, // [3:3] is the sub-list for method input_type 3, // [3:3] is the sub-list for method input_type
3, // [3:3] is the sub-list for extension type_name 3, // [3:3] is the sub-list for extension type_name
+5 -10
View File
@@ -229,19 +229,14 @@ func (h *Handler) blockDelay(rule *FinalRule) time.Duration {
func (h *Handler) blackhole(ctx context.Context, input buf.Reader, output buf.Writer, rule *FinalRule, dest *net.Destination) error { func (h *Handler) blackhole(ctx context.Context, input buf.Reader, output buf.Writer, rule *FinalRule, dest *net.Destination) error {
delay := h.blockDelay(rule) delay := h.blockDelay(rule)
errors.LogInfo(ctx, "blocked target: ", *dest, ", blackholing connection for ", delay) errors.LogInfo(ctx, "blocked target: ", *dest, ", blackholing connection for ", delay)
defer common.Close(output) timer := time.AfterFunc(delay, func() {
done := make(chan struct{})
go func() { // a DispatchLink inbound's reader ignores Interrupt, so don't wait for this copy
_ = buf.Copy(input, buf.Discard)
close(done)
}()
select {
case <-done:
case <-time.After(delay):
common.Interrupt(input) common.Interrupt(input)
common.Interrupt(output) common.Interrupt(output)
errors.LogInfo(ctx, "closed blackholed connection to blocked target: ", *dest) errors.LogInfo(ctx, "closed blackholed connection to blocked target: ", *dest)
} })
defer timer.Stop()
defer common.Close(output)
_ = buf.Copy(input, buf.Discard)
return nil return nil
} }
+2 -30
View File
@@ -10,7 +10,6 @@ import (
"net/url" "net/url"
"sync" "sync"
"text/template" "text/template"
"time"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/buf"
@@ -87,13 +86,7 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter
user := server.User user := server.User
var conn stat.Connection var conn stat.Connection
var mbuf buf.MultiBuffer mbuf, _ := link.Reader.ReadMultiBuffer()
if reader, ok := link.Reader.(buf.TimeoutReader); ok {
// Don't wait forever: server-first protocols (SMTP, FTP, MySQL...) send nothing until the server greets.
mbuf, _ = reader.ReadMultiBufferTimeout(time.Millisecond * 500)
} else {
mbuf, _ = link.Reader.ReadMultiBuffer()
}
len := mbuf.Len() len := mbuf.Len()
firstPayload := bytespool.Alloc(len) firstPayload := bytespool.Alloc(len)
mbuf, _ = buf.SplitBytes(mbuf, firstPayload) mbuf, _ = buf.SplitBytes(mbuf, firstPayload)
@@ -241,8 +234,7 @@ func setUpHTTPTunnel(ctx context.Context, dest net.Destination, target string, u
return nil, err return nil, err
} }
reader := bufio.NewReader(rawConn) resp, err := http.ReadResponse(bufio.NewReader(rawConn), req)
resp, err := http.ReadResponse(reader, req)
if err != nil { if err != nil {
rawConn.Close() rawConn.Close()
return nil, err return nil, err
@@ -253,11 +245,6 @@ func setUpHTTPTunnel(ctx context.Context, dest net.Destination, target string, u
rawConn.Close() rawConn.Close()
return nil, errors.New("Proxy responded with non 200 code: " + resp.Status) return nil, errors.New("Proxy responded with non 200 code: " + resp.Status)
} }
// Tunneled bytes (e.g. a server-first greeting) may arrive in the same read as the 200 response.
if n := reader.Buffered(); n > 0 {
early, _ := reader.Peek(n)
return &earlyDataConn{Conn: rawConn, early: early}, nil
}
return rawConn, nil return rawConn, nil
} }
@@ -388,21 +375,6 @@ func (h *http2Conn) Close() error {
return h.out.Close() return h.out.Close()
} }
// earlyDataConn returns the bytes read ahead together with the CONNECT response before reading from Conn.
type earlyDataConn struct {
net.Conn
early []byte
}
func (c *earlyDataConn) Read(p []byte) (int, error) {
if len(c.early) > 0 {
n := copy(p, c.early)
c.early = c.early[n:]
return n, nil
}
return c.Conn.Read(p)
}
func init() { func init() {
common.Must(common.RegisterConfig((*ClientConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { common.Must(common.RegisterConfig((*ClientConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
return NewClient(ctx, config.(*ClientConfig)) return NewClient(ctx, config.(*ClientConfig))
+4 -5
View File
@@ -2,10 +2,10 @@ package socks
import ( import (
"context" "context"
"net"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/signal" "github.com/xtls/xray-core/common/signal"
) )
@@ -14,9 +14,6 @@ func NewTempUDPConn(udpConn net.PacketConn, tcpConn net.Conn, expectedRemote *ne
PacketConn: udpConn, PacketConn: udpConn,
AssociatedTCPConn: tcpConn, AssociatedTCPConn: tcpConn,
} }
t.Timer = signal.CancelAfterInactivity(context.Background(), func() {
t.Close()
}, net.ConnIdleTimeout)
t.ExpectedRemote.Store(expectedRemote) t.ExpectedRemote.Store(expectedRemote)
return t return t
} }
@@ -63,7 +60,9 @@ func (c *TempUDPConn) RemoteAddr() net.Addr {
} }
func (c *TempUDPConn) SetTimeout(d time.Duration) { func (c *TempUDPConn) SetTimeout(d time.Duration) {
c.Timer.SetTimeout(d) c.Timer = signal.CancelAfterInactivity(context.Background(), func() {
c.Close()
}, d)
} }
func (c *TempUDPConn) Close() error { func (c *TempUDPConn) Close() error {
+3 -2
View File
@@ -7,11 +7,12 @@
package tun package tun
import ( import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect" reflect "reflect"
sync "sync" sync "sync"
unsafe "unsafe" unsafe "unsafe"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
) )
const ( const (
+7 -8
View File
@@ -41,9 +41,8 @@ const (
type packet struct { type packet struct {
proto byte proto byte
src, dst netip.Addr src, dst netip.Addr
// payload is what follows the IP header, without the last rest bytes of it when they are in other buffers // payload is what follows the IP header
payload []byte payload []byte
rest int
// fragment is set if payload is a part of what follows the IP header: the one at offset of the packet // fragment is set if payload is a part of what follows the IP header: the one at offset of the packet
// with the number id, which has more after it or not // with the number id, which has more after it or not
fragment bool fragment bool
@@ -52,8 +51,8 @@ type packet struct {
id uint32 id uint32
} }
// parsePacket reads the IP header of a packet that is b and rest bytes after it // parsePacket reads the IP header of b
func parsePacket(b []byte, rest int) (p packet, ok bool) { func parsePacket(b []byte) (p packet, ok bool) {
if len(b) < ipv4HeaderSize { if len(b) < ipv4HeaderSize {
return p, false return p, false
} }
@@ -61,7 +60,7 @@ func parsePacket(b []byte, rest int) (p packet, ok bool) {
case 4: case 4:
size := int(b[0]&0xf) * 4 size := int(b[0]&0xf) * 4
total := int(binary.BigEndian.Uint16(b[2:])) total := int(binary.BigEndian.Uint16(b[2:]))
if size < ipv4HeaderSize || total < size || total > len(b)+rest { if size < ipv4HeaderSize || total < size || total > len(b) {
return p, false return p, false
} }
if f := binary.BigEndian.Uint16(b[6:]); f&0x3fff != 0 { if f := binary.BigEndian.Uint16(b[6:]); f&0x3fff != 0 {
@@ -70,19 +69,19 @@ func parsePacket(b []byte, rest int) (p packet, ok bool) {
p.proto = b[9] p.proto = b[9]
p.src = netip.AddrFrom4([4]byte(b[12:16])) p.src = netip.AddrFrom4([4]byte(b[12:16]))
p.dst = netip.AddrFrom4([4]byte(b[16:20])) p.dst = netip.AddrFrom4([4]byte(b[16:20]))
p.payload, p.rest = b[size:min(total, len(b))], max(total-len(b), 0) p.payload = b[size:total]
case 6: case 6:
if len(b) < ipv6HeaderSize { if len(b) < ipv6HeaderSize {
return p, false return p, false
} }
total := ipv6HeaderSize + int(binary.BigEndian.Uint16(b[4:])) total := ipv6HeaderSize + int(binary.BigEndian.Uint16(b[4:]))
if total > len(b)+rest { if total > len(b) {
return p, false return p, false
} }
p.proto = b[6] p.proto = b[6]
p.src = netip.AddrFrom16([16]byte(b[8:24])) p.src = netip.AddrFrom16([16]byte(b[8:24]))
p.dst = netip.AddrFrom16([16]byte(b[24:40])) p.dst = netip.AddrFrom16([16]byte(b[24:40]))
p.payload, p.rest = b[ipv6HeaderSize:min(total, len(b))], max(total-len(b), 0) p.payload = b[ipv6HeaderSize:total]
// hop-by-hop options, routing, destination options // hop-by-hop options, routing, destination options
for p.proto == 0 || p.proto == 43 || p.proto == 60 { for p.proto == 0 || p.proto == 43 || p.proto == 60 {
if len(p.payload) < 8 { if len(p.payload) < 8 {
+8 -88
View File
@@ -71,10 +71,6 @@ type stack struct {
udp *udpConnectionHandler udp *udpConnectionHandler
// addresses are what connections are opened from // addresses are what connections are opened from
addresses []netip.Addr addresses []netip.Addr
// bufs is what read reads a packet into if the tun is a segmenter, one after the other, the headers in the first.
// One that keep took is nil. joined is for a packet that is in more than one and is needed in one piece
bufs buf.MultiBuffer
joined []byte
mu sync.Mutex mu sync.Mutex
conns map[flow]*tcpConn conns map[flow]*tcpConn
@@ -153,66 +149,26 @@ func (s *stack) Close() error {
// read takes the packets of the tun until it is closed // read takes the packets of the tun until it is closed
func (s *stack) read() { func (s *stack) read() {
var err error // a tun that does not split segments gives them as large as a packet can be
if s.segmenter != nil {
err = s.readSegments()
} else {
// as large as a packet can be, whatever the MTU is
b := make([]byte, packetRoom+1<<16) b := make([]byte, packetRoom+1<<16)
for err == nil { for {
var p []byte b, err := s.tun.ReadPacket(b)
if p, err = s.tun.ReadPacket(b); err == nil { if err != nil {
s.Input(p)
}
}
}
s.mu.Lock() s.mu.Lock()
closed := s.closed closed := s.closed
s.mu.Unlock() s.mu.Unlock()
if !closed { if !closed {
errors.LogErrorInner(s.ctx, err, "[tun] failed to read a packet, no more are read") errors.LogErrorInner(s.ctx, err, "[tun] failed to read a packet, no more are read")
} }
return
} }
s.Input(b)
// readSegments is read for a tun that does not split segments and gives them as large as a packet can be.
// They are read into buffers for a connection to keep what is large as it is, see keep
func (s *stack) readSegments() error {
s.bufs = make(buf.MultiBuffer, (1<<16)/buf.Size)
defer buf.ReleaseMulti(s.bufs)
parts := make([][]byte, len(s.bufs))
var r room
for {
for i, b := range s.bufs {
if b == nil {
s.bufs[i] = buf.New()
_, _ = s.bufs[i].ReadFrom(&r)
parts[i] = r.b
} }
} }
n, err := s.segmenter.ReadParts(parts)
if err != nil {
return err
}
s.input(parts[0][:min(n, buf.Size)], max(n-buf.Size, 0))
}
}
// room takes what it is given to read into as read: that is the room of a Buffer as it is, not cleared
type room struct{ b []byte }
func (r *room) Read(b []byte) (int, error) {
r.b = b
return len(b), nil
}
// Input implements Stack. Several tunnels may hand over their packets at once // Input implements Stack. Several tunnels may hand over their packets at once
func (s *stack) Input(b []byte) { func (s *stack) Input(b []byte) {
s.input(b, 0) p, ok := parsePacket(b)
}
// input takes a packet that is b and rest bytes after it, in the bufs that follow the one that b is in
func (s *stack) input(b []byte, rest int) {
p, ok := parsePacket(b, rest)
if !ok || p.src.Is4In6() || p.dst.Is4In6() || p.src.IsLoopback() || p.dst.IsLoopback() || p.dst.IsUnspecified() || p.src.IsMulticast() { if !ok || p.src.Is4In6() || p.dst.Is4In6() || p.src.IsLoopback() || p.dst.IsLoopback() || p.dst.IsUnspecified() || p.src.IsMulticast() {
// nothing comes from many at once, and an address that stands for this machine itself is not one // nothing comes from many at once, and an address that stands for this machine itself is not one
// that the other side of a tun can ask for. Nor is an IPv6 address that stands for an IPv4 one, which // that the other side of a tun can ask for. Nor is an IPv6 address that stands for an IPv4 one, which
@@ -224,10 +180,6 @@ func (s *stack) input(b []byte, rest int) {
} }
// what was put together may be of two packets with one number, its checksum tells // what was put together may be of two packets with one number, its checksum tells
verify := s.verify || p.fragment verify := s.verify || p.fragment
if p.rest != 0 && (verify || p.proto != protoTCP) {
// only a segment of TCP that is not looked at can stay in pieces
p.payload, p.rest = s.whole(p.payload, p.rest), 0
}
if p.fragment { if p.fragment {
p, ok = s.fragments.add(p) p, ok = s.fragments.add(p)
} }
@@ -244,37 +196,6 @@ func (s *stack) input(b []byte, rest int) {
} }
} }
// whole returns in one piece what is b and rest bytes after it, in the bufs that follow the one that b is in
func (s *stack) whole(b []byte, rest int) []byte {
if rest == 0 {
return b
}
s.joined = append(s.joined[:0], b...)
for i := 1; rest > 0; i++ {
n := min(rest, buf.Size)
s.joined = append(s.joined, s.bufs[i].BytesTo(int32(n))...)
rest -= n
}
return s.joined
}
// keep adds to mb what is b and rest bytes after it, in the bufs that follow the one that b is in: nothing is copied,
// the buffers are taken for it and no longer read into
func (s *stack) keep(mb buf.MultiBuffer, b []byte, rest int) buf.MultiBuffer {
// b ends where what was read into the first one does, or before it
first := s.bufs[0]
from := first.Cap() - int32(cap(b))
first.Resize(from, from+int32(len(b)))
mb, s.bufs[0] = append(mb, first), nil
for i := 1; rest > 0; i++ {
n := min(rest, buf.Size)
s.bufs[i].Resize(0, int32(n))
mb, s.bufs[i] = append(mb, s.bufs[i]), nil
rest -= n
}
return mb
}
// local returns the address that packets to remote are sent from // local returns the address that packets to remote are sent from
func (s *stack) local(remote netip.Addr) (netip.Addr, error) { func (s *stack) local(remote netip.Addr) (netip.Addr, error) {
for _, a := range s.addresses { for _, a := range s.addresses {
@@ -316,7 +237,6 @@ func (s *stack) handleTCP(p packet) {
window: binary.BigEndian.Uint16(t[14:]), window: binary.BigEndian.Uint16(t[14:]),
options: t[tcpHeaderSize:size], options: t[tcpHeaderSize:size],
payload: t[size:], payload: t[size:],
rest: p.rest,
} }
s.mu.Lock() s.mu.Lock()
@@ -363,7 +283,7 @@ func (s *stack) refuse(f flow, seg segment) {
binary.BigEndian.PutUint32(t[4:], seg.ack) binary.BigEndian.PutUint32(t[4:], seg.ack)
t[13] = tcpRst t[13] = tcpRst
} else { } else {
ack := seg.seq + uint32(len(seg.payload)+seg.rest) ack := seg.seq + uint32(len(seg.payload))
if seg.flags&(tcpSyn|tcpFin) != 0 { if seg.flags&(tcpSyn|tcpFin) != 0 {
ack++ ack++
} }
+23 -111
View File
@@ -65,20 +65,6 @@ func (l *link) Segments() bool {
return l.segments return l.segments
} }
// ReadParts implements segmenter
func (l *link) ReadParts(parts [][]byte) (int, error) {
select {
case packet := <-l.in:
n := 0
for _, p := range parts {
n += copy(p, packet[n:])
}
return n, nil
case <-l.done:
return 0, io.EOF
}
}
// WriteSegment implements segmenter as the system does when it sends a segment on: a packet for every mss bytes // WriteSegment implements segmenter as the system does when it sends a segment on: a packet for every mss bytes
func (l *link) WriteSegment(header []byte, payload [][]byte, mss int) error { func (l *link) WriteSegment(header []byte, payload [][]byte, mss int) error {
data := bytes.Join(payload, nil) data := bytes.Join(payload, nil)
@@ -220,7 +206,7 @@ func (p *peer) receive() segment {
func (p *peer) dialed() (segment, []byte) { func (p *peer) dialed() (segment, []byte) {
p.t.Helper() p.t.Helper()
b := p.packet() b := p.packet()
packet, ok := parsePacket(b, 0) packet, ok := parsePacket(b)
if !ok || packet.proto != protoTCP || len(packet.payload) < tcpHeaderSize { if !ok || packet.proto != protoTCP || len(packet.payload) < tcpHeaderSize {
p.t.Fatal("not a segment: ", b) p.t.Fatal("not a segment: ", b)
} }
@@ -237,7 +223,7 @@ func (p *peer) segment(b []byte) segment {
} else if checksum(sum(b[:size], 0)) != 0 { } else if checksum(sum(b[:size], 0)) != 0 {
p.t.Fatal("wrong checksum of the ip header") p.t.Fatal("wrong checksum of the ip header")
} }
packet, ok := parsePacket(b, 0) packet, ok := parsePacket(b)
if !ok || packet.proto != protoTCP || packet.src != p.dst.Addr() || packet.dst != p.src.Addr() { if !ok || packet.proto != protoTCP || packet.src != p.dst.Addr() || packet.dst != p.src.Addr() {
p.t.Fatal("not a segment of the connection: ", b) p.t.Fatal("not a segment of the connection: ", b)
} }
@@ -478,90 +464,30 @@ func TestTCPOutOfOrder(t *testing.T) {
read(t, a.conn, "jklmno") read(t, a.conn, "jklmno")
} }
// A segment is as large as the system makes it when the tun does not split what it reads, and one that is large, // A segment is as large as the system makes it when the tun does not split what it reads.
// the next and all taken stays in the buffers that such a tun reads it into.
func TestTCPLargeSegment(t *testing.T) { func TestTCPLargeSegment(t *testing.T) {
for _, segments := range []bool{false, true} {
conns := make(chan accepted, 1) conns := make(chan accepted, 1)
p, l := newPeer(t, "10.0.0.1:1234", "10.0.0.2:80") p, l := newPeer(t, "10.0.0.1:1234", "10.0.0.2:80")
l.segments = segments newTestStack(t, l, accept(conns))
s := newTestStack(t, l, accept(conns))
sent := make([]byte, 200000)
rand.Read(sent)
// one of no connection is refused as a whole
p.send(p.seq, tcpPsh, nil, sent[:20000])
if r := p.receive(); r.flags != tcpRst|tcpAck || r.ack != p.seq+20000 {
t.Fatal("segments: ", segments, ", not refused: flags ", r.flags, ", ack ", r.ack-p.seq, " after its seq")
}
p.connect(nil) p.connect(nil)
a := take(t, conns) a := take(t, conns)
// one that waits for the one before it, that one, one with a part that came already, small ones around sent := make([]byte, 60000)
// one that fits a buffer, and the rest rand.Read(sent)
for _, part := range [][2]int{{60000, 90000}, {0, 60000}, {80000, 110000}, {110000, 110100}, {110100, 115100}, {115100, 118100}, {118100, 170000}, {170000, 200000}} { p.send(p.seq, tcpAck|tcpPsh, nil, sent)
p.send(p.seq+uint32(part[0]), tcpAck|tcpPsh, nil, sent[part[0]:part[1]])
}
// all of it is taken before anything is read, for the buffers to be as the stack put it in them
for r := p.receive(); r.ack != p.seq+uint32(len(sent)); r = p.receive() {
}
var received []byte var received []byte
for len(received) < len(sent) { for len(received) < len(sent) {
mb, err := a.conn.(buf.Reader).ReadMultiBuffer() mb, err := a.conn.(buf.Reader).ReadMultiBuffer()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
// a buffer that was read into starts with the headers, one that was copied to is full
if kept := mb[0].Len() == buf.Size-ipv4HeaderSize-tcpHeaderSize; len(received) == 0 && kept != segments {
t.Fatal("segments: ", segments, ", the first buffer has ", mb[0].Len(), " bytes")
}
for _, b := range mb { for _, b := range mb {
// and a small one is copied to where the one before it ends
if b.Len() < 1000 {
t.Fatal("segments: ", segments, ", a buffer of ", b.Len(), " bytes")
}
received = append(received, b.Bytes()...) received = append(received, b.Bytes()...)
} }
buf.ReleaseMulti(mb) buf.ReleaseMulti(mb)
} }
if !bytes.Equal(received, sent) { if !bytes.Equal(received, sent) {
t.Fatal("segments: ", segments, ", read something else than what was sent") t.Fatal("read something else than what was sent")
}
// no more is taken than the window while nobody reads, of the last one a part,
// and what is taken is acknowledged when a quarter of the window is
p.seq += uint32(len(sent))
sent = make([]byte, (int(s.window)/50000+1)*50000)
rand.Read(sent)
for i := 0; i < len(sent); i += 50000 {
p.send(p.seq+uint32(i), tcpAck|tcpPsh, nil, sent[i:i+50000])
}
for acked := p.seq; ; {
r := p.receive()
if taken := int32(r.ack - p.seq); taken > s.window {
t.Fatal("segments: ", segments, ", ", taken, " bytes taken with a window of ", s.window)
} else if taken == s.window {
break
}
// one that acknowledges nothing more tells that the window opened for what was read before
if r.ack != acked && int32(r.ack-acked) < s.window/4 {
t.Fatal("segments: ", segments, ", ", r.ack-acked, " bytes acknowledged with a window of ", s.window)
}
acked = r.ack
}
received = received[:0]
for len(received) < int(s.window) {
mb, err := a.conn.(buf.Reader).ReadMultiBuffer()
if err != nil {
t.Fatal(err)
}
for _, b := range mb {
received = append(received, b.Bytes()...)
}
buf.ReleaseMulti(mb)
}
if !bytes.Equal(received, sent[:s.window]) {
t.Fatal("segments: ", segments, ", read something else than what the window took")
}
} }
} }
@@ -1156,20 +1082,13 @@ func TestTCPSynAfterClose(t *testing.T) {
} }
// A UDP packet starts a connection, which answers from any address, and an echo request is answered. // A UDP packet starts a connection, which answers from any address, and an echo request is answered.
// Both can be larger than a buffer of a tun that reads into more than one.
func TestUDPAndICMP(t *testing.T) { func TestUDPAndICMP(t *testing.T) {
large := make([]byte, 20000) for _, test := range []struct{ src, dst, other string }{
rand.Read(large) {"10.0.0.1:1234", "10.0.0.2:53", "10.0.0.3:5353"},
for _, test := range []struct { {"[fd00::1]:1234", "[fd00::2]:53", "[fd00::3]:5353"},
src, dst, other string
segments bool
}{
{"10.0.0.1:1234", "10.0.0.2:53", "10.0.0.3:5353", false},
{"[fd00::1]:1234", "[fd00::2]:53", "[fd00::3]:5353", true},
} { } {
conns := make(chan accepted, 1) conns := make(chan accepted, 1)
p, l := newPeer(t, test.src, test.dst) p, l := newPeer(t, test.src, test.dst)
l.segments = test.segments
newTestStack(t, l, accept(conns)) newTestStack(t, l, accept(conns))
var b [ipv6HeaderSize + udpHeaderSize]byte var b [ipv6HeaderSize + udpHeaderSize]byte
@@ -1186,13 +1105,6 @@ func TestUDPAndICMP(t *testing.T) {
t.Fatal("read ", mb.String(), " from ", a.conn.RemoteAddr(), " to ", a.dest, ", error: ", err) t.Fatal("read ", mb.String(), " from ", a.conn.RemoteAddr(), " to ", a.dest, ", error: ", err)
} }
buf.ReleaseMulti(mb) buf.ReleaseMulti(mb)
binary.BigEndian.PutUint16(u[4:], uint16(udpHeaderSize+len(large)))
setIPLength(b[:n], n+udpHeaderSize+len(large))
l.in <- append(b[:n+udpHeaderSize:n+udpHeaderSize], large...)
if mb, err = a.conn.(buf.Reader).ReadMultiBuffer(); err != nil || len(mb) != 1 || !bytes.Equal(mb[0].Bytes(), large) {
t.Fatal("read ", mb.Len(), " bytes of a large packet, error: ", err)
}
buf.ReleaseMulti(mb)
for _, from := range []string{test.dst, test.other} { for _, from := range []string{test.dst, test.other} {
answer := buf.FromBytes([]byte("defg")) answer := buf.FromBytes([]byte("defg"))
@@ -1202,7 +1114,7 @@ func TestUDPAndICMP(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
raw := p.packet() raw := p.packet()
packet, ok := parsePacket(raw, 0) packet, ok := parsePacket(raw)
u := packet.payload u := packet.payload
// the system may have to split it on the way to where it goes // the system may have to split it on the way to where it goes
if !ok || raw[0]>>4 == 4 && raw[6] != 0 || packet.proto != protoUDP || netip.AddrPortFrom(packet.src, binary.BigEndian.Uint16(u)).String() != from || if !ok || raw[0]>>4 == 4 && raw[6] != 0 || packet.proto != protoUDP || netip.AddrPortFrom(packet.src, binary.BigEndian.Uint16(u)).String() != from ||
@@ -1221,12 +1133,12 @@ func TestUDPAndICMP(t *testing.T) {
proto, request, reply = protoICMPv6, 128, 129 proto, request, reply = protoICMPv6, 128, 129
} }
n = putIPHeader(b[:], proto, p.src.Addr(), p.dst.Addr()) n = putIPHeader(b[:], proto, p.src.Addr(), p.dst.Addr())
setIPLength(b[:n], n+8+len(large)) setIPLength(b[:n], n+12)
l.in <- append(append(b[:n:n], request, 0, 0, 0, 1, 2, 3, 4), large...) l.in <- append(b[:n:n], request, 0, 0, 0, 1, 2, 3, 4, 'a', 'b', 'c', 'd')
packet, ok := parsePacket(p.joined(), 0) packet, ok := parsePacket(p.packet())
m := packet.payload m := packet.payload
if !ok || packet.proto != proto || packet.src != p.dst.Addr() || packet.dst != p.src.Addr() || m[0] != reply || string(m[4:8]) != "\x01\x02\x03\x04" || !bytes.Equal(m[8:], large) { if !ok || packet.proto != proto || packet.src != p.dst.Addr() || packet.dst != p.src.Addr() || m[0] != reply || string(m[4:]) != "\x01\x02\x03\x04abcd" {
t.Fatal("wrong echo reply of ", len(m), " bytes from ", packet.src, " to ", packet.dst) t.Fatal("wrong echo reply: ", packet)
} }
var ip [ipv6HeaderSize]byte var ip [ipv6HeaderSize]byte
size := putIPHeader(ip[:], proto, packet.src, packet.dst) size := putIPHeader(ip[:], proto, packet.src, packet.dst)
@@ -1461,7 +1373,7 @@ func TestUDPDial(t *testing.T) {
t.Fatal("wrong header of the packet to ", to, ": ", raw[:ipv4HeaderSize]) t.Fatal("wrong header of the packet to ", to, ": ", raw[:ipv4HeaderSize])
} }
numbers[to][binary.BigEndian.Uint16(raw[4:])] = true numbers[to][binary.BigEndian.Uint16(raw[4:])] = true
packet, ok := parsePacket(raw, 0) packet, ok := parsePacket(raw)
u := packet.payload u := packet.payload
if !ok || packet.proto != protoUDP || netip.AddrPortFrom(packet.src, binary.BigEndian.Uint16(u)) != local || if !ok || packet.proto != protoUDP || netip.AddrPortFrom(packet.src, binary.BigEndian.Uint16(u)) != local ||
netip.AddrPortFrom(packet.dst, binary.BigEndian.Uint16(u[2:])) != to || string(u[udpHeaderSize:]) != "abc" { netip.AddrPortFrom(packet.dst, binary.BigEndian.Uint16(u[2:])) != to || string(u[udpHeaderSize:]) != "abc" {
@@ -1483,7 +1395,7 @@ func TestUDPDial(t *testing.T) {
if _, err := conn.WriteTo([]byte("abc"), &net.UDPAddr{IP: across.Addr().AsSlice(), Port: int(across.Port())}); err != nil { if _, err := conn.WriteTo([]byte("abc"), &net.UDPAddr{IP: across.Addr().AsSlice(), Port: int(across.Port())}); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if packet, ok := parsePacket(p.packet(), 0); !ok || packet.proto != protoUDP || packet.dst != across.Addr() || if packet, ok := parsePacket(p.packet()); !ok || packet.proto != protoUDP || packet.dst != across.Addr() ||
netip.AddrPortFrom(packet.src, binary.BigEndian.Uint16(packet.payload)) != acrossLocal { netip.AddrPortFrom(packet.src, binary.BigEndian.Uint16(packet.payload)) != acrossLocal {
t.Fatal("wrong packet to ", across, ": ", packet) t.Fatal("wrong packet to ", across, ": ", packet)
} }
@@ -1495,7 +1407,7 @@ func TestUDPDial(t *testing.T) {
// a stack that takes no connections is one host: it answers a ping of its address and of no other // a stack that takes no connections is one host: it answers a ping of its address and of no other
l.in <- ping(p.src.Addr(), netip.MustParseAddrPort(test.other).Addr()) l.in <- ping(p.src.Addr(), netip.MustParseAddrPort(test.other).Addr())
l.in <- ping(p.src.Addr(), local.Addr()) l.in <- ping(p.src.Addr(), local.Addr())
if packet, ok := parsePacket(p.packet(), 0); !ok || packet.src != local.Addr() || packet.dst != p.src.Addr() || packet.payload[0]&^129 != 0 { if packet, ok := parsePacket(p.packet()); !ok || packet.src != local.Addr() || packet.dst != p.src.Addr() || packet.payload[0]&^129 != 0 {
t.Fatal("wrong echo reply: ", packet) t.Fatal("wrong echo reply: ", packet)
} }
p.silent(20 * time.Millisecond) p.silent(20 * time.Millisecond)
@@ -1961,7 +1873,7 @@ func TestFragments(t *testing.T) {
if err := a.conn.(buf.Writer).WriteMultiBuffer(buf.MultiBuffer{buf.FromBytes(sent)}); err != nil { if err := a.conn.(buf.Writer).WriteMultiBuffer(buf.MultiBuffer{buf.FromBytes(sent)}); err != nil {
t.Fatal(err) t.Fatal(err)
} }
packet, ok := parsePacket(p.joined(), 0) packet, ok := parsePacket(p.joined())
u := packet.payload u := packet.payload
if !ok || packet.fragment || packet.proto != protoUDP || netip.AddrPortFrom(packet.src, binary.BigEndian.Uint16(u)) != p.dst || if !ok || packet.fragment || packet.proto != protoUDP || netip.AddrPortFrom(packet.src, binary.BigEndian.Uint16(u)) != p.dst ||
netip.AddrPortFrom(packet.dst, binary.BigEndian.Uint16(u[2:])) != p.src || int(binary.BigEndian.Uint16(u[4:])) != len(u) || !bytes.Equal(u[udpHeaderSize:], sent) { netip.AddrPortFrom(packet.dst, binary.BigEndian.Uint16(u[2:])) != p.src || int(binary.BigEndian.Uint16(u[4:])) != len(u) || !bytes.Equal(u[udpHeaderSize:], sent) {
@@ -1986,7 +1898,7 @@ func TestFragments(t *testing.T) {
mixed := split(summed(udpPacket(p.src, p.dst, bytes.Repeat([]byte{1}, 2000))), 1024, 11) mixed := split(summed(udpPacket(p.src, p.dst, bytes.Repeat([]byte{1}, 2000))), 1024, 11)
l.in <- mixed[0] l.in <- mixed[0]
l.in <- split(summed(udpPacket(p.src, p.dst, bytes.Repeat([]byte{2}, 2000))), 1024, 11)[1] l.in <- split(summed(udpPacket(p.src, p.dst, bytes.Repeat([]byte{2}, 2000))), 1024, 11)[1]
packet, ok = parsePacket(p.joined(), 0) packet, ok = parsePacket(p.joined())
m = packet.payload m = packet.payload
if !ok || packet.proto != proto || packet.src != p.dst.Addr() || packet.dst != p.src.Addr() || m[0] != reply || !bytes.Equal(m[8:], sent) { if !ok || packet.proto != proto || packet.src != p.dst.Addr() || packet.dst != p.src.Addr() || m[0] != reply || !bytes.Equal(m[8:], sent) {
t.Fatal("wrong echo reply: ", packet.src, packet.dst, packet.proto, len(m)) t.Fatal("wrong echo reply: ", packet.src, packet.dst, packet.proto, len(m))
@@ -2128,7 +2040,7 @@ func TestChecksumsAndLoopback(t *testing.T) {
l.in <- damaged(echo) l.in <- damaged(echo)
p.silent(50 * time.Millisecond) p.silent(50 * time.Millisecond)
l.in <- echo l.in <- echo
if packet, ok := parsePacket(p.packet(), 0); !ok || packet.proto != proto || string(packet.payload[4:]) != "\x01\x02\x03\x04abcd" { if packet, ok := parsePacket(p.packet()); !ok || packet.proto != proto || string(packet.payload[4:]) != "\x01\x02\x03\x04abcd" {
t.Fatal("wrong echo reply: ", packet) t.Fatal("wrong echo reply: ", packet)
} }
+3 -19
View File
@@ -49,9 +49,6 @@ const (
// tcpParts of how many parts of what was written it is made at most // tcpParts of how many parts of what was written it is made at most
tcpSegment = math.MaxUint16 - ipv6HeaderSize - tcpHeaderSize tcpSegment = math.MaxUint16 - ipv6HeaderSize - tcpHeaderSize
tcpParts = 7 tcpParts = 7
// tcpKept is how large a payload is at least for the buffers that it was read into to be kept. A smaller one
// is copied to where the one before it ends, for a reader to get buffers that are full
tcpKept = buf.Size / 2
// tcpTick is how often a connection that waits for something looks at what it waits for // tcpTick is how often a connection that waits for something looks at what it waits for
tcpTick = 100 * time.Millisecond tcpTick = 100 * time.Millisecond
@@ -90,9 +87,7 @@ type segment struct {
flags byte flags byte
window uint16 window uint16
options []byte options []byte
// payload is without its last rest bytes when those are in other buffers of the stack, see stack.whole
payload []byte payload []byte
rest int
} }
// nextOption returns the first option of o and what follows it, kind is zero when there is none // nextOption returns the first option of o and what follows it, kind is zero when there is none
@@ -754,12 +749,6 @@ func (c *tcpConn) space() uint32 {
// receive takes the payload of a segment. c.mu is held // receive takes the payload of a segment. c.mu is held
func (c *tcpConn) receive(s segment) { func (c *tcpConn) receive(s segment) {
// what is large, the next and all taken stays in the buffers that it was read into
size := len(s.payload) + s.rest
keep := c.stack.bufs != nil && size >= tcpKept && s.seq == c.rcvNxt && size <= int(c.window-c.rcvLen)
if !keep {
s.payload = c.stack.whole(s.payload, s.rest)
}
seq, payload, fin := s.seq, s.payload, s.flags&tcpFin != 0 seq, payload, fin := s.seq, s.payload, s.flags&tcpFin != 0
if len(payload) == 0 && !fin { if len(payload) == 0 && !fin {
if int32(c.rcvNxt-seq) > 0 { if int32(c.rcvNxt-seq) > 0 {
@@ -800,14 +789,9 @@ func (c *tcpConn) receive(s segment) {
return return
} }
if keep {
c.rcvQueue = c.stack.keep(c.rcvQueue, payload, s.rest)
} else {
size = len(payload)
c.rcvQueue = buf.MergeBytes(c.rcvQueue, payload) c.rcvQueue = buf.MergeBytes(c.rcvQueue, payload)
} c.rcvLen += int32(len(payload))
c.rcvLen += int32(size) c.rcvNxt += uint32(len(payload))
c.rcvNxt += uint32(size)
for !fin && len(c.rcvHeld) > 0 && int32(c.rcvNxt-c.rcvHeld[0].seq) >= 0 { for !fin && len(c.rcvHeld) > 0 && int32(c.rcvNxt-c.rcvHeld[0].seq) >= 0 {
h := c.rcvHeld[0] h := c.rcvHeld[0]
if before := int32(c.rcvNxt - h.seq); before < h.data.Len() { if before := int32(c.rcvNxt - h.seq); before < h.data.Len() {
@@ -825,7 +809,7 @@ func (c *tcpConn) receive(s segment) {
c.readable.Signal() c.readable.Signal()
// the reader acknowledges what it takes, with one segment for all that came by then. // the reader acknowledges what it takes, with one segment for all that came by then.
// It is not waited for when it does not read for long // It is not waited for when it does not read for long
if fin || now || c.rcvNxt-c.rcvAcked >= uint32(c.window/4) { if fin || now || c.rcvNxt-c.rcvAcked >= tcpWindow/4 {
c.sendAck() c.sendAck()
} else { } else {
c.arm(tcpTick) c.arm(tcpTick)
+2 -5
View File
@@ -16,13 +16,10 @@ type Tun interface {
} }
// segmenter is a tun whose other side takes TCP segments that are larger than the MTU, // segmenter is a tun whose other side takes TCP segments that are larger than the MTU,
// splits them only if it has to send them on, and completes their checksum then. // splits them only if it has to send them on, and completes their checksum then
// What it sends comes as it is too, not split to the MTU
type segmenter interface { type segmenter interface {
// Segments reports whether ReadParts and WriteSegment can be used // Segments reports whether WriteSegment can be used
Segments() bool Segments() bool
// ReadParts waits for a packet, reads it into parts, one after the other, and returns its size
ReadParts(parts [][]byte) (int, error)
// WriteSegment writes a packet with a TCP segment whose payload is in parts, to be sent on with mss bytes // WriteSegment writes a packet with a TCP segment whose payload is in parts, to be sent on with mss bytes
// of it at most in one. Its checksum is that of the pseudo-header only, not inverted // of it at most in one. Its checksum is that of the pseudo-header only, not inverted
WriteSegment(header []byte, payload [][]byte, mss int) error WriteSegment(header []byte, payload [][]byte, mss int) error
+7 -4
View File
@@ -558,16 +558,19 @@ func ioctlPtr(fd int, req uint, arg unsafe.Pointer) error {
} }
func setinterface(network, address string, fd uintptr, iface *net.Interface) error { func setinterface(network, address string, fd uintptr, iface *net.Interface) error {
var err1, err2 error
switch network { switch network {
case "tcp6", "udp6", "ip6": case "tcp6", "udp6", "ip6":
// IPV6_BOUND_IF binds the IPv4 traffic of a dual-stack socket as err1 = unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, iface.Index)
// well, and an IPv6 socket rejects IP_BOUND_IF with EINVAL. fallthrough
return unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, iface.Index)
case "tcp4", "udp4", "ip4": case "tcp4", "udp4", "ip4":
return unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, iface.Index) err2 = unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, iface.Index)
default: default:
panic(network + " " + address) panic(network + " " + address)
} }
return errors.Join(err1, err2)
} }
func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, error) { func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, error) {
-40
View File
@@ -3,10 +3,7 @@
package tun package tun
import ( import (
"context"
"net"
"sync" "sync"
"syscall"
"testing" "testing"
"time" "time"
@@ -234,40 +231,3 @@ func TestWaitKqueueReportsPersistentFailure(t *testing.T) {
} }
} }
} }
// TestSetinterfaceSocketFamilies binds each kind of socket Xray's dialers
// open to lo0, including the dual-stack one Go opens for UDP.
func TestSetinterfaceSocketFamilies(t *testing.T) {
lo, err := net.InterfaceByName("lo0")
if err != nil {
t.Skip(err)
}
lc := net.ListenConfig{Control: func(network, address string, c syscall.RawConn) error {
var err error
if controlErr := c.Control(func(fd uintptr) {
err = setinterface(network, address, fd, lo)
}); controlErr != nil {
return controlErr
}
return err
}}
for _, s := range []struct{ network, address string }{
{"udp", "0.0.0.0:0"}, // dual-stack
{"udp4", "127.0.0.1:0"},
{"udp6", "[::1]:0"},
{"tcp4", "127.0.0.1:0"},
{"tcp6", "[::1]:0"},
} {
var closer interface{ Close() error }
if s.network[:3] == "udp" {
closer, err = lc.ListenPacket(context.Background(), s.network, s.address)
} else {
closer, err = lc.Listen(context.Background(), s.network, s.address)
}
if err != nil {
t.Errorf("%s %s: %v", s.network, s.address, err)
continue
}
closer.Close()
}
}
+1 -43
View File
@@ -6,8 +6,6 @@ import (
"encoding/binary" "encoding/binary"
"os" "os"
"sync" "sync"
"syscall"
"unsafe"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
@@ -24,14 +22,6 @@ type fdTun struct {
// vnet is the size of the virtio_net_hdr in front of every packet, zero if the tun has none. // vnet is the size of the virtio_net_hdr in front of every packet, zero if the tun has none.
// With it the system does not split what it sends to the tun and takes what is not split from it // With it the system does not split what it sends to the tun and takes what is not split from it
vnet int vnet int
// raw is file for ReadParts, readv what it calls when there may be a packet, iovecs where that reads to:
// header, which takes the virtio_net_hdr, and the parts. read and readErr are what came of it
raw syscall.RawConn
readv func(fd uintptr) bool
header [packetRoom]byte
iovecs []unix.Iovec
read int
readErr error
writeLock sync.RWMutex writeLock sync.RWMutex
closed bool closed bool
@@ -39,25 +29,7 @@ type fdTun struct {
// newFdTun takes a file descriptor that does not block // newFdTun takes a file descriptor that does not block
func newFdTun(fd, vnet int) *fdTun { func newFdTun(fd, vnet int) *fdTun {
t := &fdTun{fd: fd, file: os.NewFile(uintptr(fd), "/dev/net/tun"), vnet: vnet} return &fdTun{fd: fd, file: os.NewFile(uintptr(fd), "/dev/net/tun"), vnet: vnet}
t.raw, _ = t.file.SyscallConn()
t.readv = func(fd uintptr) bool {
for {
n, _, errno := unix.Syscall(unix.SYS_READV, fd, uintptr(unsafe.Pointer(&t.iovecs[0])), uintptr(len(t.iovecs)))
switch errno {
case 0:
t.read, t.readErr = int(n), nil
case unix.EINTR:
continue
case unix.EAGAIN:
return false
default:
t.read, t.readErr = 0, errno
}
return true
}
}
return t
} }
func (t *fdTun) ReadPacket(b []byte) ([]byte, error) { func (t *fdTun) ReadPacket(b []byte) ([]byte, error) {
@@ -66,20 +38,6 @@ func (t *fdTun) ReadPacket(b []byte) ([]byte, error) {
return b[min(t.vnet, n):n], err return b[min(t.vnet, n):n], err
} }
// ReadParts implements segmenter
func (t *fdTun) ReadParts(parts [][]byte) (int, error) {
t.iovecs = append(t.iovecs[:0], unix.Iovec{Base: &t.header[0]})
t.iovecs[0].SetLen(t.vnet)
for i, p := range parts {
t.iovecs = append(t.iovecs, unix.Iovec{Base: &p[0]})
t.iovecs[i+1].SetLen(len(p))
}
if err := t.raw.Read(t.readv); err != nil {
return 0, err
}
return max(t.read-t.vnet, 0), t.readErr
}
func (t *fdTun) WritePacket(header, payload []byte) error { func (t *fdTun) WritePacket(header, payload []byte) error {
// a virtio_net_hdr of zeros leaves nothing to the system // a virtio_net_hdr of zeros leaves nothing to the system
var vnet [packetRoom]byte var vnet [packetRoom]byte
-66
View File
@@ -1,66 +0,0 @@
//go:build linux
package tun
import (
"bytes"
"crypto/rand"
"testing"
"time"
"golang.org/x/sys/unix"
)
// A tun with a virtio_net_hdr reads one packet at a time into parts, without the header, waits for the next one,
// and stops waiting when it is closed.
func TestReadParts(t *testing.T) {
// what a tun is like: one packet for every read, and a file descriptor that does not block
fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_SEQPACKET|unix.SOCK_NONBLOCK|unix.SOCK_CLOEXEC, 0)
if err != nil {
t.Fatal(err)
}
defer unix.Close(fds[1])
tun := newFdTun(fds[0], vnetSize)
defer tun.closeFile()
large := make([]byte, 20000)
rand.Read(large)
parts := [][]byte{make([]byte, 8192), make([]byte, 8192), make([]byte, 8192)}
send := func(packet []byte) {
if _, err := unix.Write(fds[1], append(bytes.Repeat([]byte{0xee}, vnetSize), packet...)); err != nil {
t.Error(err)
}
}
send(large)
send([]byte("small"))
for _, packet := range [][]byte{large, []byte("small")} {
n, err := tun.ReadParts(parts)
if err != nil || n != len(packet) || !bytes.Equal(bytes.Join(parts, nil)[:n], packet) {
t.Fatal("read ", n, " bytes of ", len(packet), ", error: ", err)
}
}
go func() {
time.Sleep(50 * time.Millisecond)
send([]byte("later"))
}()
if n, err := tun.ReadParts(parts); err != nil || string(parts[0][:n]) != "later" {
t.Fatal("read ", parts[0][:n], " after waiting, error: ", err)
}
read := make(chan error)
go func() {
_, err := tun.ReadParts(parts)
read <- err
}()
time.Sleep(50 * time.Millisecond)
tun.closeFile()
select {
case err := <-read:
if err == nil {
t.Fatal("read after the tun was closed")
}
case <-time.After(5 * time.Second):
t.Fatal("still waits after the tun was closed")
}
}
+1 -25
View File
@@ -401,15 +401,6 @@ func resolveOnOwn() *savedResolver {
if internet.IsSkippedDNSServer(address) { if internet.IsSkippedDNSServer(address) {
return nil, errors.New("skipped DNS server ", address) return nil, errors.New("skipped DNS server ", address)
} }
// A socket of the server's IP version only needs that version bound
// to the interface, which a dual-stack one would need both of.
if ap, err := netip.ParseAddrPort(address); err == nil && (network == "udp" || network == "tcp") {
if ap.Addr().Unmap().Is4() {
network += "4"
} else {
network += "6"
}
}
return dialer.DialContext(ctx, network, address) return dialer.DialContext(ctx, network, address)
} }
net.DefaultResolver.PreferGo = true net.DefaultResolver.PreferGo = true
@@ -567,22 +558,10 @@ func setinterface(network, address string, fd uintptr, iface *net.Interface) err
switch network { switch network {
case "tcp6", "udp6", "ip6": case "tcp6", "udp6", "ip6":
dualStack := false
if network == "udp6" {
ipv6Only, err := windows.GetsockoptInt(windows.Handle(fd), windows.IPPROTO_IPV6, windows.IPV6_V6ONLY)
if err != nil {
return internet.NewBindError(err)
}
dualStack = ipv6Only == 0
}
err1 = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IPV6, IPV6_UNICAST_IF, iface.Index) err1 = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IPV6, IPV6_UNICAST_IF, iface.Index)
if network == "udp6" { if network == "udp6" {
err2 = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IPV6, windows.IPV6_MULTICAST_IF, iface.Index) err2 = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IPV6, windows.IPV6_MULTICAST_IF, iface.Index)
} }
// Only dual-stack UDP can send to both IPv4 and IPv6 destinations.
if !dualStack {
break
}
fallthrough fallthrough
case "tcp4", "udp4", "ip4": case "tcp4", "udp4", "ip4":
err3 = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IP, IP_UNICAST_IF, *(*int)(unsafe.Pointer(&index[0]))) err3 = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IP, IP_UNICAST_IF, *(*int)(unsafe.Pointer(&index[0])))
@@ -593,10 +572,7 @@ func setinterface(network, address string, fd uintptr, iface *net.Interface) err
panic(network + " " + address) panic(network + " " + address)
} }
if err := errors.Combine(err1, err2, err3, err4); err != nil { return errors.Combine(err1, err2, err3, err4)
return internet.NewBindError(err)
}
return nil
} }
func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, error) { func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, error) {
-87
View File
@@ -16,10 +16,8 @@ import (
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/common/serial" "github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/core" "github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/proxy/dokodemo"
"github.com/xtls/xray-core/proxy/freedom" "github.com/xtls/xray-core/proxy/freedom"
v2http "github.com/xtls/xray-core/proxy/http" v2http "github.com/xtls/xray-core/proxy/http"
v2httptest "github.com/xtls/xray-core/testing/servers/http" v2httptest "github.com/xtls/xray-core/testing/servers/http"
@@ -379,88 +377,3 @@ func TestHttpBasicAuth(t *testing.T) {
} }
} }
} }
// Server-first protocols (SMTP, FTP, MySQL) send nothing from the client until the server's
// greeting arrives, so the HTTP outbound must not wait for a first payload before dialing.
func TestHTTPOutboundServerFirst(t *testing.T) {
tcpServer := tcp.Server{
MsgProcessor: xor,
SendFirst: []byte("send first"),
}
dest, err := tcpServer.Start()
common.Must(err)
defer tcpServer.Close()
serverPort := tcp.PickPort()
serverConfig := &core.Config{
Inbound: []*core.InboundHandlerConfig{
{
ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}},
Listen: net.NewIPOrDomain(net.LocalHostIP),
}),
ProxySettings: serial.ToTypedMessage(&v2http.ServerConfig{}),
},
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
}
clientPort := tcp.PickPort()
clientConfig := &core.Config{
Inbound: []*core.InboundHandlerConfig{
{
ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}},
Listen: net.NewIPOrDomain(net.LocalHostIP),
}),
ProxySettings: serial.ToTypedMessage(&dokodemo.Config{
RewriteAddress: net.NewIPOrDomain(dest.Address),
RewritePort: uint32(dest.Port),
AllowedNetworks: []net.Network{net.Network_TCP},
}),
},
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&v2http.ClientConfig{
Server: &protocol.ServerEndpoint{
Address: net.NewIPOrDomain(net.LocalHostIP),
Port: uint32(serverPort),
},
}),
},
},
}
servers, err := InitializeServerConfigs(serverConfig, clientConfig)
common.Must(err)
defer CloseAllServers(servers)
conn, err := net.DialTCP("tcp", nil, &net.TCPAddr{
IP: []byte{127, 0, 0, 1},
Port: int(clientPort),
})
common.Must(err)
defer conn.Close()
common.Must(conn.SetReadDeadline(time.Now().Add(5 * time.Second)))
response := make([]byte, 1024)
nBytes, err := conn.Read(response)
if err != nil {
t.Fatal("no server greeting through HTTP outbound: ", err)
}
if string(response[:nBytes]) != "send first" {
t.Fatal("unexpected first response: ", string(response[:nBytes]))
}
common.Must(conn.SetReadDeadline(time.Time{}))
if err := testTCPConn2(conn, 1024, time.Second*5)(); err != nil {
t.Error(err)
}
}
-82
View File
@@ -3,8 +3,6 @@ package scenarios
import ( import (
"encoding/base64" "encoding/base64"
"encoding/hex" "encoding/hex"
"fmt"
"io"
"sync" "sync"
"testing" "testing"
"time" "time"
@@ -22,7 +20,6 @@ import (
"github.com/xtls/xray-core/proxy/dokodemo" "github.com/xtls/xray-core/proxy/dokodemo"
"github.com/xtls/xray-core/proxy/freedom" "github.com/xtls/xray-core/proxy/freedom"
"github.com/xtls/xray-core/proxy/vless" "github.com/xtls/xray-core/proxy/vless"
"github.com/xtls/xray-core/proxy/vless/encoding"
"github.com/xtls/xray-core/proxy/vless/inbound" "github.com/xtls/xray-core/proxy/vless/inbound"
"github.com/xtls/xray-core/proxy/vless/outbound" "github.com/xtls/xray-core/proxy/vless/outbound"
"github.com/xtls/xray-core/testing/servers/tcp" "github.com/xtls/xray-core/testing/servers/tcp"
@@ -660,82 +657,3 @@ func TestVlessRealityFingerprints(t *testing.T) {
} }
wg.Wait() wg.Wait()
} }
// A blocked connection must be closed after blockDelay even while the client keeps it open and sends nothing.
func TestVlessBlockDelay(t *testing.T) {
tcpServer := tcp.Server{
MsgProcessor: xor,
}
dest, err := tcpServer.Start()
common.Must(err)
defer tcpServer.Close()
for _, delay := range []uint64{0, 1} {
t.Run(fmt.Sprint("blockDelay=", delay), func(t *testing.T) {
userID := protocol.NewID(uuid.New())
serverPort := tcp.PickPort()
serverConfig := &core.Config{
Inbound: []*core.InboundHandlerConfig{
{
ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}},
Listen: net.NewIPOrDomain(net.LocalHostIP),
}),
ProxySettings: serial.ToTypedMessage(&inbound.Config{
Users: []*protocol.User{
{
Account: serial.ToTypedMessage(&vless.Account{
Id: userID.String(),
}),
},
},
}),
},
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
FinalRules: []*freedom.FinalRuleConfig{
{
Action: freedom.RuleAction_Block,
BlockDelay: &freedom.Range{Min: delay, Max: delay},
},
},
}),
},
},
}
servers, err := InitializeServerConfigs(serverConfig)
common.Must(err)
defer CloseAllServers(servers)
conn, err := net.DialTCP("tcp", nil, &net.TCPAddr{
IP: []byte{127, 0, 0, 1},
Port: int(serverPort),
})
common.Must(err)
defer conn.Close()
start := time.Now()
common.Must(encoding.EncodeRequestHeader(conn, &protocol.RequestHeader{
Version: encoding.Version,
User: &protocol.MemoryUser{Account: &vless.MemoryAccount{ID: userID}},
Command: protocol.RequestCommandTCP,
Address: dest.Address,
Port: dest.Port,
}, &encoding.Addons{}))
blockDelay := time.Duration(delay) * time.Second
common.Must(conn.SetReadDeadline(time.Now().Add(blockDelay + 5*time.Second)))
_, err = io.Copy(io.Discard, conn)
elapsed := time.Since(start)
if err, ok := err.(net.Error); ok && err.Timeout() {
t.Fatalf("connection is still open after %v", elapsed)
}
if elapsed < blockDelay {
t.Fatalf("connection is closed after %v, before blockDelay", elapsed)
}
})
}
}
@@ -1,63 +0,0 @@
package filter
import (
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/transport/internet/finalmask"
)
func (c *Config) buildFilter() (*filter, error) {
if len(c.BlockedSourceIps) == 0 && len(c.AllowedSourceIps) == 0 {
return nil, nil
}
f := new(filter)
var err error
if len(c.BlockedSourceIps) > 0 {
f.blocked, err = geodata.IPReg.BuildIPMatcher(c.BlockedSourceIps)
if err != nil {
return nil, err
}
}
if len(c.AllowedSourceIps) > 0 {
f.allowed, err = geodata.IPReg.BuildIPMatcher(c.AllowedSourceIps)
if err != nil {
return nil, err
}
}
return f, nil
}
func (c *Config) Listen(inner net.Listener) (net.Listener, error) {
f, err := c.buildFilter()
if err != nil {
return nil, err
}
if f == nil {
return inner, nil
}
return &listener{Listener: inner, filter: f}, nil
}
func (c *Config) WrapConnClient(conn net.Conn, dest *net.Destination, dialer *finalmask.Dialer) (net.Conn, error) {
return conn, nil
}
func (c *Config) WrapConnServer(conn net.Conn) (net.Conn, error) {
// TCP filtering is installed by Listen, before per-connection masks.
return conn, nil
}
func (c *Config) WrapPacketConnClient(conn net.PacketConn, dest *net.Destination, dialer *finalmask.Dialer) (net.PacketConn, error) {
return conn, nil
}
func (c *Config) WrapPacketConnServer(conn net.PacketConn, addr net.Addr, lc *finalmask.ListenConfig) (net.PacketConn, error) {
f, err := c.buildFilter()
if err != nil {
return nil, err
}
if f == nil {
return conn, nil
}
return &packetConn{PacketConn: conn, filter: f}, nil
}
@@ -1,136 +0,0 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v6.33.5
// source: transport/internet/finalmask/filter/config.proto
package filter
import (
geodata "github.com/xtls/xray-core/common/geodata"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Config struct {
state protoimpl.MessageState `protogen:"open.v1"`
BlockedSourceIps []*geodata.IPRule `protobuf:"bytes,1,rep,name=blocked_source_ips,json=blockedSourceIps,proto3" json:"blocked_source_ips,omitempty"`
AllowedSourceIps []*geodata.IPRule `protobuf:"bytes,2,rep,name=allowed_source_ips,json=allowedSourceIps,proto3" json:"allowed_source_ips,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Config) Reset() {
*x = Config{}
mi := &file_transport_internet_finalmask_filter_config_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Config) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Config) ProtoMessage() {}
func (x *Config) ProtoReflect() protoreflect.Message {
mi := &file_transport_internet_finalmask_filter_config_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Config.ProtoReflect.Descriptor instead.
func (*Config) Descriptor() ([]byte, []int) {
return file_transport_internet_finalmask_filter_config_proto_rawDescGZIP(), []int{0}
}
func (x *Config) GetBlockedSourceIps() []*geodata.IPRule {
if x != nil {
return x.BlockedSourceIps
}
return nil
}
func (x *Config) GetAllowedSourceIps() []*geodata.IPRule {
if x != nil {
return x.AllowedSourceIps
}
return nil
}
var File_transport_internet_finalmask_filter_config_proto protoreflect.FileDescriptor
const file_transport_internet_finalmask_filter_config_proto_rawDesc = "" +
"\n" +
"0transport/internet/finalmask/filter/config.proto\x12(xray.transport.internet.finalmask.filter\x1a\x1bcommon/geodata/geodat.proto\"\x9e\x01\n" +
"\x06Config\x12I\n" +
"\x12blocked_source_ips\x18\x01 \x03(\v2\x1b.xray.common.geodata.IPRuleR\x10blockedSourceIps\x12I\n" +
"\x12allowed_source_ips\x18\x02 \x03(\v2\x1b.xray.common.geodata.IPRuleR\x10allowedSourceIpsB\x9a\x01\n" +
",com.xray.transport.internet.finalmask.filterP\x01Z=github.com/xtls/xray-core/transport/internet/finalmask/filter\xaa\x02(Xray.Transport.Internet.Finalmask.Filterb\x06proto3"
var (
file_transport_internet_finalmask_filter_config_proto_rawDescOnce sync.Once
file_transport_internet_finalmask_filter_config_proto_rawDescData []byte
)
func file_transport_internet_finalmask_filter_config_proto_rawDescGZIP() []byte {
file_transport_internet_finalmask_filter_config_proto_rawDescOnce.Do(func() {
file_transport_internet_finalmask_filter_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_filter_config_proto_rawDesc), len(file_transport_internet_finalmask_filter_config_proto_rawDesc)))
})
return file_transport_internet_finalmask_filter_config_proto_rawDescData
}
var file_transport_internet_finalmask_filter_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_transport_internet_finalmask_filter_config_proto_goTypes = []any{
(*Config)(nil), // 0: xray.transport.internet.finalmask.filter.Config
(*geodata.IPRule)(nil), // 1: xray.common.geodata.IPRule
}
var file_transport_internet_finalmask_filter_config_proto_depIdxs = []int32{
1, // 0: xray.transport.internet.finalmask.filter.Config.blocked_source_ips:type_name -> xray.common.geodata.IPRule
1, // 1: xray.transport.internet.finalmask.filter.Config.allowed_source_ips:type_name -> xray.common.geodata.IPRule
2, // [2:2] is the sub-list for method output_type
2, // [2:2] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
}
func init() { file_transport_internet_finalmask_filter_config_proto_init() }
func file_transport_internet_finalmask_filter_config_proto_init() {
if File_transport_internet_finalmask_filter_config_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_filter_config_proto_rawDesc), len(file_transport_internet_finalmask_filter_config_proto_rawDesc)),
NumEnums: 0,
NumMessages: 1,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_transport_internet_finalmask_filter_config_proto_goTypes,
DependencyIndexes: file_transport_internet_finalmask_filter_config_proto_depIdxs,
MessageInfos: file_transport_internet_finalmask_filter_config_proto_msgTypes,
}.Build()
File_transport_internet_finalmask_filter_config_proto = out.File
file_transport_internet_finalmask_filter_config_proto_goTypes = nil
file_transport_internet_finalmask_filter_config_proto_depIdxs = nil
}
@@ -1,14 +0,0 @@
syntax = "proto3";
package xray.transport.internet.finalmask.filter;
option csharp_namespace = "Xray.Transport.Internet.Finalmask.Filter";
option go_package = "github.com/xtls/xray-core/transport/internet/finalmask/filter";
option java_package = "com.xray.transport.internet.finalmask.filter";
option java_multiple_files = true;
import "common/geodata/geodat.proto";
message Config {
repeated xray.common.geodata.IPRule blocked_source_ips = 1;
repeated xray.common.geodata.IPRule allowed_source_ips = 2;
}
@@ -1,69 +0,0 @@
package filter
import (
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net"
)
type listener struct {
net.Listener
filter *filter
}
// Accept filters connections after the TCP handshake and before higher-level processing.
func (l *listener) Accept() (net.Conn, error) {
for {
conn, err := l.Listener.Accept()
if err != nil {
return nil, err
}
if l.filter.rejects(conn.RemoteAddr()) {
conn.Close()
continue
}
return conn, nil
}
}
type packetConn struct {
net.PacketConn
filter *filter
}
func (c *packetConn) ReadFrom(p []byte) (int, net.Addr, error) {
for {
n, addr, err := c.PacketConn.ReadFrom(p)
// Reads may return both data and an error; filter the data first.
if c.filter.rejects(addr) {
if err != nil {
return 0, nil, err
}
continue
}
return n, addr, err
}
}
type filter struct {
blocked geodata.IPMatcher
allowed geodata.IPMatcher
}
func (f *filter) rejects(addr net.Addr) bool {
var ip net.IP
switch addr := addr.(type) {
case *net.TCPAddr:
ip = addr.IP
case *net.UDPAddr:
ip = addr.IP
case *net.IPAddr:
ip = addr.IP
default:
// Unknown sources cannot satisfy an allowlist.
return f.allowed != nil
}
if f.blocked != nil && f.blocked.Match(ip) {
return true
}
return f.allowed != nil && !f.allowed.Match(ip)
}
@@ -1,311 +0,0 @@
package filter
import (
"context"
"errors"
"io"
"net"
"testing"
"time"
"github.com/pires/go-proxyproto"
"github.com/xtls/xray-core/common/geodata"
xnet "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/transport/internet/finalmask"
)
func TestTCPFilterBeforeOtherMasks(t *testing.T) {
for _, policy := range testPolicies(t) {
t.Run(policy.name, func(t *testing.T) {
blocked4 := sourceConn("192.0.2.1")
blocked6 := sourceConn("2001:db8::1")
unknown := &testConn{addr: &net.UnixAddr{Name: "filter.sock", Net: "unix"}}
missing := new(testConn)
allowed := sourceConn("198.51.100.1")
allowed6 := sourceConn("2001:db8:1::1")
raw := &testListener{conns: []net.Conn{blocked4, blocked6, unknown, missing, allowed, allowed6}}
spy := new(recordMask)
fm := finalmask.NewFinalMask([]finalmask.TCPMask{spy, policy.config}, nil, nil,
func(context.Context, net.Addr) (net.Listener, error) { return raw, nil }, nil, nil)
l, err := fm.Listen(context.Background(), &net.TCPAddr{})
if err != nil {
t.Fatal(err)
}
defer l.Close()
whitelist := len(policy.config.AllowedSourceIps) > 0
want := []net.Conn{allowed, allowed6}
if !whitelist {
want = append([]net.Conn{unknown, missing}, want...)
}
for _, expected := range want {
if got, err := l.Accept(); err != nil || got != expected {
t.Fatalf("Accept() = (%v, %v), want %v", got, err, expected)
}
}
// Fake sockets panic on Read or Write. Rejection must not require handshake data.
if !blocked4.closed || !blocked6.closed || unknown.closed != whitelist || missing.closed != whitelist || allowed.closed || allowed6.closed || raw.closed {
t.Fatal("filter did not close only the blocked connections")
}
if len(spy.seen) != len(want) {
t.Fatal("blocked connections reached the next mask")
}
for i, conn := range spy.seen {
if conn != want[i] {
t.Fatal("unexpected connection reached the next mask")
}
}
if conn, err := l.Accept(); conn != nil || !errors.Is(err, io.EOF) {
t.Fatalf("listener error was not propagated: (%v, %v)", conn, err)
}
})
}
}
func TestTCPFilterUsesProxyHeader(t *testing.T) {
for _, test := range []struct {
name, peer, reported string
blocked bool
}{
{"blocked_header", "198.51.100.1", "192.0.2.1", true},
{"allowed_header", "192.0.2.1", "198.51.100.1", false},
} {
t.Run(test.name, func(t *testing.T) {
server, client := net.Pipe()
defer server.Close()
defer client.Close()
if err := server.SetDeadline(time.Now().Add(5 * time.Second)); err != nil {
t.Fatal(err)
}
peer := sourceConn(test.peer)
peer.Conn = server
conn := proxyproto.NewConn(peer)
next := sourceConn("198.51.100.2")
l, err := testConfig(t, "192.0.2.0/24").Listen(&testListener{conns: []net.Conn{conn, next}})
if err != nil {
t.Fatal(err)
}
defer l.Close()
written := make(chan error, 1)
go func() {
_, err := client.Write([]byte("PROXY TCP4 " + test.reported + " 203.0.113.1 1234 443\r\n"))
written <- err
}()
got, err := l.Accept()
if err != nil {
t.Fatal(err)
}
if err := <-written; err != nil {
t.Fatal(err)
}
var want net.Conn = conn
if test.blocked {
want = next
}
if got != want || peer.closed != test.blocked {
t.Fatal("filter did not follow the address supplied by PROXY protocol")
}
})
}
}
func TestUDPFilter(t *testing.T) {
for _, policy := range testPolicies(t) {
t.Run(policy.name, func(t *testing.T) {
allowed := &net.UDPAddr{IP: net.ParseIP("198.51.100.1"), Port: 1234}
allowed6 := &net.UDPAddr{IP: net.ParseIP("2001:db8:1::1"), Port: 1234}
unknown := datagram{data: []byte("unknown"), addr: &net.UnixAddr{Name: "filter.sock", Net: "unixgram"}}
missing := datagram{data: []byte("missing address")}
raw := &testPacketConn{reads: []datagram{
{data: []byte("blocked"), addr: &net.UDPAddr{IP: net.ParseIP("192.0.2.1"), Port: 1234}},
{addr: &net.UDPAddr{IP: net.ParseIP("2001:db8::1"), Port: 1234}},
unknown,
missing,
{addr: allowed},
{data: []byte("allowed"), addr: allowed},
{data: []byte("allowed6"), addr: allowed6},
{data: []byte("blocked again"), addr: &net.UDPAddr{IP: net.ParseIP("192.0.2.2"), Port: 1234}},
}}
fm := finalmask.NewFinalMask(nil, []finalmask.UDPMask{policy.config}, nil, nil, nil,
func(context.Context, net.Addr) (net.PacketConn, error) { return raw, nil })
conn, err := fm.ListenPacket(context.Background(), &net.UDPAddr{})
if err != nil {
t.Fatal(err)
}
defer conn.Close()
buf := make([]byte, 64)
if len(policy.config.AllowedSourceIps) == 0 {
for _, expected := range []datagram{unknown, missing} {
n, addr, err := conn.ReadFrom(buf)
if err != nil || addr != expected.addr || string(buf[:n]) != string(expected.data) {
t.Fatalf("unknown source was not allowed by blacklist: (%q, %v, %v)", buf[:n], addr, err)
}
}
}
if n, addr, err := conn.ReadFrom(buf); n != 0 || addr != allowed || err != nil {
t.Fatalf("zero-length allowed datagram was lost: (%d, %v, %v)", n, addr, err)
}
n, addr, err := conn.ReadFrom(buf)
if err != nil || addr != allowed || string(buf[:n]) != "allowed" {
t.Fatalf("ReadFrom() = (%q, %v, %v)", buf[:n], addr, err)
}
n, addr, err = conn.ReadFrom(buf)
if err != nil || addr != allowed6 || string(buf[:n]) != "allowed6" {
t.Fatalf("IPv6 ReadFrom() = (%q, %v, %v)", buf[:n], addr, err)
}
if _, _, err := conn.ReadFrom(buf); !errors.Is(err, io.EOF) {
t.Fatalf("underlying read error was not propagated: %v", err)
}
if raw.closed {
t.Fatal("dropping a packet closed the shared UDP socket")
}
})
}
}
func TestEmptyFilter(t *testing.T) {
for _, empty := range []*Config{{}, {AllowedSourceIps: []*geodata.IPRule{}}} {
rawListener := new(testListener)
rawPacket := new(testPacketConn)
if l, err := empty.Listen(rawListener); err != nil || l != rawListener {
t.Fatal("an empty TCP filter should preserve the listener")
}
if conn, err := empty.WrapPacketConnServer(rawPacket, nil, nil); err != nil || conn != rawPacket {
t.Fatal("an empty UDP filter should preserve the socket")
}
}
}
func TestUDPFilterReadError(t *testing.T) {
for _, policy := range testPolicies(t) {
t.Run(policy.name, func(t *testing.T) {
for _, test := range []struct {
name string
source net.Addr
blocked bool
}{
{"blocked", &net.UDPAddr{IP: net.ParseIP("192.0.2.1")}, true},
{"allowed", &net.UDPAddr{IP: net.ParseIP("198.51.100.1")}, false},
{"missing address", nil, len(policy.config.AllowedSourceIps) > 0},
{"unknown address", &net.UnixAddr{Name: "filter.sock", Net: "unixgram"}, len(policy.config.AllowedSourceIps) > 0},
} {
t.Run(test.name, func(t *testing.T) {
source := test.source
raw := &testPacketConn{reads: []datagram{{data: []byte("partial"), addr: source, err: io.ErrShortBuffer}}}
conn, err := policy.config.WrapPacketConnServer(raw, nil, nil)
if err != nil {
t.Fatal(err)
}
buf := make([]byte, 32)
n, addr, err := conn.ReadFrom(buf)
if !errors.Is(err, io.ErrShortBuffer) {
t.Fatalf("read error was not preserved: %v", err)
}
if test.blocked {
if n != 0 || addr != nil {
t.Fatal("partial data from a blocked source was returned with an error")
}
} else if n != 7 || addr != source || string(buf[:n]) != "partial" {
t.Fatal("partial data from an unblocked source was changed")
}
})
}
})
}
}
func testPolicies(t *testing.T) []struct {
name string
config *Config
} {
t.Helper()
blocked := testConfig(t, "192.0.2.0/24", "2001:db8::/48").BlockedSourceIps
allowed := testConfig(t, "198.51.100.0/24", "2001:db8:1::/48").BlockedSourceIps
broader := testConfig(t, "192.0.2.0/24", "198.51.100.0/24", "2001:db8::/32").BlockedSourceIps
return []struct {
name string
config *Config
}{
{"blacklist", &Config{BlockedSourceIps: blocked}},
{"whitelist", &Config{AllowedSourceIps: allowed}},
{"both", &Config{BlockedSourceIps: blocked, AllowedSourceIps: broader}},
}
}
func testConfig(t *testing.T, entries ...string) *Config {
t.Helper()
rules, err := geodata.ParseIPRules(entries)
if err != nil {
t.Fatal(err)
}
return &Config{BlockedSourceIps: rules}
}
type testConn struct {
net.Conn
addr net.Addr
closed bool
}
func sourceConn(ip string) *testConn {
return &testConn{addr: &net.TCPAddr{IP: net.ParseIP(ip), Port: 1234}}
}
func (c *testConn) RemoteAddr() net.Addr { return c.addr }
func (c *testConn) Close() error {
c.closed = true
if c.Conn != nil {
return c.Conn.Close()
}
return nil
}
type testListener struct {
net.Listener
conns []net.Conn
closed bool
}
func (l *testListener) Accept() (net.Conn, error) {
if len(l.conns) == 0 {
return nil, io.EOF
}
conn := l.conns[0]
l.conns = l.conns[1:]
return conn, nil
}
func (l *testListener) Close() error { l.closed = true; return nil }
type datagram struct {
data []byte
addr net.Addr
err error
}
type testPacketConn struct {
net.PacketConn
reads []datagram
closed bool
}
func (c *testPacketConn) ReadFrom(p []byte) (int, net.Addr, error) {
if len(c.reads) == 0 {
return 0, nil, io.EOF
}
packet := c.reads[0]
c.reads = c.reads[1:]
return copy(p, packet.data), packet.addr, packet.err
}
func (c *testPacketConn) Close() error { c.closed = true; return nil }
type recordMask struct{ seen []net.Conn }
func (m *recordMask) WrapConnClient(c net.Conn, _ *xnet.Destination, _ *finalmask.Dialer) (net.Conn, error) {
return c, nil
}
func (m *recordMask) WrapConnServer(c net.Conn) (net.Conn, error) {
m.seen = append(m.seen, c)
return c, nil
}
+8 -8
View File
@@ -114,13 +114,13 @@ func applyOutboundSocketOptions(network string, address string, fd uintptr, conf
if config.TcpKeepAliveIdle > 0 || config.TcpKeepAliveInterval > 0 { if config.TcpKeepAliveIdle > 0 || config.TcpKeepAliveInterval > 0 {
if config.TcpKeepAliveIdle > 0 { if config.TcpKeepAliveIdle > 0 {
if err := unix.SetsockoptInt(int(fd), unix.IPPROTO_TCP, unix.TCP_KEEPALIVE, int(config.TcpKeepAliveIdle)); err != nil { if err := unix.SetsockoptInt(int(fd), unix.IPPROTO_TCP, unix.TCP_KEEPALIVE, int(config.TcpKeepAliveInterval)); err != nil {
return errors.New("failed to set TCP_KEEPALIVE", err) return errors.New("failed to set TCP_KEEPINTVL", err)
} }
} }
if config.TcpKeepAliveInterval > 0 { if config.TcpKeepAliveInterval > 0 {
if err := unix.SetsockoptInt(int(fd), unix.IPPROTO_TCP, sysTCP_KEEPINTVL, int(config.TcpKeepAliveInterval)); err != nil { if err := unix.SetsockoptInt(int(fd), unix.IPPROTO_TCP, sysTCP_KEEPINTVL, int(config.TcpKeepAliveIdle)); err != nil {
return errors.New("failed to set TCP_KEEPINTVL", err) return errors.New("failed to set TCP_KEEPIDLE", err)
} }
} }
if err := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_KEEPALIVE, 1); err != nil { if err := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_KEEPALIVE, 1); err != nil {
@@ -204,13 +204,13 @@ func applyInboundSocketOptions(network string, fd uintptr, config *SocketConfig)
if config.TcpKeepAliveIdle > 0 || config.TcpKeepAliveInterval > 0 { if config.TcpKeepAliveIdle > 0 || config.TcpKeepAliveInterval > 0 {
if config.TcpKeepAliveIdle > 0 { if config.TcpKeepAliveIdle > 0 {
if err := unix.SetsockoptInt(int(fd), unix.IPPROTO_TCP, unix.TCP_KEEPALIVE, int(config.TcpKeepAliveIdle)); err != nil { if err := unix.SetsockoptInt(int(fd), unix.IPPROTO_TCP, unix.TCP_KEEPALIVE, int(config.TcpKeepAliveInterval)); err != nil {
return errors.New("failed to set TCP_KEEPALIVE", err) return errors.New("failed to set TCP_KEEPINTVL", err)
} }
} }
if config.TcpKeepAliveInterval > 0 { if config.TcpKeepAliveInterval > 0 {
if err := unix.SetsockoptInt(int(fd), unix.IPPROTO_TCP, sysTCP_KEEPINTVL, int(config.TcpKeepAliveInterval)); err != nil { if err := unix.SetsockoptInt(int(fd), unix.IPPROTO_TCP, sysTCP_KEEPINTVL, int(config.TcpKeepAliveIdle)); err != nil {
return errors.New("failed to set TCP_KEEPINTVL", err) return errors.New("failed to set TCP_KEEPIDLE", err)
} }
} }
if err := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_KEEPALIVE, 1); err != nil { if err := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_KEEPALIVE, 1); err != nil {
+2 -16
View File
@@ -2,8 +2,6 @@ package internet
import ( import (
"context" "context"
go_errors "errors"
"runtime"
"sync" "sync"
"syscall" "syscall"
"time" "time"
@@ -71,10 +69,9 @@ func (d *DefaultSystemDialer) Dial(ctx context.Context, src net.Address, dest ne
} }
var lc net.ListenConfig var lc net.ListenConfig
if sockopt != nil || len(Controllers) > 0 { if sockopt != nil || len(Controllers) > 0 {
lc.Control = func(network, _ string, c syscall.RawConn) error { lc.Control = func(network, address string, c syscall.RawConn) error {
// ListenPacket supplies a local address; dialer controllers need the destination.
for _, ctl := range Controllers { for _, ctl := range Controllers {
if err := ctl(network, destAddr.String(), c); err != nil { if err := ctl(network, address, c); err != nil {
return errors.New("failed to apply external controller").Base(err) return errors.New("failed to apply external controller").Base(err)
} }
} }
@@ -94,16 +91,6 @@ func (d *DefaultSystemDialer) Dial(ctx context.Context, src net.Address, dest ne
} }
} }
packetConn, err := lc.ListenPacket(ctx, srcAddr.Network(), srcAddr.String()) packetConn, err := lc.ListenPacket(ctx, srcAddr.Network(), srcAddr.String())
var bindErr *BindError
if err != nil && runtime.GOOS == "windows" && go_errors.As(err, &bindErr) {
// Keep dual-stack UDP when possible. If the interface cannot bind both
// families, retry a socket that cannot send on the unbound family.
network := "udp6"
if destAddr.IP.To4() != nil {
network = "udp4"
}
packetConn, err = lc.ListenPacket(ctx, network, srcAddr.String())
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -210,7 +197,6 @@ func UseAlternativeSystemDialer(dialer SystemDialer) {
// RegisterDialerController adds a controller to the effective system dialer. // RegisterDialerController adds a controller to the effective system dialer.
// The controller can be used to operate on file descriptors before they are put into use. // The controller can be used to operate on file descriptors before they are put into use.
// It receives the network and resolved remote address, as in net.Dialer.Control.
// It only works when effective dialer is the default dialer. // It only works when effective dialer is the default dialer.
// //
// xray:api:beta // xray:api:beta
@@ -1,63 +0,0 @@
package internet_test
import (
"context"
"errors"
"slices"
"syscall"
"testing"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/transport/internet"
"golang.org/x/sys/windows"
)
// TestDialUDPBindFallback checks that a UDP dial whose dual-stack socket
// cannot be bound to both IP versions, as on an interface without one of
// them, moves to a socket of the destination's version only, and still
// fails when that one cannot be bound either.
func TestDialUDPBindFallback(t *testing.T) {
for _, c := range []struct {
dest string
reject []string // the kinds of socket that cannot be bound
tried []string // the kinds of socket tried, in order
ok bool
}{
{"127.0.0.1", nil, []string{"dual"}, true},
{"127.0.0.1", []string{"dual"}, []string{"dual", "udp4"}, true},
{"::1", []string{"dual"}, []string{"dual", "udp6"}, true},
{"127.0.0.1", []string{"dual", "udp4"}, []string{"dual", "udp4"}, false},
} {
var tried []string
internet.ControllersLock.Lock()
saved := internet.Controllers
internet.Controllers = []func(string, string, syscall.RawConn) error{func(network, _ string, rc syscall.RawConn) error {
kind := network
if network == "udp6" {
rc.Control(func(fd uintptr) {
if v6only, _ := windows.GetsockoptInt(windows.Handle(fd), windows.IPPROTO_IPV6, windows.IPV6_V6ONLY); v6only == 0 {
kind = "dual"
}
})
}
tried = append(tried, kind)
if slices.Contains(c.reject, kind) {
return internet.NewBindError(errors.New("cannot bind " + kind))
}
return nil
}}
internet.ControllersLock.Unlock()
conn, err := internet.DialSystem(context.Background(), net.UDPDestination(net.ParseAddress(c.dest), 53), nil)
if conn != nil {
conn.Close()
}
internet.ControllersLock.Lock()
internet.Controllers = saved
internet.ControllersLock.Unlock()
if (err == nil) != c.ok || !slices.Equal(tried, c.tried) {
t.Errorf("%s rejecting %v: tried %v, err %v", c.dest, c.reject, tried, err)
}
}
}
@@ -46,12 +46,6 @@ func (c *connection) Read(b []byte) (int, error) {
for { for {
reader, err := c.getReader() reader, err := c.getReader()
if err != nil { if err != nil {
// A close frame with a normal status (1000) or no status (1005) ends
// the byte stream the same way a FIN does, so report io.EOF instead
// of an error. Other close codes keep their error semantics.
if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) {
return 0, io.EOF
}
return 0, err return 0, err
} }
+12 -270
View File
@@ -2,25 +2,14 @@ package xdrive
import ( import (
"context" "context"
gotls "crypto/tls"
"io" "io"
"net/http" "net/http"
"reflect"
"runtime"
"slices"
"sync" "sync"
"time" "time"
"github.com/apernet/quic-go"
"github.com/apernet/quic-go/http3"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/net/cnc"
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/transport/internet" "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/splithttp"
"github.com/xtls/xray-core/transport/internet/tls" "github.com/xtls/xray-core/transport/internet/tls"
) )
@@ -50,10 +39,6 @@ func (t *serviceTransport) RoundTrip(r *http.Request) (*http.Response, error) {
cancel(nil) cancel(nil)
} }
r = r.WithContext(ctx) r = r.WithContext(ctx)
// the headers of chrome as with XHTTP and DoH, net/http and http3 would send
// their own user agent after a hello of chrome
r.Header = r.Header.Clone()
utils.TryDefaultHeadersWith(r.Header, "fetch")
if r.Body != nil && r.Body != http.NoBody { if r.Body != nil && r.Body != http.NoBody {
r.Body = &progressBody{ReadCloser: r.Body, alive: alive} r.Body = &progressBody{ReadCloser: r.Body, alive: alive}
} }
@@ -119,40 +104,25 @@ func (b *progressBody) Close() error {
} }
// newServiceClient returns a client whose requests end after stall without // newServiceClient returns a client whose requests end after stall without
// progress, see serviceTransport.RoundTrip, and whose connections close when // progress, see serviceTransport.RoundTrip
// connCtx is done. Clients that share h1Hosts share which hosts answer over h1 func newServiceClient(streamSettings *internet.MemoryStreamConfig, stall time.Duration, maxConns int) *http.Client {
func newServiceClient(connCtx context.Context, h1Hosts *sync.Map, streamSettings *internet.MemoryStreamConfig, stall time.Duration, maxConns int) *http.Client {
var ( var (
tlsConfig *tls.Config tlsConfig *tls.Config
sockopt *internet.SocketConfig sockopt *internet.SocketConfig
fronting *net.Destination fronting *net.Destination
keepAlive = net.ChromeH2KeepAlivePeriod
// h3 keeps alive only when asked to, see below
h3KeepAlive time.Duration
) )
if streamSettings != nil { if streamSettings != nil {
tlsConfig = tls.ConfigFromStreamSettings(streamSettings) tlsConfig = tls.ConfigFromStreamSettings(streamSettings)
sockopt = streamSettings.SocketSettings sockopt = streamSettings.SocketSettings
fronting = streamSettings.Destination fronting = streamSettings.Destination
if config, ok := streamSettings.ProtocolSettings.(*Config); ok && config.Xmux != nil {
switch period := config.Xmux.HKeepAlivePeriod; {
case period > 0:
keepAlive = time.Duration(period) * time.Second
h3KeepAlive = keepAlive
case period < 0:
keepAlive = 0
}
}
} }
if tlsConfig == nil { if tlsConfig == nil {
tlsConfig = &tls.Config{} tlsConfig = &tls.Config{}
} }
fingerprint := tls.GetFingerprint(tlsConfig.Fingerprint) fingerprint := tls.GetFingerprint(tlsConfig.Fingerprint)
// fronting takes only the address and port, the network stays that of the dial := func(ctx context.Context, addr string) (net.Conn, net.Destination, error) {
// dial, which is UDP for h3 though the destination is built as TCP host, err := net.ParseDestination("tcp:" + addr)
dial := func(ctx context.Context, network, addr string) (net.Conn, net.Destination, error) {
host, err := net.ParseDestination(network + ":" + addr)
if err != nil { if err != nil {
return nil, host, errors.New("bad address: ", addr).Base(err) return nil, host, errors.New("bad address: ", addr).Base(err)
} }
@@ -166,36 +136,25 @@ func newServiceClient(connCtx context.Context, h1Hosts *sync.Map, streamSettings
} }
var conn net.Conn var conn net.Conn
switch { if streamSettings.FinalMask != nil {
case streamSettings.FinalMask == nil:
conn, err = internet.DialSystem(ctx, target, sockopt)
case target.Network == net.Network_UDP:
conn, err = streamSettings.FinalMask.DialUDP(ctx, target)
default:
conn, err = streamSettings.FinalMask.DialTCP(ctx, target) conn, err = streamSettings.FinalMask.DialTCP(ctx, target)
} else {
conn, err = internet.DialSystem(ctx, target, sockopt)
} }
if err != nil { if err != nil {
return nil, host, errors.New("failed to dial to dest").Base(err) return nil, host, errors.New("failed to dial to dest").Base(err)
} }
// h3 closes its QUIC connection instead, as the server never hears of a UDP
// socket that closes
if network == "udp" {
return conn, host, nil return conn, host, nil
} }
// CloseIdleConnections leaves an h2 connection open while a stream just
// canceled has not left it yet
stop := context.AfterFunc(connCtx, func() { conn.Close() })
return &ctxConn{Conn: conn, stop: stop}, host, nil
}
dialPlain := func(ctx context.Context, network, addr string) (net.Conn, error) { dialPlain := func(ctx context.Context, network, addr string) (net.Conn, error) {
conn, _, err := dial(ctx, "tcp", addr) conn, _, err := dial(ctx, addr)
return conn, err return conn, err
} }
// http.Transport picks h2 or h1 by the ALPN in the ConnectionState of the conn // http.Transport picks h2 or h1 by the ALPN in the ConnectionState of the conn
dialTLS := func(ctx context.Context, network, addr string) (net.Conn, error) { dialTLS := func(ctx context.Context, network, addr string) (net.Conn, error) {
conn, host, err := dial(ctx, "tcp", addr) conn, host, err := dial(ctx, addr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -226,86 +185,7 @@ func newServiceClient(connCtx context.Context, h1Hosts *sync.Map, streamSettings
MaxIdleConns: maxConns, MaxIdleConns: maxConns,
MaxIdleConnsPerHost: maxConns, MaxIdleConnsPerHost: maxConns,
MaxConnsPerHost: connsPerHost, MaxConnsPerHost: connsPerHost,
HTTP2: &http.HTTP2Config{SendPingTimeout: keepAlive}, HTTP2: &http.HTTP2Config{SendPingTimeout: net.ChromeH2KeepAlivePeriod},
}
}
var https http.RoundTripper = &secureTransport{h2: secure(1), h1: secure(maxConns), h1Hosts: h1Hosts}
// no server offers h3 by ALPN over TCP, so it is used only when asked for alone
if slices.Equal(tlsConfig.NextProtocol, []string{"h3"}) {
quicParams := streamSettings.QuicParams
if quicParams == nil {
quicParams = &internet.QuicParams{BbrProfile: string(bbr.ProfileStandard)}
}
// the defaults are those of quic-go, with no keepalive unless asked for, as
// in DoH: http3 never closes a connection that is idle or that it dropped
// after an error, so only the idle timeout or the end of connCtx ends it
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,
DisablePathMTUDiscovery: quicParams.DisablePathMtuDiscovery || (runtime.GOOS != "linux" && runtime.GOOS != "windows" && runtime.GOOS != "darwin"),
ChromeParrot: !quicParams.DisableChromeParrot,
}
if quicConfig.KeepAlivePeriod == 0 {
quicConfig.KeepAlivePeriod = h3KeepAlive
}
https = &http3.Transport{
QUICConfig: quicConfig,
Dial: func(ctx context.Context, addr string, _ *gotls.Config, cfg *quic.Config) (*quic.Conn, error) {
switch quicParams.Congestion {
case "", "bbr", "reno":
default:
// brutal is for Hysteria servers, not services, and a panic here, in
// a goroutine of http3, would end the process
return nil, errors.New("unsupported congestion control over h3: ", quicParams.Congestion)
}
conn, host, err := dial(ctx, "udp", addr)
if err != nil {
return nil, err
}
var pktConn net.PacketConn
var udpAddr net.Addr
switch c := conn.(type) {
case *net.PacketConnWrapper:
pktConn = c.PacketConn
udpAddr = c.RemoteAddr()
case *cnc.Connection:
pktConn = &internet.FakePacketConn{Conn: c}
udpAddr = &net.UDPAddr{IP: []byte{0, 0, 0, 0}}
default:
conn.Close()
return nil, errors.New("failed to dial to dest: unexpected ", reflect.TypeOf(c))
}
// built for each dial with the host as SNI, as for h2: http3 sets the
// SNI on a copy, which a pinned CA and the ECH lookup do not see
gotlsConfig := tlsConfig.GetTLSConfig(tls.WithDestination(host))
// it is for servers, and ChromeParrot refuses a config that has it
gotlsConfig.GetCertificate = nil
tr := &quic.Transport{Conn: pktConn, DisableGSO: quicParams.DisableGSO}
if !quicParams.DisableChromeParrot {
tr.ConnectionIDGenerator = quic.ZeroLengthConnectionIDGenerator{}
}
qconn, err := tr.Dial(ctx, udpAddr, gotlsConfig, cfg)
if err != nil {
tr.Close()
pktConn.Close()
return nil, err
}
stop := context.AfterFunc(connCtx, func() { qconn.CloseWithError(quic.ApplicationErrorCode(http3.ErrCodeNoError), "") })
context.AfterFunc(qconn.Context(), func() { stop(); tr.Close(); pktConn.Close() })
if quicParams.Congestion != "reno" {
congestion.UseBBR(qconn, bbr.Profile(quicParams.BbrProfile))
}
return qconn, nil
},
} }
} }
@@ -318,30 +198,18 @@ func newServiceClient(connCtx context.Context, h1Hosts *sync.Map, streamSettings
MaxIdleConnsPerHost: maxConns, MaxIdleConnsPerHost: maxConns,
MaxConnsPerHost: maxConns, MaxConnsPerHost: maxConns,
}, },
secure: https, secure: &secureTransport{h2: secure(1), h1: secure(maxConns)},
stall: stall, stall: stall,
}, },
} }
} }
// ctxConn is a connection that connCtx no longer closes once it is closed,
// so that one that ends on its own leaves nothing behind in connCtx
type ctxConn struct {
net.Conn
stop func() bool
}
func (c *ctxConn) Close() error {
c.stop()
return c.Conn.Close()
}
// secureTransport keeps a host to one connection, all that h2 needs, as without // secureTransport keeps a host to one connection, all that h2 needs, as without
// a limit parallel requests would each dial their own. A host that answers over // a limit parallel requests would each dial their own. A host that answers over
// h1 moves to a transport with more, since h1 carries one request at a time // h1 moves to a transport with more, since h1 carries one request at a time
type secureTransport struct { type secureTransport struct {
h2, h1 http.RoundTripper h2, h1 http.RoundTripper
h1Hosts *sync.Map h1Hosts sync.Map
} }
func (t *secureTransport) RoundTrip(r *http.Request) (*http.Response, error) { func (t *secureTransport) RoundTrip(r *http.Request) (*http.Response, error) {
@@ -354,129 +222,3 @@ func (t *secureTransport) RoundTrip(r *http.Request) (*http.Response, error) {
} }
return resp, err return resp, err
} }
var errSessionClosed = errors.New("the session is closed")
// connPool hands the sessions of a storage the connections of XMUX, each a
// client of its own made by newClient
type connPool struct {
mu sync.Mutex
xmux *splithttp.XmuxManager
}
func newConnPool(config *splithttp.XmuxConfig, newClient func(connCtx context.Context) *http.Client) *connPool {
var xmuxConfig splithttp.XmuxConfig
if config != nil {
xmuxConfig = *config
}
return &connPool{xmux: splithttp.NewXmuxManager(xmuxConfig, func() splithttp.XmuxConn {
ctx, cancel := context.WithCancel(context.Background())
return &poolConn{next: newClient(ctx).Transport, cancel: cancel}
})}
}
// poolConn is a connection of XMUX, which closes once XMUX drops it and the
// requests on it end
type poolConn struct {
next http.RoundTripper
cancel context.CancelFunc
mu sync.Mutex
inflight int
closing bool
}
func (c *poolConn) IsClosed() bool {
c.mu.Lock()
defer c.mu.Unlock()
return c.closing
}
func (c *poolConn) Close() error {
c.mu.Lock()
defer c.mu.Unlock()
c.closing = true
if c.inflight == 0 {
c.cancel()
}
return nil
}
func (c *poolConn) done() {
c.mu.Lock()
defer c.mu.Unlock()
c.inflight--
if c.closing && c.inflight == 0 {
c.cancel()
}
}
// roundTrip sends a request counted in inflight by the caller
func (c *poolConn) roundTrip(r *http.Request) (*http.Response, error) {
resp, err := c.next.RoundTrip(r)
if err != nil {
c.done()
return nil, err
}
resp.Body = &doneBody{ReadCloser: resp.Body, done: sync.OnceFunc(c.done)}
return resp, nil
}
type doneBody struct {
io.ReadCloser
done func()
}
func (b *doneBody) Close() error {
err := b.ReadCloser.Close()
b.done()
return err
}
// sessionTransport sends the requests of one session over a connection of
// XMUX, and over another once that one has no requests or time left
type sessionTransport struct {
pool *connPool
mu sync.Mutex
client *splithttp.XmuxClient
closed bool
}
func (t *sessionTransport) RoundTrip(r *http.Request) (*http.Response, error) {
t.mu.Lock()
if t.closed {
t.mu.Unlock()
return nil, errSessionClosed
}
client := t.client
if client == nil || client.LeftRequests.Add(-1) < 0 || !client.UnreusableAt.IsZero() && time.Now().After(client.UnreusableAt) {
t.pool.mu.Lock()
t.client = t.pool.xmux.GetXmuxClient(r.Context())
t.pool.mu.Unlock()
t.client.AddRunning()
t.client.LeftRequests.Add(-1)
if client != nil {
client.DoneRunning()
}
client = t.client
}
// counted under mu, or another request of the session that moves on could
// close the connection before this one is sent
conn := client.XmuxConn.(*poolConn)
conn.mu.Lock()
conn.inflight++
conn.mu.Unlock()
t.mu.Unlock()
return conn.roundTrip(r)
}
func (t *sessionTransport) Close() {
t.mu.Lock()
defer t.mu.Unlock()
t.closed = true
if t.client != nil {
t.client.DoneRunning()
t.client = nil
}
}
+12 -493
View File
@@ -4,7 +4,6 @@ import (
"bytes" "bytes"
"context" "context"
gotls "crypto/tls" gotls "crypto/tls"
"crypto/x509"
"encoding/base64" "encoding/base64"
"errors" "errors"
"io" "io"
@@ -12,22 +11,13 @@ import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"slices" "slices"
"strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
"testing" "testing"
"time" "time"
"github.com/apernet/quic-go"
"github.com/apernet/quic-go/http3"
xnet "github.com/xtls/xray-core/common/net" xnet "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/net/cnc"
"github.com/xtls/xray-core/common/protocol/tls/cert"
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/finalmask"
"github.com/xtls/xray-core/transport/internet/finalmask/noise"
"github.com/xtls/xray-core/transport/internet/splithttp"
"github.com/xtls/xray-core/transport/internet/tls" "github.com/xtls/xray-core/transport/internet/tls"
) )
@@ -85,7 +75,7 @@ func sniForSettings(t *testing.T, serverName string) string {
driveFilesURL = "https://www.googleapis.com/drive/v3/files" driveFilesURL = "https://www.googleapis.com/drive/v3/files"
defer func() { driveFilesURL = prev }() defer func() { driveFilesURL = prev }()
client := newServiceClient(context.Background(), new(sync.Map), settings, 5*time.Second, 8) client := newServiceClient(settings, 5*time.Second, 8)
req, err := http.NewRequest(http.MethodGet, driveFilesURL, nil) req, err := http.NewRequest(http.MethodGet, driveFilesURL, nil)
if err != nil { if err != nil {
t.Fatalf("request: %v", err) t.Fatalf("request: %v", err)
@@ -155,7 +145,7 @@ func TestServiceClientEndsOnlyStalledRequests(t *testing.T) {
} }
})) }))
defer server.Close() defer server.Close()
client := newServiceClient(context.Background(), new(sync.Map), &internet.MemoryStreamConfig{}, stall, 8) client := newServiceClient(&internet.MemoryStreamConfig{}, stall, 8)
fetch := func(path string, body io.Reader) (int, error) { fetch := func(path string, body io.Reader) (int, error) {
req, err := http.NewRequest(http.MethodPost, server.URL+path, body) req, err := http.NewRequest(http.MethodPost, server.URL+path, body)
@@ -227,7 +217,7 @@ func TestServiceClientStallOverTLS(t *testing.T) {
} }
time.Sleep(5 * stall) time.Sleep(5 * stall)
}) })
client := newServiceClient(context.Background(), new(sync.Map), server.settings(&tls.Config{}), stall, 8) client := newServiceClient(server.settings(&tls.Config{}), stall, 8)
for _, path := range []string{"/stuck", "/silent"} { for _, path := range []string{"/stuck", "/silent"} {
start := time.Now() start := time.Now()
@@ -250,13 +240,12 @@ func TestServiceClientStallOverTLS(t *testing.T) {
} }
// tlsServer is a TLS server that keeps the last hello it got and counts its // tlsServer is a TLS server that keeps the last hello it got and counts its
// connections, and those closed // connections
type tlsServer struct { type tlsServer struct {
*httptest.Server *httptest.Server
mu sync.Mutex mu sync.Mutex
hello *gotls.ClientHelloInfo hello *gotls.ClientHelloInfo
conns atomic.Int32 conns atomic.Int32
closed atomic.Int32
} }
func newTLSServer(t *testing.T, config *gotls.Config, handler http.HandlerFunc) *tlsServer { func newTLSServer(t *testing.T, config *gotls.Config, handler http.HandlerFunc) *tlsServer {
@@ -271,11 +260,8 @@ func newTLSServer(t *testing.T, config *gotls.Config, handler http.HandlerFunc)
} }
s.TLS = config s.TLS = config
s.Config.ConnState = func(_ net.Conn, state http.ConnState) { s.Config.ConnState = func(_ net.Conn, state http.ConnState) {
switch state { if state == http.StateNew {
case http.StateNew:
s.conns.Add(1) s.conns.Add(1)
case http.StateClosed:
s.closed.Add(1)
} }
} }
s.StartTLS() s.StartTLS()
@@ -312,7 +298,7 @@ func TestServiceClientSpeaksWhatServerPicks(t *testing.T) {
for _, protos := range [][]string{chromeALPN, {"http/1.1"}} { for _, protos := range [][]string{chromeALPN, {"http/1.1"}} {
server := newTLSServer(t, &gotls.Config{NextProtos: protos}, func(w http.ResponseWriter, r *http.Request) {}) server := newTLSServer(t, &gotls.Config{NextProtos: protos}, func(w http.ResponseWriter, r *http.Request) {})
for _, config := range []*tls.Config{{}, {NextProtocol: []string{"h2"}}, {NextProtocol: []string{"http/1.1"}}, {Fingerprint: "unsafe"}} { for _, config := range []*tls.Config{{}, {NextProtocol: []string{"h2"}}, {NextProtocol: []string{"http/1.1"}}, {Fingerprint: "unsafe"}} {
client := newServiceClient(context.Background(), new(sync.Map), server.settings(config), 5*time.Second, 8) client := newServiceClient(server.settings(config), 5*time.Second, 8)
resp, err := client.Get(server.URL) resp, err := client.Get(server.URL)
if err != nil { if err != nil {
t.Fatalf("server %v, alpn %v, fingerprint %q: %v", protos, config.NextProtocol, config.Fingerprint, err) t.Fatalf("server %v, alpn %v, fingerprint %q: %v", protos, config.NextProtocol, config.Fingerprint, err)
@@ -332,7 +318,7 @@ func TestServiceClientSpeaksWhatServerPicks(t *testing.T) {
// offers certificate compression // offers certificate compression
func TestServiceClientDefaultsToChrome(t *testing.T) { func TestServiceClientDefaultsToChrome(t *testing.T) {
server := newTLSServer(t, &gotls.Config{NextProtos: chromeALPN}, func(w http.ResponseWriter, r *http.Request) {}) server := newTLSServer(t, &gotls.Config{NextProtos: chromeALPN}, func(w http.ResponseWriter, r *http.Request) {})
client := newServiceClient(context.Background(), new(sync.Map), &internet.MemoryStreamConfig{}, 5*time.Second, 8) client := newServiceClient(&internet.MemoryStreamConfig{}, 5*time.Second, 8)
// the test certificate is not trusted, the hello is all that is needed // the test certificate is not trusted, the hello is all that is needed
if resp, err := client.Get(server.URL); err == nil { if resp, err := client.Get(server.URL); err == nil {
resp.Body.Close() resp.Body.Close()
@@ -346,33 +332,6 @@ func TestServiceClientDefaultsToChrome(t *testing.T) {
} }
} }
// a request says it is chrome, as the hello does, unless it says what it is
func TestServiceClientUserAgent(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, r.UserAgent())
}))
defer server.Close()
client := newServiceClient(context.Background(), new(sync.Map), &internet.MemoryStreamConfig{}, 5*time.Second, 8)
for ua, want := range map[string]string{"": utils.ChromeUA, "firefox": utils.FirefoxUA, "custom/1.0": "custom/1.0"} {
req, _ := http.NewRequest(http.MethodGet, server.URL, nil)
if ua != "" {
req.Header.Set("User-Agent", ua)
}
resp, err := client.Do(req)
if err != nil {
t.Fatal(err)
}
got, _ := io.ReadAll(resp.Body)
resp.Body.Close()
if string(got) != want {
t.Errorf("with %q the server got %q, not %q", ua, got, want)
}
if req.Header.Get("User-Agent") != ua || len(req.Header) > min(len(ua), 1) {
t.Errorf("with %q the request was changed to %v", ua, req.Header)
}
}
}
// parallel requests share one h2 connection instead of dialing one each, the // parallel requests share one h2 connection instead of dialing one each, the
// first ones as the later ones, and next to a host that answers over h1 // first ones as the later ones, and next to a host that answers over h1
func TestServiceClientOneConnection(t *testing.T) { func TestServiceClientOneConnection(t *testing.T) {
@@ -380,7 +339,7 @@ func TestServiceClientOneConnection(t *testing.T) {
time.Sleep(50 * time.Millisecond) time.Sleep(50 * time.Millisecond)
}) })
h1 := newTLSServer(t, &gotls.Config{NextProtos: []string{"http/1.1"}}, func(w http.ResponseWriter, r *http.Request) {}) h1 := newTLSServer(t, &gotls.Config{NextProtos: []string{"http/1.1"}}, func(w http.ResponseWriter, r *http.Request) {})
client := newServiceClient(context.Background(), new(sync.Map), server.settings(&tls.Config{}), 5*time.Second, driveMaxInflight) client := newServiceClient(server.settings(&tls.Config{}), 5*time.Second, driveMaxInflight)
get := func(url string) { get := func(url string) {
resp, err := client.Get(url) resp, err := client.Get(url)
if err != nil { if err != nil {
@@ -436,7 +395,7 @@ func TestServiceClientHTTP1(t *testing.T) {
server := newTLSServer(t, &gotls.Config{NextProtos: c.protos, EncryptedClientHelloKeys: echKeys}, func(w http.ResponseWriter, r *http.Request) { server := newTLSServer(t, &gotls.Config{NextProtos: c.protos, EncryptedClientHelloKeys: echKeys}, func(w http.ResponseWriter, r *http.Request) {
time.Sleep(50 * time.Millisecond) time.Sleep(50 * time.Millisecond)
}) })
client := newServiceClient(context.Background(), new(sync.Map), server.settings(c.config), 5*time.Second, 8) client := newServiceClient(server.settings(c.config), 5*time.Second, 8)
get := func() { get := func() {
resp, err := client.Get(server.URL) resp, err := client.Get(server.URL)
if err != nil { if err != nil {
@@ -467,7 +426,7 @@ func TestServiceClientHTTP1(t *testing.T) {
// without the keys the server gets the outer hello, which offers the ALPN of // without the keys the server gets the outer hello, which offers the ALPN of
// Chrome // Chrome
outer := newTLSServer(t, &gotls.Config{NextProtos: chromeALPN}, func(w http.ResponseWriter, r *http.Request) {}) outer := newTLSServer(t, &gotls.Config{NextProtos: chromeALPN}, func(w http.ResponseWriter, r *http.Request) {})
if resp, err := newServiceClient(context.Background(), new(sync.Map), outer.settings(ech), 5*time.Second, 8).Get(outer.URL); err == nil { if resp, err := newServiceClient(outer.settings(ech), 5*time.Second, 8).Get(outer.URL); err == nil {
resp.Body.Close() resp.Body.Close()
} }
if hello := outer.lastHello(); hello == nil { if hello := outer.lastHello(); hello == nil {
@@ -503,7 +462,7 @@ func TestServiceClientHungHandshake(t *testing.T) {
server.Listener = &muteFirst{Listener: server.Listener} server.Listener = &muteFirst{Listener: server.Listener}
server.StartTLS() server.StartTLS()
t.Cleanup(server.Close) t.Cleanup(server.Close)
client := newServiceClient(context.Background(), new(sync.Map), server.settings(&tls.Config{Fingerprint: fingerprint}), stall, 8) client := newServiceClient(server.settings(&tls.Config{Fingerprint: fingerprint}), stall, 8)
if _, err := client.Get(server.URL); err == nil { if _, err := client.Get(server.URL); err == nil {
t.Fatalf("fingerprint %q: a request got through a handshake that hung", fingerprint) t.Fatalf("fingerprint %q: a request got through a handshake that hung", fingerprint)
@@ -529,7 +488,7 @@ func TestServiceClientClosesFailedHandshake(t *testing.T) {
for _, fingerprint := range []string{"", "unsafe"} { for _, fingerprint := range []string{"", "unsafe"} {
settings := &internet.MemoryStreamConfig{ProtocolName: protocolName, SecuritySettings: &tls.Config{Fingerprint: fingerprint}} settings := &internet.MemoryStreamConfig{ProtocolName: protocolName, SecuritySettings: &tls.Config{Fingerprint: fingerprint}}
go newServiceClient(context.Background(), new(sync.Map), settings, 5*time.Second, 8).Get("https://" + ln.Addr().String()) go newServiceClient(settings, 5*time.Second, 8).Get("https://" + ln.Addr().String())
conn, err := ln.Accept() conn, err := ln.Accept()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -544,446 +503,6 @@ func TestServiceClientClosesFailedHandshake(t *testing.T) {
} }
} }
// afterFuncs counts the functions that context.AfterFunc registers on it and
// that are not stopped yet
type afterFuncs struct {
context.Context
live atomic.Int32
}
// Value hides the cancelCtx below, which context.AfterFunc would use instead
func (c *afterFuncs) Value(any) any { return nil }
func (c *afterFuncs) AfterFunc(func()) func() bool {
c.live.Add(1)
return func() bool {
c.live.Add(-1)
return true
}
}
// a connection that ends on its own, here after Connection: close, leaves
// nothing on the context that closes the connections of the client
func TestServiceClientForgetsClosedConnections(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Connection", "close")
}))
defer server.Close()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
connCtx := &afterFuncs{Context: ctx}
client := newServiceClient(connCtx, new(sync.Map), &internet.MemoryStreamConfig{}, 5*time.Second, 8)
for range 4 {
resp, err := client.Get(server.URL)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
}
waitFor(t, "the closed connections to leave the context", func() bool { return connCtx.live.Load() == 0 })
}
type roundTrip func(*http.Request) (*http.Response, error) type roundTrip func(*http.Request) (*http.Response, error)
func (f roundTrip) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) } func (f roundTrip) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) }
// "hKeepAlivePeriod" of XMUX sets when an idle h2 connection pings, -1 never,
// and h3 keeps alive only when it is set
func TestServiceClientKeepAlive(t *testing.T) {
for period, want := range map[int64]time.Duration{0: xnet.ChromeH2KeepAlivePeriod, 20: 20 * time.Second, -1: 0} {
settings := &internet.MemoryStreamConfig{ProtocolSettings: &Config{Xmux: &splithttp.XmuxConfig{HKeepAlivePeriod: period}}}
client := newServiceClient(context.Background(), new(sync.Map), settings, 5*time.Second, 8)
h2 := client.Transport.(*serviceTransport).secure.(*secureTransport).h2.(*http.Transport)
if got := h2.HTTP2.SendPingTimeout; got != want {
t.Errorf("hKeepAlivePeriod %d pings after %v, want %v", period, got, want)
}
}
for period, want := range map[int64]time.Duration{0: 0, 20: 20 * time.Second, -1: 0} {
settings := &internet.MemoryStreamConfig{
ProtocolSettings: &Config{Xmux: &splithttp.XmuxConfig{HKeepAlivePeriod: period}},
SecuritySettings: &tls.Config{NextProtocol: []string{"h3"}},
}
client := newServiceClient(context.Background(), new(sync.Map), settings, 5*time.Second, 8)
if got := client.Transport.(*serviceTransport).secure.(*http3.Transport).QUICConfig.KeepAlivePeriod; got != want {
t.Errorf("hKeepAlivePeriod %d keeps h3 alive every %v, want %v", period, got, want)
}
}
}
// h3 is used when "alpn" asks for it alone, and only then
func TestServiceClientH3Selection(t *testing.T) {
for _, c := range []struct {
alpn []string
h3 bool
}{
{nil, false},
{[]string{"h2"}, false},
{[]string{"http/1.1"}, false},
{[]string{"h3", "h2"}, false},
{[]string{"h2", "h3"}, false},
{[]string{"h3"}, true},
} {
client := newServiceClient(context.Background(), new(sync.Map), &internet.MemoryStreamConfig{SecuritySettings: &tls.Config{NextProtocol: c.alpn}}, 5*time.Second, 8)
if _, h3 := client.Transport.(*serviceTransport).secure.(*http3.Transport); h3 != c.h3 {
t.Errorf("alpn %v: h3 %v, want %v", c.alpn, h3, c.h3)
}
}
if _, h3 := newServiceClient(context.Background(), new(sync.Map), &internet.MemoryStreamConfig{}, 5*time.Second, 8).Transport.(*serviceTransport).secure.(*http3.Transport); h3 {
t.Error("h3 without tlsSettings")
}
}
// h3Server is an http3 server that keeps the SNI of each connection, and counts
// its connections and those closed
type h3Server struct {
port xnet.Port
hash, caHash [32]byte
mu sync.Mutex
snis []string
conns atomic.Int32
closed atomic.Int32
}
func newH3Server(t *testing.T, handler http.Handler) *h3Server {
t.Helper()
// ECDSA, as ChromeParrot offers no Ed25519, from a CA of its own
ca, caHash := cert.MustGenerate(nil, cert.Authority(true), cert.KeyUsage(x509.KeyUsageCertSign))
certificate, hash := cert.MustGenerate(ca, cert.DNSNames("www.googleapis.com", "oauth2.googleapis.com"))
key, err := x509.ParsePKCS8PrivateKey(certificate.PrivateKey)
if err != nil {
t.Fatal(err)
}
conn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)})
if err != nil {
t.Fatal(err)
}
s := &h3Server{port: xnet.Port(conn.LocalAddr().(*net.UDPAddr).Port), hash: hash, caHash: caHash}
server := &http3.Server{
Handler: handler,
TLSConfig: http3.ConfigureTLSConfig(&gotls.Config{
Certificates: []gotls.Certificate{{Certificate: [][]byte{certificate.Certificate, ca.Certificate}, PrivateKey: key}},
GetConfigForClient: func(hello *gotls.ClientHelloInfo) (*gotls.Config, error) {
s.mu.Lock()
s.snis = append(s.snis, hello.ServerName)
s.mu.Unlock()
return nil, nil
},
}),
ConnContext: func(ctx context.Context, c *quic.Conn) context.Context {
s.conns.Add(1)
context.AfterFunc(c.Context(), func() { s.closed.Add(1) })
return ctx
},
}
go server.Serve(conn)
t.Cleanup(func() {
server.Close()
conn.Close()
})
return s
}
// settings sends every host to the server over h3, through a destination built
// as TCP the way conf builds it, and pins the certificate of the server in config
func (s *h3Server) settings(config *tls.Config) *internet.MemoryStreamConfig {
config.NextProtocol = []string{"h3"}
config.PinnedPeerCertSha256 = [][]byte{s.hash[:]}
return &internet.MemoryStreamConfig{
ProtocolName: protocolName,
Destination: &xnet.Destination{Address: xnet.LocalHostIP, Port: s.port, Network: xnet.Network_TCP},
SecuritySettings: config,
}
}
func (s *h3Server) seenSNIs() []string {
s.mu.Lock()
defer s.mu.Unlock()
return slices.Sorted(slices.Values(s.snis))
}
// one client serves two hosts, and each gets its own SNI unless serverName
// says otherwise, which is also the name a pinned CA checks the certificate for
func TestServiceClientH3SNI(t *testing.T) {
for _, c := range []struct {
serverName string
pinCA bool
want []string
}{
{"", false, []string{"oauth2.googleapis.com", "www.googleapis.com"}},
{"www.google.com", false, []string{"www.google.com", "www.google.com"}},
{"", true, []string{"oauth2.googleapis.com", "www.googleapis.com"}},
} {
server := newH3Server(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
settings := server.settings(&tls.Config{ServerName: c.serverName})
if c.pinCA {
settings.SecuritySettings.(*tls.Config).PinnedPeerCertSha256 = [][]byte{server.caHash[:]}
}
client := newServiceClient(context.Background(), new(sync.Map), settings, 5*time.Second, 8)
for _, url := range []string{"https://www.googleapis.com/drive/v3/files", "https://oauth2.googleapis.com/token"} {
resp, err := client.Get(url)
if err != nil {
t.Fatalf("serverName %q, pinned CA %v, %s: %v", c.serverName, c.pinCA, url, err)
}
resp.Body.Close()
if resp.Proto != "HTTP/3.0" {
t.Errorf("serverName %q, %s: went over %s", c.serverName, url, resp.Proto)
}
}
if snis := server.seenSNIs(); !slices.Equal(snis, c.want) {
t.Errorf("serverName %q: SNIs %v, want %v", c.serverName, snis, c.want)
}
}
}
// a request that stands still ends over h3 too, and says so
func TestServiceClientStallOverH3(t *testing.T) {
const stall = 200 * time.Millisecond
server := newH3Server(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/stuck" {
w.Write([]byte("x"))
w.(http.Flusher).Flush()
}
time.Sleep(5 * stall)
}))
client := newServiceClient(context.Background(), new(sync.Map), server.settings(&tls.Config{}), stall, 8)
for _, path := range []string{"/stuck", "/silent"} {
start := time.Now()
resp, err := client.Get("https://www.googleapis.com" + path)
if err == nil {
if resp.Proto != "HTTP/3.0" {
t.Fatalf("%s went over %s", path, resp.Proto)
}
_, err = io.ReadAll(resp.Body)
resp.Body.Close()
}
if !errors.Is(err, errStalled) {
t.Errorf("%s gave %v, want %v", path, err, errStalled)
}
if took := time.Since(start); took > 4*stall {
t.Errorf("%s took %v to give up", path, took)
}
}
}
// brutal and force-brutal, which are for Hysteria servers, give an error and
// no panic in a goroutine of http3, where XHTTP and DoH panic on brutal
func TestServiceClientH3Congestion(t *testing.T) {
server := newH3Server(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
for _, cc := range []string{"", "bbr", "reno", "brutal", "force-brutal"} {
settings := server.settings(&tls.Config{})
settings.QuicParams = &internet.QuicParams{Congestion: cc, BrutalUp: 1 << 20}
resp, err := newServiceClient(context.Background(), new(sync.Map), settings, 5*time.Second, 8).Get("https://www.googleapis.com/")
if err == nil {
resp.Body.Close()
}
if brutal := strings.HasSuffix(cc, "brutal"); brutal != (err != nil) || brutal && !strings.Contains(err.Error(), cc) {
t.Errorf("congestion %q gave %v", cc, err)
}
}
}
// recordConn keeps the datagrams sent through it
type recordConn struct {
net.PacketConn
mu sync.Mutex
sent [][]byte
closed atomic.Bool
}
func (c *recordConn) WriteTo(p []byte, addr net.Addr) (int, error) {
c.mu.Lock()
c.sent = append(c.sent, bytes.Clone(p))
c.mu.Unlock()
return c.PacketConn.WriteTo(p, addr)
}
func (c *recordConn) Close() error {
c.closed.Store(true)
return c.PacketConn.Close()
}
// recorder dials UDP through recordConns and keeps them
type recorder struct {
mu sync.Mutex
conns []*recordConn
}
func (r *recorder) dialUDP(ctx context.Context, dest xnet.Destination) (net.PacketConn, net.Addr, error) {
conn, err := internet.DialSystem(ctx, dest, nil)
if err != nil {
return nil, nil, err
}
rec := &recordConn{PacketConn: conn.(*xnet.PacketConnWrapper).PacketConn}
r.mu.Lock()
r.conns = append(r.conns, rec)
r.mu.Unlock()
return rec, conn.RemoteAddr(), nil
}
// a connection ends with its socket once it is idle for its idle timeout, as
// keepalive is off unless asked for, and so does one whose handshake failed,
// and neither leaves anything on the context that closes the connections
func TestServiceClientH3Closes(t *testing.T) {
server := newH3Server(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
for _, pin := range [][]byte{server.hash[:], make([]byte, 32)} {
rec := new(recorder)
settings := server.settings(&tls.Config{})
settings.SecuritySettings.(*tls.Config).PinnedPeerCertSha256 = [][]byte{pin}
// ChromeParrot would keep the idle timeout at 30 s
settings.QuicParams = &internet.QuicParams{MaxIdleTimeout: 1, DisableChromeParrot: true}
settings.FinalMask = finalmask.NewFinalMask(nil, nil, nil, nil, rec.dialUDP, nil)
connCtx := &afterFuncs{Context: ctx}
resp, err := newServiceClient(connCtx, new(sync.Map), settings, 5*time.Second, 8).Get("https://www.googleapis.com/")
if err == nil {
resp.Body.Close()
}
if bytes.Equal(pin, server.hash[:]) != (err == nil) {
t.Fatalf("pin %x: %v", pin[:4], err)
}
closed := func() bool {
rec.mu.Lock()
defer rec.mu.Unlock()
return len(rec.conns) == 1 && rec.conns[0].closed.Load()
}
for deadline := time.Now().Add(5 * time.Second); !closed() && time.Now().Before(deadline); {
time.Sleep(10 * time.Millisecond)
}
if !closed() {
t.Errorf("pin %x: the socket was left open", pin[:4])
}
waitFor(t, "the closed connection to leave the context", func() bool { return connCtx.live.Load() == 0 })
}
}
// cncDialer gives UDP as a stream conn, the way dialerProxy does
type cncDialer struct{}
func (cncDialer) Dial(ctx context.Context, src xnet.Address, dest xnet.Destination, sockopt *internet.SocketConfig) (net.Conn, error) {
conn, err := net.Dial("udp", dest.NetAddr())
if err != nil {
return nil, err
}
return cnc.NewConnection(cnc.ConnectionInput(conn), cnc.ConnectionOutput(conn), cnc.ConnectionOnClose(conn)), nil
}
func (cncDialer) DestIpAddress() net.IP { return nil }
// h3 goes over the stream conn of a dialerProxy too
func TestServiceClientH3DialerProxy(t *testing.T) {
internet.UseAlternativeSystemDialer(cncDialer{})
defer internet.UseAlternativeSystemDialer(nil)
server := newH3Server(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
resp, err := newServiceClient(context.Background(), new(sync.Map), server.settings(&tls.Config{}), 5*time.Second, 8).Get("https://www.googleapis.com/")
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
if resp.Proto != "HTTP/3.0" {
t.Errorf("went over %s", resp.Proto)
}
}
// xdrive moves data over h3 to a fake Drive, with the noise of a UDP mask sent
// ahead of the Initial of each connection
func TestDriveOverH3(t *testing.T) {
drive := newFakeDrive(t)
server := newH3Server(t, drive.server.Config.Handler)
driveTokenURL = "https://oauth2.googleapis.com/token"
driveFilesURL = "https://www.googleapis.com/files"
driveUploadURL = "https://www.googleapis.com/upload"
rec := new(recorder)
settings := server.settings(&tls.Config{})
settings.ProtocolSettings = driveSettings().ProtocolSettings
settings.ProtocolSettings.(*Config).Xmux = &splithttp.XmuxConfig{MaxConnections: &splithttp.RangeConfig{From: 4, To: 4}}
masks := []finalmask.UDPMask{&noise.Config{Items: []*noise.Item{{Packet: []byte("noise")}}}}
settings.FinalMask = finalmask.NewFinalMask(nil, masks, nil, nil, rec.dialUDP, nil)
client, srv, cleanup := pairWith(t, settings)
defer cleanup()
if _, err := client.Write([]byte("ping")); err != nil {
t.Fatalf("client write: %v", err)
}
expectRead(t, srv, "ping")
if _, err := srv.Write([]byte("pong")); err != nil {
t.Fatalf("server write: %v", err)
}
expectRead(t, client, "pong")
payload := make([]byte, 300000)
for i := range payload {
payload[i] = byte(i % 251)
}
go client.Write(payload)
if err := srv.SetReadDeadline(time.Now().Add(testPatience)); err != nil {
t.Fatalf("SetReadDeadline: %v", err)
}
got := make([]byte, len(payload))
if _, err := io.ReadFull(srv, got); err != nil {
t.Fatalf("ReadFull: %v", err)
}
if !bytes.Equal(got, payload) {
t.Fatal("payload mismatch")
}
rec.mu.Lock()
defer rec.mu.Unlock()
// one for the listener to www.googleapis.com, one for the token to
// oauth2.googleapis.com and one of XMUX for each end of the session
if len(rec.conns) != 4 {
t.Errorf("%d connections went through the mask, want 4", len(rec.conns))
}
for i, conn := range rec.conns {
conn.mu.Lock()
sent := conn.sent
conn.mu.Unlock()
if len(sent) < 2 {
t.Errorf("connection %d sent %d datagrams", i, len(sent))
continue
}
// the noise, then a QUIC v1 Initial of 1250 bytes with no source
// connection ID, as ChromeParrot sends it
first, initial := sent[0], sent[1]
if string(first) != "noise" || len(initial) != 1250 || initial[0]&0xf0 != 0xc0 || !bytes.Equal(initial[1:5], []byte{0, 0, 0, 1}) || initial[6+int(initial[5])] != 0 {
t.Errorf("connection %d began with %q and %d bytes %x", i, first, len(initial), initial[:min(len(initial), 24)])
}
}
}
// over h3 a connection of XMUX is a QUIC connection, which closes once XMUX
// drops it, so that the server hears of it
func TestDriveXmuxOverH3(t *testing.T) {
drive := newFakeDrive(t)
server := newH3Server(t, drive.server.Config.Handler)
driveTokenURL = "https://www.googleapis.com/token"
driveFilesURL = "https://www.googleapis.com/files"
driveUploadURL = "https://www.googleapis.com/upload"
settings := server.settings(&tls.Config{})
settings.ProtocolSettings = driveSettings().ProtocolSettings
settings.ProtocolSettings.(*Config).Xmux = &splithttp.XmuxConfig{HMaxRequestTimes: &splithttp.RangeConfig{From: 3, To: 3}}
storage, err := newDriveStorage(settings, settings.ProtocolSettings.(*Config))
if err != nil {
t.Fatalf("newDriveStorage: %v", err)
}
session := sessionStorage(storage)
defer session.Close()
for _, name := range []string{"a", "b", "c", "d"} {
if err := session.Put(context.Background(), "streams/abc/c2s/"+name, []byte("x")); err != nil {
t.Fatalf("Put: %v", err)
}
}
// one for the token, the first of XMUX for 3 requests and one more
if n := server.conns.Load(); n != 3 {
t.Errorf("the token and 4 requests with 3 to a connection took %d connections", n)
}
waitFor(t, "the first connection of XMUX to close", func() bool { return server.closed.Load() == 1 })
}
+9 -106
View File
@@ -7,7 +7,6 @@
package xdrive package xdrive
import ( import (
splithttp "github.com/xtls/xray-core/transport/internet/splithttp"
protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl" protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect" reflect "reflect"
@@ -36,8 +35,6 @@ type Config struct {
EagerWindowMs uint32 `protobuf:"varint,10,opt,name=eager_window_ms,json=eagerWindowMs,proto3" json:"eager_window_ms,omitempty"` EagerWindowMs uint32 `protobuf:"varint,10,opt,name=eager_window_ms,json=eagerWindowMs,proto3" json:"eager_window_ms,omitempty"`
HoleTimeoutMs uint32 `protobuf:"varint,11,opt,name=hole_timeout_ms,json=holeTimeoutMs,proto3" json:"hole_timeout_ms,omitempty"` HoleTimeoutMs uint32 `protobuf:"varint,11,opt,name=hole_timeout_ms,json=holeTimeoutMs,proto3" json:"hole_timeout_ms,omitempty"`
Template string `protobuf:"bytes,12,opt,name=template,proto3" json:"template,omitempty"` Template string `protobuf:"bytes,12,opt,name=template,proto3" json:"template,omitempty"`
Xmux *splithttp.XmuxConfig `protobuf:"bytes,13,opt,name=xmux,proto3" json:"xmux,omitempty"`
S3 *S3Config `protobuf:"bytes,14,opt,name=s3,proto3" json:"s3,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@@ -156,93 +153,11 @@ func (x *Config) GetTemplate() string {
return "" return ""
} }
func (x *Config) GetXmux() *splithttp.XmuxConfig {
if x != nil {
return x.Xmux
}
return nil
}
func (x *Config) GetS3() *S3Config {
if x != nil {
return x.S3
}
return nil
}
type S3Config struct {
state protoimpl.MessageState `protogen:"open.v1"`
Endpoint string `protobuf:"bytes,1,opt,name=endpoint,proto3" json:"endpoint,omitempty"`
Region string `protobuf:"bytes,2,opt,name=region,proto3" json:"region,omitempty"`
Bucket string `protobuf:"bytes,3,opt,name=bucket,proto3" json:"bucket,omitempty"`
PathStyle bool `protobuf:"varint,4,opt,name=path_style,json=pathStyle,proto3" json:"path_style,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *S3Config) Reset() {
*x = S3Config{}
mi := &file_transport_internet_xdrive_config_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *S3Config) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*S3Config) ProtoMessage() {}
func (x *S3Config) ProtoReflect() protoreflect.Message {
mi := &file_transport_internet_xdrive_config_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use S3Config.ProtoReflect.Descriptor instead.
func (*S3Config) Descriptor() ([]byte, []int) {
return file_transport_internet_xdrive_config_proto_rawDescGZIP(), []int{1}
}
func (x *S3Config) GetEndpoint() string {
if x != nil {
return x.Endpoint
}
return ""
}
func (x *S3Config) GetRegion() string {
if x != nil {
return x.Region
}
return ""
}
func (x *S3Config) GetBucket() string {
if x != nil {
return x.Bucket
}
return ""
}
func (x *S3Config) GetPathStyle() bool {
if x != nil {
return x.PathStyle
}
return false
}
var File_transport_internet_xdrive_config_proto protoreflect.FileDescriptor var File_transport_internet_xdrive_config_proto protoreflect.FileDescriptor
const file_transport_internet_xdrive_config_proto_rawDesc = "" + const file_transport_internet_xdrive_config_proto_rawDesc = "" +
"\n" + "\n" +
"&transport/internet/xdrive/config.proto\x12\x1exray.transport.internet.xdrive\x1a)transport/internet/splithttp/config.proto\"\xc8\x04\n" + "&transport/internet/xdrive/config.proto\x12\x1exray.transport.internet.xdrive\"\xcb\x03\n" +
"\x06Config\x12#\n" + "\x06Config\x12#\n" +
"\rremote_folder\x18\x01 \x01(\tR\fremoteFolder\x12\x18\n" + "\rremote_folder\x18\x01 \x01(\tR\fremoteFolder\x12\x18\n" +
"\aservice\x18\x02 \x01(\tR\aservice\x12\x18\n" + "\aservice\x18\x02 \x01(\tR\aservice\x12\x18\n" +
@@ -256,15 +171,7 @@ const file_transport_internet_xdrive_config_proto_rawDesc = "" +
"\x0feager_window_ms\x18\n" + "\x0feager_window_ms\x18\n" +
" \x01(\rR\reagerWindowMs\x12&\n" + " \x01(\rR\reagerWindowMs\x12&\n" +
"\x0fhole_timeout_ms\x18\v \x01(\rR\rholeTimeoutMs\x12\x1a\n" + "\x0fhole_timeout_ms\x18\v \x01(\rR\rholeTimeoutMs\x12\x1a\n" +
"\btemplate\x18\f \x01(\tR\btemplate\x12A\n" + "\btemplate\x18\f \x01(\tR\btemplateB5Z3github.com/xtls/xray-core/transport/internet/xdriveb\x06proto3"
"\x04xmux\x18\r \x01(\v2-.xray.transport.internet.splithttp.XmuxConfigR\x04xmux\x128\n" +
"\x02s3\x18\x0e \x01(\v2(.xray.transport.internet.xdrive.S3ConfigR\x02s3\"u\n" +
"\bS3Config\x12\x1a\n" +
"\bendpoint\x18\x01 \x01(\tR\bendpoint\x12\x16\n" +
"\x06region\x18\x02 \x01(\tR\x06region\x12\x16\n" +
"\x06bucket\x18\x03 \x01(\tR\x06bucket\x12\x1d\n" +
"\n" +
"path_style\x18\x04 \x01(\bR\tpathStyleB5Z3github.com/xtls/xray-core/transport/internet/xdriveb\x06proto3"
var ( var (
file_transport_internet_xdrive_config_proto_rawDescOnce sync.Once file_transport_internet_xdrive_config_proto_rawDescOnce sync.Once
@@ -278,20 +185,16 @@ func file_transport_internet_xdrive_config_proto_rawDescGZIP() []byte {
return file_transport_internet_xdrive_config_proto_rawDescData return file_transport_internet_xdrive_config_proto_rawDescData
} }
var file_transport_internet_xdrive_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2) var file_transport_internet_xdrive_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_transport_internet_xdrive_config_proto_goTypes = []any{ var file_transport_internet_xdrive_config_proto_goTypes = []any{
(*Config)(nil), // 0: xray.transport.internet.xdrive.Config (*Config)(nil), // 0: xray.transport.internet.xdrive.Config
(*S3Config)(nil), // 1: xray.transport.internet.xdrive.S3Config
(*splithttp.XmuxConfig)(nil), // 2: xray.transport.internet.splithttp.XmuxConfig
} }
var file_transport_internet_xdrive_config_proto_depIdxs = []int32{ var file_transport_internet_xdrive_config_proto_depIdxs = []int32{
2, // 0: xray.transport.internet.xdrive.Config.xmux:type_name -> xray.transport.internet.splithttp.XmuxConfig 0, // [0:0] is the sub-list for method output_type
1, // 1: xray.transport.internet.xdrive.Config.s3:type_name -> xray.transport.internet.xdrive.S3Config 0, // [0:0] is the sub-list for method input_type
2, // [2:2] is the sub-list for method output_type 0, // [0:0] is the sub-list for extension type_name
2, // [2:2] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension extendee
2, // [2:2] is the sub-list for extension type_name 0, // [0:0] is the sub-list for field type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
} }
func init() { file_transport_internet_xdrive_config_proto_init() } func init() { file_transport_internet_xdrive_config_proto_init() }
@@ -305,7 +208,7 @@ func file_transport_internet_xdrive_config_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_xdrive_config_proto_rawDesc), len(file_transport_internet_xdrive_config_proto_rawDesc)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_xdrive_config_proto_rawDesc), len(file_transport_internet_xdrive_config_proto_rawDesc)),
NumEnums: 0, NumEnums: 0,
NumMessages: 2, NumMessages: 1,
NumExtensions: 0, NumExtensions: 0,
NumServices: 0, NumServices: 0,
}, },
-11
View File
@@ -3,8 +3,6 @@ syntax = "proto3";
package xray.transport.internet.xdrive; package xray.transport.internet.xdrive;
option go_package = "github.com/xtls/xray-core/transport/internet/xdrive"; option go_package = "github.com/xtls/xray-core/transport/internet/xdrive";
import "transport/internet/splithttp/config.proto";
message Config { message Config {
string remote_folder = 1; string remote_folder = 1;
string service = 2; string service = 2;
@@ -18,13 +16,4 @@ message Config {
uint32 eager_window_ms = 10; uint32 eager_window_ms = 10;
uint32 hole_timeout_ms = 11; uint32 hole_timeout_ms = 11;
string template = 12; string template = 12;
xray.transport.internet.splithttp.XmuxConfig xmux = 13;
S3Config s3 = 14;
}
message S3Config {
string endpoint = 1;
string region = 2;
string bucket = 3;
bool path_style = 4;
} }
+4 -24
View File
@@ -2,7 +2,6 @@ package xdrive
import ( import (
"context" "context"
"io"
"os" "os"
"sync" "sync"
"time" "time"
@@ -12,12 +11,10 @@ import (
var placeholderAddr = &net.TCPAddr{IP: net.IP{127, 0, 0, 1}, Port: 0} var placeholderAddr = &net.TCPAddr{IP: net.IP{127, 0, 0, 1}, Port: 0}
// Conn is a session in segments, walWriter and walReader, or in slots,
// slotWriter and slotReader
type Conn struct { type Conn struct {
cancel context.CancelFunc cancel context.CancelFunc
writer io.WriteCloser writer *walWriter
reader chunkReader reader *walReader
onClose func() onClose func()
readBuf []byte readBuf []byte
@@ -30,13 +27,6 @@ type Conn struct {
closeErr error closeErr error
} }
type chunkReader interface {
// chunks is closed when there are no more, then Err says why
chunks() <-chan []byte
Err() error
Wake()
}
func newConn(ctx context.Context, storage Storage, writePrefix, readPrefix string, p params, onClose func()) *Conn { func newConn(ctx context.Context, storage Storage, writePrefix, readPrefix string, p params, onClose func()) *Conn {
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
return &Conn{ return &Conn{
@@ -47,16 +37,6 @@ func newConn(ctx context.Context, storage Storage, writePrefix, readPrefix strin
} }
} }
func newSlotConn(ctx context.Context, store slotStore, write, read slotTable, p params, onClose func()) *Conn {
ctx, cancel := context.WithCancel(ctx)
return &Conn{
cancel: cancel,
writer: newSlotWriter(ctx, store, write, p),
reader: newSlotReader(ctx, store, read, p),
onClose: onClose,
}
}
func (c *Conn) Read(b []byte) (int, error) { func (c *Conn) Read(b []byte) (int, error) {
if len(c.readBuf) == 0 { if len(c.readBuf) == 0 {
data, err := c.receive() data, err := c.receive()
@@ -73,7 +53,7 @@ func (c *Conn) Read(b []byte) (int, error) {
func (c *Conn) receive() ([]byte, error) { func (c *Conn) receive() ([]byte, error) {
deadline := c.getDeadline(true) deadline := c.getDeadline(true)
if deadline.IsZero() { if deadline.IsZero() {
data, ok := <-c.reader.chunks() data, ok := <-c.reader.ch
if !ok { if !ok {
return nil, c.reader.Err() return nil, c.reader.Err()
} }
@@ -87,7 +67,7 @@ func (c *Conn) receive() ([]byte, error) {
defer timer.Stop() defer timer.Stop()
select { select {
case data, ok := <-c.reader.chunks(): case data, ok := <-c.reader.ch:
if !ok { if !ok {
return nil, c.reader.Err() return nil, c.reader.Err()
} }
+51 -142
View File
@@ -39,21 +39,11 @@ var (
) )
type driveStorage struct { type driveStorage struct {
*driveShared
// a session has a client and inflight of its own, see session
client *http.Client
inflight chan struct{}
release func()
}
// driveShared is what the sessions of a storage have in common
type driveShared struct {
conns *connPool
folder string folder string
clientID string clientID string
clientSecret string clientSecret string
refreshToken string refreshToken string
tokenClient *http.Client client *http.Client
tokenURL string tokenURL string
filesURL string filesURL string
uploadURL string uploadURL string
@@ -63,6 +53,8 @@ type driveShared struct {
token string token string
tokenExpiry time.Time tokenExpiry time.Time
inflight chan struct{}
idMu sync.Mutex idMu sync.Mutex
ids map[string]string ids map[string]string
} }
@@ -80,41 +72,21 @@ func newDriveStorage(streamSettings *internet.MemoryStreamConfig, config *Config
} }
} }
h1Hosts := new(sync.Map)
client := newServiceClient(context.Background(), h1Hosts, streamSettings, driveTimeout, driveMaxInflight)
return &driveStorage{ return &driveStorage{
driveShared: &driveShared{
conns: newConnPool(config.Xmux, func(connCtx context.Context) *http.Client {
return newServiceClient(connCtx, h1Hosts, streamSettings, driveTimeout, driveMaxInflight)
}),
folder: config.RemoteFolder, folder: config.RemoteFolder,
clientID: config.Secrets[0], clientID: config.Secrets[0],
clientSecret: config.Secrets[1], clientSecret: config.Secrets[1],
refreshToken: config.Secrets[2], refreshToken: config.Secrets[2],
tokenClient: client, client: newServiceClient(streamSettings, driveTimeout, driveMaxInflight),
tokenURL: driveTokenURL, tokenURL: driveTokenURL,
filesURL: driveFilesURL, filesURL: driveFilesURL,
uploadURL: driveUploadURL, uploadURL: driveUploadURL,
backoff: driveInitialBackoff, backoff: driveInitialBackoff,
inflight: make(chan struct{}, driveMaxInflight),
ids: make(map[string]string), ids: make(map[string]string),
},
client: client,
inflight: make(chan struct{}, driveMaxInflight),
}, nil }, nil
} }
// session returns a storage for one session, over the connections of XMUX
// until Close, with the token and ids of s
func (s *driveStorage) session() Storage {
transport := &sessionTransport{pool: s.conns}
return &driveStorage{
driveShared: s.driveShared,
client: &http.Client{Transport: transport},
inflight: make(chan struct{}, driveMaxInflight),
release: transport.Close,
}
}
func flatten(name string) string { func flatten(name string) string {
return strings.ReplaceAll(name, "/", flatSeparator) return strings.ReplaceAll(name, "/", flatSeparator)
} }
@@ -143,7 +115,7 @@ func (s *driveStorage) accessToken(ctx context.Context) (string, error) {
} }
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := s.tokenClient.Do(req) resp, err := s.client.Do(req)
if err != nil { if err != nil {
return "", errors.New("failed to refresh the access token").Base(err) return "", errors.New("failed to refresh the access token").Base(err)
} }
@@ -401,30 +373,24 @@ func (s *driveStorage) Put(ctx context.Context, name string, data []byte) error
} }
func (s *driveStorage) putInline(ctx context.Context, name string, data []byte) error { func (s *driveStorage) putInline(ctx context.Context, name string, data []byte) error {
_, err := s.create(ctx, name, base64.StdEncoding.EncodeToString(data))
return err
}
// create makes an object with description and no body and returns its id
func (s *driveStorage) create(ctx context.Context, name, description string) (string, error) {
flat := flatten(name) flat := flatten(name)
body, err := json.Marshal(map[string]interface{}{ body, err := json.Marshal(map[string]interface{}{
"name": flat, "name": flat,
"parents": []string{s.folder}, "parents": []string{s.folder},
"description": description, "description": base64.StdEncoding.EncodeToString(data),
}) })
if err != nil { if err != nil {
return "", errors.New("failed to build the inline metadata").Base(err) return errors.New("failed to build the inline metadata").Base(err)
} }
status, payload, err := s.do(ctx, http.MethodPost, s.filesURL+"?fields=id", status, payload, err := s.do(ctx, http.MethodPost, s.filesURL+"?fields=id",
"application/json; charset=UTF-8", body) "application/json; charset=UTF-8", body)
if err != nil { if err != nil {
return "", err return err
} }
if status != http.StatusOK { if status != http.StatusOK {
return "", errors.New("Drive rejected the inline upload of ", name, return errors.New("Drive rejected the inline upload of ", name,
" with ", status, ": ", string(payload)) " with ", status, ": ", string(payload))
} }
@@ -432,23 +398,12 @@ func (s *driveStorage) create(ctx context.Context, name, description string) (st
ID string `json:"id"` ID string `json:"id"`
} }
if err := json.Unmarshal(payload, &parsed); err != nil { if err := json.Unmarshal(payload, &parsed); err != nil {
return "", errors.New("failed to parse the Drive upload response").Base(err) return errors.New("failed to parse the Drive upload response").Base(err)
} }
if parsed.ID != "" { if parsed.ID != "" {
s.rememberID(flat, parsed.ID) s.rememberID(flat, parsed.ID)
} }
return parsed.ID, nil return nil
}
// multipartBody is a body of an upload with metadata and data
func multipartBody(metadata, data []byte) []byte {
var body bytes.Buffer
fmt.Fprintf(&body, "--%s\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n", driveBoundary)
body.Write(metadata)
fmt.Fprintf(&body, "\r\n--%s\r\nContent-Type: application/octet-stream\r\n\r\n", driveBoundary)
body.Write(data)
fmt.Fprintf(&body, "\r\n--%s--\r\n", driveBoundary)
return body.Bytes()
} }
func (s *driveStorage) putMedia(ctx context.Context, name string, data []byte) error { func (s *driveStorage) putMedia(ctx context.Context, name string, data []byte) error {
@@ -462,8 +417,15 @@ func (s *driveStorage) putMedia(ctx context.Context, name string, data []byte) e
return errors.New("failed to build the upload metadata").Base(err) return errors.New("failed to build the upload metadata").Base(err)
} }
var body bytes.Buffer
fmt.Fprintf(&body, "--%s\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n", driveBoundary)
body.Write(metadata)
fmt.Fprintf(&body, "\r\n--%s\r\nContent-Type: application/octet-stream\r\n\r\n", driveBoundary)
body.Write(data)
fmt.Fprintf(&body, "\r\n--%s--\r\n", driveBoundary)
status, payload, err := s.do(ctx, http.MethodPost, s.uploadURL, status, payload, err := s.do(ctx, http.MethodPost, s.uploadURL,
"multipart/related; boundary="+driveBoundary, multipartBody(metadata, data)) "multipart/related; boundary="+driveBoundary, body.Bytes())
if err != nil { if err != nil {
return err return err
} }
@@ -490,22 +452,45 @@ func (s *driveStorage) Get(ctx context.Context, name string) ([]byte, error) {
return nil, err return nil, err
} }
payload, err := s.readBody(ctx, id) status, payload, err := s.do(ctx, http.MethodGet,
if err == errNotFound { s.filesURL+"/"+url.PathEscape(id)+"?alt=media&supportsAllDrives=true", "", nil)
s.forgetID(flat) if err != nil {
return nil, err
} }
if err != nil || len(payload) > 0 { switch status {
return payload, err case http.StatusOK:
if len(payload) > 0 {
return payload, nil
} }
return s.getInline(ctx, id) return s.getInline(ctx, id)
case http.StatusNotFound:
s.forgetID(flat)
return nil, errNotFound
default:
return nil, errors.New("Drive download of ", name, " answered ", status, ": ", string(payload))
}
} }
func (s *driveStorage) getInline(ctx context.Context, id string) ([]byte, error) { func (s *driveStorage) getInline(ctx context.Context, id string) ([]byte, error) {
description, err := s.readDescription(ctx, id) status, payload, err := s.do(ctx, http.MethodGet,
if err != nil || description == "" { s.filesURL+"/"+url.PathEscape(id)+"?fields=description&supportsAllDrives=true", "", nil)
if err != nil {
return nil, err return nil, err
} }
data, err := base64.StdEncoding.DecodeString(description) if status != http.StatusOK {
return nil, errors.New("Drive answered ", status, " for inline data: ", string(payload))
}
var parsed struct {
Description string `json:"description"`
}
if err := json.Unmarshal(payload, &parsed); err != nil {
return nil, errors.New("failed to parse the inline data").Base(err)
}
if parsed.Description == "" {
return nil, nil
}
data, err := base64.StdEncoding.DecodeString(parsed.Description)
if err != nil { if err != nil {
return nil, errors.New("the inline data is not valid base64").Base(err) return nil, errors.New("the inline data is not valid base64").Base(err)
} }
@@ -587,81 +572,5 @@ func (s *driveStorage) List(ctx context.Context, prefix string) ([]Entry, error)
} }
func (s *driveStorage) Close() error { func (s *driveStorage) Close() error {
if s.release != nil {
s.release()
}
return nil return nil
} }
// makeSlot implements slotStore
func (s *driveStorage) makeSlot(ctx context.Context, name string) (string, error) {
id, err := s.create(ctx, name, "")
if err == nil && id == "" {
err = errors.New("Drive returned no id for ", name)
}
return id, err
}
// setSlot implements slotStore, a description and a body go in one request
func (s *driveStorage) setSlot(ctx context.Context, id, description string, body []byte) error {
metadata, err := json.Marshal(map[string]string{"description": description})
if err != nil {
return errors.New("failed to build the slot metadata").Base(err)
}
target := s.filesURL + "/" + url.PathEscape(id) + "?fields=id&supportsAllDrives=true"
contentType := "application/json; charset=UTF-8"
if body != nil {
upload, _, _ := strings.Cut(s.uploadURL, "?")
target = upload + "/" + url.PathEscape(id) + "?uploadType=multipart&fields=id&supportsAllDrives=true"
contentType = "multipart/related; boundary=" + driveBoundary
metadata = multipartBody(metadata, body)
}
status, payload, err := s.do(ctx, http.MethodPatch, target, contentType, metadata)
switch {
case err != nil:
return err
case status == http.StatusNotFound:
return errNotFound
case status != http.StatusOK:
return errors.New("Drive answered ", status, " for the update of ", id, ": ", string(payload))
}
return nil
}
// readDescription implements slotStore
func (s *driveStorage) readDescription(ctx context.Context, id string) (string, error) {
status, payload, err := s.do(ctx, http.MethodGet,
s.filesURL+"/"+url.PathEscape(id)+"?fields=description&supportsAllDrives=true", "", nil)
switch {
case err != nil:
return "", err
case status == http.StatusNotFound:
return "", errNotFound
case status != http.StatusOK:
return "", errors.New("Drive answered ", status, " for the description of ", id, ": ", string(payload))
}
var parsed struct {
Description string `json:"description"`
}
if err := json.Unmarshal(payload, &parsed); err != nil {
return "", errors.New("failed to parse the description of ", id).Base(err)
}
return parsed.Description, nil
}
// readBody implements slotStore
func (s *driveStorage) readBody(ctx context.Context, id string) ([]byte, error) {
status, payload, err := s.do(ctx, http.MethodGet,
s.filesURL+"/"+url.PathEscape(id)+"?alt=media&supportsAllDrives=true", "", nil)
switch {
case err != nil:
return nil, err
case status == http.StatusNotFound:
return nil, errNotFound
case status != http.StatusOK:
return nil, errors.New("Drive answered ", status, " for the body of ", id, ": ", string(payload))
}
return payload, nil
}
+5 -5
View File
@@ -57,7 +57,7 @@ func liveDriveConfig(t *testing.T) *Config {
config.Concurrency = uint32(envInt(t, "XRAY_XDRIVE_LIVE_CONCURRENCY")) config.Concurrency = uint32(envInt(t, "XRAY_XDRIVE_LIVE_CONCURRENCY"))
} }
storage, err := newDriveStorage(&internet.MemoryStreamConfig{}, config) storage, err := newDriveStorage(nil, config)
if err != nil { if err != nil {
t.Fatalf("newDriveStorage: %v", err) t.Fatalf("newDriveStorage: %v", err)
} }
@@ -83,7 +83,7 @@ func envInt(t *testing.T, name string) int {
func TestLiveDriveStorage(t *testing.T) { func TestLiveDriveStorage(t *testing.T) {
config := liveDriveConfig(t) config := liveDriveConfig(t)
storage, err := newDriveStorage(&internet.MemoryStreamConfig{}, config) storage, err := newDriveStorage(nil, config)
if err != nil { if err != nil {
t.Fatalf("newDriveStorage: %v", err) t.Fatalf("newDriveStorage: %v", err)
} }
@@ -194,7 +194,7 @@ func TestLiveDriveTransport(t *testing.T) {
func TestLiveDriveParallelPut(t *testing.T) { func TestLiveDriveParallelPut(t *testing.T) {
config := liveDriveConfig(t) config := liveDriveConfig(t)
storage, err := newDriveStorage(&internet.MemoryStreamConfig{}, config) storage, err := newDriveStorage(nil, config)
if err != nil { if err != nil {
t.Fatalf("newDriveStorage: %v", err) t.Fatalf("newDriveStorage: %v", err)
} }
@@ -266,7 +266,7 @@ func TestLiveDriveParallelPut(t *testing.T) {
func TestLiveDriveSegmentSweep(t *testing.T) { func TestLiveDriveSegmentSweep(t *testing.T) {
config := liveDriveConfig(t) config := liveDriveConfig(t)
storage, err := newDriveStorage(&internet.MemoryStreamConfig{}, config) storage, err := newDriveStorage(nil, config)
if err != nil { if err != nil {
t.Fatalf("newDriveStorage: %v", err) t.Fatalf("newDriveStorage: %v", err)
} }
@@ -303,7 +303,7 @@ func TestLiveDriveSegmentSweep(t *testing.T) {
func TestLiveDriveListLag(t *testing.T) { func TestLiveDriveListLag(t *testing.T) {
config := liveDriveConfig(t) config := liveDriveConfig(t)
storage, err := newDriveStorage(&internet.MemoryStreamConfig{}, config) storage, err := newDriveStorage(nil, config)
if err != nil { if err != nil {
t.Fatalf("newDriveStorage: %v", err) t.Fatalf("newDriveStorage: %v", err)
} }
+2 -45
View File
@@ -63,7 +63,6 @@ func newFakeDrive(t *testing.T) *fakeDrive {
} }
mux.HandleFunc("/token", record(drive.handleToken)) mux.HandleFunc("/token", record(drive.handleToken))
mux.HandleFunc("/upload", record(drive.handleUpload)) mux.HandleFunc("/upload", record(drive.handleUpload))
mux.HandleFunc("/upload/", record(drive.handleUpload))
mux.HandleFunc("/files", record(drive.handleFiles)) mux.HandleFunc("/files", record(drive.handleFiles))
mux.HandleFunc("/files/", record(drive.handleFile)) mux.HandleFunc("/files/", record(drive.handleFile))
drive.server = httptest.NewServer(mux) drive.server = httptest.NewServer(mux)
@@ -121,7 +120,6 @@ func (d *fakeDrive) handleUpload(w http.ResponseWriter, r *http.Request) {
} }
var metadata struct { var metadata struct {
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"`
} }
if err := json.NewDecoder(metaPart).Decode(&metadata); err != nil { if err := json.NewDecoder(metaPart).Decode(&metadata); err != nil {
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
@@ -133,22 +131,6 @@ func (d *fakeDrive) handleUpload(w http.ResponseWriter, r *http.Request) {
data, _ = io.ReadAll(dataPart) data, _ = io.ReadAll(dataPart)
} }
if id, ok := strings.CutPrefix(r.URL.Path, "/upload/"); ok {
// an update of the body and description of a file
d.mu.Lock()
file := d.files[id]
if file != nil {
file.data, file.description = data, metadata.Description
}
d.mu.Unlock()
if file == nil || r.Method != http.MethodPatch {
w.WriteHeader(http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(map[string]string{"id": id})
return
}
d.mu.Lock() d.mu.Lock()
d.nextID++ d.nextID++
id := fmt.Sprintf("id-%d", d.nextID) id := fmt.Sprintf("id-%d", d.nextID)
@@ -241,11 +223,6 @@ func (d *fakeDrive) handleFile(w http.ResponseWriter, r *http.Request) {
if ok && r.Method == http.MethodDelete { if ok && r.Method == http.MethodDelete {
delete(d.files, id) delete(d.files, id)
} }
var snapshot fakeFile
if ok {
// a file can be updated, its fields are read here
snapshot = *file
}
d.mu.Unlock() d.mu.Unlock()
if !ok { if !ok {
@@ -256,25 +233,11 @@ func (d *fakeDrive) handleFile(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent) w.WriteHeader(http.StatusNoContent)
return return
} }
if r.Method == http.MethodPatch {
var meta struct {
Description string `json:"description"`
}
if err := json.NewDecoder(r.Body).Decode(&meta); err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
d.mu.Lock()
file.description = meta.Description
d.mu.Unlock()
json.NewEncoder(w).Encode(map[string]string{"id": id})
return
}
if strings.Contains(r.URL.RawQuery, "fields=description") { if strings.Contains(r.URL.RawQuery, "fields=description") {
json.NewEncoder(w).Encode(map[string]string{"description": snapshot.description}) json.NewEncoder(w).Encode(map[string]string{"description": file.description})
return return
} }
w.Write(snapshot.data) w.Write(file.data)
} }
func (d *fakeDrive) shouldFail(kind string) bool { func (d *fakeDrive) shouldFail(kind string) bool {
@@ -599,12 +562,6 @@ func TestDriveSharedClient(t *testing.T) {
if first != second { if first != second {
t.Fatal("same settings did not share one storage") t.Fatal("same settings did not share one storage")
} }
// a session has an inflight of its own, as large
session := sessionStorage(first).(*driveStorage)
defer session.Close()
if session.inflight == first.(*driveStorage).inflight || cap(session.inflight) != driveMaxInflight {
t.Fatalf("a session shares inflight or has cap %d", cap(session.inflight))
}
} }
func TestDriveOldServiceName(t *testing.T) { func TestDriveOldServiceName(t *testing.T) {
-518
View File
@@ -1,518 +0,0 @@
package xdrive
import (
"bytes"
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/xml"
stderrors "errors"
"io"
"net"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"time"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/transport/internet"
"golang.org/x/net/http/httpguts"
)
type s3Storage struct {
*s3Shared
client *http.Client
inflight chan struct{}
release func()
}
type s3Shared struct {
conns *connPool
base url.URL
prefix string
region string
accessKey, secretKey string
token string
backoff time.Duration
}
func s3Region(region string) string {
if region == "" {
return "us-east-1"
}
return region
}
func ValidateS3Config(c *Config) error {
if c == nil || c.S3 == nil {
return errors.New(`service "s3" needs an "s3" object`)
}
u, err := url.Parse(c.S3.Endpoint)
if err != nil || (u.Scheme != "https" && u.Scheme != "http") || u.Hostname() == "" ||
u.User != nil || u.RawQuery != "" || u.ForceQuery || u.Fragment != "" || u.Opaque != "" {
return errors.New("S3 endpoint must be an http(s) URL without credentials, query or fragment")
}
if !httpguts.ValidHeaderFieldValue(c.S3.Region) || strings.ContainsAny(c.S3.Region, "/, \t") {
return errors.New("invalid S3 signing region")
}
bucket := c.S3.Bucket
if !validS3Name(bucket) || strings.Contains(bucket, "/") {
return errors.New("S3 needs a bucket name without path separators")
}
if !c.S3.PathStyle {
if net.ParseIP(u.Hostname()) != nil {
return errors.New("S3 virtual-hosted addressing requires a DNS endpoint")
}
// In this mode the bucket is part of the HTTP authority, not its path.
if strings.Trim(bucket, "abcdefghijklmnopqrstuvwxyz0123456789-.") != "" || strings.Contains(bucket, "..") ||
strings.HasPrefix(bucket, ".") || strings.HasSuffix(bucket, ".") {
return errors.New("S3 virtual-hosted addressing requires a DNS-compatible bucket name")
}
}
if !validS3Name(strings.TrimSuffix(c.RemoteFolder, "/")) {
return errors.New("S3 remoteFolder must be a nonempty relative prefix without empty, '.' or '..' components")
}
if len(c.Secrets) < 2 || len(c.Secrets) > 3 || c.Secrets[0] == "" || c.Secrets[1] == "" {
return errors.New("S3 needs AccessKeyID, SecretAccessKey and optionally SessionToken in secrets")
}
for _, secret := range c.Secrets {
if !httpguts.ValidHeaderFieldValue(secret) {
return errors.New("invalid S3 secret")
}
}
if strings.ContainsAny(c.Secrets[0], "/ ,\t") {
return errors.New("invalid S3 AccessKeyID")
}
return nil
}
func validS3Name(name string) bool {
for _, ch := range name {
if ch < 0x20 || ch == 0x7f || ch == '\\' {
return false
}
}
for _, part := range strings.Split(name, "/") {
if part == "" || part == "." || part == ".." {
return false
}
}
return true
}
func newS3Storage(streamSettings *internet.MemoryStreamConfig, c *Config) (*s3Storage, error) {
if err := ValidateS3Config(c); err != nil {
return nil, err
}
base, _ := url.Parse(c.S3.Endpoint)
// Encode the endpoint prefix for SigV4 without turning escaped slashes into separators.
segments := strings.Split(base.EscapedPath(), "/")
for i, segment := range segments {
decoded, _ := url.PathUnescape(segment)
segments[i] = strings.ReplaceAll(s3EscapePath(decoded), "/", "%2F")
}
base.RawPath = strings.Join(segments, "/")
if !strings.HasSuffix(base.RawPath, "/") {
base.Path += "/"
base.RawPath += "/"
}
if c.S3.PathStyle {
base.Path += c.S3.Bucket + "/"
base.RawPath += s3EscapePath(c.S3.Bucket) + "/"
} else {
host := c.S3.Bucket + "." + base.Hostname()
if base.Port() != "" {
host = net.JoinHostPort(host, base.Port())
}
base.Host = host
}
if streamSettings == nil {
streamSettings = &internet.MemoryStreamConfig{}
}
h1Hosts := new(sync.Map)
newClient := func(ctx context.Context) *http.Client {
client := newServiceClient(ctx, h1Hosts, streamSettings, driveTimeout, driveMaxInflight)
// A redirect changes the signed host/path. Do not follow it with the old
// signature or send the session token to a different endpoint.
client.CheckRedirect = s3NoRedirect
return client
}
s := &s3Storage{
s3Shared: &s3Shared{
conns: newConnPool(c.Xmux, newClient),
base: *base,
prefix: strings.TrimSuffix(c.RemoteFolder, "/") + "/",
region: s3Region(c.S3.Region),
accessKey: c.Secrets[0],
secretKey: c.Secrets[1],
backoff: 200 * time.Millisecond,
},
client: newClient(context.Background()),
inflight: make(chan struct{}, driveMaxInflight),
}
if len(c.Secrets) == 3 {
s.token = c.Secrets[2]
}
return s, nil
}
func s3NoRedirect(_ *http.Request, _ []*http.Request) error { return http.ErrUseLastResponse }
func (s *s3Storage) session() Storage {
transport := &sessionTransport{pool: s.conns}
return &s3Storage{
s3Shared: s.s3Shared,
client: &http.Client{Transport: transport, CheckRedirect: s3NoRedirect},
inflight: make(chan struct{}, cap(s.inflight)),
release: transport.Close,
}
}
func (s *s3Storage) Close() error {
if s.release != nil {
s.release()
}
return nil
}
// SigV4 requires %20 for spaces, uppercase escapes, and unescaped key separators.
func s3EscapePath(value string) string {
const digits = "0123456789ABCDEF"
var out strings.Builder
for i := 0; i < len(value); i++ {
ch := value[i]
if ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z' || ch >= '0' && ch <= '9' ||
ch == '-' || ch == '_' || ch == '.' || ch == '~' || ch == '/' {
out.WriteByte(ch)
} else {
out.WriteByte('%')
out.WriteByte(digits[ch>>4])
out.WriteByte(digits[ch&15])
}
}
return out.String()
}
func s3Hash(body []byte) string {
sum := sha256.Sum256(body)
return hex.EncodeToString(sum[:])
}
func s3HMAC(key []byte, value string) []byte {
h := hmac.New(sha256.New, key)
h.Write([]byte(value))
return h.Sum(nil)
}
func (s *s3Storage) sign(req *http.Request, payloadHash string, now time.Time) {
stamp, date := now.UTC().Format("20060102T150405Z"), now.UTC().Format("20060102")
req.Header.Set("X-Amz-Date", stamp)
req.Header.Set("X-Amz-Content-Sha256", payloadHash)
headers := "host:" + req.URL.Host + "\nx-amz-content-sha256:" + payloadHash + "\nx-amz-date:" + stamp + "\n"
signed := "host;x-amz-content-sha256;x-amz-date"
if contentType := req.Header.Get("Content-Type"); contentType != "" {
headers = "content-type:" + contentType + "\n" + headers
signed = "content-type;" + signed
}
if s.token != "" {
req.Header.Set("X-Amz-Security-Token", s.token)
headers += "x-amz-security-token:" + strings.Join(strings.Fields(s.token), " ") + "\n"
signed += ";x-amz-security-token"
}
// Sign the escaped path exactly as sent.
canonical := req.Method + "\n" + req.URL.EscapedPath() + "\n" + req.URL.RawQuery + "\n" +
headers + "\n" + signed + "\n" + payloadHash
scope := date + "/" + s.region + "/s3/aws4_request"
toSign := "AWS4-HMAC-SHA256\n" + stamp + "\n" + scope + "\n" + s3Hash([]byte(canonical))
key := s3HMAC([]byte("AWS4"+s.secretKey), date)
key = s3HMAC(key, s.region)
key = s3HMAC(key, "s3")
key = s3HMAC(key, "aws4_request")
req.Header.Set("Authorization", "AWS4-HMAC-SHA256 Credential="+s.accessKey+"/"+scope+
", SignedHeaders="+signed+", Signature="+hex.EncodeToString(s3HMAC(key, toSign)))
}
func (s *s3Storage) target(key string, query url.Values) string {
u := s.base
u.Path += key
u.RawPath += s3EscapePath(key)
// Parameters have fixed ASCII names and one value each; SigV4 uses %20 for spaces.
u.RawQuery = strings.ReplaceAll(query.Encode(), "+", "%20")
return u.String()
}
func (s *s3Storage) objectKey(name string) (string, error) {
if !validS3Name(name) {
return "", errors.New("invalid relative S3 object name")
}
key := s.prefix + name
if len(key) > 1024 {
return "", errors.New("S3 object key exceeds 1024 bytes")
}
return key, nil
}
// Retries use fresh signatures and hold a concurrency slot until the response body is read.
func (s *s3Storage) do(ctx context.Context, method, key string, query url.Values, body []byte) (int, []byte, error) {
payloadHash := s3Hash(body)
backoff := s.backoff
var lastErr error
var retryAfter time.Duration
for attempt := 0; attempt < driveMaxAttempts; attempt++ {
if attempt > 0 {
delay := max(jitter(backoff), retryAfter)
timer := time.NewTimer(min(delay, driveMaxBackoff))
select {
case <-ctx.Done():
timer.Stop()
return 0, nil, ctx.Err()
case <-timer.C:
}
backoff = min(backoff*2, driveMaxBackoff)
}
select {
case s.inflight <- struct{}{}:
case <-ctx.Done():
return 0, nil, ctx.Err()
}
status, payload, after, err := s.attempt(ctx, method, key, query, body, payloadHash)
<-s.inflight
if ctx.Err() != nil {
return 0, nil, ctx.Err()
}
if err != nil && (!retryS3Error(err) || status >= 300 && !retryableStatus(status)) {
return 0, nil, err
}
if err == nil && !retryableStatus(status) {
return status, payload, nil
}
lastErr, retryAfter = err, after
if lastErr == nil {
lastErr = s3ResponseError(method, status, payload)
}
}
return 0, nil, lastErr
}
func retryS3Error(err error) bool {
var dns *net.DNSError
if stderrors.As(err, &dns) {
return dns.IsTimeout || dns.IsTemporary
}
var op *net.OpError
var network net.Error
return stderrors.Is(err, io.EOF) || stderrors.Is(err, io.ErrUnexpectedEOF) || stderrors.Is(err, errStalled) ||
stderrors.As(err, &op) || stderrors.As(err, &network) && network.Timeout()
}
func (s *s3Storage) attempt(ctx context.Context, method, key string, query url.Values, body []byte, hash string) (int, []byte, time.Duration, error) {
req, err := http.NewRequestWithContext(ctx, method, s.target(key, query), bytes.NewReader(body))
if err != nil {
return 0, nil, 0, errors.New("failed to build S3 request")
}
if method == http.MethodPut {
req.Header.Set("Content-Type", "application/octet-stream")
}
s.sign(req, hash, time.Now())
resp, err := s.client.Do(req)
if err != nil {
if urlErr, ok := err.(*url.Error); ok {
err = urlErr.Err
}
return 0, nil, 0, errors.New("S3 request failed").Base(err)
}
defer resp.Body.Close()
limit := int64(maxSegmentBytes)
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
limit = 64 << 10
}
payload, err := io.ReadAll(io.LimitReader(resp.Body, limit+1))
if err != nil {
return resp.StatusCode, nil, 0, errors.New("failed to read S3 response (HTTP ", resp.StatusCode, ")").Base(err)
}
if int64(len(payload)) > limit {
return 0, nil, 0, errors.New("S3 response exceeds size limit")
}
var after time.Duration
if raw := resp.Header.Get("Retry-After"); raw != "" {
if seconds, err := strconv.ParseInt(raw, 10, 32); err == nil && seconds > 0 {
after = min(time.Duration(seconds)*time.Second, driveMaxBackoff)
} else if at, err := http.ParseTime(raw); err == nil {
after = min(max(time.Until(at), 0), driveMaxBackoff)
}
}
return resp.StatusCode, payload, after, nil
}
func s3Missing(status int, payload []byte) bool {
if status != http.StatusNotFound {
return false
}
code := s3ErrorCode(payload)
return code == "NoSuchKey" || code == "NotFound"
}
func s3ErrorCode(payload []byte) string {
var response struct {
XMLName xml.Name `xml:"Error"`
Code string
}
if xml.Unmarshal(payload, &response) != nil {
return ""
}
return response.Code
}
func s3ResponseError(method string, status int, payload []byte) error {
if code := s3ErrorCode(payload); code != "" {
return errors.New("S3 ", method, " answered ", status, " (", strconv.Quote(code), ")")
}
return errors.New("S3 ", method, " answered ", status)
}
func (s *s3Storage) Put(ctx context.Context, name string, data []byte) error {
key, err := s.objectKey(name)
if err != nil {
return err
}
status, payload, err := s.do(ctx, http.MethodPut, key, nil, data)
if err != nil {
return err
}
if status != http.StatusOK {
return s3ResponseError("PUT", status, payload)
}
return nil
}
func (s *s3Storage) Get(ctx context.Context, name string) ([]byte, error) {
key, err := s.objectKey(name)
if err != nil {
return nil, err
}
status, payload, err := s.do(ctx, http.MethodGet, key, nil, nil)
if err != nil {
return nil, err
}
if s3Missing(status, payload) {
return nil, errNotFound
}
if status != http.StatusOK {
return nil, s3ResponseError("GET", status, payload)
}
return payload, nil
}
type s3ListPage struct {
XMLName xml.Name `xml:"ListBucketResult"`
IsTruncated bool
NextContinuationToken string
Contents []struct{ Key string }
CommonPrefixes []struct{ Prefix string }
}
func (s *s3Storage) list(ctx context.Context, prefix, delimiter string, visit func(*s3ListPage) error) error {
query := url.Values{"list-type": {"2"}, "prefix": {prefix}, "max-keys": {"1000"}}
if delimiter != "" {
query.Set("delimiter", delimiter)
}
seen := make(map[string]bool)
for {
status, payload, err := s.do(ctx, http.MethodGet, "", query, nil)
if err != nil {
return err
}
if status != http.StatusOK {
return s3ResponseError("LIST", status, payload)
}
var page s3ListPage
if err := xml.Unmarshal(payload, &page); err != nil {
return errors.New("invalid S3 list response")
}
// Validate all keys before deleting any objects.
for _, item := range page.Contents {
if !strings.HasPrefix(item.Key, prefix) {
return errors.New("S3 returned an object outside the requested prefix")
}
}
for _, item := range page.CommonPrefixes {
if !strings.HasPrefix(item.Prefix, prefix) {
return errors.New("S3 returned a directory outside the requested prefix")
}
}
if page.IsTruncated {
if page.NextContinuationToken == "" || seen[page.NextContinuationToken] {
return errors.New("S3 list pagination did not advance")
}
seen[page.NextContinuationToken] = true
}
if err := visit(&page); err != nil {
return err
}
if !page.IsTruncated {
return nil
}
query.Set("continuation-token", page.NextContinuationToken)
}
}
func (s *s3Storage) List(ctx context.Context, prefix string) ([]Entry, error) {
key, err := s.objectKey(prefix)
if err != nil {
return nil, err
}
key += "/"
seen := make(map[string]bool)
var entries []Entry
add := func(full string) {
rest := strings.TrimPrefix(full, key)
name, _, _ := strings.Cut(rest, "/")
if name != "" && validS3Name(name) && !seen[name] {
seen[name] = true
entries = append(entries, Entry{Name: name})
}
}
err = s.list(ctx, key, "/", func(page *s3ListPage) error {
for _, item := range page.Contents {
add(item.Key)
}
for _, item := range page.CommonPrefixes {
add(item.Prefix)
}
return nil
})
return entries, err
}
func (s *s3Storage) deleteObject(ctx context.Context, key string) error {
status, payload, err := s.do(ctx, http.MethodDelete, key, nil, nil)
if err != nil {
return err
}
if status != http.StatusNoContent && status != http.StatusOK && !s3Missing(status, payload) {
return s3ResponseError("DELETE", status, payload)
}
return nil
}
// The trailing "/" prevents deleting streams/abcd when removing streams/abc.
func (s *s3Storage) Delete(ctx context.Context, name string) error {
key, err := s.objectKey(name)
if err != nil {
return err
}
if err := s.deleteObject(ctx, key); err != nil {
return err
}
return s.list(ctx, key+"/", "", func(page *s3ListPage) error {
for _, item := range page.Contents {
if err := s.deleteObject(ctx, item.Key); err != nil {
return err
}
}
return nil
})
}
-758
View File
@@ -1,758 +0,0 @@
package xdrive
import (
"bytes"
"context"
"encoding/base64"
"encoding/binary"
"encoding/json"
"hash/crc32"
"io"
"strconv"
"sync"
"time"
"github.com/xtls/xray-core/common/errors"
)
// A session on a slotStore is carried in a ring of slots for each direction,
// objects that both peers know by id: segment n is written to slot n % depth
// and read from it, with nothing to list and no index to wait for. The reader
// of a direction keeps how far it has read in a watermark, and the writer
// reuses a slot only once that says what was in it before was taken
const (
slotMagic = "XDRVSL01"
slotHeaderLen = len(slotMagic) + 8 + 1 + 4 + 4
// slotInlineLimit is how large a segment is at most to go in the description
// of its slot, which takes one request less than a body
slotInlineLimit = 14 << 10
slotDepthLimit = 8
// inlineRampSegments segments of one size in a row double it, from
// slotInlineLimit up to segmentBytes
inlineRampSegments = 8
slotInline = 1
slotMedia = 2
slotEnd = 3
slotErr = 4
)
var (
// the head is asked for between slotHuntFloor and slotHuntCeiling, a segment
// behind it at about the pace that segments come
slotHuntFloor = 50 * time.Millisecond
slotHuntCeiling = 200 * time.Millisecond
slotTailMin = 150 * time.Millisecond
slotTailMax = 800 * time.Millisecond
slotFetchAhead = 6
slotAckRetry = 100 * time.Millisecond
slotWatermarkTTL = 250 * time.Millisecond
slotStallTimeout = 60 * time.Second
slotCloseGrace = 10 * time.Second
slotGoneTimeout = 60 * time.Second
)
// slotStore is a storage of objects that are written and read again by id,
// each with a description and a body
type slotStore interface {
makeSlot(ctx context.Context, name string) (string, error)
// setSlot sets the description of a slot and, unless body is nil, its body
setSlot(ctx context.Context, id, description string, body []byte) error
readDescription(ctx context.Context, id string) (string, error)
readBody(ctx context.Context, id string) ([]byte, error)
}
// slotFrame is what the description of a slot says: which segment it holds,
// and either the segment itself or the size and checksum of the body that does
type slotFrame struct {
seq int64
mode byte
size int
crc uint32
data []byte
}
func newSlotFrame(seq int64, mode byte, data []byte) slotFrame {
f := slotFrame{seq: seq, mode: mode, size: len(data), crc: crc32.ChecksumIEEE(data)}
if mode == slotInline {
f.data = data
}
return f
}
func (f slotFrame) encode() string {
b := make([]byte, slotHeaderLen, slotHeaderLen+len(f.data))
copy(b, slotMagic)
binary.BigEndian.PutUint64(b[8:], uint64(f.seq))
b[16] = f.mode
binary.BigEndian.PutUint32(b[17:], uint32(f.size))
binary.BigEndian.PutUint32(b[21:], f.crc)
return base64.StdEncoding.EncodeToString(append(b, f.data...))
}
func decodeSlot(description string) (slotFrame, bool) {
b, err := base64.StdEncoding.DecodeString(description)
if err != nil || len(b) < slotHeaderLen || string(b[:len(slotMagic)]) != slotMagic {
return slotFrame{}, false
}
f := slotFrame{
seq: int64(binary.BigEndian.Uint64(b[8:])),
mode: b[16],
size: int(binary.BigEndian.Uint32(b[17:])),
crc: binary.BigEndian.Uint32(b[21:]),
}
if f.mode == slotInline {
f.data = b[slotHeaderLen:]
if !f.holds(f.data) {
return slotFrame{}, false
}
}
return f, true
}
// holds reports whether body is the one that f is about, which a body read
// right after its description need not be yet
func (f slotFrame) holds(body []byte) bool {
return len(body) == f.size && crc32.ChecksumIEEE(body) == f.crc
}
// slotTable is the ring of one direction: the ids of its slots and of the
// watermark of its reader
type slotTable struct {
Slots []string `json:"s"`
Watermark string `json:"w"`
}
// ring is what a client announces a session with, the tables are named from
// its side
type ring struct {
Up slotTable `json:"u"`
Down slotTable `json:"d"`
}
func decodeRing(b []byte) (ring, bool) {
var r ring
if json.Unmarshal(b, &r) != nil || len(r.Up.Slots) == 0 || len(r.Down.Slots) == 0 ||
r.Up.Watermark == "" || r.Down.Watermark == "" {
return ring{}, false
}
return r, true
}
// makeRing makes the slots and watermarks of a session, all at once
func makeRing(ctx context.Context, store slotStore, session string, p params) (ring, error) {
depth := min(max(p.concurrency, 2), slotDepthLimit)
r := ring{
Up: slotTable{Slots: make([]string, depth)},
Down: slotTable{Slots: make([]string, depth)},
}
var (
wg sync.WaitGroup
errOnce sync.Once
err error
)
create := func(id *string, name string) {
wg.Add(1)
go func() {
defer wg.Done()
var e error
if *id, e = store.makeSlot(ctx, name); e != nil {
errOnce.Do(func() { err = e })
}
}()
}
for _, t := range []struct {
table *slotTable
prefix string
}{{&r.Up, uplinkPrefix(session)}, {&r.Down, downlinkPrefix(session)}} {
for i := range t.table.Slots {
create(&t.table.Slots[i], t.prefix+"/s"+strconv.Itoa(i))
}
create(&t.table.Watermark, t.prefix+"/w")
}
wg.Wait()
return r, err
}
func readWatermark(ctx context.Context, store slotStore, id string) (int64, error) {
d, err := store.readDescription(ctx, id)
if err != nil || d == "" {
return -1, err
}
return strconv.ParseInt(d, 10, 64)
}
type slotWriter struct {
ctx context.Context
store slotStore
table slotTable
params
// slots has a token for each segment on its way, at most one for each slot
slots chan struct{}
wg sync.WaitGroup
mu sync.Mutex
buf []byte
seq int64
size int
full int
lastSize int
held int
closed bool
err error
// ackMu is held while the watermark is read, which other uploads wait for
ackMu sync.Mutex
acked int64
ackedAt time.Time
progress time.Time
}
func newSlotWriter(ctx context.Context, store slotStore, table slotTable, p params) *slotWriter {
w := &slotWriter{
ctx: ctx,
store: store,
table: table,
params: p,
slots: make(chan struct{}, len(table.Slots)),
size: min(slotInlineLimit, p.segmentBytes),
acked: -1,
progress: time.Now(),
}
go w.flushLoop()
return w
}
func (w *slotWriter) Write(p []byte) (int, error) {
w.mu.Lock()
defer w.mu.Unlock()
if w.err != nil {
return 0, w.err
}
if w.closed {
return 0, io.ErrClosedPipe
}
w.buf = append(w.buf, p...)
for len(w.buf) >= w.size {
if err := w.flushLocked(); err != nil {
return 0, err
}
}
return len(p), nil
}
func (w *slotWriter) flushLoop() {
ticker := time.NewTicker(w.flushInterval)
defer ticker.Stop()
for {
select {
case <-w.ctx.Done():
return
case <-ticker.C:
w.mu.Lock()
if !w.closed && w.err == nil && len(w.buf) > 0 && w.readyToFlush() {
w.flushLocked()
}
done := w.closed || w.err != nil
w.mu.Unlock()
if done {
return
}
}
}
}
func (w *slotWriter) readyToFlush() bool {
grew := len(w.buf) > w.lastSize
w.lastSize = len(w.buf)
if grew && w.held < maxCoalescedTicks {
w.held++
return false
}
w.held = 0
return true
}
// flushLocked sends a segment off buf once a slot is free, which it waits for
// without w.mu, so that a reader that falls behind holds Write back. w.mu is
// held
func (w *slotWriter) flushLocked() error {
w.mu.Unlock()
select {
case w.slots <- struct{}{}:
case <-w.ctx.Done():
w.mu.Lock()
return w.ctx.Err()
}
w.mu.Lock()
if w.err != nil || len(w.buf) == 0 {
<-w.slots
return w.err
}
n := min(len(w.buf), w.size)
if len(w.buf) > 4*slotInlineLimit {
// a burst goes at the full size at once
n = min(len(w.buf), w.segmentBytes)
}
if n == w.size && w.size < w.segmentBytes {
if w.full++; w.full == inlineRampSegments {
w.full = 0
w.size = min(2*w.size, w.segmentBytes)
}
}
chunk := bytes.Clone(w.buf[:n])
w.buf = append(w.buf[:0], w.buf[n:]...)
seq := w.seq
w.seq++
w.lastSize = len(w.buf)
w.held = 0
w.wg.Add(1)
go w.upload(seq, chunk)
return nil
}
func (w *slotWriter) slotID(seq int64) string {
return w.table.Slots[seq%int64(len(w.table.Slots))]
}
func (w *slotWriter) upload(seq int64, chunk []byte) {
defer w.wg.Done()
defer func() { <-w.slots }()
mode := byte(slotMedia)
if len(chunk) <= slotInlineLimit {
mode = slotInline
}
if err := w.await(seq); err != nil {
w.fail(errors.New("failed to store segment ", seq).Base(err))
return
}
if err := w.write(seq, mode, chunk); err != nil {
w.fail(errors.New("failed to store segment ", seq).Base(err))
// the slot is free, the reader learns there what happened
w.write(seq, slotErr, nil)
}
}
func (w *slotWriter) write(seq int64, mode byte, data []byte) error {
f := newSlotFrame(seq, mode, data)
var body []byte
if mode == slotMedia {
body = data
}
return w.store.setSlot(w.ctx, w.slotID(seq), f.encode(), body)
}
// drained reports whether the reader took what the slot of seq held a lap
// before. The watermark is read again only after slotWatermarkTTL, as one
// that is old costs a wait but never what is in a slot
func (w *slotWriter) drained(seq int64) (bool, error) {
w.ackMu.Lock()
defer w.ackMu.Unlock()
before := seq - int64(len(w.table.Slots))
if w.acked >= before {
return true, nil
}
if time.Since(w.ackedAt) < slotWatermarkTTL {
return false, nil
}
acked, err := readWatermark(w.ctx, w.store, w.table.Watermark)
if err != nil {
return false, err
}
w.ackedAt = time.Now()
if acked > w.acked {
w.acked, w.progress = acked, w.ackedAt
}
return w.acked >= before, nil
}
// await waits for drained, for slotStallTimeout at most, or slotCloseGrace
// without progress once the writer is closed
func (w *slotWriter) await(seq int64) error {
start := time.Now()
wait := slotAckRetry
for {
ok, err := w.drained(seq)
if ok {
return nil
}
w.mu.Lock()
closed := w.closed
w.mu.Unlock()
w.ackMu.Lock()
still := time.Since(w.progress)
w.ackMu.Unlock()
if time.Since(start) > slotStallTimeout || closed && still > slotCloseGrace {
return errors.New("the reader took nothing from slot ", w.slotID(seq), " for ", still).Base(err)
}
if err != nil {
wait = min(2*wait, 2*time.Second)
} else {
wait = slotAckRetry
}
select {
case <-w.ctx.Done():
return w.ctx.Err()
case <-time.After(wait):
}
}
}
func (w *slotWriter) fail(err error) {
w.mu.Lock()
defer w.mu.Unlock()
if w.err == nil {
w.err = err
}
}
func (w *slotWriter) Close() error {
w.mu.Lock()
if w.closed {
w.mu.Unlock()
return nil
}
w.closed = true
for len(w.buf) > 0 && w.err == nil {
if err := w.flushLocked(); err != nil {
w.mu.Unlock()
return err
}
}
seq := w.seq
w.mu.Unlock()
w.ackMu.Lock()
w.progress = time.Now()
w.ackMu.Unlock()
w.wg.Wait()
w.mu.Lock()
err := w.err
w.mu.Unlock()
if err != nil {
return err
}
if err := w.await(seq); err != nil {
return err
}
return w.write(seq, slotEnd, nil)
}
type slotReader struct {
ctx context.Context
store slotStore
table slotTable
params
ch chan []byte
wake chan struct{}
acks chan struct{}
mu sync.Mutex
head int64
started bool
// hitAt is when a segment last came and gap the time between them, activeAt
// when a segment came, was seen coming or Wake. Nothing for the eager window
// is idle, and active is closed when that ends
hitAt time.Time
gap time.Duration
activeAt time.Time
active chan struct{}
err error
}
func newSlotReader(ctx context.Context, store slotStore, table slotTable, p params) *slotReader {
r := &slotReader{
ctx: ctx,
store: store,
table: table,
params: p,
ch: make(chan []byte, p.concurrency),
wake: make(chan struct{}, 1),
acks: make(chan struct{}, 1),
hitAt: time.Now(),
activeAt: time.Now(),
active: make(chan struct{}),
}
go r.run()
go r.ackLoop()
return r
}
func (r *slotReader) chunks() <-chan []byte {
return r.ch
}
// Wake has the head asked for now, as an answer is likely to come
func (r *slotReader) Wake() {
r.mu.Lock()
r.touch()
r.mu.Unlock()
select {
case r.wake <- struct{}{}:
default:
}
}
func (r *slotReader) slotID(seq int64) string {
return r.table.Slots[seq%int64(len(r.table.Slots))]
}
type slotFetch struct {
ready chan struct{}
// promoted is closed when the segment becomes the head
promoted chan struct{}
body []byte
end bool
err error
}
func (r *slotReader) run() {
defer close(r.ch)
ctx, cancel := context.WithCancel(r.ctx)
defer cancel()
fetches := make(map[int64]*slotFetch)
for {
r.mu.Lock()
head, started := r.head, r.started
r.mu.Unlock()
window := 1
if started {
window = min(slotFetchAhead, len(r.table.Slots))
}
for i := range window {
seq := head + int64(i)
if fetches[seq] == nil {
fetches[seq] = &slotFetch{ready: make(chan struct{}), promoted: make(chan struct{})}
go r.fetch(ctx, seq, i, fetches[seq])
}
}
f := fetches[head]
select {
case <-f.ready:
case <-ctx.Done():
return
}
delete(fetches, head)
if f.err != nil {
r.setErr(f.err)
return
}
if f.end {
return
}
// the slot is free once the bytes are here, not once they are read
r.mu.Lock()
r.head++
r.started = true
r.mu.Unlock()
if next := fetches[head+1]; next != nil {
close(next.promoted)
}
select {
case r.acks <- struct{}{}:
default:
}
select {
case r.ch <- f.body:
case <-ctx.Done():
return
}
}
}
func (r *slotReader) isHead(seq int64) bool {
r.mu.Lock()
defer r.mu.Unlock()
return r.head == seq
}
// fetch asks for the slot of seq until it holds seq: the head often, a segment
// behind it at the pace that segments come, as it would wait for the head anyway
func (r *slotReader) fetch(ctx context.Context, seq int64, offset int, f *slotFetch) {
defer close(f.ready)
if offset > 0 && !r.hold(ctx, time.Duration(offset)*r.pace(), f.promoted, nil) {
return
}
id := r.slotID(seq)
backoff := slotHuntFloor
var goneSince, warnedAt time.Time
for {
description, err := r.store.readDescription(ctx, id)
frame, ok := decodeSlot(description)
switch {
case err == nil && ok && frame.seq == seq:
// what follows it is asked for while its body comes
r.mu.Lock()
r.touch()
r.mu.Unlock()
switch frame.mode {
case slotEnd:
f.end = true
return
case slotErr:
f.err = errors.New("the peer could not store segment ", seq)
return
case slotInline:
f.body = frame.data
r.hit()
return
case slotMedia:
if body, err := r.store.readBody(ctx, id); err == nil && frame.holds(body) {
f.body = body
r.hit()
return
}
}
case err == errNotFound:
if goneSince.IsZero() {
goneSince = time.Now()
} else if time.Since(goneSince) > slotGoneTimeout && r.isHead(seq) {
f.err = errors.New("slot ", id, " of segment ", seq, " is gone").Base(err)
return
}
case err != nil:
goneSince = time.Time{}
if ctx.Err() == nil && time.Since(warnedAt) > 10*time.Second {
warnedAt = time.Now()
errors.LogWarningInner(ctx, err, "failed to read slot ", id)
}
}
r.mu.Lock()
head, idle, active := r.head == seq, time.Since(r.activeAt) > r.eagerWindow, r.active
r.mu.Unlock()
wait, a, b := backoff, (<-chan struct{})(r.wake), (<-chan struct{})(nil)
switch {
case head:
ceiling := slotHuntCeiling
if idle {
ceiling = max(ceiling, r.maxPollInterval)
}
backoff = min(2*backoff, ceiling)
case idle:
// only the head is asked for until something comes
wait, a, b, backoff = 0, f.promoted, active, slotHuntFloor
default:
wait, a, backoff = min(max(r.pace(), slotTailMin), slotTailMax), f.promoted, slotHuntFloor
}
if !r.hold(ctx, wait, a, b) {
return
}
}
}
// pace is about how long segments take to come one after the other
func (r *slotReader) pace() time.Duration {
r.mu.Lock()
defer r.mu.Unlock()
return min(max(r.gap, 300*time.Millisecond), 3*time.Second)
}
func (r *slotReader) hit() {
r.mu.Lock()
defer r.mu.Unlock()
if gap := time.Since(r.hitAt); r.gap == 0 {
r.gap = gap
} else {
r.gap = (r.gap + gap) / 2
}
r.hitAt = time.Now()
r.touch()
}
// touch notes that something came or is likely to. r.mu is held
func (r *slotReader) touch() {
if time.Since(r.activeAt) > r.eagerWindow {
close(r.active)
r.active = make(chan struct{})
}
r.activeAt = time.Now()
}
// hold waits d, unless d is 0, or until a or b is closed or has a value
func (r *slotReader) hold(ctx context.Context, d time.Duration, a, b <-chan struct{}) bool {
var timeout <-chan time.Time
if d > 0 {
timer := time.NewTimer(d)
defer timer.Stop()
timeout = timer.C
}
select {
case <-ctx.Done():
return false
case <-a:
case <-b:
case <-timeout:
}
return true
}
// ackLoop writes the watermark when the head moves, the last head only if it
// moved more than once meanwhile
func (r *slotReader) ackLoop() {
acked := int64(-1)
wait := slotAckRetry
for {
select {
case <-r.ctx.Done():
return
case <-r.acks:
}
for {
r.mu.Lock()
want := r.head - 1
r.mu.Unlock()
if want <= acked {
break
}
if err := r.store.setSlot(r.ctx, r.table.Watermark, strconv.FormatInt(want, 10), nil); err == nil {
acked, wait = want, slotAckRetry
continue
}
wait = min(2*wait, 5*time.Second)
select {
case <-r.ctx.Done():
return
case <-time.After(wait):
}
}
}
}
func (r *slotReader) setErr(err error) {
r.mu.Lock()
defer r.mu.Unlock()
if r.err == nil {
r.err = err
}
}
func (r *slotReader) Err() error {
r.mu.Lock()
defer r.mu.Unlock()
if r.err != nil {
return r.err
}
return io.EOF
}
-309
View File
@@ -1,309 +0,0 @@
package xdrive
import (
"bytes"
"context"
"encoding/base64"
"errors"
"io"
"math/rand"
"os"
"strconv"
"sync"
"testing"
"time"
)
type memSlots struct {
mu sync.Mutex
next int
descs map[string]string
bodies map[string][]byte
reads int
}
func newMemSlots() *memSlots {
return &memSlots{descs: make(map[string]string), bodies: make(map[string][]byte)}
}
func (m *memSlots) makeSlot(ctx context.Context, name string) (string, error) {
m.mu.Lock()
defer m.mu.Unlock()
m.next++
id := "slot-" + strconv.Itoa(m.next)
m.descs[id] = ""
return id, nil
}
func (m *memSlots) setSlot(ctx context.Context, id, description string, body []byte) error {
m.mu.Lock()
defer m.mu.Unlock()
if _, ok := m.descs[id]; !ok {
return errNotFound
}
m.descs[id] = description
if body != nil {
m.bodies[id] = bytes.Clone(body)
}
return nil
}
func (m *memSlots) readDescription(ctx context.Context, id string) (string, error) {
m.mu.Lock()
defer m.mu.Unlock()
m.reads++
d, ok := m.descs[id]
if !ok {
return "", errNotFound
}
return d, nil
}
func (m *memSlots) readBody(ctx context.Context, id string) ([]byte, error) {
m.mu.Lock()
defer m.mu.Unlock()
return m.bodies[id], nil
}
// the slots of all tests are asked for often, set once as a goroutine of a
// closed Conn may still read them
func init() {
slotHuntFloor, slotHuntCeiling, slotTailMin, slotTailMax = 2*time.Millisecond, 10*time.Millisecond, 5*time.Millisecond, 20*time.Millisecond
slotAckRetry, slotWatermarkTTL = 2*time.Millisecond, 2*time.Millisecond
}
func slotPair(t *testing.T, store slotStore, concurrency, segmentBytes uint32) (*Conn, *Conn) {
t.Helper()
p := paramsFromConfig(&Config{
SegmentBytes: segmentBytes,
FlushIntervalMs: 1,
PollIntervalMs: 1,
MaxPollIntervalMs: 20,
EagerWindowMs: 50,
Concurrency: concurrency,
})
r, err := makeRing(context.Background(), store, "session", p)
if err != nil {
t.Fatal(err)
}
client := newSlotConn(context.Background(), store, r.Up, r.Down, p, nil)
server := newSlotConn(context.Background(), store, r.Down, r.Up, p, nil)
t.Cleanup(func() {
client.Close()
server.Close()
})
for _, c := range []*Conn{client, server} {
c.SetReadDeadline(time.Now().Add(30 * time.Second))
}
return client, server
}
// writeAll writes payload in pieces of random size, and closes c if end, the
// error goes to errs
func writeAll(c *Conn, payload []byte, end bool, errs chan<- error) {
go func() {
rng := rand.New(rand.NewSource(1))
for len(payload) > 0 {
n := min(len(payload), 1+rng.Intn(40<<10))
if _, err := c.Write(payload[:n]); err != nil {
errs <- err
return
}
payload = payload[n:]
}
var err error
if end {
err = c.Close()
}
errs <- err
}()
}
func readAll(t *testing.T, c *Conn, want []byte) {
t.Helper()
got, err := io.ReadAll(c)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, want) {
t.Fatalf("got %d bytes, not the %d that were sent", len(got), len(want))
}
}
func randomPayload(n int) []byte {
payload := make([]byte, n)
rand.New(rand.NewSource(int64(n))).Read(payload)
return payload
}
func TestSlotFrame(t *testing.T) {
data := []byte("hello")
f, ok := decodeSlot(newSlotFrame(7, slotInline, data).encode())
if !ok || f.seq != 7 || f.mode != slotInline || !bytes.Equal(f.data, data) {
t.Fatalf("inline frame came back as %+v %v", f, ok)
}
body := randomPayload(100)
f, ok = decodeSlot(newSlotFrame(8, slotMedia, body).encode())
if !ok || f.seq != 8 || f.mode != slotMedia || f.data != nil || !f.holds(body) || f.holds(body[1:]) {
t.Fatalf("media frame came back as %+v %v", f, ok)
}
corrupt, _ := base64.StdEncoding.DecodeString(newSlotFrame(1, slotInline, data).encode())
corrupt[len(corrupt)-1]++
for _, broken := range []string{
"", "not base64", newSlotFrame(1, slotInline, data).encode()[:20], "WERSVlNMMDE=",
base64.StdEncoding.EncodeToString(corrupt),
} {
if _, ok := decodeSlot(broken); ok {
t.Errorf("%q decoded", broken)
}
}
}
// both sides send more segments than the ring has slots at once, small ones
// that go in descriptions and large ones that go in bodies, then one closes
// and the other reads EOF
func TestSlotConnRoundTrip(t *testing.T) {
client, server := slotPair(t, newMemSlots(), 4, 64<<10)
up, down := randomPayload(1<<20), randomPayload(300<<10)
errs := make(chan error, 2)
writeAll(client, up, false, errs)
writeAll(server, down, false, errs)
for _, c := range []struct {
conn *Conn
want []byte
}{{server, up}, {client, down}} {
got := make([]byte, len(c.want))
if _, err := io.ReadFull(c.conn, got); err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, c.want) {
t.Fatal("what came is not what was sent")
}
}
for range 2 {
if err := <-errs; err != nil {
t.Fatal(err)
}
}
if err := client.Close(); err != nil {
t.Fatal(err)
}
readAll(t, server, nil)
}
// a reader that does not read holds the writer back instead of losing what a
// lap of the ring would overwrite
func TestSlotConnSlowReader(t *testing.T) {
client, server := slotPair(t, newMemSlots(), 2, 16<<10)
payload := randomPayload(400 << 10)
errs := make(chan error, 1)
writeAll(client, payload, true, errs)
time.Sleep(200 * time.Millisecond)
readAll(t, server, payload)
if err := <-errs; err != nil {
t.Fatal(err)
}
}
// the next segment is asked for at once when it becomes the head, not after
// the wait it had as one that was fetched ahead
func TestSlotLatency(t *testing.T) {
client, server := slotPair(t, newMemSlots(), 8, 64<<10)
start := time.Now()
for range 10 {
for _, c := range [][2]*Conn{{client, server}, {server, client}} {
c[0].Write([]byte("ping"))
if _, err := io.ReadFull(c[1], make([]byte, 4)); err != nil {
t.Fatal(err)
}
}
}
if took := time.Since(start); took > 2*time.Second {
t.Fatalf("10 round trips took %v", took)
}
}
// once nothing comes for the eager window only the heads are asked for, not
// the segments that a reader fetches ahead while things come
func TestSlotIdle(t *testing.T) {
store := newMemSlots()
client, server := slotPair(t, store, 8, 64<<10)
for _, c := range [][2]*Conn{{client, server}, {server, client}} {
c[0].Write([]byte("ping"))
if _, err := io.ReadFull(c[1], make([]byte, 4)); err != nil {
t.Fatal(err)
}
}
time.Sleep(200 * time.Millisecond)
store.mu.Lock()
before := store.reads
store.mu.Unlock()
time.Sleep(500 * time.Millisecond)
store.mu.Lock()
reads := store.reads - before
store.mu.Unlock()
// two heads, each asked for every 20 ms at most
if reads > 75 {
t.Fatalf("an idle session asked for slots %d times in 500 ms", reads)
}
}
type failingSlots struct {
*memSlots
}
// setSlot fails for segments, not for the error frame
func (f failingSlots) setSlot(ctx context.Context, id, description string, body []byte) error {
if frame, ok := decodeSlot(description); ok && (frame.mode == slotInline || frame.mode == slotMedia) {
return io.ErrUnexpectedEOF
}
return f.memSlots.setSlot(ctx, id, description, body)
}
func TestSlotWriterFailure(t *testing.T) {
client, server := slotPair(t, failingSlots{newMemSlots()}, 2, 16<<10)
client.Write([]byte("lost"))
server.SetReadDeadline(time.Now().Add(5 * time.Second))
if _, err := server.Read(make([]byte, 10)); err == nil || err == io.EOF || errors.Is(err, os.ErrDeadlineExceeded) {
t.Fatalf("the reader got %v, not the failure of the writer", err)
}
for start := time.Now(); time.Since(start) < 5*time.Second; time.Sleep(time.Millisecond) {
if _, err := client.Write([]byte("more")); err != nil {
return
}
}
t.Fatal("Write did not fail")
}
type staleSlots struct {
*memSlots
mu sync.Mutex
served map[string]bool
}
// readBody gives what a slot held before once, as a body read right after its
// description may
func (s *staleSlots) readBody(ctx context.Context, id string) ([]byte, error) {
s.mu.Lock()
first := !s.served[id]
s.served[id] = true
s.mu.Unlock()
if first {
return []byte("stale"), nil
}
return s.memSlots.readBody(ctx, id)
}
func TestSlotStaleBody(t *testing.T) {
client, server := slotPair(t, &staleSlots{memSlots: newMemSlots(), served: make(map[string]bool)}, 2, 64<<10)
payload := randomPayload(200 << 10)
errs := make(chan error, 1)
writeAll(client, payload, true, errs)
readAll(t, server, payload)
}
-27
View File
@@ -43,30 +43,11 @@ func newStorage(streamSettings *internet.MemoryStreamConfig) (Storage, error) {
return sharedStorage(streamSettings, config, func() (Storage, error) { return sharedStorage(streamSettings, config, func() (Storage, error) {
return newTemplateStorage(streamSettings, config) return newTemplateStorage(streamSettings, config)
}) })
case "s3":
return sharedStorage(streamSettings, config, func() (Storage, error) {
return newS3Storage(streamSettings, config)
})
default: default:
return nil, errors.New("unsupported service: ", config.Service) return nil, errors.New("unsupported service: ", config.Service)
} }
} }
// sessionStorage returns the storage of one session, which takes connections
// of XMUX until Close, with the token and cache of storage. Local storage has
// no connections
func sessionStorage(storage Storage) Storage {
switch s := storage.(type) {
case *driveStorage:
return s.session()
case *templateStorage:
return s.session()
case *s3Storage:
return s.session()
}
return storage
}
var ( var (
sharedMu sync.Mutex sharedMu sync.Mutex
shared = make(map[string]Storage) shared = make(map[string]Storage)
@@ -74,14 +55,6 @@ var (
func shareKey(streamSettings *internet.MemoryStreamConfig, config *Config) string { func shareKey(streamSettings *internet.MemoryStreamConfig, config *Config) string {
parts := []string{config.Service, config.RemoteFolder} parts := []string{config.Service, config.RemoteFolder}
if config.S3 != nil {
parts = append(parts, config.S3.Endpoint, s3Region(config.S3.Region), config.S3.Bucket)
if config.S3.PathStyle {
parts = append(parts, "path-style")
} else {
parts = append(parts, "virtual-hosted-style")
}
}
parts = append(parts, config.Secrets...) parts = append(parts, config.Secrets...)
if streamSettings != nil { if streamSettings != nil {
parts = append(parts, streamSettings.SecurityType) parts = append(parts, streamSettings.SecurityType)
+12 -76
View File
@@ -33,14 +33,6 @@ type opTemplate struct {
Headers map[string]string `json:"headers"` Headers map[string]string `json:"headers"`
Body string `json:"body"` Body string `json:"body"`
NamesRegex string `json:"namesRegex"` NamesRegex string `json:"namesRegex"`
// HrefPath makes the operation two-step, as in the Yandex Disk REST API: the first request
// answers with a JSON body, the URL found at HrefPath is then called with HrefMethod (GET by
// default), carrying the payload. The URL is used as given, with neither auth nor headers.
HrefPath string `json:"hrefPath"`
HrefMethod string `json:"hrefMethod"`
presigned bool
} }
type retryTemplate struct { type retryTemplate struct {
@@ -62,21 +54,13 @@ type storageTemplate struct {
} }
type templateStorage struct { type templateStorage struct {
*templateShared
// a session has a client and inflight of its own, see session
client *http.Client
inflight chan struct{}
release func()
}
// templateShared is what the sessions of a storage have in common
type templateShared struct {
conns *connPool
tmpl *storageTemplate tmpl *storageTemplate
tokenClient *http.Client client *http.Client
folder string folder string
secrets []string secrets []string
inflight chan struct{}
tokenMu sync.Mutex tokenMu sync.Mutex
token string token string
tokenExpiry time.Time tokenExpiry time.Time
@@ -110,35 +94,15 @@ func newTemplateStorage(streamSettings *internet.MemoryStreamConfig, config *Con
conc = maxTemplateConcurrency conc = maxTemplateConcurrency
} }
h1Hosts := new(sync.Map)
client := newServiceClient(context.Background(), h1Hosts, streamSettings, driveTimeout, conc)
return &templateStorage{ return &templateStorage{
templateShared: &templateShared{
conns: newConnPool(config.Xmux, func(connCtx context.Context) *http.Client {
return newServiceClient(connCtx, h1Hosts, streamSettings, driveTimeout, conc)
}),
tmpl: tmpl, tmpl: tmpl,
tokenClient: client, client: newServiceClient(streamSettings, driveTimeout, conc),
folder: config.RemoteFolder, folder: config.RemoteFolder,
secrets: config.Secrets, secrets: config.Secrets,
},
client: client,
inflight: make(chan struct{}, conc), inflight: make(chan struct{}, conc),
}, nil }, nil
} }
// session returns a storage for one session, over the connections of XMUX
// until Close, with the token of s
func (s *templateStorage) session() Storage {
transport := &sessionTransport{pool: s.conns}
return &templateStorage{
templateShared: s.templateShared,
client: &http.Client{Transport: transport},
inflight: make(chan struct{}, cap(s.inflight)),
release: transport.Close,
}
}
const maxTemplateConcurrency = 256 const maxTemplateConcurrency = 256
func (s *templateStorage) baseVars() map[string]string { func (s *templateStorage) baseVars() map[string]string {
@@ -252,7 +216,7 @@ func (s *templateStorage) accessToken(ctx context.Context) (string, error) {
} }
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := s.tokenClient.Do(req) resp, err := s.client.Do(req)
if err != nil { if err != nil {
return "", errors.New("failed to fetch the token").Base(err) return "", errors.New("failed to fetch the token").Base(err)
} }
@@ -298,11 +262,6 @@ func (s *templateStorage) do(ctx context.Context, op *opTemplate, vars map[strin
backoff := driveInitialBackoff backoff := driveInitialBackoff
var lastErr error var lastErr error
url := op.URL
if !op.presigned {
url = subst(url, vars)
}
for attempt := 0; attempt < driveMaxAttempts; attempt++ { for attempt := 0; attempt < driveMaxAttempts; attempt++ {
if attempt > 0 { if attempt > 0 {
select { select {
@@ -316,15 +275,11 @@ func (s *templateStorage) do(ctx context.Context, op *opTemplate, vars map[strin
} }
} }
var authHeaders map[string]string authHeaders, err := s.authHeaders(ctx, vars)
if !op.presigned {
headers, err := s.authHeaders(ctx, vars)
if err != nil { if err != nil {
lastErr = err lastErr = err
continue continue
} }
authHeaders = headers
}
method := op.Method method := op.Method
if method == "" { if method == "" {
@@ -335,7 +290,7 @@ func (s *templateStorage) do(ctx context.Context, op *opTemplate, vars map[strin
if body != nil { if body != nil {
reader = bytes.NewReader(body) reader = bytes.NewReader(body)
} }
req, err := http.NewRequestWithContext(ctx, method, url, reader) req, err := http.NewRequestWithContext(ctx, method, subst(op.URL, vars), reader)
if err != nil { if err != nil {
return 0, nil, errors.New("failed to build request").Base(err) return 0, nil, errors.New("failed to build request").Base(err)
} }
@@ -370,7 +325,7 @@ func (s *templateStorage) do(ctx context.Context, op *opTemplate, vars map[strin
continue continue
} }
if resp.StatusCode == http.StatusUnauthorized && s.tmpl.Auth.Type == "oauth2" && !op.presigned { if resp.StatusCode == http.StatusUnauthorized && s.tmpl.Auth.Type == "oauth2" {
s.invalidateToken() s.invalidateToken()
lastErr = errors.New("the service rejected the token") lastErr = errors.New("the service rejected the token")
continue continue
@@ -384,22 +339,6 @@ func (s *templateStorage) do(ctx context.Context, op *opTemplate, vars map[strin
return 0, nil, lastErr return 0, nil, lastErr
} }
// run performs op, following its href when the operation is two-step.
func (s *templateStorage) run(ctx context.Context, op *opTemplate, vars map[string]string, body []byte) (int, []byte, error) {
if op.HrefPath == "" {
return s.do(ctx, op, vars, body)
}
status, payload, err := s.do(ctx, op, vars, nil)
if err != nil || status < 200 || status >= 300 {
return status, payload, err
}
href := jsonString(payload, op.HrefPath)
if href == "" {
return 0, nil, errors.New("the response has no href at ", op.HrefPath)
}
return s.do(ctx, &opTemplate{Method: op.HrefMethod, URL: href, presigned: true}, vars, body)
}
func (s *templateStorage) Put(ctx context.Context, name string, data []byte) error { func (s *templateStorage) Put(ctx context.Context, name string, data []byte) error {
vars := s.baseVars() vars := s.baseVars()
vars["name"] = s.storedName(name) vars["name"] = s.storedName(name)
@@ -410,7 +349,7 @@ func (s *templateStorage) Put(ctx context.Context, name string, data []byte) err
body = []byte(subst(s.tmpl.Put.Body, vars)) body = []byte(subst(s.tmpl.Put.Body, vars))
} }
status, payload, err := s.run(ctx, &s.tmpl.Put, vars, body) status, payload, err := s.do(ctx, &s.tmpl.Put, vars, body)
if err != nil { if err != nil {
return err return err
} }
@@ -424,7 +363,7 @@ func (s *templateStorage) Get(ctx context.Context, name string) ([]byte, error)
vars := s.baseVars() vars := s.baseVars()
vars["name"] = s.storedName(name) vars["name"] = s.storedName(name)
status, payload, err := s.run(ctx, &s.tmpl.Get, vars, nil) status, payload, err := s.do(ctx, &s.tmpl.Get, vars, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -442,7 +381,7 @@ func (s *templateStorage) Delete(ctx context.Context, name string) error {
vars := s.baseVars() vars := s.baseVars()
vars["name"] = s.storedName(name) vars["name"] = s.storedName(name)
status, payload, err := s.run(ctx, &s.tmpl.Delete, vars, nil) status, payload, err := s.do(ctx, &s.tmpl.Delete, vars, nil)
if err != nil { if err != nil {
return err return err
} }
@@ -457,7 +396,7 @@ func (s *templateStorage) List(ctx context.Context, prefix string) ([]Entry, err
flat := s.storedName(prefix) flat := s.storedName(prefix)
vars["prefix"] = flat vars["prefix"] = flat
status, payload, err := s.run(ctx, &s.tmpl.List, vars, nil) status, payload, err := s.do(ctx, &s.tmpl.List, vars, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -502,8 +441,5 @@ func (s *templateStorage) List(ctx context.Context, prefix string) ([]Entry, err
} }
func (s *templateStorage) Close() error { func (s *templateStorage) Close() error {
if s.release != nil {
s.release()
}
return nil return nil
} }
+1 -117
View File
@@ -113,7 +113,7 @@ func templateSettings(store *fakeStore, auth map[string]interface{}, secrets []s
"delete": map[string]interface{}{"method": "DELETE", "url": base + "/folder/{name}"}, "delete": map[string]interface{}{"method": "DELETE", "url": base + "/folder/{name}"},
"list": map[string]interface{}{ "list": map[string]interface{}{
"method": "PROPFIND", "method": "PROPFIND",
"url": base + "/folder/?prefix={prefix}", "url": base + "/folder/",
"namesRegex": `<d:href>/folder/([^<]+)</d:href>`, "namesRegex": `<d:href>/folder/([^<]+)</d:href>`,
}, },
"retry": map[string]interface{}{"status": []int{429, 500, 502, 503}}, "retry": map[string]interface{}{"status": []int{429, 500, 502, 503}},
@@ -297,12 +297,6 @@ func TestTemplateConcurrency(t *testing.T) {
if cap(storage.inflight) != 4 { if cap(storage.inflight) != 4 {
t.Fatalf("inflight cap is %d, want 4 from the template", cap(storage.inflight)) t.Fatalf("inflight cap is %d, want 4 from the template", cap(storage.inflight))
} }
// and so has a session, of its own
session := storage.session().(*templateStorage)
defer session.Close()
if session.inflight == storage.inflight || cap(session.inflight) != 4 {
t.Fatalf("a session shares inflight or has cap %d, want 4", cap(session.inflight))
}
} }
func TestTemplateConcurrencyDefault(t *testing.T) { func TestTemplateConcurrencyDefault(t *testing.T) {
@@ -312,113 +306,3 @@ func TestTemplateConcurrencyDefault(t *testing.T) {
t.Fatalf("default inflight cap is %d, want %d", cap(storage.inflight), driveMaxInflight) t.Fatalf("default inflight cap is %d, want %d", cap(storage.inflight), driveMaxInflight)
} }
} }
// hrefStore mimics the Yandex Disk REST API: upload and download answer with an href
// to a presigned blob URL, which must be called without the API credentials.
type hrefStore struct {
*fakeStore
blobAuth string
}
func newHrefStore(t *testing.T) *hrefStore {
t.Helper()
store := &hrefStore{fakeStore: &fakeStore{objects: make(map[string][]byte)}}
store.server = httptest.NewServer(http.HandlerFunc(store.handle))
t.Cleanup(store.server.Close)
return store
}
func (s *hrefStore) handle(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/blob/") {
s.mu.Lock()
if auth := r.Header.Get("Authorization"); auth != "" {
s.blobAuth = auth
}
s.mu.Unlock()
r.URL.Path = "/folder/" + strings.TrimPrefix(r.URL.Path, "/blob/")
s.fakeStore.handle(w, r)
return
}
if r.Header.Get("Authorization") != "OAuth tok" {
w.WriteHeader(http.StatusUnauthorized)
return
}
name := r.URL.Query().Get("path")
switch r.URL.Path {
case "/upload":
case "/download":
s.mu.Lock()
_, ok := s.objects[name]
s.mu.Unlock()
if !ok {
w.WriteHeader(http.StatusNotFound)
return
}
default:
w.WriteHeader(http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(map[string]interface{}{"href": s.server.URL + "/blob/" + name})
}
func hrefSettings(store *hrefStore) *internet.MemoryStreamConfig {
base := store.server.URL
settings := templateSettings(store.fakeStore, map[string]interface{}{
"type": "static", "header": map[string]interface{}{"Authorization": "OAuth {secret0}"},
}, []string{"tok"})
cfg := settings.ProtocolSettings.(*Config)
var tmpl map[string]interface{}
json.Unmarshal([]byte(cfg.Template), &tmpl)
tmpl["put"] = map[string]interface{}{"url": base + "/upload?path={name}", "hrefPath": "href", "hrefMethod": "PUT"}
tmpl["get"] = map[string]interface{}{"url": base + "/download?path={name}", "hrefPath": "href"}
tmpl["list"] = map[string]interface{}{"method": "PROPFIND", "url": base + "/blob/", "namesRegex": `<d:href>/folder/([^<]+)</d:href>`}
tmpl["delete"] = map[string]interface{}{"method": "DELETE", "url": base + "/blob/{name}"}
raw, _ := json.Marshal(tmpl)
cfg.Template = string(raw)
return settings
}
func TestTemplateTwoStep(t *testing.T) {
store := newHrefStore(t)
settings := hrefSettings(store)
storage, err := newTemplateStorage(settings, settings.ProtocolSettings.(*Config))
if err != nil {
t.Fatalf("newTemplateStorage: %v", err)
}
ctx := context.Background()
if err := storage.Put(ctx, "streams/abc/c2s/000000000.seg", []byte("hello")); err != nil {
t.Fatalf("Put: %v", err)
}
data, err := storage.Get(ctx, "streams/abc/c2s/000000000.seg")
if err != nil {
t.Fatalf("Get: %v", err)
}
if string(data) != "hello" {
t.Fatalf("Get returned %q, want hello", data)
}
if _, err := storage.Get(ctx, "streams/abc/c2s/000000009.seg"); err != errNotFound {
t.Fatalf("Get of a missing object returned %v, want errNotFound", err)
}
if store.blobAuth != "" {
t.Fatalf("the presigned href received credentials %q", store.blobAuth)
}
}
func TestTemplateTwoStepTransport(t *testing.T) {
store := newHrefStore(t)
client, server, cleanup := pairWith(t, hrefSettings(store))
defer cleanup()
if _, err := client.Write([]byte("ping")); err != nil {
t.Fatalf("client write: %v", err)
}
expectRead(t, server, "ping")
if _, err := server.Write([]byte("pong")); err != nil {
t.Fatalf("server write: %v", err)
}
expectRead(t, client, "pong")
}
-4
View File
@@ -230,10 +230,6 @@ func newWALReader(ctx context.Context, storage Storage, prefix string, p params)
return r return r
} }
func (r *walReader) chunks() <-chan []byte {
return r.ch
}
func (r *walReader) Wake() { func (r *walReader) Wake() {
select { select {
case r.wake <- struct{}{}: case r.wake <- struct{}{}:
+9 -39
View File
@@ -4,7 +4,6 @@ import (
"context" "context"
"crypto/rand" "crypto/rand"
"encoding/hex" "encoding/hex"
"encoding/json"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
@@ -88,7 +87,6 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me
if err != nil { if err != nil {
return nil, err return nil, err
} }
storage = sessionStorage(storage)
session, err := newSessionID() session, err := newSessionID()
if err != nil { if err != nil {
@@ -96,33 +94,15 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me
return nil, err return nil, err
} }
p := paramsFromConfig(config) if err := storage.Put(ctx, announceName(session, time.Now()), nil); err != nil {
var announce []byte
var r ring
slots, ok := storage.(slotStore)
if ok {
if r, err = makeRing(ctx, slots, session, p); err == nil {
announce, _ = json.Marshal(r)
} else {
ok = false
errors.LogWarningInner(ctx, err, "failed to make the slots of session ", session, ", it goes in segments")
}
}
if err := storage.Put(ctx, announceName(session, time.Now()), announce); err != nil {
storage.Close() storage.Close()
return nil, errors.New("failed to announce session ", session).Base(err) return nil, errors.New("failed to announce session ", session).Base(err)
} }
if ok {
errors.LogInfo(ctx, "opened session ", session, " in ", len(r.Up.Slots), " slots")
return newSlotConn(context.Background(), slots, r.Up, r.Down, p, func() {
storage.Close()
}), nil
}
errors.LogInfo(ctx, "opened session ", session) errors.LogInfo(ctx, "opened session ", session)
return newConn(context.Background(), storage, return newConn(context.Background(), storage,
uplinkPrefix(session), downlinkPrefix(session), p, func() { uplinkPrefix(session), downlinkPrefix(session), paramsFromConfig(config), func() {
storage.Close() storage.Close()
}), nil }), nil
} }
@@ -225,8 +205,9 @@ func (l *Listener) acceptPending(logCtx context.Context) (bool, error) {
continue continue
} }
go l.drop(full) go l.drop(full)
errors.LogInfo(logCtx, "accepted session ", session)
accepted = true accepted = true
l.addConn(l.newSessionConn(logCtx, session, listed.Inline)) l.addConn(l.newSessionConn(session))
} }
return accepted, nil return accepted, nil
} }
@@ -246,24 +227,13 @@ func (l *Listener) claim(session string) bool {
return true return true
} }
// newSessionConn takes a session in the slots that its announcement has, or func (l *Listener) newSessionConn(session string) *Conn {
// in segments if it has none return newConn(l.ctx, l.storage,
func (l *Listener) newSessionConn(logCtx context.Context, session string, announce []byte) *Conn { downlinkPrefix(session), uplinkPrefix(session), l.params, func() {
storage := sessionStorage(l.storage)
onClose := func() {
storage.Close()
l.mu.Lock() l.mu.Lock()
delete(l.active, session) delete(l.active, session)
l.mu.Unlock() l.mu.Unlock()
} })
if slots, ok := storage.(slotStore); ok {
if r, ok := decodeRing(announce); ok {
errors.LogInfo(logCtx, "accepted session ", session, " in ", len(r.Down.Slots), " slots")
return newSlotConn(l.ctx, slots, r.Down, r.Up, l.params, onClose)
}
}
errors.LogInfo(logCtx, "accepted session ", session)
return newConn(l.ctx, storage, downlinkPrefix(session), uplinkPrefix(session), l.params, onClose)
} }
func (l *Listener) collectLoop(logCtx context.Context) { func (l *Listener) collectLoop(logCtx context.Context) {
-390
View File
@@ -4,21 +4,16 @@ import (
"bytes" "bytes"
"context" "context"
"crypto/rand" "crypto/rand"
gotls "crypto/tls"
"io" "io"
"net/http"
"os" "os"
"strings" "strings"
"sync"
"sync/atomic" "sync/atomic"
"testing" "testing"
"time" "time"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/splithttp"
"github.com/xtls/xray-core/transport/internet/stat" "github.com/xtls/xray-core/transport/internet/stat"
"github.com/xtls/xray-core/transport/internet/tls"
) )
const testPatience = 30 * time.Second const testPatience = 30 * time.Second
@@ -586,388 +581,3 @@ func TestInlinePayload(t *testing.T) {
t.Fatalf("called Get %d times for an inline payload", got) t.Fatalf("called Get %d times for an inline payload", got)
} }
} }
// runSessions opens n sessions at once, both ends in this process, passes data
// both ways over each and closes them
func runSessions(t *testing.T, streamSettings *internet.MemoryStreamConfig, n int) {
t.Helper()
accepted := make(chan stat.Connection, 1)
listener, err := Serve(context.Background(), net.LocalHostIP, net.Port(0), streamSettings, func(conn stat.Connection) {
accepted <- conn
})
if err != nil {
t.Fatalf("Serve: %v", err)
}
defer listener.Close()
var ends []stat.Connection
defer func() {
for _, end := range ends {
end.Close()
}
}()
for range n {
client, err := Dial(context.Background(), net.Destination{}, streamSettings)
if err != nil {
t.Fatalf("Dial: %v", err)
}
ends = append(ends, client)
select {
case peer := <-accepted:
ends = append(ends, peer)
case <-time.After(testPatience):
t.Fatal("listener did not accept the session")
}
}
for i := 0; i < len(ends); i += 2 {
if _, err := ends[i].Write([]byte("ping")); err != nil {
t.Fatalf("client write: %v", err)
}
expectRead(t, ends[i+1], "ping")
if _, err := ends[i+1].Write([]byte("pong")); err != nil {
t.Fatalf("server write: %v", err)
}
expectRead(t, ends[i], "pong")
}
}
// sessionEnd is the session and end of it that a request to the fake store
// comes from, or "" for the listener and the token
func sessionEnd(r *http.Request) string {
name := strings.TrimPrefix(r.URL.Path, "/folder/")
if r.Method == "PROPFIND" {
name = r.URL.Query().Get("prefix")
}
write := r.Method == http.MethodPut
parts := strings.Split(name, flatSeparator)
switch {
case len(parts) > 2 && parts[0] == streamsDir:
// each end writes its own direction and reads the other
if (parts[2] == uplinkDir) == write {
return parts[1] + " client"
}
return parts[1] + " server"
case len(parts) == 2 && parts[0] == sessionsDir && write:
_, session, _ := strings.Cut(parts[1], "-")
return session + " client"
}
return ""
}
// with "maxConcurrency" 1 every end of a session has a connection of its own,
// with "maxConnections" the ends share that many, and the sessions after them
// take the same ones again. The listener and the token use none of them, and
// the sessions get the token once
func TestXmuxConnections(t *testing.T) {
const n = 3
for _, c := range []struct {
name string
xmux *splithttp.XmuxConfig
conns int
shared bool
}{
{"maxConcurrency", &splithttp.XmuxConfig{MaxConcurrency: &splithttp.RangeConfig{From: 1, To: 1}}, 2 * n, false},
{"maxConnections", &splithttp.XmuxConfig{MaxConnections: &splithttp.RangeConfig{From: 2, To: 2}}, 2, true},
} {
t.Run(c.name, func(t *testing.T) {
resetSharedStorage()
t.Cleanup(resetSharedStorage)
store := &fakeStore{objects: make(map[string][]byte), needAuth: "Bearer tok-fake"}
var (
mu sync.Mutex
conns = make(map[string]map[string]bool)
)
server := newTLSServer(t, &gotls.Config{NextProtos: chromeALPN}, func(w http.ResponseWriter, r *http.Request) {
end := sessionEnd(r)
mu.Lock()
if conns[end] == nil {
conns[end] = make(map[string]bool)
}
conns[end][r.RemoteAddr] = true
mu.Unlock()
store.handle(w, r)
})
store.server = server.Server
settings := templateSettings(store, map[string]interface{}{
"type": "oauth2",
"tokenUrl": server.URL + "/token",
"form": map[string]interface{}{"grant_type": "refresh_token", "refresh_token": "{secret0}"},
"header": map[string]interface{}{"Authorization": "Bearer {token}"},
}, []string{"refresh"})
settings.SecuritySettings = server.settings(&tls.Config{}).SecuritySettings
settings.ProtocolSettings.(*Config).Xmux = c.xmux
runSessions(t, settings, n)
mu.Lock()
first := conns
conns = make(map[string]map[string]bool)
mu.Unlock()
dialed := server.conns.Load()
runSessions(t, settings, n)
if got := server.conns.Load(); got != dialed {
t.Errorf("the next %d sessions dialed %d connections", n, got-dialed)
}
owner := make(map[string]string)
for end, addrs := range first {
if end == "" {
continue
}
if len(addrs) != 1 {
t.Errorf("%s used %d connections", end, len(addrs))
}
for addr := range addrs {
if owner[addr] != "" && !c.shared {
t.Errorf("%s and %s shared a connection", end, owner[addr])
}
owner[addr] = end
}
}
if len(owner) != c.conns {
t.Errorf("%d sessions had %d connections, want %d", 2*n, len(owner), c.conns)
}
for addr := range first[""] {
if owner[addr] != "" {
t.Errorf("the listener or the token went over the connection of %s", owner[addr])
}
}
store.mu.Lock()
defer store.mu.Unlock()
if store.tokens != 1 {
t.Errorf("the sessions got the token %d times", store.tokens)
}
})
}
}
func TestDriveXmuxConnections(t *testing.T) {
drive := newFakeDrive(t)
server := newTLSServer(t, &gotls.Config{NextProtos: chromeALPN}, drive.server.Config.Handler.ServeHTTP)
driveTokenURL = server.URL + "/token"
driveFilesURL = server.URL + "/files"
driveUploadURL = server.URL + "/upload"
settings := driveSettings()
settings.SecuritySettings = server.settings(&tls.Config{}).SecuritySettings
settings.ProtocolSettings.(*Config).Xmux = &splithttp.XmuxConfig{MaxConnections: &splithttp.RangeConfig{From: 3, To: 3}}
runSessions(t, settings, 3)
if got := server.conns.Load(); got != 4 {
t.Errorf("6 ends of sessions with 3 connections and the listener took %d connections", got)
}
drive.mu.Lock()
defer drive.mu.Unlock()
if drive.tokens != 1 {
t.Errorf("6 ends of sessions got the token %d times", drive.tokens)
}
}
// a session knows the hosts that answered the storage over h1, the polling of
// the listener for one, so it sends requests in a row over one connection, not
// first over one for h2 that then stays idle, and parallel ones over more
func TestSessionKnowsHTTP1Hosts(t *testing.T) {
check := func(name string, server *tlsServer, storage Storage) {
put := func(s Storage, names ...string) {
for _, n := range names {
if err := s.Put(context.Background(), "streams/abc/c2s/"+n, []byte("x")); err != nil {
t.Errorf("%s: Put: %v", name, err)
}
}
}
put(storage, "a", "b")
before := server.conns.Load()
session := sessionStorage(storage)
defer session.Close()
put(session, "c", "d")
if n := server.conns.Load() - before; n != 1 {
t.Errorf("%s: 2 requests in a row of a session over h1 took %d connections", name, n)
}
before = server.conns.Load()
var wg sync.WaitGroup
for _, n := range []string{"e", "f", "g", "h"} {
wg.Go(func() { put(session, n) })
}
wg.Wait()
if server.conns.Load() == before {
t.Errorf("%s: 4 parallel requests of a session over h1 took one connection", name)
}
}
slow := func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
time.Sleep(20 * time.Millisecond)
next(w, r)
}
}
store := &fakeStore{objects: make(map[string][]byte)}
server := newTLSServer(t, &gotls.Config{NextProtos: []string{"http/1.1"}}, slow(store.handle))
store.server = server.Server
settings := templateSettings(store, map[string]interface{}{"type": "none"}, nil)
settings.SecuritySettings = server.settings(&tls.Config{}).SecuritySettings
template, err := newTemplateStorage(settings, settings.ProtocolSettings.(*Config))
if err != nil {
t.Fatalf("newTemplateStorage: %v", err)
}
check("template", server, template)
drive := newFakeDrive(t)
server = newTLSServer(t, &gotls.Config{NextProtos: []string{"http/1.1"}}, slow(drive.server.Config.Handler.ServeHTTP))
driveTokenURL = server.URL + "/token"
driveFilesURL = server.URL + "/files"
settings = driveSettings()
settings.SecuritySettings = server.settings(&tls.Config{}).SecuritySettings
google, err := newDriveStorage(settings, settings.ProtocolSettings.(*Config))
if err != nil {
t.Fatalf("newDriveStorage: %v", err)
}
check("drive", server, google)
}
// with "hMaxRequestTimes" a session moves on to a new connection after that
// many requests, and the old one closes once the requests on it end, not before
func TestXmuxRequestLimit(t *testing.T) {
store := &fakeStore{objects: make(map[string][]byte)}
arrived := make(chan struct{}, 1)
release := make(chan struct{})
server := newTLSServer(t, &gotls.Config{NextProtos: chromeALPN}, func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
arrived <- struct{}{}
<-release
}
store.handle(w, r)
})
store.server = server.Server
settings := templateSettings(store, map[string]interface{}{"type": "none"}, nil)
settings.SecuritySettings = server.settings(&tls.Config{}).SecuritySettings
settings.ProtocolSettings.(*Config).Xmux = &splithttp.XmuxConfig{HMaxRequestTimes: &splithttp.RangeConfig{From: 3, To: 3}}
storage, err := newTemplateStorage(settings, settings.ProtocolSettings.(*Config))
if err != nil {
t.Fatalf("newTemplateStorage: %v", err)
}
session := sessionStorage(storage)
defer session.Close()
put := func(names ...string) {
for _, name := range names {
if err := session.Put(context.Background(), "streams/abc/c2s/"+name, []byte("x")); err != nil {
t.Fatalf("Put: %v", err)
}
}
}
put("a")
got := make(chan error, 1)
go func() {
_, err := session.Get(context.Background(), "streams/abc/c2s/a")
got <- err
}()
<-arrived
put("b", "c")
if n := server.conns.Load(); n != 2 {
t.Errorf("4 requests with 3 to a connection took %d connections", n)
}
if server.closed.Load() != 0 {
t.Error("the first connection closed under a request")
}
close(release)
if err := <-got; err != nil {
t.Errorf("Get on the first connection: %v", err)
}
waitFor(t, "the first connection to close", func() bool { return server.closed.Load() == 1 })
put("d", "e", "f")
if n := server.conns.Load(); n != 3 {
t.Errorf("7 requests with 3 to a connection took %d connections", n)
}
waitFor(t, "the second connection to close", func() bool { return server.closed.Load() == 2 })
}
// a session that fails to announce itself lets go of its connection, which the
// next one takes with "maxConcurrency" 1
func TestFailedDialReleasesConnection(t *testing.T) {
resetSharedStorage()
t.Cleanup(resetSharedStorage)
store := &fakeStore{objects: make(map[string][]byte)}
var refuse atomic.Bool
refuse.Store(true)
server := newTLSServer(t, &gotls.Config{NextProtos: chromeALPN}, func(w http.ResponseWriter, r *http.Request) {
if refuse.Load() {
w.WriteHeader(http.StatusUnauthorized)
return
}
store.handle(w, r)
})
store.server = server.Server
settings := templateSettings(store, map[string]interface{}{"type": "none"}, nil)
settings.SecuritySettings = server.settings(&tls.Config{}).SecuritySettings
settings.ProtocolSettings.(*Config).Xmux = &splithttp.XmuxConfig{MaxConcurrency: &splithttp.RangeConfig{From: 1, To: 1}}
if _, err := Dial(context.Background(), net.Destination{}, settings); err == nil {
t.Fatal("Dial announced a session that the service refused")
}
refuse.Store(false)
conn, err := Dial(context.Background(), net.Destination{}, settings)
if err != nil {
t.Fatalf("Dial: %v", err)
}
defer conn.Close()
if n := server.conns.Load(); n != 1 {
t.Errorf("a failed Dial and the next took %d connections", n)
}
}
// a session gets the token over a connection of the storage, not over one of
// XMUX
func TestSessionTokenConnection(t *testing.T) {
check := func(name string, server *tlsServer, storage Storage) {
session := sessionStorage(storage)
defer session.Close()
if err := session.Put(context.Background(), "sessions/abc", nil); err != nil {
t.Fatalf("%s: Put: %v", name, err)
}
if n := server.conns.Load(); n != 2 {
t.Errorf("%s: the token and a Put of a session took %d connections, want 2", name, n)
}
}
store := &fakeStore{objects: make(map[string][]byte), needAuth: "Bearer tok-fake"}
server := newTLSServer(t, &gotls.Config{NextProtos: chromeALPN}, store.handle)
store.server = server.Server
settings := templateSettings(store, map[string]interface{}{
"type": "oauth2",
"tokenUrl": server.URL + "/token",
"header": map[string]interface{}{"Authorization": "Bearer {token}"},
}, nil)
settings.SecuritySettings = server.settings(&tls.Config{}).SecuritySettings
template, err := newTemplateStorage(settings, settings.ProtocolSettings.(*Config))
if err != nil {
t.Fatalf("newTemplateStorage: %v", err)
}
check("template", server, template)
drive := newFakeDrive(t)
server = newTLSServer(t, &gotls.Config{NextProtos: chromeALPN}, drive.server.Config.Handler.ServeHTTP)
driveTokenURL = server.URL + "/token"
driveFilesURL = server.URL + "/files"
settings = driveSettings()
settings.SecuritySettings = server.settings(&tls.Config{}).SecuritySettings
google, err := newDriveStorage(settings, settings.ProtocolSettings.(*Config))
if err != nil {
t.Fatalf("newDriveStorage: %v", err)
}
check("drive", server, google)
}
// local storage has no connections to keep apart
func TestLocalSessionStorage(t *testing.T) {
storage, err := newLocalStorage(t.TempDir())
if err != nil {
t.Fatalf("newLocalStorage: %v", err)
}
if sessionStorage(storage) != Storage(storage) {
t.Fatal("a session of local storage got a storage of its own")
}
}