Compare commits

..
Author SHA1 Message Date
RPRXandGitHub 00bb0dc737 Update proxy.go 2026-11-08 03:01:30 +00:00
patternihaandClaude Opus 5.5 761d8032db TUN inbound: Log "unable to get the interface" for an ignored IPv6 interface error on Windows
Like the other IPv6 steps, so that an ignored missing IPv6 interface no
longer logs as a bare "Element not found."

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 07:11:54 +03:30
patternihaandClaude Opus 5.5 4296be5d05 TUN inbound: Only log IPv6 setup errors on Windows when DisabledComponents turns IPv6 off, with "misconfigtun" blocking IPv6 then
https://github.com/XTLS/Xray-core/pull/7341#issuecomment-6700092032

IPv6 entries are no longer left out. With IPv6 turned off by
DisabledComponents (0x10, the non-tunnel interfaces, the TUN among them,
as in 0xFF), IPv6 is set up as usual, and errors doing so, clearing its
DNS servers included, are only logged; otherwise they exit, also for a
config without IPv6 entries, which replaces the skips of #6874, #7325 and
#7332. "misconfigtun" then blocks IPv6 outside the TUN whatever the routes,
as IPv6 still works until the reboot that applies the value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 05:08:06 +03:30
patternihaandClaude Opus 5.5 4bcd0c6405 TUN inbound: Recommend at least "dns" and "misconfigtun" for autoSystemWfpBlockLeak
https://github.com/XTLS/Xray-core/pull/7332#issuecomment-6683422958

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 00:00:06 +03:30
patternihaandClaude Opus 5.5 8fe26a91d5 TUN inbound: Ignore IPv6 entries on Windows when DisabledComponents turns IPv6 off on all interfaces
https://github.com/XTLS/Xray-core/pull/7332#issuecomment-6676374920

With IPv6 off on all interfaces by DisabledComponents (0x01 and 0x10 both
set, as in 0xFF), the IPv6 entries of gateway, dns and
autoSystemRoutingTable are ignored and the TUN starts with IPv4 only, so
one config works either way. The value takes effect at the next boot only,
so ignored IPv6 routes add "[::/0]:misconfigtun" to autoSystemWfpBlockLeak:
until the reboot, IPv6 the TUN would have taken cannot leave outside it,
also through interfaces connected later.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 23:53:26 +03:30
1c8bf4067b XTLS Vision: Serialize VisionWriter writes from concurrent mux sessions (#7337)
https://github.com/XTLS/Xray-core/pull/7337#issuecomment-6692618802

Fixes https://github.com/XTLS/Xray-core/issues/7336

---------

Co-authored-by: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-11-06 18:45:23 +00:00
28 changed files with 133 additions and 2225 deletions
+1 -1
View File
@@ -82,7 +82,7 @@ jobs:
uses: docker/setup-buildx-action@v4
- name: Login to GitHub Container Registry
uses: docker/login-action@v4.8.0
uses: docker/login-action@v4.7.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
+1 -1
View File
@@ -212,7 +212,7 @@ jobs:
# go build -o build_assets/wxray.exe -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-H windowsgui -X github.com/xtls/xray-core/core.build=${COMMID} -s -w -buildid=" -v ./main
elif [[ ${GOOS} == 'android' ]]; then
echo 'Building Xray for Android...'
go build -o build_assets/xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-X github.com/xtls/xray-core/core.build=${COMMID} -s -w -buildid= -checklinkname=0 -extldflags=-Wl,--pack-dyn-relocs=android" -v ./main
go build -o build_assets/xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-X github.com/xtls/xray-core/core.build=${COMMID} -s -w -buildid= -checklinkname=0" -v ./main
else
echo 'Building Xray...'
if [[ ${GOARCH} == 'mips' || ${GOARCH} == 'mipsle' ]]; then
+2 -2
View File
@@ -234,8 +234,8 @@ CGO_ENABLED=0 go build -o xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ld
For Android:
```bash
GOOS=android GOARCH=arm64 CGO_ENABLED=1 CC=/path/to/aarch64-linux-android24-clang go build -o xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-X github.com/xtls/xray-core/core.build=REPLACE -s -w -buildid= -checklinkname=0 -extldflags=-Wl,--pack-dyn-relocs=android" -v ./main
GOOS=android GOARCH=amd64 CGO_ENABLED=1 CC=/path/to/x86_64-linux-android24-clang go build -o xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-X github.com/xtls/xray-core/core.build=REPLACE -s -w -buildid= -checklinkname=0 -extldflags=-Wl,--pack-dyn-relocs=android" -v ./main
GOOS=android GOARCH=arm64 CGO_ENABLED=1 CC=/path/to/aarch64-linux-android24-clang go build -o xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-X github.com/xtls/xray-core/core.build=REPLACE -s -w -buildid= -checklinkname=0" -v ./main
GOOS=android GOARCH=amd64 CGO_ENABLED=1 CC=/path/to/x86_64-linux-android24-clang go build -o xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-X github.com/xtls/xray-core/core.build=REPLACE -s -w -buildid= -checklinkname=0" -v ./main
```
If you are compiling a 32-bit MIPS/MIPSLE target, use this command instead:
-1
View File
@@ -188,7 +188,6 @@ var (
// NewClientWorker creates a new mux.Client.
func NewClientWorker(stream transport.Link, s ClientStrategy) (*ClientWorker, error) {
stream.Writer = &MutexWriter{Writer: stream.Writer}
c := &ClientWorker{
sessionManager: NewSessionManager(),
link: stream,
+1 -1
View File
@@ -95,7 +95,7 @@ type ServerWorker struct {
func NewServerWorker(ctx context.Context, d routing.Dispatcher, link *transport.Link) (*ServerWorker, error) {
worker := &ServerWorker{
dispatcher: d,
link: &transport.Link{Reader: link.Reader, Writer: &MutexWriter{Writer: link.Writer}},
link: link,
sessionManager: NewSessionManager(),
done: done.New(),
timer: time.NewTicker(60 * time.Second),
-13
View File
@@ -1,8 +1,6 @@
package mux
import (
"sync"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/net"
@@ -11,17 +9,6 @@ import (
"github.com/xtls/xray-core/common/session"
)
type MutexWriter struct {
sync.Mutex
buf.Writer
}
func (w *MutexWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
w.Lock()
defer w.Unlock()
return w.Writer.WriteMultiBuffer(mb)
}
type Writer struct {
dest net.Destination
writer buf.Writer
+2 -2
View File
@@ -19,8 +19,8 @@ import (
var (
Version_x byte = 26
Version_y byte = 11
Version_z byte = 9
Version_y byte = 10
Version_z byte = 31
)
var (
+4 -4
View File
@@ -16,11 +16,11 @@ require (
github.com/pelletier/go-toml v1.9.5
github.com/pion/stun/v3 v3.1.7
github.com/pires/go-proxyproto v0.16.0
github.com/refraction-networking/utls v1.8.3-0.20261108164504-a13c59ffe98b
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af
github.com/robfig/cron/v3 v3.0.1
github.com/stretchr/testify v1.12.1
github.com/vishvananda/netlink v1.3.1
github.com/xtls/reality v0.0.0-20261108101329-4bb46b2afd8b
github.com/xtls/reality v0.0.0-20261014101318-83f621788ca8
github.com/yuin/gopher-lua v1.1.2
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.58.0
@@ -41,14 +41,14 @@ require (
)
require (
github.com/andybalholm/brotli v1.2.3 // indirect
github.com/google/gopacket v1.1.19 // indirect
github.com/huin/goupnp v1.2.0 // indirect
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
github.com/juju/ratelimit v1.0.2 // indirect
github.com/klauspost/compress v1.20.1 // indirect
github.com/klauspost/compress v1.20.0 // indirect
github.com/koron/go-ssdp v0.0.4 // indirect
github.com/libp2p/go-netroute v0.2.1 // indirect
github.com/molecule-man/go-brrr v1.2.0 // indirect
github.com/pion/dtls/v3 v3.1.5 // indirect
github.com/pion/logging v0.2.4 // indirect
github.com/pion/transport/v4 v4.1.0 // indirect
+10 -8
View File
@@ -1,3 +1,5 @@
github.com/andybalholm/brotli v1.2.3 h1:8H1qwOkl2LPfjf3YezB90JnCliZb6SInJ/OJkEbA5NQ=
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/go.mod h1:x7qxEvX6MCVtDuBKHj3E+88+BtrbEMuAL5qGUKItjW8=
github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
@@ -31,8 +33,8 @@ github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7Bd
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/klauspost/cpuid/v2 v2.4.2 h1:xRw7IZEsAgVCp6trS/DgWbTAqpixhmywgVgBjYGg1bQ=
github.com/klauspost/cpuid/v2 v2.4.2/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0=
@@ -47,8 +49,6 @@ github.com/libp2p/go-netroute v0.2.1 h1:V8kVrpD8GK0Riv15/7VN6RbUQ3URNZVosw7H2v9t
github.com/libp2p/go-netroute v0.2.1/go.mod h1:hraioZr0fhBjG0ZRXJJ6Zj2IVEVNx6tDTFQfSmcq7mQ=
github.com/miekg/dns v1.1.73 h1:uhT8nJxmTrPJYClxVxTCX+CVn6qnzSiybRk72Z6DgrE=
github.com/miekg/dns v1.1.73/go.mod h1:RW2Obtfd5NZHvOFe3zYG0W8koWOQtAzyHaLo8vASBuQ=
github.com/molecule-man/go-brrr v1.2.0 h1:dOJU45BC3Gc2WXoxAW0rgCAgVuu2gruAOGncfieA0Jw=
github.com/molecule-man/go-brrr v1.2.0/go.mod h1:7ybW6/7gA3oKY45jOfVNjSJDtrr6ea4tzbsTkjmQDC4=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc=
@@ -67,8 +67,8 @@ github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4
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/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/refraction-networking/utls v1.8.3-0.20261108164504-a13c59ffe98b h1:Mi/DuaTxHeNIQ9g1ylZicAqa6Cb9wkHRiNtQH9uZ7Ts=
github.com/refraction-networking/utls v1.8.3-0.20261108164504-a13c59ffe98b/go.mod h1:rc/ctWeKlh9LSk5k90KlSPcwF7P6D31mn/9GI2NAfBI=
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af h1:er2acxbi3N1nvEq6HXHUAR1nTWEJmQfqiGR8EVT9rfs=
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.16.0 h1:O9DK+vNMDVGLr2BeZqmpLeMjiMNkuXfcqntWbZV6S5g=
@@ -81,8 +81,10 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/xtls/reality v0.0.0-20261108101329-4bb46b2afd8b h1:fAn11r21QiUY2oAGwENWb5ummjhWwMcs2WzkbjDMh+k=
github.com/xtls/reality v0.0.0-20261108101329-4bb46b2afd8b/go.mod h1:ptlSkJ8k/7IDauNH0SA96JPOo+nYGLTv2Y3UMpwvAsY=
github.com/xtls/reality v0.0.0-20261014101318-83f621788ca8 h1:RjLnVvlzK4q6zWaSBPNRGsrgx2fbJDeUWffjw574uzs=
github.com/xtls/reality v0.0.0-20261014101318-83f621788ca8/go.mod h1:ptlSkJ8k/7IDauNH0SA96JPOo+nYGLTv2Y3UMpwvAsY=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
github.com/yuin/gopher-lua v1.1.2 h1:yF/FjE3hD65tBbt0VXLE13HWS9h34fdzJmrWRXwobGA=
-1
View File
@@ -275,7 +275,6 @@ var ModernFingerprints = map[string]*utls.ClientHelloID{
"hellochrome_120": &utls.HelloChrome_120,
"hellochrome_131": &utls.HelloChrome_131,
"hellochrome_133": &utls.HelloChrome_133,
"hellochrome_155": &utls.HelloChrome_155,
"helloios_13": &utls.HelloIOS_13,
"helloios_14": &utls.HelloIOS_14,
"helloedge_106": &utls.HelloEdge_106,
+1 -19
View File
@@ -433,10 +433,6 @@ func (b *doneBody) Close() error {
return err
}
// a connection that has not answered yet is given this long before its
// timeouts retire it, so that a dial or handshake is not cut short again
var stalledGrace = 12 * time.Second
// 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 {
@@ -447,8 +443,6 @@ type sessionTransport struct {
closed bool
slowAt, retiredAt time.Time
movedAt time.Time
takenAt time.Time
served bool
}
func (t *sessionTransport) RoundTrip(r *http.Request) (*http.Response, error) {
@@ -469,7 +463,6 @@ func (t *sessionTransport) RoundTrip(r *http.Request) (*http.Response, error) {
t.movedAt = time.Now()
}
client = t.client
t.takenAt, t.served = time.Now(), false
}
// counted under mu, or another request of the session that moves on could
// close the connection before this one is sent
@@ -478,15 +471,7 @@ func (t *sessionTransport) RoundTrip(r *http.Request) (*http.Response, error) {
conn.inflight++
conn.mu.Unlock()
t.mu.Unlock()
resp, err := conn.roundTrip(r)
if err == nil {
t.mu.Lock()
if t.client == client {
t.served = true
}
t.mu.Unlock()
}
return resp, err
return conn.roundTrip(r)
}
func (t *sessionTransport) stalled(began time.Time) {
@@ -496,9 +481,6 @@ func (t *sessionTransport) stalled(began time.Time) {
return
}
now := time.Now()
if !t.served && now.Sub(t.takenAt) < stalledGrace {
return
}
slow := now.Sub(t.slowAt) < 3*time.Second
t.slowAt = now
if !slow || t.client == nil || now.Sub(t.retiredAt) < 2*time.Second {
-54
View File
@@ -1006,9 +1006,6 @@ func (okConn) RoundTrip(r *http.Request) (*http.Response, error) {
}
func TestSessionRetiresStalledConn(t *testing.T) {
oldGrace := stalledGrace
stalledGrace = 0
t.Cleanup(func() { stalledGrace = oldGrace })
made := atomic.Int32{}
transport := &sessionTransport{pool: newConnPool(nil, func(connCtx context.Context) *http.Client {
if made.Add(1) == 1 {
@@ -1090,54 +1087,3 @@ func TestSessionIgnoresStallOfRetiredConn(t *testing.T) {
t.Fatalf("a stall of the current connection made %d connections, want it retired", n)
}
}
func TestSessionSparesFreshConn(t *testing.T) {
transport := &sessionTransport{pool: newConnPool(nil, func(connCtx context.Context) *http.Client {
return &http.Client{Transport: okConn{}}
})}
send := func() {
req, err := http.NewRequest(http.MethodGet, "http://drive.invalid/files", nil)
if err != nil {
t.Fatal(err)
}
resp, err := transport.RoundTrip(req)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
}
current := func() *poolConn {
transport.mu.Lock()
defer transport.mu.Unlock()
return transport.client.XmuxConn.(*poolConn)
}
send()
conn := current()
transport.mu.Lock()
transport.takenAt, transport.served = time.Now(), false
transport.mu.Unlock()
transport.stalled(time.Now())
transport.stalled(time.Now())
if conn.IsClosed() {
t.Fatal("a fresh connection was retired before its grace period ended")
}
transport.mu.Lock()
transport.takenAt = time.Now().Add(-stalledGrace - time.Second)
transport.mu.Unlock()
transport.stalled(time.Now())
transport.stalled(time.Now())
if !conn.IsClosed() {
t.Fatal("a fresh connection older than its grace period was not retired")
}
send()
conn = current()
transport.mu.Lock()
transport.takenAt, transport.served = time.Now(), true
transport.slowAt, transport.retiredAt = time.Time{}, time.Time{}
transport.mu.Unlock()
transport.stalled(time.Now())
transport.stalled(time.Now())
if !conn.IsClosed() {
t.Fatal("a connection that had answered was not retired")
}
}
+2 -2
View File
@@ -47,12 +47,12 @@ func newConn(ctx context.Context, storage Storage, writePrefix, readPrefix strin
}
}
func newSlotConn(ctx context.Context, store slotStore, write, read slotTable, p params, first []byte, onClose func()) *Conn {
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, first),
reader: newSlotReader(ctx, store, read, p),
onClose: onClose,
}
}
+36 -209
View File
@@ -11,7 +11,6 @@ import (
"net/url"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/xtls/xray-core/common/dice"
@@ -19,9 +18,6 @@ import (
"github.com/xtls/xray-core/transport/internet"
)
// testDriveIO is a nil-by-default test hook observing every Drive request
var testDriveIO func(method, target string, body []byte)
const (
flatSeparator = "~"
@@ -44,24 +40,8 @@ var (
driveCooldown = 2 * time.Second
driveSlotRead = 3 * time.Second
driveSlotWrite = 5 * time.Second
tokenRefreshBackoff = 30 * time.Second
maxTokenBackoff = 5 * time.Minute
driveMaxResponse = 32 << 20
driveMaxAnnounce = 1 << 20
)
// maxBodyKey lets a caller tighten the response-body cap (announcements).
type maxBodyKey struct{}
// tokenFlight is one shared foreground token fetch; waiters read its result
// after done closes instead of starting their own fetch.
type tokenFlight struct {
done chan struct{}
token string
lifetime time.Duration
err error
}
type laneKey struct{}
type attemptKey struct{}
@@ -88,23 +68,15 @@ type driveShared struct {
uploadURL string
backoff time.Duration
tokenMu sync.Mutex
token string
tokenExpiry time.Time
tokenLifetime time.Duration
refreshing bool
authErr error
flight *tokenFlight
tokenRetryAt time.Time
tokenBackoff time.Duration
lastTokenErr error
tokenMu sync.Mutex
token string
tokenExpiry time.Time
idMu sync.Mutex
ids map[string]string
coolMu sync.Mutex
coolUntil time.Time
throttle atomic.Int64
}
func newDriveStorage(streamSettings *internet.MemoryStreamConfig, config *Config) (*driveStorage, error) {
@@ -165,130 +137,14 @@ func quoteDriveValue(value string) string {
return strings.NewReplacer(`\`, `\\`, `'`, `\'`).Replace(value)
}
type authError struct{ code string }
func (e authError) Error() string {
return "Google rejected the OAuth credentials (" + e.code + "); create a new refresh token, see the XDRIVE setup guide"
}
func isAuthError(err error) bool {
_, ok := err.(authError)
return ok
}
func (s *driveStorage) accessToken(ctx context.Context) (string, error) {
s.tokenMu.Lock()
if s.authErr != nil {
err := s.authErr
s.tokenMu.Unlock()
return "", err
}
if s.token != "" && time.Now().Before(s.tokenExpiry) {
if s.tokenLifetime > 0 && time.Until(s.tokenExpiry) < s.tokenLifetime/5 && !s.refreshing && time.Now().After(s.tokenRetryAt) {
s.refreshing = true
go s.refreshTokenBackground()
}
token := s.token
s.tokenMu.Unlock()
return token, nil
}
if f := s.flight; f != nil {
s.tokenMu.Unlock()
select {
case <-f.done:
case <-ctx.Done():
return "", ctx.Err()
}
if f.err != nil {
return "", f.err
}
return f.token, nil
}
if time.Now().Before(s.tokenRetryAt) && s.lastTokenErr != nil {
err := s.lastTokenErr
s.tokenMu.Unlock()
return "", err
}
f := &tokenFlight{done: make(chan struct{})}
s.flight = f
s.tokenMu.Unlock()
// The shared fetch must outlive any single waiter's cancellation.
fetchCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second)
token, lifetime, err := s.fetchToken(fetchCtx)
cancel()
s.tokenMu.Lock()
s.flight = nil
f.token, f.lifetime, f.err = token, lifetime, err
if err != nil {
s.lastTokenErr = err
if isAuthError(err) {
s.authErr = err
errors.LogError(ctx, err)
}
s.noteTokenFailure()
} else {
s.storeToken(token, lifetime)
s.tokenBackoff = 0
s.tokenRetryAt = time.Time{}
s.lastTokenErr = nil
}
close(f.done)
s.tokenMu.Unlock()
if err != nil {
return "", err
}
return token, nil
}
// noteTokenFailure backs off further token fetches so a failing endpoint is
// not hit once per API call; called with tokenMu held.
func (s *driveStorage) noteTokenFailure() {
if s.tokenBackoff == 0 {
s.tokenBackoff = tokenRefreshBackoff
} else {
s.tokenBackoff = min(2*s.tokenBackoff, maxTokenBackoff)
}
s.tokenRetryAt = time.Now().Add(s.tokenBackoff)
}
func (s *driveStorage) storeToken(token string, lifetime time.Duration) {
s.token = token
s.tokenLifetime = lifetime
expiry := lifetime
if expiry > time.Minute {
expiry -= time.Minute
}
s.tokenExpiry = time.Now().Add(expiry)
}
func (s *driveStorage) refreshTokenBackground() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
token, lifetime, err := s.fetchToken(ctx)
s.tokenMu.Lock()
defer s.tokenMu.Unlock()
s.refreshing = false
if err != nil {
if isAuthError(err) && s.authErr == nil {
s.authErr = err
errors.LogError(ctx, err)
}
s.lastTokenErr = err
s.noteTokenFailure()
return
}
if s.authErr == nil {
s.storeToken(token, lifetime)
s.tokenBackoff = 0
s.tokenRetryAt = time.Time{}
s.lastTokenErr = nil
}
}
func (s *driveStorage) fetchToken(ctx context.Context) (string, time.Duration, error) {
if s.token != "" && time.Now().Before(s.tokenExpiry) {
return s.token, nil
}
form := url.Values{
"client_id": {s.clientID},
"client_secret": {s.clientSecret},
@@ -297,31 +153,22 @@ func (s *driveStorage) fetchToken(ctx context.Context) (string, time.Duration, e
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, s.tokenURL, strings.NewReader(form.Encode()))
if err != nil {
return "", 0, errors.New("failed to build the token request").Base(err)
return "", errors.New("failed to build the token request").Base(err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := s.tokenClient.Do(req)
if err != nil {
return "", 0, errors.New("failed to refresh the access token").Base(err)
return "", errors.New("failed to refresh the access token").Base(err)
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if err != nil {
return "", 0, errors.New("failed to read the token response").Base(err)
return "", errors.New("failed to read the token response").Base(err)
}
if resp.StatusCode != http.StatusOK {
var rejected struct {
Error string `json:"error"`
}
if json.Unmarshal(body, &rejected) == nil {
switch rejected.Error {
case "invalid_grant", "invalid_client", "unauthorized_client":
return "", 0, authError{rejected.Error}
}
}
return "", 0, errors.New("the token endpoint answered ", resp.StatusCode, ": ", string(body))
return "", errors.New("the token endpoint answered ", resp.StatusCode, ": ", string(body))
}
var parsed struct {
@@ -329,12 +176,19 @@ func (s *driveStorage) fetchToken(ctx context.Context) (string, time.Duration, e
ExpiresIn int64 `json:"expires_in"`
}
if err := json.Unmarshal(body, &parsed); err != nil {
return "", 0, errors.New("failed to parse the token response").Base(err)
return "", errors.New("failed to parse the token response").Base(err)
}
if parsed.AccessToken == "" {
return "", 0, errors.New("the token endpoint returned no access token")
return "", errors.New("the token endpoint returned no access token")
}
return parsed.AccessToken, time.Duration(parsed.ExpiresIn) * time.Second, nil
lifetime := parsed.ExpiresIn
if lifetime > 60 {
lifetime -= 60
}
s.token = parsed.AccessToken
s.tokenExpiry = time.Now().Add(time.Duration(lifetime) * time.Second)
return s.token, nil
}
func jitter(backoff time.Duration) time.Duration {
@@ -379,6 +233,17 @@ func (s *driveStorage) do(ctx context.Context, method, target, contentType strin
backoff := s.backoff
var lastErr error
s.coolMu.Lock()
until := s.coolUntil
s.coolMu.Unlock()
if d := time.Until(until); d > 0 {
select {
case <-ctx.Done():
return 0, nil, ctx.Err()
case <-time.After(d):
}
}
for attempt := 0; attempt < driveMaxAttempts; attempt++ {
if attempt > 0 {
select {
@@ -391,18 +256,9 @@ func (s *driveStorage) do(ctx context.Context, method, target, contentType strin
backoff = driveMaxBackoff
}
}
if err := s.waitCool(ctx); err != nil {
return 0, nil, err
}
if testDriveIO != nil {
testDriveIO(method, target, body)
}
token, err := s.accessToken(ctx)
if err != nil {
if isAuthError(err) {
return 0, nil, err
}
lastErr = err
continue
}
@@ -443,7 +299,6 @@ func (s *driveStorage) do(ctx context.Context, method, target, contentType strin
timedOut := attemptCtx.Err() == context.DeadlineExceeded && ctx.Err() == nil
cancel()
if timedOut {
s.throttle.Add(1)
if t, ok := s.client.Transport.(*sessionTransport); ok {
t.stalled(began)
}
@@ -456,11 +311,7 @@ func (s *driveStorage) do(ctx context.Context, method, target, contentType strin
attempt+1, " of ", driveMaxAttempts)
continue
}
limit := int64(driveMaxResponse)
if v, ok := ctx.Value(maxBodyKey{}).(int64); ok && v > 0 {
limit = v
}
payload, err := io.ReadAll(io.LimitReader(resp.Body, limit+1))
payload, err := io.ReadAll(resp.Body)
resp.Body.Close()
cancel()
if err != nil {
@@ -470,12 +321,9 @@ func (s *driveStorage) do(ctx context.Context, method, target, contentType strin
lastErr = errors.New("failed to read the Drive response").Base(err)
continue
}
if int64(len(payload)) > limit {
return 0, nil, errors.New("Drive response exceeds the ", limit, " byte cap")
}
if resp.StatusCode == http.StatusUnauthorized {
s.invalidateToken(token)
s.invalidateToken()
lastErr = errors.New("Drive rejected the access token")
continue
}
@@ -503,36 +351,15 @@ func (s *driveStorage) do(ctx context.Context, method, target, contentType strin
return 0, nil, lastErr
}
func (s *driveStorage) waitCool(ctx context.Context) error {
s.coolMu.Lock()
until := s.coolUntil
s.coolMu.Unlock()
if d := time.Until(until); d > 0 {
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(d):
}
}
return nil
}
func (s *driveStorage) cool() {
s.throttle.Add(1)
s.coolMu.Lock()
s.coolUntil = time.Now().Add(jitter(driveCooldown))
s.coolMu.Unlock()
}
func (s *driveStorage) throttled() int64 {
return s.throttle.Load()
}
func (s *driveStorage) invalidateToken(used string) {
func (s *driveStorage) invalidateToken() {
s.tokenMu.Lock()
if s.token == used {
s.token = ""
}
s.token = ""
s.tokenMu.Unlock()
}
@@ -1,99 +0,0 @@
package xdrive
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
func TestDecodeRingCaps(t *testing.T) {
good := ring{
Up: slotTable{Slots: []string{"a", "b"}, Watermark: "w"},
Down: slotTable{Slots: []string{"c", "d"}, Watermark: "w"},
}
b, _ := json.Marshal(good)
if _, ok := decodeRing(b); !ok {
t.Fatal("a normal ring was rejected")
}
tooMany := good
tooMany.Up.Slots = make([]string, maxConcurrency+1)
for i := range tooMany.Up.Slots {
tooMany.Up.Slots[i] = "s"
}
b, _ = json.Marshal(tooMany)
if _, ok := decodeRing(b); ok {
t.Fatalf("a ring with %d slots per table was accepted", maxConcurrency+1)
}
longID := good
longID.Up.Slots[0] = strings.Repeat("x", maxSlotIDLen+1)
b, _ = json.Marshal(longID)
if _, ok := decodeRing(b); ok {
t.Fatal("a ring with an over-long slot id was accepted")
}
longWM := good
longWM.Down.Watermark = strings.Repeat("w", maxSlotIDLen+1)
b, _ = json.Marshal(longWM)
if _, ok := decodeRing(b); ok {
t.Fatal("a ring with an over-long watermark id was accepted")
}
emptyID := good
emptyID.Down.Slots[1] = ""
b, _ = json.Marshal(emptyID)
if _, ok := decodeRing(b); ok {
t.Fatal("a ring with an empty slot id was accepted")
}
}
func capTestStorage(t *testing.T, body []byte) (*driveStorage, string) {
t.Helper()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write(body)
}))
t.Cleanup(srv.Close)
return &driveStorage{
driveShared: &driveShared{token: "t", tokenExpiry: time.Now().Add(time.Hour)},
client: srv.Client(),
inflight: make(chan struct{}, 1),
reserved: make(chan struct{}, 1),
}, srv.URL
}
func TestDriveResponseBodyCap(t *testing.T) {
old := driveMaxResponse
driveMaxResponse = 16
t.Cleanup(func() { driveMaxResponse = old })
s, url := capTestStorage(t, make([]byte, 32))
_, _, err := s.do(context.Background(), http.MethodGet, url, "", nil)
if err == nil || !strings.Contains(err.Error(), "exceeds") {
t.Fatalf("an over-cap body returned %v, want a cap error", err)
}
}
func TestDriveAnnouncementBodyCap(t *testing.T) {
s, url := capTestStorage(t, make([]byte, 32))
ctx := context.WithValue(context.Background(), maxBodyKey{}, int64(8))
_, _, err := s.do(ctx, http.MethodGet, url, "", nil)
if err == nil || !strings.Contains(err.Error(), "exceeds") {
t.Fatalf("an over-cap announcement returned %v, want a cap error", err)
}
}
func TestDriveNormal16MiBSegment(t *testing.T) {
body := make([]byte, 16<<20)
s, url := capTestStorage(t, body)
_, payload, err := s.do(context.Background(), http.MethodGet, url, "", nil)
if err != nil {
t.Fatalf("a normal 16 MiB segment failed: %v", err)
}
if len(payload) != len(body) {
t.Fatalf("read %d bytes, want %d", len(payload), len(body))
}
}
@@ -1,232 +0,0 @@
package xdrive
import (
"context"
"crypto/rand"
"encoding/binary"
"fmt"
"io"
"sort"
"strings"
"sync"
"testing"
"time"
"github.com/xtls/xray-core/transport/internet"
)
func livePercentileReport(t *testing.T, label string, samples []time.Duration) {
t.Helper()
if len(samples) == 0 {
t.Logf("%-36s no samples", label)
return
}
sort.Slice(samples, func(i, j int) bool { return samples[i] < samples[j] })
q := func(p float64) time.Duration {
return samples[min(int(p*float64(len(samples))), len(samples)-1)]
}
t.Logf("%-36s n=%d p50=%v p95=%v p99=%v max=%v",
label, len(samples), q(0.50), q(0.95), q(0.99), samples[len(samples)-1])
}
func TestLiveDrivePercentiles(t *testing.T) {
config := liveDriveConfig(t)
root, err := newDriveStorage(&internet.MemoryStreamConfig{}, config)
if err != nil {
t.Fatal(err)
}
defer root.Close()
store := root.session().(*driveStorage)
defer store.Close()
ctx := context.Background()
const n = 60
inlineDesc := strings.Repeat("a", 10<<10)
body := make([]byte, 256<<10)
if _, err := rand.Read(body); err != nil {
t.Fatal(err)
}
inlineID, err := store.makeSlot(ctx, "percentiles/inline")
if err != nil {
t.Fatal(err)
}
defer store.deleteID(ctx, flatten("percentiles/inline"), inlineID)
mediaID, err := store.makeSlot(ctx, "percentiles/media")
if err != nil {
t.Fatal(err)
}
defer store.deleteID(ctx, flatten("percentiles/media"), mediaID)
if err := store.setSlot(ctx, mediaID, "media", body); err != nil {
t.Fatal(err)
}
measure := func(label string, parallelism int, op func() error) {
var mu sync.Mutex
samples := make([]time.Duration, 0, n)
var wg sync.WaitGroup
per := n / parallelism
for w := 0; w < parallelism; w++ {
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < per; i++ {
start := time.Now()
err := op()
elapsed := time.Since(start)
if err != nil {
t.Errorf("%s: %v", label, err)
continue
}
mu.Lock()
samples = append(samples, elapsed)
mu.Unlock()
}
}()
}
wg.Wait()
livePercentileReport(t, fmt.Sprintf("%s (p=%d)", label, parallelism), samples)
}
for _, p := range []int{1, 8} {
measure("inline setSlot 10KiB", p, func() error {
return store.setSlot(ctx, inlineID, inlineDesc, nil)
})
measure("media setSlot 256KiB", p, func() error {
return store.setSlot(ctx, mediaID, "media", body)
})
measure("readDescription", p, func() error {
_, err := store.readDescription(ctx, mediaID)
return err
})
measure("readBody 256KiB", p, func() error {
_, err := store.readBody(ctx, mediaID)
return err
})
}
}
func TestLiveDriveLatencyUnderLoad(t *testing.T) {
config := liveDriveConfig(t)
config.Concurrency = 64
config.FlushIntervalMs = 10
config.MaxPollIntervalMs = 200
settings := &internet.MemoryStreamConfig{
ProtocolName: protocolName,
ProtocolSettings: config,
}
run := func(withBulk bool) []time.Duration {
client, server, cleanup := pairWith(t, settings)
defer cleanup()
if err := server.SetReadDeadline(time.Now().Add(10 * time.Minute)); err != nil {
t.Fatal(err)
}
var writeMu sync.Mutex
writeFrame := func(kind byte, payload []byte) error {
writeMu.Lock()
defer writeMu.Unlock()
var header [5]byte
header[0] = kind
binary.BigEndian.PutUint32(header[1:], uint32(len(payload)))
if _, err := client.Write(header[:]); err != nil {
return err
}
_, err := client.Write(payload)
return err
}
readFrame := func() (byte, []byte, error) {
var header [5]byte
if _, err := io.ReadFull(server, header[:]); err != nil {
return 0, nil, err
}
payload := make([]byte, binary.BigEndian.Uint32(header[1:]))
if _, err := io.ReadFull(server, payload); err != nil {
return 0, nil, err
}
return header[0], payload, nil
}
markers := make(chan time.Duration, 5)
bulkTime := make(chan time.Duration, 1)
go func() {
var bulkRecv int
var bulkStart time.Time
for {
kind, payload, err := readFrame()
if err != nil {
return
}
if kind == 1 {
if bulkRecv == 0 {
bulkStart = time.Now()
}
bulkRecv += len(payload)
if bulkRecv == 8<<20 {
bulkTime <- time.Since(bulkStart)
}
}
if kind == 2 && len(payload) == 16 {
sent := int64(binary.BigEndian.Uint64(payload[:8]))
markers <- time.Since(time.Unix(0, sent))
}
}
}()
bulkDone := make(chan struct{})
if withBulk {
go func() {
defer close(bulkDone)
chunk := make([]byte, 64<<10)
for i := 0; i < 128; i++ {
if err := writeFrame(1, chunk); err != nil {
t.Errorf("bulk frame: %v", err)
return
}
}
}()
time.Sleep(3 * time.Second)
}
for i := 0; i < 5; i++ {
var payload [16]byte
binary.BigEndian.PutUint64(payload[:8], uint64(time.Now().UnixNano()))
if err := writeFrame(2, payload[:]); err != nil {
t.Fatalf("marker frame: %v", err)
}
if i < 4 {
time.Sleep(2 * time.Second)
}
}
if withBulk {
<-bulkDone
}
out := make([]time.Duration, 0, 5)
for i := 0; i < 5; i++ {
select {
case d := <-markers:
out = append(out, d)
case <-time.After(30 * time.Second):
t.Fatal("marker did not arrive")
}
}
if withBulk {
select {
case d := <-bulkTime:
t.Logf("bulk throughput: %.2f MiB/s", 8/d.Seconds())
case <-time.After(60 * time.Second):
t.Log("bulk throughput: bulk did not complete")
}
}
return out
}
t.Log("marker one-way latency with no bulk:")
for i, d := range run(false) {
t.Logf(" marker %d: %v", i+1, d.Round(time.Millisecond))
}
t.Log("marker one-way latency under 8MiB bulk:")
for i, d := range run(true) {
t.Logf(" marker %d: %v", i+1, d.Round(time.Millisecond))
}
}
@@ -1,203 +0,0 @@
package xdrive
import (
"bytes"
"context"
"crypto/rand"
"encoding/json"
"io"
"net/http"
"sort"
"strconv"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/xtls/xray-core/transport/internet"
)
func TestLiveDriveWriteSweep(t *testing.T) {
config := liveDriveConfig(t)
root, err := newDriveStorage(&internet.MemoryStreamConfig{}, config)
if err != nil {
t.Fatal(err)
}
defer root.Close()
store := root.session().(*driveStorage)
defer store.Close()
ctx := context.Background()
inlineID, err := store.makeSlot(ctx, "sweep/inline")
if err != nil {
t.Fatal(err)
}
defer store.deleteID(ctx, flatten("sweep/inline"), inlineID)
mediaID, err := store.makeSlot(ctx, "sweep/media")
if err != nil {
t.Fatal(err)
}
defer store.deleteID(ctx, flatten("sweep/media"), mediaID)
desc := strings.Repeat("a", 10<<10)
body := make([]byte, 256<<10)
if _, err := rand.Read(body); err != nil {
t.Fatal(err)
}
kinds := []struct {
name string
op func(context.Context) error
}{
{"inline 10KiB", func(ctx context.Context) error { return store.setSlot(ctx, inlineID, desc, nil) }},
{"media 256KiB", func(ctx context.Context) error { return store.setSlot(ctx, mediaID, "media", body) }},
}
for _, p := range []int{1, 2, 3, 4, 6, 8, 12} {
for _, kind := range kinds {
levelCtx, cancel := context.WithTimeout(ctx, 60*time.Second)
thr0 := store.throttled()
var mu sync.Mutex
var samples []time.Duration
success := 0
var wg sync.WaitGroup
per := 40 / p
levelStart := time.Now()
for w := 0; w < p; w++ {
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < per; i++ {
if levelCtx.Err() != nil {
return
}
start := time.Now()
if err := kind.op(levelCtx); err != nil {
continue
}
mu.Lock()
samples = append(samples, time.Since(start))
success++
mu.Unlock()
}
}()
}
wg.Wait()
wall := time.Since(levelStart)
cancel()
if len(samples) == 0 {
t.Logf("sweep p=%-2d %-14s writes=0 wall=%v no samples", p, kind.name, wall.Round(time.Millisecond))
continue
}
sort.Slice(samples, func(i, j int) bool { return samples[i] < samples[j] })
q := func(f float64) time.Duration {
return samples[min(int(f*float64(len(samples))), len(samples)-1)]
}
t.Logf("sweep p=%-2d %-14s writes=%d wall=%v writes/s=%.2f p50=%v p95=%v p99=%v max=%v throttled=+%d",
p, kind.name, success, wall.Round(time.Millisecond), float64(success)/wall.Seconds(),
q(0.50), q(0.95), q(0.99), samples[len(samples)-1], store.throttled()-thr0)
}
}
}
func TestLiveDriveBrowsingExchanges(t *testing.T) {
config := liveDriveConfig(t)
config.Concurrency = 64
config.FlushIntervalMs = 10
config.MaxPollIntervalMs = 200
settings := &internet.MemoryStreamConfig{
ProtocolName: protocolName,
ProtocolSettings: config,
}
var slotWrites, ackWrites, announceWrites, reads atomic.Int64
testDriveIO = func(method, target string, body []byte) {
switch method {
case http.MethodGet:
reads.Add(1)
case http.MethodPatch:
var doc struct {
Description string `json:"description"`
}
if json.Unmarshal(body, &doc) != nil {
return
}
if _, ok := decodeSlot(doc.Description); ok {
slotWrites.Add(1)
return
}
if _, err := strconv.ParseInt(doc.Description, 10, 64); err == nil && doc.Description != "" {
ackWrites.Add(1)
return
}
slotWrites.Add(1)
case http.MethodPost, http.MethodPut:
if method == http.MethodPut || strings.Contains(target, "/upload") {
slotWrites.Add(1)
return
}
var doc struct {
Name string `json:"name"`
}
if json.Unmarshal(body, &doc) == nil && strings.HasPrefix(doc.Name, sessionsDir+flatSeparator) {
announceWrites.Add(1)
return
}
slotWrites.Add(1)
}
}
defer func() { testDriveIO = nil }()
client, server, cleanup := pairWith(t, settings)
if err := server.SetReadDeadline(time.Now().Add(10 * time.Minute)); err != nil {
t.Fatal(err)
}
if err := client.SetReadDeadline(time.Now().Add(10 * time.Minute)); err != nil {
t.Fatal(err)
}
answer := randomPayload(30 << 10)
latencies := make([]time.Duration, 0, 40)
for i := 0; i < 40; i++ {
start := time.Now()
request := randomPayload(500)
if _, err := client.Write(request); err != nil {
t.Fatal(err)
}
gotRequest := make([]byte, len(request))
if _, err := io.ReadFull(server, gotRequest); err != nil {
t.Fatal(err)
}
if !bytes.Equal(gotRequest, request) {
t.Fatal("the request came back changed")
}
if _, err := server.Write(answer); err != nil {
t.Fatal(err)
}
got := make([]byte, len(answer))
if _, err := io.ReadFull(client, got); err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, answer) {
t.Fatal("the answer came back changed")
}
latencies = append(latencies, time.Since(start))
if i < 39 {
time.Sleep(400 * time.Millisecond)
}
}
cleanup()
sort.Slice(latencies, func(i, j int) bool { return latencies[i] < latencies[j] })
q := func(f float64) time.Duration {
return latencies[min(int(f*float64(len(latencies))), len(latencies)-1)]
}
t.Logf("browsing: exchanges=%d p50=%v p95=%v max=%v",
len(latencies), q(0.50), q(0.95), latencies[len(latencies)-1])
total := slotWrites.Load() + ackWrites.Load() + announceWrites.Load()
share := float64(0)
if total > 0 {
share = 100 * float64(ackWrites.Load()) / float64(total)
}
t.Logf("browsing writes: slot-data=%d watermark-ack=%d announcement=%d total=%d ack-share=%.1f%% reads=%d",
slotWrites.Load(), ackWrites.Load(), announceWrites.Load(), total, share, reads.Load())
}
+8 -386
View File
@@ -17,7 +17,6 @@ import (
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/stat"
)
type fakeFile struct {
@@ -30,17 +29,14 @@ type fakeFile struct {
type fakeDrive struct {
server *httptest.Server
mu sync.Mutex
files map[string]*fakeFile
nextID int
failOnce map[string]bool
failStatus map[string]int
failBody map[string]string
tokens int
tokenStatus int
tokenBody string
uploadAt []time.Time
hosts map[string]bool
mu sync.Mutex
files map[string]*fakeFile
nextID int
failOnce map[string]bool
failStatus map[string]int
failBody map[string]string
tokens int
hosts map[string]bool
}
func newFakeDrive(t *testing.T) *fakeDrive {
@@ -93,13 +89,7 @@ func newFakeDrive(t *testing.T) *fakeDrive {
func (d *fakeDrive) handleToken(w http.ResponseWriter, r *http.Request) {
d.mu.Lock()
d.tokens++
status, body := d.tokenStatus, d.tokenBody
d.mu.Unlock()
if status != 0 {
w.WriteHeader(status)
w.Write([]byte(body))
return
}
json.NewEncoder(w).Encode(map[string]interface{}{
"access_token": "fake-token",
"expires_in": 3600,
@@ -107,9 +97,6 @@ func (d *fakeDrive) handleToken(w http.ResponseWriter, r *http.Request) {
}
func (d *fakeDrive) handleUpload(w http.ResponseWriter, r *http.Request) {
d.mu.Lock()
d.uploadAt = append(d.uploadAt, time.Now())
d.mu.Unlock()
if d.shouldFail("upload") {
if status, body := d.failure("upload"); status != 0 {
w.WriteHeader(status)
@@ -180,9 +167,6 @@ func (d *fakeDrive) handleFiles(w http.ResponseWriter, r *http.Request) {
}
func (d *fakeDrive) handleCreate(w http.ResponseWriter, r *http.Request) {
d.mu.Lock()
d.uploadAt = append(d.uploadAt, time.Now())
d.mu.Unlock()
if d.shouldFail("upload") {
if status, body := d.failure("upload"); status != 0 {
w.WriteHeader(status)
@@ -757,365 +741,3 @@ func TestDriveFronting(t *testing.T) {
}
}
}
func TestDriveLargeAnnouncement(t *testing.T) {
listenerCtx, cancelListener := context.WithCancel(context.Background())
defer cancelListener()
newFakeDrive(t)
storage := newDriveBackend(t)
ctx := context.Background()
p := paramsFromConfig(driveSettings().ProtocolSettings.(*Config))
session := "large-announcement"
r := ring{
Up: slotTable{Watermark: "up-watermark"},
Down: slotTable{Watermark: "down-watermark"},
}
for i := range 200 {
id := fmt.Sprintf("%s-%03d", strings.Repeat("s", 40), i)
r.Up.Slots = append(r.Up.Slots, id)
r.Down.Slots = append(r.Down.Slots, id)
}
announce, err := json.Marshal(r)
if err != nil {
t.Fatal(err)
}
if len(announce) <= driveInlineLimit {
t.Fatalf("announcement is %d bytes, want more than %d", len(announce), driveInlineLimit)
}
if err := storage.Put(ctx, announceName(session, time.Now()), announce); err != nil {
t.Fatal(err)
}
entries, err := storage.List(ctx, sessionsDir)
if err != nil {
t.Fatal(err)
}
if len(entries) != 1 || entries[0].Inline != nil {
t.Fatalf("large announcement was inlined: %v", entries)
}
conns := make(chan *Conn, 2)
l := &Listener{
ctx: listenerCtx,
storage: storage,
addConn: func(c stat.Connection) { conns <- c.(*Conn) },
params: p,
active: make(map[string]bool),
handled: make(map[string]time.Time),
idleSince: make(map[string]time.Time),
announceWarned: make(map[string]bool),
}
accepted, err := l.acceptPending(ctx)
if err != nil || !accepted {
t.Fatalf("acceptPending: %v, accepted %v", err, accepted)
}
conn := <-conns
reader, ok := conn.reader.(*slotReader)
if !ok || len(reader.table.Slots) != 200 {
t.Fatalf("large announcement did not become a 200-slot connection: %T", conn.reader)
}
conn.Close()
empty := "empty-announcement"
if err := storage.Put(ctx, announceName(empty, time.Now()), nil); err != nil {
t.Fatal(err)
}
accepted, err = l.acceptPending(ctx)
if err != nil || !accepted {
t.Fatalf("acceptPending empty: %v, accepted %v", err, accepted)
}
conn = <-conns
if _, ok := conn.reader.(*walReader); !ok {
t.Fatalf("empty announcement did not become a segment connection: %T", conn.reader)
}
conn.Close()
}
func TestDriveDeadCredentials(t *testing.T) {
drive := newFakeDrive(t)
drive.mu.Lock()
drive.tokenStatus = http.StatusBadRequest
drive.tokenBody = `{"error":"invalid_grant"}`
drive.mu.Unlock()
storage := newDriveBackend(t)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
start := time.Now()
_, _, err := storage.do(ctx, http.MethodGet, storage.filesURL, "", nil)
if !isAuthError(err) {
t.Fatalf("do returned %v, want the sticky OAuth error", err)
}
if time.Since(start) > time.Second {
t.Fatalf("do took %v, want it to fail at once", time.Since(start))
}
for range 3 {
if _, err := storage.readDescription(ctx, "id"); !isAuthError(err) {
t.Fatalf("readDescription returned %v, want the sticky OAuth error", err)
}
}
drive.mu.Lock()
tokens := drive.tokens
drive.mu.Unlock()
if tokens != 1 {
t.Fatalf("the fake saw %d token requests, want exactly one", tokens)
}
reader := newSlotReader(ctx, storage, slotTable{Slots: []string{"id"}, Watermark: "w"}, params{concurrency: 1}, nil)
select {
case <-reader.chunks():
case <-time.After(time.Second):
t.Fatal("the slot reader kept polling after the credentials were rejected")
}
if !isAuthError(reader.Err()) {
t.Fatalf("the slot reader ended with %v, want the sticky OAuth error", reader.Err())
}
}
func TestDriveRetryWaitsCooldown(t *testing.T) {
drive := newFakeDrive(t)
oldCooldown := driveCooldown
driveCooldown = 200 * time.Millisecond
t.Cleanup(func() { driveCooldown = oldCooldown })
storage := newDriveBackend(t)
drive.failOnceWith("upload", http.StatusForbidden,
`{"error":{"code":403,"errors":[{"reason":"userRateLimitExceeded"}]}}`)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
start := time.Now()
if err := storage.Put(ctx, "sessions/abc", nil); err != nil {
t.Fatalf("Put did not survive a 403: %v", err)
}
drive.mu.Lock()
times := append([]time.Time(nil), drive.uploadAt...)
drive.mu.Unlock()
if len(times) < 2 {
t.Fatalf("the fake saw %d writes, want a retry", len(times))
}
if gap := times[1].Sub(times[0]); gap < driveCooldown/2 {
t.Fatalf("the retry arrived after %v, less than the jitter floor %v", gap, driveCooldown/2)
}
storage.coolMu.Lock()
until := storage.coolUntil
storage.coolMu.Unlock()
if times[1].Before(until) {
t.Fatalf("the retry reached the fake at %v, before the cooldown ended at %v", times[1], until)
}
if took := time.Since(start); took < until.Sub(times[0]) {
t.Fatalf("the request took %v, less than the cooldown %v", took, until.Sub(times[0]))
}
}
func TestDriveBackgroundTokenRefresh(t *testing.T) {
var mu sync.Mutex
calls := 0
mode := "ok"
delay := 100 * time.Millisecond
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
calls++
n := calls
current := mode
pause := delay
mu.Unlock()
time.Sleep(pause)
switch current {
case "transient":
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(`{"error":"temporarily_unavailable"}`))
case "auth":
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(`{"error":"invalid_grant"}`))
default:
json.NewEncoder(w).Encode(map[string]interface{}{
"access_token": fmt.Sprintf("token-%d", n),
"expires_in": 100,
})
}
}))
defer server.Close()
newStorage := func() *driveStorage {
return &driveStorage{driveShared: &driveShared{
tokenClient: server.Client(),
tokenURL: server.URL,
}}
}
seed := func(s *driveStorage) {
s.tokenMu.Lock()
s.token = "old"
s.tokenLifetime = 100 * time.Second
s.tokenExpiry = time.Now().Add(10 * time.Second)
s.tokenMu.Unlock()
}
setMode := func(m string) {
mu.Lock()
mode = m
mu.Unlock()
}
callCount := func() int {
mu.Lock()
defer mu.Unlock()
return calls
}
t.Run("returns at once inside the window", func(t *testing.T) {
s := newStorage()
seed(s)
start := time.Now()
token, err := s.accessToken(context.Background())
if err != nil || token != "old" {
t.Fatalf("accessToken gave %q, %v", token, err)
}
if time.Since(start) > 50*time.Millisecond {
t.Fatalf("accessToken waited %v for the background refresh", time.Since(start))
}
waitFor(t, "the background refresh", func() bool {
s.tokenMu.Lock()
defer s.tokenMu.Unlock()
return s.token != "old" && !s.refreshing
})
})
t.Run("concurrent calls start one refresh", func(t *testing.T) {
mu.Lock()
calls = 0
mu.Unlock()
s := newStorage()
seed(s)
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
if token, err := s.accessToken(context.Background()); err != nil || token != "old" {
t.Errorf("accessToken gave %q, %v", token, err)
}
}()
}
wg.Wait()
waitFor(t, "one background refresh", func() bool {
s.tokenMu.Lock()
defer s.tokenMu.Unlock()
return !s.refreshing && s.token != "old"
})
if n := callCount(); n != 1 {
t.Fatalf("concurrent calls started %d refreshes, want one", n)
}
})
t.Run("transient failure keeps the old token", func(t *testing.T) {
mu.Lock()
calls = 0
mu.Unlock()
setMode("transient")
defer setMode("ok")
s := newStorage()
seed(s)
if token, err := s.accessToken(context.Background()); err != nil || token != "old" {
t.Fatalf("accessToken gave %q, %v", token, err)
}
waitFor(t, "the failed refresh to clear", func() bool {
s.tokenMu.Lock()
defer s.tokenMu.Unlock()
return !s.refreshing
})
s.tokenMu.Lock()
token := s.token
s.tokenMu.Unlock()
if token != "old" {
t.Fatalf("a transient failure replaced the token with %q", token)
}
// Simulate the backoff having elapsed so the next call may retry.
s.tokenMu.Lock()
s.tokenRetryAt = time.Time{}
s.tokenBackoff = 0
s.tokenMu.Unlock()
setMode("ok")
if _, err := s.accessToken(context.Background()); err != nil {
t.Fatal(err)
}
waitFor(t, "the later refresh", func() bool {
s.tokenMu.Lock()
defer s.tokenMu.Unlock()
return s.token != "old" && !s.refreshing
})
if n := callCount(); n != 2 {
t.Fatalf("the retry made %d token requests, want two", n)
}
})
t.Run("auth failure turns sticky", func(t *testing.T) {
mu.Lock()
calls = 0
mu.Unlock()
setMode("auth")
defer setMode("ok")
s := newStorage()
seed(s)
if _, err := s.accessToken(context.Background()); err != nil {
t.Fatal(err)
}
waitFor(t, "the sticky auth error", func() bool {
s.tokenMu.Lock()
defer s.tokenMu.Unlock()
return s.authErr != nil && !s.refreshing
})
before := callCount()
if _, err := s.accessToken(context.Background()); !isAuthError(err) {
t.Fatalf("accessToken gave %v, want the sticky auth error", err)
}
if n := callCount(); n != before {
t.Fatalf("the sticky error made %d more token requests", n-before)
}
})
}
func TestSlotPathDefaults(t *testing.T) {
p := paramsForStorage(&Config{}, &driveStorage{})
if p.concurrency != 64 || p.flushInterval != 10*time.Millisecond || p.maxPollInterval != 200*time.Millisecond {
t.Fatalf("slot defaults gave concurrency %d, flush %v, max poll %v", p.concurrency, p.flushInterval, p.maxPollInterval)
}
if wal, base := paramsForStorage(&Config{}, &localStorage{}), paramsFromConfig(&Config{}); wal != base {
t.Fatalf("wal defaults changed: %+v, want %+v", wal, base)
}
explicit := paramsForStorage(&Config{Concurrency: 4, FlushIntervalMs: 30, MaxPollIntervalMs: 100}, &driveStorage{})
if explicit.concurrency != 4 || explicit.flushInterval != 30*time.Millisecond || explicit.maxPollInterval != 100*time.Millisecond {
t.Fatalf("explicit values did not win: %+v", explicit)
}
if p := paramsForStorage(&Config{PollIntervalMs: 500}, &driveStorage{}); p.maxPollInterval < p.minPollInterval {
t.Fatalf("max poll %v is below min poll %v", p.maxPollInterval, p.minPollInterval)
}
}
func TestDialDefaultRingDepth(t *testing.T) {
newFakeDrive(t)
config := &Config{
RemoteFolder: "folder-id",
Service: "google-drive",
Secrets: []string{"client", "secret", "refresh"},
}
settings := &internet.MemoryStreamConfig{
ProtocolName: protocolName,
ProtocolSettings: config,
}
conn, err := Dial(context.Background(), net.Destination{}, settings)
if err != nil {
t.Fatal(err)
}
defer conn.Close()
writer, ok := conn.(*Conn).writer.(*slotWriter)
if !ok {
t.Fatalf("Dial with unset concurrency built %T, want a slot writer", conn.(*Conn).writer)
}
if len(writer.table.Slots) != 64 {
t.Fatalf("Dial with unset concurrency built %d slots, want 64", len(writer.table.Slots))
}
box := spareBoxFor(config)
box.stop.Store(true)
for box.busy.Load() {
time.Sleep(10 * time.Millisecond)
}
}
@@ -1,174 +0,0 @@
package xdrive
import (
"context"
"net/http"
"net/http/httptest"
"sync"
"testing"
"time"
)
type tokenServer struct {
mu sync.Mutex
count int
mode int
delay time.Duration
srv *httptest.Server
}
func newTokenServer(mode int, delay time.Duration) *tokenServer {
ts := &tokenServer{mode: mode, delay: delay}
ts.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ts.mu.Lock()
ts.count++
mode, delay := ts.mode, ts.delay
ts.mu.Unlock()
time.Sleep(delay)
if mode != http.StatusOK {
w.WriteHeader(mode)
w.Write([]byte(`{"error":"temporarily_unavailable"}`))
return
}
w.Write([]byte(`{"access_token":"new-token","expires_in":3600}`))
}))
return ts
}
func (ts *tokenServer) requests() int {
ts.mu.Lock()
defer ts.mu.Unlock()
return ts.count
}
func (ts *tokenServer) storage() *driveStorage {
return &driveStorage{driveShared: &driveShared{
tokenClient: ts.srv.Client(),
tokenURL: ts.srv.URL + "/token",
}}
}
func TestTokenSingleFlightDuringOutage(t *testing.T) {
ts := newTokenServer(http.StatusInternalServerError, 200*time.Millisecond)
defer ts.srv.Close()
s := ts.storage()
var wg sync.WaitGroup
errs := make(chan error, 9)
start := make(chan struct{})
wg.Add(1)
go func() { defer wg.Done(); <-start; _, err := s.accessToken(context.Background()); errs <- err }()
close(start)
// Wait until the single flight is in progress, then pile on waiters.
deadline := time.Now().Add(2 * time.Second)
for ts.requests() == 0 && time.Now().Before(deadline) {
time.Sleep(time.Millisecond)
}
for i := 0; i < 7; i++ {
wg.Add(1)
go func() { defer wg.Done(); _, err := s.accessToken(context.Background()); errs <- err }()
}
cancelledCtx, cancel := context.WithCancel(context.Background())
wg.Add(1)
go func() {
defer wg.Done()
time.Sleep(20 * time.Millisecond)
cancel()
_, err := s.accessToken(cancelledCtx)
errs <- err
}()
wg.Wait()
close(errs)
var cancelledSeen bool
for err := range errs {
if err == nil {
t.Fatal("an outage caller succeeded")
}
if err == context.Canceled {
cancelledSeen = true
}
}
if !cancelledSeen {
t.Fatal("the cancelled waiter did not return ctx.Err")
}
if n := ts.requests(); n != 1 {
t.Fatalf("N concurrent callers caused %d token fetches, want 1", n)
}
}
func TestTokenBackgroundFailureBacksOff(t *testing.T) {
ts := newTokenServer(http.StatusInternalServerError, 0)
defer ts.srv.Close()
s := ts.storage()
s.token = "cached"
s.tokenLifetime = 10 * time.Second
s.tokenExpiry = time.Now().Add(time.Second) // inside the refresh window
if tok, err := s.accessToken(context.Background()); err != nil || tok != "cached" {
t.Fatalf("accessToken=%q err=%v", tok, err)
}
deadline := time.Now().Add(2 * time.Second)
for ts.requests() == 0 && time.Now().Before(deadline) {
time.Sleep(time.Millisecond)
}
time.Sleep(50 * time.Millisecond)
if n := ts.requests(); n != 1 {
t.Fatalf("background refresh made %d requests, want 1", n)
}
// A second call inside the backoff must not hit the endpoint again.
if _, err := s.accessToken(context.Background()); err != nil {
t.Fatalf("cached token should still be returned: %v", err)
}
if n := ts.requests(); n != 1 {
t.Fatalf("a failing endpoint was hit %d times, want 1", n)
}
s.tokenMu.Lock()
backoff := s.tokenBackoff
retryAt := s.tokenRetryAt
s.tokenMu.Unlock()
if backoff < tokenRefreshBackoff {
t.Fatalf("backoff %v below the default %v", backoff, tokenRefreshBackoff)
}
if !retryAt.After(time.Now()) {
t.Fatal("tokenRetryAt not set into the future after a background failure")
}
}
func TestToken401RefetchOnce(t *testing.T) {
ts := newTokenServer(http.StatusOK, 0)
defer ts.srv.Close()
s := ts.storage()
s.token = "old"
s.tokenLifetime = time.Hour
s.tokenExpiry = time.Now().Add(time.Hour)
var wg sync.WaitGroup
for i := 0; i < 8; i++ {
wg.Add(1)
go func() {
defer wg.Done()
s.invalidateToken("old")
if _, err := s.accessToken(context.Background()); err != nil {
t.Errorf("accessToken: %v", err)
}
}()
}
wg.Wait()
if n := ts.requests(); n != 1 {
t.Fatalf("many 401 handlers caused %d refetches, want 1", n)
}
s.tokenMu.Lock()
tok := s.token
s.tokenMu.Unlock()
if tok != "new-token" {
t.Fatalf("token=%q, want new-token", tok)
}
// Invalidating with a stale used token must not clear the fresh one.
s.invalidateToken("old")
s.tokenMu.Lock()
tok = s.token
s.tokenMu.Unlock()
if tok != "new-token" {
t.Fatalf("stale invalidation cleared the fresh token: %q", tok)
}
}
-20
View File
@@ -67,23 +67,3 @@ func paramsFromConfig(c *Config) params {
}
return p
}
// paramsForStorage applies the Drive slot-path defaults only where the storage
// is a slotStore and only to fields the config leaves at 0; wal-based backends
// keep the defaults above because their concurrency and flush mean other things
func paramsForStorage(c *Config, storage Storage) params {
if _, ok := storage.(slotStore); ok {
cc := *c
if cc.Concurrency == 0 {
cc.Concurrency = 64
}
if cc.FlushIntervalMs == 0 {
cc.FlushIntervalMs = 10
}
if cc.MaxPollIntervalMs == 0 {
cc.MaxPollIntervalMs = 200
}
c = &cc
}
return paramsFromConfig(c)
}
+30 -138
View File
@@ -36,9 +36,6 @@ const (
slotMedia = 2
slotEnd = 3
slotErr = 4
// maxSlotIDLen bounds any slot or watermark id accepted from a peer.
maxSlotIDLen = 128
)
var (
@@ -74,12 +71,6 @@ type slotStore interface {
readBody(ctx context.Context, id string) ([]byte, error)
}
// throttleStore is the optional part of a slotStore that reports how often
// Drive throttled or timed out its requests; only driveStorage implements it
type throttleStore interface {
throttled() int64
}
// 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 {
@@ -144,9 +135,8 @@ type slotTable struct {
// 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"`
First []byte `json:"first,omitempty"`
Up slotTable `json:"u"`
Down slotTable `json:"d"`
}
func decodeRing(b []byte) (ring, bool) {
@@ -155,19 +145,6 @@ func decodeRing(b []byte) (ring, bool) {
r.Up.Watermark == "" || r.Down.Watermark == "" {
return ring{}, false
}
if len(r.Up.Slots) > maxConcurrency || len(r.Down.Slots) > maxConcurrency {
return ring{}, false
}
for _, t := range []slotTable{r.Up, r.Down} {
if len(t.Watermark) > maxSlotIDLen {
return ring{}, false
}
for _, id := range t.Slots {
if len(id) == 0 || len(id) > maxSlotIDLen {
return ring{}, false
}
}
}
return r, true
}
@@ -232,7 +209,6 @@ type slotWriter struct {
held int
closed bool
err error
first chan []byte
// ackMu is held while the watermark is read, which other uploads wait for
ackMu sync.Mutex
@@ -355,12 +331,6 @@ func (w *slotWriter) flushLocked() error {
w.buf = append(w.buf[:0], w.buf[n:]...)
seq := w.seq
w.seq++
if seq == 0 && w.first != nil {
select {
case w.first <- chunk:
default:
}
}
w.lastSize = len(w.buf)
w.held = 0
@@ -524,51 +494,32 @@ type slotReader struct {
// 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{}
frontier int64
err error
stop context.CancelFunc
reads atomic.Int64
tailMin time.Duration
tailMax time.Duration
stallTimeout time.Duration
bodyRate float64
hitAt time.Time
gap time.Duration
activeAt time.Time
active chan struct{}
frontier int64
err error
reads atomic.Int64
tailMin time.Duration
tailMax time.Duration
}
func newSlotReader(ctx context.Context, store slotStore, table slotTable, p params, first []byte) *slotReader {
head := int64(0)
if len(first) > 0 {
head = 1
}
runCtx, stop := context.WithCancel(ctx)
func newSlotReader(ctx context.Context, store slotStore, table slotTable, p params) *slotReader {
r := &slotReader{
ctx: runCtx,
stop: stop,
store: store,
table: table,
params: p,
head: head,
started: len(first) > 0,
ch: make(chan []byte, max(p.concurrency, 1)),
wake: make(chan struct{}, 1),
acks: make(chan struct{}, 1),
grow: make(chan struct{}, 1),
hitAt: time.Now(),
activeAt: time.Now(),
active: make(chan struct{}),
tailMin: slotTailMin,
tailMax: slotTailMax,
stallTimeout: slotStallTimeout,
}
if len(first) > 0 {
r.ch <- first
select {
case r.acks <- struct{}{}:
default:
}
ctx: ctx,
store: store,
table: table,
params: p,
ch: make(chan []byte, p.concurrency),
wake: make(chan struct{}, 1),
acks: make(chan struct{}, 1),
grow: make(chan struct{}, 1),
hitAt: time.Now(),
activeAt: time.Now(),
active: make(chan struct{}),
tailMin: slotTailMin,
tailMax: slotTailMax,
}
go r.run()
go r.ackLoop()
@@ -724,7 +675,7 @@ func (r *slotReader) fetch(ctx context.Context, seq int64, offset int, f *slotFe
id := r.slotID(seq)
backoff := slotHuntFloor
var goneSince, warnedAt, visibleSince time.Time
var goneSince, warnedAt time.Time
for {
var description string
var err error
@@ -738,10 +689,6 @@ func (r *slotReader) fetch(ctx context.Context, seq int64, offset int, f *slotFe
description, err = r.store.readDescription(ctx, id)
}
frame, ok := decodeSlot(description)
if err == errNotFound || (err == nil && description == "") {
// An absent or empty slot is idle, not a failure.
visibleSince = time.Time{}
}
switch {
case err == nil && ok && frame.seq == seq:
// what follows it is asked for while its body comes
@@ -761,37 +708,11 @@ func (r *slotReader) fetch(ctx context.Context, seq int64, offset int, f *slotFe
r.hit()
return
case slotMedia:
body, bodyErr := r.readBodyHedged(ctx, id, frame.size)
if bodyErr == nil && frame.holds(body) {
if body, err := r.readBodyHedged(ctx, id); err == nil && frame.holds(body) {
f.body = body
r.hit()
return
}
if ctx.Err() == nil && time.Since(warnedAt) > 10*time.Second {
warnedAt = time.Now()
if bodyErr != nil {
errors.LogWarningInner(ctx, bodyErr, "failed to read body of slot ", id)
} else {
errors.LogWarning(ctx, "body of slot ", id, " does not match segment ", seq)
}
}
if visibleSince.IsZero() {
visibleSince = time.Now()
}
if time.Since(visibleSince) > r.stallTimeout {
f.err = errors.New("segment ", seq, " has no usable body after ", r.stallTimeout).Base(bodyErr)
return
}
}
case err == nil && !ok && description != "":
// A present but undecodable description for this slot is not idle;
// fail once it has persisted beyond the stall timeout.
if visibleSince.IsZero() {
visibleSince = time.Now()
}
if time.Since(visibleSince) > r.stallTimeout {
f.err = errors.New("slot ", id, " of segment ", seq, " holds an invalid frame")
return
}
case err == errNotFound:
if goneSince.IsZero() {
@@ -801,10 +722,6 @@ func (r *slotReader) fetch(ctx context.Context, seq int64, offset int, f *slotFe
return
}
case err != nil:
if isAuthError(err) {
f.err = err
return
}
goneSince = time.Time{}
if ctx.Err() == nil && time.Since(warnedAt) > 10*time.Second {
warnedAt = time.Now()
@@ -898,22 +815,12 @@ func (r *slotReader) readHead(ctx context.Context, id string, seq int64) (string
}
}
func (r *slotReader) readBodyHedged(ctx context.Context, id string, size int) ([]byte, error) {
func (r *slotReader) readBodyHedged(ctx context.Context, id string) ([]byte, error) {
if slotBodyHedge <= 0 {
return r.store.readBody(ctx, id)
}
delay := slotBodyHedge
r.mu.Lock()
rate := r.bodyRate
r.mu.Unlock()
if rate > 0 && size > 0 {
if scaled := time.Duration(float64(2*size) / rate * float64(time.Second)); scaled > delay {
delay = scaled
}
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
start := time.Now()
type result struct {
body []byte
@@ -928,7 +835,7 @@ func (r *slotReader) readBodyHedged(ctx context.Context, id string, size int) ([
}
launch()
outstanding := 1
timer := time.NewTimer(delay)
timer := time.NewTimer(slotBodyHedge)
defer timer.Stop()
var last result
for {
@@ -936,16 +843,6 @@ func (r *slotReader) readBodyHedged(ctx context.Context, id string, size int) ([
case res := <-results:
outstanding--
if res.err == nil {
if elapsed := time.Since(start); elapsed > 0 && len(res.body) > 0 {
instant := float64(len(res.body)) / elapsed.Seconds()
r.mu.Lock()
if r.bodyRate == 0 {
r.bodyRate = instant
} else {
r.bodyRate = (r.bodyRate + instant) / 2
}
r.mu.Unlock()
}
return res.body, nil
}
last = res
@@ -1043,15 +940,10 @@ func (r *slotReader) ackLoop() {
func (r *slotReader) setErr(err error) {
r.mu.Lock()
defer r.mu.Unlock()
if r.err == nil {
r.err = err
}
r.mu.Unlock()
// A stored error means the connection is dead: stop the run loop so the
// chunk channel closes and Read returns the error instead of blocking.
if r.stop != nil {
r.stop()
}
}
func (r *slotReader) Err() error {
@@ -1,77 +0,0 @@
package xdrive
import (
"bytes"
"context"
"strings"
"testing"
"time"
)
// badBodySlots serves a media frame whose body never matches its checksum.
type badBodySlots struct{}
func (badBodySlots) makeSlot(ctx context.Context, name string) (string, error) { return name, nil }
func (badBodySlots) setSlot(ctx context.Context, id, description string, body []byte) error {
return nil
}
func (badBodySlots) readDescription(ctx context.Context, id string) (string, error) {
return newSlotFrame(0, slotMedia, make([]byte, 64)).encode(), nil
}
func (badBodySlots) readBody(ctx context.Context, id string) ([]byte, error) {
return bytes.Repeat([]byte{1}, 64), nil
}
// emptySlots serves an empty description: an idle slot, not a failure.
type emptySlots struct{}
func (emptySlots) makeSlot(ctx context.Context, name string) (string, error) { return name, nil }
func (emptySlots) setSlot(ctx context.Context, id, description string, body []byte) error {
return nil
}
func (emptySlots) readDescription(ctx context.Context, id string) (string, error) { return "", nil }
func (emptySlots) readBody(ctx context.Context, id string) ([]byte, error) { return nil, nil }
func shrinkStallClocks(t *testing.T) {
t.Helper()
// Only the stall timeout is shrunk; readers snapshot it at construction,
// so lingering goroutines from other tests never read a mutated global.
oldStall := slotStallTimeout
slotStallTimeout = 100 * time.Millisecond
t.Cleanup(func() { slotStallTimeout = oldStall })
}
func TestHeadBodyChecksumMismatchFails(t *testing.T) {
shrinkStallClocks(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
r := newSlotReader(ctx, badBodySlots{}, slotTable{Slots: []string{"id"}, Watermark: "w"},
params{concurrency: 1, eagerWindow: 50 * time.Millisecond, maxPollInterval: 5 * time.Millisecond}, nil)
select {
case _, ok := <-r.chunks():
if ok {
t.Fatal("a mismatched body was delivered")
}
case <-time.After(3 * time.Second):
t.Fatal("a persistent checksum mismatch retried forever")
}
if err := r.Err(); err == nil || !strings.Contains(err.Error(), "no usable body") {
t.Fatalf("reader ended with %v, want a clear body error", err)
}
}
func TestEmptySlotStaysIdle(t *testing.T) {
shrinkStallClocks(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
r := newSlotReader(ctx, emptySlots{}, slotTable{Slots: []string{"id"}, Watermark: "w"},
params{concurrency: 1, eagerWindow: 50 * time.Millisecond, maxPollInterval: 5 * time.Millisecond}, nil)
select {
case <-r.chunks():
t.Fatal("an empty slot failed the reader instead of staying idle")
case <-time.After(300 * time.Millisecond):
}
cancel()
}
+3 -252
View File
@@ -4,7 +4,6 @@ import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"io"
"math/rand"
@@ -88,8 +87,8 @@ func slotPair(t *testing.T, store slotStore, concurrency, segmentBytes uint32) (
if err != nil {
t.Fatal(err)
}
client := newSlotConn(context.Background(), store, r.Up, r.Down, p, nil, nil)
server := newSlotConn(context.Background(), store, r.Down, r.Up, p, nil, nil)
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()
@@ -366,84 +365,6 @@ func TestSlotBodyHedge(t *testing.T) {
}
}
type timedBodySlots struct {
*memSlots
calls atomic.Int32
delay time.Duration
stuck atomic.Bool
}
func (s *timedBodySlots) readBody(ctx context.Context, id string) ([]byte, error) {
n := s.calls.Add(1)
if s.stuck.Load() && n == 1 {
<-ctx.Done()
return nil, ctx.Err()
}
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(s.delay):
}
return make([]byte, 1000), nil
}
func TestSlotBodyHedgeRate(t *testing.T) {
old := slotBodyHedge
slotBodyHedge = 20 * time.Millisecond
t.Cleanup(func() { slotBodyHedge = old })
store := &timedBodySlots{memSlots: newMemSlots(), delay: 60 * time.Millisecond}
r := &slotReader{store: store}
ctx := context.Background()
if _, err := r.readBodyHedged(ctx, "id", 1000); err != nil {
t.Fatal(err)
}
store.calls.Store(0)
start := time.Now()
if _, err := r.readBodyHedged(ctx, "id", 1000); err != nil {
t.Fatal(err)
}
if elapsed := time.Since(start); elapsed < 60*time.Millisecond {
t.Fatalf("the progressing read returned in %v, before its body was ready", elapsed)
}
if n := store.calls.Load(); n != 1 {
t.Fatalf("a progressing read after a learned slow rate hedged, %d reads", n)
}
r.mu.Lock()
rate := r.bodyRate
r.mu.Unlock()
if rate <= 0 {
t.Fatal("no body read rate was learned")
}
expected := max(slotBodyHedge, time.Duration(float64(2*1000)/rate*float64(time.Second)))
store.stuck.Store(true)
store.calls.Store(0)
start = time.Now()
if _, err := r.readBodyHedged(ctx, "id", 1000); err != nil {
t.Fatal(err)
}
elapsed := time.Since(start)
if n := store.calls.Load(); n < 2 {
t.Fatalf("a stuck read after a learned rate was not hedged, %d reads", n)
}
if elapsed < expected/2 {
t.Fatalf("the stuck read hedged after %v, want about %v", elapsed, expected)
}
freshStore := &timedBodySlots{memSlots: newMemSlots()}
freshStore.stuck.Store(true)
fresh := &slotReader{store: freshStore}
store.calls.Store(0)
start = time.Now()
if _, err := fresh.readBodyHedged(ctx, "id", 1000); err != nil {
t.Fatal(err)
}
if elapsed := time.Since(start); elapsed < slotBodyHedge {
t.Fatalf("with no learned rate the hedge came after %v, want at least %v", elapsed, slotBodyHedge)
}
}
func TestTakeSpareSkipsStale(t *testing.T) {
cfg := &Config{}
p := params{sessionTTL: 5 * time.Minute}
@@ -639,7 +560,7 @@ func TestSlotReaderReachesPastWindow(t *testing.T) {
t.Fatal(err)
}
}
reader := newSlotReader(ctx, store, ring.Down, p, nil)
reader := newSlotReader(ctx, store, ring.Down, p)
next := func() []byte {
select {
case chunk := <-reader.chunks():
@@ -754,173 +675,3 @@ func TestSlotBurstInlineWaves(t *testing.T) {
}
}
}
type holdSlots struct {
*memSlots
target string
hold chan struct{}
}
func (s *holdSlots) setSlot(ctx context.Context, id, description string, body []byte) error {
if id == s.target {
select {
case <-s.hold:
case <-ctx.Done():
return ctx.Err()
}
}
return s.memSlots.setSlot(ctx, id, description, body)
}
func TestSlotFirstAnnouncement(t *testing.T) {
p := params{
concurrency: 2,
segmentBytes: 4096,
flushInterval: time.Millisecond,
minPollInterval: time.Millisecond,
maxPollInterval: 5 * time.Millisecond,
sessionTTL: time.Minute,
}
store := &holdSlots{memSlots: newMemSlots(), hold: make(chan struct{})}
ctx := context.Background()
r, err := makeRing(ctx, store, "first", p)
if err != nil {
t.Fatal(err)
}
store.target = r.Up.Slots[0]
client := newSlotConn(ctx, store, r.Up, r.Down, p, nil, nil)
defer client.Close()
writer := client.writer.(*slotWriter)
writer.first = make(chan []byte, 1)
payload := randomPayload(10000)
errs := make(chan error, 1)
go func() {
_, err := client.Write(payload)
if err == nil {
err = client.Close()
}
errs <- err
}()
var first []byte
select {
case first = <-writer.first:
case <-time.After(5 * time.Second):
t.Fatal("the writer never flushed seq 0")
}
announced := r
announced.First = first
encoded, err := json.Marshal(announced)
if err != nil {
t.Fatal(err)
}
if len(encoded) > driveInlineLimit {
t.Fatalf("the test announcement is %d bytes, want the inline path", len(encoded))
}
decoded, ok := decodeRing(encoded)
if !ok {
t.Fatal("the announcement with First did not decode")
}
server := newSlotConn(ctx, store, decoded.Down, decoded.Up, p, decoded.First, nil)
defer server.Close()
if err := server.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil {
t.Fatal(err)
}
gotFirst := make([]byte, len(first))
if _, err := io.ReadFull(server, gotFirst); err != nil {
t.Fatalf("the first bytes did not arrive before the ring write: %v", err)
}
if !bytes.Equal(gotFirst, first) {
t.Fatal("the primed first bytes changed")
}
close(store.hold)
rest := make([]byte, len(payload)-len(first))
if _, err := io.ReadFull(server, rest); err != nil {
t.Fatal(err)
}
if !bytes.Equal(append(gotFirst, rest...), payload) {
t.Fatal("the stream was duplicated or changed")
}
if err := <-errs; err != nil {
t.Fatal(err)
}
}
func TestSlotFirstAnnouncementTooLarge(t *testing.T) {
p := params{
concurrency: 2,
segmentBytes: 256 << 10,
flushInterval: time.Millisecond,
sessionTTL: time.Minute,
}
store := newMemSlots()
ctx := context.Background()
r, err := makeRing(ctx, store, "large-first", p)
if err != nil {
t.Fatal(err)
}
client := newSlotConn(ctx, store, r.Up, r.Down, p, nil, nil)
defer client.Close()
writer := client.writer.(*slotWriter)
writer.first = make(chan []byte, 1)
go client.Write(randomPayload(300 << 10))
var first []byte
select {
case first = <-writer.first:
case <-time.After(5 * time.Second):
t.Fatal("the writer never flushed seq 0")
}
if len(first) <= slotInlineLimit {
t.Fatalf("the first segment is %d bytes, want a media segment", len(first))
}
withFirst := r
withFirst.First = first
encoded, err := json.Marshal(withFirst)
if err != nil {
t.Fatal(err)
}
if len(encoded) <= driveInlineLimit {
t.Fatalf("a %d byte first segment fit the inline announcement", len(first))
}
plain, err := json.Marshal(r)
if err != nil {
t.Fatal(err)
}
decoded, ok := decodeRing(plain)
if !ok || decoded.First != nil {
t.Fatal("the announcement without First did not decode cleanly")
}
}
func TestSlotRingFirstCompatibility(t *testing.T) {
p := params{concurrency: 2, sessionTTL: time.Minute}
store := newMemSlots()
r, err := makeRing(context.Background(), store, "compat", p)
if err != nil {
t.Fatal(err)
}
r.First = []byte("hello")
encoded, err := json.Marshal(r)
if err != nil {
t.Fatal(err)
}
var oldServer struct {
Up slotTable `json:"u"`
Down slotTable `json:"d"`
}
if err := json.Unmarshal(encoded, &oldServer); err != nil {
t.Fatalf("an old server could not ignore First: %v", err)
}
if len(oldServer.Up.Slots) != len(r.Up.Slots) || len(oldServer.Down.Slots) != len(r.Down.Slots) {
t.Fatal("an old server lost the ring tables")
}
r.First = nil
plain, err := json.Marshal(r)
if err != nil {
t.Fatal(err)
}
decoded, ok := decodeRing(plain)
if !ok || decoded.First != nil {
t.Fatal("an old client announcement did not decode")
}
}
-8
View File
@@ -6,7 +6,6 @@ import (
"encoding/hex"
"strings"
"sync"
"sync/atomic"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/transport/internet"
@@ -27,18 +26,11 @@ type Storage interface {
Close() error
}
// testStorageFactory lets tests substitute a backend for newStorage; nil in
// production so behaviour is unchanged.
var testStorageFactory atomic.Pointer[func(config *Config) (Storage, error)]
func newStorage(streamSettings *internet.MemoryStreamConfig) (Storage, error) {
config, err := streamConfig(streamSettings)
if err != nil {
return nil, err
}
if f := testStorageFactory.Load(); f != nil {
return (*f)(config)
}
switch config.Service {
case "local":
+32 -102
View File
@@ -224,51 +224,30 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me
}
}
p = paramsForStorage(config, storage)
var announce []byte
slots, ok := storage.(slotStore)
if ok && !warm {
if r, err = makeRing(ctx, slots, session, p); err != nil {
if ok {
if warm {
announce, _ = json.Marshal(r)
} else 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()
return nil, errors.New("failed to announce session ", session).Base(err)
}
if ok {
warmSpares(streamSettings, config, p)
errors.LogInfo(ctx, "opened session ", session, " in ", len(r.Up.Slots), " slots")
conn := newSlotConn(context.Background(), slots, r.Up, r.Down, p, nil, func() {
return newSlotConn(context.Background(), slots, r.Up, r.Down, p, func() {
storage.Close()
})
writer := conn.writer.(*slotWriter)
writer.first = make(chan []byte, 1)
go func() {
var first []byte
select {
case first = <-writer.first:
case <-time.After(25 * time.Millisecond):
}
announce, _ := json.Marshal(r)
if len(first) > 0 && len(first) <= slotInlineLimit {
withFirst := r
withFirst.First = first
if encoded, err := json.Marshal(withFirst); err == nil && len(encoded) <= driveInlineLimit {
announce = encoded
}
}
// An announce failure surfaces as a connection error on the next Write/Read.
if err := storage.Put(context.Background(), announceName(session, time.Now()), announce); err != nil {
err = errors.New("failed to announce session ", session).Base(err)
writer.fail(err)
conn.reader.(*slotReader).setErr(err)
}
}()
return conn, nil
}
if err := storage.Put(ctx, announceName(session, time.Now()), nil); err != nil {
storage.Close()
return nil, errors.New("failed to announce session ", session).Base(err)
}), nil
}
errors.LogInfo(ctx, "opened session ", session)
return newConn(context.Background(), storage,
@@ -284,12 +263,10 @@ type Listener struct {
addConn internet.ConnHandler
params
mu sync.Mutex
active map[string]bool
handled map[string]time.Time
idleSince map[string]time.Time
announceWarned map[string]bool
firstSeen map[string]time.Time
mu sync.Mutex
active map[string]bool
handled map[string]time.Time
idleSince map[string]time.Time
}
func Serve(ctx context.Context, address net.Address, port net.Port, streamSettings *internet.MemoryStreamConfig, addConn internet.ConnHandler) (internet.Listener, error) {
@@ -305,16 +282,14 @@ func Serve(ctx context.Context, address net.Address, port net.Port, streamSettin
listenerCtx, cancel := context.WithCancel(context.Background())
listener := &Listener{
ctx: listenerCtx,
cancel: cancel,
storage: storage,
addConn: addConn,
params: paramsForStorage(config, storage),
active: make(map[string]bool),
handled: make(map[string]time.Time),
idleSince: make(map[string]time.Time),
announceWarned: make(map[string]bool),
firstSeen: make(map[string]time.Time),
ctx: listenerCtx,
cancel: cancel,
storage: storage,
addConn: addConn,
params: paramsFromConfig(config),
active: make(map[string]bool),
handled: make(map[string]time.Time),
idleSince: make(map[string]time.Time),
}
go listener.acceptLoop(ctx)
@@ -328,7 +303,7 @@ func (l *Listener) acceptLoop(logCtx context.Context) {
active := time.Now()
for {
accepted, err := l.acceptPending(logCtx)
if err != nil && !isAuthError(err) {
if err != nil {
errors.LogWarningInner(logCtx, err, "failed to list sessions")
}
@@ -360,72 +335,27 @@ func (l *Listener) acceptPending(logCtx context.Context) (bool, error) {
}
accepted := false
l.mu.Lock()
if l.firstSeen == nil {
l.firstSeen = make(map[string]time.Time)
}
now := time.Now()
listedEntries := make(map[string]bool, len(sessions))
for _, listed := range sessions {
listedEntries[listed.Name] = true
if _, seen := l.firstSeen[listed.Name]; !seen {
l.firstSeen[listed.Name] = now
}
}
for entry := range l.firstSeen {
if !listedEntries[entry] {
delete(l.firstSeen, entry)
}
}
l.mu.Unlock()
for _, listed := range sessions {
entry := listed.Name
full := sessionsDir + "/" + entry
session, _, ok := parseAnnounce(entry)
session, at, ok := parseAnnounce(entry)
if !ok {
go l.drop(full)
continue
}
l.mu.Lock()
age := now.Sub(l.firstSeen[entry])
l.mu.Unlock()
if age > l.sessionTTL {
if time.Since(at) > l.sessionTTL {
errors.LogInfo(logCtx, "dropping the stale announcement of session ", session)
go l.drop(full)
l.mu.Lock()
protected := l.active[session] || !l.handled[session].IsZero()
l.mu.Unlock()
if !protected {
go l.drop(sessionPrefix(session))
}
go l.drop(sessionPrefix(session))
continue
}
if !l.claim(session) {
continue
}
announce := listed.Inline
if len(announce) == 0 {
if _, ok := sessionStorage(l.storage).(slotStore); ok {
announce, err = l.storage.Get(context.WithValue(l.ctx, maxBodyKey{}, int64(driveMaxAnnounce)), full)
if err != nil {
l.mu.Lock()
delete(l.active, session)
delete(l.handled, session)
warn := !l.announceWarned[session]
l.announceWarned[session] = true
l.mu.Unlock()
if warn {
errors.LogWarningInner(logCtx, err, "failed to read the announcement of session ", session)
}
continue
}
}
}
go l.drop(full)
accepted = true
l.addConn(l.newSessionConn(logCtx, session, announce))
l.addConn(l.newSessionConn(logCtx, session, listed.Inline))
}
return accepted, nil
}
@@ -458,7 +388,7 @@ func (l *Listener) newSessionConn(logCtx context.Context, session string, announ
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, r.First, onClose)
return newSlotConn(l.ctx, slots, r.Down, r.Up, l.params, onClose)
}
}
errors.LogInfo(logCtx, "accepted session ", session)
@@ -1,91 +0,0 @@
package xdrive
import (
"context"
"errors"
"strings"
"testing"
"time"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/transport/internet"
)
// failPutStore is a slotStore whose Put always fails, to exercise the announce
// failure path without any network.
type failPutStore struct {
*memSlots
}
func (s *failPutStore) Put(ctx context.Context, name string, data []byte) error {
return errors.New("put refused")
}
func (s *failPutStore) Close() error { return nil }
func (s *failPutStore) Get(ctx context.Context, name string) ([]byte, error) {
return nil, errNotFound
}
func (s *failPutStore) List(ctx context.Context, prefix string) ([]Entry, error) {
return nil, nil
}
func (s *failPutStore) Delete(ctx context.Context, name string) error { return nil }
func TestDialAnnounceFailureFailsRead(t *testing.T) {
store := &failPutStore{memSlots: newMemSlots()}
factory := func(*Config) (Storage, error) { return store, nil }
testStorageFactory.Store(&factory)
t.Cleanup(func() { testStorageFactory.Store(nil) })
settings := &internet.MemoryStreamConfig{
ProtocolName: protocolName,
ProtocolSettings: &Config{
Service: "sim",
RemoteFolder: "x",
Secrets: []string{"a", "b", "c"},
Concurrency: 4,
FlushIntervalMs: 1,
PollIntervalMs: 1,
MaxPollIntervalMs: 5,
SessionTtlSeconds: 60,
},
}
conn, err := Dial(context.Background(), net.Destination{}, settings)
if err != nil {
t.Fatal(err)
}
defer conn.Close()
go conn.Write([]byte("hello"))
type result struct {
n int
err error
}
done := make(chan result, 1)
go func() {
n, err := conn.Read(make([]byte, 16))
done <- result{n, err}
}()
select {
case r := <-done:
if r.err == nil || !strings.Contains(r.err.Error(), "failed to announce") {
t.Fatalf("Read returned %v, want the announce error", r.err)
}
case <-time.After(3 * time.Second):
t.Fatal("Read blocked after the announcement failed")
}
// The reader loop must have exited: it no longer polls the store.
store.mu.Lock()
before := store.reads
store.mu.Unlock()
time.Sleep(100 * time.Millisecond)
store.mu.Lock()
after := store.reads
store.mu.Unlock()
if after != before {
t.Fatalf("reader kept polling after the announce failure: %d -> %d", before, after)
}
}
@@ -1,121 +0,0 @@
package xdrive
import (
"context"
"strings"
"testing"
"time"
"github.com/xtls/xray-core/transport/internet/stat"
)
func announceEntry(name string) string {
return strings.TrimPrefix(name, sessionsDir+"/")
}
func TestAcceptPendingClientClockBehind(t *testing.T) {
l := newTestListener(t, t.TempDir())
var conns int
l.addConn = func(c stat.Connection) { conns++; c.Close() }
ctx := context.Background()
session := "clockbehind"
ring := sessionPrefix(session) + "/" + uplinkDir + "/s0"
if err := l.storage.Put(ctx, ring, []byte("x")); err != nil {
t.Fatal(err)
}
// The client's clock is ten minutes behind the server's.
name := announceName(session, time.Now().Add(-10*time.Minute))
if err := l.storage.Put(ctx, name, []byte("{}")); err != nil {
t.Fatal(err)
}
accepted, err := l.acceptPending(ctx)
if err != nil || !accepted {
t.Fatalf("a slow client clock must not drop the session: accepted=%v err=%v", accepted, err)
}
if conns != 1 {
t.Fatalf("addConn called %d times, want 1", conns)
}
if _, err := l.storage.Get(ctx, ring); err != nil {
t.Fatalf("the ring of an accepted session was dropped: %v", err)
}
}
func TestAcceptPendingStaleLeftoverProtectsActiveRing(t *testing.T) {
l := newTestListener(t, t.TempDir())
l.params.sessionTTL = 50 * time.Millisecond
var conns int
l.addConn = func(c stat.Connection) { conns++; c.Close() }
ctx := context.Background()
session := "active"
l.active[session] = true
l.handled[session] = time.Now()
ring := sessionPrefix(session) + "/" + uplinkDir + "/s0"
if err := l.storage.Put(ctx, ring, []byte("x")); err != nil {
t.Fatal(err)
}
name := announceName(session, time.Now())
if err := l.storage.Put(ctx, name, []byte("{}")); err != nil {
t.Fatal(err)
}
// The listener first saw this leftover entry long ago on its own clock.
l.firstSeen = map[string]time.Time{announceEntry(name): time.Now().Add(-time.Hour)}
accepted, err := l.acceptPending(ctx)
if err != nil || accepted {
t.Fatalf("a stale leftover must not be accepted: accepted=%v err=%v", accepted, err)
}
if conns != 0 {
t.Fatalf("addConn called %d times, want 0", conns)
}
waitFor(t, "the leftover announcement file to drop", func() bool {
_, err := l.storage.Get(ctx, name)
return err == errNotFound
})
if _, err := l.storage.Get(ctx, ring); err != nil {
t.Fatalf("the ring of an active session was dropped: %v", err)
}
}
func TestAcceptPendingTrulyLeftoverDropped(t *testing.T) {
l := newTestListener(t, t.TempDir())
l.params.sessionTTL = 50 * time.Millisecond
var conns int
l.addConn = func(c stat.Connection) { conns++; c.Close() }
ctx := context.Background()
session := "leftover"
ring := sessionPrefix(session) + "/" + uplinkDir + "/s0"
if err := l.storage.Put(ctx, ring, []byte("x")); err != nil {
t.Fatal(err)
}
name := announceName(session, time.Now())
if err := l.storage.Put(ctx, name, []byte("{}")); err != nil {
t.Fatal(err)
}
l.firstSeen = map[string]time.Time{announceEntry(name): time.Now().Add(-time.Hour)}
accepted, err := l.acceptPending(ctx)
if err != nil || accepted {
t.Fatalf("accepted=%v err=%v", accepted, err)
}
waitFor(t, "the leftover announcement and ring to drop", func() bool {
_, e1 := l.storage.Get(ctx, name)
_, e2 := l.storage.Get(ctx, ring)
return e1 == errNotFound && e2 == errNotFound
})
}
func TestAcceptPendingPrunesFirstSeen(t *testing.T) {
l := newTestListener(t, t.TempDir())
l.addConn = func(c stat.Connection) { c.Close() }
ctx := context.Background()
l.firstSeen = map[string]time.Time{"gone-entry": time.Now()}
if _, err := l.acceptPending(ctx); err != nil {
t.Fatal(err)
}
if _, still := l.firstSeen["gone-entry"]; still {
t.Fatal("firstSeen kept an entry that is no longer listed")
}
}
-4
View File
@@ -375,10 +375,6 @@ func TestStaleAnnounce(t *testing.T) {
if err := listener.storage.Put(ctx, uplinkPrefix("ghost")+"/000000000.seg", []byte("x")); err != nil {
t.Fatalf("Put: %v", err)
}
// The listener first saw this entry an hour ago on its own clock.
listener.firstSeen = map[string]time.Time{
strings.TrimPrefix(stale, sessionsDir+"/"): time.Now().Add(-time.Hour),
}
accepted, err := listener.acceptPending(ctx)
if err != nil {