Compare commits

...
2 Commits
Author SHA1 Message Date
penguinehis 2f4cb008ae Merge branch 'main' of https://git.dr2.site/penguinehis/DragonCoreSSH-NewWEB 2026-07-20 00:00:52 -03:00
penguinehis 9bbd950b66 Fix quota 2026-07-20 00:00:39 -03:00
14 changed files with 444 additions and 109 deletions
+13 -7
View File
@@ -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
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:
@@ -609,7 +609,7 @@ The migration confirmation is rendered inside the panel. If saving fails, the te
### 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:
@@ -1144,14 +1144,17 @@ curl -s "http://SERVER_IP:9090/api/users" -H "X-Session-Token: $TOKEN"
#### `GET /api/users` — session
- 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
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` (010000); `expires_at` (RFC3339 string); `limit_mbps_up` and `limit_mbps_down` (01000000); `data_quota_bytes` (non-negative int64); `quota_action` (`block` or `throttle`); `quota_throttle_mbps` (01000000; 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.
- 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
- 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`.
@@ -1261,16 +1264,19 @@ Read/write a managed server's `config.json`. Query: `server_id`. Local delegates
#### `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.
- `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
- 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` (010000), `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`.
#### `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` (010000), `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`.
#### `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
- 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`.
+31
View File
@@ -750,3 +750,34 @@ function patchRenderedInbounds(inbounds) {
}
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."
});
+4 -1
View File
@@ -9,7 +9,10 @@ cancelUserBtn.addEventListener("click", () => {
function prepareNewSSHUser() {
userForm.reset();
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;
fUsageDisplay.value = "0 B";
fResetUsage.checked = false;
+10 -10
View File
@@ -35,18 +35,18 @@ function toggleUdpgwFields(on) {
const XRAY_NATIVE_TUNING_DEFAULTS = {
safe: {
runtime_gomaxprocs: 0,
mux_global_sessions: 8192,
max_concurrent_connections: 4096,
max_concurrent_xhttp_requests: 8192,
xhttp_max_sessions: 4096,
mux_global_sessions: 32768,
max_concurrent_connections: 32768,
max_concurrent_xhttp_requests: -1,
xhttp_max_sessions: 32768,
trace_packets: false,
},
"2k": {
"high": {
runtime_gomaxprocs: 0,
mux_global_sessions: 32768,
max_concurrent_connections: 8192,
max_concurrent_xhttp_requests: 16384,
xhttp_max_sessions: 8192,
mux_global_sessions: 65536,
max_concurrent_connections: 65536,
max_concurrent_xhttp_requests: -1,
xhttp_max_sessions: 65536,
trace_packets: false,
},
};
@@ -59,7 +59,7 @@ const XRAY_NATIVE_TUNING_FIELDS = {
xhttp_max_sessions: "cfgXrayMaxXHTTPSessions",
};
function setXrayNativeTuningDefaults(profile = "2k") {
function setXrayNativeTuningDefaults(profile = "high") {
const t = XRAY_NATIVE_TUNING_DEFAULTS[profile] || XRAY_NATIVE_TUNING_DEFAULTS.safe;
writeXrayNativeTuning(t);
}
+10 -10
View File
@@ -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 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>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>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>
@@ -1509,16 +1509,16 @@
<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="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 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>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>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>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"><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>
<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>
</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>
</details>
</div>
@@ -1558,14 +1558,14 @@
<!-- app.js was split into ordered modules for maintainability. They are plain
classic scripts sharing one global scope; `defer` preserves execution 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/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/05-resellers.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/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/11-update-status.js?v=20260714pamfix1"></script>
<script defer src="assets/js/12-bot.js?v=20260714pamfix1"></script>
+5 -2
View File
@@ -586,7 +586,7 @@ func startResellerExpiryChecker(store *Store) {
u.IsActive = false
adminUsers.set(u)
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)
@@ -602,6 +602,7 @@ func startResellerExpiryChecker(store *Store) {
}
u.IsActive = true
adminUsers.set(u)
restoreOwnerXrayClients(ctx, store, u.Username)
}
sessions.cleanup()
@@ -862,7 +863,9 @@ func handleCreateReseller(store *Store) http.HandlerFunc {
if u.Role == RoleReseller {
if !u.IsActive || (u.ExpiresAt != nil && time.Now().After(*u.ExpiresAt)) {
disconnectOwnerUsers(u.Username)
removeOwnerXrayClients(ctx, store, u.Username)
suspendOwnerXrayClients(ctx, store, u.Username)
} else {
restoreOwnerXrayClients(ctx, store, u.Username)
}
}
+15
View File
@@ -1907,10 +1907,25 @@ func handleCreateUser(store *Store) http.HandlerFunc {
http.Error(w, "invalid json", http.StatusBadRequest)
return
}
p.Username = strings.TrimSpace(p.Username)
if p.Username == "" {
http.Error(w, "username required", http.StatusBadRequest)
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 {
http.Error(w, err.Error(), http.StatusBadRequest)
return
+11 -5
View File
@@ -106,9 +106,9 @@ func (s *Store) ResetSSHUserTraffic(ctx context.Context, username string) error
return nil
}
_, err := s.db.ExecContext(ctx, `
UPDATE ssh_users
SET total_uplink_bytes = 0, total_downlink_bytes = 0
WHERE username = $1`, username)
UPDATE ssh_users
SET total_uplink_bytes = 0, total_downlink_bytes = 0
WHERE username = $1`, username)
return err
}
@@ -353,9 +353,15 @@ func validateQuotaConfig(quotaBytes int64, action string, throttleMbps int) erro
if quotaBytes < 0 {
return fmt.Errorf("data_quota_bytes must be non-negative")
}
action = normalizeQuotaAction(action)
if quotaBytes > 0 && action == quotaActionThrottle && throttleMbps < 0 {
rawAction := strings.ToLower(strings.TrimSpace(action))
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")
}
if throttleMbps > 1000000 {
return fmt.Errorf("quota_throttle_mbps must not exceed 1000000")
}
return nil
}
+61
View File
@@ -71,6 +71,67 @@ func TestNativeClientMaxConnectionsAndBatchedActiveDelta(t *testing.T) {
release2()
}
func TestNativeOnlineUsersAreKeyedByUUID(t *testing.T) {
oldStore := statsStore
statsStore = nil
defer func() { statsStore = oldStore }()
m := &XrayManager{}
m.recordNativeConnect("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "shared@example", nil)
m.recordNativeConnect("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "shared@example", nil)
if got := m.CountOnlineUsers(); got != 2 {
t.Fatalf("online UUID count = %d, want 2 for two UUIDs sharing one email", got)
}
m.statsMu.RLock()
_, first := m.statsByEmail["aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"]
_, second := m.statsByEmail["bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"]
m.statsMu.RUnlock()
if !first || !second {
t.Fatal("native runtime stats were not stored under canonical UUID keys")
}
}
func TestNativeExpiryRejectsAndDisconnectsClient(t *testing.T) {
const uuid = "cccccccc-cccc-cccc-cccc-cccccccccccc"
state := &xrayNativeQuotaState{hasExpiry: true, expiresAt: time.Now().Add(time.Hour), generation: 1}
m := &XrayManager{nativeQuotaByUUID: map[string]*xrayNativeQuotaState{uuid: state}}
closer := &closeTrackingReader{}
release, _, ok := m.acquireNativeClientConnection(uuid, "expiry@example", closer)
if !ok {
t.Fatal("unexpired client was rejected")
}
m.disconnectNativeClient(uuid)
if !closer.closed.Load() {
t.Fatal("active native client was not closed during revocation")
}
release()
state.mu.Lock()
state.expiresAt = time.Now().Add(-time.Second)
state.mu.Unlock()
if reason := m.nativeClientAccessDenied(uuid); reason != "expired" {
t.Fatalf("expired client denial = %q, want expired", reason)
}
if _, _, ok := m.acquireNativeClientConnection(uuid, "expiry@example"); ok {
t.Fatal("expired client acquired a new connection")
}
}
func TestQuotaAndExpiryValidationRejectsUnsafeInput(t *testing.T) {
if err := validateQuotaConfig(1, "typo", 1); err == nil {
t.Fatal("unknown quota action was accepted")
}
if err := validateQuotaConfig(1, quotaActionBlock, -1); err == nil {
t.Fatal("negative throttle setting was accepted")
}
if _, err := parseOptionalXrayExpiry("not-a-date"); err == nil {
t.Fatal("invalid Xray expiry was accepted")
}
if exp, err := parseOptionalXrayExpiry(""); err != nil || exp != nil {
t.Fatalf("empty Xray expiry = (%v, %v), want nil, nil", exp, err)
}
}
func TestRemoveNativeQuotaPolicyPrunesPendingMaps(t *testing.T) {
m := &XrayManager{
nativeQuotaByUUID: map[string]*xrayNativeQuotaState{
+92 -29
View File
@@ -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
// Xray clients from both the config file and the database every 5 minutes.
func startXrayClientExpiryChecker(store *Store) {
@@ -320,39 +380,42 @@ func startXrayClientExpiryChecker(store *Store) {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for range ticker.C {
ctx := context.Background()
expired, err := store.ListExpiredXrayClients(ctx)
if err != nil {
log.Printf("xray expiry checker: list error: %v", err)
continue
}
if len(expired) == 0 {
continue
}
needRestart := false
for _, m := range expired {
tag := m.InboundTag
if tag == "" {
_ = store.DeleteXrayClientMeta(ctx, m.UUID)
continue
}
if err := xrayMgr.RemoveXrayClient(tag, m.UUID); err != nil {
log.Printf("xray expiry: remove %s from %s: %v", m.UUID, tag, err)
} else {
needRestart = true
}
if err := store.DeleteXrayClientMeta(ctx, m.UUID); err != nil {
log.Printf("xray expiry: delete meta %s: %v", m.UUID, err)
}
log.Printf("xray expiry: removed expired client %q (%s) from inbound %s", m.Name, m.UUID, tag)
}
if needRestart {
xrayMgr.restartIfExternalRunning()
}
expireXrayClientsOnce(store)
}
}()
}
func expireXrayClientsOnce(store *Store) {
if store == nil {
return
}
ctx := context.Background()
expired, err := store.ListExpiredXrayClients(ctx)
if err != nil {
log.Printf("xray expiry checker: list error: %v", err)
return
}
needRestart := false
for _, m := range expired {
tag := m.InboundTag
xrayMgr.disconnectNativeClient(m.UUID)
if tag != "" {
if err := xrayMgr.RemoveXrayClient(tag, m.UUID); err != nil {
log.Printf("xray expiry: remove %s from %s: %v", m.UUID, tag, err)
} else {
needRestart = true
}
}
if err := store.DeleteXrayClientMeta(ctx, m.UUID); err != nil {
log.Printf("xray expiry: delete meta %s: %v", m.UUID, err)
}
log.Printf("xray expiry: removed expired client %q (%s) from inbound %s", m.Name, m.UUID, tag)
}
if needRestart {
xrayMgr.restartIfExternalRunning()
}
}
// ResetXrayClientTraffic clears a client's persistent usage without removing
// the account or changing its expiry/quota policy.
func (s *Store) ResetXrayClientTraffic(ctx context.Context, uuid string) error {
+102 -38
View File
@@ -309,6 +309,10 @@ func initXrayManager(cfg *XrayConfig) {
}
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()
// 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 {
m.statsByEmail = make(map[string]xrayRuntimeStat)
}
st := m.statsByEmail[email]
key := firstNonEmpty(uuid, email)
st := m.statsByEmail[key]
st.Email = email
st.LastActive = now
st.ActiveConnections++
m.statsByEmail[email] = st
m.statsByEmail[key] = st
m.statsMu.Unlock()
m.queueNativeActiveDelta(uuid, email, 1, true, state)
@@ -499,11 +504,12 @@ func (m *XrayManager) recordNativeDisconnect(uuid, email string, state *xrayNati
}
m.statsMu.Lock()
if m.statsByEmail != nil {
st := m.statsByEmail[email]
key := firstNonEmpty(uuid, email)
st := m.statsByEmail[key]
if st.ActiveConnections > 0 {
st.ActiveConnections--
}
m.statsByEmail[email] = st
m.statsByEmail[key] = st
}
m.statsMu.Unlock()
@@ -595,12 +601,13 @@ func (m *XrayManager) recordNativeTraffic(uuid, email string, up, down int64, ge
if m.statsByEmail == nil {
m.statsByEmail = make(map[string]xrayRuntimeStat)
}
st := m.statsByEmail[email]
key := firstNonEmpty(uuid, email)
st := m.statsByEmail[key]
st.Email = email
st.Uplink += up
st.Downlink += down
st.LastActive = now
m.statsByEmail[email] = st
m.statsByEmail[key] = st
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.
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()
defer m.mu.Unlock()
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)
}
if id == uuid {
if allowExisting {
cm["email"] = email
return nil
}
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)
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 {
http.Error(w, err.Error(), http.StatusBadRequest)
return
@@ -2645,6 +2676,11 @@ func handleXrayClientAdd(w http.ResponseWriter, r *http.Request) {
if req.Email == "" {
req.Email = req.UUID
}
expiresAt, err := parseOptionalXrayExpiry(req.ExpiresAt)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
sess := sessionFromCtx(r.Context())
ownerUsername := ""
@@ -2681,10 +2717,7 @@ func handleXrayClientAdd(w http.ResponseWriter, r *http.Request) {
return
}
}
if err := xrayMgr.AddXrayClient(req.InboundTag, req.UUID, req.Email); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
var savedMeta *XrayClientMeta
if statsStore != nil {
meta := XrayClientMeta{
UUID: req.UUID,
@@ -2697,24 +2730,22 @@ func handleXrayClientAdd(w http.ResponseWriter, r *http.Request) {
QuotaAction: normalizeQuotaAction(req.QuotaAction),
QuotaThrottleMbps: quotaThrottleMbpsOrDefault(req.QuotaThrottleMbps),
}
if req.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
}
}
meta.ExpiresAt = expiresAt
if err := statsStore.UpsertXrayClientMeta(r.Context(), meta); err != nil {
xrayLogf("xray: save meta for %s: %v", req.UUID, err)
} else {
xrayMgr.setNativeQuotaPolicy(&meta)
http.Error(w, "save client metadata failed: "+err.Error(), http.StatusInternalServerError)
return
}
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()
w.WriteHeader(http.StatusCreated)
@@ -2747,6 +2778,15 @@ func handleXrayClientUpdate(w http.ResponseWriter, r *http.Request) {
http.Error(w, "uuid required", http.StatusBadRequest)
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 {
http.Error(w, err.Error(), http.StatusBadRequest)
return
@@ -2784,6 +2824,15 @@ func handleXrayClientUpdate(w http.ResponseWriter, r *http.Request) {
http.Error(w, "forbidden", http.StatusForbidden)
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{
UUID: req.UUID,
@@ -2798,15 +2847,18 @@ func handleXrayClientUpdate(w http.ResponseWriter, r *http.Request) {
TotalUplinkBytes: existing.TotalUplinkBytes,
TotalDownlinkBytes: existing.TotalDownlinkBytes,
}
if req.ExpiresAt != "" {
for _, layout := range []string{time.RFC3339, "2006-01-02T15:04", "2006-01-02"} {
if t, err := time.Parse(layout, req.ExpiresAt); err == nil {
meta.ExpiresAt = &t
break
}
meta.ExpiresAt = expiresAt
emailChanged := req.Email != existing.Email
if emailChanged {
if err := xrayMgr.UpdateXrayClientEmail(req.UUID, req.Email); err != nil {
http.Error(w, "update config email failed: "+err.Error(), http.StatusInternalServerError)
return
}
}
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)
return
}
@@ -2819,16 +2871,28 @@ func handleXrayClientUpdate(w http.ResponseWriter, r *http.Request) {
meta.TotalDownlinkBytes = 0
}
xrayMgr.setNativeQuotaPolicy(&meta)
if req.Email != "" {
if err := xrayMgr.UpdateXrayClientEmail(req.UUID, req.Email); err != nil {
xrayLogf("xray: update config email for %s: %v", req.UUID, err)
} else {
xrayMgr.restartIfExternalRunning()
}
if meta.ExpiresAt != nil && !meta.ExpiresAt.After(time.Now()) {
xrayMgr.disconnectNativeClient(req.UUID)
}
if emailChanged {
xrayMgr.restartIfExternalRunning()
}
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) {
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
+3 -3
View File
@@ -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)
return
}
if xrayMgr.nativeQuotaBlocked(client.uuid) {
xrayLogf("native xray: inbound %q rejected VLESS user %s after data quota", ib.tag, client.email)
if reason := xrayMgr.nativeClientAccessDenied(client.uuid); reason != "" {
xrayLogf("native xray: inbound %q rejected VLESS user %s: %s", ib.tag, client.email, reason)
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])
return
}
releaseConnection, quotaState, ok := xrayMgr.acquireNativeClientConnection(client.uuid, client.email)
releaseConnection, quotaState, ok := xrayMgr.acquireNativeClientConnection(client.uuid, client.email, stream)
if !ok {
return
}
+84 -1
View File
@@ -5,6 +5,7 @@ import (
"io"
"strings"
"sync"
"time"
"golang.org/x/time/rate"
)
@@ -25,6 +26,10 @@ type xrayNativeQuotaState struct {
generation uint64
maxConns int
activeConns int
owner string
expiresAt time.Time
hasExpiry bool
connections map[io.Closer]struct{}
}
func (m *XrayManager) reloadNativeQuotaPolicies() {
@@ -60,9 +65,19 @@ func newXrayNativeQuotaState(meta *XrayClientMeta) *xrayNativeQuotaState {
throttleMbps: quotaThrottleMbpsOrDefault(meta.QuotaThrottleMbps),
generation: 1,
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 {
if v < 0 {
return 0
@@ -92,6 +107,9 @@ func (m *XrayManager) setNativeQuotaPolicy(meta *XrayClientMeta) {
existing.action = normalizeQuotaAction(meta.QuotaAction)
existing.throttleMbps = quotaThrottleMbpsOrDefault(meta.QuotaThrottleMbps)
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.mu.Unlock()
}
@@ -102,8 +120,10 @@ func (m *XrayManager) removeNativeQuotaPolicy(uuid string) {
return
}
m.nativeQuotaMu.Lock()
state := m.nativeQuotaByUUID[uuid]
delete(m.nativeQuotaByUUID, uuid)
m.nativeQuotaMu.Unlock()
closeNativeClientConnections(state)
// 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.
@@ -216,10 +236,19 @@ func (m *XrayManager) nativeQuotaState(uuid string) *xrayNativeQuotaState {
// acquireNativeClientConnection enforces the DB-backed max_conns policy across
// every native inbound and transport. The returned release function is safe to
// 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)
var closer io.Closer
if len(closers) > 0 {
closer = closers[0]
}
if state != nil {
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 {
limit := state.maxConns
state.mu.Unlock()
@@ -227,6 +256,12 @@ func (m *XrayManager) acquireNativeClientConnection(uuid, email string) (func(),
return nil, state, false
}
state.activeConns++
if closer != nil {
if state.connections == nil {
state.connections = make(map[io.Closer]struct{})
}
state.connections[closer] = struct{}{}
}
state.mu.Unlock()
}
@@ -236,6 +271,9 @@ func (m *XrayManager) acquireNativeClientConnection(uuid, email string) (func(),
once.Do(func() {
if state != nil {
state.mu.Lock()
if closer != nil {
delete(state.connections, closer)
}
if state.activeConns > 0 {
state.activeConns--
}
@@ -246,6 +284,51 @@ func (m *XrayManager) acquireNativeClientConnection(uuid, email string) (func(),
}, 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 {
return nativeQuotaStateBlocked(m.nativeQuotaState(uuid))
}
+3 -3
View File
@@ -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)
return
}
if xrayMgr.nativeQuotaBlocked(client.uuid) {
log.Printf("native xray: inbound %q rejected VMess user %s after data quota", ib.tag, client.email)
if reason := xrayMgr.nativeClientAccessDenied(client.uuid); reason != "" {
log.Printf("native xray: inbound %q rejected VMess user %s: %s", ib.tag, client.email, reason)
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)
return
}
releaseConnection, quotaState, ok := xrayMgr.acquireNativeClientConnection(client.uuid, client.email)
releaseConnection, quotaState, ok := xrayMgr.acquireNativeClientConnection(client.uuid, client.email, stream)
if !ok {
return
}