redundanet 2.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.
- redundanet/__init__.py +16 -0
- redundanet/__main__.py +6 -0
- redundanet/auth/__init__.py +9 -0
- redundanet/auth/gpg.py +323 -0
- redundanet/auth/keyserver.py +219 -0
- redundanet/cli/__init__.py +5 -0
- redundanet/cli/main.py +247 -0
- redundanet/cli/network.py +194 -0
- redundanet/cli/node.py +305 -0
- redundanet/cli/storage.py +267 -0
- redundanet/core/__init__.py +31 -0
- redundanet/core/config.py +200 -0
- redundanet/core/exceptions.py +84 -0
- redundanet/core/manifest.py +325 -0
- redundanet/core/node.py +135 -0
- redundanet/network/__init__.py +11 -0
- redundanet/network/discovery.py +218 -0
- redundanet/network/dns.py +180 -0
- redundanet/network/validation.py +279 -0
- redundanet/storage/__init__.py +13 -0
- redundanet/storage/client.py +306 -0
- redundanet/storage/furl.py +196 -0
- redundanet/storage/introducer.py +175 -0
- redundanet/storage/storage.py +195 -0
- redundanet/utils/__init__.py +15 -0
- redundanet/utils/files.py +165 -0
- redundanet/utils/logging.py +93 -0
- redundanet/utils/process.py +226 -0
- redundanet/vpn/__init__.py +12 -0
- redundanet/vpn/keys.py +173 -0
- redundanet/vpn/mesh.py +201 -0
- redundanet/vpn/tinc.py +323 -0
- redundanet-2.0.0.dist-info/LICENSE +674 -0
- redundanet-2.0.0.dist-info/METADATA +265 -0
- redundanet-2.0.0.dist-info/RECORD +37 -0
- redundanet-2.0.0.dist-info/WHEEL +4 -0
- redundanet-2.0.0.dist-info/entry_points.txt +3 -0
redundanet/vpn/mesh.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Mesh network operations for RedundaNet."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from redundanet.utils.logging import get_logger
|
|
10
|
+
from redundanet.utils.process import run_command
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from redundanet.core.config import NodeConfig
|
|
14
|
+
from redundanet.vpn.tinc import TincConfig
|
|
15
|
+
|
|
16
|
+
logger = get_logger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class PeerInfo:
|
|
21
|
+
"""Information about a connected peer."""
|
|
22
|
+
|
|
23
|
+
name: str
|
|
24
|
+
vpn_ip: str
|
|
25
|
+
public_ip: str | None = None
|
|
26
|
+
connected: bool = False
|
|
27
|
+
last_seen: datetime | None = None
|
|
28
|
+
latency_ms: float | None = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class MeshStatus:
|
|
33
|
+
"""Status of the mesh network."""
|
|
34
|
+
|
|
35
|
+
node_name: str
|
|
36
|
+
vpn_ip: str
|
|
37
|
+
interface: str = "tinc0"
|
|
38
|
+
is_running: bool = False
|
|
39
|
+
peers: list[PeerInfo] = field(default_factory=list)
|
|
40
|
+
connected_count: int = 0
|
|
41
|
+
total_peers: int = 0
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class MeshNetwork:
|
|
45
|
+
"""Manages mesh network operations and peer discovery."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, config: TincConfig) -> None:
|
|
48
|
+
self.config = config
|
|
49
|
+
self._interface = "tinc0"
|
|
50
|
+
|
|
51
|
+
def get_status(self) -> MeshStatus:
|
|
52
|
+
"""Get the current mesh network status."""
|
|
53
|
+
status = MeshStatus(
|
|
54
|
+
node_name=self.config.node_name,
|
|
55
|
+
vpn_ip=self.config.vpn_ip,
|
|
56
|
+
interface=self._interface,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Check if interface is up
|
|
60
|
+
result = run_command(f"ip link show {self._interface}", check=False)
|
|
61
|
+
status.is_running = result.success and "UP" in result.stdout
|
|
62
|
+
|
|
63
|
+
return status
|
|
64
|
+
|
|
65
|
+
def ping_peer(self, peer_ip: str, count: int = 3, timeout: float = 5.0) -> float | None:
|
|
66
|
+
"""Ping a peer and return the average latency.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
peer_ip: IP address to ping
|
|
70
|
+
count: Number of ping packets
|
|
71
|
+
timeout: Timeout in seconds
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Average latency in milliseconds, or None if unreachable
|
|
75
|
+
"""
|
|
76
|
+
result = run_command(
|
|
77
|
+
f"ping -c {count} -W {int(timeout)} -q {peer_ip}",
|
|
78
|
+
timeout=timeout * count + 5,
|
|
79
|
+
check=False,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
if not result.success:
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
# Parse ping output for average time
|
|
86
|
+
# Example: rtt min/avg/max/mdev = 0.123/0.456/0.789/0.123 ms
|
|
87
|
+
for line in result.stdout.split("\n"):
|
|
88
|
+
if "rtt" in line or "round-trip" in line:
|
|
89
|
+
parts = line.split("=")
|
|
90
|
+
if len(parts) >= 2:
|
|
91
|
+
times = parts[1].strip().split("/")
|
|
92
|
+
if len(times) >= 2:
|
|
93
|
+
try:
|
|
94
|
+
return float(times[1]) # Average time
|
|
95
|
+
except ValueError:
|
|
96
|
+
pass
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
def check_peer_connectivity(self, peers: list[NodeConfig]) -> dict[str, PeerInfo]:
|
|
100
|
+
"""Check connectivity to all peers.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
peers: List of peer node configurations
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Dictionary mapping node name to PeerInfo
|
|
107
|
+
"""
|
|
108
|
+
results = {}
|
|
109
|
+
|
|
110
|
+
for peer in peers:
|
|
111
|
+
if peer.name == self.config.node_name:
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
peer_ip = peer.vpn_ip or peer.internal_ip
|
|
115
|
+
latency = self.ping_peer(peer_ip)
|
|
116
|
+
|
|
117
|
+
info = PeerInfo(
|
|
118
|
+
name=peer.name,
|
|
119
|
+
vpn_ip=peer_ip,
|
|
120
|
+
public_ip=peer.public_ip,
|
|
121
|
+
connected=latency is not None,
|
|
122
|
+
last_seen=datetime.now() if latency else None,
|
|
123
|
+
latency_ms=latency,
|
|
124
|
+
)
|
|
125
|
+
results[peer.name] = info
|
|
126
|
+
|
|
127
|
+
if latency:
|
|
128
|
+
logger.debug("Peer reachable", peer=peer.name, latency_ms=latency)
|
|
129
|
+
else:
|
|
130
|
+
logger.debug("Peer unreachable", peer=peer.name)
|
|
131
|
+
|
|
132
|
+
return results
|
|
133
|
+
|
|
134
|
+
def get_interface_info(self) -> dict[str, str | None]:
|
|
135
|
+
"""Get information about the VPN interface."""
|
|
136
|
+
info: dict[str, str | None] = {
|
|
137
|
+
"name": self._interface,
|
|
138
|
+
"ip": None,
|
|
139
|
+
"state": "down",
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
# Get interface state
|
|
143
|
+
result = run_command(f"ip link show {self._interface}", check=False)
|
|
144
|
+
if result.success:
|
|
145
|
+
if "UP" in result.stdout:
|
|
146
|
+
info["state"] = "up"
|
|
147
|
+
elif "DOWN" in result.stdout:
|
|
148
|
+
info["state"] = "down"
|
|
149
|
+
|
|
150
|
+
# Get IP address
|
|
151
|
+
result = run_command(f"ip addr show {self._interface}", check=False)
|
|
152
|
+
if result.success:
|
|
153
|
+
for line in result.stdout.split("\n"):
|
|
154
|
+
if "inet " in line:
|
|
155
|
+
parts = line.strip().split()
|
|
156
|
+
if len(parts) >= 2:
|
|
157
|
+
info["ip"] = parts[1].split("/")[0]
|
|
158
|
+
break
|
|
159
|
+
|
|
160
|
+
return info
|
|
161
|
+
|
|
162
|
+
def discover_active_peers(self) -> list[str]:
|
|
163
|
+
"""Discover which peers are currently active on the network.
|
|
164
|
+
|
|
165
|
+
This uses ARP table and ping to find active peers.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
List of active peer IPs
|
|
169
|
+
"""
|
|
170
|
+
active_peers = []
|
|
171
|
+
|
|
172
|
+
# Check ARP table for the VPN interface
|
|
173
|
+
result = run_command(f"ip neigh show dev {self._interface}", check=False)
|
|
174
|
+
if result.success:
|
|
175
|
+
for line in result.stdout.strip().split("\n"):
|
|
176
|
+
if line and "REACHABLE" in line:
|
|
177
|
+
parts = line.split()
|
|
178
|
+
if parts:
|
|
179
|
+
active_peers.append(parts[0])
|
|
180
|
+
|
|
181
|
+
return active_peers
|
|
182
|
+
|
|
183
|
+
def wait_for_interface(self, timeout: float = 30.0) -> bool:
|
|
184
|
+
"""Wait for the VPN interface to come up.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
timeout: Maximum time to wait in seconds
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
True if interface came up, False if timed out
|
|
191
|
+
"""
|
|
192
|
+
import time
|
|
193
|
+
|
|
194
|
+
start = time.time()
|
|
195
|
+
while time.time() - start < timeout:
|
|
196
|
+
info = self.get_interface_info()
|
|
197
|
+
if info["state"] == "up" and info["ip"]:
|
|
198
|
+
return True
|
|
199
|
+
time.sleep(1)
|
|
200
|
+
|
|
201
|
+
return False
|
redundanet/vpn/tinc.py
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""Tinc VPN management for RedundaNet."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from jinja2 import Template
|
|
10
|
+
|
|
11
|
+
from redundanet.core.exceptions import VPNError
|
|
12
|
+
from redundanet.utils.files import ensure_dir, read_file, write_file
|
|
13
|
+
from redundanet.utils.logging import get_logger
|
|
14
|
+
from redundanet.utils.process import is_command_available, run_command
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from redundanet.core.config import NodeConfig
|
|
18
|
+
|
|
19
|
+
logger = get_logger(__name__)
|
|
20
|
+
|
|
21
|
+
# Tinc configuration templates
|
|
22
|
+
TINC_CONF_TEMPLATE = """# Tinc configuration for {{ network_name }}
|
|
23
|
+
# Generated by RedundaNet
|
|
24
|
+
|
|
25
|
+
Name = {{ node_name }}
|
|
26
|
+
Mode = switch
|
|
27
|
+
Device = /dev/net/tun
|
|
28
|
+
DeviceType = tun
|
|
29
|
+
AddressFamily = ipv4
|
|
30
|
+
Port = {{ port }}
|
|
31
|
+
|
|
32
|
+
{% for connect_node in connect_to %}
|
|
33
|
+
ConnectTo = {{ connect_node }}
|
|
34
|
+
{% endfor %}
|
|
35
|
+
|
|
36
|
+
# Security settings
|
|
37
|
+
Compression = 9
|
|
38
|
+
Cipher = aes-256-cbc
|
|
39
|
+
Digest = sha256
|
|
40
|
+
|
|
41
|
+
# Process settings
|
|
42
|
+
ProcessPriority = high
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
TINC_UP_TEMPLATE = """#!/bin/bash
|
|
46
|
+
# Tinc-up script for {{ network_name }}
|
|
47
|
+
# Generated by RedundaNet
|
|
48
|
+
|
|
49
|
+
ip link set $INTERFACE up
|
|
50
|
+
ip addr add {{ vpn_ip }}/32 dev $INTERFACE
|
|
51
|
+
ip route add {{ vpn_network }} dev $INTERFACE
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
TINC_DOWN_TEMPLATE = """#!/bin/bash
|
|
55
|
+
# Tinc-down script for {{ network_name }}
|
|
56
|
+
# Generated by RedundaNet
|
|
57
|
+
|
|
58
|
+
ip route del {{ vpn_network }} dev $INTERFACE 2>/dev/null || true
|
|
59
|
+
ip addr del {{ vpn_ip }}/32 dev $INTERFACE 2>/dev/null || true
|
|
60
|
+
ip link set $INTERFACE down
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
HOST_FILE_TEMPLATE = """# Host file for {{ node_name }}
|
|
64
|
+
# Generated by RedundaNet
|
|
65
|
+
|
|
66
|
+
Subnet = {{ vpn_ip }}/32
|
|
67
|
+
{% if public_ip %}Address = {{ public_ip }}{% endif %}
|
|
68
|
+
Port = {{ port }}
|
|
69
|
+
|
|
70
|
+
{{ public_key }}
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class TincConfig:
|
|
76
|
+
"""Configuration for Tinc VPN."""
|
|
77
|
+
|
|
78
|
+
network_name: str = "redundanet"
|
|
79
|
+
node_name: str = ""
|
|
80
|
+
vpn_ip: str = ""
|
|
81
|
+
vpn_network: str = "10.100.0.0/16"
|
|
82
|
+
port: int = 655
|
|
83
|
+
public_ip: str | None = None
|
|
84
|
+
connect_to: list[str] = field(default_factory=list)
|
|
85
|
+
config_dir: Path = field(default_factory=lambda: Path("/etc/tinc"))
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def network_dir(self) -> Path:
|
|
89
|
+
"""Get the network-specific configuration directory."""
|
|
90
|
+
return self.config_dir / self.network_name
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def hosts_dir(self) -> Path:
|
|
94
|
+
"""Get the hosts directory."""
|
|
95
|
+
return self.network_dir / "hosts"
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def from_node_config(cls, node: NodeConfig, network_name: str = "redundanet") -> TincConfig:
|
|
99
|
+
"""Create TincConfig from a NodeConfig."""
|
|
100
|
+
return cls(
|
|
101
|
+
network_name=network_name,
|
|
102
|
+
node_name=node.name,
|
|
103
|
+
vpn_ip=node.vpn_ip or node.internal_ip,
|
|
104
|
+
port=node.ports.tinc,
|
|
105
|
+
public_ip=node.public_ip,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class TincManager:
|
|
110
|
+
"""Manages Tinc VPN configuration and operations."""
|
|
111
|
+
|
|
112
|
+
def __init__(self, config: TincConfig) -> None:
|
|
113
|
+
self.config = config
|
|
114
|
+
self._private_key_path = self.config.network_dir / "rsa_key.priv"
|
|
115
|
+
self._public_key_path = self.config.hosts_dir / self.config.node_name
|
|
116
|
+
|
|
117
|
+
def ensure_tinc_installed(self) -> bool:
|
|
118
|
+
"""Check if tinc is installed."""
|
|
119
|
+
if not is_command_available("tincd"):
|
|
120
|
+
raise VPNError("Tinc is not installed. Please install tinc first.")
|
|
121
|
+
return True
|
|
122
|
+
|
|
123
|
+
def setup(self, peers: list[NodeConfig] | None = None) -> None:
|
|
124
|
+
"""Set up the complete Tinc configuration.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
peers: List of peer nodes to connect to
|
|
128
|
+
"""
|
|
129
|
+
logger.info("Setting up Tinc VPN", node=self.config.node_name)
|
|
130
|
+
|
|
131
|
+
# Create directories
|
|
132
|
+
ensure_dir(self.config.network_dir, mode=0o755)
|
|
133
|
+
ensure_dir(self.config.hosts_dir, mode=0o755)
|
|
134
|
+
|
|
135
|
+
# Determine which nodes to connect to
|
|
136
|
+
connect_nodes = []
|
|
137
|
+
if peers:
|
|
138
|
+
for peer in peers:
|
|
139
|
+
if peer.is_publicly_accessible and peer.name != self.config.node_name:
|
|
140
|
+
connect_nodes.append(peer.name)
|
|
141
|
+
self.config.connect_to = connect_nodes
|
|
142
|
+
|
|
143
|
+
# Generate configuration files
|
|
144
|
+
self._write_tinc_conf()
|
|
145
|
+
self._write_tinc_up()
|
|
146
|
+
self._write_tinc_down()
|
|
147
|
+
|
|
148
|
+
# Generate keys if they don't exist
|
|
149
|
+
if not self._private_key_path.exists():
|
|
150
|
+
self.generate_keys()
|
|
151
|
+
|
|
152
|
+
# Write peer host files
|
|
153
|
+
if peers:
|
|
154
|
+
for peer in peers:
|
|
155
|
+
if peer.name != self.config.node_name:
|
|
156
|
+
self._write_host_file(peer)
|
|
157
|
+
|
|
158
|
+
logger.info("Tinc VPN setup complete", node=self.config.node_name)
|
|
159
|
+
|
|
160
|
+
def _write_tinc_conf(self) -> None:
|
|
161
|
+
"""Write the main tinc.conf file."""
|
|
162
|
+
template = Template(TINC_CONF_TEMPLATE)
|
|
163
|
+
content = template.render(
|
|
164
|
+
network_name=self.config.network_name,
|
|
165
|
+
node_name=self.config.node_name,
|
|
166
|
+
port=self.config.port,
|
|
167
|
+
connect_to=self.config.connect_to,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
conf_path = self.config.network_dir / "tinc.conf"
|
|
171
|
+
write_file(conf_path, content, mode=0o644)
|
|
172
|
+
logger.debug("Wrote tinc.conf", path=str(conf_path))
|
|
173
|
+
|
|
174
|
+
def _write_tinc_up(self) -> None:
|
|
175
|
+
"""Write the tinc-up script."""
|
|
176
|
+
template = Template(TINC_UP_TEMPLATE)
|
|
177
|
+
content = template.render(
|
|
178
|
+
network_name=self.config.network_name,
|
|
179
|
+
vpn_ip=self.config.vpn_ip,
|
|
180
|
+
vpn_network=self.config.vpn_network,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
script_path = self.config.network_dir / "tinc-up"
|
|
184
|
+
write_file(script_path, content, executable=True)
|
|
185
|
+
logger.debug("Wrote tinc-up", path=str(script_path))
|
|
186
|
+
|
|
187
|
+
def _write_tinc_down(self) -> None:
|
|
188
|
+
"""Write the tinc-down script."""
|
|
189
|
+
template = Template(TINC_DOWN_TEMPLATE)
|
|
190
|
+
content = template.render(
|
|
191
|
+
network_name=self.config.network_name,
|
|
192
|
+
vpn_ip=self.config.vpn_ip,
|
|
193
|
+
vpn_network=self.config.vpn_network,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
script_path = self.config.network_dir / "tinc-down"
|
|
197
|
+
write_file(script_path, content, executable=True)
|
|
198
|
+
logger.debug("Wrote tinc-down", path=str(script_path))
|
|
199
|
+
|
|
200
|
+
def _write_host_file(
|
|
201
|
+
self,
|
|
202
|
+
node: NodeConfig,
|
|
203
|
+
public_key: str | None = None,
|
|
204
|
+
) -> None:
|
|
205
|
+
"""Write a host file for a node."""
|
|
206
|
+
template = Template(HOST_FILE_TEMPLATE)
|
|
207
|
+
content = template.render(
|
|
208
|
+
node_name=node.name,
|
|
209
|
+
vpn_ip=node.vpn_ip or node.internal_ip,
|
|
210
|
+
public_ip=node.public_ip if node.is_publicly_accessible else None,
|
|
211
|
+
port=node.ports.tinc,
|
|
212
|
+
public_key=public_key or "",
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
host_path = self.config.hosts_dir / node.name
|
|
216
|
+
write_file(host_path, content, mode=0o644)
|
|
217
|
+
logger.debug("Wrote host file", node=node.name, path=str(host_path))
|
|
218
|
+
|
|
219
|
+
def generate_keys(self) -> str:
|
|
220
|
+
"""Generate Tinc RSA keypair.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
The public key as a string
|
|
224
|
+
"""
|
|
225
|
+
logger.info("Generating Tinc keys", node=self.config.node_name)
|
|
226
|
+
|
|
227
|
+
# Create empty host file for key generation
|
|
228
|
+
host_file = self.config.hosts_dir / self.config.node_name
|
|
229
|
+
if not host_file.exists():
|
|
230
|
+
header = f"# Host file for {self.config.node_name}\n"
|
|
231
|
+
header += f"Subnet = {self.config.vpn_ip}/32\n"
|
|
232
|
+
if self.config.public_ip:
|
|
233
|
+
header += f"Address = {self.config.public_ip}\n"
|
|
234
|
+
header += f"Port = {self.config.port}\n"
|
|
235
|
+
write_file(host_file, header, mode=0o644)
|
|
236
|
+
|
|
237
|
+
# Generate keys using tincd
|
|
238
|
+
result = run_command(
|
|
239
|
+
f"tincd -n {self.config.network_name} -K4096",
|
|
240
|
+
input_text="\n", # Accept defaults
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
if not result.success:
|
|
244
|
+
raise VPNError(f"Failed to generate keys: {result.stderr}")
|
|
245
|
+
|
|
246
|
+
# Read and return the public key
|
|
247
|
+
return self.get_public_key()
|
|
248
|
+
|
|
249
|
+
def get_public_key(self) -> str:
|
|
250
|
+
"""Get the public key for this node."""
|
|
251
|
+
host_file = self.config.hosts_dir / self.config.node_name
|
|
252
|
+
if not host_file.exists():
|
|
253
|
+
raise VPNError(f"Host file not found: {host_file}")
|
|
254
|
+
|
|
255
|
+
content = read_file(host_file)
|
|
256
|
+
|
|
257
|
+
# Extract the public key portion
|
|
258
|
+
lines = content.split("\n")
|
|
259
|
+
in_key = False
|
|
260
|
+
key_lines = []
|
|
261
|
+
|
|
262
|
+
for line in lines:
|
|
263
|
+
if "BEGIN RSA PUBLIC KEY" in line:
|
|
264
|
+
in_key = True
|
|
265
|
+
if in_key:
|
|
266
|
+
key_lines.append(line)
|
|
267
|
+
if "END RSA PUBLIC KEY" in line:
|
|
268
|
+
break
|
|
269
|
+
|
|
270
|
+
if not key_lines:
|
|
271
|
+
raise VPNError("No public key found in host file")
|
|
272
|
+
|
|
273
|
+
return "\n".join(key_lines)
|
|
274
|
+
|
|
275
|
+
def start(self) -> bool:
|
|
276
|
+
"""Start the Tinc VPN daemon."""
|
|
277
|
+
logger.info("Starting Tinc VPN", network=self.config.network_name)
|
|
278
|
+
|
|
279
|
+
result = run_command(f"tincd -n {self.config.network_name}")
|
|
280
|
+
if not result.success:
|
|
281
|
+
logger.error("Failed to start Tinc", error=result.stderr)
|
|
282
|
+
return False
|
|
283
|
+
|
|
284
|
+
logger.info("Tinc VPN started")
|
|
285
|
+
return True
|
|
286
|
+
|
|
287
|
+
def stop(self) -> bool:
|
|
288
|
+
"""Stop the Tinc VPN daemon."""
|
|
289
|
+
logger.info("Stopping Tinc VPN", network=self.config.network_name)
|
|
290
|
+
|
|
291
|
+
result = run_command(f"tincd -n {self.config.network_name} -k")
|
|
292
|
+
if not result.success:
|
|
293
|
+
logger.error("Failed to stop Tinc", error=result.stderr)
|
|
294
|
+
return False
|
|
295
|
+
|
|
296
|
+
logger.info("Tinc VPN stopped")
|
|
297
|
+
return True
|
|
298
|
+
|
|
299
|
+
def reload(self) -> bool:
|
|
300
|
+
"""Reload Tinc VPN configuration."""
|
|
301
|
+
logger.info("Reloading Tinc VPN", network=self.config.network_name)
|
|
302
|
+
|
|
303
|
+
result = run_command(f"tincd -n {self.config.network_name} -kHUP")
|
|
304
|
+
return result.success
|
|
305
|
+
|
|
306
|
+
def is_running(self) -> bool:
|
|
307
|
+
"""Check if Tinc is running."""
|
|
308
|
+
pid_file = Path(f"/var/run/tinc.{self.config.network_name}.pid")
|
|
309
|
+
if not pid_file.exists():
|
|
310
|
+
return False
|
|
311
|
+
|
|
312
|
+
pid = int(pid_file.read_text().strip())
|
|
313
|
+
return Path(f"/proc/{pid}").exists()
|
|
314
|
+
|
|
315
|
+
def get_status(self) -> dict[str, object]:
|
|
316
|
+
"""Get the current status of Tinc VPN."""
|
|
317
|
+
return {
|
|
318
|
+
"running": self.is_running(),
|
|
319
|
+
"network": self.config.network_name,
|
|
320
|
+
"node": self.config.node_name,
|
|
321
|
+
"vpn_ip": self.config.vpn_ip,
|
|
322
|
+
"config_dir": str(self.config.network_dir),
|
|
323
|
+
}
|