Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/MHSanaei/3x-ui.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPeter Liu <30622363+PedroLiu1999@users.noreply.github.com>2026-04-20 01:41:50 +0300
committerGitHub <noreply@github.com>2026-04-20 01:41:50 +0300
commit36b2a586756fc279304170976c9d486498d55919 (patch)
tree71a1dd665fa14a0fc9dad28c3ef421e3e3d7fe7c /web/service
parent59e98592251cac1dedb93905cb368a3ba93ad19c (diff)
feat: Add NordVPN NordLynx (WireGuard) integration (#3827)
* feat: Add NordVPN NordLynx (WireGuard) integration with dedicated UI and backend services. * remove limit=10 to get all servers * feat: add city selector to NordVPN modal * feat: auto-select best server on country/city change * feat: simplify filter logic and enforce > 7% load * fix --------- Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
Diffstat (limited to 'web/service')
-rw-r--r--web/service/nord.go145
-rw-r--r--web/service/setting.go9
2 files changed, 154 insertions, 0 deletions
diff --git a/web/service/nord.go b/web/service/nord.go
new file mode 100644
index 00000000..db0a48f9
--- /dev/null
+++ b/web/service/nord.go
@@ -0,0 +1,145 @@
+package service
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "time"
+
+ "github.com/mhsanaei/3x-ui/v2/util/common"
+)
+
+type NordService struct {
+ SettingService
+}
+
+var nordHTTPClient = &http.Client{Timeout: 15 * time.Second}
+
+// maxResponseSize limits the maximum size of NordVPN API responses (10 MB).
+const maxResponseSize = 10 << 20
+
+func (s *NordService) GetCountries() (string, error) {
+ resp, err := nordHTTPClient.Get("https://api.nordvpn.com/v1/countries")
+ if err != nil {
+ return "", err
+ }
+ defer resp.Body.Close()
+ body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseSize))
+ if err != nil {
+ return "", err
+ }
+ return string(body), nil
+}
+
+func (s *NordService) GetServers(countryId string) (string, error) {
+ // Validate countryId is numeric to prevent URL injection
+ for _, c := range countryId {
+ if c < '0' || c > '9' {
+ return "", common.NewError("invalid country ID")
+ }
+ }
+ url := fmt.Sprintf("https://api.nordvpn.com/v2/servers?limit=0&filters[servers_technologies][id]=35&filters[country_id]=%s", countryId)
+ resp, err := nordHTTPClient.Get(url)
+ if err != nil {
+ return "", err
+ }
+ defer resp.Body.Close()
+ body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseSize))
+ if err != nil {
+ return "", err
+ }
+ var data map[string]any
+ if err := json.Unmarshal(body, &data); err != nil {
+ return string(body), nil
+ }
+
+ servers, ok := data["servers"].([]any)
+ if !ok {
+ return string(body), nil
+ }
+
+ var filtered []any
+ for _, s := range servers {
+ if server, ok := s.(map[string]any); ok {
+ if load, ok := server["load"].(float64); ok && load > 7 {
+ filtered = append(filtered, s)
+ }
+ }
+ }
+ data["servers"] = filtered
+
+ result, _ := json.Marshal(data)
+ return string(result), nil
+}
+
+func (s *NordService) SetKey(privateKey string) (string, error) {
+ if privateKey == "" {
+ return "", common.NewError("private key cannot be empty")
+ }
+ nordData := map[string]string{
+ "private_key": privateKey,
+ "token": "",
+ }
+ data, _ := json.Marshal(nordData)
+ err := s.SettingService.SetNord(string(data))
+ if err != nil {
+ return "", err
+ }
+ 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(io.LimitReader(resp.Body, maxResponseSize))
+ 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)
+ err = s.SettingService.SetNord(string(data))
+ if err != nil {
+ return "", err
+ }
+
+ return string(data), nil
+}
+
+func (s *NordService) GetNordData() (string, error) {
+ return s.SettingService.GetNord()
+}
+
+func (s *NordService) DelNordData() error {
+ return s.SettingService.SetNord("")
+}
diff --git a/web/service/setting.go b/web/service/setting.go
index 468a7960..04d8f6a8 100644
--- a/web/service/setting.go
+++ b/web/service/setting.go
@@ -80,6 +80,7 @@ var defaultValueMap = map[string]string{
"subJsonRules": "",
"datepicker": "gregorian",
"warp": "",
+ "nord": "",
"externalTrafficInformEnable": "false",
"externalTrafficInformURI": "",
"xrayOutboundTestUrl": "https://www.google.com/generate_204",
@@ -598,6 +599,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")
}