diff --git a/frontend/public/openapi.json b/frontend/public/openapi.json index 7d9505cd9..fcdbe3e3f 100644 --- a/frontend/public/openapi.json +++ b/frontend/public/openapi.json @@ -325,6 +325,11 @@ "description": "Two-factor authentication token", "type": "string" }, + "warpUpdateInterval": { + "description": "WARP", + "minimum": 0, + "type": "integer" + }, "webBasePath": { "description": "Base path for web panel URLs", "type": "string" @@ -427,6 +432,7 @@ "trustedProxyCIDRs", "twoFactorEnable", "twoFactorToken", + "warpUpdateInterval", "webBasePath", "webCertFile", "webDomain", @@ -753,6 +759,11 @@ "description": "Two-factor authentication token", "type": "string" }, + "warpUpdateInterval": { + "description": "WARP", + "minimum": 0, + "type": "integer" + }, "webBasePath": { "description": "Base path for web panel URLs", "type": "string" @@ -861,6 +872,7 @@ "trustedProxyCIDRs", "twoFactorEnable", "twoFactorToken", + "warpUpdateInterval", "webBasePath", "webCertFile", "webDomain", diff --git a/frontend/src/generated/examples.ts b/frontend/src/generated/examples.ts index 9576da449..52f360836 100644 --- a/frontend/src/generated/examples.ts +++ b/frontend/src/generated/examples.ts @@ -75,6 +75,7 @@ export const EXAMPLES: Record = { "trustedProxyCIDRs": "", "twoFactorEnable": false, "twoFactorToken": "", + "warpUpdateInterval": 0, "webBasePath": "", "webCertFile": "", "webDomain": "", @@ -163,6 +164,7 @@ export const EXAMPLES: Record = { "trustedProxyCIDRs": "", "twoFactorEnable": false, "twoFactorToken": "", + "warpUpdateInterval": 0, "webBasePath": "", "webCertFile": "", "webDomain": "", diff --git a/frontend/src/generated/schemas.ts b/frontend/src/generated/schemas.ts index 95ea0f021..96fa5b29a 100644 --- a/frontend/src/generated/schemas.ts +++ b/frontend/src/generated/schemas.ts @@ -299,6 +299,11 @@ export const SCHEMAS: Record = { "description": "Two-factor authentication token", "type": "string" }, + "warpUpdateInterval": { + "description": "WARP", + "minimum": 0, + "type": "integer" + }, "webBasePath": { "description": "Base path for web panel URLs", "type": "string" @@ -401,6 +406,7 @@ export const SCHEMAS: Record = { "trustedProxyCIDRs", "twoFactorEnable", "twoFactorToken", + "warpUpdateInterval", "webBasePath", "webCertFile", "webDomain", @@ -727,6 +733,11 @@ export const SCHEMAS: Record = { "description": "Two-factor authentication token", "type": "string" }, + "warpUpdateInterval": { + "description": "WARP", + "minimum": 0, + "type": "integer" + }, "webBasePath": { "description": "Base path for web panel URLs", "type": "string" @@ -835,6 +846,7 @@ export const SCHEMAS: Record = { "trustedProxyCIDRs", "twoFactorEnable", "twoFactorToken", + "warpUpdateInterval", "webBasePath", "webCertFile", "webDomain", diff --git a/frontend/src/generated/types.ts b/frontend/src/generated/types.ts index c5e7c0550..3f832e24e 100644 --- a/frontend/src/generated/types.ts +++ b/frontend/src/generated/types.ts @@ -80,6 +80,7 @@ export interface AllSetting { trustedProxyCIDRs: string; twoFactorEnable: boolean; twoFactorToken: string; + warpUpdateInterval: number; webBasePath: string; webCertFile: string; webDomain: string; @@ -169,6 +170,7 @@ export interface AllSettingView { trustedProxyCIDRs: string; twoFactorEnable: boolean; twoFactorToken: string; + warpUpdateInterval: number; webBasePath: string; webCertFile: string; webDomain: string; diff --git a/frontend/src/generated/zod.ts b/frontend/src/generated/zod.ts index 87a672116..96a352676 100644 --- a/frontend/src/generated/zod.ts +++ b/frontend/src/generated/zod.ts @@ -90,6 +90,7 @@ export const AllSettingSchema = z.object({ trustedProxyCIDRs: z.string(), twoFactorEnable: z.boolean(), twoFactorToken: z.string(), + warpUpdateInterval: z.number().int().min(0), webBasePath: z.string(), webCertFile: z.string(), webDomain: z.string(), @@ -180,6 +181,7 @@ export const AllSettingViewSchema = z.object({ trustedProxyCIDRs: z.string(), twoFactorEnable: z.boolean(), twoFactorToken: z.string(), + warpUpdateInterval: z.number().int().min(0), webBasePath: z.string(), webCertFile: z.string(), webDomain: z.string(), diff --git a/frontend/src/pages/xray/overrides/WarpModal.tsx b/frontend/src/pages/xray/overrides/WarpModal.tsx index 246b031fd..b495aa4fe 100644 --- a/frontend/src/pages/xray/overrides/WarpModal.tsx +++ b/frontend/src/pages/xray/overrides/WarpModal.tsx @@ -80,6 +80,7 @@ export default function WarpModal({ const [warpData, setWarpData] = useState(null); const [warpConfig, setWarpConfig] = useState(null); const [warpPlus, setWarpPlus] = useState(''); + const [updateInterval, setUpdateInterval] = useState(0); const [licenseError, setLicenseError] = useState(''); const [stagedOutbound, setStagedOutbound] = useState | null>(null); @@ -89,24 +90,29 @@ export default function WarpModal({ return list.findIndex((o) => o?.tag === 'warp'); }, [templateSettings?.outbounds]); - const collectConfig = useCallback((data: WarpData | null, config: WarpConfig | null) => { - const cfg = config?.config; - if (!cfg?.peers?.length) return; - const peer = cfg.peers[0]; - setStagedOutbound({ - tag: 'warp', - protocol: 'wireguard', - settings: { - mtu: 1420, - secretKey: data?.private_key, - address: addressesFor(cfg.interface?.addresses || {}), - reserved: reservedFor(cfg.client_id ?? data?.client_id), - domainStrategy: 'ForceIP', - peers: [{ publicKey: peer.public_key, endpoint: peer.endpoint?.host }], - noKernelTun: false, - }, - }); - }, []); + const collectConfig = useCallback( + (data: WarpData | null, config: WarpConfig | null): Record | null => { + const cfg = config?.config; + if (!cfg?.peers?.length) return null; + const peer = cfg.peers[0]; + const outbound: Record = { + tag: 'warp', + protocol: 'wireguard', + settings: { + mtu: 1420, + secretKey: data?.private_key, + address: addressesFor(cfg.interface?.addresses || {}), + reserved: reservedFor(cfg.client_id ?? data?.client_id), + domainStrategy: 'ForceIP', + peers: [{ publicKey: peer.public_key, endpoint: peer.endpoint?.host }], + noKernelTun: false, + }, + }; + setStagedOutbound(outbound); + return outbound; + }, + [], + ); const fetchData = useCallback(async () => { setLoading(true); @@ -116,6 +122,10 @@ export default function WarpModal({ const raw = msg.obj; setWarpData(raw && raw.length > 0 ? JSON.parse(raw) : null); } + const settingMsg = await HttpUtil.post>('/panel/api/setting/all'); + if (settingMsg?.success && settingMsg.obj) { + setUpdateInterval(Number(settingMsg.obj.warpUpdateInterval) || 0); + } } finally { setLoading(false); } @@ -159,6 +169,40 @@ export default function WarpModal({ } } + async function changeIp() { + setLoading(true); + try { + const msg = await HttpUtil.post('/panel/api/xray/warp/changeIp'); + if (msg?.success && msg.obj) { + const parsed = JSON.parse(msg.obj); + setWarpData(parsed.data); + setWarpConfig(parsed.config); + const built = collectConfig(parsed.data, parsed.config); + // The backend already persisted the new keys into the saved Xray + // template; keep the in-memory editor in sync so a later template + // save doesn't revert them to the old keys. + if (built && warpOutboundIndex >= 0) { + onResetOutbound({ index: warpOutboundIndex, outbound: built }); + } + messageApi.success(t('pages.xray.warp.changeIpSuccess', 'WARP IP changed successfully!')); + } + } finally { + setLoading(false); + } + } + + async function saveInterval() { + setLoading(true); + try { + const msg = await HttpUtil.post('/panel/api/xray/warp/interval', { interval: updateInterval }); + if (msg?.success) { + messageApi.success(t('pages.setting.toasts.saveSuccess', 'Settings saved successfully')); + } + } finally { + setLoading(false); + } + } + async function updateLicense() { if (warpPlus.length < 26) return; setLoading(true); @@ -281,13 +325,37 @@ export default function WarpModal({ ), }, + { + key: '2', + label: t('pages.xray.warp.autoUpdateIp', 'Auto Update IP Address'), + children: ( +
+ + setUpdateInterval(Number(e.target.value))} + /> + + +
+ ), + }, ]} /> {t('pages.xray.warp.accountInfo')} - +
+ + +
{hasConfig && ( <> diff --git a/util/wireguard.go b/util/wireguard.go new file mode 100644 index 000000000..a7f2c9236 --- /dev/null +++ b/util/wireguard.go @@ -0,0 +1,24 @@ +package util + +import ( + "crypto/rand" + "encoding/base64" + + "golang.org/x/crypto/curve25519" +) + +// GenerateWireguardKeypair generates a base64 encoded private and public key pair for Wireguard. +func GenerateWireguardKeypair() (privateKey string, publicKey string, err error) { + var priv [32]byte + if _, err := rand.Read(priv[:]); err != nil { + return "", "", err + } + priv[0] &= 248 + priv[31] &= 127 + priv[31] |= 64 + + var pub [32]byte + curve25519.ScalarBaseMult(&pub, &priv) + + return base64.StdEncoding.EncodeToString(priv[:]), base64.StdEncoding.EncodeToString(pub[:]), nil +} diff --git a/web/controller/xray_setting.go b/web/controller/xray_setting.go index 3ba9dcfaf..bccba5824 100644 --- a/web/controller/xray_setting.go +++ b/web/controller/xray_setting.go @@ -3,6 +3,8 @@ package controller import ( "encoding/json" "fmt" + "strconv" + "time" "github.com/mhsanaei/3x-ui/v3/util/common" "github.com/mhsanaei/3x-ui/v3/web/service" @@ -12,13 +14,13 @@ import ( // XraySettingController handles Xray configuration and settings operations. type XraySettingController struct { - XraySettingService service.XraySettingService - SettingService service.SettingService - InboundService service.InboundService - OutboundService service.OutboundService - XrayService service.XrayService - WarpService service.WarpService - NordService service.NordService + XraySettingService service.XraySettingService + SettingService service.SettingService + InboundService service.InboundService + OutboundService service.OutboundService + XrayService service.XrayService + WarpService service.WarpService + NordService service.NordService OutboundSubscriptionService service.OutboundSubscriptionService } @@ -165,9 +167,26 @@ func (a *XraySettingController) warp(c *gin.Context) { skey := c.PostForm("privateKey") pkey := c.PostForm("publicKey") resp, err = a.WarpService.RegWarp(skey, pkey) + case "changeIp": + resp, err = a.WarpService.ChangeWarpIP() + if err == nil { + a.XrayService.SetToNeedRestart() + // Restart the auto-update clock so a scheduled rotation + // doesn't fire right after this manual one. + _ = a.SettingService.SetWarpLastUpdate(time.Now().Unix()) + } case "license": license := c.PostForm("license") resp, err = a.WarpService.SetWarpLicense(license) + case "interval": + interval, convErr := strconv.Atoi(c.PostForm("interval")) + if convErr != nil || interval < 0 { + err = common.NewError("invalid warp update interval") + } else if err = a.SettingService.SetWarpUpdateInterval(interval); err == nil && interval > 0 { + // Count the interval from now rather than from epoch 0, + // otherwise the job would rotate on its next tick. + _ = a.SettingService.SetWarpLastUpdate(time.Now().Unix()) + } } jsonObj(c, resp, err) diff --git a/web/entity/entity.go b/web/entity/entity.go index f1c05dfb5..3c8dbbe47 100644 --- a/web/entity/entity.go +++ b/web/entity/entity.go @@ -113,6 +113,9 @@ type AllSetting struct { LdapDefaultExpiryDays int `json:"ldapDefaultExpiryDays" form:"ldapDefaultExpiryDays" validate:"gte=0"` LdapDefaultLimitIP int `json:"ldapDefaultLimitIP" form:"ldapDefaultLimitIP" validate:"gte=0"` // JSON subscription routing rules + + // WARP + WarpUpdateInterval int `json:"warpUpdateInterval" form:"warpUpdateInterval" validate:"gte=0"` } // AllSettingView is the browser-safe settings read model. Secret values diff --git a/web/job/warp_ip_job.go b/web/job/warp_ip_job.go new file mode 100644 index 000000000..55c741002 --- /dev/null +++ b/web/job/warp_ip_job.go @@ -0,0 +1,52 @@ +package job + +import ( + "time" + + "github.com/mhsanaei/3x-ui/v3/logger" + "github.com/mhsanaei/3x-ui/v3/web/service" +) + +type WarpIpJob struct { + settingService service.SettingService + warpService service.WarpService + xrayService service.XrayService +} + +func NewWarpIpJob() *WarpIpJob { + return &WarpIpJob{} +} + +func (j *WarpIpJob) Run() { + allSetting, err := j.settingService.GetAllSetting() + if err != nil { + return + } + interval := allSetting.WarpUpdateInterval + if interval <= 0 { + return + } + + lastUpdate, _ := j.settingService.GetWarpLastUpdate() + now := time.Now().Unix() + + // First run after the feature is enabled (e.g. interval set via direct + // DB edit): establish a baseline instead of rotating immediately. + if lastUpdate == 0 { + _ = j.settingService.SetWarpLastUpdate(now) + return + } + + if now-lastUpdate >= int64(interval*24*3600) { + logger.Info("Starting scheduled WARP IP update...") + _, err := j.warpService.ChangeWarpIP() + if err != nil { + logger.Warning("Failed to update WARP IP: ", err) + return + } + + _ = j.settingService.SetWarpLastUpdate(now) + j.xrayService.SetToNeedRestart() + logger.Info("Successfully updated WARP IP and scheduled Xray restart") + } +} diff --git a/web/service/setting.go b/web/service/setting.go index 6348e43a5..068654d4f 100644 --- a/web/service/setting.go +++ b/web/service/setting.go @@ -89,6 +89,7 @@ var defaultValueMap = map[string]string{ "subThemeDir": "", "datepicker": "gregorian", "warp": "", + "warpUpdateInterval": "0", "nord": "", "externalTrafficInformEnable": "false", "externalTrafficInformURI": "", @@ -323,6 +324,22 @@ func (s *SettingService) setInt(key string, value int) error { return s.setString(key, strconv.Itoa(value)) } +func (s *SettingService) GetWarpLastUpdate() (int64, error) { + val, err := s.getString("warpLastUpdate") + if err != nil || val == "" { + return 0, err + } + return strconv.ParseInt(val, 10, 64) +} + +func (s *SettingService) SetWarpLastUpdate(val int64) error { + return s.saveSetting("warpLastUpdate", strconv.FormatInt(val, 10)) +} + +func (s *SettingService) SetWarpUpdateInterval(val int) error { + return s.setInt("warpUpdateInterval", val) +} + func (s *SettingService) GetXrayConfigTemplate() (string, error) { return s.getString("xrayTemplateConfig") } diff --git a/web/service/warp.go b/web/service/warp.go index b30348eac..8afe25709 100644 --- a/web/service/warp.go +++ b/web/service/warp.go @@ -9,6 +9,8 @@ import ( "os" "time" + "github.com/mhsanaei/3x-ui/v3/logger" + "github.com/mhsanaei/3x-ui/v3/util" "github.com/mhsanaei/3x-ui/v3/util/common" ) @@ -170,6 +172,44 @@ func (s *WarpService) SetWarpLicense(license string) (string, error) { return string(newWarpData), nil } +func (s *WarpService) ChangeWarpIP() (string, error) { + warpDataMap, err := s.loadWarpCreds() + if err != nil { + return "", err + } + + privKey, pubKey, err := util.GenerateWireguardKeypair() + if err != nil { + return "", err + } + + result, err := s.RegWarp(privKey, pubKey) + if err != nil { + return "", err + } + + var parsed struct { + Data map[string]string `json:"data"` + Config map[string]interface{} `json:"config"` + } + if err := json.Unmarshal([]byte(result), &parsed); err != nil { + return "", err + } + + xraySvc := XraySettingService{} + if err := xraySvc.UpdateWarpXraySetting(parsed.Data, parsed.Config); err != nil { + return "", err + } + + if license, ok := warpDataMap["license_key"]; ok && len(license) >= 26 { + if _, licErr := s.SetWarpLicense(license); licErr != nil { + logger.Warning("ChangeWarpIP: failed to re-apply WARP license: ", licErr) + } + } + + return result, nil +} + // loadWarpCreds reads the stored warp JSON and ensures access_token + device_id are set. func (s *WarpService) loadWarpCreds() (map[string]string, error) { warp, err := s.SettingService.GetWarp() diff --git a/web/service/xray_setting.go b/web/service/xray_setting.go index 1fda04aa4..5065ef52f 100644 --- a/web/service/xray_setting.go +++ b/web/service/xray_setting.go @@ -2,6 +2,7 @@ package service import ( _ "embed" + "encoding/base64" "encoding/json" "slices" @@ -40,6 +41,94 @@ func (s *XraySettingService) CheckXrayConfig(XrayTemplateConfig string) error { return nil } +func (s *XraySettingService) UpdateWarpXraySetting(warpData map[string]string, warpConfig map[string]interface{}) error { + template, err := s.GetXrayConfigTemplate() + if err != nil { + return err + } + + var cfg map[string]interface{} + if err := json.Unmarshal([]byte(template), &cfg); err != nil { + return err + } + + outbounds, ok := cfg["outbounds"].([]interface{}) + if !ok { + return nil + } + + updated := false + for _, outIface := range outbounds { + out, ok := outIface.(map[string]interface{}) + if !ok { + continue + } + if tag, ok := out["tag"].(string); ok && tag == "warp" { + settings, ok := out["settings"].(map[string]interface{}) + if !ok { + continue + } + + settings["secretKey"] = warpData["private_key"] + + if conf, ok := warpConfig["config"].(map[string]interface{}); ok { + if iface, ok := conf["interface"].(map[string]interface{}); ok { + if addrs, ok := iface["addresses"].(map[string]interface{}); ok { + var addrList []string + if v4, ok := addrs["v4"].(string); ok && v4 != "" { + addrList = append(addrList, v4+"/32") + } + if v6, ok := addrs["v6"].(string); ok && v6 != "" { + addrList = append(addrList, v6+"/128") + } + settings["address"] = addrList + } + } + + var clientId string + if id, ok := conf["client_id"].(string); ok { + clientId = id + } else if id, ok := warpData["client_id"]; ok { + clientId = id + } + if clientId != "" { + decoded, _ := base64.StdEncoding.DecodeString(clientId) + var res []int + for _, b := range decoded { + res = append(res, int(b)) + } + settings["reserved"] = res + } + + if peers, ok := conf["peers"].([]interface{}); ok && len(peers) > 0 { + if peer, ok := peers[0].(map[string]interface{}); ok { + if pSettings, ok := settings["peers"].([]interface{}); ok && len(pSettings) > 0 { + if pSet, ok := pSettings[0].(map[string]interface{}); ok { + pSet["publicKey"] = peer["public_key"] + if endpoint, ok := peer["endpoint"].(map[string]interface{}); ok { + pSet["endpoint"] = endpoint["host"] + } + } + } + } + } + } + updated = true + break + } + } + + if updated { + outJSON, err := json.MarshalIndent(cfg, "", " ") + if err != nil { + return err + } + return s.SaveXraySetting(string(outJSON)) + } + + return nil +} + // UnwrapXrayTemplateConfig returns the raw xray config JSON from `raw`, // peeling off any number of `{ "inboundTags": ..., "outboundTestUrl": ..., // "xraySetting": }` response-shaped wrappers that may have diff --git a/web/translation/ar-EG.json b/web/translation/ar-EG.json index 73889fec4..beb47cd3a 100644 --- a/web/translation/ar-EG.json +++ b/web/translation/ar-EG.json @@ -1455,6 +1455,11 @@ "outboundUpdated": "تم تحديث صادر NordVPN" }, "warp": { + "changeIp": "تغيير الـ IP", + "changeIpSuccess": "تم تغيير عنوان IP الخاص بـ WARP بنجاح!", + "autoUpdateIp": "التحديث التلقائي لعنوان IP", + "intervalDays": "الفاصل الزمني (أيام)", + "intervalDesc": "0 للتعطيل. يغيّر عنوان IP تلقائيًا.", "licenseError": "فشل تعيين رخصة WARP.", "fetchFirst": "احصل على تكوين WARP أولاً.", "createAccount": "إنشاء حساب WARP", diff --git a/web/translation/en-US.json b/web/translation/en-US.json index 0d1eddf0b..ff46768c3 100644 --- a/web/translation/en-US.json +++ b/web/translation/en-US.json @@ -1456,6 +1456,11 @@ "outboundUpdated": "NordVPN outbound updated" }, "warp": { + "changeIp": "Change IP", + "changeIpSuccess": "WARP IP changed successfully!", + "autoUpdateIp": "Auto Update IP Address", + "intervalDays": "Interval (Days)", + "intervalDesc": "0 to disable. Changes IP address automatically.", "licenseError": "Failed to set WARP license.", "fetchFirst": "Fetch the WARP config first.", "createAccount": "Create WARP account", diff --git a/web/translation/es-ES.json b/web/translation/es-ES.json index b77d78bdc..4a70bfb01 100644 --- a/web/translation/es-ES.json +++ b/web/translation/es-ES.json @@ -1455,6 +1455,11 @@ "outboundUpdated": "Salida NordVPN actualizada" }, "warp": { + "changeIp": "Cambiar IP", + "changeIpSuccess": "¡IP de WARP cambiada correctamente!", + "autoUpdateIp": "Actualizar IP automáticamente", + "intervalDays": "Intervalo (días)", + "intervalDesc": "0 para desactivar. Cambia la dirección IP automáticamente.", "licenseError": "No se pudo establecer la licencia WARP.", "fetchFirst": "Obtén primero la configuración WARP.", "createAccount": "Crear cuenta WARP", diff --git a/web/translation/fa-IR.json b/web/translation/fa-IR.json index ba9de91dd..5f932528b 100644 --- a/web/translation/fa-IR.json +++ b/web/translation/fa-IR.json @@ -1455,6 +1455,11 @@ "outboundUpdated": "خروجی NordVPN به‌روزرسانی شد" }, "warp": { + "changeIp": "تغییر IP", + "changeIpSuccess": "آدرس IP وارپ با موفقیت تغییر کرد!", + "autoUpdateIp": "به‌روزرسانی خودکار آدرس IP", + "intervalDays": "بازه (روز)", + "intervalDesc": "برای غیرفعال‌سازی ۰ بگذارید. آدرس IP را به‌صورت خودکار تغییر می‌دهد.", "licenseError": "تنظیم لایسنس WARP ناموفق بود.", "fetchFirst": "ابتدا پیکربندی WARP را دریافت کنید.", "createAccount": "ایجاد حساب WARP", diff --git a/web/translation/id-ID.json b/web/translation/id-ID.json index bb3675276..cef57c286 100644 --- a/web/translation/id-ID.json +++ b/web/translation/id-ID.json @@ -1455,6 +1455,11 @@ "outboundUpdated": "Outbound NordVPN diperbarui" }, "warp": { + "changeIp": "Ganti IP", + "changeIpSuccess": "IP WARP berhasil diganti!", + "autoUpdateIp": "Perbarui Alamat IP Otomatis", + "intervalDays": "Interval (Hari)", + "intervalDesc": "0 untuk menonaktifkan. Mengganti alamat IP secara otomatis.", "licenseError": "Gagal mengatur lisensi WARP.", "fetchFirst": "Ambil konfig WARP terlebih dahulu.", "createAccount": "Buat akun WARP", diff --git a/web/translation/ja-JP.json b/web/translation/ja-JP.json index 917651405..1e130ea44 100644 --- a/web/translation/ja-JP.json +++ b/web/translation/ja-JP.json @@ -1455,6 +1455,11 @@ "outboundUpdated": "NordVPN アウトバウンドを更新しました" }, "warp": { + "changeIp": "IP を変更", + "changeIpSuccess": "WARP の IP を変更しました!", + "autoUpdateIp": "IP アドレスの自動更新", + "intervalDays": "間隔(日)", + "intervalDesc": "0 で無効。IP アドレスを自動的に変更します。", "licenseError": "WARP ライセンスの設定に失敗しました。", "fetchFirst": "先に WARP 構成を取得してください。", "createAccount": "WARP アカウントを作成", diff --git a/web/translation/pt-BR.json b/web/translation/pt-BR.json index de3589d09..429f835a0 100644 --- a/web/translation/pt-BR.json +++ b/web/translation/pt-BR.json @@ -1455,6 +1455,11 @@ "outboundUpdated": "Saída NordVPN atualizada" }, "warp": { + "changeIp": "Alterar IP", + "changeIpSuccess": "IP do WARP alterado com sucesso!", + "autoUpdateIp": "Atualizar endereço IP automaticamente", + "intervalDays": "Intervalo (dias)", + "intervalDesc": "0 para desativar. Altera o endereço IP automaticamente.", "licenseError": "Falha ao definir licença WARP.", "fetchFirst": "Obtenha primeiro a configuração WARP.", "createAccount": "Criar conta WARP", diff --git a/web/translation/ru-RU.json b/web/translation/ru-RU.json index b1d8e7254..f171c3517 100644 --- a/web/translation/ru-RU.json +++ b/web/translation/ru-RU.json @@ -1455,6 +1455,11 @@ "outboundUpdated": "Исходящий NordVPN обновлён" }, "warp": { + "changeIp": "Сменить IP", + "changeIpSuccess": "IP-адрес WARP успешно изменён!", + "autoUpdateIp": "Автоматическое обновление IP-адреса", + "intervalDays": "Интервал (дни)", + "intervalDesc": "0 — отключить. Автоматически меняет IP-адрес.", "licenseError": "Не удалось установить лицензию WARP.", "fetchFirst": "Сначала получите WARP-конфиг.", "createAccount": "Создать аккаунт WARP", diff --git a/web/translation/tr-TR.json b/web/translation/tr-TR.json index 93c2ddd88..ec602a040 100644 --- a/web/translation/tr-TR.json +++ b/web/translation/tr-TR.json @@ -1454,6 +1454,11 @@ "outboundUpdated": "NordVPN giden bağlantı güncellendi." }, "warp": { + "changeIp": "IP Değiştir", + "changeIpSuccess": "WARP IP adresi başarıyla değiştirildi!", + "autoUpdateIp": "IP Adresini Otomatik Güncelle", + "intervalDays": "Aralık (Gün)", + "intervalDesc": "Devre dışı bırakmak için 0. IP adresini otomatik olarak değiştirir.", "licenseError": "WARP lisansı ayarlanamadı.", "fetchFirst": "Önce WARP yapılandırmasını alın.", "createAccount": "WARP Hesabı Oluştur", diff --git a/web/translation/uk-UA.json b/web/translation/uk-UA.json index e147e423d..ef6d4f714 100644 --- a/web/translation/uk-UA.json +++ b/web/translation/uk-UA.json @@ -1455,6 +1455,11 @@ "outboundUpdated": "Вихідний NordVPN оновлено" }, "warp": { + "changeIp": "Змінити IP", + "changeIpSuccess": "IP-адресу WARP успішно змінено!", + "autoUpdateIp": "Автоматичне оновлення IP-адреси", + "intervalDays": "Інтервал (дні)", + "intervalDesc": "0 — вимкнути. Автоматично змінює IP-адресу.", "licenseError": "Не вдалося встановити ліцензію WARP.", "fetchFirst": "Спочатку отримайте WARP-конфіг.", "createAccount": "Створити акаунт WARP", diff --git a/web/translation/vi-VN.json b/web/translation/vi-VN.json index d66775387..8c7e0a2c7 100644 --- a/web/translation/vi-VN.json +++ b/web/translation/vi-VN.json @@ -1455,6 +1455,11 @@ "outboundUpdated": "Đã cập nhật outbound NordVPN" }, "warp": { + "changeIp": "Đổi IP", + "changeIpSuccess": "Đã đổi IP WARP thành công!", + "autoUpdateIp": "Tự động cập nhật địa chỉ IP", + "intervalDays": "Khoảng thời gian (ngày)", + "intervalDesc": "0 để tắt. Tự động đổi địa chỉ IP.", "licenseError": "Không thiết lập được giấy phép WARP.", "fetchFirst": "Hãy lấy cấu hình WARP trước.", "createAccount": "Tạo tài khoản WARP", diff --git a/web/translation/zh-CN.json b/web/translation/zh-CN.json index 2d0943c24..8e62cdd8b 100644 --- a/web/translation/zh-CN.json +++ b/web/translation/zh-CN.json @@ -1455,6 +1455,11 @@ "outboundUpdated": "NordVPN 出站已更新" }, "warp": { + "changeIp": "更换 IP", + "changeIpSuccess": "WARP IP 更换成功!", + "autoUpdateIp": "自动更新 IP 地址", + "intervalDays": "间隔(天)", + "intervalDesc": "设为 0 禁用。自动更换 IP 地址。", "licenseError": "设置 WARP 许可证失败。", "fetchFirst": "请先获取 WARP 配置。", "createAccount": "创建 WARP 账户", diff --git a/web/translation/zh-TW.json b/web/translation/zh-TW.json index cb1cad8b2..edb13db47 100644 --- a/web/translation/zh-TW.json +++ b/web/translation/zh-TW.json @@ -1455,6 +1455,11 @@ "outboundUpdated": "NordVPN 出站已更新" }, "warp": { + "changeIp": "更換 IP", + "changeIpSuccess": "WARP IP 更換成功!", + "autoUpdateIp": "自動更新 IP 位址", + "intervalDays": "間隔(天)", + "intervalDesc": "設為 0 停用。自動更換 IP 位址。", "licenseError": "設定 WARP 授權失敗。", "fetchFirst": "請先取得 WARP 設定。", "createAccount": "建立 WARP 帳號", diff --git a/web/web.go b/web/web.go index fa681eacf..ae2f597bc 100644 --- a/web/web.go +++ b/web/web.go @@ -299,6 +299,7 @@ func (s *Server) startTask(restartXray bool) { // check client ips from log file every day s.cron.AddJob("@daily", job.NewClearLogsJob()) + s.cron.AddJob("@hourly", job.NewWarpIpJob()) // Inbound traffic reset jobs // Run every hour