"""Randomize and restore NIC MAC addresses (Admin required).""" from __future__ import annotations import logging import random import re import subprocess from dataclasses import dataclass from .firewall import is_admin log = logging.getLogger(__name__) _MAC_RE = re.compile(r"^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$") @dataclass class NicMac: name: str mac: str description: str = "" def _run(args: list[str], timeout: float = 20.0) -> tuple[int, str, str]: try: r = subprocess.run( args, capture_output=True, text=True, timeout=timeout, creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), ) return r.returncode, r.stdout or "", r.stderr or "" except Exception as e: return 1, "", str(e) def list_nics() -> list[NicMac]: """Physical/up adapters with current MAC.""" script = ( "Get-NetAdapter | Where-Object { $_.Status -ne 'Disabled' } | " "Select-Object Name, MacAddress, InterfaceDescription | " "ConvertTo-Json -Compress" ) code, out, _ = _run(["powershell", "-NoProfile", "-Command", script]) if code != 0 or not out.strip(): return [] import json try: data = json.loads(out) except json.JSONDecodeError: return [] rows = data if isinstance(data, list) else [data] nics: list[NicMac] = [] for row in rows: if not isinstance(row, dict): continue name = str(row.get("Name", "")).strip() mac = str(row.get("MacAddress", "")).strip().replace("-", ":") desc = str(row.get("InterfaceDescription", "")).strip() if name and mac and mac != "00:00:00:00:00:00": nics.append(NicMac(name=name, mac=mac, description=desc)) return nics def random_mac() -> str: """Locally administered unicast MAC.""" b = [random.randint(0, 255) for _ in range(6)] b[0] = (b[0] | 0x02) & 0xFE return ":".join(f"{x:02X}" for x in b) def set_mac(adapter: str, mac: str) -> tuple[bool, str]: if not is_admin(): return False, "Administrator required to change MAC." mac = mac.replace("-", ":").upper() if not _MAC_RE.match(mac.replace(":", "-")): return False, f"Invalid MAC: {mac}" ps = ( f'$a = Get-NetAdapter -Name "{adapter}" -ErrorAction Stop; ' f'Set-NetAdapter -Name $a.Name -MacAddress "{mac}" -Confirm:$false' ) code, _, err = _run(["powershell", "-NoProfile", "-Command", ps]) if code != 0: return False, err or "Set-NetAdapter failed" log.info("MAC set %s → %s", adapter, mac) return True, f"{adapter} → {mac}" def spoof_all_physical(snapshot: dict[str, str] | None = None) -> tuple[dict[str, str], list[str]]: """Randomize MAC on non-virtual adapters. Returns (original_map, log lines).""" originals: dict[str, str] = dict(snapshot or {}) logs: list[str] = [] skip_hints = ("virtual", "vmware", "hyper-v", "loopback", "bluetooth", "wan miniport") for nic in list_nics(): low = (nic.description + nic.name).lower() if any(h in low for h in skip_hints): continue if nic.name not in originals: originals[nic.name] = nic.mac new_mac = random_mac() ok, msg = set_mac(nic.name, new_mac) logs.append(msg if ok else f"{nic.name}: {msg}") return originals, logs def restore_macs(originals: dict[str, str]) -> list[str]: logs: list[str] = [] for name, mac in originals.items(): ok, msg = set_mac(name, mac) logs.append(msg if ok else f"{name}: {msg}") return logs