raijin-server 0.2.41__py3-none-any.whl → 0.3.1__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.
- raijin_server/__init__.py +1 -1
- raijin_server/cli.py +6 -0
- raijin_server/modules/__init__.py +3 -1
- raijin_server/modules/grafana.py +365 -16
- raijin_server/modules/internal_dns.py +446 -0
- raijin_server/modules/kong.py +8 -4
- raijin_server/modules/minio.py +15 -5
- raijin_server/modules/observability_ingress.py +29 -1
- raijin_server/modules/prometheus.py +266 -3
- raijin_server/modules/traefik.py +35 -1
- raijin_server/modules/vpn_client.py +526 -0
- raijin_server-0.3.1.dist-info/METADATA +362 -0
- {raijin_server-0.2.41.dist-info → raijin_server-0.3.1.dist-info}/RECORD +17 -15
- raijin_server-0.2.41.dist-info/METADATA +0 -564
- {raijin_server-0.2.41.dist-info → raijin_server-0.3.1.dist-info}/WHEEL +0 -0
- {raijin_server-0.2.41.dist-info → raijin_server-0.3.1.dist-info}/entry_points.txt +0 -0
- {raijin_server-0.2.41.dist-info → raijin_server-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {raijin_server-0.2.41.dist-info → raijin_server-0.3.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
"""Gerenciamento de clientes WireGuard VPN."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import List, Tuple
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
from raijin_server.utils import ExecutionContext, logger, require_root, run_cmd, write_file
|
|
13
|
+
|
|
14
|
+
WIREGUARD_DIR = Path("/etc/wireguard")
|
|
15
|
+
WG0_CONF = WIREGUARD_DIR / "wg0.conf"
|
|
16
|
+
CLIENTS_DIR = WIREGUARD_DIR / "clients"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _generate_keypair() -> Tuple[str, str]:
|
|
20
|
+
"""Gera par de chaves WireGuard."""
|
|
21
|
+
try:
|
|
22
|
+
result = subprocess.run(["wg", "genkey"], capture_output=True, text=True, check=True)
|
|
23
|
+
private_key = result.stdout.strip()
|
|
24
|
+
|
|
25
|
+
pub_result = subprocess.run(
|
|
26
|
+
["wg", "pubkey"],
|
|
27
|
+
input=private_key,
|
|
28
|
+
capture_output=True,
|
|
29
|
+
text=True,
|
|
30
|
+
check=True,
|
|
31
|
+
)
|
|
32
|
+
public_key = pub_result.stdout.strip()
|
|
33
|
+
|
|
34
|
+
return private_key, public_key
|
|
35
|
+
except subprocess.CalledProcessError as exc:
|
|
36
|
+
typer.secho(f"Erro ao gerar chaves: {exc}", fg=typer.colors.RED)
|
|
37
|
+
raise typer.Exit(1)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _read_server_config() -> dict:
|
|
41
|
+
"""Lê configuração do servidor."""
|
|
42
|
+
if not WG0_CONF.exists():
|
|
43
|
+
typer.secho(
|
|
44
|
+
f"Arquivo {WG0_CONF} não encontrado. Execute 'raijin vpn' primeiro.",
|
|
45
|
+
fg=typer.colors.RED
|
|
46
|
+
)
|
|
47
|
+
raise typer.Exit(1)
|
|
48
|
+
|
|
49
|
+
content = WG0_CONF.read_text()
|
|
50
|
+
|
|
51
|
+
# Extrai informações do servidor
|
|
52
|
+
server_public_key = ""
|
|
53
|
+
server_port = ""
|
|
54
|
+
server_address = ""
|
|
55
|
+
endpoint = ""
|
|
56
|
+
dns = ""
|
|
57
|
+
|
|
58
|
+
for line in content.split("\n"):
|
|
59
|
+
if "ListenPort" in line:
|
|
60
|
+
server_port = line.split("=")[1].strip()
|
|
61
|
+
elif "Address" in line and not server_address:
|
|
62
|
+
server_address = line.split("=")[1].strip()
|
|
63
|
+
elif "PrivateKey" in line:
|
|
64
|
+
# Gera chave pública do servidor a partir da privada
|
|
65
|
+
private_key = line.split("=")[1].strip()
|
|
66
|
+
try:
|
|
67
|
+
result = subprocess.run(
|
|
68
|
+
["wg", "pubkey"],
|
|
69
|
+
input=private_key,
|
|
70
|
+
capture_output=True,
|
|
71
|
+
text=True,
|
|
72
|
+
check=True,
|
|
73
|
+
)
|
|
74
|
+
server_public_key = result.stdout.strip()
|
|
75
|
+
except:
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
# Tenta encontrar endpoint nos peers existentes
|
|
79
|
+
peer_match = re.search(r"# Endpoint: (.+)", content)
|
|
80
|
+
if peer_match:
|
|
81
|
+
endpoint = peer_match.group(1)
|
|
82
|
+
|
|
83
|
+
# Tenta encontrar DNS
|
|
84
|
+
dns_match = re.search(r"# DNS: (.+)", content)
|
|
85
|
+
if dns_match:
|
|
86
|
+
dns = dns_match.group(1)
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
"public_key": server_public_key,
|
|
90
|
+
"port": server_port,
|
|
91
|
+
"address": server_address.split("/")[0] if server_address else "",
|
|
92
|
+
"network": server_address,
|
|
93
|
+
"endpoint": endpoint,
|
|
94
|
+
"dns": dns or "1.1.1.1,8.8.8.8",
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _list_existing_clients() -> List[dict]:
|
|
99
|
+
"""Lista clientes existentes."""
|
|
100
|
+
if not WG0_CONF.exists():
|
|
101
|
+
return []
|
|
102
|
+
|
|
103
|
+
content = WG0_CONF.read_text()
|
|
104
|
+
clients = []
|
|
105
|
+
|
|
106
|
+
# Parse peers - suporta múltiplos formatos de comentário
|
|
107
|
+
lines = content.split("\n")
|
|
108
|
+
i = 0
|
|
109
|
+
|
|
110
|
+
while i < len(lines):
|
|
111
|
+
line = lines[i].strip()
|
|
112
|
+
|
|
113
|
+
# Detecta início de um bloco [Peer]
|
|
114
|
+
if line == "[Peer]":
|
|
115
|
+
peer_name = None
|
|
116
|
+
public_key = None
|
|
117
|
+
allowed_ips = None
|
|
118
|
+
|
|
119
|
+
# Verifica linha anterior para comentário com nome
|
|
120
|
+
if i > 0:
|
|
121
|
+
prev_line = lines[i - 1].strip()
|
|
122
|
+
# Formato: "# cliente_nome" ou "# Cliente: nome"
|
|
123
|
+
if prev_line.startswith("#"):
|
|
124
|
+
comment = prev_line[1:].strip()
|
|
125
|
+
if comment.lower().startswith("cliente:"):
|
|
126
|
+
peer_name = comment.split(":", 1)[1].strip()
|
|
127
|
+
else:
|
|
128
|
+
# Nome direto após # (formato do módulo vpn)
|
|
129
|
+
peer_name = comment
|
|
130
|
+
|
|
131
|
+
# Lê configurações do peer
|
|
132
|
+
i += 1
|
|
133
|
+
while i < len(lines):
|
|
134
|
+
peer_line = lines[i].strip()
|
|
135
|
+
if peer_line.startswith("[") or (peer_line.startswith("#") and i + 1 < len(lines) and lines[i + 1].strip() == "[Peer]"):
|
|
136
|
+
break
|
|
137
|
+
|
|
138
|
+
if peer_line.startswith("PublicKey"):
|
|
139
|
+
public_key = peer_line.split("=", 1)[1].strip()
|
|
140
|
+
elif peer_line.startswith("AllowedIPs"):
|
|
141
|
+
allowed_ips = peer_line.split("=", 1)[1].strip()
|
|
142
|
+
# Comentário inline com nome do cliente
|
|
143
|
+
elif peer_line.startswith("#") and not peer_name:
|
|
144
|
+
peer_name = peer_line[1:].strip()
|
|
145
|
+
|
|
146
|
+
i += 1
|
|
147
|
+
|
|
148
|
+
# Adiciona peer se tiver pelo menos chave pública
|
|
149
|
+
if public_key:
|
|
150
|
+
clients.append({
|
|
151
|
+
"name": peer_name or f"cliente_{len(clients) + 1}",
|
|
152
|
+
"public_key": public_key,
|
|
153
|
+
"ip": allowed_ips or "N/A",
|
|
154
|
+
})
|
|
155
|
+
continue
|
|
156
|
+
|
|
157
|
+
i += 1
|
|
158
|
+
|
|
159
|
+
return clients
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _get_next_client_ip(server_config: dict) -> str:
|
|
163
|
+
"""Calcula próximo IP disponível para cliente."""
|
|
164
|
+
network = server_config["network"]
|
|
165
|
+
base_ip = network.split("/")[0]
|
|
166
|
+
base_parts = base_ip.rsplit(".", 1)
|
|
167
|
+
base_network = base_parts[0]
|
|
168
|
+
|
|
169
|
+
existing_clients = _list_existing_clients()
|
|
170
|
+
used_ips = {client["ip"].split("/")[0] for client in existing_clients}
|
|
171
|
+
used_ips.add(base_ip) # IP do servidor
|
|
172
|
+
|
|
173
|
+
# Procura próximo IP disponível
|
|
174
|
+
for i in range(2, 255):
|
|
175
|
+
candidate = f"{base_network}.{i}"
|
|
176
|
+
if candidate not in used_ips:
|
|
177
|
+
return f"{candidate}/32"
|
|
178
|
+
|
|
179
|
+
typer.secho("Erro: Rede VPN cheia (254 clientes)", fg=typer.colors.RED)
|
|
180
|
+
raise typer.Exit(1)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _add_peer_to_server(name: str, public_key: str, client_ip: str, ctx: ExecutionContext) -> None:
|
|
184
|
+
"""Adiciona peer ao arquivo de configuração do servidor."""
|
|
185
|
+
content = WG0_CONF.read_text()
|
|
186
|
+
|
|
187
|
+
# Remove linhas em branco extras no final
|
|
188
|
+
content = content.rstrip() + "\n"
|
|
189
|
+
|
|
190
|
+
# Formato compatível com o módulo vpn original
|
|
191
|
+
peer_block = f"""
|
|
192
|
+
# {name}
|
|
193
|
+
[Peer]
|
|
194
|
+
PublicKey = {public_key}
|
|
195
|
+
AllowedIPs = {client_ip}
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
# Adiciona peer ao final
|
|
199
|
+
content += peer_block
|
|
200
|
+
|
|
201
|
+
write_file(WG0_CONF, content, ctx)
|
|
202
|
+
|
|
203
|
+
# Recarrega configuração via wg syncconf (mais rápido) ou restart
|
|
204
|
+
typer.echo("Recarregando WireGuard...")
|
|
205
|
+
# Tenta wg syncconf primeiro (não derruba conexões existentes)
|
|
206
|
+
result = run_cmd(
|
|
207
|
+
["bash", "-c", f"wg syncconf wg0 <(wg-quick strip wg0)"],
|
|
208
|
+
ctx,
|
|
209
|
+
check=False
|
|
210
|
+
)
|
|
211
|
+
if result.returncode != 0:
|
|
212
|
+
# Fallback para restart
|
|
213
|
+
run_cmd(["systemctl", "restart", "wg-quick@wg0"], ctx, check=False)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _remove_peer_from_server(public_key: str, ctx: ExecutionContext) -> None:
|
|
217
|
+
"""Remove peer do arquivo de configuração do servidor."""
|
|
218
|
+
content = WG0_CONF.read_text()
|
|
219
|
+
|
|
220
|
+
# Remove bloco do peer - suporta múltiplos formatos
|
|
221
|
+
lines = content.split("\n")
|
|
222
|
+
new_lines = []
|
|
223
|
+
skip_until_next_section = False
|
|
224
|
+
|
|
225
|
+
for i, line in enumerate(lines):
|
|
226
|
+
if f"PublicKey = {public_key}" in line:
|
|
227
|
+
skip_until_next_section = True
|
|
228
|
+
# Remove também o comentário anterior e [Peer]
|
|
229
|
+
while new_lines and (
|
|
230
|
+
new_lines[-1].startswith("#") or
|
|
231
|
+
new_lines[-1].strip() == "" or
|
|
232
|
+
new_lines[-1].strip() == "[Peer]"
|
|
233
|
+
):
|
|
234
|
+
new_lines.pop()
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
if skip_until_next_section:
|
|
238
|
+
# Próximo peer ou seção
|
|
239
|
+
stripped = line.strip()
|
|
240
|
+
if stripped.startswith("[") or (stripped.startswith("#") and i + 1 < len(lines) and lines[i + 1].strip() == "[Peer]"):
|
|
241
|
+
skip_until_next_section = False
|
|
242
|
+
else:
|
|
243
|
+
continue
|
|
244
|
+
|
|
245
|
+
new_lines.append(line)
|
|
246
|
+
|
|
247
|
+
# Remove linhas em branco extras no final
|
|
248
|
+
while new_lines and new_lines[-1].strip() == "":
|
|
249
|
+
new_lines.pop()
|
|
250
|
+
|
|
251
|
+
write_file(WG0_CONF, "\n".join(new_lines) + "\n", ctx)
|
|
252
|
+
|
|
253
|
+
# Recarrega configuração
|
|
254
|
+
typer.echo("Recarregando WireGuard...")
|
|
255
|
+
result = run_cmd(
|
|
256
|
+
["bash", "-c", f"wg syncconf wg0 <(wg-quick strip wg0)"],
|
|
257
|
+
ctx,
|
|
258
|
+
check=False
|
|
259
|
+
)
|
|
260
|
+
if result.returncode != 0:
|
|
261
|
+
run_cmd(["systemctl", "restart", "wg-quick@wg0"], ctx, check=False)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _create_client_config(
|
|
265
|
+
name: str,
|
|
266
|
+
private_key: str,
|
|
267
|
+
client_ip: str,
|
|
268
|
+
server_config: dict,
|
|
269
|
+
) -> str:
|
|
270
|
+
"""Cria arquivo de configuração do cliente."""
|
|
271
|
+
server_ip = server_config["address"]
|
|
272
|
+
server_network = server_config["network"]
|
|
273
|
+
dns = server_config["dns"]
|
|
274
|
+
endpoint = server_config["endpoint"]
|
|
275
|
+
server_public_key = server_config["public_key"]
|
|
276
|
+
server_port = server_config["port"]
|
|
277
|
+
|
|
278
|
+
if not endpoint:
|
|
279
|
+
endpoint = typer.prompt("Endpoint público do servidor (IP ou domínio)")
|
|
280
|
+
|
|
281
|
+
config = f"""[Interface]
|
|
282
|
+
# Cliente: {name}
|
|
283
|
+
PrivateKey = {private_key}
|
|
284
|
+
Address = {client_ip}
|
|
285
|
+
DNS = {dns}
|
|
286
|
+
|
|
287
|
+
[Peer]
|
|
288
|
+
PublicKey = {server_public_key}
|
|
289
|
+
Endpoint = {endpoint}:{server_port}
|
|
290
|
+
AllowedIPs = {server_network}, 10.0.0.0/8
|
|
291
|
+
PersistentKeepalive = 25
|
|
292
|
+
"""
|
|
293
|
+
|
|
294
|
+
return config
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def add_client(ctx: ExecutionContext) -> None:
|
|
298
|
+
"""Adiciona novo cliente VPN."""
|
|
299
|
+
require_root(ctx)
|
|
300
|
+
|
|
301
|
+
typer.echo("Adicionando novo cliente WireGuard VPN...\n")
|
|
302
|
+
|
|
303
|
+
# Lê configuração do servidor
|
|
304
|
+
server_config = _read_server_config()
|
|
305
|
+
|
|
306
|
+
# Lista clientes existentes
|
|
307
|
+
existing_clients = _list_existing_clients()
|
|
308
|
+
if existing_clients:
|
|
309
|
+
typer.echo("Clientes existentes:")
|
|
310
|
+
for client in existing_clients:
|
|
311
|
+
typer.echo(f" - {client['name']} ({client['ip']})")
|
|
312
|
+
typer.echo("")
|
|
313
|
+
|
|
314
|
+
# Solicita informações do novo cliente
|
|
315
|
+
name = typer.prompt("Nome do cliente")
|
|
316
|
+
|
|
317
|
+
# Verifica se já existe
|
|
318
|
+
if any(c["name"] == name for c in existing_clients):
|
|
319
|
+
typer.secho(f"Cliente '{name}' já existe!", fg=typer.colors.RED)
|
|
320
|
+
raise typer.Exit(1)
|
|
321
|
+
|
|
322
|
+
# Gera chaves
|
|
323
|
+
typer.echo("Gerando par de chaves...")
|
|
324
|
+
private_key, public_key = _generate_keypair()
|
|
325
|
+
|
|
326
|
+
# Calcula próximo IP
|
|
327
|
+
client_ip = _get_next_client_ip(server_config)
|
|
328
|
+
|
|
329
|
+
typer.echo(f"IP atribuído: {client_ip}")
|
|
330
|
+
|
|
331
|
+
# Cria diretório de clientes
|
|
332
|
+
CLIENTS_DIR.mkdir(parents=True, exist_ok=True)
|
|
333
|
+
|
|
334
|
+
# Cria configuração do cliente
|
|
335
|
+
client_config = _create_client_config(name, private_key, client_ip, server_config)
|
|
336
|
+
|
|
337
|
+
client_file = CLIENTS_DIR / f"{name}.conf"
|
|
338
|
+
write_file(client_file, client_config, ctx)
|
|
339
|
+
client_file.chmod(0o600)
|
|
340
|
+
|
|
341
|
+
# Salva chaves separadamente
|
|
342
|
+
key_file = CLIENTS_DIR / f"{name}.key"
|
|
343
|
+
write_file(key_file, f"Private: {private_key}\nPublic: {public_key}\n", ctx)
|
|
344
|
+
key_file.chmod(0o600)
|
|
345
|
+
|
|
346
|
+
# Adiciona peer ao servidor
|
|
347
|
+
_add_peer_to_server(name, public_key, client_ip, ctx)
|
|
348
|
+
|
|
349
|
+
typer.secho(f"\n✓ Cliente '{name}' adicionado com sucesso!", fg=typer.colors.GREEN, bold=True)
|
|
350
|
+
typer.echo(f"\nArquivo de configuração: {client_file}")
|
|
351
|
+
typer.echo("\nPara usar:")
|
|
352
|
+
typer.echo(f" 1. Copie o arquivo: scp root@servidor:{client_file} .")
|
|
353
|
+
typer.echo(f" 2. Importe no WireGuard (Windows/Mac/Linux)")
|
|
354
|
+
typer.echo(f" 3. Ou gere QR code: qrencode -t ansiutf8 {client_file}")
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def list_clients(ctx: ExecutionContext) -> None:
|
|
358
|
+
"""Lista todos os clientes VPN."""
|
|
359
|
+
require_root(ctx)
|
|
360
|
+
|
|
361
|
+
clients = _list_existing_clients()
|
|
362
|
+
|
|
363
|
+
if not clients:
|
|
364
|
+
typer.echo("Nenhum cliente configurado.")
|
|
365
|
+
return
|
|
366
|
+
|
|
367
|
+
typer.secho(f"\n{'Nome':<20} {'IP':<20} {'Chave Pública'}", fg=typer.colors.CYAN, bold=True)
|
|
368
|
+
typer.echo("=" * 80)
|
|
369
|
+
|
|
370
|
+
for client in clients:
|
|
371
|
+
name = client.get("name", "Desconhecido")
|
|
372
|
+
ip = client.get("ip", "N/A")
|
|
373
|
+
pubkey = client.get("public_key", "N/A")[:40] + "..."
|
|
374
|
+
typer.echo(f"{name:<20} {ip:<20} {pubkey}")
|
|
375
|
+
|
|
376
|
+
typer.echo(f"\nTotal: {len(clients)} cliente(s)")
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def remove_client(ctx: ExecutionContext) -> None:
|
|
380
|
+
"""Remove cliente VPN."""
|
|
381
|
+
require_root(ctx)
|
|
382
|
+
|
|
383
|
+
clients = _list_existing_clients()
|
|
384
|
+
|
|
385
|
+
if not clients:
|
|
386
|
+
typer.echo("Nenhum cliente configurado.")
|
|
387
|
+
return
|
|
388
|
+
|
|
389
|
+
# Mostra lista
|
|
390
|
+
typer.echo("Clientes disponíveis:")
|
|
391
|
+
for i, client in enumerate(clients, 1):
|
|
392
|
+
typer.echo(f" {i}. {client['name']} ({client['ip']})")
|
|
393
|
+
|
|
394
|
+
# Solicita nome
|
|
395
|
+
name = typer.prompt("\nNome do cliente para remover")
|
|
396
|
+
|
|
397
|
+
# Encontra cliente
|
|
398
|
+
client = next((c for c in clients if c["name"] == name), None)
|
|
399
|
+
if not client:
|
|
400
|
+
typer.secho(f"Cliente '{name}' não encontrado!", fg=typer.colors.RED)
|
|
401
|
+
raise typer.Exit(1)
|
|
402
|
+
|
|
403
|
+
# Confirma remoção
|
|
404
|
+
confirm = typer.confirm(
|
|
405
|
+
f"Remover cliente '{name}' ({client['ip']})?",
|
|
406
|
+
default=False
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
if not confirm:
|
|
410
|
+
typer.echo("Operação cancelada.")
|
|
411
|
+
return
|
|
412
|
+
|
|
413
|
+
# Remove do servidor
|
|
414
|
+
_remove_peer_from_server(client["public_key"], ctx)
|
|
415
|
+
|
|
416
|
+
# Remove arquivos locais
|
|
417
|
+
client_file = CLIENTS_DIR / f"{name}.conf"
|
|
418
|
+
key_file = CLIENTS_DIR / f"{name}.key"
|
|
419
|
+
|
|
420
|
+
if client_file.exists():
|
|
421
|
+
client_file.unlink()
|
|
422
|
+
if key_file.exists():
|
|
423
|
+
key_file.unlink()
|
|
424
|
+
|
|
425
|
+
typer.secho(f"\n✓ Cliente '{name}' removido com sucesso!", fg=typer.colors.GREEN, bold=True)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def show_client_config(ctx: ExecutionContext) -> None:
|
|
429
|
+
"""Mostra configuração de um cliente."""
|
|
430
|
+
require_root(ctx)
|
|
431
|
+
|
|
432
|
+
clients = _list_existing_clients()
|
|
433
|
+
|
|
434
|
+
if not clients:
|
|
435
|
+
typer.echo("Nenhum cliente configurado.")
|
|
436
|
+
return
|
|
437
|
+
|
|
438
|
+
# Lista clientes
|
|
439
|
+
typer.echo("Clientes disponíveis:")
|
|
440
|
+
for client in clients:
|
|
441
|
+
typer.echo(f" - {client['name']}")
|
|
442
|
+
|
|
443
|
+
# Solicita nome
|
|
444
|
+
name = typer.prompt("\nNome do cliente")
|
|
445
|
+
|
|
446
|
+
client_file = CLIENTS_DIR / f"{name}.conf"
|
|
447
|
+
|
|
448
|
+
if not client_file.exists():
|
|
449
|
+
typer.secho(f"Arquivo de configuração não encontrado: {client_file}", fg=typer.colors.RED)
|
|
450
|
+
typer.echo("\nDica: O cliente pode ter sido criado pelo módulo 'vpn' (inicial).")
|
|
451
|
+
typer.echo(f"Verifique se existe: {CLIENTS_DIR}")
|
|
452
|
+
raise typer.Exit(1)
|
|
453
|
+
|
|
454
|
+
typer.echo(f"\n{'='*60}")
|
|
455
|
+
typer.echo(f"Configuração do cliente: {name}")
|
|
456
|
+
typer.echo(f"Arquivo: {client_file}")
|
|
457
|
+
typer.echo(f"{'='*60}\n")
|
|
458
|
+
|
|
459
|
+
typer.echo(client_file.read_text())
|
|
460
|
+
|
|
461
|
+
# Detecta hostname/IP do servidor
|
|
462
|
+
import socket
|
|
463
|
+
hostname = socket.gethostname()
|
|
464
|
+
|
|
465
|
+
typer.echo(f"\n{'='*60}")
|
|
466
|
+
typer.secho("COMO COPIAR PARA WINDOWS/MAC/LINUX:", fg=typer.colors.CYAN, bold=True)
|
|
467
|
+
typer.echo(f"{'='*60}")
|
|
468
|
+
|
|
469
|
+
typer.echo("\n📋 Opção 1 - SCP (requer SSH configurado):")
|
|
470
|
+
typer.echo(f" scp usuario@{hostname}:{client_file} .")
|
|
471
|
+
typer.echo(f" # ou com IP: scp usuario@SEU_IP:{client_file} .")
|
|
472
|
+
|
|
473
|
+
typer.echo("\n📋 Opção 2 - Copiar conteúdo manualmente:")
|
|
474
|
+
typer.echo(" 1. Copie o texto acima (entre as linhas de '=')")
|
|
475
|
+
typer.echo(f" 2. No Windows, crie arquivo: {name}.conf")
|
|
476
|
+
typer.echo(" 3. Cole o conteúdo e salve")
|
|
477
|
+
|
|
478
|
+
typer.echo("\n📋 Opção 3 - SFTP (FileZilla, WinSCP):")
|
|
479
|
+
typer.echo(f" Conecte no servidor e baixe: {client_file}")
|
|
480
|
+
|
|
481
|
+
typer.echo("\n📱 Para celular (QR Code):")
|
|
482
|
+
typer.echo(f" qrencode -t ansiutf8 < {client_file}")
|
|
483
|
+
|
|
484
|
+
typer.echo(f"\n{'='*60}")
|
|
485
|
+
typer.secho("NO WINDOWS 11:", fg=typer.colors.GREEN, bold=True)
|
|
486
|
+
typer.echo(f"{'='*60}")
|
|
487
|
+
typer.echo(" 1. Baixe WireGuard: https://www.wireguard.com/install/")
|
|
488
|
+
typer.echo(" 2. Abra WireGuard → 'Import tunnel(s) from file...'")
|
|
489
|
+
typer.echo(f" 3. Selecione o arquivo {name}.conf")
|
|
490
|
+
typer.echo(" 4. Clique 'Activate' para conectar")
|
|
491
|
+
typer.echo("")
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def run(ctx: ExecutionContext) -> None:
|
|
495
|
+
"""Menu interativo para gerenciar clientes VPN."""
|
|
496
|
+
require_root(ctx)
|
|
497
|
+
|
|
498
|
+
while True:
|
|
499
|
+
typer.echo("\n" + "="*60)
|
|
500
|
+
typer.secho("Gerenciamento de Clientes VPN", fg=typer.colors.CYAN, bold=True)
|
|
501
|
+
typer.echo("="*60)
|
|
502
|
+
typer.echo("\n1. Adicionar cliente")
|
|
503
|
+
typer.echo("2. Listar clientes")
|
|
504
|
+
typer.echo("3. Remover cliente")
|
|
505
|
+
typer.echo("4. Mostrar configuração de cliente")
|
|
506
|
+
typer.echo("5. Sair")
|
|
507
|
+
|
|
508
|
+
choice = typer.prompt("\nEscolha uma opção", default="5")
|
|
509
|
+
|
|
510
|
+
try:
|
|
511
|
+
if choice == "1":
|
|
512
|
+
add_client(ctx)
|
|
513
|
+
elif choice == "2":
|
|
514
|
+
list_clients(ctx)
|
|
515
|
+
elif choice == "3":
|
|
516
|
+
remove_client(ctx)
|
|
517
|
+
elif choice == "4":
|
|
518
|
+
show_client_config(ctx)
|
|
519
|
+
elif choice == "5":
|
|
520
|
+
typer.echo("Saindo...")
|
|
521
|
+
break
|
|
522
|
+
else:
|
|
523
|
+
typer.secho("Opção inválida!", fg=typer.colors.RED)
|
|
524
|
+
except (KeyboardInterrupt, typer.Exit):
|
|
525
|
+
typer.echo("\n")
|
|
526
|
+
continue
|