nord-config-generator 1.0.3__tar.gz → 1.0.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nord_config_generator-1.0.5/PKG-INFO +39 -0
- nord_config_generator-1.0.5/README.md +21 -0
- {nord_config_generator-1.0.3 → nord_config_generator-1.0.5}/pyproject.toml +31 -32
- nord_config_generator-1.0.5/src/nord_config_generator/client.py +53 -0
- nord_config_generator-1.0.5/src/nord_config_generator/generator.py +212 -0
- nord_config_generator-1.0.5/src/nord_config_generator/main.py +101 -0
- nord_config_generator-1.0.5/src/nord_config_generator/models.py +39 -0
- nord_config_generator-1.0.5/src/nord_config_generator/ui.py +118 -0
- nord_config_generator-1.0.5/src/nord_config_generator.egg-info/PKG-INFO +39 -0
- {nord_config_generator-1.0.3 → nord_config_generator-1.0.5}/src/nord_config_generator.egg-info/SOURCES.txt +3 -0
- nord_config_generator-1.0.3/PKG-INFO +0 -90
- nord_config_generator-1.0.3/README.md +0 -72
- nord_config_generator-1.0.3/src/nord_config_generator/main.py +0 -333
- nord_config_generator-1.0.3/src/nord_config_generator/ui.py +0 -73
- nord_config_generator-1.0.3/src/nord_config_generator.egg-info/PKG-INFO +0 -90
- {nord_config_generator-1.0.3 → nord_config_generator-1.0.5}/setup.cfg +0 -0
- {nord_config_generator-1.0.3 → nord_config_generator-1.0.5}/src/nord_config_generator/__init__.py +0 -0
- {nord_config_generator-1.0.3 → nord_config_generator-1.0.5}/src/nord_config_generator.egg-info/dependency_links.txt +0 -0
- {nord_config_generator-1.0.3 → nord_config_generator-1.0.5}/src/nord_config_generator.egg-info/entry_points.txt +0 -0
- {nord_config_generator-1.0.3 → nord_config_generator-1.0.5}/src/nord_config_generator.egg-info/requires.txt +0 -0
- {nord_config_generator-1.0.3 → nord_config_generator-1.0.5}/src/nord_config_generator.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nord-config-generator
|
|
3
|
+
Version: 1.0.5
|
|
4
|
+
Summary: A command-line tool for generating optimized NordVPN WireGuard configurations.
|
|
5
|
+
Author-email: Ahmed Touhami <mustafachyi272@gmail.com>
|
|
6
|
+
License-Expression: GPL-3.0-or-later
|
|
7
|
+
Project-URL: Homepage, https://github.com/mustafachyi/NordVPN-WireGuard-Config-Generator
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/mustafachyi/NordVPN-WireGuard-Config-Generator/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Topic :: System :: Networking
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: aiohttp<4.0,>=3.12.14
|
|
16
|
+
Requires-Dist: aiofiles<25.0,>=24.1.0
|
|
17
|
+
Requires-Dist: rich<15.0,>=14.0.0
|
|
18
|
+
|
|
19
|
+
# NordVPN WireGuard Config Generator
|
|
20
|
+
|
|
21
|
+
A command-line tool for generating optimized NordVPN WireGuard configurations.
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install nord-config-generator
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
Generate configurations:
|
|
32
|
+
```bash
|
|
33
|
+
nordgen
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Retrieve Private Key:
|
|
37
|
+
```bash
|
|
38
|
+
nordgen get-key
|
|
39
|
+
```
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# NordVPN WireGuard Config Generator
|
|
2
|
+
|
|
3
|
+
A command-line tool for generating optimized NordVPN WireGuard configurations.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install nord-config-generator
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
Generate configurations:
|
|
14
|
+
```bash
|
|
15
|
+
nordgen
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Retrieve Private Key:
|
|
19
|
+
```bash
|
|
20
|
+
nordgen get-key
|
|
21
|
+
```
|
|
@@ -1,33 +1,32 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = ["setuptools>=61.0"]
|
|
3
|
-
build-backend = "setuptools.build_meta"
|
|
4
|
-
|
|
5
|
-
[project]
|
|
6
|
-
name = "nord-config-generator"
|
|
7
|
-
version = "1.0.
|
|
8
|
-
authors = [
|
|
9
|
-
{ name="Ahmed Touhami", email="mustafachyi272@gmail.com" },
|
|
10
|
-
]
|
|
11
|
-
description = "A command-line tool for generating optimized NordVPN WireGuard configurations."
|
|
12
|
-
readme = "README.md"
|
|
13
|
-
license =
|
|
14
|
-
requires-python = ">=3.9"
|
|
15
|
-
classifiers = [
|
|
16
|
-
"Programming Language :: Python :: 3",
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
[project.scripts]
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "nord-config-generator"
|
|
7
|
+
version = "1.0.5"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="Ahmed Touhami", email="mustafachyi272@gmail.com" },
|
|
10
|
+
]
|
|
11
|
+
description = "A command-line tool for generating optimized NordVPN WireGuard configurations."
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
license = "GPL-3.0-or-later"
|
|
14
|
+
requires-python = ">=3.9"
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Topic :: System :: Networking",
|
|
19
|
+
"Environment :: Console",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"aiohttp>=3.12.14, <4.0",
|
|
23
|
+
"aiofiles>=24.1.0, <25.0",
|
|
24
|
+
"rich>=14.0.0, <15.0",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
"Homepage" = "https://github.com/mustafachyi/NordVPN-WireGuard-Config-Generator"
|
|
29
|
+
"Bug Tracker" = "https://github.com/mustafachyi/NordVPN-WireGuard-Config-Generator/issues"
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
33
32
|
nordgen = "nord_config_generator.main:cli_entry_point"
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import aiohttp
|
|
2
|
+
import json
|
|
3
|
+
import base64
|
|
4
|
+
from typing import Optional, List, Dict, Tuple
|
|
5
|
+
|
|
6
|
+
class NordClient:
|
|
7
|
+
BASE_URL = "https://api.nordvpn.com/v1"
|
|
8
|
+
GEO_URL = "https://api.nordvpn.com/v1/helpers/ips/insights"
|
|
9
|
+
|
|
10
|
+
def __init__(self):
|
|
11
|
+
self._session: Optional[aiohttp.ClientSession] = None
|
|
12
|
+
|
|
13
|
+
async def __aenter__(self):
|
|
14
|
+
timeout = aiohttp.ClientTimeout(total=20)
|
|
15
|
+
connector = aiohttp.TCPConnector(limit=10, ttl_dns_cache=300)
|
|
16
|
+
self._session = aiohttp.ClientSession(timeout=timeout, connector=connector)
|
|
17
|
+
return self
|
|
18
|
+
|
|
19
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
20
|
+
if self._session:
|
|
21
|
+
await self._session.close()
|
|
22
|
+
|
|
23
|
+
async def get_key(self, token: str) -> Optional[str]:
|
|
24
|
+
if not self._session: return None
|
|
25
|
+
try:
|
|
26
|
+
auth = base64.b64encode(f'token:{token}'.encode()).decode()
|
|
27
|
+
headers = {'Authorization': f'Basic {auth}'}
|
|
28
|
+
async with self._session.get(f"{self.BASE_URL}/users/services/credentials", headers=headers) as resp:
|
|
29
|
+
if resp.status != 200: return None
|
|
30
|
+
data = await resp.json()
|
|
31
|
+
return data.get('nordlynx_private_key')
|
|
32
|
+
except (aiohttp.ClientError, json.JSONDecodeError):
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
async def get_servers(self) -> List[Dict]:
|
|
36
|
+
if not self._session: return []
|
|
37
|
+
try:
|
|
38
|
+
params = {'limit': '16384', 'filters[servers_technologies][identifier]': 'wireguard_udp'}
|
|
39
|
+
async with self._session.get(f"{self.BASE_URL}/servers", params=params) as resp:
|
|
40
|
+
if resp.status != 200: return []
|
|
41
|
+
return await resp.json()
|
|
42
|
+
except (aiohttp.ClientError, json.JSONDecodeError):
|
|
43
|
+
return []
|
|
44
|
+
|
|
45
|
+
async def get_geo(self) -> Tuple[float, float]:
|
|
46
|
+
if not self._session: return 0.0, 0.0
|
|
47
|
+
try:
|
|
48
|
+
async with self._session.get(self.GEO_URL) as resp:
|
|
49
|
+
if resp.status != 200: return 0.0, 0.0
|
|
50
|
+
data = await resp.json()
|
|
51
|
+
return float(data.get('latitude', 0)), float(data.get('longitude', 0))
|
|
52
|
+
except (aiohttp.ClientError, json.JSONDecodeError, ValueError):
|
|
53
|
+
return 0.0, 0.0
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import aiofiles
|
|
4
|
+
from typing import List, Dict, Optional, Set
|
|
5
|
+
from math import radians, sin, cos, asin, sqrt
|
|
6
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from rich.progress import TaskID
|
|
10
|
+
|
|
11
|
+
from .client import NordClient
|
|
12
|
+
from .ui import ConsoleManager
|
|
13
|
+
from .models import Server, UserPreferences, Stats
|
|
14
|
+
|
|
15
|
+
class Generator:
|
|
16
|
+
MIN_VER_MAJOR = 2
|
|
17
|
+
MIN_VER_MINOR = 1
|
|
18
|
+
|
|
19
|
+
def __init__(self, client: NordClient, ui: ConsoleManager):
|
|
20
|
+
self.client = client
|
|
21
|
+
self.ui = ui
|
|
22
|
+
self.stats = Stats()
|
|
23
|
+
self.dir_cache: Set[str] = set()
|
|
24
|
+
self.out_dir = Path(f'nordvpn_configs_{datetime.now().strftime("%Y%m%d_%H%M%S")}')
|
|
25
|
+
self.path_sanitizer = str.maketrans('', '', '<>:"/\\|?*\0')
|
|
26
|
+
|
|
27
|
+
async def process(self, key: str, prefs: UserPreferences):
|
|
28
|
+
self.ui.spin("Fetching data...")
|
|
29
|
+
|
|
30
|
+
geo_task = asyncio.create_task(self.client.get_geo())
|
|
31
|
+
srv_task = asyncio.create_task(self.client.get_servers())
|
|
32
|
+
|
|
33
|
+
lat, lon = await geo_task
|
|
34
|
+
raw_servers = await srv_task
|
|
35
|
+
|
|
36
|
+
if not raw_servers:
|
|
37
|
+
self.ui.fail("Failed to fetch server data")
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
self.ui.success("Data fetched")
|
|
41
|
+
self.ui.spin("Processing dataset...")
|
|
42
|
+
|
|
43
|
+
loop = asyncio.get_running_loop()
|
|
44
|
+
with ThreadPoolExecutor(max_workers=os.cpu_count() or 4) as pool:
|
|
45
|
+
processed = await loop.run_in_executor(pool, self._parse_batch, raw_servers, lat, lon)
|
|
46
|
+
|
|
47
|
+
unique = {s.name: s for s in processed}
|
|
48
|
+
final_list = list(unique.values())
|
|
49
|
+
final_list.sort(key=lambda x: (x.load, x.distance))
|
|
50
|
+
|
|
51
|
+
self.stats.rejected = len(raw_servers) - len(final_list)
|
|
52
|
+
self.stats.total = len(final_list)
|
|
53
|
+
|
|
54
|
+
best_map = {}
|
|
55
|
+
for s in final_list:
|
|
56
|
+
k = (s.country, s.city)
|
|
57
|
+
if k not in best_map or s.load < best_map[k].load:
|
|
58
|
+
best_map[k] = s
|
|
59
|
+
|
|
60
|
+
self.stats.best = len(best_map)
|
|
61
|
+
best_list = list(best_map.values())
|
|
62
|
+
|
|
63
|
+
self.out_dir.mkdir(exist_ok=True)
|
|
64
|
+
self.dir_cache.add(str(self.out_dir))
|
|
65
|
+
|
|
66
|
+
self.ui.start_progress()
|
|
67
|
+
t1 = self.ui.add_task("Standard Configs", self.stats.total)
|
|
68
|
+
t2 = self.ui.add_task("Optimized Configs", self.stats.best)
|
|
69
|
+
|
|
70
|
+
await asyncio.gather(
|
|
71
|
+
self._write_batch(final_list, "configs", t1, key, prefs),
|
|
72
|
+
self._write_batch(best_list, "best_configs", t2, key, prefs)
|
|
73
|
+
)
|
|
74
|
+
self.ui.stop_progress()
|
|
75
|
+
|
|
76
|
+
return str(self.out_dir)
|
|
77
|
+
|
|
78
|
+
def _parse_batch(self, raw: List[Dict], lat: float, lon: float) -> List[Server]:
|
|
79
|
+
results = []
|
|
80
|
+
for d in raw:
|
|
81
|
+
s = self._parse_one(d, lat, lon)
|
|
82
|
+
if s:
|
|
83
|
+
results.append(s)
|
|
84
|
+
return results
|
|
85
|
+
|
|
86
|
+
def _parse_one(self, data: Dict, lat: float, lon: float) -> Optional[Server]:
|
|
87
|
+
try:
|
|
88
|
+
locs = data.get('locations')
|
|
89
|
+
if not locs: return None
|
|
90
|
+
|
|
91
|
+
ver_str = "0.0.0"
|
|
92
|
+
for s in data.get('specifications', []):
|
|
93
|
+
if s.get('identifier') == 'version':
|
|
94
|
+
vals = s.get('values')
|
|
95
|
+
if vals: ver_str = vals[0].get('value', "0.0.0")
|
|
96
|
+
break
|
|
97
|
+
|
|
98
|
+
if not self._check_version(ver_str):
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
pk = ""
|
|
102
|
+
for t in data.get('technologies', []):
|
|
103
|
+
if t.get('identifier') == 'wireguard_udp':
|
|
104
|
+
for m in t.get('metadata', []):
|
|
105
|
+
if m.get('name') == 'public_key':
|
|
106
|
+
pk = m.get('value')
|
|
107
|
+
break
|
|
108
|
+
if not pk: return None
|
|
109
|
+
|
|
110
|
+
loc = locs[0]
|
|
111
|
+
d = self._haversine(lat, lon, loc['latitude'], loc['longitude'])
|
|
112
|
+
|
|
113
|
+
return Server(
|
|
114
|
+
name=data['name'],
|
|
115
|
+
hostname=data['hostname'],
|
|
116
|
+
station=data['station'],
|
|
117
|
+
load=int(data.get('load', 0)),
|
|
118
|
+
country=loc['country']['name'],
|
|
119
|
+
country_code=loc['country']['code'].lower(),
|
|
120
|
+
city=loc['country']['city']['name'],
|
|
121
|
+
latitude=loc['latitude'],
|
|
122
|
+
longitude=loc['longitude'],
|
|
123
|
+
public_key=pk,
|
|
124
|
+
distance=d
|
|
125
|
+
)
|
|
126
|
+
except (KeyError, IndexError, ValueError, TypeError):
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
def _check_version(self, v: str) -> bool:
|
|
130
|
+
if len(v) < 3: return False
|
|
131
|
+
try:
|
|
132
|
+
parts = v.split('.')
|
|
133
|
+
if len(parts) < 2: return False
|
|
134
|
+
maj = int(parts[0])
|
|
135
|
+
if maj > 2: return True
|
|
136
|
+
return maj == 2 and int(parts[1]) >= 1
|
|
137
|
+
except ValueError:
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
def _haversine(self, lat1: float, lon1: float, lat2: float, lon2: float) -> float:
|
|
141
|
+
R = 6371
|
|
142
|
+
dlat = radians(lat2 - lat1)
|
|
143
|
+
dlon = radians(lon2 - lon1)
|
|
144
|
+
a = sin(dlat/2)**2 + cos(radians(lat1)) * cos(radians(lat2)) * sin(dlon/2)**2
|
|
145
|
+
c = 2 * asin(sqrt(a))
|
|
146
|
+
return R * c
|
|
147
|
+
|
|
148
|
+
async def _write_batch(self, servers: List[Server], sub: str, task_id: TaskID, key: str, prefs: UserPreferences):
|
|
149
|
+
sem = asyncio.Semaphore(200)
|
|
150
|
+
path_counts: Dict[str, int] = {}
|
|
151
|
+
|
|
152
|
+
async def _write(s: Server):
|
|
153
|
+
async with sem:
|
|
154
|
+
country = self._sanitize(s.country)
|
|
155
|
+
city = self._sanitize(s.city)
|
|
156
|
+
base = self._basename(s)
|
|
157
|
+
rel = f"{sub}/{country}/{city}/{base}"
|
|
158
|
+
|
|
159
|
+
count = path_counts.get(rel, 0)
|
|
160
|
+
path_counts[rel] = count + 1
|
|
161
|
+
|
|
162
|
+
fname = base
|
|
163
|
+
if count > 0:
|
|
164
|
+
raw_base = base[:-5]
|
|
165
|
+
fname = f"{raw_base}_{count}.conf"
|
|
166
|
+
|
|
167
|
+
full_dir = self.out_dir / sub / country / city
|
|
168
|
+
self._ensure_dir(full_dir)
|
|
169
|
+
|
|
170
|
+
ep = s.station if prefs.use_ip else s.hostname
|
|
171
|
+
cfg = (
|
|
172
|
+
f"[Interface]\nPrivateKey = {key}\nAddress = 10.5.0.2/16\nDNS = {prefs.dns}\n\n"
|
|
173
|
+
f"[Peer]\nPublicKey = {s.public_key}\nAllowedIPs = 0.0.0.0/0, ::/0\n"
|
|
174
|
+
f"Endpoint = {ep}:51820\nPersistentKeepalive = {prefs.keepalive}"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
async with aiofiles.open(full_dir / fname, 'w') as f:
|
|
178
|
+
await f.write(cfg)
|
|
179
|
+
self.ui.update_progress(task_id)
|
|
180
|
+
|
|
181
|
+
tasks = [_write(s) for s in servers]
|
|
182
|
+
await asyncio.gather(*tasks)
|
|
183
|
+
|
|
184
|
+
def _ensure_dir(self, path: Path):
|
|
185
|
+
s_path = str(path)
|
|
186
|
+
if s_path in self.dir_cache: return
|
|
187
|
+
try:
|
|
188
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
189
|
+
self.dir_cache.add(s_path)
|
|
190
|
+
except OSError:
|
|
191
|
+
pass
|
|
192
|
+
|
|
193
|
+
def _sanitize(self, s: str) -> str:
|
|
194
|
+
return s.lower().replace(' ', '_').translate(self.path_sanitizer)
|
|
195
|
+
|
|
196
|
+
def _basename(self, s: Server) -> str:
|
|
197
|
+
name = s.name
|
|
198
|
+
num = ""
|
|
199
|
+
for i in range(len(name)-1, -1, -1):
|
|
200
|
+
if name[i].isdigit():
|
|
201
|
+
start = i
|
|
202
|
+
while start >= 0 and name[start].isdigit():
|
|
203
|
+
start -= 1
|
|
204
|
+
num = name[start+1:i+1]
|
|
205
|
+
break
|
|
206
|
+
|
|
207
|
+
if not num:
|
|
208
|
+
fallback = f"wg{s.station.replace('.', '')}"
|
|
209
|
+
return f"{fallback[:15]}.conf"
|
|
210
|
+
|
|
211
|
+
base = f"{s.country_code}{num}"
|
|
212
|
+
return f"{base[:15]}.conf"
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import asyncio
|
|
3
|
+
import argparse
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
from .ui import ConsoleManager
|
|
7
|
+
from .client import NordClient
|
|
8
|
+
from .generator import Generator
|
|
9
|
+
from .models import UserPreferences
|
|
10
|
+
|
|
11
|
+
async def main():
|
|
12
|
+
ui = ConsoleManager()
|
|
13
|
+
async with NordClient() as client:
|
|
14
|
+
if len(sys.argv) > 1 and sys.argv[1] == "get-key":
|
|
15
|
+
await run_get_key(ui, client)
|
|
16
|
+
else:
|
|
17
|
+
await run_generate(ui, client)
|
|
18
|
+
|
|
19
|
+
async def run_get_key(ui: ConsoleManager, client: NordClient):
|
|
20
|
+
parser = argparse.ArgumentParser(prog="nordgen get-key", description="Retrieve Private Key")
|
|
21
|
+
parser.add_argument('-t', '--token', help='NordVPN Access Token')
|
|
22
|
+
|
|
23
|
+
args = parser.parse_args(sys.argv[2:])
|
|
24
|
+
|
|
25
|
+
ui.clear()
|
|
26
|
+
ui.header()
|
|
27
|
+
|
|
28
|
+
key = await resolve_key(ui, client, args.token)
|
|
29
|
+
if key:
|
|
30
|
+
ui.show_key(key)
|
|
31
|
+
ui.wait()
|
|
32
|
+
|
|
33
|
+
async def run_generate(ui: ConsoleManager, client: NordClient):
|
|
34
|
+
parser = argparse.ArgumentParser(prog="nordgen", description="NordVPN WireGuard Config Generator")
|
|
35
|
+
parser.add_argument('-t', '--token', help='NordVPN Access Token')
|
|
36
|
+
parser.add_argument('-d', '--dns', default='103.86.96.100', help='DNS Server')
|
|
37
|
+
parser.add_argument('-i', '--ip', action='store_true', help='Use IP Endpoint')
|
|
38
|
+
parser.add_argument('-k', '--keepalive', type=int, default=25, help='Persistent Keepalive')
|
|
39
|
+
|
|
40
|
+
args = parser.parse_args(sys.argv[1:])
|
|
41
|
+
|
|
42
|
+
ui.clear()
|
|
43
|
+
ui.header()
|
|
44
|
+
|
|
45
|
+
prefs = UserPreferences(args.dns, args.ip, args.keepalive)
|
|
46
|
+
key = ""
|
|
47
|
+
|
|
48
|
+
if not args.token:
|
|
49
|
+
key = await resolve_key(ui, client, "")
|
|
50
|
+
if not key:
|
|
51
|
+
ui.wait()
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
ui.clear()
|
|
55
|
+
ui.header()
|
|
56
|
+
prefs = ui.prompt_prefs(prefs)
|
|
57
|
+
else:
|
|
58
|
+
key = await resolve_key(ui, client, args.token)
|
|
59
|
+
if not key:
|
|
60
|
+
ui.wait()
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
ui.clear()
|
|
64
|
+
ui.header()
|
|
65
|
+
|
|
66
|
+
gen = Generator(client, ui)
|
|
67
|
+
start = time.time()
|
|
68
|
+
out = await gen.process(key, prefs)
|
|
69
|
+
|
|
70
|
+
if out:
|
|
71
|
+
ui.clear()
|
|
72
|
+
ui.header()
|
|
73
|
+
ui.summary(out, gen.stats, time.time() - start)
|
|
74
|
+
|
|
75
|
+
ui.wait()
|
|
76
|
+
|
|
77
|
+
async def resolve_key(ui: ConsoleManager, client: NordClient, token: str) -> str:
|
|
78
|
+
if not token:
|
|
79
|
+
token = ui.prompt_secret("Please enter your NordVPN access token: ")
|
|
80
|
+
|
|
81
|
+
if len(token) != 64:
|
|
82
|
+
ui.error("Invalid token format")
|
|
83
|
+
return ""
|
|
84
|
+
|
|
85
|
+
ui.spin("Validating token...")
|
|
86
|
+
key = await client.get_key(token)
|
|
87
|
+
if not key:
|
|
88
|
+
ui.fail("Token invalid")
|
|
89
|
+
return ""
|
|
90
|
+
|
|
91
|
+
ui.success("Token validated")
|
|
92
|
+
return key
|
|
93
|
+
|
|
94
|
+
def cli_entry_point():
|
|
95
|
+
try:
|
|
96
|
+
asyncio.run(main())
|
|
97
|
+
except KeyboardInterrupt:
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
if __name__ == "__main__":
|
|
101
|
+
cli_entry_point()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
class Server:
|
|
2
|
+
__slots__ = (
|
|
3
|
+
'name', 'hostname', 'station', 'load', 'country',
|
|
4
|
+
'country_code', 'city', 'latitude', 'longitude',
|
|
5
|
+
'public_key', 'distance'
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
def __init__(
|
|
9
|
+
self, name: str, hostname: str, station: str, load: int,
|
|
10
|
+
country: str, country_code: str, city: str,
|
|
11
|
+
latitude: float, longitude: float, public_key: str, distance: float
|
|
12
|
+
):
|
|
13
|
+
self.name = name
|
|
14
|
+
self.hostname = hostname
|
|
15
|
+
self.station = station
|
|
16
|
+
self.load = load
|
|
17
|
+
self.country = country
|
|
18
|
+
self.country_code = country_code
|
|
19
|
+
self.city = city
|
|
20
|
+
self.latitude = latitude
|
|
21
|
+
self.longitude = longitude
|
|
22
|
+
self.public_key = public_key
|
|
23
|
+
self.distance = distance
|
|
24
|
+
|
|
25
|
+
class UserPreferences:
|
|
26
|
+
__slots__ = ('dns', 'use_ip', 'keepalive')
|
|
27
|
+
|
|
28
|
+
def __init__(self, dns: str = "103.86.96.100", use_ip: bool = False, keepalive: int = 25):
|
|
29
|
+
self.dns = dns
|
|
30
|
+
self.use_ip = use_ip
|
|
31
|
+
self.keepalive = keepalive
|
|
32
|
+
|
|
33
|
+
class Stats:
|
|
34
|
+
__slots__ = ('total', 'best', 'rejected')
|
|
35
|
+
|
|
36
|
+
def __init__(self):
|
|
37
|
+
self.total = 0
|
|
38
|
+
self.best = 0
|
|
39
|
+
self.rejected = 0
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.panel import Panel
|
|
5
|
+
from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn, TaskID
|
|
6
|
+
from rich.theme import Theme
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from .models import UserPreferences, Stats
|
|
11
|
+
|
|
12
|
+
class ConsoleManager:
|
|
13
|
+
def __init__(self):
|
|
14
|
+
theme = Theme({
|
|
15
|
+
"info": "cyan",
|
|
16
|
+
"success": "bold green",
|
|
17
|
+
"warning": "yellow",
|
|
18
|
+
"error": "bold red",
|
|
19
|
+
"title": "bold magenta",
|
|
20
|
+
})
|
|
21
|
+
self.console = Console(theme=theme)
|
|
22
|
+
self.progress = None
|
|
23
|
+
self.task_ids = {}
|
|
24
|
+
|
|
25
|
+
def clear(self):
|
|
26
|
+
os.system('cls' if os.name == 'nt' else 'clear')
|
|
27
|
+
|
|
28
|
+
def header(self):
|
|
29
|
+
self.console.print(Panel(
|
|
30
|
+
"[title]NordVPN Configuration Generator[/title]",
|
|
31
|
+
expand=False,
|
|
32
|
+
border_style="cyan",
|
|
33
|
+
padding=(0, 2)
|
|
34
|
+
))
|
|
35
|
+
|
|
36
|
+
def prompt_secret(self, msg: str) -> str:
|
|
37
|
+
return self.console.input(f"[cyan]{msg}[/cyan]", password=True).strip()
|
|
38
|
+
|
|
39
|
+
def prompt_prefs(self, defaults: "UserPreferences") -> "UserPreferences":
|
|
40
|
+
from .models import UserPreferences
|
|
41
|
+
self.console.print("[info]Configuration Options (Enter for default)[/info]")
|
|
42
|
+
|
|
43
|
+
d_in = self.console.input(f"DNS IP (default: {defaults.dns}): ").strip()
|
|
44
|
+
dns = d_in if d_in else defaults.dns
|
|
45
|
+
|
|
46
|
+
i_in = self.console.input("Use IP for endpoints? (y/N): ").strip().lower()
|
|
47
|
+
use_ip = i_in == 'y'
|
|
48
|
+
|
|
49
|
+
k_in = self.console.input(f"PersistentKeepalive (default: {defaults.keepalive}): ").strip()
|
|
50
|
+
ka = int(k_in) if k_in.isdigit() else defaults.keepalive
|
|
51
|
+
|
|
52
|
+
return UserPreferences(dns, use_ip, ka)
|
|
53
|
+
|
|
54
|
+
def spin(self, msg: str):
|
|
55
|
+
with self.console.status(f"[cyan]{msg}"):
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
def success(self, msg: str):
|
|
59
|
+
self.console.print(f"[success]{msg}[/success]")
|
|
60
|
+
|
|
61
|
+
def fail(self, msg: str):
|
|
62
|
+
self.console.print(f"[error]{msg}[/error]")
|
|
63
|
+
|
|
64
|
+
def error(self, msg: str):
|
|
65
|
+
self.console.print(f"[error]{msg}[/error]")
|
|
66
|
+
|
|
67
|
+
def show_key(self, key: str):
|
|
68
|
+
self.console.print(Panel(
|
|
69
|
+
f"[green]{key}[/green]",
|
|
70
|
+
title="NordLynx Private Key",
|
|
71
|
+
border_style="green",
|
|
72
|
+
expand=False
|
|
73
|
+
))
|
|
74
|
+
|
|
75
|
+
def summary(self, path: str, stats: "Stats", sec: float):
|
|
76
|
+
grid = Table.grid(padding=(0, 2))
|
|
77
|
+
grid.add_column(style="cyan")
|
|
78
|
+
grid.add_column()
|
|
79
|
+
grid.add_row("Output Directory:", path)
|
|
80
|
+
grid.add_row("Standard Configs:", str(stats.total))
|
|
81
|
+
grid.add_row("Optimized Configs:", str(stats.best))
|
|
82
|
+
grid.add_row("Incompatible:", f"[yellow]{stats.rejected}[/yellow]")
|
|
83
|
+
grid.add_row("Duration:", f"{sec:.2f}s")
|
|
84
|
+
|
|
85
|
+
self.console.print(Panel(
|
|
86
|
+
grid,
|
|
87
|
+
title="Complete",
|
|
88
|
+
border_style="green",
|
|
89
|
+
expand=False
|
|
90
|
+
))
|
|
91
|
+
|
|
92
|
+
def start_progress(self):
|
|
93
|
+
self.progress = Progress(
|
|
94
|
+
SpinnerColumn(),
|
|
95
|
+
TextColumn("[progress.description]{task.description}"),
|
|
96
|
+
BarColumn(),
|
|
97
|
+
TextColumn("{task.completed}/{task.total}"),
|
|
98
|
+
console=self.console,
|
|
99
|
+
transient=False
|
|
100
|
+
)
|
|
101
|
+
self.progress.start()
|
|
102
|
+
|
|
103
|
+
def add_task(self, name: str, total: int) -> TaskID:
|
|
104
|
+
if self.progress:
|
|
105
|
+
return self.progress.add_task(name, total=total)
|
|
106
|
+
return TaskID(0)
|
|
107
|
+
|
|
108
|
+
def update_progress(self, task_id: TaskID):
|
|
109
|
+
if self.progress:
|
|
110
|
+
self.progress.update(task_id, advance=1)
|
|
111
|
+
|
|
112
|
+
def stop_progress(self):
|
|
113
|
+
if self.progress:
|
|
114
|
+
self.progress.stop()
|
|
115
|
+
|
|
116
|
+
def wait(self):
|
|
117
|
+
self.console.print()
|
|
118
|
+
self.console.input("[info]Press Enter to exit...[/info]")
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nord-config-generator
|
|
3
|
+
Version: 1.0.5
|
|
4
|
+
Summary: A command-line tool for generating optimized NordVPN WireGuard configurations.
|
|
5
|
+
Author-email: Ahmed Touhami <mustafachyi272@gmail.com>
|
|
6
|
+
License-Expression: GPL-3.0-or-later
|
|
7
|
+
Project-URL: Homepage, https://github.com/mustafachyi/NordVPN-WireGuard-Config-Generator
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/mustafachyi/NordVPN-WireGuard-Config-Generator/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Topic :: System :: Networking
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: aiohttp<4.0,>=3.12.14
|
|
16
|
+
Requires-Dist: aiofiles<25.0,>=24.1.0
|
|
17
|
+
Requires-Dist: rich<15.0,>=14.0.0
|
|
18
|
+
|
|
19
|
+
# NordVPN WireGuard Config Generator
|
|
20
|
+
|
|
21
|
+
A command-line tool for generating optimized NordVPN WireGuard configurations.
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install nord-config-generator
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
Generate configurations:
|
|
32
|
+
```bash
|
|
33
|
+
nordgen
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Retrieve Private Key:
|
|
37
|
+
```bash
|
|
38
|
+
nordgen get-key
|
|
39
|
+
```
|