manta-node 0.5b0.dev337__py3-none-any.whl → 0.5b0.dev342__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.
- manta_node/cli/commands/network.py +1174 -0
- manta_node/cli/main.py +4 -0
- manta_node/domain/value_objects/__init__.py +5 -0
- manta_node/domain/value_objects/network.py +102 -0
- manta_node/infrastructure/config/node_config_manager.py +57 -1
- manta_node/infrastructure/grpc/client.py +113 -0
- manta_node/infrastructure/network/__init__.py +17 -0
- manta_node/infrastructure/network/event_registry.py +47 -0
- manta_node/infrastructure/network/factory.py +134 -0
- manta_node/infrastructure/network/protocols.py +85 -0
- manta_node/infrastructure/network/wireguard_provider.py +1027 -0
- {manta_node-0.5b0.dev337.dist-info → manta_node-0.5b0.dev342.dist-info}/METADATA +5 -1
- {manta_node-0.5b0.dev337.dist-info → manta_node-0.5b0.dev342.dist-info}/RECORD +17 -9
- {manta_node-0.5b0.dev337.dist-info → manta_node-0.5b0.dev342.dist-info}/WHEEL +0 -0
- {manta_node-0.5b0.dev337.dist-info → manta_node-0.5b0.dev342.dist-info}/entry_points.txt +0 -0
- {manta_node-0.5b0.dev337.dist-info → manta_node-0.5b0.dev342.dist-info}/licenses/LICENSE +0 -0
- {manta_node-0.5b0.dev337.dist-info → manta_node-0.5b0.dev342.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1174 @@
|
|
|
1
|
+
"""Network management command implementation for WireGuard SDN.
|
|
2
|
+
|
|
3
|
+
Provides CLI tools for troubleshooting and managing WireGuard networks on the node.
|
|
4
|
+
Uses wireguard-tools Python library for programmatic access to WireGuard state.
|
|
5
|
+
Uses icmplib for network connectivity testing.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.panel import Panel
|
|
14
|
+
from rich.syntax import Syntax
|
|
15
|
+
from rich.table import Table
|
|
16
|
+
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _load_node_config():
|
|
21
|
+
"""Load the node's config manager and node_id.
|
|
22
|
+
|
|
23
|
+
Returns (config_manager, node_id) or (None, None) if no config found.
|
|
24
|
+
"""
|
|
25
|
+
import glob
|
|
26
|
+
|
|
27
|
+
config_files = glob.glob(str(Path.home() / ".manta/nodes/*.toml"))
|
|
28
|
+
if not config_files:
|
|
29
|
+
return None, None
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
from manta_node.infrastructure.config.node_config_manager import (
|
|
33
|
+
NodeConfigManager,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
config_manager = NodeConfigManager(config_files[0])
|
|
37
|
+
node_id = (
|
|
38
|
+
config_manager.config.identity.alias
|
|
39
|
+
or config_manager.config.metadata.name
|
|
40
|
+
or Path(config_files[0]).stem
|
|
41
|
+
)
|
|
42
|
+
return config_manager, node_id
|
|
43
|
+
except Exception as e:
|
|
44
|
+
console.print(f"[dim]Warning: Could not load node config: {e}[/dim]")
|
|
45
|
+
return None, None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def list_interfaces() -> list[str]:
|
|
49
|
+
"""List all WireGuard interfaces using wireguard-tools."""
|
|
50
|
+
try:
|
|
51
|
+
from wireguard_tools import WireguardDevice
|
|
52
|
+
|
|
53
|
+
devices = list(WireguardDevice.list())
|
|
54
|
+
return [device.interface for device in devices]
|
|
55
|
+
except ImportError:
|
|
56
|
+
console.print("[red]Error: wireguard-tools library not installed[/red]")
|
|
57
|
+
console.print("Install with: [cyan]pip install wireguard-tools[/cyan]")
|
|
58
|
+
return []
|
|
59
|
+
except Exception as e:
|
|
60
|
+
console.print(f"[red]Error listing interfaces: {e}[/red]")
|
|
61
|
+
return []
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def get_device_info(interface: str) -> dict | None:
|
|
65
|
+
"""Get detailed device information using wireguard-tools."""
|
|
66
|
+
try:
|
|
67
|
+
from wireguard_tools import WireguardDevice, WireguardKey
|
|
68
|
+
|
|
69
|
+
device = WireguardDevice.get(interface)
|
|
70
|
+
config = device.get_config()
|
|
71
|
+
|
|
72
|
+
public_key_str = None
|
|
73
|
+
if hasattr(config, "private_key") and config.private_key:
|
|
74
|
+
# Derive public key from private key
|
|
75
|
+
private_key_obj = WireguardKey(config.private_key)
|
|
76
|
+
public_key_str = str(private_key_obj.public_key())
|
|
77
|
+
elif hasattr(config, "public_key") and config.public_key:
|
|
78
|
+
# Fallback when public_key is exposed directly on the config object
|
|
79
|
+
public_key_str = str(config.public_key)
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
"interface": device.interface,
|
|
83
|
+
"public_key": public_key_str,
|
|
84
|
+
"listen_port": config.listen_port
|
|
85
|
+
if hasattr(config, "listen_port")
|
|
86
|
+
else None,
|
|
87
|
+
"fwmark": config.fwmark if hasattr(config, "fwmark") else None,
|
|
88
|
+
"peers": list(config.peers.values()) if hasattr(config, "peers") else [],
|
|
89
|
+
}
|
|
90
|
+
except ImportError:
|
|
91
|
+
console.print("[red]Error: wireguard-tools library not installed[/red]")
|
|
92
|
+
console.print("Install with: [cyan]pip install wireguard-tools[/cyan]")
|
|
93
|
+
return None
|
|
94
|
+
except Exception as e:
|
|
95
|
+
console.print(f"[red]Error getting device info: {e}[/red]")
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def list_networks_command(args: list[str]) -> int:
|
|
100
|
+
"""List all WireGuard networks on this node."""
|
|
101
|
+
parser = argparse.ArgumentParser(
|
|
102
|
+
prog="manta_node network list", description="List all active WireGuard networks"
|
|
103
|
+
)
|
|
104
|
+
parser.add_argument(
|
|
105
|
+
"--plain", action="store_true", help="Use plain text output instead of table"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
parsed_args = parser.parse_args(args)
|
|
110
|
+
except SystemExit:
|
|
111
|
+
return 1
|
|
112
|
+
|
|
113
|
+
interfaces = list_interfaces()
|
|
114
|
+
|
|
115
|
+
if not interfaces:
|
|
116
|
+
console.print("[yellow]No WireGuard interfaces found.[/yellow]")
|
|
117
|
+
console.print(
|
|
118
|
+
"\nNetworks are created when swarms with network requirements are deployed."
|
|
119
|
+
)
|
|
120
|
+
return 0
|
|
121
|
+
|
|
122
|
+
if parsed_args.plain:
|
|
123
|
+
console.print(f"Found {len(interfaces)} WireGuard interface(s):")
|
|
124
|
+
for iface in interfaces:
|
|
125
|
+
console.print(f" - {iface}")
|
|
126
|
+
else:
|
|
127
|
+
table = Table(title="WireGuard Networks")
|
|
128
|
+
table.add_column("Interface", style="cyan")
|
|
129
|
+
table.add_column("Network ID", style="magenta")
|
|
130
|
+
table.add_column("Status", style="green")
|
|
131
|
+
|
|
132
|
+
for iface in interfaces:
|
|
133
|
+
# Extract network_id from interface name (format: wg-{swarm_id})
|
|
134
|
+
network_id = iface if iface.startswith("wg-") else f"wg-{iface}"
|
|
135
|
+
table.add_row(iface, network_id, "active")
|
|
136
|
+
|
|
137
|
+
console.print(table)
|
|
138
|
+
|
|
139
|
+
return 0
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def show_network_command(args: list[str]) -> int:
|
|
143
|
+
"""Show detailed information about a specific network."""
|
|
144
|
+
parser = argparse.ArgumentParser(
|
|
145
|
+
prog="manta_node network show",
|
|
146
|
+
description="Show detailed information about a WireGuard network",
|
|
147
|
+
)
|
|
148
|
+
parser.add_argument("interface", help="Interface name (e.g., wg-abc123)")
|
|
149
|
+
parser.add_argument("--json", action="store_true", help="Output in JSON format")
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
parsed_args = parser.parse_args(args)
|
|
153
|
+
except SystemExit:
|
|
154
|
+
return 1
|
|
155
|
+
|
|
156
|
+
info = get_device_info(parsed_args.interface)
|
|
157
|
+
|
|
158
|
+
if not info:
|
|
159
|
+
console.print(
|
|
160
|
+
f"[red]Interface '{parsed_args.interface}' not found or not accessible[/red]"
|
|
161
|
+
)
|
|
162
|
+
return 1
|
|
163
|
+
|
|
164
|
+
if parsed_args.json:
|
|
165
|
+
import json
|
|
166
|
+
|
|
167
|
+
console.print(json.dumps(info, indent=2))
|
|
168
|
+
else:
|
|
169
|
+
# Display formatted information
|
|
170
|
+
console.print(
|
|
171
|
+
Panel(
|
|
172
|
+
f"[bold cyan]{info['interface']}[/bold cyan]",
|
|
173
|
+
title="WireGuard Network",
|
|
174
|
+
expand=False,
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
console.print("\n[bold]Interface Configuration:[/bold]")
|
|
179
|
+
console.print(f" Public Key: {info.get('public_key', 'N/A')}")
|
|
180
|
+
console.print(f" Listen Port: {info.get('listen_port', 'N/A')}")
|
|
181
|
+
if info.get("fwmark"):
|
|
182
|
+
console.print(f" Firewall Mark: {info['fwmark']}")
|
|
183
|
+
|
|
184
|
+
peer_count = len(info.get("peers", []))
|
|
185
|
+
console.print(f"\n[bold]Peers:[/bold] {peer_count} connected")
|
|
186
|
+
|
|
187
|
+
if peer_count == 0:
|
|
188
|
+
console.print(" [dim]No peers configured[/dim]")
|
|
189
|
+
|
|
190
|
+
return 0
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def list_peers_command(args: list[str]) -> int:
|
|
194
|
+
"""List all peers for a specific network."""
|
|
195
|
+
parser = argparse.ArgumentParser(
|
|
196
|
+
prog="manta_node network peers",
|
|
197
|
+
description="List all peers in a WireGuard network",
|
|
198
|
+
)
|
|
199
|
+
parser.add_argument("interface", help="Interface name (e.g., wg-abc123)")
|
|
200
|
+
parser.add_argument(
|
|
201
|
+
"--plain", action="store_true", help="Use plain text output instead of table"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
parsed_args = parser.parse_args(args)
|
|
206
|
+
except SystemExit:
|
|
207
|
+
return 1
|
|
208
|
+
|
|
209
|
+
info = get_device_info(parsed_args.interface)
|
|
210
|
+
|
|
211
|
+
if not info:
|
|
212
|
+
console.print(f"[red]Interface '{parsed_args.interface}' not found[/red]")
|
|
213
|
+
return 1
|
|
214
|
+
|
|
215
|
+
peers = info.get("peers", [])
|
|
216
|
+
|
|
217
|
+
if not peers:
|
|
218
|
+
console.print(
|
|
219
|
+
f"[yellow]No peers configured for {parsed_args.interface}[/yellow]"
|
|
220
|
+
)
|
|
221
|
+
return 0
|
|
222
|
+
|
|
223
|
+
if parsed_args.plain:
|
|
224
|
+
console.print(f"Peers for {parsed_args.interface}:")
|
|
225
|
+
for i, peer in enumerate(peers, 1):
|
|
226
|
+
console.print(f"\n Peer {i}:")
|
|
227
|
+
for key, value in peer.items():
|
|
228
|
+
console.print(f" {key}: {value}")
|
|
229
|
+
else:
|
|
230
|
+
table = Table(title=f"Peers for {parsed_args.interface}")
|
|
231
|
+
table.add_column("Public Key", style="cyan", max_width=40)
|
|
232
|
+
table.add_column("Endpoint", style="magenta")
|
|
233
|
+
table.add_column("Allowed IPs", style="green")
|
|
234
|
+
table.add_column("Latest Handshake", style="yellow")
|
|
235
|
+
table.add_column("Transfer", style="blue")
|
|
236
|
+
|
|
237
|
+
for peer in peers:
|
|
238
|
+
# Handle both wireguard-tools format and fallback format
|
|
239
|
+
if hasattr(peer, "public_key"):
|
|
240
|
+
# wireguard-tools WireguardPeer object
|
|
241
|
+
pubkey = str(peer.public_key)[:20] + "..."
|
|
242
|
+
endpoint = str(peer.endpoint) if peer.endpoint else "N/A"
|
|
243
|
+
allowed_ips = ", ".join(str(ip) for ip in peer.allowed_ips)
|
|
244
|
+
handshake = (
|
|
245
|
+
str(peer.latest_handshake) if peer.latest_handshake else "Never"
|
|
246
|
+
)
|
|
247
|
+
transfer = (
|
|
248
|
+
f"↓{peer.rx_bytes} ↑{peer.tx_bytes}"
|
|
249
|
+
if hasattr(peer, "rx_bytes")
|
|
250
|
+
else "N/A"
|
|
251
|
+
)
|
|
252
|
+
else:
|
|
253
|
+
# Fallback dict format
|
|
254
|
+
pubkey = peer.get("public_key", "N/A")[:20] + "..."
|
|
255
|
+
endpoint = peer.get("endpoint", "N/A")
|
|
256
|
+
allowed_ips = peer.get("allowed ips", "N/A")
|
|
257
|
+
handshake = peer.get("latest handshake", "Never")
|
|
258
|
+
transfer_rx = peer.get("transfer", {}).get("received", "0 B")
|
|
259
|
+
transfer_tx = peer.get("transfer", {}).get("sent", "0 B")
|
|
260
|
+
transfer = f"↓{transfer_rx} ↑{transfer_tx}"
|
|
261
|
+
|
|
262
|
+
table.add_row(pubkey, endpoint, allowed_ips, handshake, transfer)
|
|
263
|
+
|
|
264
|
+
console.print(table)
|
|
265
|
+
|
|
266
|
+
return 0
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def show_peer_command(args: list[str]) -> int:
|
|
270
|
+
"""Show detailed information about a specific peer."""
|
|
271
|
+
parser = argparse.ArgumentParser(
|
|
272
|
+
prog="manta_node network peer",
|
|
273
|
+
description="Show detailed information about a peer",
|
|
274
|
+
)
|
|
275
|
+
parser.add_argument("interface", help="Interface name (e.g., wg-abc123)")
|
|
276
|
+
parser.add_argument("peer_pubkey", help="Peer public key (or partial match)")
|
|
277
|
+
|
|
278
|
+
try:
|
|
279
|
+
parsed_args = parser.parse_args(args)
|
|
280
|
+
except SystemExit:
|
|
281
|
+
return 1
|
|
282
|
+
|
|
283
|
+
info = get_device_info(parsed_args.interface)
|
|
284
|
+
|
|
285
|
+
if not info:
|
|
286
|
+
console.print(f"[red]Interface '{parsed_args.interface}' not found[/red]")
|
|
287
|
+
return 1
|
|
288
|
+
|
|
289
|
+
# Find matching peer
|
|
290
|
+
peers = info.get("peers", [])
|
|
291
|
+
matching_peer = None
|
|
292
|
+
|
|
293
|
+
for peer in peers:
|
|
294
|
+
pubkey = (
|
|
295
|
+
str(peer.public_key)
|
|
296
|
+
if hasattr(peer, "public_key")
|
|
297
|
+
else peer.get("public_key", "")
|
|
298
|
+
)
|
|
299
|
+
if (
|
|
300
|
+
pubkey.startswith(parsed_args.peer_pubkey)
|
|
301
|
+
or parsed_args.peer_pubkey in pubkey
|
|
302
|
+
):
|
|
303
|
+
matching_peer = peer
|
|
304
|
+
break
|
|
305
|
+
|
|
306
|
+
if not matching_peer:
|
|
307
|
+
console.print(
|
|
308
|
+
f"[red]Peer with public key matching '{parsed_args.peer_pubkey}' not found[/red]"
|
|
309
|
+
)
|
|
310
|
+
return 1
|
|
311
|
+
|
|
312
|
+
# Display peer information
|
|
313
|
+
console.print(
|
|
314
|
+
Panel(
|
|
315
|
+
"[bold cyan]Peer Details[/bold cyan]",
|
|
316
|
+
title=f"Network: {parsed_args.interface}",
|
|
317
|
+
expand=False,
|
|
318
|
+
)
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
if hasattr(matching_peer, "public_key"):
|
|
322
|
+
console.print("\n[bold]Identity:[/bold]")
|
|
323
|
+
console.print(f" Public Key: {matching_peer.public_key}")
|
|
324
|
+
console.print("\n[bold]Connection:[/bold]")
|
|
325
|
+
console.print(f" Endpoint: {matching_peer.endpoint or 'N/A'}")
|
|
326
|
+
console.print(
|
|
327
|
+
f" Allowed IPs: {', '.join(str(ip) for ip in matching_peer.allowed_ips)}"
|
|
328
|
+
)
|
|
329
|
+
console.print(
|
|
330
|
+
f" Latest Handshake: {matching_peer.latest_handshake or 'Never'}"
|
|
331
|
+
)
|
|
332
|
+
console.print(
|
|
333
|
+
f" Persistent Keepalive: {matching_peer.persistent_keepalive or 'Off'}"
|
|
334
|
+
)
|
|
335
|
+
if hasattr(matching_peer, "rx_bytes"):
|
|
336
|
+
console.print("\n[bold]Transfer:[/bold]")
|
|
337
|
+
console.print(f" Received: {matching_peer.rx_bytes} bytes")
|
|
338
|
+
console.print(f" Sent: {matching_peer.tx_bytes} bytes")
|
|
339
|
+
else:
|
|
340
|
+
for key, value in matching_peer.items():
|
|
341
|
+
console.print(f" {key}: {value}")
|
|
342
|
+
|
|
343
|
+
return 0
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def test_connectivity_command(args: list[str]) -> int:
|
|
347
|
+
"""Test connectivity to a peer."""
|
|
348
|
+
parser = argparse.ArgumentParser(
|
|
349
|
+
prog="manta_node network test",
|
|
350
|
+
description="Test connectivity to a peer via WireGuard",
|
|
351
|
+
)
|
|
352
|
+
parser.add_argument("interface", help="Interface name (e.g., wg-abc123)")
|
|
353
|
+
parser.add_argument("peer_ip", help="Peer IP address to ping")
|
|
354
|
+
parser.add_argument(
|
|
355
|
+
"--count",
|
|
356
|
+
type=int,
|
|
357
|
+
default=4,
|
|
358
|
+
help="Number of ping packets to send (default: 4)",
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
try:
|
|
362
|
+
parsed_args = parser.parse_args(args)
|
|
363
|
+
except SystemExit:
|
|
364
|
+
return 1
|
|
365
|
+
|
|
366
|
+
# Check interface exists
|
|
367
|
+
interfaces = list_interfaces()
|
|
368
|
+
if parsed_args.interface not in interfaces:
|
|
369
|
+
console.print(f"[red]Interface '{parsed_args.interface}' not found[/red]")
|
|
370
|
+
return 1
|
|
371
|
+
|
|
372
|
+
console.print(
|
|
373
|
+
f"Testing connectivity to {parsed_args.peer_ip} via {parsed_args.interface}..."
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
try:
|
|
377
|
+
from icmplib import ping
|
|
378
|
+
|
|
379
|
+
# Try unprivileged ping first (works if ping_group_range is configured)
|
|
380
|
+
# Fall back to privileged mode if that fails
|
|
381
|
+
host = None
|
|
382
|
+
privileged_mode = False
|
|
383
|
+
|
|
384
|
+
try:
|
|
385
|
+
host = ping(
|
|
386
|
+
parsed_args.peer_ip,
|
|
387
|
+
count=parsed_args.count,
|
|
388
|
+
interval=1,
|
|
389
|
+
timeout=2,
|
|
390
|
+
privileged=False, # Try unprivileged first
|
|
391
|
+
)
|
|
392
|
+
except (PermissionError, OSError):
|
|
393
|
+
# Unprivileged failed, try privileged mode
|
|
394
|
+
try:
|
|
395
|
+
host = ping(
|
|
396
|
+
parsed_args.peer_ip,
|
|
397
|
+
count=parsed_args.count,
|
|
398
|
+
interval=1,
|
|
399
|
+
timeout=2,
|
|
400
|
+
privileged=True, # Requires root/sudo or CAP_NET_RAW
|
|
401
|
+
)
|
|
402
|
+
privileged_mode = True
|
|
403
|
+
except (PermissionError, OSError):
|
|
404
|
+
console.print(
|
|
405
|
+
"[red]Error: Insufficient permissions for ICMP ping[/red]"
|
|
406
|
+
)
|
|
407
|
+
console.print("\n[bold]To enable unprivileged ping, configure:[/bold]")
|
|
408
|
+
console.print(
|
|
409
|
+
" [cyan]echo 'net.ipv4.ping_group_range = 0 2147483647' | sudo tee -a /etc/sysctl.conf[/cyan]"
|
|
410
|
+
)
|
|
411
|
+
console.print(" [cyan]sudo sysctl -p[/cyan]")
|
|
412
|
+
console.print("\n[bold]Or run with sudo:[/bold]")
|
|
413
|
+
console.print(" [cyan]sudo manta_node network test ...[/cyan]")
|
|
414
|
+
return 1
|
|
415
|
+
|
|
416
|
+
# Display results
|
|
417
|
+
console.print(f"\n[bold]PING {parsed_args.peer_ip}[/bold]")
|
|
418
|
+
if privileged_mode:
|
|
419
|
+
console.print(" [dim](using privileged ICMP sockets)[/dim]")
|
|
420
|
+
else:
|
|
421
|
+
console.print(" [dim](using unprivileged ping)[/dim]")
|
|
422
|
+
|
|
423
|
+
console.print(f" Packets sent: {host.packets_sent}")
|
|
424
|
+
console.print(f" Packets received: {host.packets_received}")
|
|
425
|
+
console.print(f" Packet loss: {host.packet_loss * 100:.1f}%")
|
|
426
|
+
|
|
427
|
+
if host.packets_received > 0:
|
|
428
|
+
console.print(f" Min RTT: {host.min_rtt:.2f} ms")
|
|
429
|
+
console.print(f" Avg RTT: {host.avg_rtt:.2f} ms")
|
|
430
|
+
console.print(f" Max RTT: {host.max_rtt:.2f} ms")
|
|
431
|
+
|
|
432
|
+
if host.is_alive:
|
|
433
|
+
console.print("\n[green]✓ Connectivity successful[/green]")
|
|
434
|
+
return 0
|
|
435
|
+
else:
|
|
436
|
+
console.print("\n[red]✗ Connectivity failed (no response)[/red]")
|
|
437
|
+
return 1
|
|
438
|
+
|
|
439
|
+
except ImportError:
|
|
440
|
+
console.print("[red]Error: icmplib library not installed[/red]")
|
|
441
|
+
console.print("Install with: [cyan]pip install icmplib[/cyan]")
|
|
442
|
+
return 1
|
|
443
|
+
except Exception as e:
|
|
444
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
445
|
+
return 1
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def show_config_command(args: list[str]) -> int:
|
|
449
|
+
"""Show WireGuard configuration file."""
|
|
450
|
+
parser = argparse.ArgumentParser(
|
|
451
|
+
prog="manta_node network config",
|
|
452
|
+
description="Show WireGuard configuration file",
|
|
453
|
+
)
|
|
454
|
+
parser.add_argument("interface", help="Interface name (e.g., wg-abc123)")
|
|
455
|
+
|
|
456
|
+
try:
|
|
457
|
+
parsed_args = parser.parse_args(args)
|
|
458
|
+
except SystemExit:
|
|
459
|
+
return 1
|
|
460
|
+
|
|
461
|
+
config_path = Path(f"/etc/wireguard/{parsed_args.interface}.conf")
|
|
462
|
+
|
|
463
|
+
if not config_path.exists():
|
|
464
|
+
console.print(f"[red]Configuration file not found: {config_path}[/red]")
|
|
465
|
+
return 1
|
|
466
|
+
|
|
467
|
+
try:
|
|
468
|
+
config_content = config_path.read_text()
|
|
469
|
+
|
|
470
|
+
syntax = Syntax(config_content, "ini", theme="monokai", line_numbers=True)
|
|
471
|
+
|
|
472
|
+
console.print(
|
|
473
|
+
Panel(syntax, title=f"Configuration: {parsed_args.interface}", expand=False)
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
return 0
|
|
477
|
+
except PermissionError:
|
|
478
|
+
console.print(f"[red]Permission denied reading {config_path}[/red]")
|
|
479
|
+
console.print("Try running with sudo")
|
|
480
|
+
return 1
|
|
481
|
+
except Exception as e:
|
|
482
|
+
console.print(f"[red]Error reading config: {e}[/red]")
|
|
483
|
+
return 1
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def generate_key_command(args: list[str]) -> int:
|
|
487
|
+
"""Generate WireGuard keypair."""
|
|
488
|
+
parser = argparse.ArgumentParser(
|
|
489
|
+
prog="manta_node network generate-key",
|
|
490
|
+
description="Generate a WireGuard keypair",
|
|
491
|
+
)
|
|
492
|
+
parser.add_argument(
|
|
493
|
+
"--output",
|
|
494
|
+
"-o",
|
|
495
|
+
help="Output file for private key (public key goes to <file>.pub)",
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
try:
|
|
499
|
+
parsed_args = parser.parse_args(args)
|
|
500
|
+
except SystemExit:
|
|
501
|
+
return 1
|
|
502
|
+
|
|
503
|
+
try:
|
|
504
|
+
from wireguard_tools import WireguardKey
|
|
505
|
+
|
|
506
|
+
# Generate keypair
|
|
507
|
+
private_key = WireguardKey.generate()
|
|
508
|
+
public_key = private_key.public_key()
|
|
509
|
+
|
|
510
|
+
if parsed_args.output:
|
|
511
|
+
# Write to files
|
|
512
|
+
private_file = Path(parsed_args.output)
|
|
513
|
+
public_file = Path(f"{parsed_args.output}.pub")
|
|
514
|
+
|
|
515
|
+
# Check if files exist
|
|
516
|
+
if private_file.exists() or public_file.exists():
|
|
517
|
+
console.print("[yellow]Warning: File(s) already exist[/yellow]")
|
|
518
|
+
response = input("Overwrite? (y/N): ").strip().lower()
|
|
519
|
+
if response != "y":
|
|
520
|
+
console.print("[dim]Operation cancelled[/dim]")
|
|
521
|
+
return 0
|
|
522
|
+
|
|
523
|
+
# Write private key, pre-restricted so it is never world/group readable
|
|
524
|
+
private_file.touch(mode=0o600)
|
|
525
|
+
private_file.write_text(str(private_key))
|
|
526
|
+
|
|
527
|
+
# Write public key
|
|
528
|
+
public_file.write_text(str(public_key))
|
|
529
|
+
public_file.chmod(0o644)
|
|
530
|
+
|
|
531
|
+
console.print(f"[green]✓[/green] Private key written to: {private_file}")
|
|
532
|
+
console.print(f"[green]✓[/green] Public key written to: {public_file}")
|
|
533
|
+
else:
|
|
534
|
+
# Print to console
|
|
535
|
+
console.print("\n[bold]Private Key:[/bold]")
|
|
536
|
+
console.print(f"[cyan]{private_key}[/cyan]")
|
|
537
|
+
console.print("\n[bold]Public Key:[/bold]")
|
|
538
|
+
console.print(f"[cyan]{public_key}[/cyan]")
|
|
539
|
+
console.print("\n[dim]Tip: Use --output to save keys to files[/dim]")
|
|
540
|
+
|
|
541
|
+
return 0
|
|
542
|
+
|
|
543
|
+
except ImportError:
|
|
544
|
+
console.print("[red]Error: wireguard-tools library not installed[/red]")
|
|
545
|
+
console.print("Install with: [cyan]pip install wireguard-tools[/cyan]")
|
|
546
|
+
return 1
|
|
547
|
+
except Exception as e:
|
|
548
|
+
console.print(f"[red]Error generating keypair: {e}[/red]")
|
|
549
|
+
return 1
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def create_network_command(args: list[str]) -> int:
|
|
553
|
+
"""Create a new WireGuard network interface."""
|
|
554
|
+
parser = argparse.ArgumentParser(
|
|
555
|
+
prog="manta_node network create",
|
|
556
|
+
description="Create a new WireGuard network interface",
|
|
557
|
+
)
|
|
558
|
+
parser.add_argument("interface", help="Interface name (e.g., wg-test)")
|
|
559
|
+
parser.add_argument(
|
|
560
|
+
"--ip",
|
|
561
|
+
required=True,
|
|
562
|
+
help="Assigned IP address with CIDR (e.g., 10.100.0.5/24)",
|
|
563
|
+
)
|
|
564
|
+
parser.add_argument(
|
|
565
|
+
"--port", type=int, default=51820, help="Listen port (default: 51820)"
|
|
566
|
+
)
|
|
567
|
+
parser.add_argument("--mtu", type=int, default=1420, help="MTU (default: 1420)")
|
|
568
|
+
parser.add_argument(
|
|
569
|
+
"--private-key", help="Private key (will generate if not provided)"
|
|
570
|
+
)
|
|
571
|
+
parser.add_argument(
|
|
572
|
+
"--force", action="store_true", help="Skip confirmation prompts"
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
try:
|
|
576
|
+
parsed_args = parser.parse_args(args)
|
|
577
|
+
except SystemExit:
|
|
578
|
+
return 1
|
|
579
|
+
|
|
580
|
+
interface = parsed_args.interface
|
|
581
|
+
assigned_ip = parsed_args.ip
|
|
582
|
+
listen_port = parsed_args.port
|
|
583
|
+
mtu = parsed_args.mtu
|
|
584
|
+
private_key = parsed_args.private_key
|
|
585
|
+
|
|
586
|
+
# Check if interface already exists
|
|
587
|
+
existing_interfaces = list_interfaces()
|
|
588
|
+
if interface in existing_interfaces:
|
|
589
|
+
console.print(f"[red]Error: Interface {interface} already exists[/red]")
|
|
590
|
+
return 1
|
|
591
|
+
|
|
592
|
+
# Confirm operation
|
|
593
|
+
if not parsed_args.force:
|
|
594
|
+
console.print("\n[bold]Create WireGuard Network:[/bold]")
|
|
595
|
+
console.print(f" Interface: [cyan]{interface}[/cyan]")
|
|
596
|
+
console.print(f" IP: [cyan]{assigned_ip}[/cyan]")
|
|
597
|
+
console.print(f" Port: [cyan]{listen_port}[/cyan]")
|
|
598
|
+
console.print(f" MTU: [cyan]{mtu}[/cyan]")
|
|
599
|
+
response = input("\nProceed? (y/N): ").strip().lower()
|
|
600
|
+
if response != "y":
|
|
601
|
+
console.print("[dim]Operation cancelled[/dim]")
|
|
602
|
+
return 0
|
|
603
|
+
|
|
604
|
+
try:
|
|
605
|
+
from pyroute2 import IPRoute
|
|
606
|
+
from wireguard_tools import WireguardConfig, WireguardDevice, WireguardKey
|
|
607
|
+
|
|
608
|
+
# Generate or parse private key
|
|
609
|
+
if not private_key:
|
|
610
|
+
console.print("[dim]Generating keypair...[/dim]")
|
|
611
|
+
priv_key_obj = WireguardKey.generate()
|
|
612
|
+
pub_key_obj = priv_key_obj.public_key()
|
|
613
|
+
private_key = str(priv_key_obj)
|
|
614
|
+
console.print(f"Public key: [cyan]{pub_key_obj}[/cyan]")
|
|
615
|
+
else:
|
|
616
|
+
priv_key_obj = WireguardKey(private_key)
|
|
617
|
+
pub_key_obj = priv_key_obj.public_key()
|
|
618
|
+
|
|
619
|
+
# Parse IP address
|
|
620
|
+
ip_addr, prefix_len = assigned_ip.split("/")
|
|
621
|
+
|
|
622
|
+
# Create interface using pyroute2
|
|
623
|
+
console.print(f"[dim]Creating interface {interface}...[/dim]")
|
|
624
|
+
with IPRoute() as ip:
|
|
625
|
+
# Create interface
|
|
626
|
+
ip.link("add", ifname=interface, kind="wireguard")
|
|
627
|
+
|
|
628
|
+
# Get interface index
|
|
629
|
+
idx = ip.link_lookup(ifname=interface)[0]
|
|
630
|
+
|
|
631
|
+
# Set MTU
|
|
632
|
+
ip.link("set", index=idx, mtu=mtu)
|
|
633
|
+
|
|
634
|
+
# Add IP address
|
|
635
|
+
ip.addr("add", index=idx, address=ip_addr, prefixlen=int(prefix_len))
|
|
636
|
+
|
|
637
|
+
# Bring interface up
|
|
638
|
+
ip.link("set", index=idx, state="up")
|
|
639
|
+
|
|
640
|
+
# Configure WireGuard-specific parameters
|
|
641
|
+
console.print("[dim]Configuring WireGuard...[/dim]")
|
|
642
|
+
device = WireguardDevice.get(interface)
|
|
643
|
+
config = WireguardConfig(
|
|
644
|
+
private_key=private_key,
|
|
645
|
+
listen_port=listen_port,
|
|
646
|
+
peers={},
|
|
647
|
+
)
|
|
648
|
+
device.set_config(config)
|
|
649
|
+
|
|
650
|
+
# Persist state to keyring + TOML config
|
|
651
|
+
try:
|
|
652
|
+
import keyring
|
|
653
|
+
|
|
654
|
+
from manta_node.infrastructure.config.node_config_manager import (
|
|
655
|
+
WireGuardNetworkState,
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
config_manager, node_id = _load_node_config()
|
|
659
|
+
if config_manager and node_id:
|
|
660
|
+
keyring_service = "manta_node_wireguard"
|
|
661
|
+
keyring_key = f"{node_id}:{interface}"
|
|
662
|
+
keyring.set_password(keyring_service, keyring_key, private_key)
|
|
663
|
+
|
|
664
|
+
wg_state = WireGuardNetworkState(
|
|
665
|
+
network_id=interface,
|
|
666
|
+
interface_name=interface,
|
|
667
|
+
private_key_ref=keyring_key,
|
|
668
|
+
assigned_ip=assigned_ip,
|
|
669
|
+
network_cidr=assigned_ip,
|
|
670
|
+
listen_port=listen_port,
|
|
671
|
+
mtu=mtu,
|
|
672
|
+
peers=[],
|
|
673
|
+
)
|
|
674
|
+
config_manager.config.wireguard.networks[interface] = wg_state
|
|
675
|
+
config_manager.save()
|
|
676
|
+
console.print("[dim]Persisted network state to config + keyring[/dim]")
|
|
677
|
+
else:
|
|
678
|
+
console.print(
|
|
679
|
+
"[dim]Warning: No node config found — state not persisted[/dim]"
|
|
680
|
+
)
|
|
681
|
+
except Exception as e:
|
|
682
|
+
console.print(f"[yellow]Warning: Could not persist state: {e}[/yellow]")
|
|
683
|
+
console.print("[dim]Interface is up but keys won't survive reboot[/dim]")
|
|
684
|
+
|
|
685
|
+
console.print(
|
|
686
|
+
f"\n[green]✓[/green] Created WireGuard network: [cyan]{interface}[/cyan]"
|
|
687
|
+
)
|
|
688
|
+
console.print(f" IP: {assigned_ip}")
|
|
689
|
+
console.print(f" Public key: {pub_key_obj}")
|
|
690
|
+
console.print(f" Port: {listen_port}")
|
|
691
|
+
|
|
692
|
+
console.print("\n[bold]Next steps:[/bold]")
|
|
693
|
+
console.print(" 1. Share your public key with peers")
|
|
694
|
+
console.print(
|
|
695
|
+
f" 2. Add peers: [cyan]manta_node network add-peer {interface} --pubkey <KEY> --endpoint <IP:PORT> --allowed-ips <CIDR>[/cyan]"
|
|
696
|
+
)
|
|
697
|
+
console.print(
|
|
698
|
+
f" 3. Test connectivity: [cyan]manta_node network test {interface} <PEER_IP>[/cyan]"
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
return 0
|
|
702
|
+
|
|
703
|
+
except ImportError as e:
|
|
704
|
+
console.print(f"[red]Error: Required library not installed: {e}[/red]")
|
|
705
|
+
console.print("Install with: [cyan]pip install wireguard-tools pyroute2[/cyan]")
|
|
706
|
+
return 1
|
|
707
|
+
except Exception as e:
|
|
708
|
+
console.print(f"[red]Error creating interface: {e}[/red]")
|
|
709
|
+
return 1
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def delete_network_command(args: list[str]) -> int:
|
|
713
|
+
"""Delete a WireGuard network interface."""
|
|
714
|
+
parser = argparse.ArgumentParser(
|
|
715
|
+
prog="manta_node network delete",
|
|
716
|
+
description="Delete a WireGuard network interface",
|
|
717
|
+
)
|
|
718
|
+
parser.add_argument("interface", help="Interface name to delete")
|
|
719
|
+
parser.add_argument(
|
|
720
|
+
"--force", action="store_true", help="Skip confirmation prompts"
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
try:
|
|
724
|
+
parsed_args = parser.parse_args(args)
|
|
725
|
+
except SystemExit:
|
|
726
|
+
return 1
|
|
727
|
+
|
|
728
|
+
interface = parsed_args.interface
|
|
729
|
+
|
|
730
|
+
# Check if interface exists
|
|
731
|
+
existing_interfaces = list_interfaces()
|
|
732
|
+
if interface not in existing_interfaces:
|
|
733
|
+
console.print(f"[red]Error: Interface {interface} not found[/red]")
|
|
734
|
+
return 1
|
|
735
|
+
|
|
736
|
+
# Confirm operation
|
|
737
|
+
if not parsed_args.force:
|
|
738
|
+
console.print(
|
|
739
|
+
f"\n[yellow]Warning: This will delete interface {interface}[/yellow]"
|
|
740
|
+
)
|
|
741
|
+
response = input("Proceed? (y/N): ").strip().lower()
|
|
742
|
+
if response != "y":
|
|
743
|
+
console.print("[dim]Operation cancelled[/dim]")
|
|
744
|
+
return 0
|
|
745
|
+
|
|
746
|
+
try:
|
|
747
|
+
from pyroute2 import IPRoute
|
|
748
|
+
|
|
749
|
+
console.print(f"[dim]Deleting interface {interface}...[/dim]")
|
|
750
|
+
|
|
751
|
+
with IPRoute() as ip:
|
|
752
|
+
# Get interface index
|
|
753
|
+
idx = ip.link_lookup(ifname=interface)
|
|
754
|
+
|
|
755
|
+
if idx:
|
|
756
|
+
# Delete the interface
|
|
757
|
+
ip.link("del", index=idx[0])
|
|
758
|
+
console.print(
|
|
759
|
+
f"[green]✓[/green] Deleted interface: [cyan]{interface}[/cyan]"
|
|
760
|
+
)
|
|
761
|
+
else:
|
|
762
|
+
console.print(f"[red]Error: Interface {interface} not found[/red]")
|
|
763
|
+
return 1
|
|
764
|
+
|
|
765
|
+
# Clean up persisted state from keyring + TOML config
|
|
766
|
+
try:
|
|
767
|
+
config_manager, _ = _load_node_config()
|
|
768
|
+
if config_manager and interface in config_manager.config.wireguard.networks:
|
|
769
|
+
wg_state = config_manager.config.wireguard.networks[interface]
|
|
770
|
+
|
|
771
|
+
# Delete private key from keyring
|
|
772
|
+
try:
|
|
773
|
+
import keyring
|
|
774
|
+
|
|
775
|
+
keyring.delete_password(
|
|
776
|
+
"manta_node_wireguard", wg_state.private_key_ref
|
|
777
|
+
)
|
|
778
|
+
console.print("[dim]Removed key from keyring[/dim]")
|
|
779
|
+
except Exception as e:
|
|
780
|
+
console.print(
|
|
781
|
+
f"[dim]Warning: Could not remove keyring entry: {e}[/dim]"
|
|
782
|
+
)
|
|
783
|
+
|
|
784
|
+
# Remove network from config
|
|
785
|
+
del config_manager.config.wireguard.networks[interface]
|
|
786
|
+
config_manager.save()
|
|
787
|
+
console.print("[dim]Removed network from config[/dim]")
|
|
788
|
+
except Exception as e:
|
|
789
|
+
console.print(
|
|
790
|
+
f"[yellow]Warning: Could not clean up persisted state: {e}[/yellow]"
|
|
791
|
+
)
|
|
792
|
+
|
|
793
|
+
return 0
|
|
794
|
+
|
|
795
|
+
except ImportError:
|
|
796
|
+
console.print("[red]Error: pyroute2 library not installed[/red]")
|
|
797
|
+
console.print("Install with: [cyan]pip install pyroute2[/cyan]")
|
|
798
|
+
return 1
|
|
799
|
+
except Exception as e:
|
|
800
|
+
console.print(f"[red]Error deleting interface: {e}[/red]")
|
|
801
|
+
return 1
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
def add_peer_command(args: list[str]) -> int:
|
|
805
|
+
"""Add a peer to a WireGuard network."""
|
|
806
|
+
parser = argparse.ArgumentParser(
|
|
807
|
+
prog="manta_node network add-peer",
|
|
808
|
+
description="Add a peer to a WireGuard network",
|
|
809
|
+
)
|
|
810
|
+
parser.add_argument("interface", help="Interface name")
|
|
811
|
+
parser.add_argument("--pubkey", required=True, help="Peer's public key")
|
|
812
|
+
parser.add_argument("--endpoint", help="Peer endpoint (IP:PORT)")
|
|
813
|
+
parser.add_argument(
|
|
814
|
+
"--allowed-ips",
|
|
815
|
+
required=True,
|
|
816
|
+
help="Comma-separated list of allowed IPs (e.g., 10.100.0.2/32)",
|
|
817
|
+
)
|
|
818
|
+
parser.add_argument(
|
|
819
|
+
"--keepalive",
|
|
820
|
+
type=int,
|
|
821
|
+
default=25,
|
|
822
|
+
help="Persistent keepalive interval in seconds (default: 25)",
|
|
823
|
+
)
|
|
824
|
+
parser.add_argument(
|
|
825
|
+
"--force", action="store_true", help="Skip confirmation prompts"
|
|
826
|
+
)
|
|
827
|
+
|
|
828
|
+
try:
|
|
829
|
+
parsed_args = parser.parse_args(args)
|
|
830
|
+
except SystemExit:
|
|
831
|
+
return 1
|
|
832
|
+
|
|
833
|
+
interface = parsed_args.interface
|
|
834
|
+
public_key = parsed_args.pubkey
|
|
835
|
+
endpoint = parsed_args.endpoint
|
|
836
|
+
allowed_ips = [ip.strip() for ip in parsed_args.allowed_ips.split(",")]
|
|
837
|
+
keepalive = parsed_args.keepalive
|
|
838
|
+
|
|
839
|
+
# Check if interface exists
|
|
840
|
+
existing_interfaces = list_interfaces()
|
|
841
|
+
if interface not in existing_interfaces:
|
|
842
|
+
console.print(f"[red]Error: Interface {interface} not found[/red]")
|
|
843
|
+
console.print("\nCreate the interface first:")
|
|
844
|
+
console.print(
|
|
845
|
+
f" [cyan]manta_node network create {interface} --ip <IP/CIDR>[/cyan]"
|
|
846
|
+
)
|
|
847
|
+
return 1
|
|
848
|
+
|
|
849
|
+
# Confirm operation
|
|
850
|
+
if not parsed_args.force:
|
|
851
|
+
console.print("\n[bold]Add Peer:[/bold]")
|
|
852
|
+
console.print(f" Interface: [cyan]{interface}[/cyan]")
|
|
853
|
+
console.print(f" Public key: [cyan]{public_key[:32]}...[/cyan]")
|
|
854
|
+
console.print(f" Endpoint: [cyan]{endpoint or 'None'}[/cyan]")
|
|
855
|
+
console.print(f" Allowed IPs: [cyan]{', '.join(allowed_ips)}[/cyan]")
|
|
856
|
+
response = input("\nProceed? (y/N): ").strip().lower()
|
|
857
|
+
if response != "y":
|
|
858
|
+
console.print("[dim]Operation cancelled[/dim]")
|
|
859
|
+
return 0
|
|
860
|
+
|
|
861
|
+
try:
|
|
862
|
+
from pyroute2 import IPRoute
|
|
863
|
+
from wireguard_tools import (
|
|
864
|
+
WireguardDevice,
|
|
865
|
+
WireguardKey,
|
|
866
|
+
)
|
|
867
|
+
from wireguard_tools import (
|
|
868
|
+
WireguardPeer as WGPeer,
|
|
869
|
+
)
|
|
870
|
+
|
|
871
|
+
console.print(f"[dim]Adding peer to {interface}...[/dim]")
|
|
872
|
+
|
|
873
|
+
# Parse endpoint string (e.g., "192.168.104.1:51820") into host/port
|
|
874
|
+
endpoint_host = None
|
|
875
|
+
endpoint_port = None
|
|
876
|
+
if endpoint:
|
|
877
|
+
parts = endpoint.rsplit(":", 1)
|
|
878
|
+
endpoint_host = parts[0]
|
|
879
|
+
endpoint_port = int(parts[1]) if len(parts) > 1 else None
|
|
880
|
+
|
|
881
|
+
# Get current device configuration
|
|
882
|
+
device = WireguardDevice.get(interface)
|
|
883
|
+
config = device.get_config()
|
|
884
|
+
|
|
885
|
+
# config.peers is a dict[WireguardKey, WireguardPeer]
|
|
886
|
+
peer_key = WireguardKey(public_key)
|
|
887
|
+
peer_exists = peer_key in config.peers
|
|
888
|
+
|
|
889
|
+
config.peers[peer_key] = WGPeer(
|
|
890
|
+
public_key=peer_key,
|
|
891
|
+
endpoint_host=endpoint_host,
|
|
892
|
+
endpoint_port=endpoint_port,
|
|
893
|
+
allowed_ips=allowed_ips,
|
|
894
|
+
persistent_keepalive=keepalive,
|
|
895
|
+
)
|
|
896
|
+
|
|
897
|
+
# Apply configuration
|
|
898
|
+
device.set_config(config)
|
|
899
|
+
|
|
900
|
+
# Add routes for allowed_ips
|
|
901
|
+
console.print("[dim]Adding routes...[/dim]")
|
|
902
|
+
with IPRoute() as ip:
|
|
903
|
+
idx = ip.link_lookup(ifname=interface)[0]
|
|
904
|
+
|
|
905
|
+
for allowed_ip in allowed_ips:
|
|
906
|
+
try:
|
|
907
|
+
# Add route
|
|
908
|
+
ip.route("add", dst=allowed_ip, oif=idx)
|
|
909
|
+
except Exception as e:
|
|
910
|
+
# Route might already exist
|
|
911
|
+
console.print(
|
|
912
|
+
f"[dim]Route for {allowed_ip} may already exist: {e}[/dim]"
|
|
913
|
+
)
|
|
914
|
+
|
|
915
|
+
# Persist peer to TOML config
|
|
916
|
+
try:
|
|
917
|
+
from manta_node.infrastructure.config.node_config_manager import (
|
|
918
|
+
WireGuardPeerState,
|
|
919
|
+
)
|
|
920
|
+
|
|
921
|
+
config_manager, _ = _load_node_config()
|
|
922
|
+
if config_manager and interface in config_manager.config.wireguard.networks:
|
|
923
|
+
network_state = config_manager.config.wireguard.networks[interface]
|
|
924
|
+
new_peer = WireGuardPeerState(
|
|
925
|
+
node_id="",
|
|
926
|
+
public_key=public_key,
|
|
927
|
+
endpoint=endpoint,
|
|
928
|
+
allowed_ips=allowed_ips,
|
|
929
|
+
persistent_keepalive=keepalive,
|
|
930
|
+
)
|
|
931
|
+
|
|
932
|
+
# Update existing peer or append new one
|
|
933
|
+
updated = False
|
|
934
|
+
for i, existing in enumerate(network_state.peers):
|
|
935
|
+
if existing.public_key == public_key:
|
|
936
|
+
network_state.peers[i] = new_peer
|
|
937
|
+
updated = True
|
|
938
|
+
break
|
|
939
|
+
if not updated:
|
|
940
|
+
network_state.peers.append(new_peer)
|
|
941
|
+
|
|
942
|
+
config_manager.save()
|
|
943
|
+
console.print("[dim]Persisted peer to config[/dim]")
|
|
944
|
+
except Exception as e:
|
|
945
|
+
console.print(f"[yellow]Warning: Could not persist peer: {e}[/yellow]")
|
|
946
|
+
|
|
947
|
+
action = "Updated" if peer_exists else "Added"
|
|
948
|
+
console.print(f"\n[green]✓[/green] {action} peer on [cyan]{interface}[/cyan]")
|
|
949
|
+
console.print(f" Public key: {public_key[:32]}...")
|
|
950
|
+
console.print(f" Allowed IPs: {', '.join(allowed_ips)}")
|
|
951
|
+
|
|
952
|
+
if endpoint:
|
|
953
|
+
console.print("\n[bold]Test connectivity:[/bold]")
|
|
954
|
+
peer_ip = allowed_ips[0].split("/")[0] # Get first IP without CIDR
|
|
955
|
+
console.print(
|
|
956
|
+
f" [cyan]manta_node network test {interface} {peer_ip}[/cyan]"
|
|
957
|
+
)
|
|
958
|
+
|
|
959
|
+
return 0
|
|
960
|
+
|
|
961
|
+
except ImportError as e:
|
|
962
|
+
console.print(f"[red]Error: Required library not installed: {e}[/red]")
|
|
963
|
+
console.print("Install with: [cyan]pip install wireguard-tools pyroute2[/cyan]")
|
|
964
|
+
return 1
|
|
965
|
+
except Exception as e:
|
|
966
|
+
console.print(f"[red]Error adding peer: {e}[/red]")
|
|
967
|
+
return 1
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
def remove_peer_command(args: list[str]) -> int:
|
|
971
|
+
"""Remove a peer from a WireGuard network."""
|
|
972
|
+
parser = argparse.ArgumentParser(
|
|
973
|
+
prog="manta_node network remove-peer",
|
|
974
|
+
description="Remove a peer from a WireGuard network",
|
|
975
|
+
)
|
|
976
|
+
parser.add_argument("interface", help="Interface name")
|
|
977
|
+
parser.add_argument("--pubkey", required=True, help="Peer's public key to remove")
|
|
978
|
+
parser.add_argument(
|
|
979
|
+
"--force", action="store_true", help="Skip confirmation prompts"
|
|
980
|
+
)
|
|
981
|
+
|
|
982
|
+
try:
|
|
983
|
+
parsed_args = parser.parse_args(args)
|
|
984
|
+
except SystemExit:
|
|
985
|
+
return 1
|
|
986
|
+
|
|
987
|
+
interface = parsed_args.interface
|
|
988
|
+
public_key = parsed_args.pubkey
|
|
989
|
+
|
|
990
|
+
# Check if interface exists
|
|
991
|
+
existing_interfaces = list_interfaces()
|
|
992
|
+
if interface not in existing_interfaces:
|
|
993
|
+
console.print(f"[red]Error: Interface {interface} not found[/red]")
|
|
994
|
+
return 1
|
|
995
|
+
|
|
996
|
+
# Confirm operation
|
|
997
|
+
if not parsed_args.force:
|
|
998
|
+
console.print(f"\n[yellow]Remove peer from {interface}?[/yellow]")
|
|
999
|
+
console.print(f" Public key: [cyan]{public_key[:32]}...[/cyan]")
|
|
1000
|
+
response = input("\nProceed? (y/N): ").strip().lower()
|
|
1001
|
+
if response != "y":
|
|
1002
|
+
console.print("[dim]Operation cancelled[/dim]")
|
|
1003
|
+
return 0
|
|
1004
|
+
|
|
1005
|
+
try:
|
|
1006
|
+
from pyroute2 import IPRoute
|
|
1007
|
+
from wireguard_tools import WireguardDevice, WireguardKey
|
|
1008
|
+
|
|
1009
|
+
console.print(f"[dim]Removing peer from {interface}...[/dim]")
|
|
1010
|
+
|
|
1011
|
+
# Get current device configuration
|
|
1012
|
+
device = WireguardDevice.get(interface)
|
|
1013
|
+
config = device.get_config()
|
|
1014
|
+
|
|
1015
|
+
# config.peers is a dict[WireguardKey, WireguardPeer]
|
|
1016
|
+
peer_key = WireguardKey(public_key)
|
|
1017
|
+
peer_allowed_ips = []
|
|
1018
|
+
|
|
1019
|
+
if peer_key not in config.peers:
|
|
1020
|
+
console.print(f"[yellow]Warning: Peer not found in {interface}[/yellow]")
|
|
1021
|
+
return 1
|
|
1022
|
+
|
|
1023
|
+
existing_peer = config.peers[peer_key]
|
|
1024
|
+
peer_allowed_ips = (
|
|
1025
|
+
list(existing_peer.allowed_ips) if existing_peer.allowed_ips else []
|
|
1026
|
+
)
|
|
1027
|
+
del config.peers[peer_key]
|
|
1028
|
+
|
|
1029
|
+
device.set_config(config)
|
|
1030
|
+
|
|
1031
|
+
# Remove routes for peer's allowed_ips
|
|
1032
|
+
if peer_allowed_ips:
|
|
1033
|
+
console.print("[dim]Removing routes...[/dim]")
|
|
1034
|
+
with IPRoute() as ip:
|
|
1035
|
+
idx = ip.link_lookup(ifname=interface)[0]
|
|
1036
|
+
|
|
1037
|
+
for allowed_ip in peer_allowed_ips:
|
|
1038
|
+
try:
|
|
1039
|
+
# Remove route
|
|
1040
|
+
ip.route("del", dst=allowed_ip, oif=idx)
|
|
1041
|
+
except Exception as e:
|
|
1042
|
+
# Route might not exist
|
|
1043
|
+
console.print(
|
|
1044
|
+
f"[dim]Route for {allowed_ip} may not exist: {e}[/dim]"
|
|
1045
|
+
)
|
|
1046
|
+
|
|
1047
|
+
console.print(f"\n[green]✓[/green] Removed peer from [cyan]{interface}[/cyan]")
|
|
1048
|
+
console.print(f" Public key: {public_key[:32]}...")
|
|
1049
|
+
|
|
1050
|
+
return 0
|
|
1051
|
+
|
|
1052
|
+
except ImportError as e:
|
|
1053
|
+
console.print(f"[red]Error: Required library not installed: {e}[/red]")
|
|
1054
|
+
console.print("Install with: [cyan]pip install wireguard-tools pyroute2[/cyan]")
|
|
1055
|
+
return 1
|
|
1056
|
+
except Exception as e:
|
|
1057
|
+
console.print(f"[red]Error removing peer: {e}[/red]")
|
|
1058
|
+
return 1
|
|
1059
|
+
|
|
1060
|
+
|
|
1061
|
+
def network_command(args: list[str]) -> int:
|
|
1062
|
+
"""Handle network command with subcommands.
|
|
1063
|
+
|
|
1064
|
+
Args:
|
|
1065
|
+
args: Command line arguments
|
|
1066
|
+
|
|
1067
|
+
Returns:
|
|
1068
|
+
Exit code (0 for success, non-zero for error)
|
|
1069
|
+
"""
|
|
1070
|
+
if not args or args[0] in ["-h", "--help", "help"]:
|
|
1071
|
+
show_network_help()
|
|
1072
|
+
return 0
|
|
1073
|
+
|
|
1074
|
+
subcommand = args[0]
|
|
1075
|
+
subcommand_args = args[1:]
|
|
1076
|
+
|
|
1077
|
+
# Route to appropriate subcommand
|
|
1078
|
+
if subcommand == "list":
|
|
1079
|
+
return list_networks_command(subcommand_args)
|
|
1080
|
+
elif subcommand == "show":
|
|
1081
|
+
return show_network_command(subcommand_args)
|
|
1082
|
+
elif subcommand == "peers":
|
|
1083
|
+
return list_peers_command(subcommand_args)
|
|
1084
|
+
elif subcommand == "peer":
|
|
1085
|
+
return show_peer_command(subcommand_args)
|
|
1086
|
+
elif subcommand == "test":
|
|
1087
|
+
return test_connectivity_command(subcommand_args)
|
|
1088
|
+
elif subcommand == "config":
|
|
1089
|
+
return show_config_command(subcommand_args)
|
|
1090
|
+
elif subcommand == "generate-key":
|
|
1091
|
+
return generate_key_command(subcommand_args)
|
|
1092
|
+
elif subcommand == "create":
|
|
1093
|
+
return create_network_command(subcommand_args)
|
|
1094
|
+
elif subcommand == "delete":
|
|
1095
|
+
return delete_network_command(subcommand_args)
|
|
1096
|
+
elif subcommand == "add-peer":
|
|
1097
|
+
return add_peer_command(subcommand_args)
|
|
1098
|
+
elif subcommand == "remove-peer":
|
|
1099
|
+
return remove_peer_command(subcommand_args)
|
|
1100
|
+
else:
|
|
1101
|
+
console.print(f"[red]Unknown subcommand: {subcommand}[/red]")
|
|
1102
|
+
console.print("\nUse 'manta_node network help' to see available subcommands")
|
|
1103
|
+
return 1
|
|
1104
|
+
|
|
1105
|
+
|
|
1106
|
+
def show_network_help():
|
|
1107
|
+
"""Show network command help."""
|
|
1108
|
+
help_text = """
|
|
1109
|
+
[bold cyan]Manta Node Network Management[/bold cyan]
|
|
1110
|
+
|
|
1111
|
+
Manage and troubleshoot WireGuard SDN networks on this node.
|
|
1112
|
+
|
|
1113
|
+
[bold]Usage:[/bold]
|
|
1114
|
+
manta_node network <subcommand> [options]
|
|
1115
|
+
|
|
1116
|
+
[bold]Read Operations (Query):[/bold]
|
|
1117
|
+
[cyan]list[/cyan] List all active WireGuard networks
|
|
1118
|
+
[cyan]show <interface>[/cyan] Show detailed network information
|
|
1119
|
+
[cyan]peers <interface>[/cyan] List all peers in a network
|
|
1120
|
+
[cyan]peer <interface> <pubkey>[/cyan] Show detailed peer information
|
|
1121
|
+
[cyan]test <interface> <ip>[/cyan] Test connectivity to a peer
|
|
1122
|
+
[cyan]config <interface>[/cyan] Show WireGuard configuration file
|
|
1123
|
+
|
|
1124
|
+
[bold]Write Operations (Manage):[/bold]
|
|
1125
|
+
[cyan]generate-key[/cyan] Generate a WireGuard keypair
|
|
1126
|
+
[cyan]create <interface> --ip <CIDR>[/cyan] Create a new WireGuard network
|
|
1127
|
+
[cyan]delete <interface>[/cyan] Delete a WireGuard network
|
|
1128
|
+
[cyan]add-peer <interface> ...[/cyan] Add a peer to a network
|
|
1129
|
+
[cyan]remove-peer <interface> ...[/cyan] Remove a peer from a network
|
|
1130
|
+
|
|
1131
|
+
[bold]Examples:[/bold]
|
|
1132
|
+
[dim]# Read operations[/dim]
|
|
1133
|
+
manta_node network list
|
|
1134
|
+
manta_node network show wg-abc123
|
|
1135
|
+
manta_node network peers wg-abc123
|
|
1136
|
+
manta_node network test wg-abc123 10.100.0.2
|
|
1137
|
+
|
|
1138
|
+
[dim]# Write operations[/dim]
|
|
1139
|
+
manta_node network generate-key --output /tmp/wg-key
|
|
1140
|
+
manta_node network create wg-test --ip 10.100.0.1/24 --port 51820
|
|
1141
|
+
manta_node network add-peer wg-test --pubkey <KEY> --endpoint 192.0.2.1:51820 --allowed-ips 10.100.0.2/32
|
|
1142
|
+
manta_node network remove-peer wg-test --pubkey <KEY>
|
|
1143
|
+
manta_node network delete wg-test
|
|
1144
|
+
|
|
1145
|
+
[bold]Permissions:[/bold]
|
|
1146
|
+
Most commands require elevated permissions to access WireGuard devices.
|
|
1147
|
+
|
|
1148
|
+
[bold]Option 1: Run with sudo[/bold]
|
|
1149
|
+
sudo manta_node network <subcommand>
|
|
1150
|
+
|
|
1151
|
+
[bold]Option 2: One-time setup (recommended)[/bold]
|
|
1152
|
+
Run the setup script to enable group-based access:
|
|
1153
|
+
sudo ./scripts/setup-wireguard-access.sh
|
|
1154
|
+
|
|
1155
|
+
This configures:
|
|
1156
|
+
- wireguard group membership
|
|
1157
|
+
- Unprivileged ping support
|
|
1158
|
+
- WireGuard socket permissions
|
|
1159
|
+
|
|
1160
|
+
[bold]Notes:[/bold]
|
|
1161
|
+
- Networks are automatically created when swarms are deployed via the manager
|
|
1162
|
+
- Manual network creation is useful for testing or standalone deployments
|
|
1163
|
+
- Network IDs follow the format: wg-<swarm_id> for managed networks
|
|
1164
|
+
- Write operations require root/sudo privileges
|
|
1165
|
+
- Use --force flag to skip confirmation prompts in scripts
|
|
1166
|
+
|
|
1167
|
+
For more help on a specific subcommand:
|
|
1168
|
+
manta_node network <subcommand> --help
|
|
1169
|
+
"""
|
|
1170
|
+
console.print(help_text)
|
|
1171
|
+
|
|
1172
|
+
|
|
1173
|
+
if __name__ == "__main__":
|
|
1174
|
+
sys.exit(network_command(sys.argv[1:]))
|