mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-28 00:24:19 +00:00
feat: add manual and automatic WARP IP rotation (#5099)
* feat: add manual and automatic WARP IP rotation * fix: update generated api and frontend schemas * fix(warp): validate rotation interval, fix auto-update timing, sync editor - Validate the auto-update interval as an integer and store it via setInt; a non-integer value previously broke GetAllSetting for the whole panel. - Seed warpLastUpdate when the interval is saved and when changing IP manually, so auto-update counts from "now" instead of epoch 0 and a manual rotation doesn't trigger an immediate scheduled one. - Guard WarpIpJob: when lastUpdate is unset, establish a baseline and skip instead of rotating on the next tick. - Log WARP license re-apply failures instead of swallowing them. - After a manual "Change IP", sync the in-memory Xray editor with the keys the backend persisted so a later template save can't revert them; only toast success when the interval save actually succeeds. - Add the WARP rotation UI strings to all 13 locales. - Drop trailing whitespace introduced in entity.go and xray_setting.go. --------- Co-authored-by: Rqzbeh <Rqzbeh@example.com> Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -75,6 +75,7 @@ export const EXAMPLES: Record<string, unknown> = {
|
||||
"trustedProxyCIDRs": "",
|
||||
"twoFactorEnable": false,
|
||||
"twoFactorToken": "",
|
||||
"warpUpdateInterval": 0,
|
||||
"webBasePath": "",
|
||||
"webCertFile": "",
|
||||
"webDomain": "",
|
||||
@@ -163,6 +164,7 @@ export const EXAMPLES: Record<string, unknown> = {
|
||||
"trustedProxyCIDRs": "",
|
||||
"twoFactorEnable": false,
|
||||
"twoFactorToken": "",
|
||||
"warpUpdateInterval": 0,
|
||||
"webBasePath": "",
|
||||
"webCertFile": "",
|
||||
"webDomain": "",
|
||||
|
||||
@@ -299,6 +299,11 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"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<string, unknown> = {
|
||||
"trustedProxyCIDRs",
|
||||
"twoFactorEnable",
|
||||
"twoFactorToken",
|
||||
"warpUpdateInterval",
|
||||
"webBasePath",
|
||||
"webCertFile",
|
||||
"webDomain",
|
||||
@@ -727,6 +733,11 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"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<string, unknown> = {
|
||||
"trustedProxyCIDRs",
|
||||
"twoFactorEnable",
|
||||
"twoFactorToken",
|
||||
"warpUpdateInterval",
|
||||
"webBasePath",
|
||||
"webCertFile",
|
||||
"webDomain",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -80,6 +80,7 @@ export default function WarpModal({
|
||||
const [warpData, setWarpData] = useState<WarpData | null>(null);
|
||||
const [warpConfig, setWarpConfig] = useState<WarpConfig | null>(null);
|
||||
const [warpPlus, setWarpPlus] = useState('');
|
||||
const [updateInterval, setUpdateInterval] = useState<number>(0);
|
||||
const [licenseError, setLicenseError] = useState('');
|
||||
const [stagedOutbound, setStagedOutbound] = useState<Record<string, unknown> | 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<string, unknown> | null => {
|
||||
const cfg = config?.config;
|
||||
if (!cfg?.peers?.length) return null;
|
||||
const peer = cfg.peers[0];
|
||||
const outbound: Record<string, unknown> = {
|
||||
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<Record<string, unknown>>('/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<string>('/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({
|
||||
</Form>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: t('pages.xray.warp.autoUpdateIp', 'Auto Update IP Address'),
|
||||
children: (
|
||||
<Form colon={false} labelCol={{ md: { span: 8 } }} wrapperCol={{ md: { span: 12 } }}>
|
||||
<Form.Item label={t('pages.xray.warp.intervalDays', 'Interval (Days)')} extra={t('pages.xray.warp.intervalDesc', '0 to disable. Changes IP address automatically.')}>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
value={updateInterval}
|
||||
onChange={(e) => setUpdateInterval(Number(e.target.value))}
|
||||
/>
|
||||
<Button className="mt-8" type="primary" loading={loading} onClick={saveInterval}>
|
||||
{t('save', 'Save')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Divider className="zero-margin">{t('pages.xray.warp.accountInfo')}</Divider>
|
||||
<Button className="my-8" loading={loading} type="primary" icon={<SyncOutlined />} onClick={getConfig}>
|
||||
{t('refresh')}
|
||||
</Button>
|
||||
<div className="my-8">
|
||||
<Button loading={loading} type="primary" icon={<SyncOutlined />} onClick={getConfig}>
|
||||
{t('refresh')}
|
||||
</Button>
|
||||
<Button loading={loading} type="primary" className="ml-8" icon={<SyncOutlined />} onClick={changeIp}>
|
||||
{t('pages.xray.warp.changeIp', 'Change IP')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{hasConfig && (
|
||||
<>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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": <real config> }` response-shaped wrappers that may have
|
||||
|
||||
@@ -1455,6 +1455,11 @@
|
||||
"outboundUpdated": "تم تحديث صادر NordVPN"
|
||||
},
|
||||
"warp": {
|
||||
"changeIp": "تغيير الـ IP",
|
||||
"changeIpSuccess": "تم تغيير عنوان IP الخاص بـ WARP بنجاح!",
|
||||
"autoUpdateIp": "التحديث التلقائي لعنوان IP",
|
||||
"intervalDays": "الفاصل الزمني (أيام)",
|
||||
"intervalDesc": "0 للتعطيل. يغيّر عنوان IP تلقائيًا.",
|
||||
"licenseError": "فشل تعيين رخصة WARP.",
|
||||
"fetchFirst": "احصل على تكوين WARP أولاً.",
|
||||
"createAccount": "إنشاء حساب WARP",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1455,6 +1455,11 @@
|
||||
"outboundUpdated": "خروجی NordVPN بهروزرسانی شد"
|
||||
},
|
||||
"warp": {
|
||||
"changeIp": "تغییر IP",
|
||||
"changeIpSuccess": "آدرس IP وارپ با موفقیت تغییر کرد!",
|
||||
"autoUpdateIp": "بهروزرسانی خودکار آدرس IP",
|
||||
"intervalDays": "بازه (روز)",
|
||||
"intervalDesc": "برای غیرفعالسازی ۰ بگذارید. آدرس IP را بهصورت خودکار تغییر میدهد.",
|
||||
"licenseError": "تنظیم لایسنس WARP ناموفق بود.",
|
||||
"fetchFirst": "ابتدا پیکربندی WARP را دریافت کنید.",
|
||||
"createAccount": "ایجاد حساب WARP",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1455,6 +1455,11 @@
|
||||
"outboundUpdated": "NordVPN アウトバウンドを更新しました"
|
||||
},
|
||||
"warp": {
|
||||
"changeIp": "IP を変更",
|
||||
"changeIpSuccess": "WARP の IP を変更しました!",
|
||||
"autoUpdateIp": "IP アドレスの自動更新",
|
||||
"intervalDays": "間隔(日)",
|
||||
"intervalDesc": "0 で無効。IP アドレスを自動的に変更します。",
|
||||
"licenseError": "WARP ライセンスの設定に失敗しました。",
|
||||
"fetchFirst": "先に WARP 構成を取得してください。",
|
||||
"createAccount": "WARP アカウントを作成",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1455,6 +1455,11 @@
|
||||
"outboundUpdated": "Исходящий NordVPN обновлён"
|
||||
},
|
||||
"warp": {
|
||||
"changeIp": "Сменить IP",
|
||||
"changeIpSuccess": "IP-адрес WARP успешно изменён!",
|
||||
"autoUpdateIp": "Автоматическое обновление IP-адреса",
|
||||
"intervalDays": "Интервал (дни)",
|
||||
"intervalDesc": "0 — отключить. Автоматически меняет IP-адрес.",
|
||||
"licenseError": "Не удалось установить лицензию WARP.",
|
||||
"fetchFirst": "Сначала получите WARP-конфиг.",
|
||||
"createAccount": "Создать аккаунт WARP",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1455,6 +1455,11 @@
|
||||
"outboundUpdated": "Вихідний NordVPN оновлено"
|
||||
},
|
||||
"warp": {
|
||||
"changeIp": "Змінити IP",
|
||||
"changeIpSuccess": "IP-адресу WARP успішно змінено!",
|
||||
"autoUpdateIp": "Автоматичне оновлення IP-адреси",
|
||||
"intervalDays": "Інтервал (дні)",
|
||||
"intervalDesc": "0 — вимкнути. Автоматично змінює IP-адресу.",
|
||||
"licenseError": "Не вдалося встановити ліцензію WARP.",
|
||||
"fetchFirst": "Спочатку отримайте WARP-конфіг.",
|
||||
"createAccount": "Створити акаунт WARP",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1455,6 +1455,11 @@
|
||||
"outboundUpdated": "NordVPN 出站已更新"
|
||||
},
|
||||
"warp": {
|
||||
"changeIp": "更换 IP",
|
||||
"changeIpSuccess": "WARP IP 更换成功!",
|
||||
"autoUpdateIp": "自动更新 IP 地址",
|
||||
"intervalDays": "间隔(天)",
|
||||
"intervalDesc": "设为 0 禁用。自动更换 IP 地址。",
|
||||
"licenseError": "设置 WARP 许可证失败。",
|
||||
"fetchFirst": "请先获取 WARP 配置。",
|
||||
"createAccount": "创建 WARP 账户",
|
||||
|
||||
@@ -1455,6 +1455,11 @@
|
||||
"outboundUpdated": "NordVPN 出站已更新"
|
||||
},
|
||||
"warp": {
|
||||
"changeIp": "更換 IP",
|
||||
"changeIpSuccess": "WARP IP 更換成功!",
|
||||
"autoUpdateIp": "自動更新 IP 位址",
|
||||
"intervalDays": "間隔(天)",
|
||||
"intervalDesc": "設為 0 停用。自動更換 IP 位址。",
|
||||
"licenseError": "設定 WARP 授權失敗。",
|
||||
"fetchFirst": "請先取得 WARP 設定。",
|
||||
"createAccount": "建立 WARP 帳號",
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user