From a6063efbb9f94b9387ee28139efa2af023dc7278 Mon Sep 17 00:00:00 2001 From: anti Date: Sat, 11 Apr 2026 19:36:46 -0400 Subject: [PATCH] fix(collector): daemonize background subprocesses with start_new_session MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Collector and mutator watcher subprocesses were spawned without start_new_session=True, leaving them in the parent's process group. SIGHUP (sent when the controlling terminal closes) killed both processes silently — stdout/stderr were DEVNULL so the crash was invisible. Also update test_services and test_composer to reflect the ssh plugin no longer using Cowrie env vars (replaced with SSH_ROOT_PASSWORD / SSH_HOSTNAME matching the real_ssh plugin). --- decnet/cli.py | 4 +++- tests/test_composer.py | 9 ++++---- tests/test_services.py | 50 +++++++++++++++++++----------------------- 3 files changed, 31 insertions(+), 32 deletions(-) diff --git a/decnet/cli.py b/decnet/cli.py index c210694..1660c2f 100644 --- a/decnet/cli.py +++ b/decnet/cli.py @@ -390,7 +390,8 @@ def deploy( subprocess.Popen( # nosec B603 [sys.executable, "-m", "decnet.cli", "mutate", "--watch"], stdout=subprocess.DEVNULL, - stderr=subprocess.STDOUT + stderr=subprocess.STDOUT, + start_new_session=True, ) except (FileNotFoundError, subprocess.SubprocessError): console.print("[red]Failed to start mutator watcher.[/]") @@ -405,6 +406,7 @@ def deploy( [sys.executable, "-m", "decnet.cli", "collect", "--log-file", str(effective_log_file)], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, + start_new_session=True, ) if api and not dry_run: diff --git a/tests/test_composer.py b/tests/test_composer.py index 68be330..5e45e17 100644 --- a/tests/test_composer.py +++ b/tests/test_composer.py @@ -121,8 +121,8 @@ def test_service_config_http_server_header(): assert env.get("SERVER_HEADER") == "nginx/1.18.0" -def test_service_config_ssh_kernel_version(): - """service_config for ssh must inject COWRIE_HONEYPOT_KERNEL_VERSION.""" +def test_service_config_ssh_password(): + """service_config for ssh must inject SSH_ROOT_PASSWORD.""" from decnet.config import DeckyConfig, DecnetConfig from decnet.distros import DISTROS profile = DISTROS["debian"] @@ -131,7 +131,7 @@ def test_service_config_ssh_kernel_version(): services=["ssh"], distro="debian", base_image=profile.image, build_base=profile.build_base, hostname="test-host", - service_config={"ssh": {"kernel_version": "5.15.0-76-generic"}}, + service_config={"ssh": {"password": "s3cr3t!"}}, ) config = DecnetConfig( mode="unihost", interface="eth0", @@ -140,7 +140,8 @@ def test_service_config_ssh_kernel_version(): ) compose = generate_compose(config) env = compose["services"]["decky-01-ssh"]["environment"] - assert env.get("COWRIE_HONEYPOT_KERNEL_VERSION") == "5.15.0-76-generic" + assert env.get("SSH_ROOT_PASSWORD") == "s3cr3t!" + assert not any(k.startswith("COWRIE_") for k in env) def test_service_config_for_one_service_does_not_affect_another(): diff --git a/tests/test_services.py b/tests/test_services.py index 14a9a95..a258895 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -40,7 +40,7 @@ UPSTREAM_SERVICES = { # --------------------------------------------------------------------------- BUILD_SERVICES = { - "ssh": ([22, 2222], "ssh"), + "ssh": ([22], "ssh"), "http": ([80, 443], "http"), "rdp": ([3389], "rdp"), "smb": ([445, 139], "smb"), @@ -155,7 +155,10 @@ def test_build_service_restart_policy(name): assert frag.get("restart") == "unless-stopped" -@pytest.mark.parametrize("name", BUILD_SERVICES) +_NODE_NAME_SERVICES = [n for n in BUILD_SERVICES if n not in ("ssh", "real_ssh")] + + +@pytest.mark.parametrize("name", _NODE_NAME_SERVICES) def test_build_service_node_name_env(name): frag = _fragment(name) env = frag.get("environment", {}) @@ -163,8 +166,8 @@ def test_build_service_node_name_env(name): assert env["NODE_NAME"] == "test-decky" -# SSH uses COWRIE_OUTPUT_TCP_* instead of LOG_TARGET — exclude from generic tests -_LOG_TARGET_SERVICES = [n for n in BUILD_SERVICES if n != "ssh"] +# ssh and real_ssh do not use LOG_TARGET (rsyslog handles log forwarding inside the container) +_LOG_TARGET_SERVICES = [n for n in BUILD_SERVICES if n not in ("ssh", "real_ssh")] @pytest.mark.parametrize("name", _LOG_TARGET_SERVICES) @@ -181,13 +184,11 @@ def test_build_service_no_log_target_by_default(name): assert "LOG_TARGET" not in env -def test_ssh_log_target_uses_cowrie_tcp_output(): - """SSH forwards logs via Cowrie TCP output, not LOG_TARGET.""" +def test_ssh_no_log_target_env(): + """SSH uses rsyslog internally — no LOG_TARGET or COWRIE_* vars.""" env = _fragment("ssh", log_target="10.0.0.1:5140").get("environment", {}) - assert env.get("COWRIE_OUTPUT_TCP_ENABLED") == "true" - assert env.get("COWRIE_OUTPUT_TCP_HOST") == "10.0.0.1" - assert env.get("COWRIE_OUTPUT_TCP_PORT") == "5140" assert "LOG_TARGET" not in env + assert not any(k.startswith("COWRIE_") for k in env) # --------------------------------------------------------------------------- @@ -266,31 +267,26 @@ def test_http_empty_service_cfg_no_extra_env(): # SSH ------------------------------------------------------------------------ -def test_ssh_default_no_persona_env(): +def test_ssh_default_env(): env = _fragment("ssh").get("environment", {}) - for key in ("COWRIE_HONEYPOT_KERNEL_VERSION", "COWRIE_HONEYPOT_HARDWARE_PLATFORM", - "COWRIE_SSH_VERSION", "COWRIE_USERDB_ENTRIES"): - assert key not in env, f"Expected {key} absent by default" + assert env.get("SSH_ROOT_PASSWORD") == "admin" + assert not any(k.startswith("COWRIE_") for k in env) + assert "NODE_NAME" not in env -def test_ssh_kernel_version(): - env = _fragment("ssh", service_cfg={"kernel_version": "5.15.0-76-generic"}).get("environment", {}) - assert env.get("COWRIE_HONEYPOT_KERNEL_VERSION") == "5.15.0-76-generic" +def test_ssh_custom_password(): + env = _fragment("ssh", service_cfg={"password": "h4x!"}).get("environment", {}) + assert env.get("SSH_ROOT_PASSWORD") == "h4x!" -def test_ssh_hardware_platform(): - env = _fragment("ssh", service_cfg={"hardware_platform": "aarch64"}).get("environment", {}) - assert env.get("COWRIE_HONEYPOT_HARDWARE_PLATFORM") == "aarch64" +def test_ssh_custom_hostname(): + env = _fragment("ssh", service_cfg={"hostname": "prod-db"}).get("environment", {}) + assert env.get("SSH_HOSTNAME") == "prod-db" -def test_ssh_banner(): - env = _fragment("ssh", service_cfg={"ssh_banner": "SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.3"}).get("environment", {}) - assert env.get("COWRIE_SSH_VERSION") == "SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.3" - - -def test_ssh_users(): - env = _fragment("ssh", service_cfg={"users": "root:toor,admin:admin123"}).get("environment", {}) - assert env.get("COWRIE_USERDB_ENTRIES") == "root:toor,admin:admin123" +def test_ssh_no_hostname_by_default(): + env = _fragment("ssh").get("environment", {}) + assert "SSH_HOSTNAME" not in env # SMTP -----------------------------------------------------------------------