This commit is contained in:
2026-07-22 17:30:42 -03:00
parent b903775fb7
commit 3d64d6394b
11 changed files with 173 additions and 223 deletions
+10 -10
View File
@@ -71,13 +71,13 @@ A confirmação dessa migração é exibida dentro do próprio painel. Se a grav
Contas SSH e clientes VLESS/VMess do modo nativo podem usar `data_quota_bytes` com ação `block` ou `throttle`. O botão **Reset/Zerar tráfego** limpa apenas os contadores; não renova validade, senha ou configuração da conta. Não existe reset periódico automático no servidor: qualquer período comercial mostrado no site é independente e o reset ocorre somente por ação explícita no painel/API. O valor `max_conns` é aplicado no momento em que o usuário VLESS/VMess é autenticado e vale em conjunto para TCP, UDP, WebSocket, XHTTP e conexões Mux (uma conexão Mux autenticada conta como uma conexão, independentemente dos streams filhos). Contas SSH e clientes VLESS/VMess do modo nativo podem usar `data_quota_bytes` com ação `block` ou `throttle`. O botão **Reset/Zerar tráfego** limpa apenas os contadores; não renova validade, senha ou configuração da conta. Não existe reset periódico automático no servidor: qualquer período comercial mostrado no site é independente e o reset ocorre somente por ação explícita no painel/API. O valor `max_conns` é aplicado no momento em que o usuário VLESS/VMess é autenticado e vale em conjunto para TCP, UDP, WebSocket, XHTTP e conexões Mux (uma conexão Mux autenticada conta como uma conexão, independentemente dos streams filhos).
O runtime nativo também possui limites globais para impedir crescimento sem controle de sockets, goroutines e sessões HTTP: As antigas chaves globais de admissão continuam no JSON somente para compatibilidade, mas são sempre normalizadas para `-1` (ilimitado), inclusive quando um `config.json` antigo ainda contém `4096`, `8192`, `32768` ou outro valor positivo:
- `max_concurrent_connections`: conexões de transporte TCP/TLS/WebSocket/XHTTP; padrão `32768`; - `max_concurrent_connections`: sem limite global de conexões de transporte;
- `max_concurrent_xhttp_requests`: mantido apenas para compatibilidade de configuração; o limite de requisições web fica desativado (`-1`) no XHTTP; - `max_concurrent_xhttp_requests`: sem limite global de requisições XHTTP;
- `xhttp_max_sessions`: sessões XHTTP ativas; padrão `32768`. - `xhttp_max_sessions`: sem limite global de sessões XHTTP.
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. O painel não expõe mais esses três controles como limites ajustáveis. Xray XHTTP e XHTTP SSH usam o mesmo listener VPN sem teto por quantidade de requisições, streams HTTP/2, conexões de transporte ou sessões XHTTP. Rajadas de `packet-up` e a remontagem fora de ordem usam backpressure cancelável contabilizado em bytes; até pacotes vazios consomem um custo mínimo de memória contabilizada, portanto remover o limite por quantidade não cria uma fila de metadados sem limite. Não existem respostas `429` nem rejeições `503` por capacidade global. As políticas reais por usuário (`max_conns`, cota e banda) continuam ativas. Cada transporte Mux aceita no máximo 64 sessões filhas, com limite global padrão de 32768. 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
@@ -612,13 +612,13 @@ The migration confirmation is rendered inside the panel. If saving fails, the te
SSH accounts and native-mode VLESS/VMess clients can use `data_quota_bytes` with either the `block` or `throttle` action. The **Reset traffic** action clears only usage counters; it does not renew expiry, change a password, or alter account settings. The server does not perform an automatic periodic reset: any commercial period shown on the website is independent, and counters reset only through an explicit panel/API action. `max_conns` is enforced when a native VLESS/VMess user is authenticated and is shared across TCP, UDP, WebSocket, XHTTP, and Mux transports (one authenticated Mux transport counts as one connection, regardless of its child streams). SSH accounts and native-mode VLESS/VMess clients can use `data_quota_bytes` with either the `block` or `throttle` action. The **Reset traffic** action clears only usage counters; it does not renew expiry, change a password, or alter account settings. The server does not perform an automatic periodic reset: any commercial period shown on the website is independent, and counters reset only through an explicit panel/API action. `max_conns` is enforced when a native VLESS/VMess user is authenticated and is shared across TCP, UDP, WebSocket, XHTTP, and Mux transports (one authenticated Mux transport counts as one connection, regardless of its child streams).
The native runtime also has global ceilings that prevent unbounded socket, goroutine, and HTTP-session growth: The old global admission keys remain in JSON for compatibility, but they are always normalized to `-1` (unlimited), including when an old `config.json` still contains `4096`, `8192`, `32768`, or any other positive value:
- `max_concurrent_connections`: TCP/TLS/WebSocket/XHTTP transport connections; default `32768`; - `max_concurrent_connections`: no global transport-connection count cap;
- `max_concurrent_xhttp_requests`: retained for configuration compatibility; the web-request cap is disabled (`-1`) for XHTTP; - `max_concurrent_xhttp_requests`: no global XHTTP-request count cap;
- `xhttp_max_sessions`: active XHTTP sessions; default `32768`. - `xhttp_max_sessions`: no global XHTTP-session count cap.
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. The panel no longer exposes those three controls as adjustable ceilings. Xray XHTTP and XHTTP SSH share the same VPN listener with no count ceiling for requests, HTTP/2 streams, transport connections, or XHTTP sessions. Packet-up bursts and out-of-order reassembly use cancelable byte-accounted backpressure; even empty packets are charged a minimum accounted-memory cost, so removing the request-count limit does not create an unbounded metadata queue. There are no `429` responses or global-capacity `503` rejections. Real per-user policies (`max_conns`, quota, and bandwidth) remain active. Each Mux transport accepts at most 64 child sessions, with a default global ceiling of 32768. 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
+6 -10
View File
@@ -761,28 +761,24 @@ Object.assign(I18N_TEXT["en-US"], {
"Native Xray scale tuning":"Native Xray scale tuning", "Native Xray scale tuning":"Native Xray scale tuning",
"Go CPU threads (GOMAXPROCS)":"Go CPU threads (GOMAXPROCS)", "Go CPU threads (GOMAXPROCS)":"Go CPU threads (GOMAXPROCS)",
"Global mux backend sessions":"Global mux backend sessions", "Global mux backend sessions":"Global mux backend sessions",
"Global transport connections":"Global transport connections", "Transport and XHTTP admission":"Transport and XHTTP admission",
"XHTTP web request cap":"XHTTP web request cap", "Unlimited for VPN traffic. There is no global HTTP request, HTTP/2 stream, transport-connection, or XHTTP-session count cap.":"Unlimited for VPN traffic. There is no global HTTP request, HTTP/2 stream, transport-connection, or XHTTP-session count cap.",
"disabled for VPN traffic":"disabled for VPN traffic",
"Active XHTTP sessions":"Active XHTTP sessions",
"Trace every XHTTP/mux packet":"Trace every XHTTP/mux packet", "Trace every XHTTP/mux packet":"Trace every XHTTP/mux packet",
"debug only, slows QUIC":"debug only, slows QUIC", "debug only, slows QUIC":"debug only, slows QUIC",
"Apply high-traffic VPN defaults":"Apply high-traffic VPN defaults", "Apply high-traffic VPN defaults":"Apply high-traffic VPN defaults",
"Apply safe defaults":"Apply safe defaults", "Apply safe defaults":"Apply safe defaults",
"XHTTP is handled as VPN tunnel traffic: packet requests use bounded backpressure and are never rejected by an HTTP request-rate ceiling. At the transport ceiling, new sockets wait in the kernel backlog instead of being reset. Keep the safe defaults unless the server is sized and load-tested for the high-traffic profile. HTTP/2 retains a 1024-stream flow-control guard per connection, while upload memory stays globally bounded. Saved in the panel config and applied live on restart/reload.":"XHTTP is handled as VPN tunnel traffic: packet requests use bounded backpressure and are never rejected by an HTTP request-rate ceiling. At the transport ceiling, new sockets wait in the kernel backlog instead of being reset. Keep the safe defaults unless the server is sized and load-tested for the high-traffic profile. HTTP/2 retains a 1024-stream flow-control guard per connection, while upload memory stays globally bounded. Saved in the panel config and applied live on restart/reload." "XHTTP is handled as VPN tunnel traffic: packet requests and reassembly are limited only by bounded byte backpressure, never by a request count. Existing saved web-style caps are ignored automatically after update. Per-user max_conns, quota, and bandwidth policies still work normally.":"XHTTP is handled as VPN tunnel traffic: packet requests and reassembly are limited only by bounded byte backpressure, never by a request count. Existing saved web-style caps are ignored automatically after update. Per-user max_conns, quota, and bandwidth policies still work normally."
}); });
Object.assign(I18N_TEXT["pt-BR"], { Object.assign(I18N_TEXT["pt-BR"], {
"Sort by":"Ordenar por","Show":"Mostrar","All":"Todos","Offline":"Offline","Connections":"Conexões","Usage":"Uso","Quota reached":"Cota atingida","{visible} of {total} users":"{visible} de {total} usuários","No Xray users match this filter.":"Nenhum usuário Xray corresponde a este filtro.","No SSH users match this filter.":"Nenhum usuário SSH corresponde a este filtro.", "Sort by":"Ordenar por","Show":"Mostrar","All":"Todos","Offline":"Offline","Connections":"Conexões","Usage":"Uso","Quota reached":"Cota atingida","{visible} of {total} users":"{visible} de {total} usuários","No Xray users match this filter.":"Nenhum usuário Xray corresponde a este filtro.","No SSH users match this filter.":"Nenhum usuário SSH corresponde a este filtro.",
"Native Xray scale tuning":"Ajustes de escala do Xray nativo", "Native Xray scale tuning":"Ajustes de escala do Xray nativo",
"Go CPU threads (GOMAXPROCS)":"Threads de CPU do Go (GOMAXPROCS)", "Go CPU threads (GOMAXPROCS)":"Threads de CPU do Go (GOMAXPROCS)",
"Global mux backend sessions":"Sessões globais de backend Mux", "Global mux backend sessions":"Sessões globais de backend Mux",
"Global transport connections":"Conexões globais de transporte", "Transport and XHTTP admission":"Admissão de transporte e XHTTP",
"XHTTP web request cap":"Limite web de requisições XHTTP", "Unlimited for VPN traffic. There is no global HTTP request, HTTP/2 stream, transport-connection, or XHTTP-session count cap.":"Ilimitado para tráfego VPN. Não existe limite global por quantidade de requisições HTTP, streams HTTP/2, conexões de transporte ou sessões XHTTP.",
"disabled for VPN traffic":"desativado para tráfego VPN",
"Active XHTTP sessions":"Sessões XHTTP ativas",
"Trace every XHTTP/mux packet":"Registrar cada pacote XHTTP/Mux", "Trace every XHTTP/mux packet":"Registrar cada pacote XHTTP/Mux",
"debug only, slows QUIC":"somente debug, reduz a velocidade do QUIC", "debug only, slows QUIC":"somente debug, reduz a velocidade do QUIC",
"Apply high-traffic VPN defaults":"Aplicar padrão VPN de alto tráfego", "Apply high-traffic VPN defaults":"Aplicar padrão VPN de alto tráfego",
"Apply safe defaults":"Aplicar padrões seguros", "Apply safe defaults":"Aplicar padrões seguros",
"XHTTP is handled as VPN tunnel traffic: packet requests use bounded backpressure and are never rejected by an HTTP request-rate ceiling. At the transport ceiling, new sockets wait in the kernel backlog instead of being reset. Keep the safe defaults unless the server is sized and load-tested for the high-traffic profile. HTTP/2 retains a 1024-stream flow-control guard per connection, while upload memory stays globally bounded. Saved in the panel config and applied live on restart/reload.":"O XHTTP é tratado como tráfego de túnel VPN: as requisições de pacotes usam backpressure com memória limitada e nunca são rejeitadas por um limite de requisições web. Ao atingir o teto de transporte, novos sockets aguardam no backlog do kernel em vez de serem resetados. Mantenha os padrões seguros, exceto se o servidor estiver dimensionado e testado para o perfil de alto tráfego. O HTTP/2 mantém um controle de fluxo de 1024 streams por conexão, e a memória de upload continua limitada globalmente. Salvo na configuração do painel e aplicado ao vivo ao reiniciar ou recarregar." "XHTTP is handled as VPN tunnel traffic: packet requests and reassembly are limited only by bounded byte backpressure, never by a request count. Existing saved web-style caps are ignored automatically after update. Per-user max_conns, quota, and bandwidth policies still work normally.":"O XHTTP é tratado como tráfego de túnel VPN: requisições de pacotes e remontagem usam somente backpressure com limite de bytes, nunca limite por quantidade de requisições. Limites web antigos já salvos são ignorados automaticamente após a atualização. As regras por usuário de max_conns, cota e banda continuam funcionando normalmente."
}); });
+13 -10
View File
@@ -29,24 +29,24 @@ function toggleUdpgwFields(on) {
} }
// Only operator-safe knobs remain. Transport buffers (HTTP/2 flow control, XHTTP // Only operator-safe knobs remain. Global transport/XHTTP count admission is
// reorder buffer, mux/UDP buffers) are fixed to xray-core defaults in the backend // fixed to unlimited; byte backpressure and protocol buffers stay internal so a
// and are no longer exposed here, so they cannot be misconfigured. // panel value cannot turn normal VPN traffic into HTTP overload responses.
const XRAY_NATIVE_TUNING_DEFAULTS = { const XRAY_NATIVE_TUNING_DEFAULTS = {
safe: { safe: {
runtime_gomaxprocs: 0, runtime_gomaxprocs: 0,
mux_global_sessions: 32768, mux_global_sessions: 32768,
max_concurrent_connections: 32768, max_concurrent_connections: -1,
max_concurrent_xhttp_requests: -1, max_concurrent_xhttp_requests: -1,
xhttp_max_sessions: 32768, xhttp_max_sessions: -1,
trace_packets: false, trace_packets: false,
}, },
"high": { "high": {
runtime_gomaxprocs: 0, runtime_gomaxprocs: 0,
mux_global_sessions: 65536, mux_global_sessions: 65536,
max_concurrent_connections: 65536, max_concurrent_connections: -1,
max_concurrent_xhttp_requests: -1, max_concurrent_xhttp_requests: -1,
xhttp_max_sessions: 65536, xhttp_max_sessions: -1,
trace_packets: false, trace_packets: false,
}, },
}; };
@@ -54,9 +54,6 @@ const XRAY_NATIVE_TUNING_DEFAULTS = {
const XRAY_NATIVE_TUNING_FIELDS = { const XRAY_NATIVE_TUNING_FIELDS = {
runtime_gomaxprocs: "cfgXrayRuntimeGomaxprocs", runtime_gomaxprocs: "cfgXrayRuntimeGomaxprocs",
mux_global_sessions: "cfgXrayMuxGlobalSessions", mux_global_sessions: "cfgXrayMuxGlobalSessions",
max_concurrent_connections: "cfgXrayMaxConnections",
max_concurrent_xhttp_requests: "cfgXrayMaxXHTTPRequests",
xhttp_max_sessions: "cfgXrayMaxXHTTPSessions",
}; };
function setXrayNativeTuningDefaults(profile = "high") { function setXrayNativeTuningDefaults(profile = "high") {
@@ -80,6 +77,12 @@ function readXrayNativeTuning() {
const el = document.getElementById(id); const el = document.getElementById(id);
out[key] = parseInt(el?.value || "0", 10) || 0; out[key] = parseInt(el?.value || "0", 10) || 0;
}); });
// These legacy JSON keys are intentionally fixed at unlimited. Keeping them
// in saved configs makes upgrades/downgrades explicit without exposing web
// request ceilings that do not belong on a VPN transport.
out.max_concurrent_connections = -1;
out.max_concurrent_xhttp_requests = -1;
out.xhttp_max_sessions = -1;
out.trace_packets = !!document.getElementById("cfgXrayTracePackets")?.checked; out.trace_packets = !!document.getElementById("cfgXrayTracePackets")?.checked;
return out; return out;
} }
+4 -6
View File
@@ -1532,15 +1532,13 @@
<div class="grid2" style="margin-top:10px;gap:8px;"> <div class="grid2" style="margin-top:10px;gap:8px;">
<div class="field"><label>Go CPU threads (GOMAXPROCS)</label><input type="number" min="0" id="cfgXrayRuntimeGomaxprocs" placeholder="0 = all CPU cores"/></div> <div class="field"><label>Go CPU threads (GOMAXPROCS)</label><input type="number" min="0" id="cfgXrayRuntimeGomaxprocs" placeholder="0 = all CPU cores"/></div>
<div class="field"><label>Global mux backend sessions</label><input type="number" min="1" id="cfgXrayMuxGlobalSessions" placeholder="32768"/></div> <div class="field"><label>Global mux backend sessions</label><input type="number" min="1" id="cfgXrayMuxGlobalSessions" placeholder="32768"/></div>
<div class="field"><label>Global transport connections <span class="hint">0=32768, -1=unlimited</span></label><input type="number" min="-1" id="cfgXrayMaxConnections" placeholder="32768"/></div> <div class="field" style="grid-column:1/-1;"><label>Transport and XHTTP admission</label><div class="hint">Unlimited for VPN traffic. There is no global HTTP request, HTTP/2 stream, transport-connection, or XHTTP-session count cap.</div></div>
<div class="field"><label>XHTTP web request cap <span class="hint">disabled for VPN traffic</span></label><input type="number" min="-1" id="cfgXrayMaxXHTTPRequests" value="-1" readonly/></div>
<div class="field"><label>Active XHTTP sessions <span class="hint">0=32768, -1=unlimited</span></label><input type="number" min="-1" id="cfgXrayMaxXHTTPSessions" placeholder="32768"/></div>
<label style="font-size:.73rem;display:flex;align-items:center;gap:5px;cursor:pointer;grid-column:1/-1"><input type="checkbox" id="cfgXrayTracePackets"/> Trace every XHTTP/mux packet <span class="hint">debug only, slows QUIC</span></label> <label style="font-size:.73rem;display:flex;align-items:center;gap:5px;cursor:pointer;grid-column:1/-1"><input type="checkbox" id="cfgXrayTracePackets"/> Trace every XHTTP/mux packet <span class="hint">debug only, slows QUIC</span></label>
<div class="card-actions" style="grid-column:1/-1;"> <div class="card-actions" style="grid-column:1/-1;">
<button class="btn btn-ghost btn-sm" type="button" onclick="setXrayNativeTuningDefaults('high')">Apply high-traffic VPN defaults</button> <button class="btn btn-ghost btn-sm" type="button" onclick="setXrayNativeTuningDefaults('high')">Apply high-traffic VPN defaults</button>
<button class="btn btn-ghost btn-sm" type="button" onclick="setXrayNativeTuningDefaults('safe')">Apply safe defaults</button> <button class="btn btn-ghost btn-sm" type="button" onclick="setXrayNativeTuningDefaults('safe')">Apply safe defaults</button>
</div> </div>
<div class="hint" style="grid-column:1/-1;margin-top:-4px;">XHTTP is handled as VPN tunnel traffic: packet requests use bounded backpressure and are never rejected by an HTTP request-rate ceiling. At the transport ceiling, new sockets wait in the kernel backlog instead of being reset. Keep the safe defaults unless the server is sized and load-tested for the high-traffic profile. HTTP/2 retains a 1024-stream flow-control guard per connection, while upload memory stays globally bounded. Saved in the panel config and applied live on restart/reload.</div> <div class="hint" style="grid-column:1/-1;margin-top:-4px;">XHTTP is handled as VPN tunnel traffic: packet requests and reassembly are limited only by bounded byte backpressure, never by a request count. Existing saved web-style caps are ignored automatically after update. Per-user max_conns, quota, and bandwidth policies still work normally.</div>
</div> </div>
</details> </details>
</div> </div>
@@ -1580,14 +1578,14 @@
<!-- app.js was split into ordered modules for maintainability. They are plain <!-- app.js was split into ordered modules for maintainability. They are plain
classic scripts sharing one global scope; `defer` preserves execution order, classic scripts sharing one global scope; `defer` preserves execution order,
so behavior is identical to the old single file. Keep this load order. --> so behavior is identical to the old single file. Keep this load order. -->
<script defer src="assets/js/01-core.js?v=20260720sshfilters1"></script> <script defer src="assets/js/01-core.js?v=20260722xhttpunlimited2"></script>
<script defer src="assets/js/02-shell.js?v=20260714pamfix1"></script> <script defer src="assets/js/02-shell.js?v=20260714pamfix1"></script>
<script defer src="assets/js/03-ssh-users.js?v=20260720sshfilters1"></script> <script defer src="assets/js/03-ssh-users.js?v=20260720sshfilters1"></script>
<script defer src="assets/js/04-xray.js?v=20260720sshfilters1"></script> <script defer src="assets/js/04-xray.js?v=20260720sshfilters1"></script>
<script defer src="assets/js/05-resellers.js?v=20260714pamfix1"></script> <script defer src="assets/js/05-resellers.js?v=20260714pamfix1"></script>
<script defer src="assets/js/06-servers.js?v=20260714pamfix1"></script> <script defer src="assets/js/06-servers.js?v=20260714pamfix1"></script>
<script defer src="assets/js/07-stats-logs.js?v=20260714pamfix1"></script> <script defer src="assets/js/07-stats-logs.js?v=20260714pamfix1"></script>
<script defer src="assets/js/08-server-config.js?v=20260719xhttp502fix1"></script> <script defer src="assets/js/08-server-config.js?v=20260722xhttpunlimited2"></script>
<script defer src="assets/js/09-xray-wizard.js?v=20260714quota1"></script> <script defer src="assets/js/09-xray-wizard.js?v=20260714quota1"></script>
<script defer src="assets/js/11-update-status.js?v=20260714pamfix1"></script> <script defer src="assets/js/11-update-status.js?v=20260714pamfix1"></script>
<script defer src="assets/js/12-bot.js?v=20260714pamfix1"></script> <script defer src="assets/js/12-bot.js?v=20260714pamfix1"></script>
+1 -1
View File
@@ -674,7 +674,7 @@ ExecStart=${INSTALL_DIR}/sshpanel -config ${INSTALL_DIR}/config.json
Restart=always Restart=always
RestartSec=5 RestartSec=5
User=root User=root
LimitNOFILE=65536 LimitNOFILE=1048576
StandardOutput=journal StandardOutput=journal
StandardError=journal StandardError=journal
+61 -49
View File
@@ -217,11 +217,7 @@ func TestTrackedNativeConnectionsAreClosedOnShutdown(t *testing.T) {
} }
} }
func TestCloseAllXHTTPSessionsReleasesGlobalSlots(t *testing.T) { func TestXHTTPSessionsIgnoreLegacyGlobalCapAndReleaseCounters(t *testing.T) {
oldLimit := nativeTuneXHTTPMaxSessions.Load()
nativeTuneXHTTPMaxSessions.Store(8)
defer nativeTuneXHTTPMaxSessions.Store(oldLimit)
before := nativeXHTTPSessions.Load() before := nativeXHTTPSessions.Load()
ib := &nativeInbound{xhttpMaxBufferedPosts: 2} ib := &nativeInbound{xhttpMaxBufferedPosts: 2}
for _, id := range []string{"one", "two"} { for _, id := range []string{"one", "two"} {
@@ -245,28 +241,14 @@ func TestCloseAllXHTTPSessionsReleasesGlobalSlots(t *testing.T) {
} }
func TestNegativeXHTTPSessionLimitMeansUnlimited(t *testing.T) { func TestNegativeXHTTPSessionLimitMeansUnlimited(t *testing.T) {
old := nativeTuneXHTTPMaxSessions.Load()
nativeTuneXHTTPMaxSessions.Store(0)
defer nativeTuneXHTTPMaxSessions.Store(old)
if got := (&nativeInbound{}).xhttpMaxActiveSessions(); got != 0 { if got := (&nativeInbound{}).xhttpMaxActiveSessions(); got != 0 {
t.Fatalf("unlimited XHTTP session limit normalized to %d", got) t.Fatalf("unlimited XHTTP session limit normalized to %d", got)
} }
} }
func TestNativeProtocolGuardsRemainFinite(t *testing.T) { func TestNativeHTTP2StreamsIgnoreLegacyRequestCeiling(t *testing.T) {
oldRequests := nativeTuneMaxXHTTPRequests.Load() if got := nativeHTTP2MaxConcurrentStreams(); got != ^uint32(0) {
defer nativeTuneMaxXHTTPRequests.Store(oldRequests) t.Fatalf("HTTP/2 stream setting = %d, want unlimited uint32 range", got)
// Zero is the internal representation of an explicitly disabled application
// request counter. HTTP/2 must still retain a finite per-connection guard.
nativeTuneMaxXHTTPRequests.Store(0)
if got := nativeHTTP2MaxConcurrentStreams(); got != defaultNativeHTTP2MaxStreams {
t.Fatalf("HTTP/2 stream guard = %d, want %d", got, defaultNativeHTTP2MaxStreams)
}
nativeTuneMaxXHTTPRequests.Store(32)
if got := nativeHTTP2MaxConcurrentStreams(); got != 32 {
t.Fatalf("HTTP/2 stream guard did not honor lower request cap: %d", got)
} }
if got := nativeMuxMaxSessionLimit(); got != 64 { if got := nativeMuxMaxSessionLimit(); got != 64 {
t.Fatalf("per-transport Mux session guard = %d, want 64", got) t.Fatalf("per-transport Mux session guard = %d, want 64", got)
@@ -274,15 +256,6 @@ func TestNativeProtocolGuardsRemainFinite(t *testing.T) {
} }
func TestXHTTPHandlerDoesNotApplyWebRequestCeiling(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: "/"} ib := &nativeInbound{transport: "xhttp", path: "/"}
req := httptest.NewRequest(http.MethodOptions, "/", nil) req := httptest.NewRequest(http.MethodOptions, "/", nil)
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
@@ -292,18 +265,18 @@ func TestXHTTPHandlerDoesNotApplyWebRequestCeiling(t *testing.T) {
} }
} }
func TestLegacyXHTTPTuningMigratesToVPNDefaults(t *testing.T) { func TestPersistedXHTTPAdmissionTuningIsAlwaysUnlimited(t *testing.T) {
got := normalizeNativeXrayTuning(&XrayNativeTuning{ got := normalizeNativeXrayTuning(&XrayNativeTuning{
MuxGlobalSessions: 8192, MuxGlobalSessions: 8192,
MaxConcurrentConnections: 4096, MaxConcurrentConnections: 4096,
MaxConcurrentXHTTPRequests: 8192, MaxConcurrentXHTTPRequests: 8192,
XHTTPMaxSessions: 4096, XHTTPMaxSessions: 4096,
}) })
if got.MuxGlobalSessions != defaultNativeMuxGlobalSessions || if got.MuxGlobalSessions != 8192 ||
got.MaxConcurrentConnections != defaultNativeMaxConnections || got.MaxConcurrentConnections != -1 ||
got.MaxConcurrentXHTTPRequests != defaultNativeMaxXHTTPRequests || got.MaxConcurrentXHTTPRequests != defaultNativeMaxXHTTPRequests ||
got.XHTTPMaxSessions != defaultNativeXHTTPMaxSessions { got.XHTTPMaxSessions != -1 {
t.Fatalf("legacy tuning was not migrated: %+v", got) t.Fatalf("persisted admission limits were not removed: %+v", got)
} }
} }
@@ -325,20 +298,20 @@ func TestXHTTPMetadataLengthIsBoundedBeforeSessionAllocation(t *testing.T) {
func TestXHTTPUploadMemoryIsReleasedOnReadAndClose(t *testing.T) { func TestXHTTPUploadMemoryIsReleasedOnReadAndClose(t *testing.T) {
before := nativeXHTTPBufferedBytes.Load() before := nativeXHTTPBufferedBytes.Load()
q := newNativeXHTTPUploadQueue(4, 8) q := newNativeXHTTPUploadQueue(4, 512)
lease, ok := acquireNativeXHTTPMemory(8) accounted := nativeXHTTPAccountedPacketBytes(4)
lease, ok := acquireNativeXHTTPMemory(accounted)
if !ok { if !ok {
t.Fatal("failed to reserve XHTTP test memory") t.Fatal("failed to reserve XHTTP test memory")
} }
lease.shrink(4)
if err := q.push(context.Background(), nativeXHTTPPacket{Payload: []byte("test"), Seq: 0}, lease); err != nil { if err := q.push(context.Background(), nativeXHTTPPacket{Payload: []byte("test"), Seq: 0}, lease); err != nil {
lease.release() lease.release()
t.Fatalf("queue push failed: %v", err) t.Fatalf("queue push failed: %v", err)
} }
lease.release() // transferred leases are a no-op for the producer. lease.release() // transferred leases are a no-op for the producer.
if got := nativeXHTTPBufferedBytes.Load(); got != before+4 { if got := nativeXHTTPBufferedBytes.Load(); got != before+accounted {
t.Fatalf("buffered bytes after push = %d, want %d", got, before+4) t.Fatalf("buffered bytes after push = %d, want %d", got, before+accounted)
} }
buf := make([]byte, 4) buf := make([]byte, 4)
@@ -349,7 +322,7 @@ func TestXHTTPUploadMemoryIsReleasedOnReadAndClose(t *testing.T) {
t.Fatalf("buffered bytes after read = %d, want %d", got, before) t.Fatalf("buffered bytes after read = %d, want %d", got, before)
} }
lease, ok = acquireNativeXHTTPMemory(3) lease, ok = acquireNativeXHTTPMemory(nativeXHTTPAccountedPacketBytes(3))
if !ok { if !ok {
t.Fatal("failed to reserve second XHTTP test memory") t.Fatal("failed to reserve second XHTTP test memory")
} }
@@ -366,10 +339,10 @@ func TestXHTTPUploadMemoryIsReleasedOnReadAndClose(t *testing.T) {
func TestXHTTPUploadQueueEnforcesPerSessionByteBudget(t *testing.T) { func TestXHTTPUploadQueueEnforcesPerSessionByteBudget(t *testing.T) {
before := nativeXHTTPBufferedBytes.Load() before := nativeXHTTPBufferedBytes.Load()
q := newNativeXHTTPUploadQueue(4, 4) q := newNativeXHTTPUploadQueue(4, nativeXHTTPMinPacketAccountingBytes-1)
defer q.close() defer q.close()
lease, ok := acquireNativeXHTTPMemory(5) lease, ok := acquireNativeXHTTPMemory(nativeXHTTPAccountedPacketBytes(5))
if !ok { if !ok {
t.Fatal("failed to reserve XHTTP test memory") t.Fatal("failed to reserve XHTTP test memory")
} }
@@ -386,10 +359,10 @@ func TestXHTTPUploadQueueEnforcesPerSessionByteBudget(t *testing.T) {
func TestXHTTPUploadQueueBackpressuresInsteadOfRejectingBurst(t *testing.T) { func TestXHTTPUploadQueueBackpressuresInsteadOfRejectingBurst(t *testing.T) {
before := nativeXHTTPBufferedBytes.Load() before := nativeXHTTPBufferedBytes.Load()
q := newNativeXHTTPUploadQueue(2, 4) q := newNativeXHTTPUploadQueue(2, nativeXHTTPMinPacketAccountingBytes)
defer q.close() defer q.close()
first, ok := acquireNativeXHTTPMemory(4) first, ok := acquireNativeXHTTPMemory(nativeXHTTPAccountedPacketBytes(4))
if !ok { if !ok {
t.Fatal("failed to reserve first XHTTP payload") t.Fatal("failed to reserve first XHTTP payload")
} }
@@ -399,7 +372,7 @@ func TestXHTTPUploadQueueBackpressuresInsteadOfRejectingBurst(t *testing.T) {
} }
first.release() first.release()
second, ok := acquireNativeXHTTPMemory(4) second, ok := acquireNativeXHTTPMemory(nativeXHTTPAccountedPacketBytes(4))
if !ok { if !ok {
t.Fatal("failed to reserve second XHTTP payload") t.Fatal("failed to reserve second XHTTP payload")
} }
@@ -438,11 +411,50 @@ func TestXHTTPUploadQueueBackpressuresInsteadOfRejectingBurst(t *testing.T) {
} }
} }
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) { func TestXHTTPBodyReservationUsesActualContentLength(t *testing.T) {
ib := &nativeInbound{xhttpMaxEachPostBytes: 1_000_000} ib := &nativeInbound{xhttpMaxEachPostBytes: 1_000_000}
req := httptest.NewRequest(http.MethodPost, "/session/0", strings.NewReader("small")) req := httptest.NewRequest(http.MethodPost, "/session/0", strings.NewReader("small"))
if got := ib.xhttpUploadReservationBytes(req); got != 5 { if got := ib.xhttpUploadReservationBytes(req); got != nativeXHTTPMinPacketAccountingBytes {
t.Fatalf("body reservation = %d, want actual payload length 5", got) t.Fatalf("body reservation = %d, want minimum accounted packet size", got)
} }
req.ContentLength = -1 req.ContentLength = -1
if got := ib.xhttpUploadReservationBytes(req); got != 1_000_000 { if got := ib.xhttpUploadReservationBytes(req); got != 1_000_000 {
+1
View File
@@ -480,6 +480,7 @@ write_sshpanel_systemd_override() {
echo "[Service]" echo "[Service]"
echo "Environment=PANEL_LOG_FILE=${INSTALL_DIR}/logs/panel.log" echo "Environment=PANEL_LOG_FILE=${INSTALL_DIR}/logs/panel.log"
echo "Environment=PANEL_LOG_MAX_BYTES=${PANEL_LOG_MAX_BYTES}" echo "Environment=PANEL_LOG_MAX_BYTES=${PANEL_LOG_MAX_BYTES}"
echo "LimitNOFILE=1048576"
echo "ExecStartPre=" echo "ExecStartPre="
echo "ExecStartPre=${MKDIR_BIN} -p ${INSTALL_DIR}/logs" echo "ExecStartPre=${MKDIR_BIN} -p ${INSTALL_DIR}/logs"
echo "ExecStartPre=${SH_BIN} -c '${MOUNTPOINT_BIN} -q ${INSTALL_DIR}/logs || ${MOUNT_BIN} -t tmpfs -o size=${LOG_TMPFS_SIZE},mode=0755 tmpfs ${INSTALL_DIR}/logs || true'" echo "ExecStartPre=${SH_BIN} -c '${MOUNTPOINT_BIN} -q ${INSTALL_DIR}/logs || ${MOUNT_BIN} -t tmpfs -o size=${LOG_TMPFS_SIZE},mode=0755 tmpfs ${INSTALL_DIR}/logs || true'"
+3 -3
View File
@@ -204,9 +204,9 @@ func (s *nativeXrayServer) start(configFile string) error {
} }
return fmt.Errorf("native xray: listen %s (shared XHTTP): %w", addr, err) return fmt.Errorf("native xray: listen %s (shared XHTTP): %w", addr, err)
} }
// Apply the global pre-authentication ceiling before net/http can spawn a // Track accepted sockets so stop/reload can close them. VPN transports are
// goroutine or begin a TLS handshake for the accepted socket. // not subject to a global website-style connection ceiling.
serveLn := limitNativeListener(ln) serveLn := trackNativeListener(ln)
if group.security == "tls" { if group.security == "tls" {
serveLn = tls.NewListener(serveLn, group.tlsConfig) serveLn = tls.NewListener(serveLn, group.tlsConfig)
} }
+20 -45
View File
@@ -34,14 +34,10 @@ func init() {
} }
var ( var (
nativeTransportConnections atomic.Int64 nativeTransportConnections atomic.Int64
nativeTransportRejected atomic.Int64 nativeXHTTPSessions atomic.Int64
nativeXHTTPRequests atomic.Int64 nativeClientConnsRejected atomic.Int64
nativeXHTTPRequestsRejected atomic.Int64 nativePreAuthRejected atomic.Int64
nativeXHTTPSessions atomic.Int64
nativeXHTTPSessionsRejected atomic.Int64
nativeClientConnsRejected atomic.Int64
nativePreAuthRejected atomic.Int64
nativeTransportAccepting atomic.Bool nativeTransportAccepting atomic.Bool
nativeTransportRegistry = struct { nativeTransportRegistry = struct {
@@ -50,9 +46,9 @@ var (
}{conns: make(map[*nativeCountedConn]struct{})} }{conns: make(map[*nativeCountedConn]struct{})}
) )
// acquireNativeCounter reserves one slot without blocking. Blocking the accept // acquireNativeCounter tracks a counted resource and returns an exactly-once
// loop or an HTTP handler when the process is already at its safety ceiling // release function. Native transport/XHTTP admission calls it with limit=0
// would retain yet more sockets/goroutines, so overload is rejected promptly. // because VPN traffic must not be rejected by a global website-style ceiling.
func acquireNativeCounter(active *atomic.Int64, limit int) (func(), bool) { func acquireNativeCounter(active *atomic.Int64, limit int) (func(), bool) {
for { for {
current := active.Load() current := active.Load()
@@ -100,30 +96,11 @@ func logNativePreAuthRejection(format string, args ...interface{}) {
} }
func acquireNativeTransportConnection() (func(), bool) { func acquireNativeTransportConnection() (func(), bool) {
limit := nativeMaxConnectionLimit() return acquireNativeCounter(&nativeTransportConnections, 0)
release, ok := acquireNativeCounter(&nativeTransportConnections, limit)
if !ok {
logNativeLimitRejection("transport connection", &nativeTransportRejected, limit)
}
return release, ok
}
func acquireNativeXHTTPRequest() (func(), bool) {
limit := nativeMaxXHTTPRequestLimit()
release, ok := acquireNativeCounter(&nativeXHTTPRequests, limit)
if !ok {
logNativeLimitRejection("XHTTP request", &nativeXHTTPRequestsRejected, limit)
}
return release, ok
} }
func acquireNativeXHTTPSession() (func(), bool) { func acquireNativeXHTTPSession() (func(), bool) {
limit := nativeXHTTPMaxSessionLimit() return acquireNativeCounter(&nativeXHTTPSessions, 0)
release, ok := acquireNativeCounter(&nativeXHTTPSessions, limit)
if !ok {
logNativeLimitRejection("XHTTP session", &nativeXHTTPSessionsRejected, limit)
}
return release, ok
} }
func configureNativeTransportSocket(c net.Conn) { func configureNativeTransportSocket(c net.Conn) {
@@ -215,9 +192,8 @@ func registerTrackedNativeTransportConn(c net.Conn, release func()) (net.Conn, b
return counted, true return counted, true
} }
// waitWrapTrackedNativeTransportConn is used by raw native accept loops. It // waitWrapTrackedNativeTransportConn is used by raw native accept loops. Global
// holds at most one already-accepted socket while capacity is busy, leaving the // admission is unlimited; the loop remains only to coordinate listener shutdown.
// rest in the kernel backlog instead of creating origin-side resets/502s.
func waitWrapTrackedNativeTransportConn(c net.Conn) (net.Conn, bool) { func waitWrapTrackedNativeTransportConn(c net.Conn) (net.Conn, bool) {
if c == nil { if c == nil {
return nil, false return nil, false
@@ -254,20 +230,19 @@ func closeAllNativeTransportConnections() {
} }
} }
// nativeLimitedListener applies the same pre-authentication ceiling to XHTTP // nativeTrackingListener registers every accepted XHTTP socket so a live
// listeners. net/http receives only sockets that own a slot; when capacity is // stop/reload can close it. It counts sockets for diagnostics but never rejects
// busy, new sockets remain in the kernel backlog until a slot becomes available. // or delays one because of a global application limit.
type nativeLimitedListener struct { type nativeTrackingListener struct {
net.Listener net.Listener
} }
func (l nativeLimitedListener) Accept() (net.Conn, error) { func (l nativeTrackingListener) 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() release, ok := acquireNativeTransportConnection()
if !ok { if !ok {
// Unlimited admission can only fail if this implementation changes. Avoid
// accepting and resetting a socket if that ever happens.
time.Sleep(nativeOverloadBackoff) time.Sleep(nativeOverloadBackoff)
continue continue
} }
@@ -286,9 +261,9 @@ func (l nativeLimitedListener) Accept() (net.Conn, error) {
} }
} }
func limitNativeListener(ln net.Listener) net.Listener { func trackNativeListener(ln net.Listener) net.Listener {
if ln == nil { if ln == nil {
return nil return nil
} }
return nativeLimitedListener{Listener: ln} return nativeTrackingListener{Listener: ln}
} }
+16 -50
View File
@@ -18,10 +18,11 @@ type XrayNativeTuning struct {
const ( const (
defaultNativeRuntimeGOMAXPROCS = 0 defaultNativeRuntimeGOMAXPROCS = 0
defaultNativeMuxGlobalSessions = 32768 defaultNativeMuxGlobalSessions = 32768
defaultNativeMaxConnections = 32768 // Transport sockets, XHTTP requests, and XHTTP sessions are VPN traffic, not
// XHTTP packet handlers are governed by HTTP/2 flow control and bounded byte // website requests. Keep the legacy JSON fields for config compatibility, but
// queues, not a website-style request ceiling. A negative configured value is // always normalize them to unlimited. Actual resource protection is provided by
// normalized to the internal unlimited representation. // socket/HTTP flow control and the bounded byte queues in xray_xhttp.go.
defaultNativeMaxConnections = -1
defaultNativeMaxXHTTPRequests = -1 defaultNativeMaxXHTTPRequests = -1
fixedNativeMuxMaxSessions = 64 fixedNativeMuxMaxSessions = 64
@@ -29,8 +30,7 @@ const (
fixedNativeMuxUDPReadBuffer = 256 * 1024 fixedNativeMuxUDPReadBuffer = 256 * 1024
fixedNativeMuxUDPWriteBuffer = 256 * 1024 fixedNativeMuxUDPWriteBuffer = 256 * 1024
defaultNativeXHTTPMaxSessions = 32768 defaultNativeXHTTPMaxSessions = -1
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
@@ -48,9 +48,6 @@ const (
var ( var (
nativeTuneRuntimeGOMAXPROCS atomic.Int64 nativeTuneRuntimeGOMAXPROCS atomic.Int64
nativeTuneMuxGlobalSessions atomic.Int64 nativeTuneMuxGlobalSessions atomic.Int64
nativeTuneMaxConnections atomic.Int64
nativeTuneMaxXHTTPRequests atomic.Int64
nativeTuneXHTTPMaxSessions atomic.Int64
nativeTuneTracePackets atomic.Bool nativeTuneTracePackets atomic.Bool
) )
@@ -63,33 +60,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
} }
if out.MuxGlobalSessions <= 0 { if out.MuxGlobalSessions <= 0 {
out.MuxGlobalSessions = defaultNativeMuxGlobalSessions out.MuxGlobalSessions = defaultNativeMuxGlobalSessions
} }
if out.MaxConcurrentConnections == 0 { // Ignore every old positive/zero admission ceiling. This migration is
out.MaxConcurrentConnections = defaultNativeMaxConnections // deliberately unconditional so upgrading an existing server immediately
} // removes the old 4K/8K/32K web-style caps without requiring a panel save.
if out.MaxConcurrentXHTTPRequests == 0 { out.MaxConcurrentConnections = defaultNativeMaxConnections
out.MaxConcurrentXHTTPRequests = defaultNativeMaxXHTTPRequests out.MaxConcurrentXHTTPRequests = defaultNativeMaxXHTTPRequests
} out.XHTTPMaxSessions = defaultNativeXHTTPMaxSessions
if out.XHTTPMaxSessions == 0 {
out.XHTTPMaxSessions = defaultNativeXHTTPMaxSessions
}
return out return out
} }
@@ -105,27 +87,12 @@ func applyNativeXrayTuning(t *XrayNativeTuning) XrayNativeTuning {
runtime.GOMAXPROCS(gomax) runtime.GOMAXPROCS(gomax)
nativeTuneRuntimeGOMAXPROCS.Store(int64(gomax)) nativeTuneRuntimeGOMAXPROCS.Store(int64(gomax))
nativeTuneMuxGlobalSessions.Store(int64(out.MuxGlobalSessions)) nativeTuneMuxGlobalSessions.Store(int64(out.MuxGlobalSessions))
nativeTuneMaxConnections.Store(nativeLimitValue(out.MaxConcurrentConnections))
nativeTuneMaxXHTTPRequests.Store(nativeLimitValue(out.MaxConcurrentXHTTPRequests))
nativeTuneXHTTPMaxSessions.Store(nativeLimitValue(out.XHTTPMaxSessions))
nativeTuneTracePackets.Store(out.TracePackets) nativeTuneTracePackets.Store(out.TracePackets)
return out return out
} }
// Native tuning limits use zero internally for unlimited. In configuration,
// zero means "use the safe default" and any negative value disables the cap.
func nativeLimitValue(v int) int64 {
if v < 0 {
return 0
}
return int64(v)
}
func nativeRuntimeGOMAXPROCS() int { return int(nativeTuneRuntimeGOMAXPROCS.Load()) } func nativeRuntimeGOMAXPROCS() int { return int(nativeTuneRuntimeGOMAXPROCS.Load()) }
func nativeMuxGlobalSessionLimit() int { return int(nativeTuneMuxGlobalSessions.Load()) } func nativeMuxGlobalSessionLimit() int { return int(nativeTuneMuxGlobalSessions.Load()) }
func nativeMaxConnectionLimit() int { return int(nativeTuneMaxConnections.Load()) }
func nativeMaxXHTTPRequestLimit() int { return int(nativeTuneMaxXHTTPRequests.Load()) }
func nativeXHTTPMaxSessionLimit() int { return int(nativeTuneXHTTPMaxSessions.Load()) }
func nativeTracePacketsEnabled() bool { return nativeTuneTracePackets.Load() } func nativeTracePacketsEnabled() bool { return nativeTuneTracePackets.Load() }
func nativeMuxMaxSessionLimit() int { return fixedNativeMuxMaxSessions } func nativeMuxMaxSessionLimit() int { return fixedNativeMuxMaxSessions }
@@ -133,11 +100,10 @@ func nativeMuxUDPReadBufferSize() int { return fixedNativeMuxUDPReadBuffer }
func nativeMuxUDPWriteBufferSize() int { return fixedNativeMuxUDPWriteBuffer } func nativeMuxUDPWriteBufferSize() int { return fixedNativeMuxUDPWriteBuffer }
func nativeXHTTPBufferedPostLimit() int { return defaultNativeXHTTPBufferedPosts } func nativeXHTTPBufferedPostLimit() int { return defaultNativeXHTTPBufferedPosts }
func nativeHTTP2MaxConcurrentStreams() uint32 { func nativeHTTP2MaxConcurrentStreams() uint32 {
limit := nativeMaxXHTTPRequestLimit() // x/net/http2 otherwise installs its own finite default when this is zero.
if limit <= 0 || limit > defaultNativeHTTP2MaxStreams { // Advertise the protocol's full uint32 range so ordinary packet-up bursts can
return defaultNativeHTTP2MaxStreams // never be refused by a website-style concurrent-stream setting.
} return ^uint32(0)
return uint32(limit)
} }
func nativeMuxUDPIdleTimeout() time.Duration { func nativeMuxUDPIdleTimeout() time.Duration {
return fixedNativeMuxUDPIdleMS * time.Millisecond return fixedNativeMuxUDPIdleMS * time.Millisecond
+38 -39
View File
@@ -21,8 +21,6 @@ import (
"golang.org/x/net/http2/h2c" "golang.org/x/net/http2/h2c"
) )
const nativeXHTTPServerIdleTimeout = 90 * time.Second
const ( const (
nativeXHTTPMaxSessionIDBytes = 256 nativeXHTTPMaxSessionIDBytes = 256
nativeXHTTPMaxSequenceBytes = 20 nativeXHTTPMaxSequenceBytes = 20
@@ -31,6 +29,10 @@ const (
nativeXHTTPMaxBufferedPosts = 512 nativeXHTTPMaxBufferedPosts = 512
nativeXHTTPMaxBufferedSessionBytes = 16 * 1024 * 1024 nativeXHTTPMaxBufferedSessionBytes = 16 * 1024 * 1024
nativeXHTTPMaxBufferedGlobalBytes = 128 * 1024 * 1024 nativeXHTTPMaxBufferedGlobalBytes = 128 * 1024 * 1024
// Tiny/empty packet-up requests still retain queue metadata. Charge a minimum
// amount against the byte budgets so the reassembly queue can be count-unlimited
// without allowing zero-byte packets to grow the heap without bound.
nativeXHTTPMinPacketAccountingBytes int64 = 256
) )
var ( var (
@@ -181,7 +183,6 @@ func (ib *nativeInbound) serveXHTTPListener(ln net.Listener) {
func (g *nativeXHTTPListener) serve(ln net.Listener) { func (g *nativeXHTTPListener) serve(ln net.Listener) {
defer xrayRecover(fmt.Sprintf("native xray shared XHTTP listener addr=%s", ln.Addr())) defer xrayRecover(fmt.Sprintf("native xray shared XHTTP listener addr=%s", ln.Addr()))
h2s := &http2.Server{ h2s := &http2.Server{
IdleTimeout: nativeXHTTPServerIdleTimeout,
MaxConcurrentStreams: nativeHTTP2MaxConcurrentStreams(), MaxConcurrentStreams: nativeHTTP2MaxConcurrentStreams(),
} }
handler := http.Handler(g) handler := http.Handler(g)
@@ -195,7 +196,6 @@ func (g *nativeXHTTPListener) serve(ln net.Listener) {
srv := &http.Server{ srv := &http.Server{
Handler: handler, Handler: handler,
ReadHeaderTimeout: 4 * time.Second, ReadHeaderTimeout: 4 * time.Second,
IdleTimeout: nativeXHTTPServerIdleTimeout,
MaxHeaderBytes: g.headerSize, MaxHeaderBytes: g.headerSize,
} }
if g.security == "tls" && g.tlsConfig != nil { if g.security == "tls" && g.tlsConfig != nil {
@@ -560,16 +560,7 @@ func (ib *nativeInbound) upsertXHTTPSession(w http.ResponseWriter, id string) *n
s.touch() s.touch()
return s return s
} }
if max := ib.xhttpMaxActiveSessions(); max > 0 && len(ib.xhttpSessions) >= max { releaseSlot, _ := acquireNativeXHTTPSession()
http.Error(w, "native XHTTP session capacity reached", http.StatusServiceUnavailable)
logNativePreAuthRejection("native xray: xhttp session rejected inbound=%q active=%d limit=%d", ib.tag, len(ib.xhttpSessions), max)
return nil
}
releaseSlot, ok := acquireNativeXHTTPSession()
if !ok {
http.Error(w, "native XHTTP global session capacity reached", http.StatusServiceUnavailable)
return nil
}
s := &nativeXHTTPSession{ s := &nativeXHTTPSession{
id: id, id: id,
queue: newNativeXHTTPUploadQueue(ib.xhttpMaxBufferedPosts, nativeXHTTPMaxBufferedSessionBytes), queue: newNativeXHTTPUploadQueue(ib.xhttpMaxBufferedPosts, nativeXHTTPMaxBufferedSessionBytes),
@@ -585,9 +576,7 @@ func (ib *nativeInbound) upsertXHTTPSession(w http.ResponseWriter, id string) *n
} }
func (ib *nativeInbound) xhttpMaxActiveSessions() int { func (ib *nativeInbound) xhttpMaxActiveSessions() int {
// normalizeNativeXrayTuning already installs the safe default. A zero value return 0
// here therefore intentionally means the operator configured -1 (unlimited).
return nativeXHTTPMaxSessionLimit()
} }
func (ib *nativeInbound) reapUnconnectedXHTTPSession(id string, s *nativeXHTTPSession) { func (ib *nativeInbound) reapUnconnectedXHTTPSession(id string, s *nativeXHTTPSession) {
@@ -679,7 +668,7 @@ func (ib *nativeInbound) handleXHTTPPacketUpload(w http.ResponseWriter, r *http.
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
memory.shrink(int64(len(payload))) memory.shrink(nativeXHTTPAccountedPacketBytes(int64(len(payload))))
xrayTracef("native xray: xhttp packet-up inbound=%q session=%q seq=%d payload=%d remote=%s", ib.tag, sess.id, seq, len(payload), r.RemoteAddr) xrayTracef("native xray: xhttp packet-up inbound=%q session=%q seq=%d payload=%d remote=%s", ib.tag, sess.id, seq, len(payload), r.RemoteAddr)
if err := sess.queue.push(r.Context(), nativeXHTTPPacket{Payload: payload, Seq: seq}, memory); err != nil { if err := sess.queue.push(r.Context(), nativeXHTTPPacket{Payload: payload, Seq: seq}, memory); err != nil {
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
@@ -816,11 +805,18 @@ func (ib *nativeInbound) xhttpUploadReservationBytes(r *http.Request) int64 {
placement := firstNonEmpty(ib.xhttpUplinkDataPlacement, xhttpPlacementBody) placement := firstNonEmpty(ib.xhttpUplinkDataPlacement, xhttpPlacementBody)
if placement == xhttpPlacementBody && r.ContentLength >= 0 { if placement == xhttpPlacementBody && r.ContentLength >= 0 {
if r.ContentLength > maxBytes { if r.ContentLength > maxBytes {
return maxBytes return nativeXHTTPAccountedPacketBytes(maxBytes)
} }
return r.ContentLength return nativeXHTTPAccountedPacketBytes(r.ContentLength)
} }
return maxBytes return nativeXHTTPAccountedPacketBytes(maxBytes)
}
func nativeXHTTPAccountedPacketBytes(payloadBytes int64) int64 {
if payloadBytes < nativeXHTTPMinPacketAccountingBytes {
return nativeXHTTPMinPacketAccountingBytes
}
return payloadBytes
} }
func (ib *nativeInbound) handleXHTTPStreamOne(w http.ResponseWriter, r *http.Request) { func (ib *nativeInbound) handleXHTTPStreamOne(w http.ResponseWriter, r *http.Request) {
@@ -1186,14 +1182,14 @@ func (l *nativeXHTTPMemoryLease) release() {
} }
type nativeXHTTPPacket struct { type nativeXHTTPPacket struct {
Reader io.ReadCloser Reader io.ReadCloser
Payload []byte Payload []byte
Seq uint64 Seq uint64
accountedBytes int64
} }
type nativeXHTTPUploadQueue struct { type nativeXHTTPUploadQueue struct {
pushedPackets chan nativeXHTTPPacket pushedPackets chan nativeXHTTPPacket
maxPackets int
maxBytes int64 maxBytes int64
// readMu serializes the single decoded stream reader with close-time queue // readMu serializes the single decoded stream reader with close-time queue
@@ -1227,7 +1223,6 @@ func newNativeXHTTPUploadQueue(maxPackets int, maxBytes int64) *nativeXHTTPUploa
} }
return &nativeXHTTPUploadQueue{ return &nativeXHTTPUploadQueue{
pushedPackets: make(chan nativeXHTTPPacket, maxPackets), pushedPackets: make(chan nativeXHTTPPacket, maxPackets),
maxPackets: maxPackets,
maxBytes: maxBytes, maxBytes: maxBytes,
closed: make(chan struct{}), closed: make(chan struct{}),
spaceChanged: make(chan struct{}), spaceChanged: make(chan struct{}),
@@ -1318,15 +1313,18 @@ func (q *nativeXHTTPUploadQueue) push(ctx context.Context, p nativeXHTTPPacket,
} }
}() }()
} }
payloadBytes := int64(len(p.Payload)) accountedBytes := int64(0)
if err := q.adoptPayloadMemory(ctx, memory, payloadBytes); err != nil { if p.Reader == nil {
accountedBytes = nativeXHTTPAccountedPacketBytes(int64(len(p.Payload)))
}
if err := q.adoptPayloadMemory(ctx, memory, accountedBytes); err != nil {
return err return err
} }
transferred := payloadBytes > 0 p.accountedBytes = accountedBytes
if transferred { if accountedBytes > 0 {
defer func() { defer func() {
if payloadBytes > 0 { if accountedBytes > 0 {
q.releasePayloadMemory(payloadBytes) q.releasePayloadMemory(accountedBytes)
} }
}() }()
} }
@@ -1334,7 +1332,7 @@ func (q *nativeXHTTPUploadQueue) push(ctx context.Context, p nativeXHTTPPacket,
case q.pushedPackets <- p: case q.pushedPackets <- p:
// Ownership has moved to the queue. close() waits for this producer and // Ownership has moved to the queue. close() waits for this producer and
// then drains/releases anything not consumed by the stream reader. // then drains/releases anything not consumed by the stream reader.
payloadBytes = 0 accountedBytes = 0
readerReserved = false readerReserved = false
return nil return nil
case <-q.closed: case <-q.closed:
@@ -1449,7 +1447,7 @@ func (q *nativeXHTTPUploadQueue) Read(b []byte) (int, error) {
} }
select { select {
case <-q.closed: case <-q.closed:
q.releasePayloadMemory(int64(len(p.Payload))) q.releasePayloadMemory(p.accountedBytes)
return 0, io.EOF return 0, io.EOF
default: default:
} }
@@ -1461,20 +1459,21 @@ func (q *nativeXHTTPUploadQueue) Read(b []byte) (int, error) {
if packet.Seq == q.nextSeq { if packet.Seq == q.nextSeq {
n := copy(b, packet.Payload) n := copy(b, packet.Payload)
q.releasePayloadMemory(int64(n))
if n < len(packet.Payload) { if n < len(packet.Payload) {
q.releasePayloadMemory(int64(n))
packet.accountedBytes -= int64(n)
packet.Payload = packet.Payload[n:] packet.Payload = packet.Payload[n:]
heap.Push(&q.heap, packet) heap.Push(&q.heap, packet)
} else { } else {
q.releasePayloadMemory(packet.accountedBytes)
q.nextSeq = packet.Seq + 1 q.nextSeq = packet.Seq + 1
} }
return n, nil return n, nil
} }
if packet.Seq > q.nextSeq { if packet.Seq > q.nextSeq {
if len(q.heap) > q.maxPackets { // Do not apply a packet/request count ceiling. The per-session and global
return 0, errors.New("xhttp upload reassembly buffer too large") // accounted-byte budgets backpressure producers, including empty packets.
}
heap.Push(&q.heap, packet) heap.Push(&q.heap, packet)
p, err := q.recv() p, err := q.recv()
if err != nil { if err != nil {
@@ -1489,7 +1488,7 @@ func (q *nativeXHTTPUploadQueue) Read(b []byte) (int, error) {
} }
// A duplicate/late packet is discarded; release the bytes it owned. // A duplicate/late packet is discarded; release the bytes it owned.
q.releasePayloadMemory(int64(len(packet.Payload))) q.releasePayloadMemory(packet.accountedBytes)
} }
return 0, nil return 0, nil