diff --git a/database/db.go b/database/db.go index 114440ae..6685db1d 100644 --- a/database/db.go +++ b/database/db.go @@ -409,9 +409,19 @@ func InitDB(dbPath string) error { if err != nil { return err } - sqlDB.SetMaxOpenConns(8) - sqlDB.SetMaxIdleConns(4) + var maxOpen, maxIdle int + switch config.GetDBKind() { + case "postgres": + maxOpen = envInt("XUI_DB_MAX_OPEN_CONNS", 25) + maxIdle = envInt("XUI_DB_MAX_IDLE_CONNS", 25) + default: + maxOpen = envInt("XUI_DB_MAX_OPEN_CONNS", 8) + maxIdle = envInt("XUI_DB_MAX_IDLE_CONNS", 4) + } + sqlDB.SetMaxOpenConns(maxOpen) + sqlDB.SetMaxIdleConns(maxIdle) sqlDB.SetConnMaxLifetime(time.Hour) + sqlDB.SetConnMaxIdleTime(30 * time.Minute) if err := initModels(); err != nil { return err @@ -428,6 +438,18 @@ func InitDB(dbPath string) error { return runSeeders(isUsersEmpty) } +func envInt(key string, def int) int { + v := strings.TrimSpace(os.Getenv(key)) + if v == "" { + return def + } + n, err := strconv.Atoi(v) + if err != nil || n <= 0 { + return def + } + return n +} + // CloseDB closes the database connection if it exists. func CloseDB() error { if db != nil { diff --git a/database/model/model.go b/database/model/model.go index 411b74c2..f9e333df 100644 --- a/database/model/model.go +++ b/database/model/model.go @@ -220,10 +220,19 @@ func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig { listen = fmt.Sprintf("\"%v\"", listen) protocol := string(i.Protocol) settings := i.Settings - if i.Protocol == Shadowsocks { + switch i.Protocol { + case Shadowsocks: if healed, ok := HealShadowsocksClientMethods(settings); ok { settings = healed } + case VMESS: + if stripped, ok := StripVmessClientSecurity(settings); ok { + settings = stripped + } + case VLESS: + if stripped, ok := StripVlessInboundEncryption(settings); ok { + settings = stripped + } } return &xray.InboundConfig{ Listen: json_util.RawMessage(listen), @@ -236,6 +245,59 @@ func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig { } } +func StripVmessClientSecurity(settings string) (string, bool) { + if settings == "" { + return settings, false + } + var parsed map[string]any + if err := json.Unmarshal([]byte(settings), &parsed); err != nil { + return settings, false + } + clients, ok := parsed["clients"].([]any) + if !ok { + return settings, false + } + changed := false + for i := range clients { + cm, ok := clients[i].(map[string]any) + if !ok { + continue + } + if _, has := cm["security"]; has { + delete(cm, "security") + clients[i] = cm + changed = true + } + } + if !changed { + return settings, false + } + out, err := json.MarshalIndent(parsed, "", " ") + if err != nil { + return settings, false + } + return string(out), true +} + +func StripVlessInboundEncryption(settings string) (string, bool) { + if settings == "" { + return settings, false + } + var parsed map[string]any + if err := json.Unmarshal([]byte(settings), &parsed); err != nil { + return settings, false + } + if _, has := parsed["encryption"]; !has { + return settings, false + } + delete(parsed, "encryption") + out, err := json.MarshalIndent(parsed, "", " ") + if err != nil { + return settings, false + } + return string(out), true +} + // HealShadowsocksClientMethods normalises the per-client `method` field // on a shadowsocks inbound's settings JSON before it leaves for xray-core: // - Legacy ciphers (aes-*, chacha20-*): every client must carry a diff --git a/frontend/src/pages/clients/ClientFormModal.tsx b/frontend/src/pages/clients/ClientFormModal.tsx index c2f83de3..190048f5 100644 --- a/frontend/src/pages/clients/ClientFormModal.tsx +++ b/frontend/src/pages/clients/ClientFormModal.tsx @@ -26,6 +26,7 @@ import type { ClientRecord, InboundOption } from '@/hooks/useClients'; import { ClientFormSchema, ClientCreateFormSchema } from '@/schemas/client'; const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL); +const VMESS_SECURITY_OPTIONS = ['auto', 'aes-128-gcm', 'chacha20-poly1305', 'none', 'zero'] as const; const MULTI_CLIENT_PROTOCOLS = new Set([ 'shadowsocks', 'vless', 'vmess', 'trojan', 'hysteria', @@ -77,6 +78,7 @@ interface FormState { password: string; auth: string; flow: string; + security: string; reverseTag: string; totalGB: number; expiryDate: Dayjs | null; @@ -99,6 +101,7 @@ function emptyForm(): FormState { password: '', auth: '', flow: '', + security: 'auto', reverseTag: '', totalGB: 0, expiryDate: null, @@ -163,6 +166,7 @@ export default function ClientFormModal({ password: client.password || '', auth: client.auth || '', flow: client.flow || '', + security: client.security || 'auto', reverseTag: client.reverse?.tag || '', totalGB: bytesToGB(client.totalGB || 0), reset: Number(client.reset) || 0, @@ -214,6 +218,14 @@ export default function ClientFormModal({ return ids; }, [inbounds]); + const vmessIds = useMemo(() => { + const ids = new Set(); + for (const row of inbounds || []) { + if (row && row.protocol === 'vmess') ids.add(row.id); + } + return ids; + }, [inbounds]); + const showFlow = useMemo( () => (form.inboundIds || []).some((id) => flowCapableIds.has(id)), [form.inboundIds, flowCapableIds], @@ -224,6 +236,11 @@ export default function ClientFormModal({ [form.inboundIds, vlessLikeIds], ); + const showSecurity = useMemo( + () => (form.inboundIds || []).some((id) => vmessIds.has(id)), + [form.inboundIds, vmessIds], + ); + useEffect(() => { if (!showFlow && form.flow) { @@ -286,6 +303,7 @@ export default function ClientFormModal({ password: form.password, auth: form.auth, flow: form.flow, + security: form.security, reverseTag: form.reverseTag, totalGB: form.totalGB, delayedStart: form.delayedStart, @@ -313,6 +331,7 @@ export default function ClientFormModal({ password: form.password, auth: form.auth, flow: showFlow ? (form.flow || '') : '', + security: showSecurity ? (form.security || 'auto') : 'auto', totalGB: gbToBytes(form.totalGB), expiryTime, reset: Number(form.reset) || 0, @@ -497,6 +516,17 @@ export default function ClientFormModal({ )} + {showSecurity && ( + + +