nord-config-generator 1.0.3__py3-none-any.whl → 1.0.5__py3-none-any.whl
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/client.py +53 -0
- nord_config_generator/generator.py +212 -0
- nord_config_generator/main.py +100 -332
- nord_config_generator/models.py +39 -0
- nord_config_generator/ui.py +118 -73
- nord_config_generator-1.0.5.dist-info/METADATA +39 -0
- nord_config_generator-1.0.5.dist-info/RECORD +11 -0
- {nord_config_generator-1.0.3.dist-info → nord_config_generator-1.0.5.dist-info}/WHEEL +1 -1
- nord_config_generator-1.0.3.dist-info/METADATA +0 -90
- nord_config_generator-1.0.3.dist-info/RECORD +0 -8
- {nord_config_generator-1.0.3.dist-info → nord_config_generator-1.0.5.dist-info}/entry_points.txt +0 -0
- {nord_config_generator-1.0.3.dist-info → nord_config_generator-1.0.5.dist-info}/top_level.txt +0 -0
|
@@ -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"
|
nord_config_generator/main.py
CHANGED
|
@@ -1,333 +1,101 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
from
|
|
9
|
-
from
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
def __init__(self, private_key: str, preferences: UserPreferences, console_manager: ConsoleManager, api_client: NordVpnApiClient):
|
|
103
|
-
self._private_key = private_key
|
|
104
|
-
self._preferences = preferences
|
|
105
|
-
self._console = console_manager
|
|
106
|
-
self._api_client = api_client
|
|
107
|
-
self._output_dir = Path(f'nordvpn_configs_{datetime.now().strftime("%Y%m%d_%H%M%S")}')
|
|
108
|
-
self._semaphore = asyncio.Semaphore(self.CONCURRENT_LIMIT)
|
|
109
|
-
self.stats = GenerationStats()
|
|
110
|
-
|
|
111
|
-
async def generate(self) -> Optional[Path]:
|
|
112
|
-
user_location, all_servers_data = await self._fetch_remote_data()
|
|
113
|
-
if not user_location or not all_servers_data:
|
|
114
|
-
return None
|
|
115
|
-
|
|
116
|
-
processed_servers = await self._process_server_data(all_servers_data, user_location)
|
|
117
|
-
|
|
118
|
-
sorted_servers = sorted(processed_servers, key=lambda s: (s.load, s.distance))
|
|
119
|
-
best_servers_by_location = self._get_best_servers(sorted_servers)
|
|
120
|
-
|
|
121
|
-
self._output_dir.mkdir(exist_ok=True)
|
|
122
|
-
servers_info = self._build_servers_info(sorted_servers)
|
|
123
|
-
|
|
124
|
-
await self._save_all_configurations(sorted_servers, best_servers_by_location, servers_info)
|
|
125
|
-
return self._output_dir
|
|
126
|
-
|
|
127
|
-
async def _fetch_remote_data(self) -> Tuple[Optional[Tuple[float, float]], List[Dict[str, Any]]]:
|
|
128
|
-
with self._console.create_progress_bar() as progress:
|
|
129
|
-
task = progress.add_task("Fetching remote data...", total=2)
|
|
130
|
-
user_location, all_servers_data = await asyncio.gather(
|
|
131
|
-
self._api_client.get_user_geolocation(),
|
|
132
|
-
self._api_client.get_all_servers()
|
|
133
|
-
)
|
|
134
|
-
progress.update(task, advance=2)
|
|
135
|
-
return user_location, all_servers_data
|
|
136
|
-
|
|
137
|
-
async def _process_server_data(self, all_servers_data: List[Dict[str, Any]], user_location: Tuple[float, float]) -> List[Server]:
|
|
138
|
-
loop = asyncio.get_running_loop()
|
|
139
|
-
parse_func = partial(self._parse_server_data, user_location=user_location)
|
|
140
|
-
with ThreadPoolExecutor(max_workers=min(32, (os.cpu_count() or 1) + 4)) as executor:
|
|
141
|
-
tasks = [loop.run_in_executor(executor, parse_func, s) for s in all_servers_data]
|
|
142
|
-
processed_servers = await asyncio.gather(*tasks)
|
|
143
|
-
return [server for server in processed_servers if server]
|
|
144
|
-
|
|
145
|
-
def _get_best_servers(self, sorted_servers: List[Server]) -> Dict[Tuple[str, str], Server]:
|
|
146
|
-
best = {}
|
|
147
|
-
for server in sorted_servers:
|
|
148
|
-
key = (server.country, server.city)
|
|
149
|
-
if key not in best or server.load < best[key].load:
|
|
150
|
-
best[key] = server
|
|
151
|
-
return best
|
|
152
|
-
|
|
153
|
-
def _build_servers_info(self, sorted_servers: List[Server]) -> Dict:
|
|
154
|
-
info = {}
|
|
155
|
-
for server in sorted_servers:
|
|
156
|
-
country_info = info.setdefault(server.country, {})
|
|
157
|
-
city_info = country_info.setdefault(server.city, {"distance": int(server.distance), "servers": []})
|
|
158
|
-
city_info["servers"].append((server.name, server.load))
|
|
159
|
-
return info
|
|
160
|
-
|
|
161
|
-
async def _save_all_configurations(self, sorted_servers: List[Server], best_servers: Dict, servers_info: Dict):
|
|
162
|
-
with self._console.create_progress_bar(transient=False) as progress:
|
|
163
|
-
self.stats.total_configs = len(sorted_servers)
|
|
164
|
-
self.stats.best_configs = len(best_servers)
|
|
165
|
-
|
|
166
|
-
task_all = progress.add_task("Generating standard configs...", total=self.stats.total_configs)
|
|
167
|
-
task_best = progress.add_task("Generating optimized configs...", total=self.stats.best_configs)
|
|
168
|
-
|
|
169
|
-
save_tasks = [self._create_save_task(s, 'configs', progress, task_all) for s in sorted_servers]
|
|
170
|
-
save_tasks.extend([self._create_save_task(s, 'best_configs', progress, task_best) for s in best_servers.values()])
|
|
171
|
-
|
|
172
|
-
await asyncio.gather(*save_tasks)
|
|
173
|
-
async with aiofiles.open(self._output_dir / 'servers.json', 'w') as f:
|
|
174
|
-
await f.write(json.dumps(servers_info, indent=2, separators=(',', ':'), ensure_ascii=False))
|
|
175
|
-
|
|
176
|
-
def _create_save_task(self, server: Server, subfolder: str, progress, task_id):
|
|
177
|
-
config_str = self._generate_wireguard_config_string(server, self._preferences, self._private_key)
|
|
178
|
-
path = self._output_dir / subfolder / self._sanitize_path_part(server.country) / self._sanitize_path_part(server.city)
|
|
179
|
-
filename = self._generate_compliant_filename(server)
|
|
180
|
-
return self._save_config_file(config_str, path, filename, progress, task_id)
|
|
181
|
-
|
|
182
|
-
async def _save_config_file(self, config_string: str, path: Path, filename: str, progress, task_id):
|
|
183
|
-
path.mkdir(parents=True, exist_ok=True)
|
|
184
|
-
async with self._semaphore:
|
|
185
|
-
async with aiofiles.open(path / filename, 'w') as f:
|
|
186
|
-
await f.write(config_string)
|
|
187
|
-
progress.update(task_id, advance=1)
|
|
188
|
-
|
|
189
|
-
@staticmethod
|
|
190
|
-
def _generate_compliant_filename(server: Server) -> str:
|
|
191
|
-
server_number_match = re.search(r'\d+$', server.name)
|
|
192
|
-
if not server_number_match:
|
|
193
|
-
fallback_name = f"wg{server.station.replace('.', '')}"
|
|
194
|
-
return f"{fallback_name[:15]}.conf"
|
|
195
|
-
|
|
196
|
-
server_number = server_number_match.group(0)
|
|
197
|
-
base_name = f"{server.country_code}{server_number}"
|
|
198
|
-
return f"{base_name[:15]}.conf"
|
|
199
|
-
|
|
200
|
-
@staticmethod
|
|
201
|
-
def _generate_wireguard_config_string(server: Server, preferences: UserPreferences, private_key: str) -> str:
|
|
202
|
-
endpoint = server.station if preferences.use_ip_for_endpoint else server.hostname
|
|
203
|
-
return f"[Interface]\nPrivateKey = {private_key}\nAddress = 10.5.0.2/16\nDNS = {preferences.dns}\n\n[Peer]\nPublicKey = {server.public_key}\nAllowedIPs = 0.0.0.0/0, ::/0\nEndpoint = {endpoint}:51820\nPersistentKeepalive = {preferences.persistent_keepalive}"
|
|
204
|
-
|
|
205
|
-
@staticmethod
|
|
206
|
-
def _parse_server_data(server_data: Dict[str, Any], user_location: Tuple[float, float]) -> Optional[Server]:
|
|
207
|
-
try:
|
|
208
|
-
location = server_data['locations'][0]
|
|
209
|
-
country_info = location['country']
|
|
210
|
-
|
|
211
|
-
public_key = next(
|
|
212
|
-
m['value'] for t in server_data['technologies']
|
|
213
|
-
if t['identifier'] == 'wireguard_udp'
|
|
214
|
-
for m in t['metadata'] if m['name'] == 'public_key'
|
|
215
|
-
)
|
|
216
|
-
distance = ConfigurationOrchestrator._calculate_distance(
|
|
217
|
-
user_location[0], user_location[1], location['latitude'], location['longitude']
|
|
218
|
-
)
|
|
219
|
-
return Server(
|
|
220
|
-
name=server_data['name'], hostname=server_data['hostname'],
|
|
221
|
-
station=server_data['station'], load=int(server_data.get('load', 0)),
|
|
222
|
-
country=country_info['name'], country_code=country_info['code'].lower(),
|
|
223
|
-
city=country_info.get('city', {}).get('name', 'Unknown'),
|
|
224
|
-
latitude=location['latitude'], longitude=location['longitude'],
|
|
225
|
-
public_key=public_key, distance=distance
|
|
226
|
-
)
|
|
227
|
-
except (KeyError, IndexError, StopIteration):
|
|
228
|
-
return None
|
|
229
|
-
|
|
230
|
-
@staticmethod
|
|
231
|
-
def _calculate_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
|
|
232
|
-
lon1_rad, lat1_rad, lon2_rad, lat2_rad = map(radians, [lon1, lat1, lon2, lat2])
|
|
233
|
-
dlon = lon2_rad - lon1_rad
|
|
234
|
-
dlat = lat2_rad - lat1_rad
|
|
235
|
-
a = sin(dlat / 2)**2 + cos(lat1_rad) * cos(lat2_rad) * sin(dlon / 2)**2
|
|
236
|
-
c = 2 * asin(sqrt(a))
|
|
237
|
-
return c * 6371
|
|
238
|
-
|
|
239
|
-
@staticmethod
|
|
240
|
-
def _sanitize_path_part(part: str) -> str:
|
|
241
|
-
return re.sub(r'[<>:"/\\|?*\0]', '', part.lower().replace(' ', '_')).replace('#', '')
|
|
242
|
-
|
|
243
|
-
class Application:
|
|
244
|
-
def __init__(self):
|
|
245
|
-
self._console = ConsoleManager()
|
|
246
|
-
|
|
247
|
-
async def run(self, args: List[str]):
|
|
248
|
-
async with NordVpnApiClient(self._console) as api_client:
|
|
249
|
-
try:
|
|
250
|
-
if not args:
|
|
251
|
-
await self._run_generate_command(api_client)
|
|
252
|
-
elif args[0] == "get-key" and len(args) == 1:
|
|
253
|
-
await self._run_get_key_command(api_client)
|
|
254
|
-
else:
|
|
255
|
-
command = " ".join(args)
|
|
256
|
-
self._console.print_message("error", f"Unknown command or invalid arguments: '{command}'.")
|
|
257
|
-
self._console.print_message("info", "Usage: nordgen | nordgen get-key")
|
|
258
|
-
except Exception as e:
|
|
259
|
-
self._console.print_message("error", f"An unrecoverable error occurred: {e}")
|
|
260
|
-
|
|
261
|
-
async def _run_get_key_command(self, api_client: NordVpnApiClient):
|
|
262
|
-
self._console.clear()
|
|
263
|
-
self._console.print_title()
|
|
264
|
-
private_key = await self._get_validated_private_key(api_client)
|
|
265
|
-
if private_key:
|
|
266
|
-
self._console.display_key(private_key)
|
|
267
|
-
|
|
268
|
-
async def _run_generate_command(self, api_client: NordVpnApiClient):
|
|
269
|
-
self._console.clear()
|
|
270
|
-
self._console.print_title()
|
|
271
|
-
private_key = await self._get_validated_private_key(api_client)
|
|
272
|
-
if not private_key:
|
|
273
|
-
return
|
|
274
|
-
|
|
275
|
-
preferences = self._collect_user_preferences()
|
|
276
|
-
|
|
277
|
-
self._console.clear()
|
|
278
|
-
|
|
279
|
-
start_time = time.time()
|
|
280
|
-
orchestrator = ConfigurationOrchestrator(private_key, preferences, self._console, api_client)
|
|
281
|
-
output_dir = await orchestrator.generate()
|
|
282
|
-
elapsed_time = time.time() - start_time
|
|
283
|
-
|
|
284
|
-
if output_dir:
|
|
285
|
-
self._console.display_summary(output_dir, orchestrator.stats, elapsed_time)
|
|
286
|
-
else:
|
|
287
|
-
self._console.print_message("error", "Process failed. Check logs for details.")
|
|
288
|
-
|
|
289
|
-
def _collect_user_preferences(self) -> UserPreferences:
|
|
290
|
-
defaults = UserPreferences()
|
|
291
|
-
user_input = self._console.get_preferences(defaults)
|
|
292
|
-
|
|
293
|
-
dns_input = user_input.get("dns")
|
|
294
|
-
dns = dns_input if dns_input and re.match(r'^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$', dns_input) else defaults.dns
|
|
295
|
-
|
|
296
|
-
use_ip = user_input.get("endpoint_type", "").lower() == 'y'
|
|
297
|
-
|
|
298
|
-
keepalive = defaults.persistent_keepalive
|
|
299
|
-
keepalive_input = user_input.get("keepalive")
|
|
300
|
-
if keepalive_input and keepalive_input.isdigit():
|
|
301
|
-
keepalive_val = int(keepalive_input)
|
|
302
|
-
if 15 <= keepalive_val <= 120:
|
|
303
|
-
keepalive = keepalive_val
|
|
304
|
-
|
|
305
|
-
return UserPreferences(dns=dns, use_ip_for_endpoint=use_ip, persistent_keepalive=keepalive)
|
|
306
|
-
|
|
307
|
-
async def _get_validated_private_key(self, api_client: NordVpnApiClient) -> Optional[str]:
|
|
308
|
-
token = self._console.get_user_input("Please enter your NordVPN access token: ", is_secret=True)
|
|
309
|
-
if not re.match(r'^[a-fA-F0-9]{64}$', token):
|
|
310
|
-
self._console.print_message("error", "Invalid token format.")
|
|
311
|
-
return None
|
|
312
|
-
|
|
313
|
-
with self._console.create_progress_bar() as progress:
|
|
314
|
-
task = progress.add_task("Validating token...", total=1)
|
|
315
|
-
private_key = await api_client.get_private_key(token)
|
|
316
|
-
progress.update(task, advance=1)
|
|
317
|
-
|
|
318
|
-
if private_key:
|
|
319
|
-
self._console.print_message("success", "Token validated successfully.")
|
|
320
|
-
return private_key
|
|
321
|
-
else:
|
|
322
|
-
self._console.print_message("error", "Token is invalid or could not be verified.")
|
|
323
|
-
return None
|
|
324
|
-
|
|
325
|
-
def cli_entry_point():
|
|
326
|
-
try:
|
|
327
|
-
app = Application()
|
|
328
|
-
asyncio.run(app.run(sys.argv[1:]))
|
|
329
|
-
except KeyboardInterrupt:
|
|
330
|
-
print("\nProcess interrupted by user.")
|
|
331
|
-
|
|
332
|
-
if __name__ == "__main__":
|
|
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__":
|
|
333
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
|
nord_config_generator/ui.py
CHANGED
|
@@ -1,73 +1,118 @@
|
|
|
1
|
-
|
|
2
|
-
from
|
|
3
|
-
from rich.
|
|
4
|
-
from rich.
|
|
5
|
-
from rich.
|
|
6
|
-
from
|
|
7
|
-
from
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
self.
|
|
24
|
-
|
|
25
|
-
def clear(self):
|
|
26
|
-
os.system('cls' if os.name == 'nt' else 'clear')
|
|
27
|
-
|
|
28
|
-
def
|
|
29
|
-
self.console.print(Panel(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
self.console.print(Panel(
|
|
69
|
-
|
|
70
|
-
title="
|
|
71
|
-
border_style="
|
|
72
|
-
expand=False
|
|
73
|
-
))
|
|
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
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
nord_config_generator/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
nord_config_generator/client.py,sha256=Jxi5D0jKEIt4QZBH-cpUdbZgi7MFTE4WWWH_hfqQobU,2251
|
|
3
|
+
nord_config_generator/generator.py,sha256=d58TqzhLGv0Uhv5XotKOl9cl0OT29WKuy1Yq4n_iP7Q,7768
|
|
4
|
+
nord_config_generator/main.py,sha256=iL5Hrs7le3s-Nh9x3_oFkgfaEtJ1vV8gLJyyGtvRg7A,2734
|
|
5
|
+
nord_config_generator/models.py,sha256=6fBRRMXIPHsmeFw9yQdZKXaMjjAvS9uCTfiYe1Kyy8I,1203
|
|
6
|
+
nord_config_generator/ui.py,sha256=f-Uh4KYGSa18IgbYDA9PMSc4w1Q-WTSqpZE7G7bgMmg,3828
|
|
7
|
+
nord_config_generator-1.0.5.dist-info/METADATA,sha256=yL-amA6sw5vHfaQv1pT4PFffvdrMLBGsOj8hCqaB7Aw,1105
|
|
8
|
+
nord_config_generator-1.0.5.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
9
|
+
nord_config_generator-1.0.5.dist-info/entry_points.txt,sha256=TEZSrG2vKDWS4hcLAy-s65yjdxzcXAGbExHwWSYqd04,71
|
|
10
|
+
nord_config_generator-1.0.5.dist-info/top_level.txt,sha256=wGNt1MATKpHqIKJ3G0ByTkE2thqRe6xPZWrIU0FstSo,22
|
|
11
|
+
nord_config_generator-1.0.5.dist-info/RECORD,,
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: nord-config-generator
|
|
3
|
-
Version: 1.0.3
|
|
4
|
-
Summary: A command-line tool for generating optimized NordVPN WireGuard configurations.
|
|
5
|
-
Author-email: Ahmed Touhami <mustafachyi272@gmail.com>
|
|
6
|
-
Project-URL: Homepage, https://github.com/mustafachyi/NordVPN-WireGuard-Config-Generator
|
|
7
|
-
Project-URL: Bug Tracker, https://github.com/mustafachyi/NordVPN-WireGuard-Config-Generator/issues
|
|
8
|
-
Classifier: Programming Language :: Python :: 3
|
|
9
|
-
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
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 Configuration Generator
|
|
20
|
-
|
|
21
|
-
A command-line tool for generating optimized NordVPN WireGuard configurations.
|
|
22
|
-
|
|
23
|
-
## Project Philosophy: A Focus on Quality
|
|
24
|
-
|
|
25
|
-
This project has been fundamentally refocused. Previously, multiple versions existed across several programming languages. This approach divided development effort and resulted in inconsistent quality.
|
|
26
|
-
|
|
27
|
-
The new directive is singular: to provide one exceptionally engineered tool that is robust, maintainable, and correct.
|
|
28
|
-
|
|
29
|
-
To this end, all previous language implementations have been archived. Development is now concentrated on two platforms:
|
|
30
|
-
|
|
31
|
-
1. **This Command-Line Tool:** A complete rewrite in Python, packaged for professional use.
|
|
32
|
-
2. **A Web Interface:** For users who require a graphical frontend.
|
|
33
|
-
|
|
34
|
-
This consolidated effort ensures a higher standard of quality and a more reliable end-product.
|
|
35
|
-
|
|
36
|
-
## Core Capabilities
|
|
37
|
-
|
|
38
|
-
* **Package Distribution:** The tool is a proper command-line application, installable via PyPI. This eliminates manual dependency management.
|
|
39
|
-
* **Performance:** Asynchronous architecture processes the entire NordVPN server list in seconds.
|
|
40
|
-
* **Optimization:** Intelligently sorts servers by current load and geographic proximity to the user, generating configurations for the most performant connections.
|
|
41
|
-
* **Structured Output:** Automatically creates a clean directory structure containing standard configurations, a `best_configs` folder for optimal servers per location, and a `servers.json` file with detailed metadata for analysis.
|
|
42
|
-
* **Interactive and Non-Interactive:** A guided rich-CLI for interactive use. The core logic is structured to be scriptable.
|
|
43
|
-
|
|
44
|
-
## Installation
|
|
45
|
-
|
|
46
|
-
Prerequisites: Python 3.9+
|
|
47
|
-
|
|
48
|
-
Install the package using `pip`:
|
|
49
|
-
|
|
50
|
-
```bash
|
|
51
|
-
pip install nord-config-generator
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
## Usage
|
|
55
|
-
|
|
56
|
-
### Generate Configurations (Default Action)
|
|
57
|
-
|
|
58
|
-
Execute the application without any arguments. This is the primary function.
|
|
59
|
-
|
|
60
|
-
```bash
|
|
61
|
-
nordgen
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
The application will prompt for the required access token and configuration preferences.
|
|
65
|
-
|
|
66
|
-
### Retrieve Private Key
|
|
67
|
-
|
|
68
|
-
To retrieve and display your NordLynx private key without generating configurations, use the `get-key` command:
|
|
69
|
-
|
|
70
|
-
```bash
|
|
71
|
-
nordgen get-key
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
## Web Version
|
|
75
|
-
|
|
76
|
-
A graphical alternative is available for direct use in a web browser.
|
|
77
|
-
|
|
78
|
-
* **Current Version:** [https://nord-configs.selfhoster.nl/](https://nord-configs.selfhoster.nl/)
|
|
79
|
-
* **Legacy Version:** [https://wg-nord.pages.dev/](https://wg-nord.pages.dev/)
|
|
80
|
-
|
|
81
|
-
## Support
|
|
82
|
-
|
|
83
|
-
Project visibility and continued development are supported by two actions:
|
|
84
|
-
|
|
85
|
-
1. **Star the Repository:** Starring the project on GitHub increases its visibility.
|
|
86
|
-
2. **NordVPN Referral:** Using the referral link for new subscriptions provides support at no additional cost. Link: [https://ref.nordvpn.com/MXIVDoJGpKT](https://ref.nordvpn.com/MXIVDoJGpKT)
|
|
87
|
-
|
|
88
|
-
## License
|
|
89
|
-
|
|
90
|
-
This project is distributed under the GNU General Public License v3.0. See the `LICENSE` file for full details.
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
nord_config_generator/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
nord_config_generator/main.py,sha256=vCBDo5-xWBhp7Qw39OtGi1PAX8aNnvmp3Giy2zbM950,14895
|
|
3
|
-
nord_config_generator/ui.py,sha256=r9ulCF3t8h9WlepXM4_LAdDLi8TFM9HufXrO95cMOOM,3148
|
|
4
|
-
nord_config_generator-1.0.3.dist-info/METADATA,sha256=ue3-udbGR-jWDw-8hBJWlJawc5Mx-5hgT_Qrw9YmcH8,3876
|
|
5
|
-
nord_config_generator-1.0.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
-
nord_config_generator-1.0.3.dist-info/entry_points.txt,sha256=TEZSrG2vKDWS4hcLAy-s65yjdxzcXAGbExHwWSYqd04,71
|
|
7
|
-
nord_config_generator-1.0.3.dist-info/top_level.txt,sha256=wGNt1MATKpHqIKJ3G0ByTkE2thqRe6xPZWrIU0FstSo,22
|
|
8
|
-
nord_config_generator-1.0.3.dist-info/RECORD,,
|
{nord_config_generator-1.0.3.dist-info → nord_config_generator-1.0.5.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{nord_config_generator-1.0.3.dist-info → nord_config_generator-1.0.5.dist-info}/top_level.txt
RENAMED
|
File without changes
|