feat(smtp_relay): move probe forwarding to realism worker via bus
Attacker probe emails are now forwarded by the master (realism worker) rather than inside the MACVLAN container, which has no internet gateway. - New smtp.probe.pending bus topic: ingester publishes when smtp_relay message_stored fires; worker subscribes and does the actual delivery - decnet/orchestrator/drivers/smtp_relay.py: pure-sync forward_probe() reads the .eml from disk and sends via smtplib on a thread executor - worker.py: _run_smtp_probe_listener + _handle_probe_pending subtask; limit enforced via count_probe_relays() (DB-backed, restart-safe) - bounties.py: count_probe_relays() query on probe_relay bounty type - fleet.py: get_fleet_decky_by_name() to pull service config from DB - services/smtp_relay.py: upstream_* and probe_limit fields defined in config_schema but NOT injected into container env (credentials stay out of docker env vars) - ingester.py: stripped of smtplib; publishes probe.pending and exits - tests: assert upstream keys absent from container environment
This commit is contained in:
58
decnet/orchestrator/drivers/smtp_relay.py
Normal file
58
decnet/orchestrator/drivers/smtp_relay.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""SMTP probe-relay driver.
|
||||
|
||||
Forwards the attacker's first probe email via the master's real internet
|
||||
connection. The smtp_relay decky runs on MACVLAN and has no gateway access;
|
||||
the master (where this worker runs) does.
|
||||
|
||||
Called by the realism worker's smtp probe listener, not the main tick loop.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import smtplib
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
_ARTIFACTS_ROOT_DEFAULT = "/var/lib/decnet/artifacts"
|
||||
|
||||
|
||||
def forward_probe(
|
||||
*,
|
||||
svc_cfg: dict[str, Any],
|
||||
stored_as: str,
|
||||
decky_name: str,
|
||||
mail_from: str,
|
||||
rcpt_to: list[str],
|
||||
artifacts_root: str = _ARTIFACTS_ROOT_DEFAULT,
|
||||
) -> tuple[bool, str]:
|
||||
"""Read the .eml from disk and forward it via the upstream relay.
|
||||
|
||||
Returns (True, "") on success or (False, reason) on failure.
|
||||
Always safe to call in a thread — uses only blocking I/O.
|
||||
"""
|
||||
upstream_host = (svc_cfg.get("upstream_host") or "").strip()
|
||||
if not upstream_host:
|
||||
return False, "upstream_host not configured"
|
||||
|
||||
eml_path = Path(artifacts_root) / decky_name / "smtp" / stored_as
|
||||
try:
|
||||
body = eml_path.read_bytes()
|
||||
except OSError as exc:
|
||||
return False, f"cannot read eml: {exc}"
|
||||
|
||||
if not rcpt_to:
|
||||
return False, "no recipients"
|
||||
|
||||
upstream_port = int(svc_cfg.get("upstream_port") or 25)
|
||||
upstream_user = (svc_cfg.get("upstream_user") or "").strip()
|
||||
upstream_pass = (svc_cfg.get("upstream_pass") or "").strip()
|
||||
envelope_from = (svc_cfg.get("upstream_sender") or "").strip() or mail_from
|
||||
|
||||
try:
|
||||
with smtplib.SMTP(upstream_host, upstream_port, timeout=15) as conn:
|
||||
conn.ehlo()
|
||||
if upstream_user and upstream_pass:
|
||||
conn.login(upstream_user, upstream_pass)
|
||||
conn.sendmail(envelope_from, rcpt_to, body)
|
||||
return True, ""
|
||||
except Exception as exc:
|
||||
return False, str(exc)[:256]
|
||||
Reference in New Issue
Block a user