feat(macos): give nested containers working egress
tracker-policy-pr / check-pr (pull_request) Successful in 11s
test / integration-docker (pull_request) Successful in 17s
test / unit (pull_request) Successful in 43s
lint / lint (push) Successful in 56s
test / integration-firecracker (pull_request) Successful in 3m32s
test / coverage (pull_request) Successful in 23s
test / publish-infra (pull_request) Has been skipped
tracker-policy-pr / check-pr (pull_request) Successful in 11s
test / integration-docker (pull_request) Successful in 17s
test / unit (pull_request) Successful in 43s
lint / lint (push) Successful in 56s
test / integration-firecracker (pull_request) Successful in 3m32s
test / coverage (pull_request) Successful in 23s
test / publish-infra (pull_request) Has been skipped
Answers the last open question in #392: DNS from inside a nested container was not a phantom, but it was also not a DNS problem. Public resolution fails there by design — everything egresses through the proxy — and the actual breakage was two missing pieces: - The proxy URL names `bot-bottle-gateway`, which resolves only through the bottle's own /etc/hosts. Containers got their own hosts file, so they failed at "Could not resolve proxy", which reads like broken DNS. base_hosts_file propagates the bottle's entries. - The gateway TLS-intercepts, so a container that does not trust the bottle's CA bundle fails with "unable to get local issuer certificate". The bundle is now mounted read-only and the usual env vars point at it, which covers curl, wget, python, and node without distro-specific trust commands. Verified on the macOS host by reproducing each failure and confirming that these three conditions applied by hand produced HTTP 200 from inside a nested container. podman already forwards the proxy env, so that needed nothing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,14 @@ done
|
|||||||
|
|
||||||
# Short by necessity, not by accident: conmon's attach socket lives under
|
# Short by necessity, not by accident: conmon's attach socket lives under
|
||||||
# this directory and must fit in a 108-byte sun_path. See nested_containers.py.
|
# this directory and must fit in a 108-byte sun_path. See nested_containers.py.
|
||||||
|
# Must stay in step with AGENT_CA_BUNDLE in bot_bottle/backend/util.py; a unit
|
||||||
|
# test pins the two together.
|
||||||
|
CA_BUNDLE="/etc/ssl/certs/ca-certificates.crt"
|
||||||
|
[ -r "$CA_BUNDLE" ] || {
|
||||||
|
echo "gateway CA bundle $CA_BUNDLE is missing or unreadable" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/tmp/bbp}"
|
export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/tmp/bbp}"
|
||||||
config="$HOME/.config/containers"
|
config="$HOME/.config/containers"
|
||||||
mkdir -p "$XDG_RUNTIME_DIR" "$config"
|
mkdir -p "$XDG_RUNTIME_DIR" "$config"
|
||||||
@@ -57,9 +65,32 @@ CONF
|
|||||||
|
|
||||||
# No cgroup delegation reaches this guest, so asking podman to manage cgroups
|
# No cgroup delegation reaches this guest, so asking podman to manage cgroups
|
||||||
# fails; events_logger=file avoids the journald socket that is equally absent.
|
# fails; events_logger=file avoids the journald socket that is equally absent.
|
||||||
cat > "$config/containers.conf" <<'CONF'
|
#
|
||||||
|
# The rest of this block is what makes a nested container reach the network at
|
||||||
|
# all. podman already forwards the proxy env, but that alone is useless here:
|
||||||
|
#
|
||||||
|
# base_hosts_file the proxy URL names `bot-bottle-gateway`, which resolves
|
||||||
|
# only through the bottle's own /etc/hosts. Without this a
|
||||||
|
# container fails at "Could not resolve proxy", which reads
|
||||||
|
# like broken DNS but is a missing hosts entry — public DNS
|
||||||
|
# is *supposed* to fail, since everything egresses via the
|
||||||
|
# proxy.
|
||||||
|
# volumes/env the gateway TLS-intercepts, so a container that does not
|
||||||
|
# trust the bottle's CA bundle gets "unable to get local
|
||||||
|
# issuer certificate". Mounting the bundle read-only and
|
||||||
|
# pointing the usual env vars at it covers curl, wget,
|
||||||
|
# python, and node without distro-specific trust commands.
|
||||||
|
cat > "$config/containers.conf" <<CONF
|
||||||
[containers]
|
[containers]
|
||||||
cgroups="disabled"
|
cgroups="disabled"
|
||||||
|
base_hosts_file="/etc/hosts"
|
||||||
|
volumes=["${CA_BUNDLE}:${CA_BUNDLE}:ro"]
|
||||||
|
env=[
|
||||||
|
"SSL_CERT_FILE=${CA_BUNDLE}",
|
||||||
|
"CURL_CA_BUNDLE=${CA_BUNDLE}",
|
||||||
|
"REQUESTS_CA_BUNDLE=${CA_BUNDLE}",
|
||||||
|
"NODE_EXTRA_CA_CERTS=${CA_BUNDLE}",
|
||||||
|
]
|
||||||
[engine]
|
[engine]
|
||||||
cgroup_manager="cgroupfs"
|
cgroup_manager="cgroupfs"
|
||||||
events_logger="file"
|
events_logger="file"
|
||||||
|
|||||||
@@ -151,6 +151,39 @@ class TestNestedContainersImage(unittest.TestCase):
|
|||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
|
class TestInitScript(unittest.TestCase):
|
||||||
|
"""The bootstrap runs inside the bottle, so its wiring is only checkable
|
||||||
|
here or on a live macOS host."""
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.script = (
|
||||||
|
Path(nested_containers.__file__).with_name("nested-containers-init.sh")
|
||||||
|
.read_text(encoding="utf-8")
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_ca_bundle_path_matches_the_backend_constant(self) -> None:
|
||||||
|
from bot_bottle.backend.util import AGENT_CA_BUNDLE
|
||||||
|
self.assertIn(f'CA_BUNDLE="{AGENT_CA_BUNDLE}"', self.script)
|
||||||
|
|
||||||
|
def test_propagates_the_bottle_hosts_file_into_containers(self) -> None:
|
||||||
|
"""The proxy URL names bot-bottle-gateway, which resolves only via the
|
||||||
|
bottle's /etc/hosts. Without this a nested container dies at "Could
|
||||||
|
not resolve proxy" — which reads as broken DNS but is a missing hosts
|
||||||
|
entry; public DNS is meant to fail."""
|
||||||
|
self.assertIn('base_hosts_file="/etc/hosts"', self.script)
|
||||||
|
|
||||||
|
def test_mounts_and_trusts_the_gateway_ca(self) -> None:
|
||||||
|
"""The gateway TLS-intercepts, so without the bundle every HTTPS call
|
||||||
|
from a nested container fails with "unable to get local issuer
|
||||||
|
certificate"."""
|
||||||
|
self.assertIn("volumes=[", self.script)
|
||||||
|
for var in (
|
||||||
|
"SSL_CERT_FILE", "CURL_CA_BUNDLE", "REQUESTS_CA_BUNDLE",
|
||||||
|
"NODE_EXTRA_CA_CERTS",
|
||||||
|
):
|
||||||
|
self.assertIn(f'"{var}=${{CA_BUNDLE}}"', self.script)
|
||||||
|
|
||||||
|
|
||||||
class TestGuestEnvironment(unittest.TestCase):
|
class TestGuestEnvironment(unittest.TestCase):
|
||||||
def test_disabled_bottle_gets_no_docker_environment(self) -> None:
|
def test_disabled_bottle_gets_no_docker_environment(self) -> None:
|
||||||
self.assertEqual({}, nested_containers.guest_env(False))
|
self.assertEqual({}, nested_containers.guest_env(False))
|
||||||
|
|||||||
Reference in New Issue
Block a user