mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-25 13:56:10 +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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user