Additional Networking Changes to WireGuard

This commit is contained in:
2026-01-15 03:54:34 -07:00
parent 5c0952d95b
commit 18573c241f
6 changed files with 96 additions and 44 deletions

View File

@@ -1,5 +1,5 @@
# ======================================================
# Data\Agent\Roles\role_VpnShell.py
# Data\Agent\Roles\role_RemotePowershell.py
# Description: PowerShell TCP server for VPN shell access (Engine connects over WireGuard /32).
#
# API Endpoints (if applicable): None
@@ -19,19 +19,19 @@ from pathlib import Path
from typing import Any, Optional
import os
ROLE_NAME = "VpnShell"
ROLE_NAME = "RemotePowershell"
ROLE_CONTEXTS = ["system"]
FIREWALL_RULE_NAME = "Borealis - WireGuard - Shell"
FIREWALL_REMOTE_ADDRESS = "10.255.0.1/32"
def _log_path() -> Path:
# Keep shell logs alongside other VPN tunnel artifacts.
root = Path(__file__).resolve().parents[2] / "Logs" / "VPN_Tunnel"
root.mkdir(parents=True, exist_ok=True)
return root / "remote_shell.log"
def _write_log(message: str) -> None:
# Lightweight file logger for the shell bridge; avoid raising on failures.
ts = time.strftime("%Y-%m-%dT%H:%M:%S", time.localtime())
try:
_log_path().open("a", encoding="utf-8").write(f"[{ts}] [vpn-shell] {message}\n")
@@ -40,13 +40,16 @@ def _write_log(message: str) -> None:
def _b64encode(data: bytes) -> str:
# Wire payloads are JSON lines; encode binary stdout safely.
return base64.b64encode(data).decode("ascii").strip()
def _b64decode(value: str) -> bytes:
# Decode base64-encoded stdin payloads from the engine.
return base64.b64decode(value.encode("ascii"))
def _resolve_shell_port() -> int:
# Use the configured port when present, otherwise default to 47002.
raw = os.environ.get("BOREALIS_WIREGUARD_SHELL_PORT")
try:
value = int(raw) if raw is not None else 47002
@@ -57,30 +60,6 @@ def _resolve_shell_port() -> int:
return value
def _ensure_firewall_rule(port: int) -> None:
if os.name != "nt":
return
rule_name = FIREWALL_RULE_NAME.replace("'", "''")
command = (
"Remove-NetFirewallRule -DisplayName '{name}' -ErrorAction SilentlyContinue; "
"New-NetFirewallRule -DisplayName '{name}' -Direction Inbound -Action Allow "
"-Protocol TCP -LocalPort {port} -RemoteAddress {remote} -Profile Any"
).format(name=rule_name, port=port, remote=FIREWALL_REMOTE_ADDRESS)
try:
result = subprocess.run(
["powershell.exe", "-NoProfile", "-Command", command],
capture_output=True,
text=True,
check=False,
)
if result.returncode != 0:
_write_log(f"Failed to ensure firewall rule for VPN shell: {result.stderr.strip()}")
else:
_write_log(f"Ensured firewall rule for VPN shell on port {port}.")
except Exception as exc:
_write_log(f"Failed to ensure firewall rule for VPN shell: {exc}")
class ShellSession:
def __init__(self, conn: socket.socket, address: tuple[str, int]) -> None:
self.conn = conn
@@ -93,6 +72,7 @@ class ShellSession:
self.output_bytes = 0
def start(self) -> None:
# Spawn an interactive PowerShell process and bridge stdin/stdout.
_write_log(f"Shell session starting for {self.address[0]}:{self.address[1]}")
self.proc = subprocess.Popen(
["powershell.exe", "-NoLogo", "-NoProfile", "-NoExit", "-Command", "-"],
@@ -106,6 +86,7 @@ class ShellSession:
self._writer_loop()
def _reader_loop(self) -> None:
# Forward PowerShell stdout to the engine as JSONL payloads.
if not self.proc or not self.proc.stdout:
return
try:
@@ -126,6 +107,7 @@ class ShellSession:
_write_log(f"Shell stdout error: {exc}")
def _writer_loop(self) -> None:
# Read JSONL stdin from the engine and feed it into PowerShell.
buffer = b""
try:
while not self._stop.is_set():
@@ -165,6 +147,7 @@ class ShellSession:
self.close()
def close(self) -> None:
# Ensure the TCP connection and PowerShell child are cleaned up.
self._stop.set()
try:
self.conn.close()
@@ -189,12 +172,12 @@ class ShellServer:
def __init__(self, host: str = "0.0.0.0", port: Optional[int] = None) -> None:
self.host = host
self.port = port or _resolve_shell_port()
_ensure_firewall_rule(self.port)
self._thread = threading.Thread(target=self._serve, daemon=True)
self._thread.start()
_write_log(f"VPN shell server listening on {self.host}:{self.port}")
def _serve(self) -> None:
# Accept TCP shell connections; restrict to the WireGuard subnet.
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((self.host, self.port))
@@ -213,6 +196,7 @@ class ShellServer:
class Role:
def __init__(self, ctx) -> None:
# Start the shell server immediately when the role loads.
self.ctx = ctx
self.server = ShellServer()

View File

@@ -52,6 +52,7 @@ TUNNEL_NAME = "Borealis"
TUNNEL_DISPLAY_NAME = "Borealis"
SERVICE_DISPLAY_NAME = "Borealis - WireGuard - Agent"
TUNNEL_IDLE_ADDRESS = "169.254.255.254/32"
FIREWALL_RULE_NAME = "Borealis - WireGuard - Shell"
def _log_path() -> Path:
@@ -108,6 +109,17 @@ def _generate_client_keys(root: Path) -> Dict[str, str]:
return {"private": priv, "public": pub}
def _resolve_shell_port() -> int:
raw = os.environ.get("BOREALIS_WIREGUARD_SHELL_PORT")
try:
value = int(raw) if raw is not None else 47002
except Exception:
value = 47002
if value < 1 or value > 65535:
return 47002
return value
class SessionConfig:
def __init__(
self,
@@ -254,6 +266,63 @@ class WireGuardClient:
]
)
def _normalize_firewall_remote(self, allowed_ips: Optional[str]) -> Optional[str]:
if not allowed_ips:
return None
try:
network = ipaddress.ip_network(str(allowed_ips).strip(), strict=False)
except Exception:
_write_log(f"Refusing to apply shell firewall rule; invalid allowed_ips={allowed_ips}.")
return None
if network.prefixlen != 32:
_write_log(f"Refusing to apply shell firewall rule; allowed_ips not /32: {network}.")
return None
return str(network)
def _ensure_shell_firewall(self, allowed_ips: Optional[str]) -> None:
if os.name != "nt":
return
remote = self._normalize_firewall_remote(allowed_ips)
if not remote:
return
rule_name = FIREWALL_RULE_NAME.replace("'", "''")
port = _resolve_shell_port()
command = (
"Remove-NetFirewallRule -DisplayName '{name}' -ErrorAction SilentlyContinue; "
"New-NetFirewallRule -DisplayName '{name}' -Direction Inbound -Action Allow "
"-Protocol TCP -LocalPort {port} -RemoteAddress {remote} -Profile Any"
).format(name=rule_name, port=port, remote=remote)
try:
result = subprocess.run(
["powershell.exe", "-NoProfile", "-Command", command],
capture_output=True,
text=True,
check=False,
)
if result.returncode != 0:
_write_log(f"Failed to ensure shell firewall rule: {result.stderr.strip()}")
else:
_write_log(f"Ensured shell firewall rule for {remote} on port {port}.")
except Exception as exc:
_write_log(f"Failed to ensure shell firewall rule: {exc}")
def _remove_shell_firewall(self) -> None:
if os.name != "nt":
return
rule_name = FIREWALL_RULE_NAME.replace("'", "''")
command = "Remove-NetFirewallRule -DisplayName '{name}' -ErrorAction SilentlyContinue".format(
name=rule_name
)
try:
subprocess.run(
["powershell.exe", "-NoProfile", "-Command", command],
capture_output=True,
text=True,
check=False,
)
except Exception:
pass
def _service_exists(self) -> bool:
code, _, _ = self._run(["sc.exe", "query", self._service_id()])
if code == 0:
@@ -447,6 +516,7 @@ class WireGuardClient:
self._restart_service()
self._ensure_adapter_name()
self._ensure_service_display_name()
self._ensure_shell_firewall(session.allowed_ips)
self.session = session
self.idle_deadline = time.time() + max(60, session.idle_seconds)
@@ -455,6 +525,7 @@ class WireGuardClient:
def stop_session(self, reason: str = "stop", ignore_missing: bool = False) -> None:
with self._session_lock:
self._remove_shell_firewall()
if not self._service_exists():
if not ignore_missing:
_write_log("WireGuard tunnel service not found when stopping session.")

View File

@@ -80,7 +80,7 @@ API_LOG_FILE_PATH = LOG_ROOT / "api.log"
VPN_TUNNEL_LOG_FILE_PATH = LOG_ROOT / "VPN_Tunnel" / "tunnel.log"
DEFAULT_WIREGUARD_PORT = 30000
DEFAULT_WIREGUARD_ENGINE_VIRTUAL_IP = "10.255.0.1/32"
DEFAULT_WIREGUARD_PEER_NETWORK = "10.255.0.0/24"
DEFAULT_WIREGUARD_PEER_NETWORK = "10.255.0.0/16"
DEFAULT_WIREGUARD_SHELL_PORT = 47002
DEFAULT_WIREGUARD_ACL_WINDOWS = (3389, 5985, 5986, 5900, 3478, DEFAULT_WIREGUARD_SHELL_PORT)
VPN_SERVER_CERT_ROOT = PROJECT_ROOT / "Engine" / "Certificates" / "VPN_Server"