raijin-server 0.2.41__py3-none-any.whl → 0.3.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.
Potentially problematic release.
This version of raijin-server might be problematic. Click here for more details.
- 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 +438 -0
- raijin_server-0.3.0.dist-info/METADATA +361 -0
- {raijin_server-0.2.41.dist-info → raijin_server-0.3.0.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.0.dist-info}/WHEEL +0 -0
- {raijin_server-0.2.41.dist-info → raijin_server-0.3.0.dist-info}/entry_points.txt +0 -0
- {raijin_server-0.2.41.dist-info → raijin_server-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {raijin_server-0.2.41.dist-info → raijin_server-0.3.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,438 @@
|
|
|
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
|
|
107
|
+
current_peer = {}
|
|
108
|
+
for line in content.split("\n"):
|
|
109
|
+
line = line.strip()
|
|
110
|
+
|
|
111
|
+
if line.startswith("# Cliente:"):
|
|
112
|
+
if current_peer:
|
|
113
|
+
clients.append(current_peer)
|
|
114
|
+
current_peer = {"name": line.split(":", 1)[1].strip()}
|
|
115
|
+
elif line.startswith("PublicKey =") and current_peer:
|
|
116
|
+
current_peer["public_key"] = line.split("=")[1].strip()
|
|
117
|
+
elif line.startswith("AllowedIPs =") and current_peer:
|
|
118
|
+
current_peer["ip"] = line.split("=")[1].strip()
|
|
119
|
+
|
|
120
|
+
if current_peer:
|
|
121
|
+
clients.append(current_peer)
|
|
122
|
+
|
|
123
|
+
return clients
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _get_next_client_ip(server_config: dict) -> str:
|
|
127
|
+
"""Calcula próximo IP disponível para cliente."""
|
|
128
|
+
network = server_config["network"]
|
|
129
|
+
base_ip = network.split("/")[0]
|
|
130
|
+
base_parts = base_ip.rsplit(".", 1)
|
|
131
|
+
base_network = base_parts[0]
|
|
132
|
+
|
|
133
|
+
existing_clients = _list_existing_clients()
|
|
134
|
+
used_ips = {client["ip"].split("/")[0] for client in existing_clients}
|
|
135
|
+
used_ips.add(base_ip) # IP do servidor
|
|
136
|
+
|
|
137
|
+
# Procura próximo IP disponível
|
|
138
|
+
for i in range(2, 255):
|
|
139
|
+
candidate = f"{base_network}.{i}"
|
|
140
|
+
if candidate not in used_ips:
|
|
141
|
+
return f"{candidate}/32"
|
|
142
|
+
|
|
143
|
+
typer.secho("Erro: Rede VPN cheia (254 clientes)", fg=typer.colors.RED)
|
|
144
|
+
raise typer.Exit(1)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _add_peer_to_server(name: str, public_key: str, client_ip: str, ctx: ExecutionContext) -> None:
|
|
148
|
+
"""Adiciona peer ao arquivo de configuração do servidor."""
|
|
149
|
+
content = WG0_CONF.read_text()
|
|
150
|
+
|
|
151
|
+
peer_block = f"""
|
|
152
|
+
# Cliente: {name}
|
|
153
|
+
[Peer]
|
|
154
|
+
PublicKey = {public_key}
|
|
155
|
+
AllowedIPs = {client_ip}
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
# Adiciona peer ao final
|
|
159
|
+
content += "\n" + peer_block
|
|
160
|
+
|
|
161
|
+
write_file(WG0_CONF, content, ctx)
|
|
162
|
+
|
|
163
|
+
# Recarrega configuração
|
|
164
|
+
typer.echo("Recarregando WireGuard...")
|
|
165
|
+
run_cmd(["systemctl", "restart", "wg-quick@wg0"], ctx, check=False)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _remove_peer_from_server(public_key: str, ctx: ExecutionContext) -> None:
|
|
169
|
+
"""Remove peer do arquivo de configuração do servidor."""
|
|
170
|
+
content = WG0_CONF.read_text()
|
|
171
|
+
|
|
172
|
+
# Remove bloco do peer
|
|
173
|
+
lines = content.split("\n")
|
|
174
|
+
new_lines = []
|
|
175
|
+
skip_until_next_section = False
|
|
176
|
+
|
|
177
|
+
for line in lines:
|
|
178
|
+
if f"PublicKey = {public_key}" in line:
|
|
179
|
+
skip_until_next_section = True
|
|
180
|
+
# Remove também o comentário anterior
|
|
181
|
+
if new_lines and new_lines[-1].startswith("# Cliente:"):
|
|
182
|
+
new_lines.pop()
|
|
183
|
+
if new_lines and new_lines[-1].strip() == "":
|
|
184
|
+
new_lines.pop()
|
|
185
|
+
if new_lines and new_lines[-1].strip() == "[Peer]":
|
|
186
|
+
new_lines.pop()
|
|
187
|
+
continue
|
|
188
|
+
|
|
189
|
+
if skip_until_next_section:
|
|
190
|
+
if line.startswith("[") or line.startswith("# Cliente:"):
|
|
191
|
+
skip_until_next_section = False
|
|
192
|
+
else:
|
|
193
|
+
continue
|
|
194
|
+
|
|
195
|
+
new_lines.append(line)
|
|
196
|
+
|
|
197
|
+
write_file(WG0_CONF, "\n".join(new_lines), ctx)
|
|
198
|
+
|
|
199
|
+
# Recarrega configuração
|
|
200
|
+
typer.echo("Recarregando WireGuard...")
|
|
201
|
+
run_cmd(["systemctl", "restart", "wg-quick@wg0"], ctx, check=False)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _create_client_config(
|
|
205
|
+
name: str,
|
|
206
|
+
private_key: str,
|
|
207
|
+
client_ip: str,
|
|
208
|
+
server_config: dict,
|
|
209
|
+
) -> str:
|
|
210
|
+
"""Cria arquivo de configuração do cliente."""
|
|
211
|
+
server_ip = server_config["address"]
|
|
212
|
+
server_network = server_config["network"]
|
|
213
|
+
dns = server_config["dns"]
|
|
214
|
+
endpoint = server_config["endpoint"]
|
|
215
|
+
server_public_key = server_config["public_key"]
|
|
216
|
+
server_port = server_config["port"]
|
|
217
|
+
|
|
218
|
+
if not endpoint:
|
|
219
|
+
endpoint = typer.prompt("Endpoint público do servidor (IP ou domínio)")
|
|
220
|
+
|
|
221
|
+
config = f"""[Interface]
|
|
222
|
+
# Cliente: {name}
|
|
223
|
+
PrivateKey = {private_key}
|
|
224
|
+
Address = {client_ip}
|
|
225
|
+
DNS = {dns}
|
|
226
|
+
|
|
227
|
+
[Peer]
|
|
228
|
+
PublicKey = {server_public_key}
|
|
229
|
+
Endpoint = {endpoint}:{server_port}
|
|
230
|
+
AllowedIPs = {server_network}, 10.0.0.0/8
|
|
231
|
+
PersistentKeepalive = 25
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
return config
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def add_client(ctx: ExecutionContext) -> None:
|
|
238
|
+
"""Adiciona novo cliente VPN."""
|
|
239
|
+
require_root(ctx)
|
|
240
|
+
|
|
241
|
+
typer.echo("Adicionando novo cliente WireGuard VPN...\n")
|
|
242
|
+
|
|
243
|
+
# Lê configuração do servidor
|
|
244
|
+
server_config = _read_server_config()
|
|
245
|
+
|
|
246
|
+
# Lista clientes existentes
|
|
247
|
+
existing_clients = _list_existing_clients()
|
|
248
|
+
if existing_clients:
|
|
249
|
+
typer.echo("Clientes existentes:")
|
|
250
|
+
for client in existing_clients:
|
|
251
|
+
typer.echo(f" - {client['name']} ({client['ip']})")
|
|
252
|
+
typer.echo("")
|
|
253
|
+
|
|
254
|
+
# Solicita informações do novo cliente
|
|
255
|
+
name = typer.prompt("Nome do cliente")
|
|
256
|
+
|
|
257
|
+
# Verifica se já existe
|
|
258
|
+
if any(c["name"] == name for c in existing_clients):
|
|
259
|
+
typer.secho(f"Cliente '{name}' já existe!", fg=typer.colors.RED)
|
|
260
|
+
raise typer.Exit(1)
|
|
261
|
+
|
|
262
|
+
# Gera chaves
|
|
263
|
+
typer.echo("Gerando par de chaves...")
|
|
264
|
+
private_key, public_key = _generate_keypair()
|
|
265
|
+
|
|
266
|
+
# Calcula próximo IP
|
|
267
|
+
client_ip = _get_next_client_ip(server_config)
|
|
268
|
+
|
|
269
|
+
typer.echo(f"IP atribuído: {client_ip}")
|
|
270
|
+
|
|
271
|
+
# Cria diretório de clientes
|
|
272
|
+
CLIENTS_DIR.mkdir(parents=True, exist_ok=True)
|
|
273
|
+
|
|
274
|
+
# Cria configuração do cliente
|
|
275
|
+
client_config = _create_client_config(name, private_key, client_ip, server_config)
|
|
276
|
+
|
|
277
|
+
client_file = CLIENTS_DIR / f"{name}.conf"
|
|
278
|
+
write_file(client_file, client_config, ctx)
|
|
279
|
+
client_file.chmod(0o600)
|
|
280
|
+
|
|
281
|
+
# Salva chaves separadamente
|
|
282
|
+
key_file = CLIENTS_DIR / f"{name}.key"
|
|
283
|
+
write_file(key_file, f"Private: {private_key}\nPublic: {public_key}\n", ctx)
|
|
284
|
+
key_file.chmod(0o600)
|
|
285
|
+
|
|
286
|
+
# Adiciona peer ao servidor
|
|
287
|
+
_add_peer_to_server(name, public_key, client_ip, ctx)
|
|
288
|
+
|
|
289
|
+
typer.secho(f"\n✓ Cliente '{name}' adicionado com sucesso!", fg=typer.colors.GREEN, bold=True)
|
|
290
|
+
typer.echo(f"\nArquivo de configuração: {client_file}")
|
|
291
|
+
typer.echo("\nPara usar:")
|
|
292
|
+
typer.echo(f" 1. Copie o arquivo: scp root@servidor:{client_file} .")
|
|
293
|
+
typer.echo(f" 2. Importe no WireGuard (Windows/Mac/Linux)")
|
|
294
|
+
typer.echo(f" 3. Ou gere QR code: qrencode -t ansiutf8 {client_file}")
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def list_clients(ctx: ExecutionContext) -> None:
|
|
298
|
+
"""Lista todos os clientes VPN."""
|
|
299
|
+
require_root(ctx)
|
|
300
|
+
|
|
301
|
+
clients = _list_existing_clients()
|
|
302
|
+
|
|
303
|
+
if not clients:
|
|
304
|
+
typer.echo("Nenhum cliente configurado.")
|
|
305
|
+
return
|
|
306
|
+
|
|
307
|
+
typer.secho(f"\n{'Nome':<20} {'IP':<20} {'Chave Pública'}", fg=typer.colors.CYAN, bold=True)
|
|
308
|
+
typer.echo("=" * 80)
|
|
309
|
+
|
|
310
|
+
for client in clients:
|
|
311
|
+
name = client.get("name", "Desconhecido")
|
|
312
|
+
ip = client.get("ip", "N/A")
|
|
313
|
+
pubkey = client.get("public_key", "N/A")[:40] + "..."
|
|
314
|
+
typer.echo(f"{name:<20} {ip:<20} {pubkey}")
|
|
315
|
+
|
|
316
|
+
typer.echo(f"\nTotal: {len(clients)} cliente(s)")
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def remove_client(ctx: ExecutionContext) -> None:
|
|
320
|
+
"""Remove cliente VPN."""
|
|
321
|
+
require_root(ctx)
|
|
322
|
+
|
|
323
|
+
clients = _list_existing_clients()
|
|
324
|
+
|
|
325
|
+
if not clients:
|
|
326
|
+
typer.echo("Nenhum cliente configurado.")
|
|
327
|
+
return
|
|
328
|
+
|
|
329
|
+
# Mostra lista
|
|
330
|
+
typer.echo("Clientes disponíveis:")
|
|
331
|
+
for i, client in enumerate(clients, 1):
|
|
332
|
+
typer.echo(f" {i}. {client['name']} ({client['ip']})")
|
|
333
|
+
|
|
334
|
+
# Solicita nome
|
|
335
|
+
name = typer.prompt("\nNome do cliente para remover")
|
|
336
|
+
|
|
337
|
+
# Encontra cliente
|
|
338
|
+
client = next((c for c in clients if c["name"] == name), None)
|
|
339
|
+
if not client:
|
|
340
|
+
typer.secho(f"Cliente '{name}' não encontrado!", fg=typer.colors.RED)
|
|
341
|
+
raise typer.Exit(1)
|
|
342
|
+
|
|
343
|
+
# Confirma remoção
|
|
344
|
+
confirm = typer.confirm(
|
|
345
|
+
f"Remover cliente '{name}' ({client['ip']})?",
|
|
346
|
+
default=False
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
if not confirm:
|
|
350
|
+
typer.echo("Operação cancelada.")
|
|
351
|
+
return
|
|
352
|
+
|
|
353
|
+
# Remove do servidor
|
|
354
|
+
_remove_peer_from_server(client["public_key"], ctx)
|
|
355
|
+
|
|
356
|
+
# Remove arquivos locais
|
|
357
|
+
client_file = CLIENTS_DIR / f"{name}.conf"
|
|
358
|
+
key_file = CLIENTS_DIR / f"{name}.key"
|
|
359
|
+
|
|
360
|
+
if client_file.exists():
|
|
361
|
+
client_file.unlink()
|
|
362
|
+
if key_file.exists():
|
|
363
|
+
key_file.unlink()
|
|
364
|
+
|
|
365
|
+
typer.secho(f"\n✓ Cliente '{name}' removido com sucesso!", fg=typer.colors.GREEN, bold=True)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def show_client_config(ctx: ExecutionContext) -> None:
|
|
369
|
+
"""Mostra configuração de um cliente."""
|
|
370
|
+
require_root(ctx)
|
|
371
|
+
|
|
372
|
+
clients = _list_existing_clients()
|
|
373
|
+
|
|
374
|
+
if not clients:
|
|
375
|
+
typer.echo("Nenhum cliente configurado.")
|
|
376
|
+
return
|
|
377
|
+
|
|
378
|
+
# Lista clientes
|
|
379
|
+
typer.echo("Clientes disponíveis:")
|
|
380
|
+
for client in clients:
|
|
381
|
+
typer.echo(f" - {client['name']}")
|
|
382
|
+
|
|
383
|
+
# Solicita nome
|
|
384
|
+
name = typer.prompt("\nNome do cliente")
|
|
385
|
+
|
|
386
|
+
client_file = CLIENTS_DIR / f"{name}.conf"
|
|
387
|
+
|
|
388
|
+
if not client_file.exists():
|
|
389
|
+
typer.secho(f"Arquivo de configuração não encontrado: {client_file}", fg=typer.colors.RED)
|
|
390
|
+
raise typer.Exit(1)
|
|
391
|
+
|
|
392
|
+
typer.echo(f"\n{'='*60}")
|
|
393
|
+
typer.echo(f"Configuração do cliente: {name}")
|
|
394
|
+
typer.echo(f"Arquivo: {client_file}")
|
|
395
|
+
typer.echo(f"{'='*60}\n")
|
|
396
|
+
|
|
397
|
+
typer.echo(client_file.read_text())
|
|
398
|
+
|
|
399
|
+
typer.echo(f"\n{'='*60}")
|
|
400
|
+
typer.echo("\nPara copiar:")
|
|
401
|
+
typer.echo(f" scp root@servidor:{client_file} .")
|
|
402
|
+
typer.echo("\nPara QR code (mobile):")
|
|
403
|
+
typer.echo(f" qrencode -t ansiutf8 {client_file}")
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def run(ctx: ExecutionContext) -> None:
|
|
407
|
+
"""Menu interativo para gerenciar clientes VPN."""
|
|
408
|
+
require_root(ctx)
|
|
409
|
+
|
|
410
|
+
while True:
|
|
411
|
+
typer.echo("\n" + "="*60)
|
|
412
|
+
typer.secho("Gerenciamento de Clientes VPN", fg=typer.colors.CYAN, bold=True)
|
|
413
|
+
typer.echo("="*60)
|
|
414
|
+
typer.echo("\n1. Adicionar cliente")
|
|
415
|
+
typer.echo("2. Listar clientes")
|
|
416
|
+
typer.echo("3. Remover cliente")
|
|
417
|
+
typer.echo("4. Mostrar configuração de cliente")
|
|
418
|
+
typer.echo("5. Sair")
|
|
419
|
+
|
|
420
|
+
choice = typer.prompt("\nEscolha uma opção", default="5")
|
|
421
|
+
|
|
422
|
+
try:
|
|
423
|
+
if choice == "1":
|
|
424
|
+
add_client(ctx)
|
|
425
|
+
elif choice == "2":
|
|
426
|
+
list_clients(ctx)
|
|
427
|
+
elif choice == "3":
|
|
428
|
+
remove_client(ctx)
|
|
429
|
+
elif choice == "4":
|
|
430
|
+
show_client_config(ctx)
|
|
431
|
+
elif choice == "5":
|
|
432
|
+
typer.echo("Saindo...")
|
|
433
|
+
break
|
|
434
|
+
else:
|
|
435
|
+
typer.secho("Opção inválida!", fg=typer.colors.RED)
|
|
436
|
+
except (KeyboardInterrupt, typer.Exit):
|
|
437
|
+
typer.echo("\n")
|
|
438
|
+
continue
|