nord-config-generator 1.0.0__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/__init__.py +0 -0
- nord_config_generator/main.py +282 -0
- nord_config_generator/ui.py +65 -0
- nord_config_generator-1.0.0.dist-info/METADATA +80 -0
- nord_config_generator-1.0.0.dist-info/RECORD +8 -0
- nord_config_generator-1.0.0.dist-info/WHEEL +5 -0
- nord_config_generator-1.0.0.dist-info/entry_points.txt +2 -0
- nord_config_generator-1.0.0.dist-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import os
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import base64
|
|
6
|
+
import re
|
|
7
|
+
import time
|
|
8
|
+
from typing import List, Tuple, Optional, Dict, Any
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from math import radians, sin, cos, asin, sqrt
|
|
12
|
+
from functools import partial
|
|
13
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
|
|
16
|
+
import aiohttp
|
|
17
|
+
import aiofiles
|
|
18
|
+
|
|
19
|
+
from .ui import ConsoleManager
|
|
20
|
+
|
|
21
|
+
NORD_API_BASE_URL = "https://api.nordvpn.com/v1"
|
|
22
|
+
LOCATION_API_URL = "https://ipinfo.io/json"
|
|
23
|
+
CONCURRENT_LIMIT = 200
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class Server:
|
|
27
|
+
name: str
|
|
28
|
+
hostname: str
|
|
29
|
+
station: str
|
|
30
|
+
load: int
|
|
31
|
+
country: str
|
|
32
|
+
city: str
|
|
33
|
+
latitude: float
|
|
34
|
+
longitude: float
|
|
35
|
+
public_key: str
|
|
36
|
+
distance: float = 0.0
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class UserPreferences:
|
|
40
|
+
dns: str = "103.86.96.100"
|
|
41
|
+
use_ip_for_endpoint: bool = False
|
|
42
|
+
persistent_keepalive: int = 25
|
|
43
|
+
|
|
44
|
+
def update_from_input(self, user_input: dict):
|
|
45
|
+
dns_input = user_input.get("dns")
|
|
46
|
+
if dns_input and re.match(r'^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$', dns_input):
|
|
47
|
+
self.dns = dns_input
|
|
48
|
+
|
|
49
|
+
self.use_ip_for_endpoint = user_input.get("endpoint_type", "").lower() == 'y'
|
|
50
|
+
|
|
51
|
+
keepalive_input = user_input.get("keepalive")
|
|
52
|
+
if keepalive_input and keepalive_input.isdigit() and 15 <= int(keepalive_input) <= 120:
|
|
53
|
+
self.persistent_keepalive = int(keepalive_input)
|
|
54
|
+
|
|
55
|
+
class NordVpnApiClient:
|
|
56
|
+
def __init__(self, console_manager: ConsoleManager):
|
|
57
|
+
self._session = aiohttp.ClientSession()
|
|
58
|
+
self._console = console_manager
|
|
59
|
+
|
|
60
|
+
async def _get(self, url: str, **kwargs) -> Optional[Any]:
|
|
61
|
+
try:
|
|
62
|
+
async with self._session.get(url, **kwargs) as response:
|
|
63
|
+
response.raise_for_status()
|
|
64
|
+
return await response.json()
|
|
65
|
+
except (aiohttp.ClientError, json.JSONDecodeError) as e:
|
|
66
|
+
self._console.print_message("error", f"API request failed for {url}: {e}")
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
async def get_private_key(self, token: str) -> Optional[str]:
|
|
70
|
+
auth_header = base64.b64encode(f'token:{token}'.encode()).decode()
|
|
71
|
+
url = f"{NORD_API_BASE_URL}/users/services/credentials"
|
|
72
|
+
data = await self._get(url, headers={'Authorization': f'Basic {auth_header}'})
|
|
73
|
+
if isinstance(data, dict):
|
|
74
|
+
return data.get('nordlynx_private_key')
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
async def get_all_servers(self) -> List[Dict[str, Any]]:
|
|
78
|
+
url = f"{NORD_API_BASE_URL}/servers"
|
|
79
|
+
params = {'limit': 9000, 'filters[servers_technologies][identifier]': 'wireguard_udp'}
|
|
80
|
+
data = await self._get(url, params=params)
|
|
81
|
+
if isinstance(data, list):
|
|
82
|
+
return data
|
|
83
|
+
return []
|
|
84
|
+
|
|
85
|
+
async def get_user_geolocation(self) -> Optional[Tuple[float, float]]:
|
|
86
|
+
data = await self._get(LOCATION_API_URL)
|
|
87
|
+
if not isinstance(data, dict):
|
|
88
|
+
return None
|
|
89
|
+
try:
|
|
90
|
+
lat, lon = data.get('loc', '').split(',')
|
|
91
|
+
return float(lat), float(lon)
|
|
92
|
+
except (ValueError, IndexError):
|
|
93
|
+
self._console.print_message("error", "Could not parse location data.")
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
async def close(self):
|
|
97
|
+
if self._session and not self._session.closed:
|
|
98
|
+
await self._session.close()
|
|
99
|
+
|
|
100
|
+
class ConfigurationOrchestrator:
|
|
101
|
+
def __init__(self, private_key: str, preferences: UserPreferences, console_manager: ConsoleManager, api_client: NordVpnApiClient):
|
|
102
|
+
self._api_client = api_client
|
|
103
|
+
self._private_key = private_key
|
|
104
|
+
self._preferences = preferences
|
|
105
|
+
self._console = console_manager
|
|
106
|
+
self._output_dir = Path(f'nordvpn_configs_{datetime.now().strftime("%Y%m%d_%H%M%S")}')
|
|
107
|
+
self._semaphore = asyncio.Semaphore(CONCURRENT_LIMIT)
|
|
108
|
+
self._thread_pool = ThreadPoolExecutor(max_workers=min(32, (os.cpu_count() or 1) + 4))
|
|
109
|
+
self.generation_succeeded = False
|
|
110
|
+
self.stats = {"total": 0, "best": 0}
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def _calculate_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
|
|
114
|
+
lon1_rad, lat1_rad, lon2_rad, lat2_rad = map(radians, [lon1, lat1, lon2, lat2])
|
|
115
|
+
a = sin((lat2_rad - lat1_rad) / 2)**2 + cos(lat1_rad) * cos(lat2_rad) * sin((lon2_rad - lon1_rad) / 2)**2
|
|
116
|
+
return 2 * asin(sqrt(a)) * 6371
|
|
117
|
+
|
|
118
|
+
def _parse_server_data(self, server_data: Dict[str, Any], user_location: Tuple[float, float]) -> Optional[Server]:
|
|
119
|
+
try:
|
|
120
|
+
location = server_data['locations'][0]
|
|
121
|
+
public_key = next(
|
|
122
|
+
tech_meta['value']
|
|
123
|
+
for tech in server_data['technologies'] if tech['identifier'] == 'wireguard_udp'
|
|
124
|
+
for tech_meta in tech['metadata'] if tech_meta['name'] == 'public_key'
|
|
125
|
+
)
|
|
126
|
+
return Server(
|
|
127
|
+
name=server_data['name'], hostname=server_data['hostname'], station=server_data['station'],
|
|
128
|
+
load=int(server_data.get('load', 0)), country=location['country']['name'],
|
|
129
|
+
city=location['country'].get('city', {}).get('name', 'Unknown'), latitude=location['latitude'],
|
|
130
|
+
longitude=location['longitude'], public_key=public_key,
|
|
131
|
+
distance=self._calculate_distance(user_location[0], user_location[1], location['latitude'], location['longitude'])
|
|
132
|
+
)
|
|
133
|
+
except (KeyError, IndexError, StopIteration):
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
def _generate_wireguard_config_string(self, server: Server) -> str:
|
|
137
|
+
endpoint = server.station if self._preferences.use_ip_for_endpoint else server.hostname
|
|
138
|
+
return f"[Interface]\nPrivateKey = {self._private_key}\nAddress = 10.5.0.2/16\nDNS = {self._preferences.dns}\n\n[Peer]\nPublicKey = {server.public_key}\nAllowedIPs = 0.0.0.0/0, ::/0\nEndpoint = {endpoint}:51820\nPersistentKeepalive = {self._preferences.persistent_keepalive}"
|
|
139
|
+
|
|
140
|
+
@staticmethod
|
|
141
|
+
def _sanitize_path_part(part: str) -> str:
|
|
142
|
+
return re.sub(r'[<>:"/\\|?*\0]', '', part.lower().replace(' ', '_')).replace('#', '')
|
|
143
|
+
|
|
144
|
+
async def _save_config_file(self, config_string: str, path: Path, filename: str, progress, task):
|
|
145
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
146
|
+
async with self._semaphore:
|
|
147
|
+
async with aiofiles.open(path / filename, 'w') as f:
|
|
148
|
+
await f.write(config_string)
|
|
149
|
+
progress.update(task, advance=1)
|
|
150
|
+
|
|
151
|
+
async def generate(self) -> Optional[Path]:
|
|
152
|
+
progress = self._console.create_progress_bar()
|
|
153
|
+
with progress:
|
|
154
|
+
task_data = progress.add_task("Fetching remote data...", total=2)
|
|
155
|
+
|
|
156
|
+
progress.update(task_data, description="Fetching user location...")
|
|
157
|
+
user_location, all_servers_data = await asyncio.gather(
|
|
158
|
+
self._api_client.get_user_geolocation(),
|
|
159
|
+
self._api_client.get_all_servers()
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
if not user_location or not all_servers_data:
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
progress.update(task_data, advance=2, description="Processing servers...")
|
|
166
|
+
|
|
167
|
+
loop = asyncio.get_running_loop()
|
|
168
|
+
parse_func = partial(self._parse_server_data, user_location=user_location)
|
|
169
|
+
parse_tasks = [loop.run_in_executor(self._thread_pool, parse_func, s) for s in all_servers_data]
|
|
170
|
+
|
|
171
|
+
processed_servers = [server for server in await asyncio.gather(*parse_tasks) if server]
|
|
172
|
+
self._thread_pool.shutdown(wait=False, cancel_futures=True)
|
|
173
|
+
|
|
174
|
+
sorted_servers = sorted(processed_servers, key=lambda s: (s.load, s.distance))
|
|
175
|
+
|
|
176
|
+
self._output_dir.mkdir(exist_ok=True)
|
|
177
|
+
servers_info, best_servers_by_location = {}, {}
|
|
178
|
+
|
|
179
|
+
config_progress = self._console.create_progress_bar(transient=False)
|
|
180
|
+
with config_progress:
|
|
181
|
+
total_configs = len(sorted_servers)
|
|
182
|
+
best_configs = 0
|
|
183
|
+
|
|
184
|
+
save_tasks = []
|
|
185
|
+
task_save_all = config_progress.add_task("Generating configs...", total=total_configs)
|
|
186
|
+
for server in sorted_servers:
|
|
187
|
+
country_sanitized = self._sanitize_path_part(server.country)
|
|
188
|
+
city_sanitized = self._sanitize_path_part(server.city)
|
|
189
|
+
config_str = self._generate_wireguard_config_string(server)
|
|
190
|
+
path = self._output_dir / 'configs' / country_sanitized / city_sanitized
|
|
191
|
+
filename = f"{self._sanitize_path_part(server.name)}.conf"
|
|
192
|
+
save_tasks.append(self._save_config_file(config_str, path, filename, config_progress, task_save_all))
|
|
193
|
+
|
|
194
|
+
location_key = (server.country, server.city)
|
|
195
|
+
if location_key not in best_servers_by_location or server.load < best_servers_by_location[location_key].load:
|
|
196
|
+
best_servers_by_location[location_key] = server
|
|
197
|
+
|
|
198
|
+
country_info = servers_info.setdefault(server.country, {})
|
|
199
|
+
city_info = country_info.setdefault(server.city, {"distance": int(server.distance), "servers": []})
|
|
200
|
+
city_info["servers"].append((server.name, server.load))
|
|
201
|
+
|
|
202
|
+
self.stats["total"] = total_configs
|
|
203
|
+
await asyncio.gather(*save_tasks)
|
|
204
|
+
|
|
205
|
+
best_save_tasks = []
|
|
206
|
+
best_configs = len(best_servers_by_location)
|
|
207
|
+
task_save_best = config_progress.add_task("Generating optimized configs...", total=best_configs)
|
|
208
|
+
for server in best_servers_by_location.values():
|
|
209
|
+
country_sanitized = self._sanitize_path_part(server.country)
|
|
210
|
+
city_sanitized = self._sanitize_path_part(server.city)
|
|
211
|
+
config_str = self._generate_wireguard_config_string(server)
|
|
212
|
+
path = self._output_dir / 'best_configs' / country_sanitized / city_sanitized
|
|
213
|
+
filename = f"{self._sanitize_path_part(server.name)}.conf"
|
|
214
|
+
best_save_tasks.append(self._save_config_file(config_str, path, filename, config_progress, task_save_best))
|
|
215
|
+
|
|
216
|
+
self.stats["best"] = best_configs
|
|
217
|
+
await asyncio.gather(*best_save_tasks)
|
|
218
|
+
|
|
219
|
+
async with aiofiles.open(self._output_dir / 'servers.json', 'w') as f:
|
|
220
|
+
await f.write(json.dumps(servers_info, indent=2, separators=(',', ':'), ensure_ascii=False))
|
|
221
|
+
|
|
222
|
+
self.generation_succeeded = True
|
|
223
|
+
return self._output_dir
|
|
224
|
+
|
|
225
|
+
def is_valid_token_format(token: str) -> bool:
|
|
226
|
+
return bool(re.match(r'^[a-fA-F0-9]{64}$', token))
|
|
227
|
+
|
|
228
|
+
async def main_async():
|
|
229
|
+
console = ConsoleManager()
|
|
230
|
+
api_client = NordVpnApiClient(console)
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
console.clear()
|
|
234
|
+
console.print_title()
|
|
235
|
+
|
|
236
|
+
token = console.get_user_input("Please enter your NordVPN access token: ", is_secret=True)
|
|
237
|
+
if not is_valid_token_format(token):
|
|
238
|
+
console.print_message("error", "Invalid token format.")
|
|
239
|
+
return
|
|
240
|
+
|
|
241
|
+
private_key = None
|
|
242
|
+
with console.create_progress_bar() as progress:
|
|
243
|
+
task = progress.add_task("Validating token...", total=1)
|
|
244
|
+
private_key = await api_client.get_private_key(token)
|
|
245
|
+
progress.update(task, advance=1)
|
|
246
|
+
|
|
247
|
+
if not private_key:
|
|
248
|
+
console.print_message("error", "Token is invalid or could not be verified. Please check the token and try again.")
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
console.print_message("success", "Token validated successfully.")
|
|
252
|
+
|
|
253
|
+
preferences = UserPreferences()
|
|
254
|
+
user_input = console.get_preferences(preferences)
|
|
255
|
+
preferences.update_from_input(user_input)
|
|
256
|
+
|
|
257
|
+
console.clear()
|
|
258
|
+
|
|
259
|
+
start_time = time.time()
|
|
260
|
+
orchestrator = ConfigurationOrchestrator(private_key, preferences, console, api_client)
|
|
261
|
+
|
|
262
|
+
output_directory = await orchestrator.generate()
|
|
263
|
+
elapsed_time = time.time() - start_time
|
|
264
|
+
|
|
265
|
+
if orchestrator.generation_succeeded and output_directory:
|
|
266
|
+
console.print_summary(output_directory, orchestrator.stats["total"], orchestrator.stats["best"], elapsed_time)
|
|
267
|
+
else:
|
|
268
|
+
console.print_message("error", "Process failed. Check the logs for details.")
|
|
269
|
+
|
|
270
|
+
except Exception as e:
|
|
271
|
+
console.print_message("error", f"An unrecoverable error occurred: {e}")
|
|
272
|
+
finally:
|
|
273
|
+
await api_client.close()
|
|
274
|
+
|
|
275
|
+
def cli_entry_point():
|
|
276
|
+
try:
|
|
277
|
+
asyncio.run(main_async())
|
|
278
|
+
except KeyboardInterrupt:
|
|
279
|
+
print("\nProcess interrupted by user.")
|
|
280
|
+
|
|
281
|
+
if __name__ == "__main__":
|
|
282
|
+
cli_entry_point()
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from rich.console import Console
|
|
2
|
+
from rich.panel import Panel
|
|
3
|
+
from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn, TimeElapsedColumn
|
|
4
|
+
from rich.theme import Theme
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
class ConsoleManager:
|
|
10
|
+
def __init__(self):
|
|
11
|
+
custom_theme = Theme({
|
|
12
|
+
"info": "cyan",
|
|
13
|
+
"success": "bold green",
|
|
14
|
+
"warning": "yellow",
|
|
15
|
+
"error": "bold red",
|
|
16
|
+
"title": "bold magenta",
|
|
17
|
+
"path": "underline bright_blue"
|
|
18
|
+
})
|
|
19
|
+
self.console = Console(theme=custom_theme)
|
|
20
|
+
|
|
21
|
+
def clear(self):
|
|
22
|
+
os.system('cls' if os.name == 'nt' else 'clear')
|
|
23
|
+
|
|
24
|
+
def print_title(self):
|
|
25
|
+
self.console.print(Panel("[title]NordVPN Configuration Generator[/title]", expand=False, border_style="info"))
|
|
26
|
+
|
|
27
|
+
def get_user_input(self, prompt: str, is_secret: bool = False) -> str:
|
|
28
|
+
return self.console.input(f"[info]{prompt}[/info]", password=is_secret).strip()
|
|
29
|
+
|
|
30
|
+
def get_preferences(self, defaults) -> dict:
|
|
31
|
+
self.console.print("\n[info]Configuration Options (press Enter to use defaults)[/info]")
|
|
32
|
+
dns = self.get_user_input(f"Enter DNS server IP (default: {defaults.dns}): ")
|
|
33
|
+
endpoint_type = self.get_user_input("Use IP instead of hostname for endpoints? (y/N): ")
|
|
34
|
+
keepalive = self.get_user_input(f"Enter PersistentKeepalive value (default: {defaults.persistent_keepalive}): ")
|
|
35
|
+
return {"dns": dns, "endpoint_type": endpoint_type, "keepalive": keepalive}
|
|
36
|
+
|
|
37
|
+
def print_message(self, style: str, message: str):
|
|
38
|
+
self.console.print(f"[{style}]{message}[/{style}]")
|
|
39
|
+
|
|
40
|
+
def create_progress_bar(self, transient: bool = True) -> Progress:
|
|
41
|
+
return Progress(
|
|
42
|
+
SpinnerColumn(),
|
|
43
|
+
TextColumn("[progress.description]{task.description}"),
|
|
44
|
+
BarColumn(),
|
|
45
|
+
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
|
46
|
+
TimeElapsedColumn(),
|
|
47
|
+
console=self.console,
|
|
48
|
+
transient=transient
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def print_summary(self, output_dir: Path, total_configs: int, best_configs: int, elapsed_time: float):
|
|
52
|
+
summary_table = Table.grid(padding=(0, 2))
|
|
53
|
+
summary_table.add_column(style="info")
|
|
54
|
+
summary_table.add_column()
|
|
55
|
+
summary_table.add_row("Output Directory:", f"[path]{output_dir}[/path]")
|
|
56
|
+
summary_table.add_row("Standard Configs:", f"{total_configs}")
|
|
57
|
+
summary_table.add_row("Optimized Configs:", f"{best_configs}")
|
|
58
|
+
summary_table.add_row("Time Taken:", f"{elapsed_time:.2f} seconds")
|
|
59
|
+
|
|
60
|
+
self.console.print(Panel(
|
|
61
|
+
summary_table,
|
|
62
|
+
title="[success]Generation Complete[/success]",
|
|
63
|
+
border_style="success",
|
|
64
|
+
expand=False
|
|
65
|
+
))
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nord-config-generator
|
|
3
|
+
Version: 1.0.0
|
|
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.8
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: aiohttp>=3.8.0
|
|
16
|
+
Requires-Dist: aiofiles>=0.8.0
|
|
17
|
+
Requires-Dist: rich>=12.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 TUI for interactive use. The core logic is structured to be scriptable.
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
Prerequisites: Python 3.8+
|
|
47
|
+
|
|
48
|
+
Install the package using `pip`:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install nord-config-generator
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Usage
|
|
55
|
+
|
|
56
|
+
Execute the installed application:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
nord-generator
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The application will prompt for the required access token and configuration preferences.
|
|
63
|
+
|
|
64
|
+
## Web Version
|
|
65
|
+
|
|
66
|
+
A graphical alternative is available for direct use in a web browser.
|
|
67
|
+
|
|
68
|
+
* **Current Version:** [https://nord-configs.selfhoster.nl/](https://nord-configs.selfhoster.nl/)
|
|
69
|
+
* **Legacy Version:** [https://wg-nord.pages.dev/](https://wg-nord.pages.dev/)
|
|
70
|
+
|
|
71
|
+
## Support
|
|
72
|
+
|
|
73
|
+
Project visibility and continued development are supported by two actions:
|
|
74
|
+
|
|
75
|
+
1. **Star the Repository:** Starring the project on GitHub increases its visibility.
|
|
76
|
+
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)
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
This project is distributed under the GNU General Public License v3.0. See the `LICENSE` file for full details.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
nord_config_generator/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
nord_config_generator/main.py,sha256=P_xPIeXwUn4FJNPjR7aEun0EfL5vbHko3IOu4bnxfdI,12740
|
|
3
|
+
nord_config_generator/ui.py,sha256=YEnSy9ltwul-3KzmEKLA3pynD29eEEf7hikzqRuI_l4,2838
|
|
4
|
+
nord_config_generator-1.0.0.dist-info/METADATA,sha256=m1GQxkfmHG63UuylocH7M4leju66BObnmTirtlF-A10,3592
|
|
5
|
+
nord_config_generator-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
+
nord_config_generator-1.0.0.dist-info/entry_points.txt,sha256=NEVrNutXaNQAPTdWa9R5iZfdpVKZ_RYCYqR8hWJBv2Q,78
|
|
7
|
+
nord_config_generator-1.0.0.dist-info/top_level.txt,sha256=wGNt1MATKpHqIKJ3G0ByTkE2thqRe6xPZWrIU0FstSo,22
|
|
8
|
+
nord_config_generator-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nord_config_generator
|