Files
pi-pm3/app/backend/managers/wifi_manager.py
michael 2ec89041ef Phase 4 enhancements: WebSocket auth, HTTPS UI, plugin hooks, build fixes
Security:
- Add token-based WebSocket authentication (closes critical security gap)
  - In-memory token store with 24h TTL (token_store.py)
  - POST /api/auth/token exchanges Basic Auth for WS token
  - GET /api/auth/status public endpoint for auth check
  - WebSocket validates token query param, rejects with close code 4401
  - Frontend LoginPrompt modal for credential entry
  - WebSocket manager handles full auth flow with auth_required state
  - No-op when AUTH_ENABLED=false (preserves existing behavior)

HTTPS:
- Wire HTTPS toggle in Settings UI (POST /api/system/ssl/toggle)
- Add certificate regeneration button
- Display SSL info (expiration, SANs, SHA256 fingerprint)

Plugins:
- Wire trigger_hook("pm3_command") in PM3 service
- Wire trigger_hook("update_check") in update manager

Build/Infrastructure:
- Enable NetworkManager in pi-gen AP setup stage
- Add HF booster board detection patch for Proxmark3
- Update LED PWM control patch
- Fix BLE adapter, UPS drivers, WiFi manager improvements
- Update HTTPS support stage script

Documentation:
- Update PROJECT_STATUS.md and IMPLEMENTATION_PRIORITIES.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 11:45:11 -08:00

1013 lines
34 KiB
Python

"""WiFi manager for network configuration and mode switching."""
import asyncio
import logging
import re
import subprocess
from dataclasses import dataclass
from enum import Enum
from typing import List, Optional, Dict
from pathlib import Path
from .. import config
logger = logging.getLogger(__name__)
# Persistent storage for WiFi mode
WIFI_MODE_FILE = Path("/opt/dangerous-pi/data/wifi_mode")
WIFI_DATA_DIR = Path("/opt/dangerous-pi/data")
class WiFiMode(str, Enum):
"""WiFi operation modes."""
AP = "ap" # Access Point only
CLIENT = "client" # Client mode only
DUAL = "dual" # AP + Client (requires USB WiFi)
AUTO = "auto" # Automatically choose best mode
OFF = "off" # WiFi disabled
@dataclass
class WiFiInterface:
"""WiFi interface information."""
name: str
mac: str
is_usb: bool
is_up: bool
connected: bool
ssid: Optional[str] = None
ip_address: Optional[str] = None
mode: str = "managed" # "AP" or "managed" (client)
@dataclass
class WiFiNetwork:
"""Available WiFi network."""
ssid: str
bssid: str
signal_strength: int
frequency: int
encrypted: bool
in_use: bool = False
@dataclass
class WiFiStatus:
"""Current WiFi status."""
mode: WiFiMode
interfaces: List[WiFiInterface]
current_ssid: Optional[str]
current_ip: Optional[str]
ap_ssid: Optional[str]
ap_ip: Optional[str]
supports_dual: bool
class WiFiManager:
"""Manages WiFi configuration and mode switching."""
def __init__(self):
"""Initialize WiFi manager."""
self._current_mode = WiFiMode.AUTO
self._interfaces: List[WiFiInterface] = []
self._supports_dual = False
self._startup_applied = False
def _save_mode(self, mode: WiFiMode) -> bool:
"""Save WiFi mode to persistent storage.
Args:
mode: WiFi mode to save
Returns:
True if saved successfully
"""
try:
# Ensure data directory exists
WIFI_DATA_DIR.mkdir(parents=True, exist_ok=True)
# Write mode to file
WIFI_MODE_FILE.write_text(mode.value)
logger.info(f"WiFi mode saved: {mode.value}")
return True
except Exception as e:
logger.error(f"Failed to save WiFi mode: {e}")
return False
def _load_mode(self) -> Optional[WiFiMode]:
"""Load WiFi mode from persistent storage.
Returns:
Saved WiFi mode or None if not found
"""
try:
if WIFI_MODE_FILE.exists():
mode_str = WIFI_MODE_FILE.read_text().strip()
mode = WiFiMode(mode_str)
logger.info(f"WiFi mode loaded: {mode.value}")
return mode
except Exception as e:
logger.warning(f"Failed to load WiFi mode: {e}")
return None
async def apply_saved_mode(self) -> bool:
"""Apply the saved WiFi mode on startup.
This should be called once during service startup to restore
the previously configured WiFi mode.
Returns:
True if mode was applied successfully
"""
if self._startup_applied:
logger.debug("Saved mode already applied, skipping")
return True
saved_mode = self._load_mode()
if saved_mode:
logger.info(f"Applying saved WiFi mode: {saved_mode.value}")
success = await self.set_mode(saved_mode, persist=False) # Don't re-save
self._startup_applied = True
return success
else:
logger.info("No saved WiFi mode found, using default (AUTO)")
self._startup_applied = True
return True
async def detect_interfaces(self) -> List[WiFiInterface]:
"""Detect available WiFi interfaces.
Returns:
List of WiFi interfaces found
"""
interfaces = []
try:
# Get all wireless interfaces using iw
result = await self._run_command("iw dev")
if not result:
return interfaces
# Parse iw dev output
# Example output:
# phy#0
# Interface wlan0
# ifindex 2
# wdev 0x1
# addr 2c:cf:67:87:db:13
# ssid Dangerous-Pi
# type AP
# channel 6 (2437 MHz), width: 20 MHz
current_interface = None
for line in result.split('\n'):
line = line.strip()
if line.startswith('Interface '):
if current_interface:
interfaces.append(current_interface)
iface_name = line.split()[1]
current_interface = {
'name': iface_name,
'mac': '',
'is_usb': self._is_usb_interface(iface_name),
'is_up': False,
'connected': False,
'ssid': None,
'ip_address': None,
'mode': 'managed' # Default to managed (client) mode
}
elif current_interface and line.startswith('addr '):
current_interface['mac'] = line.split()[1]
elif current_interface and line.startswith('ssid '):
current_interface['ssid'] = ' '.join(line.split()[1:])
elif current_interface and line.startswith('type '):
# Parse interface type: "AP" or "managed"
iface_type = line.split()[1]
current_interface['mode'] = iface_type
# Only mark as "connected" if in managed (client) mode with SSID
if iface_type == 'managed' and current_interface.get('ssid'):
current_interface['connected'] = True
if current_interface:
interfaces.append(current_interface)
# Get interface status and IP addresses
for iface_dict in interfaces:
# Check if interface is up
iface_dict['is_up'] = await self._is_interface_up(iface_dict['name'])
# Get IP address if interface is up
if iface_dict['is_up']:
iface_dict['ip_address'] = await self._get_interface_ip(iface_dict['name'])
# For AP mode, mark connected if we have IP (AP is "active")
if iface_dict['mode'] == 'AP' and iface_dict['ip_address']:
iface_dict['connected'] = True
# Convert dicts to WiFiInterface objects
self._interfaces = [WiFiInterface(**iface) for iface in interfaces]
# Check if dual mode is supported (requires at least 2 interfaces)
self._supports_dual = len(self._interfaces) >= 2
return self._interfaces
except Exception as e:
print(f"Error detecting WiFi interfaces: {e}")
return []
def _is_usb_interface(self, iface_name: str) -> bool:
"""Check if interface is USB-based.
Args:
iface_name: Interface name (e.g., wlan0)
Returns:
True if USB interface, False otherwise
"""
try:
# Check if device is USB by looking at sysfs
device_path = Path(f"/sys/class/net/{iface_name}/device")
if not device_path.exists():
return False
# Read uevent to check for USB
uevent_path = device_path / "uevent"
if uevent_path.exists():
content = uevent_path.read_text()
return "usb" in content.lower()
return False
except Exception:
return False
async def _is_interface_up(self, iface_name: str) -> bool:
"""Check if interface is up.
Args:
iface_name: Interface name
Returns:
True if interface is up
"""
try:
result = await self._run_command(f"ip link show {iface_name}")
return "UP" in result
except Exception:
return False
async def _get_interface_ip(self, iface_name: str) -> Optional[str]:
"""Get IP address of interface.
Args:
iface_name: Interface name
Returns:
IP address or None
"""
try:
result = await self._run_command(f"ip -4 addr show {iface_name}")
match = re.search(r'inet (\d+\.\d+\.\d+\.\d+)', result)
return match.group(1) if match else None
except Exception:
return None
async def get_status(self) -> WiFiStatus:
"""Get current WiFi status.
Returns:
WiFiStatus object with current state
"""
await self.detect_interfaces()
# Determine current mode
current_mode = await self._detect_current_mode()
# Find client and AP interfaces
client_ssid = None
client_ip = None
ap_ssid = None
ap_ip = None
for iface in self._interfaces:
# Check for AP mode interface
if iface.mode == 'AP':
ap_ip = iface.ip_address
ap_ssid = iface.ssid # SSID from iw dev output
# If no SSID from iw, try hostapd config
if not ap_ssid:
ap_ssid = await self._get_ap_ssid()
# Check for client mode interface that's connected
elif iface.mode == 'managed' and iface.connected and iface.ssid:
client_ssid = iface.ssid
client_ip = iface.ip_address
# Fallback: try to get AP SSID from hostapd if we detected AP mode
if current_mode == WiFiMode.AP and not ap_ssid:
ap_ssid = await self._get_ap_ssid()
return WiFiStatus(
mode=current_mode,
interfaces=self._interfaces,
current_ssid=client_ssid,
current_ip=client_ip,
ap_ssid=ap_ssid or "Dangerous-Pi", # Default
ap_ip=ap_ip,
supports_dual=self._supports_dual
)
async def _detect_current_mode(self) -> WiFiMode:
"""Detect current WiFi mode based on interface types.
Returns:
Current WiFi mode
"""
if not self._interfaces:
return WiFiMode.OFF
# Check interface modes from iw dev output
has_ap = any(iface.mode == 'AP' for iface in self._interfaces)
has_client = any(
iface.mode == 'managed' and iface.connected
for iface in self._interfaces
)
# Also check if hostapd is running as backup detection
if not has_ap:
has_ap = await self._is_hostapd_running()
if has_ap and has_client:
return WiFiMode.DUAL
elif has_ap:
return WiFiMode.AP
elif has_client:
return WiFiMode.CLIENT
elif any(iface.is_up for iface in self._interfaces):
return WiFiMode.AUTO # Interface up but not in AP or connected
else:
return WiFiMode.OFF
async def _is_hostapd_running(self) -> bool:
"""Check if hostapd service is running.
Returns:
True if hostapd is active
"""
try:
result = await self._run_command(
"systemctl is-active hostapd",
check=False
)
return result.strip() == "active"
except Exception:
return False
async def _get_ap_ssid(self) -> Optional[str]:
"""Get AP SSID from hostapd config.
Returns:
AP SSID or None
"""
try:
config_path = Path("/etc/hostapd/hostapd.conf")
if config_path.exists():
content = config_path.read_text()
match = re.search(r'ssid=(.+)', content)
return match.group(1) if match else None
except Exception:
return None
async def scan_networks(self, interface: Optional[str] = None) -> List[WiFiNetwork]:
"""Scan for available WiFi networks.
Args:
interface: Interface to scan with (default: first available)
Returns:
List of available networks
"""
# Ensure interfaces are detected
if not self._interfaces:
await self.detect_interfaces()
if not interface:
# Use first non-USB interface, or first available
for iface in self._interfaces:
if not iface.is_usb:
interface = iface.name
break
if not interface and self._interfaces:
interface = self._interfaces[0].name
if not interface:
return []
try:
# Request scan
# Note: Requires CAP_NET_ADMIN capability (set via systemd service)
await self._run_command(f"iw dev {interface} scan trigger", check=False)
# Wait for scan to complete
await asyncio.sleep(2)
# Get scan results
result = await self._run_command(f"iw dev {interface} scan")
networks = []
current_network = None
for line in result.split('\n'):
line = line.strip()
if line.startswith('BSS ') and line.split()[1].count(':') >= 5:
# Match BSS lines with MAC addresses (xx:xx:xx:xx:xx:xx), not "BSS Load:" etc.
if current_network:
networks.append(current_network)
# Handle format: BSS aa:bb:cc:dd:ee:ff(on wlan0)
bssid = line.split()[1].split('(')[0]
current_network = {
'ssid': '',
'bssid': bssid,
'signal_strength': 0,
'frequency': 0,
'encrypted': False,
'in_use': False
}
elif current_network:
if line.startswith('SSID: '):
current_network['ssid'] = line[6:]
elif line.startswith('freq: '):
# freq can be "2437" or "2437.0" depending on iw version
current_network['frequency'] = int(float(line[6:]))
elif line.startswith('signal: '):
# Parse signal strength (e.g., "-50.00 dBm")
signal = line[8:].split()[0]
current_network['signal_strength'] = int(float(signal))
elif 'RSN:' in line or 'WPA:' in line:
current_network['encrypted'] = True
if current_network:
networks.append(current_network)
# Convert to WiFiNetwork objects and filter out empty SSIDs
return [
WiFiNetwork(**net)
for net in networks
if net['ssid']
]
except Exception as e:
print(f"Error scanning networks: {e}")
return []
async def set_mode(self, mode: WiFiMode, persist: bool = True) -> bool:
"""Set WiFi mode.
Args:
mode: Desired WiFi mode
persist: If True, save mode to persistent storage for boot persistence
Returns:
True if mode was set successfully
"""
if mode == WiFiMode.DUAL and not self._supports_dual:
raise ValueError("Dual mode requires USB WiFi adapter")
try:
if mode == WiFiMode.AP:
await self._enable_ap_mode()
elif mode == WiFiMode.CLIENT:
await self._enable_client_mode()
elif mode == WiFiMode.DUAL:
await self._enable_dual_mode()
elif mode == WiFiMode.OFF:
await self._disable_wifi()
elif mode == WiFiMode.AUTO:
await self._enable_auto_mode()
self._current_mode = mode
# Persist mode for boot persistence
if persist:
self._save_mode(mode)
return True
except Exception as e:
print(f"Error setting WiFi mode: {e}")
return False
async def _systemctl(self, action: str, service: str):
"""Control systemd service via dbus (no sudo required with polkit rule).
Args:
action: "start", "stop", or "restart"
service: Service name (e.g., "hostapd.service")
"""
if not service.endswith(".service"):
service = f"{service}.service"
method = {
"start": "StartUnit",
"stop": "StopUnit",
"restart": "RestartUnit"
}.get(action, "StartUnit")
cmd = (
f'busctl call org.freedesktop.systemd1 '
f'/org/freedesktop/systemd1 org.freedesktop.systemd1.Manager '
f'{method} ss "{service}" "replace"'
)
await self._run_command(cmd, check=False)
async def _enable_nm_management(self):
"""Enable NetworkManager to manage wlan0 for client mode.
Uses nmcli to set wlan0 as managed (no file permissions needed).
"""
import logging
logger = logging.getLogger(__name__)
# Check current device status
status = await self._run_command("nmcli device status", check=False)
logger.warning(f"[NM Management] Current status:\n{status}")
# Use nmcli to set device as managed (works without root)
logger.warning("[NM Management] Setting wlan0 as managed...")
result = await self._run_command("nmcli device set wlan0 managed yes", check=False)
logger.warning(f"[NM Management] nmcli set managed result: '{result}'")
# Give NM time to pick up the device
logger.warning("[NM Management] Waiting 2s...")
await asyncio.sleep(2)
# Verify device is now managed
status = await self._run_command("nmcli device status", check=False)
logger.warning(f"[NM Management] Status after:\n{status}")
async def _disable_nm_management(self):
"""Disable NetworkManager management of wlan0 for AP mode.
Uses nmcli to set wlan0 as unmanaged (for hostapd).
"""
import logging
logger = logging.getLogger(__name__)
# Use nmcli to set device as unmanaged
logger.warning("[NM Management] Setting wlan0 as unmanaged...")
result = await self._run_command("nmcli device set wlan0 managed no", check=False)
logger.warning(f"[NM Management] nmcli set unmanaged result: '{result}'")
await asyncio.sleep(1)
async def _enable_ap_mode(self):
"""Enable Access Point mode."""
# Ensure NM doesn't manage wlan0
await self._disable_nm_management()
# Start hostapd and dnsmasq for AP
await self._systemctl("start", "hostapd")
await self._systemctl("start", "dnsmasq")
# Set static IP for AP
await self._run_command("ip addr add 192.168.4.1/24 dev wlan0", check=False)
print("AP mode enabled")
async def _enable_client_mode(self):
"""Enable Client mode using NetworkManager."""
import logging
logger = logging.getLogger(__name__)
# Stop AP services
logger.warning("[Client Mode] Stopping hostapd...")
await self._systemctl("stop", "hostapd")
logger.warning("[Client Mode] Stopping dnsmasq...")
await self._systemctl("stop", "dnsmasq")
# Check hostapd status
hostapd_status = await self._run_command("systemctl is-active hostapd", check=False)
logger.warning(f"[Client Mode] hostapd status after stop: {hostapd_status.strip()}")
# Enable NetworkManager to manage wlan0
logger.warning("[Client Mode] Enabling NM management...")
await self._enable_nm_management()
logger.warning("[Client Mode] Client mode enabled")
async def _enable_dual_mode(self):
"""Enable Dual mode (AP + Client)."""
# Enable both AP and client
await self._systemctl("start", "hostapd")
await self._systemctl("start", "dnsmasq")
await self._systemctl("start", "wpa_supplicant")
print("Dual mode enabled")
async def _disable_wifi(self):
"""Disable all WiFi."""
await self._systemctl("stop", "hostapd")
await self._systemctl("stop", "dnsmasq")
await self._systemctl("stop", "wpa_supplicant")
print("WiFi disabled")
async def _enable_auto_mode(self):
"""Enable Auto mode."""
# Auto-detect best mode based on saved networks and hardware
if self._supports_dual:
await self._enable_dual_mode()
else:
# Check if we have saved networks
saved_networks = await self.get_saved_networks()
if saved_networks:
await self._enable_client_mode()
else:
await self._enable_ap_mode()
print("Auto mode enabled")
async def connect_to_network(
self,
ssid: str,
password: Optional[str] = None,
interface: Optional[str] = None,
hidden: bool = False,
fallback_to_ap: bool = True
) -> bool:
"""Connect to a WiFi network using NetworkManager.
Args:
ssid: Network SSID
password: Network password (if encrypted)
interface: Interface to use (default: first available)
hidden: Whether network is hidden
fallback_to_ap: If True, revert to AP mode on connection failure
Returns:
True if connection successful
"""
# Ensure interfaces are detected before trying to connect
if not self._interfaces:
await self.detect_interfaces()
if not interface:
# Use first non-USB interface, or first available
for iface in self._interfaces:
if not iface.is_usb:
interface = iface.name
break
if not interface and self._interfaces:
interface = self._interfaces[0].name
if not interface:
import logging
logger = logging.getLogger(__name__)
logger.warning("[WiFi Connect] No interface found!")
return False
try:
import logging
logger = logging.getLogger(__name__)
logger.warning(f"[WiFi Connect] Starting connect to {ssid}")
# Switch to client mode (stops hostapd, enables NM management)
logger.warning("[WiFi Connect] Step 1: Enabling client mode...")
await self._enable_client_mode()
logger.warning("[WiFi Connect] Step 1: Client mode enabled")
# Wait for NM to be ready and detect wlan0
logger.warning("[WiFi Connect] Step 2: Waiting 2s for NM...")
await asyncio.sleep(2)
# Check device status
dev_status = await self._run_command("nmcli device status", check=False)
logger.warning(f"[WiFi Connect] Device status after wait:\n{dev_status}")
# Delete any existing saved connection for this SSID
# (handles corrupted connections with missing key-mgmt property)
ssid_escaped = ssid.replace("'", "'\\''")
logger.warning(f"[WiFi Connect] Step 3: Deleting existing connection for {ssid}...")
del_result = await self._run_command(
f"nmcli connection delete '{ssid_escaped}'",
check=False
)
logger.warning(f"[WiFi Connect] Delete result: {del_result}")
await asyncio.sleep(1)
# Build nmcli connect command
cmd = f"nmcli device wifi connect '{ssid_escaped}'"
if password:
password_escaped = password.replace("'", "'\\''")
cmd += f" password '{password_escaped}'"
if hidden:
cmd += " hidden yes"
# Attempt connection
logger.warning(f"[WiFi Connect] Step 4: Running nmcli connect...")
result = await self._run_command(cmd, check=False)
logger.warning(f"[WiFi Connect] Connect result: {result}")
# Check if connection was successful
if "successfully activated" in result.lower():
logger.warning(f"[WiFi Connect] SUCCESS - connected to {ssid}")
# Verify we have a non-AP IP address
await asyncio.sleep(2)
ip_result = await self._run_command(f"ip addr show {interface}")
logger.warning(f"[WiFi Connect] IP result: {ip_result}")
# Extract all IP addresses and check for non-AP IPs
# AP uses 192.168.4.x subnet
ip_matches = re.findall(r'inet (\d+\.\d+\.\d+\.\d+)', ip_result)
client_ips = [ip for ip in ip_matches if not ip.startswith("192.168.4.")]
if client_ips:
logger.warning(f"[WiFi Connect] Found client IP(s): {client_ips}")
# Remove the AP static IP since we're in client mode now
await self._run_command(f"ip addr del 192.168.4.1/24 dev {interface}", check=False)
# Save client mode for boot persistence
self._current_mode = WiFiMode.CLIENT
self._save_mode(WiFiMode.CLIENT)
logger.info(f"WiFi client mode saved for boot persistence")
return True
else:
logger.warning(f"[WiFi Connect] No client IP found, only AP IPs: {ip_matches}")
# Connection failed
logger.warning(f"[WiFi Connect] FAILED - {result}")
if fallback_to_ap:
print("Falling back to AP mode...")
await self._enable_ap_mode()
return False
except Exception as e:
print(f"Error connecting to network: {e}")
return False
async def _add_network_via_wpa_cli(self, ssid: str, password: Optional[str], hidden: bool):
"""Add network using wpa_cli commands."""
try:
# Add network
result = await self._run_command("sudo wpa_cli add_network")
network_id = result.strip().split('\n')[-1]
# Set SSID
await self._run_command(f'sudo wpa_cli set_network {network_id} ssid \\"{ssid}\\"')
# Set password or open network
if password:
await self._run_command(f'sudo wpa_cli set_network {network_id} psk \\"{password}\\"')
else:
await self._run_command(f'sudo wpa_cli set_network {network_id} key_mgmt NONE')
# Set scan_ssid for hidden networks
if hidden:
await self._run_command(f'sudo wpa_cli set_network {network_id} scan_ssid 1')
# Enable network
await self._run_command(f'sudo wpa_cli enable_network {network_id}')
# Save configuration
await self._run_command('sudo wpa_cli save_config')
return True
except Exception as e:
print(f"Error adding network via wpa_cli: {e}")
return False
async def disconnect_from_network(self, interface: Optional[str] = None) -> bool:
"""Disconnect from current network and return to AP mode.
Args:
interface: Interface to disconnect (default: first connected)
Returns:
True if disconnection successful
"""
if not interface:
for iface in self._interfaces:
if iface.connected:
interface = iface.name
break
if not interface:
return False
try:
# Disconnect using nmcli
await self._run_command(f"nmcli device disconnect {interface}", check=False)
# Return to AP mode
await self._enable_ap_mode()
return True
except Exception as e:
print(f"Error disconnecting: {e}")
return False
async def get_saved_networks(self) -> List[Dict[str, str]]:
"""Get list of saved WiFi networks from NetworkManager.
Returns:
List of saved networks with SSID and other info
"""
try:
result = await self._run_command(
"nmcli -t -f NAME,TYPE connection show",
check=False
)
networks = []
for line in result.split('\n'):
if line.strip():
parts = line.split(':')
if len(parts) >= 2 and parts[1] == '802-11-wireless':
networks.append({
'ssid': parts[0],
'type': 'wifi',
'enabled': True
})
return networks
except Exception as e:
print(f"Error getting saved networks: {e}")
return []
async def forget_network(self, ssid: str) -> bool:
"""Forget a saved network.
Args:
ssid: SSID of network to forget
Returns:
True if network was forgotten
"""
try:
# Delete connection using nmcli
ssid_escaped = ssid.replace("'", "'\\''")
result = await self._run_command(
f"nmcli connection delete '{ssid_escaped}'",
check=False
)
return "successfully deleted" in result.lower()
except Exception as e:
print(f"Error forgetting network: {e}")
return False
async def set_static_ip(
self,
interface: str,
ip_address: str,
netmask: str = "255.255.255.0",
gateway: Optional[str] = None,
dns: Optional[List[str]] = None
) -> bool:
"""Set static IP for interface.
Args:
interface: Interface name
ip_address: Static IP address
netmask: Network mask
gateway: Gateway IP (optional)
dns: DNS servers (optional)
Returns:
True if configuration successful
"""
try:
# Configure static IP using dhcpcd
config_path = Path("/etc/dhcpcd.conf")
# Read existing config
if config_path.exists():
content = config_path.read_text()
else:
content = ""
# Remove existing config for this interface
lines = content.split('\n')
new_lines = []
skip_interface = False
for line in lines:
if line.startswith(f'interface {interface}'):
skip_interface = True
continue
if skip_interface and (line.startswith('interface ') or line.strip() == ''):
skip_interface = False
if not skip_interface:
new_lines.append(line)
# Add new configuration
new_config = f"\ninterface {interface}\n"
new_config += f"static ip_address={ip_address}/{self._netmask_to_cidr(netmask)}\n"
if gateway:
new_config += f"static routers={gateway}\n"
if dns:
new_config += f"static domain_name_servers={' '.join(dns)}\n"
new_content = '\n'.join(new_lines) + new_config
# Write configuration (would need sudo)
# In production, this should use a proper mechanism
print(f"Would write to {config_path}:\n{new_config}")
# Restart interface
await self._run_command(f"sudo ip link set {interface} down")
await self._run_command(f"sudo ip link set {interface} up")
await self._run_command("sudo systemctl restart dhcpcd")
return True
except Exception as e:
print(f"Error setting static IP: {e}")
return False
def _netmask_to_cidr(self, netmask: str) -> int:
"""Convert netmask to CIDR notation.
Args:
netmask: Netmask (e.g., 255.255.255.0)
Returns:
CIDR prefix length (e.g., 24)
"""
return sum([bin(int(x)).count('1') for x in netmask.split('.')])
async def enable_dhcp(self, interface: str) -> bool:
"""Enable DHCP for interface.
Args:
interface: Interface name
Returns:
True if DHCP enabled
"""
try:
# Remove static IP configuration
config_path = Path("/etc/dhcpcd.conf")
if config_path.exists():
content = config_path.read_text()
lines = content.split('\n')
new_lines = []
skip_interface = False
for line in lines:
if line.startswith(f'interface {interface}'):
skip_interface = True
continue
if skip_interface and (line.startswith('interface ') or line.strip() == ''):
skip_interface = False
if not skip_interface:
new_lines.append(line)
# Write back
print(f"Would update {config_path} to enable DHCP")
# Restart interface
await self._run_command(f"sudo ip link set {interface} down")
await self._run_command(f"sudo ip link set {interface} up")
await self._run_command("sudo systemctl restart dhcpcd")
return True
except Exception as e:
print(f"Error enabling DHCP: {e}")
return False
async def _run_command(self, command: str, check: bool = True) -> str:
"""Run a shell command asynchronously.
Args:
command: Command to run
check: Raise exception on non-zero exit code
Returns:
Command output
Raises:
subprocess.CalledProcessError: If command fails and check=True
"""
loop = asyncio.get_event_loop()
def _execute():
result = subprocess.run(
command,
shell=True,
capture_output=True,
text=True,
check=check
)
return result.stdout
return await loop.run_in_executor(None, _execute)
# Global instance
wifi_manager = WiFiManager()