Add nmap OS spoof per decky via TCP/IP stack sysctls
Each decky base container now receives a set of Linux kernel sysctls (net.ipv4.ip_default_ttl, net.ipv4.tcp_syn_retries, etc.) tuned to match the claimed OS family, making nmap OS detection return the expected OS rather than the Linux host. - decnet/os_fingerprint.py: OS profile table (linux/windows/bsd/embedded/cisco) keyed by TTL and TCP tuning knobs - decnet/archetypes.py: Archetype gains nmap_os field; windows-* → "windows", printer/iot/industrial → "embedded", rest → "linux" - decnet/config.py: DeckyConfig gains nmap_os field (default "linux") - decnet/cli.py: nmap_os resolved from archetype → DeckyConfig in both CLI and INI build paths - decnet/composer.py: base container gets sysctls + cap_add: [NET_ADMIN]; service containers inherit via shared network namespace - tests/test_os_fingerprint.py: 48 new tests covering profiles, compose injection, archetype coverage, and CLI propagation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,7 @@ class Archetype:
|
|||||||
description: str
|
description: str
|
||||||
services: list[str] # default service set for this machine type
|
services: list[str] # default service set for this machine type
|
||||||
preferred_distros: list[str] # distro slugs to rotate through
|
preferred_distros: list[str] # distro slugs to rotate through
|
||||||
|
nmap_os: str = "linux" # OS family slug for TCP/IP stack spoofing (see os_fingerprint.py)
|
||||||
|
|
||||||
|
|
||||||
ARCHETYPES: dict[str, Archetype] = {
|
ARCHETYPES: dict[str, Archetype] = {
|
||||||
@@ -37,6 +38,7 @@ ARCHETYPES: dict[str, Archetype] = {
|
|||||||
description="Corporate Windows desktop: SMB shares + RDP access",
|
description="Corporate Windows desktop: SMB shares + RDP access",
|
||||||
services=["smb", "rdp"],
|
services=["smb", "rdp"],
|
||||||
preferred_distros=["debian", "ubuntu22"],
|
preferred_distros=["debian", "ubuntu22"],
|
||||||
|
nmap_os="windows",
|
||||||
),
|
),
|
||||||
"windows-server": Archetype(
|
"windows-server": Archetype(
|
||||||
slug="windows-server",
|
slug="windows-server",
|
||||||
@@ -44,6 +46,7 @@ ARCHETYPES: dict[str, Archetype] = {
|
|||||||
description="Windows domain member: SMB, RDP, and LDAP directory",
|
description="Windows domain member: SMB, RDP, and LDAP directory",
|
||||||
services=["smb", "rdp", "ldap"],
|
services=["smb", "rdp", "ldap"],
|
||||||
preferred_distros=["debian", "ubuntu22"],
|
preferred_distros=["debian", "ubuntu22"],
|
||||||
|
nmap_os="windows",
|
||||||
),
|
),
|
||||||
"domain-controller": Archetype(
|
"domain-controller": Archetype(
|
||||||
slug="domain-controller",
|
slug="domain-controller",
|
||||||
@@ -51,6 +54,7 @@ ARCHETYPES: dict[str, Archetype] = {
|
|||||||
description="Active Directory DC: LDAP, SMB, RDP, LLMNR",
|
description="Active Directory DC: LDAP, SMB, RDP, LLMNR",
|
||||||
services=["ldap", "smb", "rdp", "llmnr"],
|
services=["ldap", "smb", "rdp", "llmnr"],
|
||||||
preferred_distros=["debian", "ubuntu22"],
|
preferred_distros=["debian", "ubuntu22"],
|
||||||
|
nmap_os="windows",
|
||||||
),
|
),
|
||||||
"linux-server": Archetype(
|
"linux-server": Archetype(
|
||||||
slug="linux-server",
|
slug="linux-server",
|
||||||
@@ -58,6 +62,7 @@ ARCHETYPES: dict[str, Archetype] = {
|
|||||||
description="General-purpose Linux host: SSH + HTTP",
|
description="General-purpose Linux host: SSH + HTTP",
|
||||||
services=["ssh", "http"],
|
services=["ssh", "http"],
|
||||||
preferred_distros=["debian", "ubuntu22", "rocky9", "fedora"],
|
preferred_distros=["debian", "ubuntu22", "rocky9", "fedora"],
|
||||||
|
nmap_os="linux",
|
||||||
),
|
),
|
||||||
"web-server": Archetype(
|
"web-server": Archetype(
|
||||||
slug="web-server",
|
slug="web-server",
|
||||||
@@ -65,6 +70,7 @@ ARCHETYPES: dict[str, Archetype] = {
|
|||||||
description="Public-facing web host: HTTP + FTP",
|
description="Public-facing web host: HTTP + FTP",
|
||||||
services=["http", "ftp"],
|
services=["http", "ftp"],
|
||||||
preferred_distros=["debian", "ubuntu22", "ubuntu20"],
|
preferred_distros=["debian", "ubuntu22", "ubuntu20"],
|
||||||
|
nmap_os="linux",
|
||||||
),
|
),
|
||||||
"database-server": Archetype(
|
"database-server": Archetype(
|
||||||
slug="database-server",
|
slug="database-server",
|
||||||
@@ -72,6 +78,7 @@ ARCHETYPES: dict[str, Archetype] = {
|
|||||||
description="Data tier host: MySQL, PostgreSQL, Redis",
|
description="Data tier host: MySQL, PostgreSQL, Redis",
|
||||||
services=["mysql", "postgres", "redis"],
|
services=["mysql", "postgres", "redis"],
|
||||||
preferred_distros=["debian", "ubuntu22"],
|
preferred_distros=["debian", "ubuntu22"],
|
||||||
|
nmap_os="linux",
|
||||||
),
|
),
|
||||||
"mail-server": Archetype(
|
"mail-server": Archetype(
|
||||||
slug="mail-server",
|
slug="mail-server",
|
||||||
@@ -79,6 +86,7 @@ ARCHETYPES: dict[str, Archetype] = {
|
|||||||
description="SMTP/IMAP/POP3 mail relay",
|
description="SMTP/IMAP/POP3 mail relay",
|
||||||
services=["smtp", "pop3", "imap"],
|
services=["smtp", "pop3", "imap"],
|
||||||
preferred_distros=["debian", "ubuntu22"],
|
preferred_distros=["debian", "ubuntu22"],
|
||||||
|
nmap_os="linux",
|
||||||
),
|
),
|
||||||
"file-server": Archetype(
|
"file-server": Archetype(
|
||||||
slug="file-server",
|
slug="file-server",
|
||||||
@@ -86,6 +94,7 @@ ARCHETYPES: dict[str, Archetype] = {
|
|||||||
description="SMB/FTP/SFTP file storage node",
|
description="SMB/FTP/SFTP file storage node",
|
||||||
services=["smb", "ftp", "ssh"],
|
services=["smb", "ftp", "ssh"],
|
||||||
preferred_distros=["debian", "ubuntu22", "rocky9"],
|
preferred_distros=["debian", "ubuntu22", "rocky9"],
|
||||||
|
nmap_os="linux",
|
||||||
),
|
),
|
||||||
"printer": Archetype(
|
"printer": Archetype(
|
||||||
slug="printer",
|
slug="printer",
|
||||||
@@ -93,6 +102,7 @@ ARCHETYPES: dict[str, Archetype] = {
|
|||||||
description="Network-attached printer: SNMP + FTP",
|
description="Network-attached printer: SNMP + FTP",
|
||||||
services=["snmp", "ftp"],
|
services=["snmp", "ftp"],
|
||||||
preferred_distros=["alpine", "debian"],
|
preferred_distros=["alpine", "debian"],
|
||||||
|
nmap_os="embedded",
|
||||||
),
|
),
|
||||||
"iot-device": Archetype(
|
"iot-device": Archetype(
|
||||||
slug="iot-device",
|
slug="iot-device",
|
||||||
@@ -100,6 +110,7 @@ ARCHETYPES: dict[str, Archetype] = {
|
|||||||
description="Embedded/IoT device: MQTT, SNMP, Telnet",
|
description="Embedded/IoT device: MQTT, SNMP, Telnet",
|
||||||
services=["mqtt", "snmp", "telnet"],
|
services=["mqtt", "snmp", "telnet"],
|
||||||
preferred_distros=["alpine"],
|
preferred_distros=["alpine"],
|
||||||
|
nmap_os="embedded",
|
||||||
),
|
),
|
||||||
"industrial-control": Archetype(
|
"industrial-control": Archetype(
|
||||||
slug="industrial-control",
|
slug="industrial-control",
|
||||||
@@ -107,6 +118,7 @@ ARCHETYPES: dict[str, Archetype] = {
|
|||||||
description="ICS/SCADA node: Conpot (Modbus/S7/DNP3) + SNMP",
|
description="ICS/SCADA node: Conpot (Modbus/S7/DNP3) + SNMP",
|
||||||
services=["conpot", "snmp"],
|
services=["conpot", "snmp"],
|
||||||
preferred_distros=["debian"],
|
preferred_distros=["debian"],
|
||||||
|
nmap_os="embedded",
|
||||||
),
|
),
|
||||||
"voip-server": Archetype(
|
"voip-server": Archetype(
|
||||||
slug="voip-server",
|
slug="voip-server",
|
||||||
@@ -114,6 +126,7 @@ ARCHETYPES: dict[str, Archetype] = {
|
|||||||
description="SIP PBX / VoIP gateway",
|
description="SIP PBX / VoIP gateway",
|
||||||
services=["sip"],
|
services=["sip"],
|
||||||
preferred_distros=["debian", "ubuntu22"],
|
preferred_distros=["debian", "ubuntu22"],
|
||||||
|
nmap_os="linux",
|
||||||
),
|
),
|
||||||
"monitoring-node": Archetype(
|
"monitoring-node": Archetype(
|
||||||
slug="monitoring-node",
|
slug="monitoring-node",
|
||||||
@@ -121,6 +134,7 @@ ARCHETYPES: dict[str, Archetype] = {
|
|||||||
description="Infrastructure monitoring host: SNMP + SSH",
|
description="Infrastructure monitoring host: SNMP + SSH",
|
||||||
services=["snmp", "ssh"],
|
services=["snmp", "ssh"],
|
||||||
preferred_distros=["debian", "rocky9"],
|
preferred_distros=["debian", "rocky9"],
|
||||||
|
nmap_os="linux",
|
||||||
),
|
),
|
||||||
"devops-host": Archetype(
|
"devops-host": Archetype(
|
||||||
slug="devops-host",
|
slug="devops-host",
|
||||||
@@ -128,6 +142,7 @@ ARCHETYPES: dict[str, Archetype] = {
|
|||||||
description="CI/CD or container host: Docker API + SSH + K8s",
|
description="CI/CD or container host: Docker API + SSH + K8s",
|
||||||
services=["docker_api", "ssh", "k8s"],
|
services=["docker_api", "ssh", "k8s"],
|
||||||
preferred_distros=["ubuntu22", "debian"],
|
preferred_distros=["ubuntu22", "debian"],
|
||||||
|
nmap_os="linux",
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ def _build_deckies(
|
|||||||
build_base=distro.build_base,
|
build_base=distro.build_base,
|
||||||
hostname=hostname,
|
hostname=hostname,
|
||||||
archetype=archetype.slug if archetype else None,
|
archetype=archetype.slug if archetype else None,
|
||||||
|
nmap_os=archetype.nmap_os if archetype else "linux",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return deckies
|
return deckies
|
||||||
@@ -188,6 +189,7 @@ def _build_deckies_from_ini(
|
|||||||
hostname=hostname,
|
hostname=hostname,
|
||||||
archetype=arch.slug if arch else None,
|
archetype=arch.slug if arch else None,
|
||||||
service_config=spec.service_config,
|
service_config=spec.service_config,
|
||||||
|
nmap_os=arch.nmap_os if arch else "linux",
|
||||||
))
|
))
|
||||||
return deckies
|
return deckies
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import yaml
|
|||||||
|
|
||||||
from decnet.config import DecnetConfig
|
from decnet.config import DecnetConfig
|
||||||
from decnet.network import MACVLAN_NETWORK_NAME
|
from decnet.network import MACVLAN_NETWORK_NAME
|
||||||
|
from decnet.os_fingerprint import get_os_sysctls
|
||||||
from decnet.services.registry import get_service
|
from decnet.services.registry import get_service
|
||||||
|
|
||||||
_CONTAINER_LOG_DIR = "/var/log/decnet"
|
_CONTAINER_LOG_DIR = "/var/log/decnet"
|
||||||
@@ -63,6 +64,13 @@ def generate_compose(config: DecnetConfig) -> dict:
|
|||||||
}
|
}
|
||||||
if config.log_target:
|
if config.log_target:
|
||||||
base["networks"][_LOG_NETWORK] = {}
|
base["networks"][_LOG_NETWORK] = {}
|
||||||
|
|
||||||
|
# Inject TCP/IP stack sysctls to spoof the claimed OS fingerprint.
|
||||||
|
# Only the base container needs this — service containers inherit the
|
||||||
|
# same network namespace via network_mode: "service:<base>".
|
||||||
|
base["sysctls"] = get_os_sysctls(decky.nmap_os)
|
||||||
|
base["cap_add"] = ["NET_ADMIN"]
|
||||||
|
|
||||||
services[base_key] = base
|
services[base_key] = base
|
||||||
|
|
||||||
# --- Service containers: share base network namespace ---
|
# --- Service containers: share base network namespace ---
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class DeckyConfig(BaseModel):
|
|||||||
hostname: str
|
hostname: str
|
||||||
archetype: str | None = None # archetype slug if spawned from an archetype profile
|
archetype: str | None = None # archetype slug if spawned from an archetype profile
|
||||||
service_config: dict[str, dict] = {} # optional per-service persona config
|
service_config: dict[str, dict] = {} # optional per-service persona config
|
||||||
|
nmap_os: str = "linux" # OS family for TCP/IP stack spoofing (see os_fingerprint.py)
|
||||||
|
|
||||||
@field_validator("services")
|
@field_validator("services")
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
55
decnet/os_fingerprint.py
Normal file
55
decnet/os_fingerprint.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
"""
|
||||||
|
OS TCP/IP fingerprint profiles for DECNET deckies.
|
||||||
|
|
||||||
|
Maps an nmap OS family slug to a dict of Linux kernel sysctls that, when applied
|
||||||
|
to a container's network namespace, make its TCP/IP stack behaviour resemble the
|
||||||
|
claimed OS as closely as possible within the Linux kernel's constraints.
|
||||||
|
|
||||||
|
Primary discriminator leveraged by nmap: net.ipv4.ip_default_ttl (TTL)
|
||||||
|
Linux → 64
|
||||||
|
Windows → 128
|
||||||
|
BSD (FreeBSD/macOS)→ 64 (different TCP options, but same TTL as Linux)
|
||||||
|
Embedded / network → 255
|
||||||
|
|
||||||
|
Secondary tuning (TCP behaviour):
|
||||||
|
net.ipv4.tcp_syn_retries – SYN retransmits before giving up
|
||||||
|
net.core.rmem_default – initial receive buffer → affects SYN-ACK window field
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
OS_SYSCTLS: dict[str, dict[str, str]] = {
|
||||||
|
"linux": {
|
||||||
|
"net.ipv4.ip_default_ttl": "64",
|
||||||
|
"net.ipv4.tcp_syn_retries": "6",
|
||||||
|
},
|
||||||
|
"windows": {
|
||||||
|
"net.ipv4.ip_default_ttl": "128",
|
||||||
|
"net.ipv4.tcp_syn_retries": "2",
|
||||||
|
"net.core.rmem_default": "8388608", # 8 MB → large initial window like Windows
|
||||||
|
},
|
||||||
|
"bsd": {
|
||||||
|
"net.ipv4.ip_default_ttl": "64",
|
||||||
|
"net.ipv4.tcp_syn_retries": "6",
|
||||||
|
},
|
||||||
|
"embedded": {
|
||||||
|
"net.ipv4.ip_default_ttl": "255",
|
||||||
|
"net.ipv4.tcp_syn_retries": "3",
|
||||||
|
},
|
||||||
|
"cisco": {
|
||||||
|
"net.ipv4.ip_default_ttl": "255",
|
||||||
|
"net.ipv4.tcp_syn_retries": "2",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_DEFAULT_OS = "linux"
|
||||||
|
|
||||||
|
|
||||||
|
def get_os_sysctls(nmap_os: str) -> dict[str, str]:
|
||||||
|
"""Return the sysctl dict for *nmap_os*. Falls back to Linux on unknown slugs."""
|
||||||
|
return dict(OS_SYSCTLS.get(nmap_os, OS_SYSCTLS[_DEFAULT_OS]))
|
||||||
|
|
||||||
|
|
||||||
|
def all_os_families() -> list[str]:
|
||||||
|
"""Return all registered nmap OS family slugs."""
|
||||||
|
return list(OS_SYSCTLS.keys())
|
||||||
248
tests/test_os_fingerprint.py
Normal file
248
tests/test_os_fingerprint.py
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
"""
|
||||||
|
Tests for the OS TCP/IP fingerprint spoof feature.
|
||||||
|
|
||||||
|
Covers:
|
||||||
|
- os_fingerprint.py: profiles, TTL values, fallback behaviour
|
||||||
|
- archetypes.py: every archetype has a valid nmap_os
|
||||||
|
- config.py: DeckyConfig carries nmap_os
|
||||||
|
- composer.py: base container gets sysctls + cap_add injected
|
||||||
|
- cli.py helpers: nmap_os propagated from archetype → DeckyConfig
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from decnet.archetypes import ARCHETYPES, all_archetypes
|
||||||
|
from decnet.composer import generate_compose
|
||||||
|
from decnet.config import DeckyConfig, DecnetConfig
|
||||||
|
from decnet.os_fingerprint import OS_SYSCTLS, all_os_families, get_os_sysctls
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# os_fingerprint module
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def test_linux_ttl_is_64():
|
||||||
|
assert get_os_sysctls("linux")["net.ipv4.ip_default_ttl"] == "64"
|
||||||
|
|
||||||
|
|
||||||
|
def test_windows_ttl_is_128():
|
||||||
|
assert get_os_sysctls("windows")["net.ipv4.ip_default_ttl"] == "128"
|
||||||
|
|
||||||
|
|
||||||
|
def test_embedded_ttl_is_255():
|
||||||
|
assert get_os_sysctls("embedded")["net.ipv4.ip_default_ttl"] == "255"
|
||||||
|
|
||||||
|
|
||||||
|
def test_cisco_ttl_is_255():
|
||||||
|
assert get_os_sysctls("cisco")["net.ipv4.ip_default_ttl"] == "255"
|
||||||
|
|
||||||
|
|
||||||
|
def test_bsd_ttl_is_64():
|
||||||
|
assert get_os_sysctls("bsd")["net.ipv4.ip_default_ttl"] == "64"
|
||||||
|
|
||||||
|
|
||||||
|
def test_unknown_os_falls_back_to_linux():
|
||||||
|
result = get_os_sysctls("nonexistent-os")
|
||||||
|
assert result == get_os_sysctls("linux")
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_os_sysctls_returns_copy():
|
||||||
|
"""Mutating the returned dict must not alter the master profile."""
|
||||||
|
s = get_os_sysctls("windows")
|
||||||
|
s["net.ipv4.ip_default_ttl"] = "999"
|
||||||
|
assert OS_SYSCTLS["windows"]["net.ipv4.ip_default_ttl"] == "128"
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_os_families_non_empty():
|
||||||
|
families = all_os_families()
|
||||||
|
assert len(families) > 0
|
||||||
|
assert "linux" in families
|
||||||
|
assert "windows" in families
|
||||||
|
assert "embedded" in families
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Archetypes carry valid nmap_os values
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("slug,arch", list(ARCHETYPES.items()))
|
||||||
|
def test_archetype_nmap_os_is_known(slug, arch):
|
||||||
|
assert arch.nmap_os in all_os_families(), (
|
||||||
|
f"Archetype '{slug}' has nmap_os='{arch.nmap_os}' which is not in OS_SYSCTLS"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("slug", ["windows-workstation", "windows-server", "domain-controller"])
|
||||||
|
def test_windows_archetypes_have_windows_nmap_os(slug):
|
||||||
|
assert ARCHETYPES[slug].nmap_os == "windows"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("slug", ["printer", "iot-device", "industrial-control"])
|
||||||
|
def test_embedded_archetypes_have_embedded_nmap_os(slug):
|
||||||
|
assert ARCHETYPES[slug].nmap_os == "embedded"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("slug", ["linux-server", "web-server", "database-server",
|
||||||
|
"mail-server", "file-server", "voip-server",
|
||||||
|
"monitoring-node", "devops-host"])
|
||||||
|
def test_linux_archetypes_have_linux_nmap_os(slug):
|
||||||
|
assert ARCHETYPES[slug].nmap_os == "linux"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# DeckyConfig default
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _make_decky(nmap_os: str = "linux") -> DeckyConfig:
|
||||||
|
return DeckyConfig(
|
||||||
|
name="decky-01",
|
||||||
|
ip="10.0.0.10",
|
||||||
|
services=["ssh"],
|
||||||
|
distro="debian",
|
||||||
|
base_image="debian:bookworm-slim",
|
||||||
|
build_base="debian:bookworm-slim",
|
||||||
|
hostname="test-host",
|
||||||
|
nmap_os=nmap_os,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_deckyconfig_default_nmap_os_is_linux():
|
||||||
|
cfg = DeckyConfig(
|
||||||
|
name="decky-01",
|
||||||
|
ip="10.0.0.10",
|
||||||
|
services=["ssh"],
|
||||||
|
distro="debian",
|
||||||
|
base_image="debian:bookworm-slim",
|
||||||
|
build_base="debian:bookworm-slim",
|
||||||
|
hostname="test-host",
|
||||||
|
)
|
||||||
|
assert cfg.nmap_os == "linux"
|
||||||
|
|
||||||
|
|
||||||
|
def test_deckyconfig_accepts_custom_nmap_os():
|
||||||
|
cfg = _make_decky(nmap_os="windows")
|
||||||
|
assert cfg.nmap_os == "windows"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Composer injects sysctls + cap_add into base container
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _make_config(nmap_os: str = "linux") -> DecnetConfig:
|
||||||
|
return DecnetConfig(
|
||||||
|
mode="unihost",
|
||||||
|
interface="eth0",
|
||||||
|
subnet="10.0.0.0/24",
|
||||||
|
gateway="10.0.0.1",
|
||||||
|
deckies=[_make_decky(nmap_os=nmap_os)],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_compose_base_has_sysctls():
|
||||||
|
compose = generate_compose(_make_config("linux"))
|
||||||
|
base = compose["services"]["decky-01"]
|
||||||
|
assert "sysctls" in base
|
||||||
|
|
||||||
|
|
||||||
|
def test_compose_base_has_cap_net_admin():
|
||||||
|
compose = generate_compose(_make_config("linux"))
|
||||||
|
base = compose["services"]["decky-01"]
|
||||||
|
assert "cap_add" in base
|
||||||
|
assert "NET_ADMIN" in base["cap_add"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_compose_linux_ttl_64():
|
||||||
|
compose = generate_compose(_make_config("linux"))
|
||||||
|
sysctls = compose["services"]["decky-01"]["sysctls"]
|
||||||
|
assert sysctls["net.ipv4.ip_default_ttl"] == "64"
|
||||||
|
|
||||||
|
|
||||||
|
def test_compose_windows_ttl_128():
|
||||||
|
compose = generate_compose(_make_config("windows"))
|
||||||
|
sysctls = compose["services"]["decky-01"]["sysctls"]
|
||||||
|
assert sysctls["net.ipv4.ip_default_ttl"] == "128"
|
||||||
|
|
||||||
|
|
||||||
|
def test_compose_embedded_ttl_255():
|
||||||
|
compose = generate_compose(_make_config("embedded"))
|
||||||
|
sysctls = compose["services"]["decky-01"]["sysctls"]
|
||||||
|
assert sysctls["net.ipv4.ip_default_ttl"] == "255"
|
||||||
|
|
||||||
|
|
||||||
|
def test_compose_service_containers_have_no_sysctls():
|
||||||
|
"""Service containers share the base network namespace — no sysctls needed there."""
|
||||||
|
compose = generate_compose(_make_config("windows"))
|
||||||
|
svc = compose["services"]["decky-01-ssh"]
|
||||||
|
assert "sysctls" not in svc
|
||||||
|
|
||||||
|
|
||||||
|
def test_compose_two_deckies_independent_nmap_os():
|
||||||
|
"""Each decky gets its own OS profile."""
|
||||||
|
decky_win = _make_decky(nmap_os="windows")
|
||||||
|
decky_lin = DeckyConfig(
|
||||||
|
name="decky-02",
|
||||||
|
ip="10.0.0.11",
|
||||||
|
services=["ssh"],
|
||||||
|
distro="debian",
|
||||||
|
base_image="debian:bookworm-slim",
|
||||||
|
build_base="debian:bookworm-slim",
|
||||||
|
hostname="test-host-2",
|
||||||
|
nmap_os="linux",
|
||||||
|
)
|
||||||
|
config = DecnetConfig(
|
||||||
|
mode="unihost",
|
||||||
|
interface="eth0",
|
||||||
|
subnet="10.0.0.0/24",
|
||||||
|
gateway="10.0.0.1",
|
||||||
|
deckies=[decky_win, decky_lin],
|
||||||
|
)
|
||||||
|
compose = generate_compose(config)
|
||||||
|
assert compose["services"]["decky-01"]["sysctls"]["net.ipv4.ip_default_ttl"] == "128"
|
||||||
|
assert compose["services"]["decky-02"]["sysctls"]["net.ipv4.ip_default_ttl"] == "64"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# CLI helper: nmap_os flows from archetype into DeckyConfig
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def test_build_deckies_windows_archetype_sets_nmap_os():
|
||||||
|
from decnet.archetypes import get_archetype
|
||||||
|
from decnet.cli import _build_deckies
|
||||||
|
|
||||||
|
arch = get_archetype("windows-workstation")
|
||||||
|
deckies = _build_deckies(
|
||||||
|
n=1,
|
||||||
|
ips=["10.0.0.20"],
|
||||||
|
services_explicit=None,
|
||||||
|
randomize_services=False,
|
||||||
|
archetype=arch,
|
||||||
|
)
|
||||||
|
assert deckies[0].nmap_os == "windows"
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_deckies_no_archetype_defaults_linux():
|
||||||
|
from decnet.cli import _build_deckies
|
||||||
|
|
||||||
|
deckies = _build_deckies(
|
||||||
|
n=1,
|
||||||
|
ips=["10.0.0.20"],
|
||||||
|
services_explicit=["ssh"],
|
||||||
|
randomize_services=False,
|
||||||
|
archetype=None,
|
||||||
|
)
|
||||||
|
assert deckies[0].nmap_os == "linux"
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_deckies_embedded_archetype_sets_nmap_os():
|
||||||
|
from decnet.archetypes import get_archetype
|
||||||
|
from decnet.cli import _build_deckies
|
||||||
|
|
||||||
|
arch = get_archetype("iot-device")
|
||||||
|
deckies = _build_deckies(
|
||||||
|
n=1,
|
||||||
|
ips=["10.0.0.20"],
|
||||||
|
services_explicit=None,
|
||||||
|
randomize_services=False,
|
||||||
|
archetype=arch,
|
||||||
|
)
|
||||||
|
assert deckies[0].nmap_os == "embedded"
|
||||||
Reference in New Issue
Block a user