mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-28 00:24:19 +00:00
feat(iplimit): gate IP limit on fail2ban and reset stale limits
Per-client IP limit only enforces where fail2ban is installed, so the panel now reports enforceability and disables the field otherwise: - Add GET /panel/api/server/fail2banStatus (enabled/installed/usable/windows), cached 30s. - ClientFormModal and ClientBulkAddModal disable the IP Limit input when not usable and show a hover tooltip; Windows gets a platform-specific message instead of the bash-menu hint. - One-time migration ResetIpLimitNoFail2ban zeroes existing client limitIp (inbound settings JSON + clients table) on hosts without fail2ban, where the limit never applied. - Drop the recurring '[LimitIP] Fail2Ban is not installed' warning. - Add limitIpFail2banMissing/limitIpFail2banWindows/limitIpDisabled across all 13 locales.
This commit is contained in:
@@ -3316,6 +3316,45 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/panel/api/server/fail2banStatus": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Server"
|
||||
],
|
||||
"summary": "Reports whether per-client IP limits can be enforced on this host. The panel uses it to gate the \"IP Limit\" field, since enforcement depends on Fail2ban being installed.",
|
||||
"operationId": "get_panel_api_server_fail2banStatus",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"msg": {
|
||||
"type": "string"
|
||||
},
|
||||
"obj": {}
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"success": true,
|
||||
"obj": {
|
||||
"enabled": true,
|
||||
"installed": true,
|
||||
"usable": true,
|
||||
"windows": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/panel/api/server/cpuHistory/{bucket}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { HttpUtil } from '@/utils';
|
||||
import { keys } from '@/api/queryKeys';
|
||||
|
||||
export interface Fail2banStatus {
|
||||
enabled: boolean;
|
||||
installed: boolean;
|
||||
usable: boolean;
|
||||
windows: boolean;
|
||||
}
|
||||
|
||||
const FAIL_OPEN_STATUS: Fail2banStatus = {
|
||||
enabled: true,
|
||||
installed: true,
|
||||
usable: true,
|
||||
windows: false,
|
||||
};
|
||||
|
||||
async function fetchFail2banStatus(): Promise<Fail2banStatus> {
|
||||
const msg = await HttpUtil.get<Fail2banStatus>('/panel/api/server/fail2banStatus', undefined, { silent: true });
|
||||
if (!msg?.success || !msg.obj) throw new Error(msg?.msg || 'Failed to fetch fail2ban status');
|
||||
return { ...FAIL_OPEN_STATUS, ...msg.obj };
|
||||
}
|
||||
|
||||
export function getLimitIpNotice(status: Fail2banStatus, t: (key: string) => string): string {
|
||||
if (status.usable) return '';
|
||||
if (!status.enabled) return t('pages.clients.limitIpDisabled');
|
||||
if (status.windows) return t('pages.clients.limitIpFail2banWindows');
|
||||
return t('pages.clients.limitIpFail2banMissing');
|
||||
}
|
||||
|
||||
export function useFail2banStatusQuery() {
|
||||
const query = useQuery({
|
||||
queryKey: keys.server.fail2banStatus(),
|
||||
queryFn: fetchFail2banStatus,
|
||||
staleTime: 60_000,
|
||||
});
|
||||
|
||||
return query.data ?? FAIL_OPEN_STATUS;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
export const keys = {
|
||||
server: {
|
||||
status: () => ['server', 'status'] as const,
|
||||
fail2banStatus: () => ['server', 'fail2banStatus'] as const,
|
||||
},
|
||||
nodes: {
|
||||
root: () => ['nodes'] as const,
|
||||
|
||||
@@ -252,6 +252,12 @@ export const sections: readonly Section[] = [
|
||||
summary: 'Real-time machine snapshot: CPU, memory, swap, disk, network IO, load averages, open connections, Xray state. Cached and refreshed every 2 seconds in the background.',
|
||||
response: '{\n "success": true,\n "obj": {\n "cpu": 12.5,\n "mem": { "current": 2147483648, "total": 8589934592 },\n "swap": { "current": 0, "total": 4294967296 },\n "disk": { "current": 53687091200, "total": 268435456000 },\n "netIO": { "up": 1073741824, "down": 2147483648 },\n "xray": { "state": "running", "version": "v25.10.31" },\n "tcpCount": 42,\n "load": { "load1": 0.5, "load5": 0.3, "load15": 0.2 }\n }\n}',
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/panel/api/server/fail2banStatus',
|
||||
summary: 'Reports whether per-client IP limits can be enforced on this host. The panel uses it to gate the "IP Limit" field, since enforcement depends on Fail2ban being installed.',
|
||||
response: '{\n "success": true,\n "obj": {\n "enabled": true,\n "installed": true,\n "usable": true,\n "windows": false\n }\n}',
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/panel/api/server/cpuHistory/:bucket',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AutoComplete, Button, Form, Input, InputNumber, Modal, Select, Space, Switch, message } from 'antd';
|
||||
import { AutoComplete, Button, Form, Input, InputNumber, Modal, Select, Space, Switch, Tooltip, message } from 'antd';
|
||||
import { ReloadOutlined } from '@ant-design/icons';
|
||||
import dayjs from 'dayjs';
|
||||
import type { Dayjs } from 'dayjs';
|
||||
@@ -10,6 +10,7 @@ import { formatInboundLabel } from '@/lib/inbounds/label';
|
||||
import { TLS_FLOW_CONTROL } from '@/schemas/primitives';
|
||||
import { DateTimePicker, SelectAllClearButtons } from '@/components/form';
|
||||
import { useClients, type InboundOption } from '@/hooks/useClients';
|
||||
import { useFail2banStatusQuery, getLimitIpNotice } from '@/api/queries/useFail2banStatusQuery';
|
||||
import { ClientBulkAddFormSchema, type ClientBulkAddFormValues } from '@/schemas/client';
|
||||
|
||||
const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL);
|
||||
@@ -62,6 +63,9 @@ export default function ClientBulkAddModal({
|
||||
const [form, setForm] = useState<FormState>(emptyForm);
|
||||
const [delayedStart, setDelayedStart] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const fail2ban = useFail2banStatusQuery();
|
||||
const limitIpDisabled = !fail2ban.usable;
|
||||
const limitIpNotice = getLimitIpNotice(fail2ban, t);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
@@ -311,7 +315,13 @@ export default function ClientBulkAddModal({
|
||||
)}
|
||||
|
||||
<Form.Item label={t('pages.clients.limitIp')}>
|
||||
<InputNumber value={form.limitIp} min={0} onChange={(v) => update('limitIp', Number(v) || 0)} />
|
||||
<Tooltip title={limitIpNotice || undefined}>
|
||||
<span style={{ display: 'inline-flex' }}>
|
||||
<InputNumber value={form.limitIp} min={0} disabled={limitIpDisabled}
|
||||
style={limitIpDisabled ? { pointerEvents: 'none' } : undefined}
|
||||
onChange={(v) => update('limitIp', Number(v) || 0)} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t('pages.clients.totalGB')}>
|
||||
|
||||
@@ -28,6 +28,7 @@ import { normalizeClientIps, type ClientIpInfo } from '@/lib/clients/ip-log';
|
||||
import { DateTimePicker, SelectAllClearButtons } from '@/components/form';
|
||||
import { TLS_FLOW_CONTROL } from '@/schemas/primitives';
|
||||
import type { ClientRecord, InboundOption, ExternalLink, ExternalLinkInput } from '@/hooks/useClients';
|
||||
import { useFail2banStatusQuery, getLimitIpNotice } from '@/api/queries/useFail2banStatusQuery';
|
||||
import { ClientFormSchema, ClientCreateFormSchema } from '@/schemas/client';
|
||||
|
||||
const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL);
|
||||
@@ -182,6 +183,9 @@ export default function ClientFormModal({
|
||||
const [ipsLoading, setIpsLoading] = useState(false);
|
||||
const [ipsClearing, setIpsClearing] = useState(false);
|
||||
const [ipsModalOpen, setIpsModalOpen] = useState(false);
|
||||
const fail2ban = useFail2banStatusQuery();
|
||||
const limitIpDisabled = !fail2ban.usable;
|
||||
const limitIpNotice = getLimitIpNotice(fail2ban, t);
|
||||
|
||||
function update<K extends keyof FormState>(key: K, value: FormState[K]) {
|
||||
setForm((prev) => ({ ...prev, [key]: value }));
|
||||
@@ -550,17 +554,22 @@ export default function ClientFormModal({
|
||||
</Col>
|
||||
<Col xs={24} md={6}>
|
||||
<Form.Item label={t('pages.clients.limitIp')} tooltip={t('pages.clients.limitIpDesc')}>
|
||||
<Space.Compact style={{ display: 'flex' }}>
|
||||
<InputNumber value={form.limitIp} min={0} style={{ flex: 1 }}
|
||||
onChange={(v) => update('limitIp', Number(v) || 0)} />
|
||||
{isEdit && (
|
||||
<Tooltip title={t('pages.clients.ipLog')}>
|
||||
<Button icon={<EyeOutlined />} loading={ipsLoading} onClick={openIpsModal}>
|
||||
{clientIps.length > 0 ? clientIps.length : ''}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Space.Compact>
|
||||
<Tooltip title={limitIpNotice || undefined}>
|
||||
<span style={{ display: 'flex', width: '100%' }}>
|
||||
<Space.Compact style={{ display: 'flex', flex: 1 }}>
|
||||
<InputNumber value={form.limitIp} min={0} disabled={limitIpDisabled}
|
||||
style={{ flex: 1, ...(limitIpDisabled ? { pointerEvents: 'none' } : null) }}
|
||||
onChange={(v) => update('limitIp', Number(v) || 0)} />
|
||||
{isEdit && (
|
||||
<Tooltip title={t('pages.clients.ipLog')}>
|
||||
<Button icon={<EyeOutlined />} loading={ipsLoading} onClick={openIpsModal}>
|
||||
{clientIps.length > 0 ? clientIps.length : ''}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Space.Compact>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
@@ -11,7 +11,9 @@ import (
|
||||
"log"
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -464,9 +466,104 @@ func runSeeders(isUsersEmpty bool) error {
|
||||
if err := seedHostsFromExternalProxy(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Self-gated on the "ResetIpLimitNoFail2ban" row.
|
||||
if err := resetIpLimitsWithoutFail2ban(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// resetIpLimitsWithoutFail2ban zeroes every client's IP limit on hosts where
|
||||
// fail2ban can't enforce it (not installed, or the integration disabled). The
|
||||
// limit silently does nothing there yet kept logging a repeated warning, so a
|
||||
// stale value is just misleading — the panel also disables the field on these
|
||||
// hosts. One-time, self-gated on the seeder row.
|
||||
func resetIpLimitsWithoutFail2ban() error {
|
||||
var history []string
|
||||
if err := db.Model(&model.HistoryOfSeeders{}).Pluck("seeder_name", &history).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if slices.Contains(history, "ResetIpLimitNoFail2ban") {
|
||||
return nil
|
||||
}
|
||||
|
||||
if fail2banCanEnforce() {
|
||||
return db.Create(&model.HistoryOfSeeders{SeederName: "ResetIpLimitNoFail2ban"}).Error
|
||||
}
|
||||
|
||||
var inbounds []model.Inbound
|
||||
if err := db.Find(&inbounds).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
for _, inbound := range inbounds {
|
||||
if strings.TrimSpace(inbound.Settings) == "" {
|
||||
continue
|
||||
}
|
||||
var settings map[string]any
|
||||
if err := json.Unmarshal([]byte(inbound.Settings), &settings); err != nil {
|
||||
log.Printf("ResetIpLimitNoFail2ban: skip inbound %d (invalid settings json): %v", inbound.Id, err)
|
||||
continue
|
||||
}
|
||||
clients, ok := settings["clients"].([]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
mutated := false
|
||||
for i, raw := range clients {
|
||||
obj, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
v, present := obj["limitIp"]
|
||||
if !present {
|
||||
continue
|
||||
}
|
||||
if n, isNum := v.(float64); isNum && n == 0 {
|
||||
continue
|
||||
}
|
||||
obj["limitIp"] = 0
|
||||
clients[i] = obj
|
||||
mutated = true
|
||||
}
|
||||
if !mutated {
|
||||
continue
|
||||
}
|
||||
settings["clients"] = clients
|
||||
newSettings, err := json.MarshalIndent(settings, "", " ")
|
||||
if err != nil {
|
||||
log.Printf("ResetIpLimitNoFail2ban: skip inbound %d (marshal failed): %v", inbound.Id, err)
|
||||
continue
|
||||
}
|
||||
if err := tx.Model(&model.Inbound{}).Where("id = ?", inbound.Id).
|
||||
Update("settings", string(newSettings)).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := tx.Model(&model.ClientRecord{}).Where("limit_ip <> ?", 0).
|
||||
Update("limit_ip", 0).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Create(&model.HistoryOfSeeders{SeederName: "ResetIpLimitNoFail2ban"}).Error
|
||||
})
|
||||
}
|
||||
|
||||
// fail2banCanEnforce reports whether per-client IP limits can actually be
|
||||
// enforced on this host: the integration must be enabled (XUI_ENABLE_FAIL2BAN)
|
||||
// and fail2ban-client must be present. Mirrors the service-layer check, kept
|
||||
// local to avoid an import cycle.
|
||||
func fail2banCanEnforce() bool {
|
||||
if v, ok := os.LookupEnv("XUI_ENABLE_FAIL2BAN"); ok && v != "true" {
|
||||
return false
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
return false
|
||||
}
|
||||
return exec.Command("fail2ban-client", "-h").Run() == nil
|
||||
}
|
||||
|
||||
// clearLegacyProxySettings drops the deprecated panelProxy/tgBotProxy rows so a
|
||||
// stale tgBotProxy no longer masks the panelOutbound egress fallback.
|
||||
func clearLegacyProxySettings() error {
|
||||
|
||||
@@ -63,6 +63,7 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
|
||||
g.GET("/getNewmlkem768", a.getNewmlkem768)
|
||||
g.GET("/getNewVlessEnc", a.getNewVlessEnc)
|
||||
g.GET("/clientIps", a.getClientIps)
|
||||
g.GET("/fail2banStatus", a.getFail2banStatus)
|
||||
|
||||
g.POST("/stopXrayService", a.stopXrayService)
|
||||
g.POST("/restartXrayService", a.restartXrayService)
|
||||
@@ -103,6 +104,10 @@ func (a *ServerController) startTask() {
|
||||
// status returns the current server status information.
|
||||
func (a *ServerController) status(c *gin.Context) { jsonObj(c, a.serverService.LastStatus(), nil) }
|
||||
|
||||
func (a *ServerController) getFail2banStatus(c *gin.Context) {
|
||||
jsonObj(c, a.serverService.GetFail2banStatus(), nil)
|
||||
}
|
||||
|
||||
func parseHistoryBucket(c *gin.Context) (int, bool) {
|
||||
bucket, err := strconv.Atoi(c.Param("bucket"))
|
||||
if err != nil || bucket <= 0 || !service.IsAllowedHistoryBucket(bucket) {
|
||||
|
||||
@@ -88,11 +88,12 @@ func (j *CheckClientIpJob) Run() {
|
||||
}
|
||||
}
|
||||
|
||||
// resolveEnforce decides whether limits can actually be enforced this run,
|
||||
// warning when fail2ban is missing on a platform that needs it.
|
||||
// resolveEnforce decides whether limits can actually be enforced this run.
|
||||
// Without fail2ban on a platform that needs it the limit can't be applied, so
|
||||
// enforcement is skipped (the panel resets these limits to 0 on upgrade and
|
||||
// disables the field, so this is normally a no-op).
|
||||
func (j *CheckClientIpJob) resolveEnforce(hasLimit, f2bInstalled bool) bool {
|
||||
if hasLimit && runtime.GOOS != "windows" && !f2bInstalled {
|
||||
logger.Warning("[LimitIP] Fail2Ban is not installed, Please install Fail2Ban from the x-ui bash menu.")
|
||||
return false
|
||||
}
|
||||
return hasLimit
|
||||
|
||||
@@ -142,6 +142,10 @@ type ServerService struct {
|
||||
|
||||
versionsCacheMu sync.Mutex
|
||||
versionsCache *cachedXrayVersions
|
||||
|
||||
fail2banMu sync.Mutex
|
||||
fail2banInstalled bool
|
||||
fail2banCheckedAt time.Time
|
||||
}
|
||||
|
||||
type cachedXrayVersions struct {
|
||||
@@ -185,6 +189,53 @@ func (s *ServerService) LastStatus() *Status {
|
||||
return s.lastStatus
|
||||
}
|
||||
|
||||
// Fail2banStatus tells the frontend whether the per-client IP limit can
|
||||
// actually be enforced. Enforcement depends on fail2ban, so a limit set
|
||||
// without it would silently do nothing.
|
||||
type Fail2banStatus struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Installed bool `json:"installed"`
|
||||
Usable bool `json:"usable"`
|
||||
Windows bool `json:"windows"`
|
||||
}
|
||||
|
||||
const fail2banInstalledCacheTTL = 30 * time.Second
|
||||
|
||||
func (s *ServerService) GetFail2banStatus() Fail2banStatus {
|
||||
enabled := isFail2banEnabled()
|
||||
|
||||
installed := false
|
||||
if enabled {
|
||||
installed = s.isFail2banInstalled()
|
||||
}
|
||||
|
||||
return Fail2banStatus{
|
||||
Enabled: enabled,
|
||||
Installed: installed,
|
||||
Usable: enabled && installed,
|
||||
Windows: runtime.GOOS == "windows",
|
||||
}
|
||||
}
|
||||
|
||||
func isFail2banEnabled() bool {
|
||||
value, ok := os.LookupEnv("XUI_ENABLE_FAIL2BAN")
|
||||
return !ok || value == "true"
|
||||
}
|
||||
|
||||
func (s *ServerService) isFail2banInstalled() bool {
|
||||
s.fail2banMu.Lock()
|
||||
defer s.fail2banMu.Unlock()
|
||||
|
||||
if !s.fail2banCheckedAt.IsZero() && time.Since(s.fail2banCheckedAt) < fail2banInstalledCacheTTL {
|
||||
return s.fail2banInstalled
|
||||
}
|
||||
|
||||
err := exec.Command("fail2ban-client", "-h").Run()
|
||||
s.fail2banInstalled = err == nil
|
||||
s.fail2banCheckedAt = time.Now()
|
||||
return s.fail2banInstalled
|
||||
}
|
||||
|
||||
// RefreshStatus collects a new system snapshot, stores it as LastStatus, and
|
||||
// appends it to the system-metrics time series. Returns the new snapshot (may
|
||||
// be nil if collection failed). Called by the background ticker; the caller is
|
||||
|
||||
@@ -747,6 +747,9 @@
|
||||
"addClients": "إضافة عملاء",
|
||||
"limitIp": "حد عناوين IP",
|
||||
"limitIpDesc": "الحد الأقصى لعناوين IP المتزامنة. 0 = غير محدود.",
|
||||
"limitIpFail2banMissing": "Fail2ban غير مثبّت، لذا لا يمكن تطبيق حد عناوين IP. ثبّت Fail2ban من قائمة x-ui النصية لتفعيل هذا الخيار.",
|
||||
"limitIpFail2banWindows": "Fail2ban غير متوفّر على نظام Windows، لذا لا يمكن تطبيق حد عناوين IP.",
|
||||
"limitIpDisabled": "ميزة حد عناوين IP معطّلة على هذا الخادم.",
|
||||
"password": "كلمة المرور",
|
||||
"subId": "معرّف الاشتراك",
|
||||
"online": "متصل",
|
||||
|
||||
@@ -747,6 +747,9 @@
|
||||
"addClients": "Add Clients",
|
||||
"limitIp": "IP Limit",
|
||||
"limitIpDesc": "Maximum simultaneous IPs. 0 = unlimited.",
|
||||
"limitIpFail2banMissing": "Fail2ban is not installed, so the IP limit cannot be enforced. Install Fail2ban from the x-ui bash menu to enable this option.",
|
||||
"limitIpFail2banWindows": "Fail2ban is not available on Windows, so the IP limit cannot be enforced.",
|
||||
"limitIpDisabled": "The IP limit feature is disabled on this server.",
|
||||
"password": "Password",
|
||||
"subId": "Subscription ID",
|
||||
"online": "Online",
|
||||
|
||||
@@ -747,6 +747,9 @@
|
||||
"addClients": "Añadir clientes",
|
||||
"limitIp": "Límite de IP",
|
||||
"limitIpDesc": "Máximo de IP simultáneas. 0 = ilimitado.",
|
||||
"limitIpFail2banMissing": "Fail2ban no está instalado, por lo que no se puede aplicar el límite de IP. Instala Fail2ban desde el menú bash de x-ui para habilitar esta opción.",
|
||||
"limitIpFail2banWindows": "Fail2ban no está disponible en Windows, por lo que no se puede aplicar el límite de IP.",
|
||||
"limitIpDisabled": "La función de límite de IP está deshabilitada en este servidor.",
|
||||
"password": "Contraseña",
|
||||
"subId": "ID de suscripción",
|
||||
"online": "En línea",
|
||||
|
||||
@@ -747,6 +747,9 @@
|
||||
"addClients": "افزودن کلاینتها",
|
||||
"limitIp": "محدودیت IP",
|
||||
"limitIpDesc": "حداکثر تعداد IP همزمان. ۰ = نامحدود",
|
||||
"limitIpFail2banMissing": "Fail2ban نصب نشده است، بنابراین محدودیت IP اعمال نمیشود. برای فعالسازی این گزینه، Fail2ban را از منوی بش x-ui نصب کنید.",
|
||||
"limitIpFail2banWindows": "Fail2ban روی ویندوز در دسترس نیست، بنابراین محدودیت IP قابل اعمال نیست.",
|
||||
"limitIpDisabled": "قابلیت محدودیت IP روی این سرور غیرفعال است.",
|
||||
"password": "رمز عبور",
|
||||
"subId": "شناسه اشتراک",
|
||||
"online": "آنلاین",
|
||||
|
||||
@@ -747,6 +747,9 @@
|
||||
"addClients": "Tambah klien",
|
||||
"limitIp": "Batas IP",
|
||||
"limitIpDesc": "Jumlah maksimum IP bersamaan. 0 = tidak terbatas.",
|
||||
"limitIpFail2banMissing": "Fail2ban tidak terpasang, sehingga batas IP tidak dapat diterapkan. Pasang Fail2ban dari menu bash x-ui untuk mengaktifkan opsi ini.",
|
||||
"limitIpFail2banWindows": "Fail2ban tidak tersedia di Windows, sehingga batas IP tidak dapat diterapkan.",
|
||||
"limitIpDisabled": "Fitur batas IP dinonaktifkan di server ini.",
|
||||
"password": "Kata sandi",
|
||||
"subId": "ID Langganan",
|
||||
"online": "Online",
|
||||
|
||||
@@ -747,6 +747,9 @@
|
||||
"addClients": "クライアントを追加",
|
||||
"limitIp": "IP 制限",
|
||||
"limitIpDesc": "同時接続 IP の最大数。0 = 無制限。",
|
||||
"limitIpFail2banMissing": "Fail2ban がインストールされていないため、IP 制限を適用できません。このオプションを有効にするには、x-ui の bash メニューから Fail2ban をインストールしてください。",
|
||||
"limitIpFail2banWindows": "Windows では Fail2ban を利用できないため、IP 制限を適用できません。",
|
||||
"limitIpDisabled": "このサーバーでは IP 制限機能が無効になっています。",
|
||||
"password": "パスワード",
|
||||
"subId": "サブスクリプション ID",
|
||||
"online": "オンライン",
|
||||
|
||||
@@ -747,6 +747,9 @@
|
||||
"addClients": "Adicionar clientes",
|
||||
"limitIp": "Limite de IP",
|
||||
"limitIpDesc": "Máximo de IPs simultâneos. 0 = ilimitado.",
|
||||
"limitIpFail2banMissing": "O Fail2ban não está instalado, portanto o limite de IP não pode ser aplicado. Instale o Fail2ban pelo menu bash do x-ui para ativar esta opção.",
|
||||
"limitIpFail2banWindows": "O Fail2ban não está disponível no Windows, portanto o limite de IP não pode ser aplicado.",
|
||||
"limitIpDisabled": "O recurso de limite de IP está desativado neste servidor.",
|
||||
"password": "Senha",
|
||||
"subId": "ID da assinatura",
|
||||
"online": "Online",
|
||||
|
||||
@@ -747,6 +747,9 @@
|
||||
"addClients": "Добавить клиентов",
|
||||
"limitIp": "Лимит IP",
|
||||
"limitIpDesc": "Максимум одновременных IP-адресов. 0 = без ограничений.",
|
||||
"limitIpFail2banMissing": "Fail2ban не установлен, поэтому ограничение по IP не может быть применено. Установите Fail2ban из bash-меню x-ui, чтобы включить эту опцию.",
|
||||
"limitIpFail2banWindows": "Fail2ban недоступен в Windows, поэтому ограничение по IP не может быть применено.",
|
||||
"limitIpDisabled": "Функция ограничения по IP отключена на этом сервере.",
|
||||
"password": "Пароль",
|
||||
"subId": "ID подписки",
|
||||
"online": "В сети",
|
||||
|
||||
@@ -747,6 +747,9 @@
|
||||
"addClients": "Kullanıcı Ekle",
|
||||
"limitIp": "IP Limiti",
|
||||
"limitIpDesc": "Eş zamanlı en fazla IP sayısı. 0 = sınırsız.",
|
||||
"limitIpFail2banMissing": "Fail2ban yüklü değil, bu nedenle IP sınırı uygulanamaz. Bu seçeneği etkinleştirmek için x-ui bash menüsünden Fail2ban'ı yükleyin.",
|
||||
"limitIpFail2banWindows": "Fail2ban Windows'ta kullanılamadığından IP sınırı uygulanamaz.",
|
||||
"limitIpDisabled": "IP sınırı özelliği bu sunucuda devre dışı.",
|
||||
"password": "Şifre",
|
||||
"subId": "Abonelik ID'si",
|
||||
"online": "Çevrimiçi",
|
||||
|
||||
@@ -747,6 +747,9 @@
|
||||
"addClients": "Додати клієнтів",
|
||||
"limitIp": "Ліміт IP",
|
||||
"limitIpDesc": "Максимум одночасних IP-адрес. 0 = без обмежень.",
|
||||
"limitIpFail2banMissing": "Fail2ban не встановлено, тому обмеження за IP не може бути застосоване. Встановіть Fail2ban із bash-меню x-ui, щоб увімкнути цю опцію.",
|
||||
"limitIpFail2banWindows": "Fail2ban недоступний у Windows, тому обмеження за IP не може бути застосоване.",
|
||||
"limitIpDisabled": "Функцію обмеження за IP вимкнено на цьому сервері.",
|
||||
"password": "Пароль",
|
||||
"subId": "ID підписки",
|
||||
"online": "У мережі",
|
||||
|
||||
@@ -747,6 +747,9 @@
|
||||
"addClients": "Thêm khách hàng",
|
||||
"limitIp": "Giới hạn IP",
|
||||
"limitIpDesc": "Số IP đồng thời tối đa. 0 = không giới hạn.",
|
||||
"limitIpFail2banMissing": "Fail2ban chưa được cài đặt nên không thể áp dụng giới hạn IP. Hãy cài đặt Fail2ban từ menu bash x-ui để bật tùy chọn này.",
|
||||
"limitIpFail2banWindows": "Fail2ban không khả dụng trên Windows nên không thể áp dụng giới hạn IP.",
|
||||
"limitIpDisabled": "Tính năng giới hạn IP đã bị tắt trên máy chủ này.",
|
||||
"password": "Mật khẩu",
|
||||
"subId": "ID đăng ký",
|
||||
"online": "Trực tuyến",
|
||||
|
||||
@@ -747,6 +747,9 @@
|
||||
"addClients": "添加客户端",
|
||||
"limitIp": "IP 限制",
|
||||
"limitIpDesc": "最大同时连接 IP 数。0 = 不限制。",
|
||||
"limitIpFail2banMissing": "未安装 Fail2ban,无法实施 IP 限制。请从 x-ui 命令行菜单安装 Fail2ban 以启用此选项。",
|
||||
"limitIpFail2banWindows": "Windows 上不支持 Fail2ban,无法实施 IP 限制。",
|
||||
"limitIpDisabled": "此服务器已禁用 IP 限制功能。",
|
||||
"password": "密码",
|
||||
"subId": "订阅 ID",
|
||||
"online": "在线",
|
||||
|
||||
@@ -747,6 +747,9 @@
|
||||
"addClients": "新增客戶端",
|
||||
"limitIp": "IP 限制",
|
||||
"limitIpDesc": "最大同時連線 IP 數。0 = 不限制。",
|
||||
"limitIpFail2banMissing": "未安裝 Fail2ban,無法實施 IP 限制。請從 x-ui 命令列選單安裝 Fail2ban 以啟用此選項。",
|
||||
"limitIpFail2banWindows": "Windows 上不支援 Fail2ban,無法實施 IP 限制。",
|
||||
"limitIpDisabled": "此伺服器已停用 IP 限制功能。",
|
||||
"password": "密碼",
|
||||
"subId": "訂閱 ID",
|
||||
"online": "上線",
|
||||
|
||||
Reference in New Issue
Block a user