Files
bot-bottle/bot_bottle/backend/firecracker/isolation_probe.py
T
didericis cd9f023f3d
prd-number-check / require-numbered-prds (pull_request) Successful in 11s
tracker-policy-pr / check-pr (pull_request) Successful in 14s
test / unit (pull_request) Successful in 59s
test / integration-docker (pull_request) Successful in 1m6s
test / coverage (push) Successful in 21s
test / image-input-builds (push) Successful in 44s
test / image-input-builds (pull_request) Successful in 1m15s
test / unit (push) Successful in 57s
test / coverage (pull_request) Successful in 20s
lint / lint (push) Successful in 1m3s
Update Quality Badges / update-badges (push) Successful in 1m12s
test / integration-docker (push) Successful in 1m0s
docs: name bot-bottle, not ./cli.py, in every instruction
`doctor` told users to run `./cli.py backend setup`. cli.py is a four-line
wrapper at the repo root that calls bot_bottle.cli:main, and it does not ship —
[tool.setuptools.packages.find] includes only bot_bottle*, so anyone who
installed rather than cloned has no such file. Confirmed against a real
install: the user's tree contains exactly one executable, `bot-bottle`, and no
cli.py anywhere, while doctor recommended `./cli.py` three times. Invisible in
development, where ./cli.py works fine from a checkout, which is why only a
clean-install test surfaced it.

The two entry points are the same code, so the fix is to name the one that
always exists. 141 replacements across 45 files: the runtime messages that
caused this, plus README, docs, PRDs, research notes and test prose, so nothing
teaches the invocation a user cannot run.

scripts/demo.sh is deliberately untouched — it *executes* ./cli.py from a
checkout, where that is the correct and available path.

Verified end to end: a sandbox install now reports
"Run: bot-bottle backend setup --backend=firecracker", and bot-bottle is on
that user's PATH. Unit suite unchanged against the pre-existing baseline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WEfZZhakx13bxTfXcZCoS5
2026-07-27 10:20:17 -04:00

117 lines
4.0 KiB
Python

"""Empirical, post-boot isolation probe — the authoritative fail-closed
egress-boundary check.
The pre-boot nft check can't be trusted on every host (listing an
nftables table typically needs root; the launcher runs unprivileged).
So before the agent ever runs, we prove the boundary directly: open a
canary TCP listener on a host IP the VM must NOT be able to reach (the
host's primary non-TAP address), then have the guest try to connect. If
the isolation rules are in force the connection is dropped and times
out; if it succeeds, the VM can reach host services it shouldn't — a
sandbox escape — and we refuse to continue.
The listener is load-bearing: without it a "blocked" result could just
be connection-refused. With it, a reachable canary means a real leak
and a timeout means a real drop.
"""
from __future__ import annotations
import socket
import subprocess
import threading
from pathlib import Path
from ...log import die, info, warn
from . import util
def _host_primary_ip() -> str:
"""The source IP the host uses for off-box traffic (its LAN
address) — a canary the VM must not reach. Empty if the host has no
such route (isolated box)."""
result = subprocess.run(
["ip", "-o", "route", "get", "1.1.1.1"],
capture_output=True, text=True, check=False,
)
if result.returncode != 0:
return ""
tokens = result.stdout.split()
# `... src <addr> ...` — the address the host would use as source.
if "src" in tokens:
idx = tokens.index("src")
if idx + 1 < len(tokens):
return tokens[idx + 1]
return ""
# Guest-side connect test: exit 0 = reached the canary (LEAK), 1 =
# blocked (good), 2 = no usable tool (inconclusive -> fail-closed).
_GUEST_PROBE = r"""
h="$1"; p="$2"
if command -v python3 >/dev/null 2>&1; then
python3 - "$h" "$p" <<'PY'
import socket, sys
s = socket.socket(); s.settimeout(3)
sys.exit(0 if s.connect_ex((sys.argv[1], int(sys.argv[2]))) == 0 else 1)
PY
elif command -v bash >/dev/null 2>&1; then
timeout 3 bash -c "exec 3<>/dev/tcp/$h/$p" >/dev/null 2>&1
elif command -v nc >/dev/null 2>&1; then
nc -w3 -z "$h" "$p" >/dev/null 2>&1
else
exit 2
fi
"""
def verify_isolation(private_key: Path, guest_ip: str) -> None:
"""Prove the VM cannot reach the host's primary IP. Dies
(fail-closed) on a confirmed leak or if the guest has no tool to
run the test. Warns (does not fail) when the host has no canary
address to test against."""
canary_ip = _host_primary_ip()
if not canary_ip:
warn("isolation probe skipped: host has no non-TAP route to test "
"against (isolated box).")
return
listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listener.bind((canary_ip, 0))
listener.listen(1)
listener.settimeout(6)
canary_port = listener.getsockname()[1]
accepted: list[bool] = []
def _accept() -> None:
try:
conn, _ = listener.accept()
accepted.append(True)
conn.close()
except OSError:
pass
thread = threading.Thread(target=_accept, daemon=True)
thread.start()
ssh = util.ssh_base_argv(private_key, guest_ip)
result = subprocess.run(
[*ssh, "--", "sh", "-s", "--", canary_ip, str(canary_port)],
input=_GUEST_PROBE, capture_output=True, text=True, check=False,
)
thread.join(timeout=7)
listener.close()
if result.returncode == 0 or accepted:
die(f"ISOLATION FAILURE: the VM reached the host canary "
f"{canary_ip}:{canary_port}. The egress boundary is not in "
f"force — refusing to run the agent (fail-closed). Verify the "
f"nft table with: bot-bottle backend setup --backend=firecracker")
if result.returncode == 2:
die("isolation probe inconclusive: the guest has no python3/bash/nc "
"to run the connectivity test. Refusing to continue "
"(fail-closed).")
info(f"isolation verified: VM cannot reach host {canary_ip}")