feat(cloak): wire cloak into the deploy path for windows* deckies

Base containers whose nmap_os has a mangle profile now build the cloak image
(FROM the per-decky distro), ship the light decnet subtree, and run
'python -m decnet.cloak' alongside holding the MACVLAN IP — netns-safe (cloak
backgrounded behind 'exec sleep infinity' so a cloak crash never tears down the
base/netns). composer injects build/command/NET_RAW/env (DECNET_NMAP_OS,
DECNET_OPEN_PORTS, DECKY_IP); deployer._sync_cloak_sources syncs the subtree;
non-windows deckies are unchanged. Mangler signal-guarded for thread use;
entry runs mangler in main thread, responder as daemon.

Verified live: real path makes nmap -O read 'Microsoft Windows Server 2012/2016'
with handshakes intact.
This commit is contained in:
2026-06-20 00:22:38 -04:00
parent f715ac6bcd
commit 402c1ef7a2
8 changed files with 258 additions and 13 deletions

View File

@@ -35,17 +35,15 @@ def main() -> int:
log.info("cloak: no mangle profile for %r — exiting", nmap_os)
return 0
threads = [
threading.Thread(target=mangler.run, args=(nmap_os,),
name="cloak-mangler", daemon=True),
threading.Thread(target=responder.run, args=(nmap_os, _open_ports()),
name="cloak-responder", daemon=True),
]
for t in threads:
t.start()
# Responder runs in a daemon thread; the mangler runs in the MAIN thread so
# its SIGTERM/SIGINT iptables-teardown handlers can be installed (signal only
# works in the main thread).
threading.Thread(
target=responder.run, args=(nmap_os, _open_ports()),
name="cloak-responder", daemon=True,
).start()
log.info("cloak: started for nmap_os=%r", nmap_os)
for t in threads:
t.join()
mangler.run(nmap_os)
return 0

View File

@@ -18,6 +18,7 @@ import os
import signal
import subprocess # nosec B404 — fixed-arg iptables, no shell
import sys
import threading
from typing import Any
from decnet.logging import get_logger
@@ -120,8 +121,12 @@ def run(nmap_os: str) -> int:
finally:
sys.exit(0)
signal.signal(signal.SIGTERM, _cleanup)
signal.signal(signal.SIGINT, _cleanup)
# signal.signal() only works in the main thread; the `finally` below still
# removes the rule on a normal exit, and on container stop the netns (and
# its iptables rules) are torn down regardless.
if threading.current_thread() is threading.main_thread():
signal.signal(signal.SIGTERM, _cleanup)
signal.signal(signal.SIGINT, _cleanup)
log.info("cloak.mangler: rewriting SYN-ACK -> %s (window=%#x ipid=%s)",
nmap_os, profile.window, profile.ipid)
try: