harden macOS networking port

This commit is contained in:
drjones
2026-05-23 22:09:43 -07:00
parent 1f5e63ca1f
commit 35899ba1d1
24 changed files with 581 additions and 742 deletions

View File

@@ -7,6 +7,7 @@ import random
import re
import string
import subprocess
import sys
import winreg
from dataclasses import dataclass, field
@@ -14,6 +15,9 @@ from .firewall import is_admin
from .mac_spoof import list_nics
from .win_compat import probe as _win_probe
if sys.platform == "darwin":
from .macos_privileged import run_shell, run_shell_as_admin, shell_quote
log = logging.getLogger(__name__)
_WEBRTC_CHROME = r"SOFTWARE\Policies\Google\Chrome"
@@ -26,6 +30,43 @@ _WEBRTC_VALUE_STR = "WebRtcIPHandling"
_WEBRTC_DISABLE_STR = "disable_non_proxied_udp"
def _mac_network_services() -> list[str]:
if sys.platform != "darwin":
return []
try:
r = subprocess.run(
["networksetup", "-listallnetworkservices"],
capture_output=True,
text=True,
timeout=10,
)
except Exception:
return []
services: list[str] = []
for line in (r.stdout or "").splitlines():
name = line.strip()
if not name or name.startswith("An asterisk"):
continue
services.append(name.lstrip("*").strip())
return services
def _mac_ipv6_state(service: str) -> str:
try:
r = subprocess.run(
["networksetup", "-getinfo", service],
capture_output=True,
text=True,
timeout=8,
)
for line in (r.stdout or "").splitlines():
if line.strip().startswith("IPv6:"):
return line.split(":", 1)[1].strip() or "Automatic"
except Exception:
pass
return "Automatic"
@dataclass
class FingerprintAudit:
lines: list[str] = field(default_factory=list)
@@ -51,6 +92,12 @@ def _run_ps(script: str, timeout: float = 15.0) -> str:
def get_computer_name() -> str:
if sys.platform == "darwin":
try:
r = subprocess.run(["scutil", "--get", "ComputerName"], capture_output=True, text=True, timeout=5)
return (r.stdout or "").strip() or platform.node() or ""
except Exception:
return platform.node() or ""
try:
import os
return os.environ.get("COMPUTERNAME", "") or platform.node() or ""
@@ -77,6 +124,19 @@ def random_hostname(prefix: str = "PC") -> str:
def set_computer_name(name: str) -> tuple[bool, str]:
"""Set NetBIOS / computer name (Admin). Reboot may be required for all apps."""
if sys.platform == "darwin":
clean = re.sub(r"[^A-Za-z0-9\-]", "", name)[:63]
local = clean[:63].strip("-") or random_hostname("MAC")
script = "\n".join([
f"scutil --set ComputerName {shell_quote(clean)}",
f"scutil --set HostName {shell_quote(clean)}",
f"scutil --set LocalHostName {shell_quote(local)}",
"dscacheutil -flushcache || true",
])
code, _, err = run_shell_as_admin(script)
if code == 0:
return True, f"macOS host identity set to {clean}."
return False, f"macOS hostname change failed: {err.strip() or 'permission denied'}"
if not is_admin():
return False, "Administrator required to change computer name."
name = re.sub(r"[^A-Za-z0-9\-]", "", name)[:15]
@@ -110,6 +170,24 @@ def set_computer_name(name: str) -> tuple[bool, str]:
def disable_ipv6_on_adapters() -> tuple[list[str], list[str]]:
"""Disable IPv6 binding on up physical adapters. Returns (adapter names, log lines)."""
if sys.platform == "darwin":
services = _mac_network_services()
if not services:
return [], ["IPv6 disable skipped: no macOS network services found."]
states: list[str] = []
commands = ["set -e"]
for service in services:
state = _mac_ipv6_state(service)
if state.lower() == "off":
continue
states.append(f"{service}|{state}")
commands.append(f"networksetup -setv6off {shell_quote(service)}")
if not states:
return [], ["IPv6 already off on all detected macOS network services."]
code, _, err = run_shell_as_admin("\n".join(commands))
if code != 0:
return [], [f"IPv6 disable failed: {err.strip() or 'permission denied'}"]
return states, [f"IPv6 disabled on {len(states)} macOS network service(s)."]
if not is_admin():
return [], ["IPv6 disable skipped (not Admin)."]
if not _win_probe().has_net_cmdlets:
@@ -144,6 +222,23 @@ def disable_ipv6_on_adapters() -> tuple[list[str], list[str]]:
def enable_ipv6_on_adapters(adapters: list[str]) -> list[str]:
if sys.platform == "darwin":
if not adapters:
return []
commands = ["set -e"]
for item in adapters:
service, _, state = item.partition("|")
state_l = (state or "Automatic").lower()
if "link-local" in state_l:
commands.append(f"networksetup -setv6linklocal {shell_quote(service)}")
elif "manual" in state_l:
commands.append(f"networksetup -setv6automatic {shell_quote(service)}")
else:
commands.append(f"networksetup -setv6automatic {shell_quote(service)}")
code, _, err = run_shell_as_admin("\n".join(commands))
if code != 0:
return [f"IPv6 restore failed: {err.strip() or 'permission denied'}"]
return [f"IPv6 restored on {len(adapters)} macOS network service(s)."]
if not is_admin() or not adapters:
return []
if not _win_probe().has_net_cmdlets:
@@ -171,6 +266,8 @@ def apply_webrtc_hardening(enable: bool) -> tuple[bool, str]:
the modern REG_SZ key (WebRtcIPHandling=disable_non_proxied_udp) so that
all Chrome/Edge versions are covered.
"""
if sys.platform == "darwin":
return False, "Chrome/Edge WebRTC policy is not applied on macOS; use the hardened Firefox profile controls."
if not is_admin():
return False, "Administrator required for browser WebRTC policy."
paths = [_WEBRTC_CHROME, _WEBRTC_EDGE]