Compare commits
2
Commits
44b2313299
...
2f4cb008ae
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f4cb008ae | ||
|
|
9bbd950b66 |
@@ -68,7 +68,7 @@ A confirmação dessa migração é exibida dentro do próprio painel. Se a grav
|
|||||||
|
|
||||||
### Cota de tráfego e proteção de recursos
|
### Cota de tráfego e proteção de recursos
|
||||||
|
|
||||||
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. 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:
|
O runtime nativo também possui limites globais para impedir crescimento sem controle de sockets, goroutines e sessões HTTP:
|
||||||
|
|
||||||
@@ -609,7 +609,7 @@ The migration confirmation is rendered inside the panel. If saving fails, the te
|
|||||||
|
|
||||||
### Traffic quotas and resource protection
|
### Traffic quotas and resource protection
|
||||||
|
|
||||||
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. `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 native runtime also has global ceilings that prevent unbounded socket, goroutine, and HTTP-session growth:
|
||||||
|
|
||||||
@@ -1144,14 +1144,17 @@ curl -s "http://SERVER_IP:9090/api/users" -H "X-Session-Token: $TOKEN"
|
|||||||
|
|
||||||
#### `GET /api/users` — session
|
#### `GET /api/users` — session
|
||||||
- Optional query: `server_id`. Resellers see only their own users; superadmins see all.
|
- Optional query: `server_id`. Resellers see only their own users; superadmins see all.
|
||||||
- `200`: array of user objects: `username` (string), `active_conns` (int), `max_connections` (int), `expires_at` (string/null), `limit_mbps_up` (int), `limit_mbps_down` (int), `totp_secret` (string, omitempty), `totp_period` (int), `totp_window` (int), `totp_digits` (int), `allow_static_password` (bool), `totp_enabled` (bool), `owner_username` (string, omitempty), `server_id` (string, omitempty).
|
- `200`: array of user objects: `username` (string), `active_conns` (int), `max_connections` (int), `expires_at` (string/null), `limit_mbps_up` (int), `limit_mbps_down` (int), `data_quota_bytes` (int64), `quota_action` (`block` or `throttle`), `quota_throttle_mbps` (int), `total_uplink_bytes`, `total_downlink_bytes`, `total_bytes`, `quota_exceeded`, `totp_secret` (string, omitempty), `totp_period` (int), `totp_window` (int), `totp_digits` (int), `allow_static_password` (bool), `totp_enabled` (bool), `owner_username` (string, omitempty), `server_id` (string, omitempty).
|
||||||
|
|
||||||
#### `POST /api/users/create` — session
|
#### `POST /api/users/create` — session
|
||||||
Creates or updates (upsert) an SSH user.
|
Creates or updates (upsert) an SSH user.
|
||||||
- Body: `username` (string, required); `password` (string, optional — empty keeps the existing password on an existing user; for a new user either `password` or `totp_secret` is required); `max_connections` (int); `expires_at` (string); `limit_mbps_up` (int); `limit_mbps_down` (int); `totp_secret` (string); `totp_period` (int); `totp_window` (int); `totp_digits` (int); `allow_static_password` (bool); `owner_username` (string, optional — honored only for superadmin; resellers are forced to themselves); `server_id` (string, optional).
|
- Body: `username` (string, required); `password` (string, optional — empty keeps the existing password on an existing user; for a new user either `password` or `totp_secret` is required); `max_connections` (0–10000); `expires_at` (RFC3339 string); `limit_mbps_up` and `limit_mbps_down` (0–1000000); `data_quota_bytes` (non-negative int64); `quota_action` (`block` or `throttle`); `quota_throttle_mbps` (0–1000000; zero defaults to 1); `reset_usage` (bool); `totp_secret` (string); `totp_period` (int); `totp_window` (int); `totp_digits` (int); `allow_static_password` (bool); `owner_username` (string, optional — honored only for superadmin; resellers are forced to themselves); `server_id` (string, optional).
|
||||||
- `201 Created` (empty body). A proxied create returns the remote node's status/body.
|
- `201 Created` (empty body). A proxied create returns the remote node's status/body.
|
||||||
- Errors: `400 username required`, `400 password or totp_secret required for new user`; `403 user limit reached (N)`; `403 SSH creation is disabled for this server`; `503 database not configured`.
|
- Errors: `400 username required`, `400 password or totp_secret required for new user`; `403 user limit reached (N)`; `403 SSH creation is disabled for this server`; `503 database not configured`.
|
||||||
|
|
||||||
|
#### `POST /api/users/reset-traffic` — session
|
||||||
|
- Body: `username` (required), `server_id` (optional). Resets only byte counters. Resellers may reset only their own users.
|
||||||
|
|
||||||
#### `DELETE /api/users/delete` — session
|
#### `DELETE /api/users/delete` — session
|
||||||
- Query: `username` (string, required); optional `server_id`. Resellers may delete only their own users.
|
- Query: `username` (string, required); optional `server_id`. Resellers may delete only their own users.
|
||||||
- `204 No Content`. Errors: `400 username required`; `403 forbidden`; `503 database not configured`.
|
- `204 No Content`. Errors: `400 username required`; `403 forbidden`; `503 database not configured`.
|
||||||
@@ -1261,16 +1264,19 @@ Read/write a managed server's `config.json`. Query: `server_id`. Local delegates
|
|||||||
#### `GET /api/xray/inbounds` — session
|
#### `GET /api/xray/inbounds` — session
|
||||||
- Optional `server_id`. Lists only inbounds that carry client lists (vless/vmess/trojan). Resellers see all inbounds but only their own clients. Clients are enriched with DB metadata and runtime stats.
|
- Optional `server_id`. Lists only inbounds that carry client lists (vless/vmess/trojan). Resellers see all inbounds but only their own clients. Clients are enriched with DB metadata and runtime stats.
|
||||||
- `200`: array of `{ "tag": string, "protocol": string, "port": <raw>, "listen": string, "clients": [ XrayClientInfo ] }`.
|
- `200`: array of `{ "tag": string, "protocol": string, "port": <raw>, "listen": string, "clients": [ XrayClientInfo ] }`.
|
||||||
- **XrayClientInfo**: `id` (string, the UUID), `password` (string, omitempty), `email` (string), `level` (int), `online` (bool), `last_active` (string/null), `uplink_bytes` (int64), `downlink_bytes` (int64), `total_bytes` (int64), `active_connections` (int), `name` (string), `expires_at` (string/null), `expiration_days` (int; `-1` = no expiry, `0` = expired), `max_conns`, `owner_username`, `expired`.
|
- **XrayClientInfo**: `id` (string, the UUID), `password` (string, omitempty), `email` (string), `level` (int), `online` (bool), `last_active` (string/null), `uplink_bytes` (int64), `downlink_bytes` (int64), `total_bytes` (int64), `active_connections` (int), `name` (string), `expires_at` (string/null), `expiration_days` (int; `-1` = no expiry, `0` = expired), `max_conns`, `data_quota_bytes`, `quota_action`, `quota_throttle_mbps`, `quota_exceeded`, `owner_username`, `expired`.
|
||||||
|
|
||||||
#### `POST /api/xray/clients/add` — session
|
#### `POST /api/xray/clients/add` — session
|
||||||
- Body: `inbound_tag` (string, required), `uuid` (string, required), `email` (string, optional — defaults to name then uuid), `name` (string, optional), `expires_at` (string, optional), `max_connections` (int), `owner_username` (string, optional — superadmin only), `server_id` (string, optional).
|
- Body: `inbound_tag` (string, required), `uuid` (valid UUID, required), `email` (string, optional — defaults to name then uuid), `name` (string, optional), `expires_at` (RFC3339, `YYYY-MM-DDThh:mm`, or `YYYY-MM-DD`), `max_connections` (0–10000), `data_quota_bytes`, `quota_action`, `quota_throttle_mbps`, `owner_username` (string, optional — superadmin only), `server_id` (string, optional).
|
||||||
- `201 Created` (empty). Errors: `400 inbound_tag and uuid required` / `UUID already exists in database`; `403 reseller account suspended or expired` / `user limit reached (N)` / `Xray creation is disabled for this server`; `500`.
|
- `201 Created` (empty). Errors: `400 inbound_tag and uuid required` / `UUID already exists in database`; `403 reseller account suspended or expired` / `user limit reached (N)` / `Xray creation is disabled for this server`; `500`.
|
||||||
|
|
||||||
#### `POST /api/xray/clients/update` — session
|
#### `POST /api/xray/clients/update` — session
|
||||||
- Body: `uuid` (string, required), `name` (string), `email` (string), `expires_at` (string), `max_connections` (int), `server_id` (string, optional). Inbound tag and owner are preserved from existing metadata. Resellers may update only their own clients.
|
- Body: `uuid` (valid UUID, required), `name` (string), `email` (string), `expires_at` (string), `max_connections` (0–10000), `data_quota_bytes`, `quota_action`, `quota_throttle_mbps`, `reset_usage` (bool), `server_id` (string, optional). Inbound tag and owner are preserved from existing metadata. Resellers may update only their own clients.
|
||||||
- `200`. Errors: `400 uuid required`; `403 forbidden`; `404 client metadata not found`; `500`.
|
- `200`. Errors: `400 uuid required`; `403 forbidden`; `404 client metadata not found`; `500`.
|
||||||
|
|
||||||
|
#### `POST /api/xray/clients/reset-traffic` — session
|
||||||
|
- Body: `uuid` (required), `server_id` (optional). Resets only byte counters. Resellers may reset only their own clients.
|
||||||
|
|
||||||
#### `DELETE /api/xray/clients/remove` — session
|
#### `DELETE /api/xray/clients/remove` — session
|
||||||
- Query: `inbound_tag` (string, required), `uuid` (string, required); optional `server_id`. Resellers may remove only their own clients.
|
- Query: `inbound_tag` (string, required), `uuid` (string, required); optional `server_id`. Resellers may remove only their own clients.
|
||||||
- `204 No Content`. Errors: `400 inbound_tag and uuid required`; `403 forbidden`; `500`.
|
- `204 No Content`. Errors: `400 inbound_tag and uuid required`; `403 forbidden`; `500`.
|
||||||
|
|||||||
@@ -750,3 +750,34 @@ function patchRenderedInbounds(inbounds) {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Native XHTTP/VPN tuning labels introduced by the high-traffic backpressure
|
||||||
|
// update. Keep this block close to the UI code so both languages stay complete.
|
||||||
|
Object.assign(I18N_TEXT["en-US"], {
|
||||||
|
"Native Xray scale tuning":"Native Xray scale tuning",
|
||||||
|
"Go CPU threads (GOMAXPROCS)":"Go CPU threads (GOMAXPROCS)",
|
||||||
|
"Global mux backend sessions":"Global mux backend sessions",
|
||||||
|
"Global transport connections":"Global transport connections",
|
||||||
|
"XHTTP web request cap":"XHTTP web request 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",
|
||||||
|
"debug only, slows QUIC":"debug only, slows QUIC",
|
||||||
|
"Apply high-traffic VPN defaults":"Apply high-traffic VPN 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."
|
||||||
|
});
|
||||||
|
Object.assign(I18N_TEXT["pt-BR"], {
|
||||||
|
"Native Xray scale tuning":"Ajustes de escala do Xray nativo",
|
||||||
|
"Go CPU threads (GOMAXPROCS)":"Threads de CPU do Go (GOMAXPROCS)",
|
||||||
|
"Global mux backend sessions":"Sessões globais de backend Mux",
|
||||||
|
"Global transport connections":"Conexões globais de transporte",
|
||||||
|
"XHTTP web request cap":"Limite web de requisiçõ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",
|
||||||
|
"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 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."
|
||||||
|
});
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ cancelUserBtn.addEventListener("click", () => {
|
|||||||
function prepareNewSSHUser() {
|
function prepareNewSSHUser() {
|
||||||
userForm.reset();
|
userForm.reset();
|
||||||
fTotpPeriod.value = 60; fTotpWindow.value = 1; fTotpDigits.value = 6;
|
fTotpPeriod.value = 60; fTotpWindow.value = 1; fTotpDigits.value = 6;
|
||||||
fQuotaAction.value = "block";
|
// SSH and SSH-over-XHTTP plans normally remain connected after quota and
|
||||||
|
// fall back to the configured post-quota speed. Existing users keep their
|
||||||
|
// saved action when edited.
|
||||||
|
fQuotaAction.value = "throttle";
|
||||||
fQuotaThrottle.value = 1;
|
fQuotaThrottle.value = 1;
|
||||||
fUsageDisplay.value = "0 B";
|
fUsageDisplay.value = "0 B";
|
||||||
fResetUsage.checked = false;
|
fResetUsage.checked = false;
|
||||||
|
|||||||
@@ -35,18 +35,18 @@ function toggleUdpgwFields(on) {
|
|||||||
const XRAY_NATIVE_TUNING_DEFAULTS = {
|
const XRAY_NATIVE_TUNING_DEFAULTS = {
|
||||||
safe: {
|
safe: {
|
||||||
runtime_gomaxprocs: 0,
|
runtime_gomaxprocs: 0,
|
||||||
mux_global_sessions: 8192,
|
mux_global_sessions: 32768,
|
||||||
max_concurrent_connections: 4096,
|
max_concurrent_connections: 32768,
|
||||||
max_concurrent_xhttp_requests: 8192,
|
max_concurrent_xhttp_requests: -1,
|
||||||
xhttp_max_sessions: 4096,
|
xhttp_max_sessions: 32768,
|
||||||
trace_packets: false,
|
trace_packets: false,
|
||||||
},
|
},
|
||||||
"2k": {
|
"high": {
|
||||||
runtime_gomaxprocs: 0,
|
runtime_gomaxprocs: 0,
|
||||||
mux_global_sessions: 32768,
|
mux_global_sessions: 65536,
|
||||||
max_concurrent_connections: 8192,
|
max_concurrent_connections: 65536,
|
||||||
max_concurrent_xhttp_requests: 16384,
|
max_concurrent_xhttp_requests: -1,
|
||||||
xhttp_max_sessions: 8192,
|
xhttp_max_sessions: 65536,
|
||||||
trace_packets: false,
|
trace_packets: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -59,7 +59,7 @@ const XRAY_NATIVE_TUNING_FIELDS = {
|
|||||||
xhttp_max_sessions: "cfgXrayMaxXHTTPSessions",
|
xhttp_max_sessions: "cfgXrayMaxXHTTPSessions",
|
||||||
};
|
};
|
||||||
|
|
||||||
function setXrayNativeTuningDefaults(profile = "2k") {
|
function setXrayNativeTuningDefaults(profile = "high") {
|
||||||
const t = XRAY_NATIVE_TUNING_DEFAULTS[profile] || XRAY_NATIVE_TUNING_DEFAULTS.safe;
|
const t = XRAY_NATIVE_TUNING_DEFAULTS[profile] || XRAY_NATIVE_TUNING_DEFAULTS.safe;
|
||||||
writeXrayNativeTuning(t);
|
writeXrayNativeTuning(t);
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-10
@@ -311,7 +311,7 @@
|
|||||||
<div class="field"><label>Max Upload (Mb/s)</label><input id="fUp" type="number" min="0" placeholder="0 = default"/></div>
|
<div class="field"><label>Max Upload (Mb/s)</label><input id="fUp" type="number" min="0" placeholder="0 = default"/></div>
|
||||||
<div class="field"><label>Max Download (Mb/s)</label><input id="fDown" type="number" min="0" placeholder="0 = default"/></div>
|
<div class="field"><label>Max Download (Mb/s)</label><input id="fDown" type="number" min="0" placeholder="0 = default"/></div>
|
||||||
<div class="field"><label>Data quota (GB) <span class="hint">0 = unlimited · 1024 = 1 TB</span></label><input id="fQuotaGB" type="number" min="0" step="0.01" placeholder="0"/></div>
|
<div class="field"><label>Data quota (GB) <span class="hint">0 = unlimited · 1024 = 1 TB</span></label><input id="fQuotaGB" type="number" min="0" step="0.01" placeholder="0"/></div>
|
||||||
<div class="field"><label>When quota is reached</label><select id="fQuotaAction"><option value="block">Block user</option><option value="throttle">Reduce speed</option></select></div>
|
<div class="field"><label>When quota is reached</label><select id="fQuotaAction"><option value="block">Block user</option><option value="throttle" selected>Reduce speed</option></select></div>
|
||||||
<div class="field"><label>Post-quota speed (Mb/s)</label><input id="fQuotaThrottle" type="number" min="1" value="1"/></div>
|
<div class="field"><label>Post-quota speed (Mb/s)</label><input id="fQuotaThrottle" type="number" min="1" value="1"/></div>
|
||||||
<div class="field"><label>Current usage</label><input id="fUsageDisplay" readonly value="0 B"/></div>
|
<div class="field"><label>Current usage</label><input id="fUsageDisplay" readonly value="0 B"/></div>
|
||||||
<div class="field"><label>Reset traffic counter</label><input id="fResetUsage" type="checkbox" style="width:16px;height:16px;margin-top:10px;"/></div>
|
<div class="field"><label>Reset traffic counter</label><input id="fResetUsage" type="checkbox" style="width:16px;height:16px;margin-top:10px;"/></div>
|
||||||
@@ -1509,16 +1509,16 @@
|
|||||||
<summary style="cursor:pointer;font-size:.76rem;font-weight:700;color:var(--text);">Native Xray scale tuning</summary>
|
<summary style="cursor:pointer;font-size:.76rem;font-weight:700;color:var(--text);">Native Xray scale tuning</summary>
|
||||||
<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="8192"/></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=4096, -1=unlimited</span></label><input type="number" min="-1" id="cfgXrayMaxConnections" placeholder="4096"/></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"><label>Concurrent XHTTP requests <span class="hint">0=8192, -1=unlimited</span></label><input type="number" min="-1" id="cfgXrayMaxXHTTPRequests" placeholder="8192"/></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=4096, -1=unlimited</span></label><input type="number" min="-1" id="cfgXrayMaxXHTTPSessions" placeholder="4096"/></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('2k')">Apply 2K 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;">The transport ceiling rejects sockets before native protocol/TLS work starts; the XHTTP ceilings bound concurrent handlers and session state. Keep the safe defaults unless load testing proves the VPS can sustain more; -1 disables an application ceiling and is not recommended on public listeners. HTTP/2 still keeps a 256-stream guard per connection. Transport buffers remain fixed to safe defaults. 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 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>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
@@ -1558,14 +1558,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=20260715quotareset1"></script>
|
<script defer src="assets/js/01-core.js?v=20260719xhttp502fix1"></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=20260715sshtraffic1"></script>
|
<script defer src="assets/js/03-ssh-users.js?v=20260719quotaaudit1"></script>
|
||||||
<script defer src="assets/js/04-xray.js?v=20260715quotareset1"></script>
|
<script defer src="assets/js/04-xray.js?v=20260715quotareset1"></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=20260715hardening1"></script>
|
<script defer src="assets/js/08-server-config.js?v=20260719xhttp502fix1"></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>
|
||||||
|
|||||||
@@ -586,7 +586,7 @@ func startResellerExpiryChecker(store *Store) {
|
|||||||
u.IsActive = false
|
u.IsActive = false
|
||||||
adminUsers.set(u)
|
adminUsers.set(u)
|
||||||
disconnectOwnerUsers(u.Username)
|
disconnectOwnerUsers(u.Username)
|
||||||
removeOwnerXrayClients(ctx, store, u.Username)
|
suspendOwnerXrayClients(ctx, store, u.Username)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reactivate resellers that have been renewed (inactive but expiry now in future/nil)
|
// Reactivate resellers that have been renewed (inactive but expiry now in future/nil)
|
||||||
@@ -602,6 +602,7 @@ func startResellerExpiryChecker(store *Store) {
|
|||||||
}
|
}
|
||||||
u.IsActive = true
|
u.IsActive = true
|
||||||
adminUsers.set(u)
|
adminUsers.set(u)
|
||||||
|
restoreOwnerXrayClients(ctx, store, u.Username)
|
||||||
}
|
}
|
||||||
|
|
||||||
sessions.cleanup()
|
sessions.cleanup()
|
||||||
@@ -862,7 +863,9 @@ func handleCreateReseller(store *Store) http.HandlerFunc {
|
|||||||
if u.Role == RoleReseller {
|
if u.Role == RoleReseller {
|
||||||
if !u.IsActive || (u.ExpiresAt != nil && time.Now().After(*u.ExpiresAt)) {
|
if !u.IsActive || (u.ExpiresAt != nil && time.Now().After(*u.ExpiresAt)) {
|
||||||
disconnectOwnerUsers(u.Username)
|
disconnectOwnerUsers(u.Username)
|
||||||
removeOwnerXrayClients(ctx, store, u.Username)
|
suspendOwnerXrayClients(ctx, store, u.Username)
|
||||||
|
} else {
|
||||||
|
restoreOwnerXrayClients(ctx, store, u.Username)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1907,10 +1907,25 @@ func handleCreateUser(store *Store) http.HandlerFunc {
|
|||||||
http.Error(w, "invalid json", http.StatusBadRequest)
|
http.Error(w, "invalid json", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
p.Username = strings.TrimSpace(p.Username)
|
||||||
if p.Username == "" {
|
if p.Username == "" {
|
||||||
http.Error(w, "username required", http.StatusBadRequest)
|
http.Error(w, "username required", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if p.MaxConnections < 0 || p.MaxConnections > 10000 {
|
||||||
|
http.Error(w, "max_connections must be between 0 and 10000", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if p.LimitUpMbps < 0 || p.LimitUpMbps > 1000000 || p.LimitDownMbps < 0 || p.LimitDownMbps > 1000000 {
|
||||||
|
http.Error(w, "bandwidth limits must be between 0 and 1000000 Mbps", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if p.ExpiresAt != "" {
|
||||||
|
if _, err := time.Parse(time.RFC3339, p.ExpiresAt); err != nil {
|
||||||
|
http.Error(w, "invalid expires_at (RFC3339 required)", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
if err := validateQuotaConfig(p.DataQuotaBytes, p.QuotaAction, p.QuotaThrottleMbps); err != nil {
|
if err := validateQuotaConfig(p.DataQuotaBytes, p.QuotaAction, p.QuotaThrottleMbps); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -353,9 +353,15 @@ func validateQuotaConfig(quotaBytes int64, action string, throttleMbps int) erro
|
|||||||
if quotaBytes < 0 {
|
if quotaBytes < 0 {
|
||||||
return fmt.Errorf("data_quota_bytes must be non-negative")
|
return fmt.Errorf("data_quota_bytes must be non-negative")
|
||||||
}
|
}
|
||||||
action = normalizeQuotaAction(action)
|
rawAction := strings.ToLower(strings.TrimSpace(action))
|
||||||
if quotaBytes > 0 && action == quotaActionThrottle && throttleMbps < 0 {
|
if rawAction != "" && rawAction != quotaActionBlock && rawAction != quotaActionThrottle {
|
||||||
|
return fmt.Errorf("quota_action must be block or throttle")
|
||||||
|
}
|
||||||
|
if throttleMbps < 0 {
|
||||||
return fmt.Errorf("quota_throttle_mbps must be non-negative")
|
return fmt.Errorf("quota_throttle_mbps must be non-negative")
|
||||||
}
|
}
|
||||||
|
if throttleMbps > 1000000 {
|
||||||
|
return fmt.Errorf("quota_throttle_mbps must not exceed 1000000")
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,67 @@ func TestNativeClientMaxConnectionsAndBatchedActiveDelta(t *testing.T) {
|
|||||||
release2()
|
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) {
|
func TestRemoveNativeQuotaPolicyPrunesPendingMaps(t *testing.T) {
|
||||||
m := &XrayManager{
|
m := &XrayManager{
|
||||||
nativeQuotaByUUID: map[string]*xrayNativeQuotaState{
|
nativeQuotaByUUID: map[string]*xrayNativeQuotaState{
|
||||||
|
|||||||
+73
-10
@@ -310,6 +310,66 @@ func removeOwnerXrayClients(ctx context.Context, store *Store, ownerUsername str
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// suspendOwnerXrayClients revokes transport access while preserving metadata,
|
||||||
|
// expiry, traffic and quota. This makes reseller suspension/renewal reversible.
|
||||||
|
func suspendOwnerXrayClients(ctx context.Context, store *Store, ownerUsername string) {
|
||||||
|
if store == nil || ownerUsername == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
clients, err := store.ListXrayClientsByOwner(ctx, ownerUsername)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("xray owner suspension: list %s: %v", ownerUsername, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
changed := false
|
||||||
|
for _, m := range clients {
|
||||||
|
xrayMgr.disconnectNativeClient(m.UUID)
|
||||||
|
if m.InboundTag == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := xrayMgr.RemoveXrayClient(m.InboundTag, m.UUID); err != nil {
|
||||||
|
log.Printf("xray owner suspension: remove %s from %s: %v", m.UUID, m.InboundTag, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if changed {
|
||||||
|
xrayMgr.restartIfExternalRunning()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// restoreOwnerXrayClients reactivates non-expired clients after reseller renewal.
|
||||||
|
func restoreOwnerXrayClients(ctx context.Context, store *Store, ownerUsername string) {
|
||||||
|
if store == nil || ownerUsername == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
clients, err := store.ListXrayClientsByOwner(ctx, ownerUsername)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("xray owner restore: list %s: %v", ownerUsername, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
changed := false
|
||||||
|
for _, m := range clients {
|
||||||
|
if m.InboundTag == "" || (m.ExpiresAt != nil && !m.ExpiresAt.After(now)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
email := m.Email
|
||||||
|
if email == "" {
|
||||||
|
email = m.UUID
|
||||||
|
}
|
||||||
|
if err := xrayMgr.EnsureXrayClient(m.InboundTag, m.UUID, email); err != nil {
|
||||||
|
log.Printf("xray owner restore: add %s to %s: %v", m.UUID, m.InboundTag, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
xrayMgr.setNativeQuotaPolicy(m)
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if changed {
|
||||||
|
xrayMgr.restartIfExternalRunning()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// startXrayClientExpiryChecker runs a background goroutine that removes expired
|
// startXrayClientExpiryChecker runs a background goroutine that removes expired
|
||||||
// Xray clients from both the config file and the database every 5 minutes.
|
// Xray clients from both the config file and the database every 5 minutes.
|
||||||
func startXrayClientExpiryChecker(store *Store) {
|
func startXrayClientExpiryChecker(store *Store) {
|
||||||
@@ -320,27 +380,32 @@ func startXrayClientExpiryChecker(store *Store) {
|
|||||||
ticker := time.NewTicker(5 * time.Minute)
|
ticker := time.NewTicker(5 * time.Minute)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
for range ticker.C {
|
for range ticker.C {
|
||||||
|
expireXrayClientsOnce(store)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func expireXrayClientsOnce(store *Store) {
|
||||||
|
if store == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
expired, err := store.ListExpiredXrayClients(ctx)
|
expired, err := store.ListExpiredXrayClients(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("xray expiry checker: list error: %v", err)
|
log.Printf("xray expiry checker: list error: %v", err)
|
||||||
continue
|
return
|
||||||
}
|
|
||||||
if len(expired) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
needRestart := false
|
needRestart := false
|
||||||
for _, m := range expired {
|
for _, m := range expired {
|
||||||
tag := m.InboundTag
|
tag := m.InboundTag
|
||||||
if tag == "" {
|
xrayMgr.disconnectNativeClient(m.UUID)
|
||||||
_ = store.DeleteXrayClientMeta(ctx, m.UUID)
|
if tag != "" {
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := xrayMgr.RemoveXrayClient(tag, m.UUID); err != nil {
|
if err := xrayMgr.RemoveXrayClient(tag, m.UUID); err != nil {
|
||||||
log.Printf("xray expiry: remove %s from %s: %v", m.UUID, tag, err)
|
log.Printf("xray expiry: remove %s from %s: %v", m.UUID, tag, err)
|
||||||
} else {
|
} else {
|
||||||
needRestart = true
|
needRestart = true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if err := store.DeleteXrayClientMeta(ctx, m.UUID); err != nil {
|
if err := store.DeleteXrayClientMeta(ctx, m.UUID); err != nil {
|
||||||
log.Printf("xray expiry: delete meta %s: %v", m.UUID, err)
|
log.Printf("xray expiry: delete meta %s: %v", m.UUID, err)
|
||||||
}
|
}
|
||||||
@@ -350,8 +415,6 @@ func startXrayClientExpiryChecker(store *Store) {
|
|||||||
xrayMgr.restartIfExternalRunning()
|
xrayMgr.restartIfExternalRunning()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResetXrayClientTraffic clears a client's persistent usage without removing
|
// ResetXrayClientTraffic clears a client's persistent usage without removing
|
||||||
// the account or changing its expiry/quota policy.
|
// the account or changing its expiry/quota policy.
|
||||||
|
|||||||
+101
-37
@@ -309,6 +309,10 @@ func initXrayManager(cfg *XrayConfig) {
|
|||||||
}
|
}
|
||||||
xrayMgr.mu.Unlock()
|
xrayMgr.mu.Unlock()
|
||||||
|
|
||||||
|
// Reconcile already-expired rows before the runtime loads DB-backed clients.
|
||||||
|
// The periodic checker intentionally sleeps between passes, so doing one pass
|
||||||
|
// here closes the startup window in which an expired UUID could reconnect.
|
||||||
|
expireXrayClientsOnce(statsStore)
|
||||||
xrayMgr.reloadNativeQuotaPolicies()
|
xrayMgr.reloadNativeQuotaPolicies()
|
||||||
|
|
||||||
// In native mode the in-process emulator records traffic directly, so the
|
// In native mode the in-process emulator records traffic directly, so the
|
||||||
@@ -478,11 +482,12 @@ func (m *XrayManager) recordNativeConnect(uuid, email string, state *xrayNativeQ
|
|||||||
if m.statsByEmail == nil {
|
if m.statsByEmail == nil {
|
||||||
m.statsByEmail = make(map[string]xrayRuntimeStat)
|
m.statsByEmail = make(map[string]xrayRuntimeStat)
|
||||||
}
|
}
|
||||||
st := m.statsByEmail[email]
|
key := firstNonEmpty(uuid, email)
|
||||||
|
st := m.statsByEmail[key]
|
||||||
st.Email = email
|
st.Email = email
|
||||||
st.LastActive = now
|
st.LastActive = now
|
||||||
st.ActiveConnections++
|
st.ActiveConnections++
|
||||||
m.statsByEmail[email] = st
|
m.statsByEmail[key] = st
|
||||||
m.statsMu.Unlock()
|
m.statsMu.Unlock()
|
||||||
|
|
||||||
m.queueNativeActiveDelta(uuid, email, 1, true, state)
|
m.queueNativeActiveDelta(uuid, email, 1, true, state)
|
||||||
@@ -499,11 +504,12 @@ func (m *XrayManager) recordNativeDisconnect(uuid, email string, state *xrayNati
|
|||||||
}
|
}
|
||||||
m.statsMu.Lock()
|
m.statsMu.Lock()
|
||||||
if m.statsByEmail != nil {
|
if m.statsByEmail != nil {
|
||||||
st := m.statsByEmail[email]
|
key := firstNonEmpty(uuid, email)
|
||||||
|
st := m.statsByEmail[key]
|
||||||
if st.ActiveConnections > 0 {
|
if st.ActiveConnections > 0 {
|
||||||
st.ActiveConnections--
|
st.ActiveConnections--
|
||||||
}
|
}
|
||||||
m.statsByEmail[email] = st
|
m.statsByEmail[key] = st
|
||||||
}
|
}
|
||||||
m.statsMu.Unlock()
|
m.statsMu.Unlock()
|
||||||
|
|
||||||
@@ -595,12 +601,13 @@ func (m *XrayManager) recordNativeTraffic(uuid, email string, up, down int64, ge
|
|||||||
if m.statsByEmail == nil {
|
if m.statsByEmail == nil {
|
||||||
m.statsByEmail = make(map[string]xrayRuntimeStat)
|
m.statsByEmail = make(map[string]xrayRuntimeStat)
|
||||||
}
|
}
|
||||||
st := m.statsByEmail[email]
|
key := firstNonEmpty(uuid, email)
|
||||||
|
st := m.statsByEmail[key]
|
||||||
st.Email = email
|
st.Email = email
|
||||||
st.Uplink += up
|
st.Uplink += up
|
||||||
st.Downlink += down
|
st.Downlink += down
|
||||||
st.LastActive = now
|
st.LastActive = now
|
||||||
m.statsByEmail[email] = st
|
m.statsByEmail[key] = st
|
||||||
m.statsMu.Unlock()
|
m.statsMu.Unlock()
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -2261,6 +2268,16 @@ func (m *XrayManager) modifyRawConfig(fn func(cfg map[string]interface{}) error)
|
|||||||
|
|
||||||
// AddXrayClient adds a client to the named inbound and saves the config.
|
// AddXrayClient adds a client to the named inbound and saves the config.
|
||||||
func (m *XrayManager) AddXrayClient(inboundTag, uuid, email string) error {
|
func (m *XrayManager) AddXrayClient(inboundTag, uuid, email string) error {
|
||||||
|
return m.addXrayClient(inboundTag, uuid, email, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureXrayClient restores a previously suspended DB-backed client without
|
||||||
|
// failing if it is already present in the active config.
|
||||||
|
func (m *XrayManager) EnsureXrayClient(inboundTag, uuid, email string) error {
|
||||||
|
return m.addXrayClient(inboundTag, uuid, email, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *XrayManager) addXrayClient(inboundTag, uuid, email string, allowExisting bool) error {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
err := m.modifyRawConfig(func(raw map[string]interface{}) error {
|
err := m.modifyRawConfig(func(raw map[string]interface{}) error {
|
||||||
@@ -2287,6 +2304,10 @@ func (m *XrayManager) AddXrayClient(inboundTag, uuid, email string) error {
|
|||||||
id, _ = cm["password"].(string)
|
id, _ = cm["password"].(string)
|
||||||
}
|
}
|
||||||
if id == uuid {
|
if id == uuid {
|
||||||
|
if allowExisting {
|
||||||
|
cm["email"] = email
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return fmt.Errorf("UUID %s already exists in inbound %s", uuid, inboundTag)
|
return fmt.Errorf("UUID %s already exists in inbound %s", uuid, inboundTag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2594,6 +2615,16 @@ func handleXrayClientAdd(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "inbound_tag and uuid required", http.StatusBadRequest)
|
http.Error(w, "inbound_tag and uuid required", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
req.InboundTag = strings.TrimSpace(req.InboundTag)
|
||||||
|
req.UUID = strings.TrimSpace(req.UUID)
|
||||||
|
if _, err := parseUUID(req.UUID); err != nil {
|
||||||
|
http.Error(w, "invalid uuid: "+err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.MaxConnections < 0 || req.MaxConnections > 10000 {
|
||||||
|
http.Error(w, "max_connections must be between 0 and 10000", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
if err := validateQuotaConfig(req.DataQuotaBytes, req.QuotaAction, req.QuotaThrottleMbps); err != nil {
|
if err := validateQuotaConfig(req.DataQuotaBytes, req.QuotaAction, req.QuotaThrottleMbps); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
@@ -2645,6 +2676,11 @@ func handleXrayClientAdd(w http.ResponseWriter, r *http.Request) {
|
|||||||
if req.Email == "" {
|
if req.Email == "" {
|
||||||
req.Email = req.UUID
|
req.Email = req.UUID
|
||||||
}
|
}
|
||||||
|
expiresAt, err := parseOptionalXrayExpiry(req.ExpiresAt)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
sess := sessionFromCtx(r.Context())
|
sess := sessionFromCtx(r.Context())
|
||||||
ownerUsername := ""
|
ownerUsername := ""
|
||||||
@@ -2681,10 +2717,7 @@ func handleXrayClientAdd(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := xrayMgr.AddXrayClient(req.InboundTag, req.UUID, req.Email); err != nil {
|
var savedMeta *XrayClientMeta
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if statsStore != nil {
|
if statsStore != nil {
|
||||||
meta := XrayClientMeta{
|
meta := XrayClientMeta{
|
||||||
UUID: req.UUID,
|
UUID: req.UUID,
|
||||||
@@ -2697,24 +2730,22 @@ func handleXrayClientAdd(w http.ResponseWriter, r *http.Request) {
|
|||||||
QuotaAction: normalizeQuotaAction(req.QuotaAction),
|
QuotaAction: normalizeQuotaAction(req.QuotaAction),
|
||||||
QuotaThrottleMbps: quotaThrottleMbpsOrDefault(req.QuotaThrottleMbps),
|
QuotaThrottleMbps: quotaThrottleMbpsOrDefault(req.QuotaThrottleMbps),
|
||||||
}
|
}
|
||||||
if req.ExpiresAt != "" {
|
meta.ExpiresAt = expiresAt
|
||||||
var t time.Time
|
|
||||||
var err error
|
|
||||||
for _, layout := range []string{time.RFC3339, "2006-01-02T15:04", "2006-01-02"} {
|
|
||||||
t, err = time.Parse(layout, req.ExpiresAt)
|
|
||||||
if err == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err == nil {
|
|
||||||
meta.ExpiresAt = &t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := statsStore.UpsertXrayClientMeta(r.Context(), meta); err != nil {
|
if err := statsStore.UpsertXrayClientMeta(r.Context(), meta); err != nil {
|
||||||
xrayLogf("xray: save meta for %s: %v", req.UUID, err)
|
http.Error(w, "save client metadata failed: "+err.Error(), http.StatusInternalServerError)
|
||||||
} else {
|
return
|
||||||
xrayMgr.setNativeQuotaPolicy(&meta)
|
|
||||||
}
|
}
|
||||||
|
xrayMgr.setNativeQuotaPolicy(&meta)
|
||||||
|
savedMeta = &meta
|
||||||
|
}
|
||||||
|
// Publish the credential only after its quota/owner/expiry policy exists, so
|
||||||
|
// a fast native client can never enter an unmetered window during creation.
|
||||||
|
if err := xrayMgr.AddXrayClient(req.InboundTag, req.UUID, req.Email); err != nil {
|
||||||
|
if savedMeta != nil {
|
||||||
|
_ = statsStore.DeleteXrayClientMeta(r.Context(), req.UUID)
|
||||||
|
}
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
xrayMgr.restartIfExternalRunning()
|
xrayMgr.restartIfExternalRunning()
|
||||||
w.WriteHeader(http.StatusCreated)
|
w.WriteHeader(http.StatusCreated)
|
||||||
@@ -2747,6 +2778,15 @@ func handleXrayClientUpdate(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "uuid required", http.StatusBadRequest)
|
http.Error(w, "uuid required", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
req.UUID = strings.TrimSpace(req.UUID)
|
||||||
|
if _, err := parseUUID(req.UUID); err != nil {
|
||||||
|
http.Error(w, "invalid uuid: "+err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.MaxConnections < 0 || req.MaxConnections > 10000 {
|
||||||
|
http.Error(w, "max_connections must be between 0 and 10000", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
if err := validateQuotaConfig(req.DataQuotaBytes, req.QuotaAction, req.QuotaThrottleMbps); err != nil {
|
if err := validateQuotaConfig(req.DataQuotaBytes, req.QuotaAction, req.QuotaThrottleMbps); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
@@ -2784,6 +2824,15 @@ func handleXrayClientUpdate(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "forbidden", http.StatusForbidden)
|
http.Error(w, "forbidden", http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
req.Email = strings.TrimSpace(req.Email)
|
||||||
|
if req.Email == "" {
|
||||||
|
req.Email = firstNonEmpty(strings.TrimSpace(req.Name), existing.Email, req.UUID)
|
||||||
|
}
|
||||||
|
expiresAt, err := parseOptionalXrayExpiry(req.ExpiresAt)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
meta := XrayClientMeta{
|
meta := XrayClientMeta{
|
||||||
UUID: req.UUID,
|
UUID: req.UUID,
|
||||||
@@ -2798,15 +2847,18 @@ func handleXrayClientUpdate(w http.ResponseWriter, r *http.Request) {
|
|||||||
TotalUplinkBytes: existing.TotalUplinkBytes,
|
TotalUplinkBytes: existing.TotalUplinkBytes,
|
||||||
TotalDownlinkBytes: existing.TotalDownlinkBytes,
|
TotalDownlinkBytes: existing.TotalDownlinkBytes,
|
||||||
}
|
}
|
||||||
if req.ExpiresAt != "" {
|
meta.ExpiresAt = expiresAt
|
||||||
for _, layout := range []string{time.RFC3339, "2006-01-02T15:04", "2006-01-02"} {
|
emailChanged := req.Email != existing.Email
|
||||||
if t, err := time.Parse(layout, req.ExpiresAt); err == nil {
|
if emailChanged {
|
||||||
meta.ExpiresAt = &t
|
if err := xrayMgr.UpdateXrayClientEmail(req.UUID, req.Email); err != nil {
|
||||||
break
|
http.Error(w, "update config email failed: "+err.Error(), http.StatusInternalServerError)
|
||||||
}
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := statsStore.UpsertXrayClientMeta(r.Context(), meta); err != nil {
|
if err := statsStore.UpsertXrayClientMeta(r.Context(), meta); err != nil {
|
||||||
|
if emailChanged {
|
||||||
|
_ = xrayMgr.UpdateXrayClientEmail(req.UUID, existing.Email)
|
||||||
|
}
|
||||||
http.Error(w, "update failed: "+err.Error(), http.StatusInternalServerError)
|
http.Error(w, "update failed: "+err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -2819,16 +2871,28 @@ func handleXrayClientUpdate(w http.ResponseWriter, r *http.Request) {
|
|||||||
meta.TotalDownlinkBytes = 0
|
meta.TotalDownlinkBytes = 0
|
||||||
}
|
}
|
||||||
xrayMgr.setNativeQuotaPolicy(&meta)
|
xrayMgr.setNativeQuotaPolicy(&meta)
|
||||||
if req.Email != "" {
|
if meta.ExpiresAt != nil && !meta.ExpiresAt.After(time.Now()) {
|
||||||
if err := xrayMgr.UpdateXrayClientEmail(req.UUID, req.Email); err != nil {
|
xrayMgr.disconnectNativeClient(req.UUID)
|
||||||
xrayLogf("xray: update config email for %s: %v", req.UUID, err)
|
}
|
||||||
} else {
|
if emailChanged {
|
||||||
xrayMgr.restartIfExternalRunning()
|
xrayMgr.restartIfExternalRunning()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseOptionalXrayExpiry(raw string) (*time.Time, error) {
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
if raw == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
for _, layout := range []string{time.RFC3339, "2006-01-02T15:04", "2006-01-02"} {
|
||||||
|
if t, err := time.Parse(layout, raw); err == nil {
|
||||||
|
return &t, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("invalid expires_at (RFC3339, YYYY-MM-DDThh:mm, or YYYY-MM-DD required)")
|
||||||
|
}
|
||||||
|
|
||||||
func handleXrayClientResetTraffic(w http.ResponseWriter, r *http.Request) {
|
func handleXrayClientResetTraffic(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
|||||||
+3
-3
@@ -379,8 +379,8 @@ func (ib *nativeInbound) handleVLESS(stream net.Conn, remote net.Addr) {
|
|||||||
logNativePreAuthRejection("native xray: inbound %q rejected unknown VLESS uuid from %s", ib.tag, remote)
|
logNativePreAuthRejection("native xray: inbound %q rejected unknown VLESS uuid from %s", ib.tag, remote)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if xrayMgr.nativeQuotaBlocked(client.uuid) {
|
if reason := xrayMgr.nativeClientAccessDenied(client.uuid); reason != "" {
|
||||||
xrayLogf("native xray: inbound %q rejected VLESS user %s after data quota", ib.tag, client.email)
|
xrayLogf("native xray: inbound %q rejected VLESS user %s: %s", ib.tag, client.email, reason)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -433,7 +433,7 @@ func (ib *nativeInbound) handleVLESS(stream net.Conn, remote net.Addr) {
|
|||||||
xrayLogf("native xray: inbound %q VLESS command %d not supported yet", ib.tag, cmd[0])
|
xrayLogf("native xray: inbound %q VLESS command %d not supported yet", ib.tag, cmd[0])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
releaseConnection, quotaState, ok := xrayMgr.acquireNativeClientConnection(client.uuid, client.email)
|
releaseConnection, quotaState, ok := xrayMgr.acquireNativeClientConnection(client.uuid, client.email, stream)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
+84
-1
@@ -5,6 +5,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"golang.org/x/time/rate"
|
"golang.org/x/time/rate"
|
||||||
)
|
)
|
||||||
@@ -25,6 +26,10 @@ type xrayNativeQuotaState struct {
|
|||||||
generation uint64
|
generation uint64
|
||||||
maxConns int
|
maxConns int
|
||||||
activeConns int
|
activeConns int
|
||||||
|
owner string
|
||||||
|
expiresAt time.Time
|
||||||
|
hasExpiry bool
|
||||||
|
connections map[io.Closer]struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *XrayManager) reloadNativeQuotaPolicies() {
|
func (m *XrayManager) reloadNativeQuotaPolicies() {
|
||||||
@@ -60,9 +65,19 @@ func newXrayNativeQuotaState(meta *XrayClientMeta) *xrayNativeQuotaState {
|
|||||||
throttleMbps: quotaThrottleMbpsOrDefault(meta.QuotaThrottleMbps),
|
throttleMbps: quotaThrottleMbpsOrDefault(meta.QuotaThrottleMbps),
|
||||||
generation: 1,
|
generation: 1,
|
||||||
maxConns: normalizeXrayMaxConns(meta.MaxConns),
|
maxConns: normalizeXrayMaxConns(meta.MaxConns),
|
||||||
|
owner: strings.TrimSpace(meta.OwnerUsername),
|
||||||
|
hasExpiry: meta.ExpiresAt != nil,
|
||||||
|
expiresAt: xrayExpiryValue(meta.ExpiresAt),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func xrayExpiryValue(expiry *time.Time) time.Time {
|
||||||
|
if expiry == nil {
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
return *expiry
|
||||||
|
}
|
||||||
|
|
||||||
func normalizeXrayMaxConns(v int) int {
|
func normalizeXrayMaxConns(v int) int {
|
||||||
if v < 0 {
|
if v < 0 {
|
||||||
return 0
|
return 0
|
||||||
@@ -92,6 +107,9 @@ func (m *XrayManager) setNativeQuotaPolicy(meta *XrayClientMeta) {
|
|||||||
existing.action = normalizeQuotaAction(meta.QuotaAction)
|
existing.action = normalizeQuotaAction(meta.QuotaAction)
|
||||||
existing.throttleMbps = quotaThrottleMbpsOrDefault(meta.QuotaThrottleMbps)
|
existing.throttleMbps = quotaThrottleMbpsOrDefault(meta.QuotaThrottleMbps)
|
||||||
existing.maxConns = normalizeXrayMaxConns(meta.MaxConns)
|
existing.maxConns = normalizeXrayMaxConns(meta.MaxConns)
|
||||||
|
existing.owner = strings.TrimSpace(meta.OwnerUsername)
|
||||||
|
existing.hasExpiry = meta.ExpiresAt != nil
|
||||||
|
existing.expiresAt = xrayExpiryValue(meta.ExpiresAt)
|
||||||
existing.limiter = nil
|
existing.limiter = nil
|
||||||
existing.mu.Unlock()
|
existing.mu.Unlock()
|
||||||
}
|
}
|
||||||
@@ -102,8 +120,10 @@ func (m *XrayManager) removeNativeQuotaPolicy(uuid string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
m.nativeQuotaMu.Lock()
|
m.nativeQuotaMu.Lock()
|
||||||
|
state := m.nativeQuotaByUUID[uuid]
|
||||||
delete(m.nativeQuotaByUUID, uuid)
|
delete(m.nativeQuotaByUUID, uuid)
|
||||||
m.nativeQuotaMu.Unlock()
|
m.nativeQuotaMu.Unlock()
|
||||||
|
closeNativeClientConnections(state)
|
||||||
|
|
||||||
// Do not retain failed traffic/active deltas for a client that no longer
|
// Do not retain failed traffic/active deltas for a client that no longer
|
||||||
// exists. This also bounds the pending maps during a prolonged DB outage.
|
// exists. This also bounds the pending maps during a prolonged DB outage.
|
||||||
@@ -216,10 +236,19 @@ func (m *XrayManager) nativeQuotaState(uuid string) *xrayNativeQuotaState {
|
|||||||
// acquireNativeClientConnection enforces the DB-backed max_conns policy across
|
// acquireNativeClientConnection enforces the DB-backed max_conns policy across
|
||||||
// every native inbound and transport. The returned release function is safe to
|
// every native inbound and transport. The returned release function is safe to
|
||||||
// call more than once and keeps runtime/DB online counters in sync.
|
// call more than once and keeps runtime/DB online counters in sync.
|
||||||
func (m *XrayManager) acquireNativeClientConnection(uuid, email string) (func(), *xrayNativeQuotaState, bool) {
|
func (m *XrayManager) acquireNativeClientConnection(uuid, email string, closers ...io.Closer) (func(), *xrayNativeQuotaState, bool) {
|
||||||
state := m.nativeQuotaState(uuid)
|
state := m.nativeQuotaState(uuid)
|
||||||
|
var closer io.Closer
|
||||||
|
if len(closers) > 0 {
|
||||||
|
closer = closers[0]
|
||||||
|
}
|
||||||
if state != nil {
|
if state != nil {
|
||||||
state.mu.Lock()
|
state.mu.Lock()
|
||||||
|
if reason := nativeClientAccessDeniedLocked(state); reason != "" {
|
||||||
|
state.mu.Unlock()
|
||||||
|
xrayLogf("native xray: rejected user %s: %s", email, reason)
|
||||||
|
return nil, state, false
|
||||||
|
}
|
||||||
if state.maxConns > 0 && state.activeConns >= state.maxConns {
|
if state.maxConns > 0 && state.activeConns >= state.maxConns {
|
||||||
limit := state.maxConns
|
limit := state.maxConns
|
||||||
state.mu.Unlock()
|
state.mu.Unlock()
|
||||||
@@ -227,6 +256,12 @@ func (m *XrayManager) acquireNativeClientConnection(uuid, email string) (func(),
|
|||||||
return nil, state, false
|
return nil, state, false
|
||||||
}
|
}
|
||||||
state.activeConns++
|
state.activeConns++
|
||||||
|
if closer != nil {
|
||||||
|
if state.connections == nil {
|
||||||
|
state.connections = make(map[io.Closer]struct{})
|
||||||
|
}
|
||||||
|
state.connections[closer] = struct{}{}
|
||||||
|
}
|
||||||
state.mu.Unlock()
|
state.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,6 +271,9 @@ func (m *XrayManager) acquireNativeClientConnection(uuid, email string) (func(),
|
|||||||
once.Do(func() {
|
once.Do(func() {
|
||||||
if state != nil {
|
if state != nil {
|
||||||
state.mu.Lock()
|
state.mu.Lock()
|
||||||
|
if closer != nil {
|
||||||
|
delete(state.connections, closer)
|
||||||
|
}
|
||||||
if state.activeConns > 0 {
|
if state.activeConns > 0 {
|
||||||
state.activeConns--
|
state.activeConns--
|
||||||
}
|
}
|
||||||
@@ -246,6 +284,51 @@ func (m *XrayManager) acquireNativeClientConnection(uuid, email string) (func(),
|
|||||||
}, state, true
|
}, state, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func closeNativeClientConnections(state *xrayNativeQuotaState) {
|
||||||
|
if state == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state.mu.Lock()
|
||||||
|
closers := make([]io.Closer, 0, len(state.connections))
|
||||||
|
for closer := range state.connections {
|
||||||
|
closers = append(closers, closer)
|
||||||
|
}
|
||||||
|
state.mu.Unlock()
|
||||||
|
for _, closer := range closers {
|
||||||
|
_ = closer.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *XrayManager) disconnectNativeClient(uuid string) {
|
||||||
|
closeNativeClientConnections(m.nativeQuotaState(uuid))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *XrayManager) nativeClientAccessDenied(uuid string) string {
|
||||||
|
state := m.nativeQuotaState(uuid)
|
||||||
|
if state == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
state.mu.Lock()
|
||||||
|
reason := nativeClientAccessDeniedLocked(state)
|
||||||
|
state.mu.Unlock()
|
||||||
|
return reason
|
||||||
|
}
|
||||||
|
|
||||||
|
func nativeClientAccessDeniedLocked(state *xrayNativeQuotaState) string {
|
||||||
|
if state.hasExpiry && !state.expiresAt.After(time.Now()) {
|
||||||
|
return "expired"
|
||||||
|
}
|
||||||
|
if state.owner != "" {
|
||||||
|
if err := ownerIsActive(state.owner); err != nil {
|
||||||
|
return "owner suspended or expired"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if state.quotaBytes > 0 && normalizeQuotaAction(state.action) == quotaActionBlock && state.usedBytes >= state.quotaBytes {
|
||||||
|
return "data quota exceeded"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func (m *XrayManager) nativeQuotaBlocked(uuid string) bool {
|
func (m *XrayManager) nativeQuotaBlocked(uuid string) bool {
|
||||||
return nativeQuotaStateBlocked(m.nativeQuotaState(uuid))
|
return nativeQuotaStateBlocked(m.nativeQuotaState(uuid))
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -673,8 +673,8 @@ func (ib *nativeInbound) handleVMess(stream net.Conn, remote net.Addr) {
|
|||||||
logNativePreAuthRejection("native xray: inbound %q rejected unknown/expired VMess auth id from %s", ib.tag, remote)
|
logNativePreAuthRejection("native xray: inbound %q rejected unknown/expired VMess auth id from %s", ib.tag, remote)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if xrayMgr.nativeQuotaBlocked(client.uuid) {
|
if reason := xrayMgr.nativeClientAccessDenied(client.uuid); reason != "" {
|
||||||
log.Printf("native xray: inbound %q rejected VMess user %s after data quota", ib.tag, client.email)
|
log.Printf("native xray: inbound %q rejected VMess user %s: %s", ib.tag, client.email, reason)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -694,7 +694,7 @@ func (ib *nativeInbound) handleVMess(stream net.Conn, remote net.Addr) {
|
|||||||
log.Printf("native xray: inbound %q VMess command %d not supported yet", ib.tag, req.command)
|
log.Printf("native xray: inbound %q VMess command %d not supported yet", ib.tag, req.command)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
releaseConnection, quotaState, ok := xrayMgr.acquireNativeClientConnection(client.uuid, client.email)
|
releaseConnection, quotaState, ok := xrayMgr.acquireNativeClientConnection(client.uuid, client.email, stream)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user