Fix
This commit is contained in:
@@ -72,11 +72,11 @@ Contas SSH e clientes VLESS/VMess do modo nativo podem usar `data_quota_bytes` c
|
|||||||
|
|
||||||
O runtime nativo também possui limites globais para impedir crescimento sem controle de sockets, goroutines e sessões HTTP:
|
O runtime nativo também possui limites globais para impedir crescimento sem controle de sockets, goroutines e sessões HTTP:
|
||||||
|
|
||||||
- `max_concurrent_connections`: conexões de transporte TCP/TLS/WebSocket/XHTTP; padrão `4096`;
|
- `max_concurrent_connections`: conexões de transporte TCP/TLS/WebSocket/XHTTP; padrão `32768`;
|
||||||
- `max_concurrent_xhttp_requests`: handlers XHTTP simultâneos; padrão `8192`;
|
- `max_concurrent_xhttp_requests`: mantido apenas para compatibilidade de configuração; o limite de requisições web fica desativado (`-1`) no XHTTP;
|
||||||
- `xhttp_max_sessions`: sessões XHTTP ativas; padrão `4096`.
|
- `xhttp_max_sessions`: sessões XHTTP ativas; padrão `32768`.
|
||||||
|
|
||||||
Esses campos ficam em **Configurações → Xray → Native Xray scale tuning**. `0` seleciona o padrão seguro e `-1` desativa o respectivo contador de admissão, o que não é recomendado em listeners públicos; conexões HTTP/2 continuam com um limite de 256 streams simultâneos por conexão. Cada transporte Mux aceita no máximo 64 sessões filhas, com limite global padrão de 8192. Sockets WebSocket incompletos têm timeout de handshake, conexões HTTP ociosas têm timeout, e parar/reiniciar o Xray nativo fecha conexões e sessões existentes. Atualizações de tráfego e de conexões ativas são agregadas e persistidas em lote a cada cinco segundos, sem criar uma goroutine ou consulta PostgreSQL por conexão. Entradas pendentes de usuários removidos são descartadas para manter os mapas de retry limitados ao conjunto atual de contas.
|
Esses campos ficam em **Configurações → Xray → Native Xray scale tuning**. O XHTTP é tratado como transporte VPN: rajadas de packet-up usam backpressure cancelável e buffers de bytes limitados, sem respostas `429` nem semântica de “too many requests”. Ao atingir o teto de transporte, novos sockets permanecem no backlog do kernel em vez de serem aceitos e resetados. Conexões HTTP/2 mantêm um limite de fluxo de 1024 streams simultâneos por conexão. Cada transporte Mux aceita no máximo 64 sessões filhas, com limite global padrão de 32768. Sockets WebSocket incompletos têm timeout de handshake, conexões HTTP ociosas têm timeout, e parar/reiniciar o Xray nativo fecha conexões e sessões existentes. Atualizações de tráfego e de conexões ativas são agregadas e persistidas em lote a cada cinco segundos, sem criar uma goroutine ou consulta PostgreSQL por conexão. Entradas pendentes de usuários removidos são descartadas para manter os mapas de retry limitados ao conjunto atual de contas.
|
||||||
|
|
||||||
### Requisitos
|
### Requisitos
|
||||||
|
|
||||||
@@ -613,11 +613,11 @@ SSH accounts and native-mode VLESS/VMess clients can use `data_quota_bytes` with
|
|||||||
|
|
||||||
The native runtime also has global ceilings that prevent unbounded socket, goroutine, and HTTP-session growth:
|
The native runtime also has global ceilings that prevent unbounded socket, goroutine, and HTTP-session growth:
|
||||||
|
|
||||||
- `max_concurrent_connections`: TCP/TLS/WebSocket/XHTTP transport connections; default `4096`;
|
- `max_concurrent_connections`: TCP/TLS/WebSocket/XHTTP transport connections; default `32768`;
|
||||||
- `max_concurrent_xhttp_requests`: simultaneous XHTTP handlers; default `8192`;
|
- `max_concurrent_xhttp_requests`: retained for configuration compatibility; the web-request cap is disabled (`-1`) for XHTTP;
|
||||||
- `xhttp_max_sessions`: active XHTTP sessions; default `4096`.
|
- `xhttp_max_sessions`: active XHTTP sessions; default `32768`.
|
||||||
|
|
||||||
These fields are available under **Settings → Xray → Native Xray scale tuning**. `0` selects the safe default and `-1` disables the corresponding admission counter, which is not recommended on public listeners; HTTP/2 connections still retain a 256-stream concurrent guard. Each Mux transport accepts at most 64 child sessions, with a default global ceiling of 8192. Incomplete WebSocket handshakes time out, idle HTTP connections time out, and stopping/restarting native Xray closes existing transports and XHTTP sessions. Traffic and active-connection changes are aggregated and written in five-second batches rather than creating a PostgreSQL query or goroutine for every connection. Pending retry entries for deleted clients are removed so retry maps stay bounded by the current account set.
|
These fields are available under **Settings → Xray → Native Xray scale tuning**. XHTTP is treated as VPN transport traffic: packet-up bursts use cancelable backpressure and bounded byte buffers, with no `429` or “too many requests” behavior. At the transport ceiling, new sockets remain in the kernel backlog instead of being accepted and reset. HTTP/2 connections retain a 1024-stream flow-control guard per connection. Each Mux transport accepts at most 64 child sessions, with a default global ceiling of 32768. Incomplete WebSocket handshakes time out, idle HTTP connections time out, and stopping/restarting native Xray closes existing transports and XHTTP sessions. Traffic and active-connection changes are aggregated and written in five-second batches rather than creating a PostgreSQL query or goroutine for every connection. Pending retry entries for deleted clients are removed so retry maps stay bounded by the current account set.
|
||||||
|
|
||||||
### Requirements
|
### Requirements
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -211,6 +212,40 @@ func TestNativeProtocolGuardsRemainFinite(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestXHTTPHandlerDoesNotApplyWebRequestCeiling(t *testing.T) {
|
||||||
|
oldLimit := nativeTuneMaxXHTTPRequests.Load()
|
||||||
|
oldActive := nativeXHTTPRequests.Load()
|
||||||
|
nativeTuneMaxXHTTPRequests.Store(1)
|
||||||
|
nativeXHTTPRequests.Store(1)
|
||||||
|
defer func() {
|
||||||
|
nativeTuneMaxXHTTPRequests.Store(oldLimit)
|
||||||
|
nativeXHTTPRequests.Store(oldActive)
|
||||||
|
}()
|
||||||
|
|
||||||
|
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 TestLegacyXHTTPTuningMigratesToVPNDefaults(t *testing.T) {
|
||||||
|
got := normalizeNativeXrayTuning(&XrayNativeTuning{
|
||||||
|
MuxGlobalSessions: 8192,
|
||||||
|
MaxConcurrentConnections: 4096,
|
||||||
|
MaxConcurrentXHTTPRequests: 8192,
|
||||||
|
XHTTPMaxSessions: 4096,
|
||||||
|
})
|
||||||
|
if got.MuxGlobalSessions != defaultNativeMuxGlobalSessions ||
|
||||||
|
got.MaxConcurrentConnections != defaultNativeMaxConnections ||
|
||||||
|
got.MaxConcurrentXHTTPRequests != defaultNativeMaxXHTTPRequests ||
|
||||||
|
got.XHTTPMaxSessions != defaultNativeXHTTPMaxSessions {
|
||||||
|
t.Fatalf("legacy tuning was not migrated: %+v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestXHTTPMetadataLengthIsBoundedBeforeSessionAllocation(t *testing.T) {
|
func TestXHTTPMetadataLengthIsBoundedBeforeSessionAllocation(t *testing.T) {
|
||||||
ib := &nativeInbound{transport: "xhttp", path: "/"}
|
ib := &nativeInbound{transport: "xhttp", path: "/"}
|
||||||
req := httptest.NewRequest("GET", "/"+strings.Repeat("a", nativeXHTTPMaxSessionIDBytes+1), nil)
|
req := httptest.NewRequest("GET", "/"+strings.Repeat("a", nativeXHTTPMaxSessionIDBytes+1), nil)
|
||||||
@@ -288,6 +323,72 @@ func TestXHTTPUploadQueueEnforcesPerSessionByteBudget(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestXHTTPUploadQueueBackpressuresInsteadOfRejectingBurst(t *testing.T) {
|
||||||
|
before := nativeXHTTPBufferedBytes.Load()
|
||||||
|
q := newNativeXHTTPUploadQueue(2, 4)
|
||||||
|
defer q.close()
|
||||||
|
|
||||||
|
first, ok := acquireNativeXHTTPMemory(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(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 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 != 5 {
|
||||||
|
t.Fatalf("body reservation = %d, want actual payload length 5", 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) {
|
func TestNativeQuotaResetWaitsForInFlightTraffic(t *testing.T) {
|
||||||
const uuid = "22222222-2222-2222-2222-222222222222"
|
const uuid = "22222222-2222-2222-2222-222222222222"
|
||||||
state := &xrayNativeQuotaState{usedBytes: 123, generation: 1}
|
state := &xrayNativeQuotaState{usedBytes: 123, generation: 1}
|
||||||
|
|||||||
+41
-5
@@ -189,7 +189,14 @@ func wrapTrackedNativeTransportConn(c net.Conn) (net.Conn, bool) {
|
|||||||
_ = c.Close()
|
_ = c.Close()
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
return registerTrackedNativeTransportConn(c, release)
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerTrackedNativeTransportConn finishes registration when the caller has
|
||||||
|
// already reserved a transport slot. Keeping reservation and Accept separate is
|
||||||
|
// what lets the production listener apply kernel/socket backpressure instead of
|
||||||
|
// accepting and immediately resetting connections at capacity.
|
||||||
|
func registerTrackedNativeTransportConn(c net.Conn, release func()) (net.Conn, bool) {
|
||||||
counted := &nativeCountedConn{Conn: c, release: release}
|
counted := &nativeCountedConn{Conn: c, release: release}
|
||||||
counted.onClose = func() {
|
counted.onClose = func() {
|
||||||
nativeTransportRegistry.Lock()
|
nativeTransportRegistry.Lock()
|
||||||
@@ -208,6 +215,25 @@ func wrapTrackedNativeTransportConn(c net.Conn) (net.Conn, bool) {
|
|||||||
return counted, true
|
return counted, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// waitWrapTrackedNativeTransportConn is used by raw native accept loops. It
|
||||||
|
// holds at most one already-accepted socket while capacity is busy, leaving the
|
||||||
|
// rest in the kernel backlog instead of creating origin-side resets/502s.
|
||||||
|
func waitWrapTrackedNativeTransportConn(c net.Conn) (net.Conn, bool) {
|
||||||
|
if c == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
configureNativeTransportSocket(c)
|
||||||
|
for nativeTransportAccepting.Load() {
|
||||||
|
release, ok := acquireNativeTransportConnection()
|
||||||
|
if ok {
|
||||||
|
return registerTrackedNativeTransportConn(c, release)
|
||||||
|
}
|
||||||
|
time.Sleep(nativeOverloadBackoff)
|
||||||
|
}
|
||||||
|
_ = c.Close()
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
func beginNativeTransportAccepting() {
|
func beginNativeTransportAccepting() {
|
||||||
nativeTransportAccepting.Store(true)
|
nativeTransportAccepting.Store(true)
|
||||||
}
|
}
|
||||||
@@ -229,23 +255,33 @@ func closeAllNativeTransportConnections() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// nativeLimitedListener applies the same pre-authentication ceiling to XHTTP
|
// nativeLimitedListener applies the same pre-authentication ceiling to XHTTP
|
||||||
// listeners. net/http receives only sockets that own a slot; rejected sockets
|
// listeners. net/http receives only sockets that own a slot; when capacity is
|
||||||
// are closed before it can allocate a per-connection goroutine or perform TLS.
|
// busy, new sockets remain in the kernel backlog until a slot becomes available.
|
||||||
type nativeLimitedListener struct {
|
type nativeLimitedListener struct {
|
||||||
net.Listener
|
net.Listener
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l nativeLimitedListener) Accept() (net.Conn, error) {
|
func (l nativeLimitedListener) Accept() (net.Conn, error) {
|
||||||
for {
|
for {
|
||||||
|
// Reserve before accepting. When the transport is at capacity, connections
|
||||||
|
// remain queued by the kernel rather than being accepted and reset, which is
|
||||||
|
// the behavior CDNs commonly report as an origin 502.
|
||||||
|
release, ok := acquireNativeTransportConnection()
|
||||||
|
if !ok {
|
||||||
|
time.Sleep(nativeOverloadBackoff)
|
||||||
|
continue
|
||||||
|
}
|
||||||
c, err := l.Listener.Accept()
|
c, err := l.Listener.Accept()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
release()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if counted, ok := wrapTrackedNativeTransportConn(c); ok {
|
configureNativeTransportSocket(c)
|
||||||
|
if counted, ok := registerTrackedNativeTransportConn(c, release); ok {
|
||||||
return counted, nil
|
return counted, nil
|
||||||
}
|
}
|
||||||
// At the ceiling, a hot accept/close loop can itself consume a CPU core.
|
// Shutdown may race Accept. Registration closes the socket and releases the
|
||||||
// A short fixed backoff also lets the kernel backlog absorb brief spikes.
|
// slot; the next Accept observes the listener close.
|
||||||
time.Sleep(nativeOverloadBackoff)
|
time.Sleep(nativeOverloadBackoff)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-5
@@ -17,17 +17,20 @@ type XrayNativeTuning struct {
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
defaultNativeRuntimeGOMAXPROCS = 0
|
defaultNativeRuntimeGOMAXPROCS = 0
|
||||||
defaultNativeMuxGlobalSessions = 8192
|
defaultNativeMuxGlobalSessions = 32768
|
||||||
defaultNativeMaxConnections = 4096
|
defaultNativeMaxConnections = 32768
|
||||||
defaultNativeMaxXHTTPRequests = 8192
|
// XHTTP packet handlers are governed by HTTP/2 flow control and bounded byte
|
||||||
|
// queues, not a website-style request ceiling. A negative configured value is
|
||||||
|
// normalized to the internal unlimited representation.
|
||||||
|
defaultNativeMaxXHTTPRequests = -1
|
||||||
|
|
||||||
fixedNativeMuxMaxSessions = 64
|
fixedNativeMuxMaxSessions = 64
|
||||||
fixedNativeMuxUDPIdleMS = 120000
|
fixedNativeMuxUDPIdleMS = 120000
|
||||||
fixedNativeMuxUDPReadBuffer = 256 * 1024
|
fixedNativeMuxUDPReadBuffer = 256 * 1024
|
||||||
fixedNativeMuxUDPWriteBuffer = 256 * 1024
|
fixedNativeMuxUDPWriteBuffer = 256 * 1024
|
||||||
|
|
||||||
defaultNativeXHTTPMaxSessions = 4096
|
defaultNativeXHTTPMaxSessions = 32768
|
||||||
defaultNativeHTTP2MaxStreams = 256
|
defaultNativeHTTP2MaxStreams = 1024
|
||||||
// Packet-up posts are also protected by byte budgets in xray_xhttp.go. Keep
|
// Packet-up posts are also protected by byte budgets in xray_xhttp.go. Keep
|
||||||
// the default reorder queue modest so thousands of unauthenticated sessions
|
// the default reorder queue modest so thousands of unauthenticated sessions
|
||||||
// cannot consume large amounts of memory merely by allocating empty channel
|
// cannot consume large amounts of memory merely by allocating empty channel
|
||||||
@@ -60,6 +63,18 @@ func normalizeNativeXrayTuning(t *XrayNativeTuning) XrayNativeTuning {
|
|||||||
t = &XrayNativeTuning{}
|
t = &XrayNativeTuning{}
|
||||||
}
|
}
|
||||||
out := *t
|
out := *t
|
||||||
|
// Migrate the two profiles written by older panel builds. Those defaults were
|
||||||
|
// sized like a web service (4K/8K sessions and a global request cap) and cause
|
||||||
|
// valid high-volume XHTTP VPN traffic to be rejected after an upgrade unless
|
||||||
|
// the persisted values are translated here.
|
||||||
|
legacySafe := out.MaxConcurrentConnections == 4096 && out.MaxConcurrentXHTTPRequests == 8192 && out.XHTTPMaxSessions == 4096
|
||||||
|
legacy2K := out.MaxConcurrentConnections == 8192 && out.MaxConcurrentXHTTPRequests == 16384 && out.XHTTPMaxSessions == 8192
|
||||||
|
if legacySafe || legacy2K {
|
||||||
|
out.MuxGlobalSessions = defaultNativeMuxGlobalSessions
|
||||||
|
out.MaxConcurrentConnections = defaultNativeMaxConnections
|
||||||
|
out.MaxConcurrentXHTTPRequests = defaultNativeMaxXHTTPRequests
|
||||||
|
out.XHTTPMaxSessions = defaultNativeXHTTPMaxSessions
|
||||||
|
}
|
||||||
if out.RuntimeGOMAXPROCS < 0 {
|
if out.RuntimeGOMAXPROCS < 0 {
|
||||||
out.RuntimeGOMAXPROCS = defaultNativeRuntimeGOMAXPROCS
|
out.RuntimeGOMAXPROCS = defaultNativeRuntimeGOMAXPROCS
|
||||||
}
|
}
|
||||||
|
|||||||
+114
-29
@@ -37,6 +37,10 @@ var (
|
|||||||
nativeXHTTPBufferedBytes atomic.Int64
|
nativeXHTTPBufferedBytes atomic.Int64
|
||||||
nativeXHTTPBufferRejected atomic.Int64
|
nativeXHTTPBufferRejected atomic.Int64
|
||||||
errNativeXHTTPUploadBufferFull = errors.New("xhttp upload buffer limit reached")
|
errNativeXHTTPUploadBufferFull = errors.New("xhttp upload buffer limit reached")
|
||||||
|
nativeXHTTPMemoryWait = struct {
|
||||||
|
sync.Mutex
|
||||||
|
changed chan struct{}
|
||||||
|
}{changed: make(chan struct{})}
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -295,13 +299,12 @@ func (ib *nativeInbound) xhttpServerMaxHeaderBytes() int {
|
|||||||
// byte stream to the VLESS/VMess handlers as a net.Conn.
|
// byte stream to the VLESS/VMess handlers as a net.Conn.
|
||||||
func (ib *nativeInbound) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (ib *nativeInbound) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
defer xrayRecover(fmt.Sprintf("native xray XHTTP request inbound=%q method=%s path=%q remote=%s", ib.tag, r.Method, r.URL.RequestURI(), r.RemoteAddr))
|
defer xrayRecover(fmt.Sprintf("native xray XHTTP request inbound=%q method=%s path=%q remote=%s", ib.tag, r.Method, r.URL.RequestURI(), r.RemoteAddr))
|
||||||
releaseRequest, ok := acquireNativeXHTTPRequest()
|
// XHTTP is a VPN transport, not a web API. A single connected user keeps a
|
||||||
if !ok {
|
// long-lived download handler and can generate many short packet-up handlers.
|
||||||
w.Header().Set("Retry-After", "1")
|
// Rejecting handlers at an application request ceiling turns normal tunnel
|
||||||
http.Error(w, "native XHTTP request limit reached", http.StatusTooManyRequests)
|
// bursts into 429s and, through CDNs/reverse proxies, intermittent 502s.
|
||||||
return
|
// HTTP/2 flow control plus the bounded, cancelable upload queues below provide
|
||||||
}
|
// backpressure without applying website rate-limit semantics to tunnel traffic.
|
||||||
defer releaseRequest()
|
|
||||||
if !ib.isXHTTP() {
|
if !ib.isXHTTP() {
|
||||||
logNativePreAuthRejection("native xray: xhttp reject inbound=%q reason=not-xhttp method=%s path=%q host=%q remote=%s", ib.tag, r.Method, r.URL.RequestURI(), r.Host, r.RemoteAddr)
|
logNativePreAuthRejection("native xray: xhttp reject inbound=%q reason=not-xhttp method=%s path=%q host=%q remote=%s", ib.tag, r.Method, r.URL.RequestURI(), r.Host, r.RemoteAddr)
|
||||||
xhttpBadRequest(w)
|
xhttpBadRequest(w)
|
||||||
@@ -558,15 +561,13 @@ func (ib *nativeInbound) upsertXHTTPSession(w http.ResponseWriter, id string) *n
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
if max := ib.xhttpMaxActiveSessions(); max > 0 && len(ib.xhttpSessions) >= max {
|
if max := ib.xhttpMaxActiveSessions(); max > 0 && len(ib.xhttpSessions) >= max {
|
||||||
w.Header().Set("Retry-After", "1")
|
http.Error(w, "native XHTTP session capacity reached", http.StatusServiceUnavailable)
|
||||||
http.Error(w, "native XHTTP session limit reached", http.StatusTooManyRequests)
|
|
||||||
logNativePreAuthRejection("native xray: xhttp session rejected inbound=%q active=%d limit=%d", ib.tag, len(ib.xhttpSessions), max)
|
logNativePreAuthRejection("native xray: xhttp session rejected inbound=%q active=%d limit=%d", ib.tag, len(ib.xhttpSessions), max)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
releaseSlot, ok := acquireNativeXHTTPSession()
|
releaseSlot, ok := acquireNativeXHTTPSession()
|
||||||
if !ok {
|
if !ok {
|
||||||
w.Header().Set("Retry-After", "1")
|
http.Error(w, "native XHTTP global session capacity reached", http.StatusServiceUnavailable)
|
||||||
http.Error(w, "native XHTTP global session limit reached", http.StatusTooManyRequests)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
s := &nativeXHTTPSession{
|
s := &nativeXHTTPSession{
|
||||||
@@ -662,10 +663,14 @@ func (ib *nativeInbound) handleXHTTPPacketUpload(w http.ResponseWriter, r *http.
|
|||||||
http.Error(w, "bad xhttp sequence", http.StatusBadRequest)
|
http.Error(w, "bad xhttp sequence", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
memory, ok := acquireNativeXHTTPMemory(ib.xhttpMaxPostBytes())
|
// Reserve the expected payload rather than the configured maximum. Normal
|
||||||
if !ok {
|
// XHTTP body uploads have a Content-Length, so small packets no longer each
|
||||||
w.Header().Set("Retry-After", "1")
|
// consume a full 1 MB reservation. Unknown/chunked or metadata-carried uploads
|
||||||
http.Error(w, errNativeXHTTPUploadBufferFull.Error(), http.StatusTooManyRequests)
|
// still reserve the maximum before decoding to preserve the hard memory bound.
|
||||||
|
memory, err := acquireNativeXHTTPMemoryContext(r.Context(), ib.xhttpUploadReservationBytes(r))
|
||||||
|
if err != nil {
|
||||||
|
// If the client/CDN canceled while waiting for backpressure, there is no
|
||||||
|
// useful HTTP error to send. Returning also releases every reservation.
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer memory.release()
|
defer memory.release()
|
||||||
@@ -680,9 +685,14 @@ func (ib *nativeInbound) handleXHTTPPacketUpload(w http.ResponseWriter, r *http.
|
|||||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if errors.Is(err, io.ErrClosedPipe) {
|
||||||
|
// A packet can race the stream-down request closing. Acknowledge the late
|
||||||
|
// upload instead of leaking an origin 500/502 into the reconnect path.
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
if errors.Is(err, errNativeXHTTPUploadBufferFull) {
|
if errors.Is(err, errNativeXHTTPUploadBufferFull) {
|
||||||
w.Header().Set("Retry-After", "1")
|
http.Error(w, err.Error(), http.StatusRequestEntityTooLarge)
|
||||||
http.Error(w, err.Error(), http.StatusTooManyRequests)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
xrayTracef("native xray: xhttp packet-up push failed inbound=%q session=%q seq=%d: %v", ib.tag, sess.id, seq, err)
|
xrayTracef("native xray: xhttp packet-up push failed inbound=%q session=%q seq=%d: %v", ib.tag, sess.id, seq, err)
|
||||||
@@ -797,6 +807,22 @@ func (ib *nativeInbound) xhttpMaxPostBytes() int64 {
|
|||||||
return 1_000_000
|
return 1_000_000
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// xhttpUploadReservationBytes returns a safe pre-read reservation. Body-mode
|
||||||
|
// clients normally send Content-Length, which lets thousands of small packets
|
||||||
|
// share the global budget. Header/cookie/auto and chunked bodies reserve the
|
||||||
|
// configured maximum because their decoded size is not known until parsed.
|
||||||
|
func (ib *nativeInbound) xhttpUploadReservationBytes(r *http.Request) int64 {
|
||||||
|
maxBytes := ib.xhttpMaxPostBytes()
|
||||||
|
placement := firstNonEmpty(ib.xhttpUplinkDataPlacement, xhttpPlacementBody)
|
||||||
|
if placement == xhttpPlacementBody && r.ContentLength >= 0 {
|
||||||
|
if r.ContentLength > maxBytes {
|
||||||
|
return maxBytes
|
||||||
|
}
|
||||||
|
return r.ContentLength
|
||||||
|
}
|
||||||
|
return maxBytes
|
||||||
|
}
|
||||||
|
|
||||||
func (ib *nativeInbound) handleXHTTPStreamOne(w http.ResponseWriter, r *http.Request) {
|
func (ib *nativeInbound) handleXHTTPStreamOne(w http.ResponseWriter, r *http.Request) {
|
||||||
defer xrayRecover(fmt.Sprintf("native xray XHTTP stream-one inbound=%q remote=%s", ib.tag, r.RemoteAddr))
|
defer xrayRecover(fmt.Sprintf("native xray XHTTP stream-one inbound=%q remote=%s", ib.tag, r.RemoteAddr))
|
||||||
xrayTracef("native xray: xhttp stream-one inbound=%q len=%d remote=%s", ib.tag, r.ContentLength, r.RemoteAddr)
|
xrayTracef("native xray: xhttp stream-one inbound=%q len=%d remote=%s", ib.tag, r.ContentLength, r.RemoteAddr)
|
||||||
@@ -1081,6 +1107,40 @@ func acquireNativeXHTTPMemory(n int64) (*nativeXHTTPMemoryLease, bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// acquireNativeXHTTPMemoryContext applies process-wide memory backpressure.
|
||||||
|
// Unlike the old fail-fast admission path, a legitimate tunnel burst waits for
|
||||||
|
// queued bytes to be consumed and remains cancelable if its HTTP request ends.
|
||||||
|
func acquireNativeXHTTPMemoryContext(ctx context.Context, n int64) (*nativeXHTTPMemoryLease, error) {
|
||||||
|
if n <= 0 {
|
||||||
|
return &nativeXHTTPMemoryLease{}, nil
|
||||||
|
}
|
||||||
|
if n > nativeXHTTPMaxBufferedGlobalBytes {
|
||||||
|
return nil, errNativeXHTTPUploadBufferFull
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
current := nativeXHTTPBufferedBytes.Load()
|
||||||
|
if current <= nativeXHTTPMaxBufferedGlobalBytes-n && nativeXHTTPBufferedBytes.CompareAndSwap(current, current+n) {
|
||||||
|
return &nativeXHTTPMemoryLease{bytes: n}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
nativeXHTTPMemoryWait.Lock()
|
||||||
|
// Recheck while holding the generation lock so a release cannot happen
|
||||||
|
// between the failed check and subscribing to the notification channel.
|
||||||
|
current = nativeXHTTPBufferedBytes.Load()
|
||||||
|
if current <= nativeXHTTPMaxBufferedGlobalBytes-n {
|
||||||
|
nativeXHTTPMemoryWait.Unlock()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
changed := nativeXHTTPMemoryWait.changed
|
||||||
|
nativeXHTTPMemoryWait.Unlock()
|
||||||
|
select {
|
||||||
|
case <-changed:
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func releaseNativeXHTTPMemory(n int64) {
|
func releaseNativeXHTTPMemory(n int64) {
|
||||||
if n <= 0 {
|
if n <= 0 {
|
||||||
return
|
return
|
||||||
@@ -1092,6 +1152,10 @@ func releaseNativeXHTTPMemory(n int64) {
|
|||||||
next = 0
|
next = 0
|
||||||
}
|
}
|
||||||
if nativeXHTTPBufferedBytes.CompareAndSwap(current, next) {
|
if nativeXHTTPBufferedBytes.CompareAndSwap(current, next) {
|
||||||
|
nativeXHTTPMemoryWait.Lock()
|
||||||
|
close(nativeXHTTPMemoryWait.changed)
|
||||||
|
nativeXHTTPMemoryWait.changed = make(chan struct{})
|
||||||
|
nativeXHTTPMemoryWait.Unlock()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1145,6 +1209,7 @@ type nativeXHTTPUploadQueue struct {
|
|||||||
readDeadline time.Time
|
readDeadline time.Time
|
||||||
bufferedBytes int64
|
bufferedBytes int64
|
||||||
closedFlag bool
|
closedFlag bool
|
||||||
|
spaceChanged chan struct{}
|
||||||
|
|
||||||
closed chan struct{}
|
closed chan struct{}
|
||||||
closeOnce sync.Once
|
closeOnce sync.Once
|
||||||
@@ -1165,6 +1230,7 @@ func newNativeXHTTPUploadQueue(maxPackets int, maxBytes int64) *nativeXHTTPUploa
|
|||||||
maxPackets: maxPackets,
|
maxPackets: maxPackets,
|
||||||
maxBytes: maxBytes,
|
maxBytes: maxBytes,
|
||||||
closed: make(chan struct{}),
|
closed: make(chan struct{}),
|
||||||
|
spaceChanged: make(chan struct{}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1178,21 +1244,38 @@ func (q *nativeXHTTPUploadQueue) beginPush() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *nativeXHTTPUploadQueue) adoptPayloadMemory(memory *nativeXHTTPMemoryLease, n int64) bool {
|
func (q *nativeXHTTPUploadQueue) adoptPayloadMemory(ctx context.Context, memory *nativeXHTTPMemoryLease, n int64) error {
|
||||||
if n <= 0 {
|
if n <= 0 {
|
||||||
return true
|
return nil
|
||||||
}
|
}
|
||||||
if memory == nil || memory.bytes != n {
|
if memory == nil || memory.bytes != n {
|
||||||
return false
|
return errNativeXHTTPUploadBufferFull
|
||||||
}
|
}
|
||||||
q.mu.Lock()
|
if n > q.maxBytes {
|
||||||
defer q.mu.Unlock()
|
return errNativeXHTTPUploadBufferFull
|
||||||
if q.closedFlag || q.bufferedBytes > q.maxBytes-n {
|
}
|
||||||
return false
|
for {
|
||||||
|
q.mu.Lock()
|
||||||
|
if q.closedFlag {
|
||||||
|
q.mu.Unlock()
|
||||||
|
return io.ErrClosedPipe
|
||||||
|
}
|
||||||
|
if q.bufferedBytes <= q.maxBytes-n {
|
||||||
|
q.bufferedBytes += n
|
||||||
|
memory.bytes = 0
|
||||||
|
q.mu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
changed := q.spaceChanged
|
||||||
|
q.mu.Unlock()
|
||||||
|
select {
|
||||||
|
case <-changed:
|
||||||
|
case <-q.closed:
|
||||||
|
return io.ErrClosedPipe
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
q.bufferedBytes += n
|
|
||||||
memory.bytes = 0
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *nativeXHTTPUploadQueue) releasePayloadMemory(n int64) {
|
func (q *nativeXHTTPUploadQueue) releasePayloadMemory(n int64) {
|
||||||
@@ -1205,6 +1288,8 @@ func (q *nativeXHTTPUploadQueue) releasePayloadMemory(n int64) {
|
|||||||
release = q.bufferedBytes
|
release = q.bufferedBytes
|
||||||
}
|
}
|
||||||
q.bufferedBytes -= release
|
q.bufferedBytes -= release
|
||||||
|
close(q.spaceChanged)
|
||||||
|
q.spaceChanged = make(chan struct{})
|
||||||
q.mu.Unlock()
|
q.mu.Unlock()
|
||||||
releaseNativeXHTTPMemory(release)
|
releaseNativeXHTTPMemory(release)
|
||||||
}
|
}
|
||||||
@@ -1234,8 +1319,8 @@ func (q *nativeXHTTPUploadQueue) push(ctx context.Context, p nativeXHTTPPacket,
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
payloadBytes := int64(len(p.Payload))
|
payloadBytes := int64(len(p.Payload))
|
||||||
if !q.adoptPayloadMemory(memory, payloadBytes) {
|
if err := q.adoptPayloadMemory(ctx, memory, payloadBytes); err != nil {
|
||||||
return errNativeXHTTPUploadBufferFull
|
return err
|
||||||
}
|
}
|
||||||
transferred := payloadBytes > 0
|
transferred := payloadBytes > 0
|
||||||
if transferred {
|
if transferred {
|
||||||
|
|||||||
Reference in New Issue
Block a user