Files
DragonCoreSSH-NewWEB/quota_hardening_test.go
T
2026-07-24 10:08:06 -03:00

871 lines
27 KiB
Go

package main
import (
"context"
"errors"
"io"
"net"
"net/http"
"net/http/httptest"
"os"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
"golang.org/x/time/rate"
)
func TestNativeClientMaxConnectionsAndBatchedActiveDelta(t *testing.T) {
oldStore := statsStore
statsStore = &Store{}
defer func() { statsStore = oldStore }()
const uuid = "11111111-1111-1111-1111-111111111111"
m := &XrayManager{
nativeQuotaByUUID: map[string]*xrayNativeQuotaState{
uuid: {maxConns: 1, generation: 1},
},
}
state := m.nativeQuotaState(uuid)
release, acquiredState, ok := m.acquireNativeClientConnection(uuid, "user@example")
if !ok || release == nil {
t.Fatal("first native connection was rejected")
}
if acquiredState != state {
t.Fatal("connection lease did not retain the authenticated policy state")
}
if _, _, ok := m.acquireNativeClientConnection(uuid, "user@example"); ok {
t.Fatal("connection above max_conns was accepted")
}
m.nativeDBMu.Lock()
pending := m.nativeActivePending[uuid]
m.nativeDBMu.Unlock()
if pending.Delta != 1 || !pending.Connected || pending.State != state {
t.Fatalf("connect was not queued for batch persistence: %+v", pending)
}
release()
release() // idempotent release must not underflow counters.
m.nativeDBMu.Lock()
pending = m.nativeActivePending[uuid]
m.nativeDBMu.Unlock()
if pending.Delta != 0 || !pending.Connected {
t.Fatalf("connect/disconnect batch should net to zero and retain last-active: %+v", pending)
}
state.mu.Lock()
active := state.activeConns
state.mu.Unlock()
if active != 0 {
t.Fatalf("active connection count = %d, want 0", active)
}
release2, _, ok := m.acquireNativeClientConnection(uuid, "user@example")
if !ok {
t.Fatal("slot was not reusable after release")
}
release2()
}
func TestNativeOnlineUsersAreKeyedByUUID(t *testing.T) {
oldStore := statsStore
statsStore = nil
defer func() { statsStore = oldStore }()
m := &XrayManager{}
m.recordNativeConnect("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "shared@example", nil)
m.recordNativeConnect("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "shared@example", nil)
if got := m.CountOnlineUsers(); got != 2 {
t.Fatalf("online UUID count = %d, want 2 for two UUIDs sharing one email", got)
}
m.statsMu.RLock()
_, first := m.statsByEmail["aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"]
_, second := m.statsByEmail["bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"]
m.statsMu.RUnlock()
if !first || !second {
t.Fatal("native runtime stats were not stored under canonical UUID keys")
}
}
func TestNativeExpiryRejectsAndDisconnectsClient(t *testing.T) {
const uuid = "cccccccc-cccc-cccc-cccc-cccccccccccc"
state := &xrayNativeQuotaState{hasExpiry: true, expiresAt: time.Now().Add(time.Hour), generation: 1}
m := &XrayManager{nativeQuotaByUUID: map[string]*xrayNativeQuotaState{uuid: state}}
closer := &closeTrackingReader{}
release, _, ok := m.acquireNativeClientConnection(uuid, "expiry@example", closer)
if !ok {
t.Fatal("unexpired client was rejected")
}
m.disconnectNativeClient(uuid)
if !closer.closed.Load() {
t.Fatal("active native client was not closed during revocation")
}
release()
state.mu.Lock()
state.expiresAt = time.Now().Add(-time.Second)
state.mu.Unlock()
if reason := m.nativeClientAccessDenied(uuid); reason != "expired" {
t.Fatalf("expired client denial = %q, want expired", reason)
}
if _, _, ok := m.acquireNativeClientConnection(uuid, "expiry@example"); ok {
t.Fatal("expired client acquired a new connection")
}
}
func TestQuotaAndExpiryValidationRejectsUnsafeInput(t *testing.T) {
if err := validateQuotaConfig(1, "typo", 1); err == nil {
t.Fatal("unknown quota action was accepted")
}
if err := validateQuotaConfig(1, quotaActionBlock, -1); err == nil {
t.Fatal("negative throttle setting was accepted")
}
if _, err := parseOptionalXrayExpiry("not-a-date"); err == nil {
t.Fatal("invalid Xray expiry was accepted")
}
if exp, err := parseOptionalXrayExpiry(""); err != nil || exp != nil {
t.Fatalf("empty Xray expiry = (%v, %v), want nil, nil", exp, err)
}
}
func TestRemoveNativeQuotaPolicyPrunesPendingMaps(t *testing.T) {
m := &XrayManager{
nativeQuotaByUUID: map[string]*xrayNativeQuotaState{
"gone": {generation: 1},
},
nativeTrafficPending: map[string]xrayPendingTraffic{
"gone": {Uplink: 10},
},
nativeActivePending: map[string]xrayPendingActive{
"gone": {Delta: 1},
},
}
m.removeNativeQuotaPolicy("gone")
if m.nativeQuotaState("gone") != nil {
t.Fatal("quota policy was not removed")
}
m.nativeDBMu.Lock()
_, trafficExists := m.nativeTrafficPending["gone"]
_, activeExists := m.nativeActivePending["gone"]
m.nativeDBMu.Unlock()
if trafficExists || activeExists {
t.Fatal("deleted UUID remained in a pending persistence map")
}
}
func TestNativeCounterIsBoundedAndReleaseIsIdempotent(t *testing.T) {
var active atomicInt64ForTest
release1, ok := acquireNativeCounter(&active.Int64, 2)
if !ok {
t.Fatal("first slot rejected")
}
release2, ok := acquireNativeCounter(&active.Int64, 2)
if !ok {
t.Fatal("second slot rejected")
}
if _, ok := acquireNativeCounter(&active.Int64, 2); ok {
t.Fatal("slot above limit accepted")
}
release1()
release1()
if got := active.Load(); got != 1 {
t.Fatalf("active after double release = %d, want 1", got)
}
release2()
if got := active.Load(); got != 0 {
t.Fatalf("active after releases = %d, want 0", got)
}
}
// Embedding keeps the test declaration readable while still passing the exact
// atomic.Int64 type required by acquireNativeCounter.
type atomicInt64ForTest struct{ Int64 atomic.Int64 }
func (a *atomicInt64ForTest) Load() int64 { return a.Int64.Load() }
func TestTrackedNativeConnectionsAreClosedOnShutdown(t *testing.T) {
oldAccepting := nativeTransportAccepting.Load()
defer nativeTransportAccepting.Store(oldAccepting)
beginNativeTransportAccepting()
before := nativeTransportConnections.Load()
serverSide, clientSide := net.Pipe()
defer clientSide.Close()
wrapped, ok := wrapTrackedNativeTransportConn(serverSide)
if !ok {
t.Fatal("tracked connection was rejected")
}
if got := nativeTransportConnections.Load(); got != before+1 {
t.Fatalf("transport count = %d, want %d", got, before+1)
}
stopNativeTransportAccepting()
closeAllNativeTransportConnections()
_ = clientSide.SetReadDeadline(time.Now().Add(time.Second))
if _, err := clientSide.Read(make([]byte, 1)); err == nil {
t.Fatal("peer remained open after native shutdown")
}
if err := wrapped.Close(); err != nil && !errors.Is(err, net.ErrClosed) {
t.Fatalf("second close returned unexpected error: %v", err)
}
if got := nativeTransportConnections.Load(); got != before {
t.Fatalf("transport count after shutdown = %d, want %d", got, before)
}
}
func TestXHTTPSessionsIgnoreLegacyGlobalCapAndReleaseCounters(t *testing.T) {
before := nativeXHTTPSessions.Load()
ib := &nativeInbound{xhttpMaxBufferedPosts: 2}
for _, id := range []string{"one", "two"} {
if sess := ib.upsertXHTTPSession(httptest.NewRecorder(), id); sess == nil {
t.Fatalf("session %q was rejected", id)
}
}
if got := nativeXHTTPSessions.Load(); got != before+2 {
t.Fatalf("global XHTTP sessions = %d, want %d", got, before+2)
}
ib.closeAllXHTTPSessions()
if got := nativeXHTTPSessions.Load(); got != before {
t.Fatalf("global XHTTP sessions after close = %d, want %d", got, before)
}
ib.xhttpMu.Lock()
remaining := len(ib.xhttpSessions)
ib.xhttpMu.Unlock()
if remaining != 0 {
t.Fatalf("inbound retained %d XHTTP sessions", remaining)
}
}
func TestXHTTPSessionSafetyWindowIsAboveProductionScale(t *testing.T) {
if got := (&nativeInbound{}).xhttpMaxActiveSessions(); got != fixedNativeMaxXHTTPSessions {
t.Fatalf("XHTTP session safety window = %d, want %d", got, fixedNativeMaxXHTTPSessions)
}
if got := nativeXHTTPSessionLimit(); got < 8_000 {
t.Fatalf("XHTTP session safety window = %d, want room for at least 8K users", got)
}
}
func TestNativeHTTP2StreamsUseFiniteTransportBackpressure(t *testing.T) {
if got := nativeHTTP2MaxConcurrentStreams(); got != fixedNativeHTTP2ConcurrentStreams {
t.Fatalf("HTTP/2 stream setting = %d, want %d", got, fixedNativeHTTP2ConcurrentStreams)
}
if got := nativeHTTP2MaxConcurrentStreams(); got < 1024 {
t.Fatalf("HTTP/2 stream setting = %d, too small for XHTTP packet bursts", got)
}
if got := nativeMuxMaxSessionLimit(); got != 64 {
t.Fatalf("per-transport Mux session guard = %d, want 64", got)
}
}
func TestXHTTPHandlerDoesNotApplyWebRequestCeiling(t *testing.T) {
ib := &nativeInbound{transport: "xhttp", path: "/"}
req := httptest.NewRequest(http.MethodOptions, "/", nil)
rec := httptest.NewRecorder()
ib.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("XHTTP OPTIONS at legacy request ceiling = %d, want 200", rec.Code)
}
}
func TestXHTTPHandlerSafetySlotIsReleased(t *testing.T) {
before := nativeXHTTPRequests.Load()
ib := &nativeInbound{transport: "xhttp", path: "/"}
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
ib.ServeHTTP(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("empty XHTTP request status = %d, want 400", rec.Code)
}
if got := nativeXHTTPRequests.Load(); got != before {
t.Fatalf("XHTTP handler counter after return = %d, want %d", got, before)
}
}
func TestPersistedXHTTPAdmissionTuningIsAlwaysUnlimited(t *testing.T) {
got := normalizeNativeXrayTuning(&XrayNativeTuning{
MuxGlobalSessions: 8192,
MaxConcurrentConnections: 4096,
MaxConcurrentXHTTPRequests: 8192,
XHTTPMaxSessions: 4096,
})
if got.MuxGlobalSessions != 8192 ||
got.MaxConcurrentConnections != -1 ||
got.MaxConcurrentXHTTPRequests != defaultNativeMaxXHTTPRequests ||
got.XHTTPMaxSessions != -1 {
t.Fatalf("persisted admission limits were not removed: %+v", got)
}
}
func TestXHTTPMetadataLengthIsBoundedBeforeSessionAllocation(t *testing.T) {
ib := &nativeInbound{transport: "xhttp", path: "/"}
req := httptest.NewRequest("GET", "/"+strings.Repeat("a", nativeXHTTPMaxSessionIDBytes+1), nil)
rec := httptest.NewRecorder()
ib.ServeHTTP(rec, req)
if rec.Code != 400 {
t.Fatalf("oversized XHTTP session id status = %d, want 400", rec.Code)
}
ib.xhttpMu.Lock()
sessions := len(ib.xhttpSessions)
ib.xhttpMu.Unlock()
if sessions != 0 {
t.Fatalf("oversized metadata allocated %d sessions", sessions)
}
}
func TestXHTTPUploadMemoryIsReleasedOnReadAndClose(t *testing.T) {
before := nativeXHTTPBufferedBytes.Load()
q := newNativeXHTTPUploadQueue(4, 512)
accounted := nativeXHTTPAccountedPacketBytes(4)
lease, ok := acquireNativeXHTTPMemory(accounted)
if !ok {
t.Fatal("failed to reserve XHTTP test memory")
}
if err := q.push(context.Background(), nativeXHTTPPacket{Payload: []byte("test"), Seq: 0}, lease); err != nil {
lease.release()
t.Fatalf("queue push failed: %v", err)
}
lease.release() // transferred leases are a no-op for the producer.
if got := nativeXHTTPBufferedBytes.Load(); got != before+accounted {
t.Fatalf("buffered bytes after push = %d, want %d", got, before+accounted)
}
buf := make([]byte, 4)
if n, err := q.Read(buf); err != nil || n != 4 || string(buf) != "test" {
t.Fatalf("queue read = (%d, %v, %q), want (4, nil, test)", n, err, string(buf))
}
if got := nativeXHTTPBufferedBytes.Load(); got != before {
t.Fatalf("buffered bytes after read = %d, want %d", got, before)
}
lease, ok = acquireNativeXHTTPMemory(nativeXHTTPAccountedPacketBytes(3))
if !ok {
t.Fatal("failed to reserve second XHTTP test memory")
}
if err := q.push(context.Background(), nativeXHTTPPacket{Payload: []byte("xyz"), Seq: 2}, lease); err != nil {
lease.release()
t.Fatalf("second queue push failed: %v", err)
}
lease.release()
q.close()
if got := nativeXHTTPBufferedBytes.Load(); got != before {
t.Fatalf("buffered bytes after close = %d, want %d", got, before)
}
}
func TestXHTTPUploadQueueEnforcesPerSessionByteBudget(t *testing.T) {
before := nativeXHTTPBufferedBytes.Load()
q := newNativeXHTTPUploadQueue(4, nativeXHTTPMinPacketAccountingBytes-1)
defer q.close()
lease, ok := acquireNativeXHTTPMemory(nativeXHTTPAccountedPacketBytes(5))
if !ok {
t.Fatal("failed to reserve XHTTP test memory")
}
defer lease.release()
err := q.push(context.Background(), nativeXHTTPPacket{Payload: make([]byte, 5)}, lease)
if !errors.Is(err, errNativeXHTTPUploadBufferFull) {
t.Fatalf("oversized queue push error = %v, want buffer limit", err)
}
lease.release()
if got := nativeXHTTPBufferedBytes.Load(); got != before {
t.Fatalf("rejected payload retained %d bytes, baseline %d", got, before)
}
}
func TestXHTTPUploadQueueBackpressuresInsteadOfRejectingBurst(t *testing.T) {
before := nativeXHTTPBufferedBytes.Load()
q := newNativeXHTTPUploadQueue(2, nativeXHTTPMinPacketAccountingBytes)
defer q.close()
first, ok := acquireNativeXHTTPMemory(nativeXHTTPAccountedPacketBytes(4))
if !ok {
t.Fatal("failed to reserve first XHTTP payload")
}
if err := q.push(context.Background(), nativeXHTTPPacket{Payload: []byte("one!"), Seq: 0}, first); err != nil {
first.release()
t.Fatalf("first queue push failed: %v", err)
}
first.release()
second, ok := acquireNativeXHTTPMemory(nativeXHTTPAccountedPacketBytes(4))
if !ok {
t.Fatal("failed to reserve second XHTTP payload")
}
done := make(chan error, 1)
go func() {
done <- q.push(context.Background(), nativeXHTTPPacket{Payload: []byte("two!"), Seq: 1}, second)
}()
select {
case err := <-done:
second.release()
t.Fatalf("second burst packet did not backpressure: %v", err)
case <-time.After(25 * time.Millisecond):
}
buf := make([]byte, 4)
if n, err := q.Read(buf); err != nil || n != 4 || string(buf) != "one!" {
second.release()
t.Fatalf("first queue read = (%d, %v, %q)", n, err, string(buf))
}
select {
case err := <-done:
if err != nil {
second.release()
t.Fatalf("backpressured packet failed after space released: %v", err)
}
second.release()
case <-time.After(time.Second):
second.release()
t.Fatal("backpressured packet did not resume")
}
q.close()
if got := nativeXHTTPBufferedBytes.Load(); got != before {
t.Fatalf("backpressure test leaked %d buffered bytes (baseline %d)", got, before)
}
}
func TestXHTTPGlobalMemoryBackpressureWakesWaitersFIFO(t *testing.T) {
before := nativeXHTTPBufferedBytes.Load()
fillBytes := nativeXHTTPMaxBufferedGlobalBytes - before
filler, ok := acquireNativeXHTTPMemory(fillBytes)
if !ok {
t.Fatal("failed to fill XHTTP memory budget for waiter test")
}
defer filler.release()
type result struct {
lease *nativeXHTTPMemoryLease
err error
}
startWaiter := func() <-chan result {
done := make(chan result, 1)
go func() {
lease, err := acquireNativeXHTTPMemoryContext(context.Background(), nativeXHTTPMinPacketAccountingBytes)
done <- result{lease: lease, err: err}
}()
return done
}
waitForWaiters := func(want int) {
t.Helper()
deadline := time.Now().Add(time.Second)
for {
nativeXHTTPMemoryWait.Lock()
got := nativeXHTTPMemoryWait.queued
nativeXHTTPMemoryWait.Unlock()
if got == want {
return
}
if time.Now().After(deadline) {
t.Fatalf("memory waiters = %d, want %d", got, want)
}
time.Sleep(time.Millisecond)
}
}
firstDone := startWaiter()
waitForWaiters(1)
secondDone := startWaiter()
waitForWaiters(2)
filler.shrink(fillBytes - nativeXHTTPMinPacketAccountingBytes)
first := <-firstDone
if first.err != nil || first.lease == nil {
t.Fatalf("first memory waiter = (%v, %v)", first.lease, first.err)
}
select {
case second := <-secondDone:
if second.lease != nil {
second.lease.release()
}
t.Fatalf("second waiter woke before FIFO capacity was released: %v", second.err)
case <-time.After(25 * time.Millisecond):
}
first.lease.release()
select {
case second := <-secondDone:
if second.err != nil || second.lease == nil {
t.Fatalf("second memory waiter = (%v, %v)", second.lease, second.err)
}
second.lease.release()
case <-time.After(time.Second):
t.Fatal("second memory waiter did not wake after first released")
}
filler.release()
if got := nativeXHTTPBufferedBytes.Load(); got != before {
t.Fatalf("FIFO waiter test leaked %d buffered bytes (baseline %d)", got, before)
}
}
func TestXHTTPReassemblyHasNoPacketRequestCountCeiling(t *testing.T) {
before := nativeXHTTPBufferedBytes.Load()
q := newNativeXHTTPUploadQueue(1, 4*nativeXHTTPMinPacketAccountingBytes)
defer q.close()
done := make(chan error, 1)
go func() {
for _, seq := range []uint64{3, 2, 1, 0} {
lease, ok := acquireNativeXHTTPMemory(nativeXHTTPAccountedPacketBytes(1))
if !ok {
done <- errors.New("could not reserve packet memory")
return
}
err := q.push(context.Background(), nativeXHTTPPacket{Payload: []byte{byte('a' + seq)}, Seq: seq}, lease)
lease.release()
if err != nil {
done <- err
return
}
}
done <- nil
}()
buf := make([]byte, 1)
for want := byte('a'); want <= byte('d'); want++ {
n, err := q.Read(buf)
if err != nil || n != 1 || buf[0] != want {
t.Fatalf("reassembled packet = (%d, %v, %q), want %q", n, err, buf[:n], []byte{want})
}
}
if err := <-done; err != nil {
t.Fatalf("out-of-order burst was rejected: %v", err)
}
q.close()
if got := nativeXHTTPBufferedBytes.Load(); got != before {
t.Fatalf("reassembly test leaked %d buffered bytes (baseline %d)", got, before)
}
}
func TestXHTTPBodyReservationUsesActualContentLength(t *testing.T) {
ib := &nativeInbound{xhttpMaxEachPostBytes: 1_000_000}
req := httptest.NewRequest(http.MethodPost, "/session/0", strings.NewReader("small"))
if got := ib.xhttpUploadReservationBytes(req); got != nativeXHTTPMinPacketAccountingBytes {
t.Fatalf("body reservation = %d, want minimum accounted packet size", got)
}
req.ContentLength = -1
if got := ib.xhttpUploadReservationBytes(req); got != 1_000_000 {
t.Fatalf("chunked body reservation = %d, want configured maximum", got)
}
}
func TestNativeQuotaResetWaitsForInFlightTraffic(t *testing.T) {
const uuid = "22222222-2222-2222-2222-222222222222"
state := &xrayNativeQuotaState{usedBytes: 123, generation: 1}
m := &XrayManager{nativeQuotaByUUID: map[string]*xrayNativeQuotaState{uuid: state}}
state.trafficMu.RLock()
done := make(chan struct{})
go func() {
m.resetNativeQuotaUsage(uuid)
close(done)
}()
select {
case <-done:
state.trafficMu.RUnlock()
t.Fatal("traffic reset crossed an in-flight writer boundary")
case <-time.After(25 * time.Millisecond):
}
state.trafficMu.RUnlock()
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("traffic reset did not complete after writer released")
}
state.mu.Lock()
used, generation := state.usedBytes, state.generation
state.mu.Unlock()
if used != 0 || generation != 2 {
t.Fatalf("reset state = used %d generation %d, want 0/2", used, generation)
}
}
func TestNativeRateWaitCanBeCanceled(t *testing.T) {
lim := rate.NewLimiter(1, 1)
if !lim.AllowN(time.Now(), 1) {
t.Fatal("failed to consume initial limiter token")
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
if err := waitNativeRate(ctx, lim, 1); !errors.Is(err, context.Canceled) {
t.Fatalf("waitNativeRate error = %v, want context.Canceled", err)
}
}
func TestSSHDirtyQueueDoesNotScanInactiveUsers(t *testing.T) {
sshTrafficDirtyMu.Lock()
old := sshTrafficDirty
sshTrafficDirty = make(map[string]*UserState)
sshTrafficDirtyMu.Unlock()
defer func() {
sshTrafficDirtyMu.Lock()
sshTrafficDirty = old
sshTrafficDirtyMu.Unlock()
}()
active := &UserState{Cfg: UserConfig{Username: "active"}}
inactive := &UserState{Cfg: UserConfig{Username: "inactive"}}
markSSHUserTrafficDirty(active)
dirty := takeSSHUserTrafficDirty()
if len(dirty) != 1 || dirty["active"] != active {
t.Fatalf("dirty queue = %#v", dirty)
}
if _, found := dirty[inactive.Cfg.Username]; found {
t.Fatal("inactive user appeared in dirty queue")
}
if next := takeSSHUserTrafficDirty(); len(next) != 0 {
t.Fatalf("dirty queue was not drained: %#v", next)
}
}
func TestOldNativeConnectionCannotDecrementReplacementAccount(t *testing.T) {
oldStore := statsStore
statsStore = &Store{}
defer func() { statsStore = oldStore }()
const uuid = "replacement-active-user"
oldState := &xrayNativeQuotaState{maxConns: 1, generation: 1}
m := &XrayManager{nativeQuotaByUUID: map[string]*xrayNativeQuotaState{uuid: oldState}}
release, acquiredState, ok := m.acquireNativeClientConnection(uuid, "old@example")
if !ok || acquiredState != oldState {
t.Fatal("failed to acquire old account connection")
}
// Discard the old account's successful connect delta so this assertion only
// measures what happens when that old connection later disconnects.
m.nativeDBMu.Lock()
m.nativeActivePending = nil
m.nativeDBMu.Unlock()
newState := &xrayNativeQuotaState{maxConns: 1, generation: 1}
m.nativeQuotaMu.Lock()
m.nativeQuotaByUUID[uuid] = newState
m.nativeQuotaMu.Unlock()
release()
m.nativeDBMu.Lock()
pending := m.nativeActivePending[uuid]
m.nativeDBMu.Unlock()
if pending.Delta != 0 || pending.State != nil {
t.Fatalf("old disconnect was queued against replacement account: %+v", pending)
}
newState.mu.Lock()
active := newState.activeConns
newState.mu.Unlock()
if active != 0 {
t.Fatalf("replacement account active count changed to %d", active)
}
}
func TestOldNativeTrafficCannotAttachToReplacementAccount(t *testing.T) {
oldStore := statsStore
statsStore = &Store{}
defer func() { statsStore = oldStore }()
const uuid = "replacement-traffic-user"
oldState := &xrayNativeQuotaState{generation: 1}
newState := &xrayNativeQuotaState{generation: 1}
m := &XrayManager{nativeQuotaByUUID: map[string]*xrayNativeQuotaState{uuid: oldState}}
meter := newTrafficMeter(uuid, "old@example", true, oldState)
meter.n = 1234
m.nativeQuotaMu.Lock()
m.nativeQuotaByUUID[uuid] = newState
m.nativeQuotaMu.Unlock()
oldMgr := xrayMgr
xrayMgr = m
defer func() { xrayMgr = oldMgr }()
meter.flush()
m.nativeDBMu.Lock()
pending := m.nativeTrafficPending[uuid]
m.nativeDBMu.Unlock()
if pending.Uplink != 0 || pending.Downlink != 0 || pending.State != nil {
t.Fatalf("old traffic was queued against replacement account: %+v", pending)
}
m.statsMu.RLock()
stat := m.statsByEmail["old@example"]
m.statsMu.RUnlock()
if stat.Uplink != 0 || stat.Downlink != 0 {
t.Fatalf("old traffic resurfaced in runtime stats: %+v", stat)
}
}
func TestNativeFlusherDropsMismatchedPolicyIdentity(t *testing.T) {
oldStore := statsStore
statsStore = &Store{}
defer func() { statsStore = oldStore }()
const uuid = "identity-prune-user"
oldState := &xrayNativeQuotaState{generation: 1}
newState := &xrayNativeQuotaState{generation: 1}
m := &XrayManager{
nativeQuotaByUUID: map[string]*xrayNativeQuotaState{uuid: newState},
nativeTrafficPending: map[string]xrayPendingTraffic{
uuid: {Email: "old@example", Uplink: 99, State: oldState},
},
nativeActivePending: map[string]xrayPendingActive{
uuid: {Email: "old@example", Delta: -1, State: oldState},
},
}
m.flushNativeStatsToDB()
m.nativeDBMu.Lock()
defer m.nativeDBMu.Unlock()
if len(m.nativeTrafficPending) != 0 || len(m.nativeActivePending) != 0 {
t.Fatalf("mismatched pending deltas survived prune: traffic=%v active=%v", m.nativeTrafficPending, m.nativeActivePending)
}
}
func TestNativeMuxFinishRunsOnce(t *testing.T) {
var calls atomic.Int32
s := &nativeMuxSession{
closed: make(chan struct{}),
uplink: make(chan nativeMuxUplinkItem),
onClose: func(*nativeMuxSession) {
calls.Add(1)
},
}
var wg sync.WaitGroup
for i := 0; i < 32; i++ {
wg.Add(1)
go func() {
defer wg.Done()
s.finish()
}()
}
wg.Wait()
if got := calls.Load(); got != 1 {
t.Fatalf("mux onClose called %d times, want 1", got)
}
}
type closeTrackingReader struct {
closed atomic.Bool
}
func (r *closeTrackingReader) Read([]byte) (int, error) { return 0, io.EOF }
func (r *closeTrackingReader) Close() error {
r.closed.Store(true)
return nil
}
func TestNativeXHTTPQueueCloseClosesQueuedStreamReader(t *testing.T) {
q := newNativeXHTTPUploadQueue(1, 1024)
r := &closeTrackingReader{}
if err := q.push(context.Background(), nativeXHTTPPacket{Reader: r}, nil); err != nil {
t.Fatalf("queue stream reader: %v", err)
}
q.close()
if !r.closed.Load() {
t.Fatal("queued stream reader was not closed during queue shutdown")
}
}
func TestNativeXHTTPQueueSkipsEmptyPacketsWithoutZeroProgressRead(t *testing.T) {
before := nativeXHTTPBufferedBytes.Load()
q := newNativeXHTTPUploadQueue(2, 2*nativeXHTTPMinPacketAccountingBytes)
defer q.close()
for seq, payload := range [][]byte{nil, []byte("x")} {
accounted := nativeXHTTPAccountedPacketBytes(int64(len(payload)))
lease, ok := acquireNativeXHTTPMemory(accounted)
if !ok {
t.Fatal("failed to reserve packet memory")
}
if err := q.push(context.Background(), nativeXHTTPPacket{Payload: payload, Seq: uint64(seq)}, lease); err != nil {
lease.release()
t.Fatalf("queue packet %d: %v", seq, err)
}
lease.release()
}
buf := make([]byte, 1)
n, err := q.Read(buf)
if err != nil || n != 1 || string(buf[:n]) != "x" {
t.Fatalf("queue read after empty packet = (%d, %v, %q), want (1, nil, x)", n, err, buf[:n])
}
q.close()
if got := nativeXHTTPBufferedBytes.Load(); got != before {
t.Fatalf("empty-packet test leaked %d buffered bytes (baseline %d)", got, before)
}
}
type deadlineUnblockingResponseWriter struct {
header http.Header
writeStart chan struct{}
unblock chan struct{}
startOnce sync.Once
unblockOnce sync.Once
}
func newDeadlineUnblockingResponseWriter() *deadlineUnblockingResponseWriter {
return &deadlineUnblockingResponseWriter{
header: make(http.Header),
writeStart: make(chan struct{}),
unblock: make(chan struct{}),
}
}
func (w *deadlineUnblockingResponseWriter) Header() http.Header { return w.header }
func (w *deadlineUnblockingResponseWriter) WriteHeader(int) {}
func (w *deadlineUnblockingResponseWriter) Flush() {}
func (w *deadlineUnblockingResponseWriter) Write([]byte) (int, error) {
w.startOnce.Do(func() { close(w.writeStart) })
<-w.unblock
return 0, os.ErrDeadlineExceeded
}
func (w *deadlineUnblockingResponseWriter) SetWriteDeadline(deadline time.Time) error {
if !deadline.IsZero() && !deadline.After(time.Now().Add(10*time.Millisecond)) {
w.unblockOnce.Do(func() { close(w.unblock) })
}
return nil
}
func TestNativeXHTTPResponseCloseInterruptsStalledWrite(t *testing.T) {
underlying := newDeadlineUnblockingResponseWriter()
writer := newNativeXHTTPResponseWriter(underlying)
writeDone := make(chan error, 1)
go func() {
_, err := writer.Write([]byte("blocked"))
writeDone <- err
}()
select {
case <-underlying.writeStart:
case <-time.After(time.Second):
t.Fatal("response write did not start")
}
closeDone := make(chan struct{})
go func() {
writer.close()
close(closeDone)
}()
select {
case <-closeDone:
case <-time.After(time.Second):
t.Fatal("response close blocked behind stalled write")
}
select {
case err := <-writeDone:
if !errors.Is(err, os.ErrDeadlineExceeded) {
t.Fatalf("stalled write error = %v, want deadline exceeded", err)
}
case <-time.After(time.Second):
t.Fatal("stalled response write was not interrupted")
}
}