mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-02-27 20:53:01 +00:00
feat: Add NordVPN NordLynx (WireGuard) integration with dedicated UI and backend services.
This commit is contained in:
parent
37f0880f8f
commit
402b41893d
10 changed files with 469 additions and 3 deletions
|
|
@ -17,6 +17,7 @@ type XraySettingController struct {
|
|||
OutboundService service.OutboundService
|
||||
XrayService service.XrayService
|
||||
WarpService service.WarpService
|
||||
NordService service.NordService
|
||||
}
|
||||
|
||||
// NewXraySettingController creates a new XraySettingController and initializes its routes.
|
||||
|
|
@ -35,6 +36,7 @@ func (a *XraySettingController) initRouter(g *gin.RouterGroup) {
|
|||
|
||||
g.POST("/", a.getXraySetting)
|
||||
g.POST("/warp/:action", a.warp)
|
||||
g.POST("/nord/:action", a.nord)
|
||||
g.POST("/update", a.updateSetting)
|
||||
g.POST("/resetOutboundsTraffic", a.resetOutboundsTraffic)
|
||||
g.POST("/testOutbound", a.testOutbound)
|
||||
|
|
@ -123,6 +125,32 @@ func (a *XraySettingController) warp(c *gin.Context) {
|
|||
jsonObj(c, resp, err)
|
||||
}
|
||||
|
||||
// nord handles NordVPN-related operations based on the action parameter.
|
||||
func (a *XraySettingController) nord(c *gin.Context) {
|
||||
action := c.Param("action")
|
||||
var resp string
|
||||
var err error
|
||||
switch action {
|
||||
case "countries":
|
||||
resp, err = a.NordService.GetCountries()
|
||||
case "servers":
|
||||
countryId := c.PostForm("countryId")
|
||||
resp, err = a.NordService.GetServers(countryId)
|
||||
case "reg":
|
||||
token := c.PostForm("token")
|
||||
resp, err = a.NordService.GetCredentials(token)
|
||||
case "setKey":
|
||||
key := c.PostForm("key")
|
||||
resp, err = a.NordService.SetKey(key)
|
||||
case "data":
|
||||
resp, err = a.NordService.GetNordData()
|
||||
case "del":
|
||||
err = a.NordService.DelNordData()
|
||||
}
|
||||
|
||||
jsonObj(c, resp, err)
|
||||
}
|
||||
|
||||
// getOutboundsTraffic retrieves the traffic statistics for outbounds.
|
||||
func (a *XraySettingController) getOutboundsTraffic(c *gin.Context) {
|
||||
outboundsTraffic, err := a.OutboundService.GetOutboundsTraffic()
|
||||
|
|
|
|||
255
web/html/modals/nord_modal.html
Normal file
255
web/html/modals/nord_modal.html
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
{{define "modals/nordModal"}}
|
||||
<a-modal id="nord-modal" v-model="nordModal.visible" title="NordVPN NordLynx"
|
||||
:confirm-loading="nordModal.confirmLoading" :closable="true" :mask-closable="true"
|
||||
:footer="null" :class="themeSwitcher.currentTheme">
|
||||
<template v-if="nordModal.nordData == null">
|
||||
<a-tabs default-active-key="token" :class="themeSwitcher.currentTheme">
|
||||
<a-tab-pane key="token" tab='{{ i18n "pages.xray.outbound.accessToken" }}'>
|
||||
<a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:18} }" :style="{ marginTop: '20px' }">
|
||||
<a-form-item label='{{ i18n "pages.xray.outbound.accessToken" }}'>
|
||||
<a-input v-model="nordModal.token" placeholder='{{ i18n "pages.xray.outbound.accessToken" }}'></a-input>
|
||||
<div :style="{ marginTop: '10px' }">
|
||||
<a-button type="primary" icon="login" @click="login()" :loading="nordModal.confirmLoading">{{ i18n "login" }}</a-button>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="key" tab='{{ i18n "pages.xray.outbound.privateKey" }}'>
|
||||
<a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:18} }" :style="{ marginTop: '20px' }">
|
||||
<a-form-item label='{{ i18n "pages.xray.outbound.privateKey" }}'>
|
||||
<a-input v-model="nordModal.manualKey" placeholder='{{ i18n "pages.xray.outbound.privateKey" }}'></a-input>
|
||||
<div :style="{ marginTop: '10px' }">
|
||||
<a-button type="primary" icon="save" @click="saveKey()" :loading="nordModal.confirmLoading">{{ i18n "save" }}</a-button>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</template>
|
||||
<template v-else>
|
||||
<table :style="{ margin: '5px 0', width: '100%' }">
|
||||
<tr class="client-table-odd-row" v-if="nordModal.nordData.token">
|
||||
<td>{{ i18n "pages.xray.outbound.accessToken" }}</td>
|
||||
<td>[[ nordModal.nordData.token ]]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.xray.outbound.privateKey" }}</td>
|
||||
<td>[[ nordModal.nordData.private_key ]]</td>
|
||||
</tr>
|
||||
</table>
|
||||
<a-button @click="logout" :loading="nordModal.confirmLoading" type="danger">{{ i18n "logout" }}</a-button>
|
||||
<a-divider :style="{ margin: '0' }">{{ i18n "pages.xray.outbound.settings" }}</a-divider>
|
||||
<a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:18} }" :style="{ marginTop: '10px' }">
|
||||
<a-form-item label='{{ i18n "pages.xray.outbound.country" }}'>
|
||||
<a-select v-model="nordModal.countryId" @change="fetchServers" show-search option-filter-prop="label">
|
||||
<a-select-option v-for="c in nordModal.countries" :key="c.id" :value="c.id" :label="c.name">
|
||||
[[ c.name ]] ([[ c.code ]])
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.outbound.server" }}' v-if="nordModal.servers.length > 0">
|
||||
<a-select v-model="nordModal.serverId">
|
||||
<a-select-option v-for="s in nordModal.servers" :key="s.id" :value="s.id">
|
||||
[[ s.name ]] ({{ i18n "pages.xray.outbound.load" }}: [[ s.load ]]%)
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-divider :style="{ margin: '10px 0' }">{{ i18n "pages.xray.outbound.outboundStatus" }}</a-divider>
|
||||
<a-form :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<template v-if="nordOutboundIndex>=0">
|
||||
<a-tag color="green" :style="{ lineHeight: '31px' }">{{ i18n "enabled" }}</a-tag>
|
||||
<a-button @click="resetOutbound" :loading="nordModal.confirmLoading" type="danger">{{ i18n "reset" }}</a-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-tag color="orange" :style="{ lineHeight: '31px' }">{{ i18n "disabled" }}</a-tag>
|
||||
<a-button @click="addOutbound" :disabled="!nordModal.serverId" :loading="nordModal.confirmLoading" type="primary">{{ i18n "pages.xray.outbound.addOutbound" }}</a-button>
|
||||
</template>
|
||||
</a-form>
|
||||
</template>
|
||||
</a-modal>
|
||||
|
||||
<script>
|
||||
const nordModal = {
|
||||
visible: false,
|
||||
confirmLoading: false,
|
||||
nordData: null,
|
||||
token: '',
|
||||
manualKey: '',
|
||||
countries: [],
|
||||
countryId: null,
|
||||
servers: [],
|
||||
serverId: null,
|
||||
show() {
|
||||
this.visible = true;
|
||||
this.getData();
|
||||
},
|
||||
close() {
|
||||
this.visible = false;
|
||||
},
|
||||
loading(loading = true) {
|
||||
this.confirmLoading = loading;
|
||||
},
|
||||
async getData() {
|
||||
this.loading(true);
|
||||
const msg = await HttpUtil.post('/panel/xray/nord/data');
|
||||
if (msg.success) {
|
||||
this.nordData = msg.obj ? JSON.parse(msg.obj) : null;
|
||||
if (this.nordData) {
|
||||
await this.fetchCountries();
|
||||
}
|
||||
}
|
||||
this.loading(false);
|
||||
},
|
||||
async login() {
|
||||
this.loading(true);
|
||||
const msg = await HttpUtil.post('/panel/xray/nord/reg', { token: this.token });
|
||||
if (msg.success) {
|
||||
this.nordData = JSON.parse(msg.obj);
|
||||
await this.fetchCountries();
|
||||
}
|
||||
this.loading(false);
|
||||
},
|
||||
async saveKey() {
|
||||
this.loading(true);
|
||||
const msg = await HttpUtil.post('/panel/xray/nord/setKey', { key: this.manualKey });
|
||||
if (msg.success) {
|
||||
this.nordData = JSON.parse(msg.obj);
|
||||
await this.fetchCountries();
|
||||
}
|
||||
this.loading(false);
|
||||
},
|
||||
async logout(index) {
|
||||
this.loading(true);
|
||||
const msg = await HttpUtil.post('/panel/xray/nord/del');
|
||||
if (msg.success) {
|
||||
this.delOutbound(index);
|
||||
this.delRouting();
|
||||
this.nordData = null;
|
||||
this.token = '';
|
||||
this.manualKey = '';
|
||||
this.countries = [];
|
||||
this.servers = [];
|
||||
this.countryId = null;
|
||||
}
|
||||
this.loading(false);
|
||||
},
|
||||
async fetchCountries() {
|
||||
const msg = await HttpUtil.post('/panel/xray/nord/countries');
|
||||
if (msg.success) {
|
||||
this.countries = JSON.parse(msg.obj);
|
||||
}
|
||||
},
|
||||
async fetchServers() {
|
||||
this.loading(true);
|
||||
this.servers = [];
|
||||
this.serverId = null;
|
||||
const msg = await HttpUtil.post('/panel/xray/nord/servers', { countryId: this.countryId });
|
||||
if (msg.success) {
|
||||
const data = JSON.parse(msg.obj);
|
||||
this.servers = (data.servers || []).sort((a, b) => a.load - b.load);
|
||||
if (this.servers.length === 0) {
|
||||
app.$message.warning('No servers found for the selected country');
|
||||
}
|
||||
}
|
||||
this.loading(false);
|
||||
},
|
||||
addOutbound() {
|
||||
const server = this.servers.find(s => s.id === this.serverId);
|
||||
if (!server) return;
|
||||
|
||||
const tech = server.technologies.find(t => t.id === 35);
|
||||
const publicKey = tech.metadata.find(m => m.name === 'public_key').value;
|
||||
|
||||
const outbound = {
|
||||
tag: `nord-${server.hostname}`,
|
||||
protocol: 'wireguard',
|
||||
settings: {
|
||||
secretKey: this.nordData.private_key,
|
||||
address: ['10.5.0.2/32'],
|
||||
peers: [{
|
||||
publicKey: publicKey,
|
||||
endpoint: server.station + ':51820'
|
||||
}],
|
||||
noKernelTun: false
|
||||
}
|
||||
};
|
||||
|
||||
app.templateSettings.outbounds.push(outbound);
|
||||
app.outboundSettings = JSON.stringify(app.templateSettings.outbounds);
|
||||
this.close();
|
||||
app.$message.success('NordVPN outbound added');
|
||||
},
|
||||
resetOutbound(index) {
|
||||
const server = this.servers.find(s => s.id === this.serverId);
|
||||
if (!server || index === -1) return;
|
||||
|
||||
const tech = server.technologies.find(t => t.id === 35);
|
||||
const publicKey = tech.metadata.find(m => m.name === 'public_key').value;
|
||||
|
||||
const oldTag = app.templateSettings.outbounds[index].tag;
|
||||
const newTag = `nord-${server.hostname}`;
|
||||
|
||||
const outbound = {
|
||||
tag: newTag,
|
||||
protocol: 'wireguard',
|
||||
settings: {
|
||||
secretKey: this.nordData.private_key,
|
||||
address: ['10.5.0.2/32'],
|
||||
peers: [{
|
||||
publicKey: publicKey,
|
||||
endpoint: server.station + ':51820'
|
||||
}],
|
||||
noKernelTun: false
|
||||
}
|
||||
};
|
||||
app.templateSettings.outbounds[index] = outbound;
|
||||
|
||||
// Sync routing rules
|
||||
app.templateSettings.routing.rules.forEach(r => {
|
||||
if (r.outboundTag === oldTag) {
|
||||
r.outboundTag = newTag;
|
||||
}
|
||||
});
|
||||
|
||||
app.outboundSettings = JSON.stringify(app.templateSettings.outbounds);
|
||||
this.close();
|
||||
app.$message.success('NordVPN outbound updated');
|
||||
},
|
||||
delOutbound(index) {
|
||||
if (index !== -1) {
|
||||
app.templateSettings.outbounds.splice(index, 1);
|
||||
app.outboundSettings = JSON.stringify(app.templateSettings.outbounds);
|
||||
}
|
||||
},
|
||||
delRouting() {
|
||||
if (app.templateSettings && app.templateSettings.routing) {
|
||||
app.templateSettings.routing.rules = app.templateSettings.routing.rules.filter(r => !r.outboundTag.startsWith("nord-"));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#nord-modal',
|
||||
data: {
|
||||
nordModal: nordModal,
|
||||
},
|
||||
methods: {
|
||||
login: () => nordModal.login(),
|
||||
saveKey: () => nordModal.saveKey(),
|
||||
logout() { nordModal.logout(this.nordOutboundIndex) },
|
||||
fetchServers: () => nordModal.fetchServers(),
|
||||
addOutbound: () => nordModal.addOutbound(),
|
||||
resetOutbound() { nordModal.resetOutbound(this.nordOutboundIndex) },
|
||||
},
|
||||
computed: {
|
||||
nordOutboundIndex: {
|
||||
get: function () {
|
||||
return app.templateSettings ? app.templateSettings.outbounds.findIndex((o) => o.tag.startsWith("nord-")) : -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
|
|
@ -313,6 +313,25 @@
|
|||
</template>
|
||||
</template>
|
||||
</a-setting-list-item>
|
||||
<a-setting-list-item paddings="small">
|
||||
<template #title>{{ i18n "pages.xray.nordRouting" }}</template>
|
||||
<template #control>
|
||||
<template v-if="NordExist">
|
||||
<a-select mode="tags" :style="{ width: '100%' }"
|
||||
v-model="nordDomains"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option :value="p.value" :label="p.label"
|
||||
v-for="p in settingsData.ServicesOptions">
|
||||
<span>[[ p.label ]]</span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-button type="primary" icon="api"
|
||||
@click="showNord()">{{ i18n "pages.xray.outbound.nordvpn" }}</a-button>
|
||||
</template>
|
||||
</template>
|
||||
</a-setting-list-item>
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel key="6"
|
||||
header='{{ i18n "pages.settings.resetDefaultConfig"}}'>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@
|
|||
</a-button>
|
||||
<a-button type="primary" icon="cloud"
|
||||
@click="showWarp()">WARP</a-button>
|
||||
<a-button type="primary" icon="api"
|
||||
@click="showNord()">NordVPN</a-button>
|
||||
</a-space>
|
||||
</a-col>
|
||||
<a-col :xs="12" :sm="12" :lg="12" :style="{ textAlign: 'right' }">
|
||||
|
|
|
|||
|
|
@ -166,6 +166,7 @@
|
|||
{{template "modals/dnsPresetsModal"}}
|
||||
{{template "modals/fakednsModal"}}
|
||||
{{template "modals/warpModal"}}
|
||||
{{template "modals/nordModal"}}
|
||||
<script>
|
||||
const rulesColumns = [
|
||||
{ title: "#", align: 'center', width: 15, scopedSlots: { customRender: 'action' } },
|
||||
|
|
@ -1057,6 +1058,9 @@
|
|||
},
|
||||
showWarp() {
|
||||
warpModal.show();
|
||||
},
|
||||
showNord() {
|
||||
nordModal.show();
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
|
|
@ -1397,6 +1401,19 @@
|
|||
this.templateRuleSetter({ outboundTag: "warp", property: "domain", data: newValue });
|
||||
}
|
||||
},
|
||||
nordTag: {
|
||||
get: function () {
|
||||
return this.templateSettings ? (this.templateSettings.outbounds.find((o) => o.tag.startsWith("nord-")) || { tag: "nord" }).tag : "nord";
|
||||
}
|
||||
},
|
||||
nordDomains: {
|
||||
get: function () {
|
||||
return this.templateRuleGetter({ outboundTag: this.nordTag, property: "domain" });
|
||||
},
|
||||
set: function (newValue) {
|
||||
this.templateRuleSetter({ outboundTag: this.nordTag, property: "domain", data: newValue });
|
||||
}
|
||||
},
|
||||
torrentSettings: {
|
||||
get: function () {
|
||||
return ArrayUtils.doAllItemsExist(this.settingsData.protocols.bittorrent, this.blockedProtocols);
|
||||
|
|
@ -1414,6 +1431,11 @@
|
|||
return this.templateSettings ? this.templateSettings.outbounds.findIndex((o) => o.tag == "warp") >= 0 : false;
|
||||
},
|
||||
},
|
||||
NordExist: {
|
||||
get: function () {
|
||||
return this.templateSettings ? this.templateSettings.outbounds.findIndex((o) => o.tag.startsWith("nord-")) >= 0 : false;
|
||||
},
|
||||
},
|
||||
enableDNS: {
|
||||
get: function () {
|
||||
return this.templateSettings ? this.templateSettings.dns != null : false;
|
||||
|
|
|
|||
104
web/service/nord.go
Normal file
104
web/service/nord.go
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v2/util/common"
|
||||
)
|
||||
|
||||
type NordService struct {
|
||||
SettingService
|
||||
}
|
||||
|
||||
func (s *NordService) GetCountries() (string, error) {
|
||||
resp, err := http.Get("https://api.nordvpn.com/v1/countries")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
func (s *NordService) GetServers(countryId string) (string, error) {
|
||||
url := fmt.Sprintf("https://api.nordvpn.com/v2/servers?limit=10&filters[servers_technologies][id]=35&filters[country_id]=%s", countryId)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
func (s *NordService) SetKey(privateKey string) (string, error) {
|
||||
nordData := map[string]string{
|
||||
"private_key": privateKey,
|
||||
"token": "", // No token for manual key
|
||||
}
|
||||
data, _ := json.Marshal(nordData)
|
||||
s.SettingService.SetNord(string(data))
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func (s *NordService) GetCredentials(token string) (string, error) {
|
||||
url := "https://api.nordvpn.com/v1/users/services/credentials"
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.SetBasicAuth("token", token)
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", common.NewErrorf("NordVPN API error: %s", resp.Status)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var creds map[string]any
|
||||
if err := json.Unmarshal(body, &creds); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
privateKey, ok := creds["nordlynx_private_key"].(string)
|
||||
if !ok || privateKey == "" {
|
||||
return "", common.NewError("failed to retrieve NordLynx private key")
|
||||
}
|
||||
|
||||
nordData := map[string]string{
|
||||
"private_key": privateKey,
|
||||
"token": token,
|
||||
}
|
||||
data, _ := json.Marshal(nordData)
|
||||
s.SettingService.SetNord(string(data))
|
||||
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func (s *NordService) GetNordData() (string, error) {
|
||||
return s.SettingService.GetNord()
|
||||
}
|
||||
|
||||
func (s *NordService) DelNordData() error {
|
||||
return s.SettingService.SetNord("")
|
||||
}
|
||||
|
|
@ -77,6 +77,7 @@ var defaultValueMap = map[string]string{
|
|||
"subJsonRules": "",
|
||||
"datepicker": "gregorian",
|
||||
"warp": "",
|
||||
"nord": "",
|
||||
"externalTrafficInformEnable": "false",
|
||||
"externalTrafficInformURI": "",
|
||||
"xrayOutboundTestUrl": "https://www.google.com/generate_204",
|
||||
|
|
@ -583,6 +584,14 @@ func (s *SettingService) SetWarp(data string) error {
|
|||
return s.setString("warp", data)
|
||||
}
|
||||
|
||||
func (s *SettingService) GetNord() (string, error) {
|
||||
return s.getString("nord")
|
||||
}
|
||||
|
||||
func (s *SettingService) SetNord(data string) error {
|
||||
return s.setString("nord", data)
|
||||
}
|
||||
|
||||
func (s *SettingService) GetExternalTrafficInformEnable() (bool, error) {
|
||||
return s.getBool("externalTrafficInformEnable")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
"confirm" = "Confirm"
|
||||
"cancel" = "Cancel"
|
||||
"close" = "Close"
|
||||
"save" = "Save"
|
||||
"logout" = "Log Out"
|
||||
"create" = "Create"
|
||||
"update" = "Update"
|
||||
"copy" = "Copy"
|
||||
|
|
@ -454,6 +456,8 @@
|
|||
"ipv4RoutingDesc" = "These options will route traffic based on a specific destination via IPv4."
|
||||
"warpRouting" = "WARP Routing"
|
||||
"warpRoutingDesc" = "These options will route traffic based on a specific destination via WARP."
|
||||
"nordRouting" = "NordVPN Routing"
|
||||
"nordRoutingDesc" = "These options will route traffic based on a specific destination via NordVPN."
|
||||
"Template" = "Advanced Xray Configuration Template"
|
||||
"TemplateDesc" = "The final Xray config file will be generated based on this template."
|
||||
"FreedomStrategy" = "Freedom Protocol Strategy"
|
||||
|
|
@ -531,6 +535,12 @@
|
|||
"testSuccess" = "Test successful"
|
||||
"testFailed" = "Test failed"
|
||||
"testError" = "Failed to test outbound"
|
||||
"nordvpn" = "NordVPN"
|
||||
"accessToken" = "Access Token"
|
||||
"country" = "Country"
|
||||
"server" = "Server"
|
||||
"privateKey" = "Private Key"
|
||||
"load" = "Load"
|
||||
|
||||
[pages.xray.balancer]
|
||||
"addBalancer" = "Add Balancer"
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
"confirm" = "تایید"
|
||||
"cancel" = "انصراف"
|
||||
"close" = "بستن"
|
||||
"save" = "ذخیره"
|
||||
"logout" = "خروج"
|
||||
"create" = "ایجاد"
|
||||
"update" = "بهروزرسانی"
|
||||
"copy" = "کپی"
|
||||
|
|
@ -454,6 +456,8 @@
|
|||
"ipv4RoutingDesc" = "این گزینهها ترافیک را از طریق آیپی نسخه4 سرور، به مقصد هدایت میکند"
|
||||
"warpRouting" = "WARP مسیریابی"
|
||||
"warpRoutingDesc" = "این گزینهها ترافیک را از طریق وارپ کلادفلر به مقصد هدایت میکند"
|
||||
"nordRouting" = "مسیریابی NordVPN"
|
||||
"nordRoutingDesc" = "این گزینهها ترافیک را بر اساس مقصد خاص از طریق NordVPN مسیریابی میکنند."
|
||||
"Template" = "پیکربندی پیشرفته الگو ایکسری"
|
||||
"TemplateDesc" = "فایل پیکربندی نهایی ایکسری بر اساس این الگو ایجاد میشود"
|
||||
"FreedomStrategy" = "Freedom استراتژی پروتکل"
|
||||
|
|
@ -531,6 +535,12 @@
|
|||
"testSuccess" = "تست موفقیتآمیز"
|
||||
"testFailed" = "تست ناموفق"
|
||||
"testError" = "خطا در تست خروجی"
|
||||
"nordvpn" = "NordVPN"
|
||||
"accessToken" = "توکن دسترسی"
|
||||
"country" = "کشور"
|
||||
"server" = "سرور"
|
||||
"privateKey" = "کلید خصوصی"
|
||||
"load" = "فشار سرور"
|
||||
|
||||
[pages.xray.balancer]
|
||||
"addBalancer" = "افزودن بالانسر"
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
"confirm" = "确定"
|
||||
"cancel" = "取消"
|
||||
"close" = "关闭"
|
||||
"save" = "保存"
|
||||
"logout" = "登出"
|
||||
"create" = "创建"
|
||||
"update" = "更新"
|
||||
"copy" = "复制"
|
||||
|
|
@ -454,6 +456,8 @@
|
|||
"ipv4RoutingDesc" = "此选项将仅通过 IPv4 路由到目标域"
|
||||
"warpRouting" = "WARP 路由"
|
||||
"warpRoutingDesc" = "注意:在使用这些选项之前,请按照面板 GitHub 上的步骤在你的服务器上以 socks5 代理模式安装 WARP。WARP 将通过 Cloudflare 服务器将流量路由到网站。"
|
||||
"nordRouting" = "NordVPN 路由"
|
||||
"nordRoutingDesc" = "这些选项将根据特定目的地通过 NordVPN 路由流量。"
|
||||
"Template" = "高级 Xray 配置模板"
|
||||
"TemplateDesc" = "最终的 Xray 配置文件将基于此模板生成"
|
||||
"FreedomStrategy" = "Freedom 协议策略"
|
||||
|
|
@ -528,9 +532,12 @@
|
|||
"test" = "测试"
|
||||
"testResult" = "测试结果"
|
||||
"testing" = "正在测试连接..."
|
||||
"testSuccess" = "测试成功"
|
||||
"testFailed" = "测试失败"
|
||||
"testError" = "测试出站失败"
|
||||
"nordvpn" = "NordVPN"
|
||||
"accessToken" = "访问令牌"
|
||||
"country" = "国家"
|
||||
"server" = "服务器"
|
||||
"privateKey" = "私钥"
|
||||
"load" = "负载"
|
||||
|
||||
[pages.xray.balancer]
|
||||
"addBalancer" = "添加负载均衡"
|
||||
|
|
|
|||
Loading…
Reference in a new issue