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:
@@ -137,3 +137,15 @@ class BountiesMixin:
|
||||
pass
|
||||
grouped[item.attacker_ip].append(d)
|
||||
return dict(grouped)
|
||||
|
||||
async def count_probe_relays(self, attacker_ip: str, decky: str) -> int:
|
||||
"""Return how many probe_relay bounties exist for this (attacker_ip, decky) pair."""
|
||||
async with self._session() as session:
|
||||
result = await session.execute(
|
||||
select(func.count()).select_from(Bounty).where(
|
||||
Bounty.attacker_ip == attacker_ip,
|
||||
Bounty.decky == decky,
|
||||
Bounty.bounty_type == "probe_relay",
|
||||
)
|
||||
)
|
||||
return result.scalar() or 0
|
||||
|
||||
@@ -59,6 +59,16 @@ class FleetMixin:
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
async def get_fleet_decky_by_name(self, name: str) -> dict[str, Any] | None:
|
||||
async with self._session() as session:
|
||||
result = await session.execute(
|
||||
select(FleetDecky).where(FleetDecky.name == name)
|
||||
)
|
||||
row = result.scalar_one_or_none()
|
||||
if row is None:
|
||||
return None
|
||||
return _deserialize_json_fields(row.model_dump(mode="json"), ("services", "decky_config"))
|
||||
|
||||
async def list_fleet_deckies(
|
||||
self, *, host_uuid: Optional[str] = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
|
||||
@@ -613,21 +613,32 @@ async def _extract_bounty(
|
||||
"content_type": _fields.get("content_type"),
|
||||
},
|
||||
})
|
||||
elif _evt == "probe_forwarded":
|
||||
# Record whether the upstream relay accepted the probe. forwarded=1
|
||||
# means the attacker's test email actually landed in their inbox;
|
||||
# forwarded=0 means the upstream refused (attacker still got 250).
|
||||
await repo.add_bounty({
|
||||
"decky": log_data.get("decky"),
|
||||
"service": log_data.get("service"),
|
||||
"attacker_ip": log_data.get("attacker_ip"),
|
||||
"bounty_type": "probe_relay",
|
||||
"payload": {
|
||||
"msg_id": _fields.get("msg_id"),
|
||||
"forwarded": _fields.get("forwarded") == "1",
|
||||
"delivery_count": _fields.get("delivery_count"),
|
||||
# Signal the realism worker to forward this as a probe if it's the
|
||||
# first message from this IP on an smtp_relay decky. The worker has
|
||||
# real internet access (the container is on MACVLAN and doesn't).
|
||||
if log_data.get("service") == "smtp_relay":
|
||||
await _publish_probe_pending(log_data, _fields)
|
||||
|
||||
|
||||
async def _publish_probe_pending(log_data: dict, fields: dict) -> None:
|
||||
try:
|
||||
bus = get_bus(client_name="ingester-probe")
|
||||
await bus.connect()
|
||||
await publish_safely(
|
||||
bus,
|
||||
_topics.smtp("probe.pending"),
|
||||
{
|
||||
"decky": log_data.get("decky"),
|
||||
"attacker_ip": log_data.get("attacker_ip"),
|
||||
"stored_as": fields.get("stored_as"),
|
||||
"mail_from": fields.get("mail_from"),
|
||||
"rcpt_to": fields.get("rcpt_to"),
|
||||
},
|
||||
})
|
||||
event_type="probe.pending",
|
||||
)
|
||||
await bus.close()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.debug("probe pending publish failed: %s", exc)
|
||||
|
||||
|
||||
# ─── IP-leak detection (XFF / Forwarded / X-Real-IP / CDN variants) ──────────
|
||||
|
||||
Reference in New Issue
Block a user