stuff
This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
"""VPN connection management using NetworkManager (nmcli)."""
|
||||
"""Enhanced VPN management with VPNTray naming and route control."""
|
||||
|
||||
import subprocess
|
||||
import tempfile
|
||||
import os
|
||||
import re
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Dict, List
|
||||
from typing import Optional, List
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from models import Location
|
||||
|
||||
|
||||
class VPNStatus(Enum):
|
||||
@@ -26,286 +26,482 @@ class VPNConnectionError(Exception):
|
||||
|
||||
|
||||
@dataclass
|
||||
class VPNConnection:
|
||||
"""Represents a NetworkManager VPN connection."""
|
||||
class VPNConnectionInfo:
|
||||
"""Information about a VPN connection."""
|
||||
name: str
|
||||
uuid: str
|
||||
type: str
|
||||
vpntray_name: str # Our custom name with vpntray_ prefix
|
||||
status: VPNStatus
|
||||
device: Optional[str] = None
|
||||
state: VPNStatus = VPNStatus.UNKNOWN
|
||||
vpn_type: Optional[str] = None # OpenVPN, WireGuard, etc.
|
||||
routes: List[str] = None # List of routes added
|
||||
|
||||
|
||||
class VPNManager:
|
||||
"""Manages VPN connections through NetworkManager CLI (nmcli)."""
|
||||
"""Enhanced VPN manager with VPNTray naming and route management."""
|
||||
|
||||
VPNTRAY_PREFIX = "vpntray_"
|
||||
VPN_CONFIG_DIR = Path.home() / ".vpntray" / "vpn"
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize VPN manager and check for nmcli availability."""
|
||||
"""Initialize VPN manager."""
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self._check_nmcli_available()
|
||||
self._ensure_vpn_config_dir()
|
||||
|
||||
def _check_nmcli_available(self) -> None:
|
||||
"""Check if nmcli is available on the system."""
|
||||
"""Check if nmcli is available."""
|
||||
try:
|
||||
subprocess.run(['nmcli', '--version'],
|
||||
capture_output=True, check=True)
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
raise VPNConnectionError(
|
||||
"nmcli is not available. Please install NetworkManager.")
|
||||
"nmcli is not available. Install NetworkManager.")
|
||||
|
||||
def _ensure_vpn_config_dir(self) -> None:
|
||||
"""Ensure VPN config directory exists."""
|
||||
self.VPN_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def _run_nmcli(self, args: List[str], check: bool = True, timeout: int = 30) -> subprocess.CompletedProcess:
|
||||
"""Run nmcli command with logging and timeout."""
|
||||
command = ['nmcli'] + args
|
||||
command_str = ' '.join(command)
|
||||
|
||||
def _run_nmcli(self, args: List[str], check: bool = True) -> subprocess.CompletedProcess:
|
||||
"""Run an nmcli command with error handling."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['nmcli'] + args,
|
||||
command,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=check
|
||||
check=check,
|
||||
timeout=timeout # Add timeout to prevent hanging
|
||||
)
|
||||
|
||||
self.logger.debug(f"Command: {command_str}")
|
||||
if result.stdout.strip():
|
||||
self.logger.debug(f"Output: {result.stdout.strip()}")
|
||||
if result.stderr.strip():
|
||||
self.logger.warning(f"Stderr: {result.stderr.strip()}")
|
||||
if result.returncode == 0:
|
||||
self.logger.debug("Command completed successfully")
|
||||
else:
|
||||
self.logger.error(
|
||||
f"Command exited with code: {result.returncode}")
|
||||
|
||||
return result
|
||||
except subprocess.TimeoutExpired:
|
||||
self.logger.error(
|
||||
f"Command timed out after {timeout}s: {command_str}")
|
||||
raise VPNConnectionError(
|
||||
f"nmcli command timed out after {timeout} seconds")
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise VPNConnectionError(f"nmcli command failed: {e.stderr}")
|
||||
|
||||
def import_ovpn(self, ovpn_path: str, connection_name: Optional[str] = None) -> str:
|
||||
"""Import an OpenVPN configuration file.
|
||||
|
||||
Args:
|
||||
ovpn_path: Path to the .ovpn configuration file
|
||||
connection_name: Optional custom name for the connection
|
||||
|
||||
Returns:
|
||||
The name of the imported connection
|
||||
"""
|
||||
ovpn_file = Path(ovpn_path)
|
||||
if not ovpn_file.exists():
|
||||
self.logger.debug(f"Failed command: {command_str}")
|
||||
if e.stdout and e.stdout.strip():
|
||||
self.logger.debug(f"Output: {e.stdout.strip()}")
|
||||
if e.stderr and e.stderr.strip():
|
||||
self.logger.error(f"Error: {e.stderr.strip()}")
|
||||
error_details = e.stderr or str(e)
|
||||
raise VPNConnectionError(
|
||||
f"OpenVPN config file not found: {ovpn_path}")
|
||||
f"nmcli command failed (exit code {e.returncode}): {error_details}")
|
||||
|
||||
# Import the configuration
|
||||
result = self._run_nmcli([
|
||||
'connection', 'import', 'type', 'openvpn', 'file', str(ovpn_file)
|
||||
])
|
||||
def _get_vpntray_connection_name(self, config_filename: str) -> str:
|
||||
"""Generate VPNTray-specific connection name."""
|
||||
# Remove extension and sanitize
|
||||
base_name = Path(config_filename).stem
|
||||
sanitized = re.sub(r'[^a-zA-Z0-9_-]', '_', base_name)
|
||||
return f"{self.VPNTRAY_PREFIX}{sanitized}"
|
||||
|
||||
# Extract connection name from output
|
||||
# nmcli typically outputs: "Connection 'name' (uuid) successfully added."
|
||||
match = re.search(r"Connection '([^']+)'", result.stdout)
|
||||
if not match:
|
||||
raise VPNConnectionError(
|
||||
"Failed to parse imported connection name")
|
||||
|
||||
imported_name = match.group(1)
|
||||
|
||||
# Rename if custom name provided
|
||||
if connection_name and connection_name != imported_name:
|
||||
self._run_nmcli([
|
||||
'connection', 'modify', imported_name,
|
||||
'connection.id', connection_name
|
||||
])
|
||||
return connection_name
|
||||
|
||||
return imported_name
|
||||
|
||||
def connect(self, connection_name: str,
|
||||
username: Optional[str] = None,
|
||||
password: Optional[str] = None) -> None:
|
||||
"""Connect to a VPN.
|
||||
|
||||
Args:
|
||||
connection_name: Name of the NetworkManager connection
|
||||
username: Optional username for authentication
|
||||
password: Optional password for authentication
|
||||
"""
|
||||
if username and password:
|
||||
# Create temporary secrets file
|
||||
with tempfile.NamedTemporaryFile(mode='w', delete=False) as f:
|
||||
f.write(f"vpn.secrets.password:{password}\n")
|
||||
if username:
|
||||
f.write(f"vpn.data.username:{username}\n")
|
||||
secrets_file = f.name
|
||||
|
||||
try:
|
||||
self._run_nmcli([
|
||||
'connection', 'up', connection_name,
|
||||
'passwd-file', secrets_file
|
||||
])
|
||||
finally:
|
||||
# Always clean up secrets file
|
||||
os.unlink(secrets_file)
|
||||
else:
|
||||
# Connect without credentials (will prompt if needed)
|
||||
self._run_nmcli(['connection', 'up', connection_name])
|
||||
|
||||
def disconnect(self, connection_name: str) -> None:
|
||||
"""Disconnect from a VPN.
|
||||
|
||||
Args:
|
||||
connection_name: Name of the NetworkManager connection
|
||||
"""
|
||||
self._run_nmcli(['connection', 'down', connection_name])
|
||||
|
||||
def get_status(self, connection_name: str) -> VPNStatus:
|
||||
"""Get the status of a VPN connection.
|
||||
|
||||
Args:
|
||||
connection_name: Name of the NetworkManager connection
|
||||
|
||||
Returns:
|
||||
Current status of the VPN connection
|
||||
"""
|
||||
result = self._run_nmcli(
|
||||
['connection', 'show', '--active'],
|
||||
check=False
|
||||
)
|
||||
|
||||
if connection_name in result.stdout:
|
||||
# Parse the actual state
|
||||
state_result = self._run_nmcli(
|
||||
['connection', 'show', connection_name],
|
||||
check=False
|
||||
)
|
||||
|
||||
if 'GENERAL.STATE:' in state_result.stdout:
|
||||
if 'activated' in state_result.stdout:
|
||||
return VPNStatus.CONNECTED
|
||||
elif 'activating' in state_result.stdout:
|
||||
return VPNStatus.CONNECTING
|
||||
elif 'deactivating' in state_result.stdout:
|
||||
return VPNStatus.DISCONNECTING
|
||||
|
||||
return VPNStatus.DISCONNECTED
|
||||
|
||||
def list_connections(self, vpn_only: bool = True) -> List[VPNConnection]:
|
||||
"""List all NetworkManager connections.
|
||||
|
||||
Args:
|
||||
vpn_only: If True, only return VPN connections
|
||||
|
||||
Returns:
|
||||
List of VPNConnection objects
|
||||
"""
|
||||
args = ['connection', 'show']
|
||||
if vpn_only:
|
||||
args.extend(['--type', 'vpn'])
|
||||
|
||||
result = self._run_nmcli(args, check=False)
|
||||
def get_vpn_config_path(self, filename: str) -> Path:
|
||||
"""Get full path to VPN config file."""
|
||||
return self.VPN_CONFIG_DIR / filename
|
||||
|
||||
def list_vpntray_connections(self) -> List[VPNConnectionInfo]:
|
||||
"""List all VPNTray-managed connections."""
|
||||
connections = []
|
||||
for line in result.stdout.strip().split('\n')[1:]: # Skip header
|
||||
if not line:
|
||||
continue
|
||||
|
||||
parts = line.split()
|
||||
if len(parts) >= 4:
|
||||
name = parts[0]
|
||||
uuid = parts[1]
|
||||
conn_type = parts[2]
|
||||
device = parts[3] if parts[3] != '--' else None
|
||||
try:
|
||||
result = self._run_nmcli(['connection', 'show'])
|
||||
for line in result.stdout.strip().split('\\n'):
|
||||
if self.VPNTRAY_PREFIX in line:
|
||||
parts = line.split()
|
||||
if len(parts) >= 4:
|
||||
name = parts[0]
|
||||
uuid = parts[1]
|
||||
device = parts[3] if parts[3] != '--' else None
|
||||
|
||||
# Get current status
|
||||
status = self.get_status(name)
|
||||
# Get detailed status
|
||||
status = self._get_connection_status(name)
|
||||
|
||||
connections.append(VPNConnection(
|
||||
name=name,
|
||||
uuid=uuid,
|
||||
type=conn_type,
|
||||
device=device,
|
||||
state=status
|
||||
))
|
||||
connections.append(VPNConnectionInfo(
|
||||
name=name,
|
||||
uuid=uuid,
|
||||
vpntray_name=name,
|
||||
status=status,
|
||||
device=device
|
||||
))
|
||||
except VPNConnectionError:
|
||||
pass # No connections or nmcli error
|
||||
|
||||
return connections
|
||||
|
||||
def delete_connection(self, connection_name: str) -> None:
|
||||
"""Delete a NetworkManager connection.
|
||||
def _get_connection_status(self, connection_name: str) -> VPNStatus:
|
||||
"""Get the status of a specific connection."""
|
||||
try:
|
||||
result = self._run_nmcli(['connection', 'show', connection_name])
|
||||
|
||||
Args:
|
||||
connection_name: Name of the connection to delete
|
||||
"""
|
||||
self._run_nmcli(['connection', 'delete', connection_name])
|
||||
# Parse connection state from output
|
||||
for line in result.stdout.split('\\n'):
|
||||
if 'GENERAL.STATE:' in line:
|
||||
state = line.split(':')[1].strip()
|
||||
if 'activated' in state.lower():
|
||||
return VPNStatus.CONNECTED
|
||||
elif 'activating' in state.lower():
|
||||
return VPNStatus.CONNECTING
|
||||
elif 'deactivating' in state.lower():
|
||||
return VPNStatus.DISCONNECTING
|
||||
else:
|
||||
return VPNStatus.DISCONNECTED
|
||||
except VPNConnectionError:
|
||||
pass
|
||||
|
||||
def connection_exists(self, connection_name: str) -> bool:
|
||||
"""Check if a connection exists.
|
||||
return VPNStatus.UNKNOWN
|
||||
|
||||
Args:
|
||||
connection_name: Name of the connection to check
|
||||
def import_vpn_config(self, location: Location) -> str:
|
||||
"""Import VPN configuration for a location with VPNTray naming."""
|
||||
config_path = self.get_vpn_config_path(location.vpn_config)
|
||||
|
||||
Returns:
|
||||
True if the connection exists
|
||||
"""
|
||||
result = self._run_nmcli(
|
||||
['connection', 'show', connection_name],
|
||||
check=False
|
||||
)
|
||||
return result.returncode == 0
|
||||
if not config_path.exists():
|
||||
raise VPNConnectionError(f"VPN config not found: {config_path}")
|
||||
|
||||
def modify_connection(self, connection_name: str,
|
||||
settings: Dict[str, str]) -> None:
|
||||
"""Modify connection settings.
|
||||
self.logger.info(
|
||||
f"Config file exists: {config_path} ({config_path.stat().st_size} bytes)")
|
||||
|
||||
Args:
|
||||
connection_name: Name of the connection to modify
|
||||
settings: Dictionary of setting key-value pairs
|
||||
e.g., {'vpn.data.comp-lzo': 'yes'}
|
||||
"""
|
||||
for key, value in settings.items():
|
||||
vpntray_name = self._get_vpntray_connection_name(location.vpn_config)
|
||||
|
||||
# Check if already imported
|
||||
if self._get_connection_by_name(vpntray_name):
|
||||
self.logger.info(f"Connection already imported: {vpntray_name}")
|
||||
return vpntray_name
|
||||
|
||||
# Import based on VPN type
|
||||
self.logger.info(
|
||||
f"Importing {location.vpn_type.value} config: {config_path.name}")
|
||||
|
||||
if location.vpn_type.value == "OpenVPN":
|
||||
return self._import_openvpn(config_path, vpntray_name, location)
|
||||
elif location.vpn_type.value == "WireGuard":
|
||||
return self._import_wireguard(config_path, vpntray_name, location)
|
||||
else:
|
||||
raise VPNConnectionError(
|
||||
f"Unsupported VPN type: {location.vpn_type.value}")
|
||||
|
||||
def _import_openvpn(self, config_path: Path, vpntray_name: str, location: Location) -> str:
|
||||
"""Import OpenVPN configuration with route control."""
|
||||
# Import the config file first (nmcli will auto-generate a name)
|
||||
import_args = [
|
||||
'connection', 'import', 'type', 'openvpn',
|
||||
'file', str(config_path)
|
||||
]
|
||||
self.logger.info(f"Running nmcli import: {' '.join(import_args)}")
|
||||
|
||||
try:
|
||||
result = self._run_nmcli(import_args)
|
||||
|
||||
# Extract the auto-generated connection name from the output
|
||||
# nmcli outputs: "Connection 'name' (uuid) successfully added."
|
||||
import re
|
||||
match = re.search(r"Connection '([^']+)'", result.stdout)
|
||||
if not match:
|
||||
raise VPNConnectionError(
|
||||
"Failed to parse imported connection name from nmcli output")
|
||||
|
||||
auto_generated_name = match.group(1)
|
||||
self.logger.info(
|
||||
f"Config imported with auto name: {auto_generated_name}")
|
||||
|
||||
# Rename to our VPNTray naming convention
|
||||
rename_args = [
|
||||
'connection', 'modify', auto_generated_name,
|
||||
'connection.id', vpntray_name
|
||||
]
|
||||
self.logger.info(f"Renaming to: {vpntray_name}")
|
||||
|
||||
self._run_nmcli(rename_args)
|
||||
self.logger.info(
|
||||
f"OpenVPN config imported as {vpntray_name}")
|
||||
|
||||
except VPNConnectionError as e:
|
||||
self.logger.error(f"OpenVPN import failed: {e}")
|
||||
raise
|
||||
|
||||
# Configure credentials immediately after import if provided
|
||||
if location.vpn_credentials:
|
||||
self._configure_credentials(vpntray_name, location)
|
||||
|
||||
# Configure the connection to not route everything by default
|
||||
self._configure_connection_routes(vpntray_name, location)
|
||||
|
||||
return vpntray_name
|
||||
|
||||
def _import_wireguard(self, config_path: Path, vpntray_name: str, location: Location) -> str:
|
||||
"""Import WireGuard configuration with route control."""
|
||||
# Import the config file first (nmcli will auto-generate a name)
|
||||
import_args = [
|
||||
'connection', 'import', 'type', 'wireguard',
|
||||
'file', str(config_path)
|
||||
]
|
||||
self.logger.info(
|
||||
f"Running nmcli import: {' '.join(import_args)}")
|
||||
|
||||
try:
|
||||
result = self._run_nmcli(import_args)
|
||||
|
||||
# Extract the auto-generated connection name from the output
|
||||
# nmcli outputs: "Connection 'name' (uuid) successfully added."
|
||||
import re
|
||||
match = re.search(r"Connection '([^']+)'", result.stdout)
|
||||
if not match:
|
||||
raise VPNConnectionError(
|
||||
"Failed to parse imported connection name from nmcli output")
|
||||
|
||||
auto_generated_name = match.group(1)
|
||||
self.logger.info(
|
||||
f"Config imported with auto name: {auto_generated_name}")
|
||||
|
||||
# Rename to our VPNTray naming convention
|
||||
rename_args = [
|
||||
'connection', 'modify', auto_generated_name,
|
||||
'connection.id', vpntray_name
|
||||
]
|
||||
self.logger.info(f"Renaming to: {vpntray_name}")
|
||||
|
||||
self._run_nmcli(rename_args)
|
||||
self.logger.info(
|
||||
f"WireGuard config imported as {vpntray_name}")
|
||||
|
||||
except VPNConnectionError as e:
|
||||
self.logger.error(f"WireGuard import failed: {e}")
|
||||
raise
|
||||
|
||||
# Configure credentials immediately after import if provided
|
||||
if location.vpn_credentials:
|
||||
self._configure_credentials(vpntray_name, location)
|
||||
|
||||
# Configure routes
|
||||
self._configure_connection_routes(vpntray_name, location)
|
||||
|
||||
return vpntray_name
|
||||
|
||||
def _configure_connection_routes(self, connection_name: str, location: Location) -> None:
|
||||
"""Configure connection to only route specified network segments."""
|
||||
try:
|
||||
# Disable automatic default route
|
||||
self._run_nmcli([
|
||||
'connection', 'modify', connection_name,
|
||||
key, value
|
||||
'ipv4.never-default', 'true'
|
||||
])
|
||||
|
||||
def get_connection_details(self, connection_name: str) -> Dict[str, str]:
|
||||
"""Get detailed information about a connection.
|
||||
# Add routes for each network segment
|
||||
routes = []
|
||||
for segment in location.network_segments:
|
||||
# Add route for the network segment
|
||||
routes.append(segment.cidr)
|
||||
|
||||
Args:
|
||||
connection_name: Name of the connection
|
||||
if routes:
|
||||
routes_str = ','.join(routes)
|
||||
self._run_nmcli([
|
||||
'connection', 'modify', connection_name,
|
||||
'ipv4.routes', routes_str
|
||||
])
|
||||
self.logger.info(
|
||||
f"Configured routes for {connection_name}: {routes_str}")
|
||||
|
||||
Returns:
|
||||
Dictionary of connection properties
|
||||
"""
|
||||
result = self._run_nmcli(['connection', 'show', connection_name])
|
||||
except VPNConnectionError as e:
|
||||
self.logger.error(f"Failed to configure routes: {e}")
|
||||
# Don't fail the import, just log the error
|
||||
|
||||
details = {}
|
||||
for line in result.stdout.strip().split('\n'):
|
||||
if ':' in line:
|
||||
key, value = line.split(':', 1)
|
||||
details[key.strip()] = value.strip()
|
||||
|
||||
return details
|
||||
|
||||
def get_active_vpn_interface(self, connection_name: str) -> Optional[str]:
|
||||
"""Get the network interface used by an active VPN connection.
|
||||
|
||||
Args:
|
||||
connection_name: Name of the VPN connection
|
||||
|
||||
Returns:
|
||||
Interface name (e.g., 'tun0') or None if not connected
|
||||
"""
|
||||
if self.get_status(connection_name) != VPNStatus.CONNECTED:
|
||||
def _get_connection_by_name(self, name: str) -> Optional[VPNConnectionInfo]:
|
||||
"""Get connection info by name."""
|
||||
try:
|
||||
# Check if connection exists (simple and fast)
|
||||
result = self._run_nmcli(['connection', 'show', name], check=False)
|
||||
if result.returncode == 0:
|
||||
# Connection exists, create minimal info object
|
||||
return VPNConnectionInfo(
|
||||
name=name,
|
||||
uuid="unknown",
|
||||
vpntray_name=name,
|
||||
status=VPNStatus.UNKNOWN # Status will be checked when needed
|
||||
)
|
||||
return None
|
||||
except VPNConnectionError:
|
||||
return None
|
||||
|
||||
details = self.get_connection_details(connection_name)
|
||||
return details.get('GENERAL.DEVICES')
|
||||
def connect_vpn(self, location: Location) -> bool:
|
||||
"""Connect to VPN for a location."""
|
||||
try:
|
||||
vpntray_name = self._get_vpntray_connection_name(
|
||||
location.vpn_config)
|
||||
config_path = self.get_vpn_config_path(location.vpn_config)
|
||||
self.logger.info(f"VPN config: {config_path}")
|
||||
self.logger.info(f"Connection name: {vpntray_name}")
|
||||
|
||||
def get_vpn_ip_address(self, connection_name: str) -> Optional[str]:
|
||||
"""Get the IP address assigned to the VPN connection.
|
||||
# Check if config file exists
|
||||
if not config_path.exists():
|
||||
error_msg = f"VPN config file not found: {config_path}"
|
||||
self.logger.error(error_msg)
|
||||
return False
|
||||
|
||||
Args:
|
||||
connection_name: Name of the VPN connection
|
||||
# Import if not already imported
|
||||
existing_conn = self._get_connection_by_name(vpntray_name)
|
||||
if not existing_conn:
|
||||
self.logger.info(
|
||||
"Importing VPN config for first time...")
|
||||
try:
|
||||
self.import_vpn_config(location)
|
||||
self.logger.info(
|
||||
"VPN config imported successfully")
|
||||
except Exception as import_error:
|
||||
error_msg = f"Failed to import VPN config: {import_error}"
|
||||
self.logger.error(error_msg)
|
||||
return False
|
||||
else:
|
||||
self.logger.info(
|
||||
f"Using existing connection: {existing_conn.status.value}")
|
||||
|
||||
Returns:
|
||||
IP address or None if not connected
|
||||
"""
|
||||
interface = self.get_active_vpn_interface(connection_name)
|
||||
if not interface:
|
||||
return None
|
||||
# Connect with simple command - credentials already set during import
|
||||
self.logger.info("Attempting connection...")
|
||||
|
||||
result = self._run_nmcli(['device', 'show', interface], check=False)
|
||||
# Simple connection command without credential complications
|
||||
connect_args = ['connection', 'up', vpntray_name]
|
||||
self._run_nmcli(connect_args, timeout=60)
|
||||
self.logger.info(f"Connected to {vpntray_name}")
|
||||
|
||||
for line in result.stdout.split('\n'):
|
||||
if 'IP4.ADDRESS' in line and 'IP4.ADDRESS[2]' not in line:
|
||||
# Format is usually "IP4.ADDRESS[1]: 10.0.0.1/24"
|
||||
if ':' in line:
|
||||
addr_part = line.split(':', 1)[1].strip()
|
||||
if '/' in addr_part:
|
||||
return addr_part.split('/')[0]
|
||||
return True
|
||||
|
||||
return None
|
||||
except VPNConnectionError as e:
|
||||
self.logger.error(f"VPN connection failed: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.error(
|
||||
f"Unexpected error during connection: {e}")
|
||||
return False
|
||||
|
||||
def disconnect_vpn(self, location: Location) -> bool:
|
||||
"""Disconnect VPN for a location."""
|
||||
try:
|
||||
vpntray_name = self._get_vpntray_connection_name(
|
||||
location.vpn_config)
|
||||
self.logger.info(f"Disconnecting from {vpntray_name}...")
|
||||
|
||||
# Check if connection exists
|
||||
existing_conn = self._get_connection_by_name(vpntray_name)
|
||||
if not existing_conn:
|
||||
self.logger.error(
|
||||
f"Connection {vpntray_name} not found")
|
||||
return False
|
||||
|
||||
# Disconnect
|
||||
self._run_nmcli(['connection', 'down', vpntray_name])
|
||||
self.logger.info(f"Disconnected from {vpntray_name}")
|
||||
|
||||
return True
|
||||
|
||||
except VPNConnectionError as e:
|
||||
self.logger.error(f"Failed to disconnect: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.error(
|
||||
f"Unexpected error during disconnection: {e}")
|
||||
return False
|
||||
|
||||
def get_connection_status(self, location: Location) -> VPNStatus:
|
||||
"""Get connection status for a location."""
|
||||
vpntray_name = self._get_vpntray_connection_name(location.vpn_config)
|
||||
return self._get_connection_status(vpntray_name)
|
||||
|
||||
def remove_vpn_config(self, location: Location) -> bool:
|
||||
"""Remove VPN connection configuration."""
|
||||
try:
|
||||
vpntray_name = self._get_vpntray_connection_name(
|
||||
location.vpn_config)
|
||||
|
||||
# First disconnect if connected
|
||||
try:
|
||||
self._run_nmcli(
|
||||
['connection', 'down', vpntray_name], check=False)
|
||||
except VPNConnectionError:
|
||||
pass # Ignore if already disconnected
|
||||
|
||||
# Remove the connection
|
||||
self._run_nmcli(['connection', 'delete', vpntray_name])
|
||||
self.logger.info(
|
||||
f"Removed VPN configuration {vpntray_name}")
|
||||
|
||||
return True
|
||||
|
||||
except VPNConnectionError as e:
|
||||
self.logger.error(f"Failed to remove config: {e}")
|
||||
return False
|
||||
|
||||
def cleanup_vpntray_connections(self) -> int:
|
||||
"""Remove all VPNTray-managed connections. Returns count removed."""
|
||||
connections = self.list_vpntray_connections()
|
||||
removed_count = 0
|
||||
|
||||
for conn in connections:
|
||||
try:
|
||||
# Disconnect first
|
||||
self._run_nmcli(['connection', 'down', conn.name], check=False)
|
||||
# Remove
|
||||
self._run_nmcli(['connection', 'delete', conn.name])
|
||||
removed_count += 1
|
||||
except VPNConnectionError:
|
||||
pass # Continue with other connections
|
||||
|
||||
if self.logger and removed_count > 0:
|
||||
self.logger.info(
|
||||
f"Cleaned up {removed_count} VPNTray connections")
|
||||
|
||||
return removed_count
|
||||
|
||||
def _configure_credentials(self, connection_name: str, location: Location) -> None:
|
||||
"""Configure VPN credentials directly in the connection."""
|
||||
if not location.vpn_credentials:
|
||||
self.logger.info(
|
||||
f"No credentials provided for {connection_name}")
|
||||
return
|
||||
|
||||
try:
|
||||
# Handle dictionary credentials (username/password)
|
||||
if isinstance(location.vpn_credentials, dict):
|
||||
username = location.vpn_credentials.get('username')
|
||||
password = location.vpn_credentials.get('password')
|
||||
self.logger.info(
|
||||
f"Setting credentials for {connection_name}...")
|
||||
|
||||
# Set username and password with correct nmcli syntax
|
||||
if username:
|
||||
self._run_nmcli([
|
||||
'connection', 'modify', connection_name,
|
||||
'+vpn.data', f'username={username}'
|
||||
])
|
||||
self.logger.info(
|
||||
f"Username configured for {connection_name}")
|
||||
|
||||
if password:
|
||||
self._run_nmcli([
|
||||
'connection', 'modify', connection_name,
|
||||
'+vpn.secrets', f'password={password}'
|
||||
])
|
||||
self.logger.info(
|
||||
f"Password configured for {connection_name}")
|
||||
|
||||
if username and password:
|
||||
self.logger.info(
|
||||
f"Full credentials configured for {connection_name}")
|
||||
elif username or password:
|
||||
self.logger.info(
|
||||
f"Partial credentials configured for {connection_name}")
|
||||
|
||||
except VPNConnectionError as e:
|
||||
self.logger.error(f"Failed to configure credentials: {e}")
|
||||
# Don't fail the whole operation for credential issues
|
||||
|
||||
Reference in New Issue
Block a user