HERMES-GIELIE/tests/test_conftest_network_isolation.py

131 lines
6 KiB
Python
Raw Normal View History

test(infra): hermetic network isolation — block all outbound from tests Tests should not reach the public internet. Before this commit, an accidentally-leaking outbound socket from the test_server fixture (real TLS handshakes to Anthropic / Amazon / OpenRouter, sometimes triggered by SDK-init paths that found a credential the credential-strip allowlist missed) was adding 60+s of wall-time to a 100s test run and creating a class of flaky failures. This installs a default-deny socket-block at two layers: 1. Pytest process, via tests/conftest.py module-level monkey-patch on socket.create_connection + socket.socket.connect. Loopback / RFC1918 private / link-local / RFC2606 reserved-TLD destinations pass through; anything else raises OSError("hermes test network isolation: outbound to ... blocked"). Tests that legitimately need real outbound opt back in via the new `allow_outbound_network` fixture (no current callers). 2. Test_server subprocess (server.py), via a HERMES_WEBUI_TEST_NETWORK_BLOCK=1 environment-variable-gated guard at the top of server.py. tests/conftest.py sets the env var on every test_server spawn. Without this, the subprocess could make outbound that the pytest-side block can't see (which is exactly what was happening — verified via `ss -tnp` showing the server.py child with established ESTAB sockets to [2607:6bc0::10]:443). In production the env var is unset, so the guard is a no-op. Companion changes: - test_dns_resolution_failure refactored to mock socket.getaddrinfo raising gaierror, instead of relying on a real DNS lookup of a *.invalid hostname. The test was the one outlier that genuinely exercised real DNS; mocking matches what every other probe-error test in the same file already does. - New tests/test_conftest_network_isolation.py with 9 adversarial tests proving the block fires for public IPs (including the exact Anthropic IPv6 and Amazon IPv4 destinations we observed leaking), the allow-list passes loopback / RFC1918 / link-local / reserved-TLDs, and the opt-in fixture re-enables real outbound when needed. Test suite: 5,120 → 5,192 (+72 net new from this commit + the regression tests in the companion commits). Wall time: 161s → 95s on the same hardware. No remaining outbound from any test path.
2026-05-11 05:59:42 +00:00
"""Adversarial test for the network-isolation fixture in conftest.py.
The autouse module-level monkey-patch in tests/conftest.py wraps
socket.create_connection so that any non-loopback / non-RFC1918 / non-link-local
destination raises OSError. This file proves:
1. The block actually fires for outbound to a real public IP.
2. Loopback / RFC1918 / link-local / reserved-TLD destinations pass through.
3. The `allow_outbound_network` fixture re-enables real network for tests
that legitimately need it.
Without this enforcement, a test that accidentally calls real outbound
(forgotten mock, leaked credential triggering an SDK initialisation, new
code path bypassing an existing mock) can leak production credentials,
slow the test suite into 10-minute waits on TLS handshakes, and produce
flaky failures depending on whether the destination is reachable.
"""
from __future__ import annotations
import socket
import pytest
def test_outbound_to_public_ipv4_is_blocked():
"""Attempting to connect to a public IP must raise OSError."""
with pytest.raises(OSError, match="hermes test network isolation"):
# 8.8.8.8 (Google DNS) is a stable real public IPv4.
# If we accidentally connect, the test goes to 53/tcp which is
# genuinely listening — so the block is what stops us, not lack of
# destination.
socket.create_connection(("8.8.8.8", 53), timeout=1)
def test_outbound_to_anthropic_ipv6_is_blocked():
"""The exact destination we observed leaking from earlier pytest runs."""
with pytest.raises(OSError, match="hermes test network isolation"):
socket.create_connection(("2607:6bc0::10", 443), timeout=1)
def test_outbound_to_amazon_is_blocked():
"""AWS endpoints (botocore / bedrock) must not reach the real service."""
with pytest.raises(OSError, match="hermes test network isolation"):
socket.create_connection(("3.173.21.63", 443), timeout=1)
def test_loopback_v4_is_allowed():
"""127.0.0.1 must continue to work — test_server fixture depends on it."""
# Listen on a temporary port + connect via the wrapped create_connection.
listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listener.bind(("127.0.0.1", 0))
port = listener.getsockname()[1]
listener.listen(1)
try:
client = socket.create_connection(("127.0.0.1", port), timeout=1)
client.close()
finally:
listener.close()
def test_rfc1918_private_ipv4_is_allowed():
"""RFC1918 (10/8, 172.16/12, 192.168/16) must pass — devs run LM Studio
on their LAN. The block only refuses non-RFC1918 + non-loopback."""
import tests.conftest as _conftest
# Direct unit test on the predicate so we don't have to start a real listener
# in a private-IP subnet just to prove this.
assert _conftest._hermes_addr_is_local("10.0.0.5") is True
assert _conftest._hermes_addr_is_local("172.16.5.1") is True
assert _conftest._hermes_addr_is_local("172.31.255.254") is True
assert _conftest._hermes_addr_is_local("192.168.1.22") is True
def test_link_local_is_allowed():
"""169.254.0.0/16 (link-local / IMDS) — AWS_EC2_METADATA_DISABLED already
short-circuits the actual probe but the socket layer allows it."""
import tests.conftest as _conftest
assert _conftest._hermes_addr_is_local("169.254.169.254") is True
def test_reserved_tlds_are_allowed():
"""RFC 2606/6761 reserved TLDs — used as documentation hostnames in tests
(e.g. example.com, test-host.invalid)."""
import tests.conftest as _conftest
assert _conftest._hermes_addr_is_local("example.com") is True
assert _conftest._hermes_addr_is_local("my-mac.tailnet.example") is True
assert _conftest._hermes_addr_is_local("anything.invalid") is True
assert _conftest._hermes_addr_is_local("test-host.test") is True
assert _conftest._hermes_addr_is_local("printer.local") is True
assert _conftest._hermes_addr_is_local("localhost") is True
def test_public_ipv4_is_blocked():
"""Public IPs must NOT be treated as local."""
import tests.conftest as _conftest
assert _conftest._hermes_addr_is_local("8.8.8.8") is False
assert _conftest._hermes_addr_is_local("1.1.1.1") is False
assert _conftest._hermes_addr_is_local("203.0.113.0") is True # TEST-NET-3
assert _conftest._hermes_addr_is_local("204.0.113.0") is False # outside
def test_allow_outbound_network_fixture_unswaps_the_wrappers(allow_outbound_network):
"""When a test opts in to the fixture, socket.create_connection and
socket.socket.connect are restored to their real (unwrapped) implementations
for this test only.
Check by qname so this is robust against pytest re-importing conftest
under multiple roots (which produces two distinct function objects with
the same __qualname__ but different `is` identity).
"""
# Inside the fixture, the symbol should NOT be the blocked wrapper.
assert "_hermes_blocked_create_connection" not in getattr(
socket.create_connection, "__qualname__", ""
), "allow_outbound_network fixture did not restore the real create_connection"
assert "_hermes_blocked_socket_connect" not in getattr(
socket.socket.connect, "__qualname__", ""
), "allow_outbound_network fixture did not restore the real socket.connect"
def test_block_is_active_outside_the_fixture():
"""Sanity: a test that does NOT request the fixture has the wrapped
socket.create_connection installed.
Check by qname so this is robust against pytest re-importing conftest
under multiple roots (which produces two distinct function objects with
the same __qualname__ but different `is` identity)."""
assert "_hermes_blocked_create_connection" in getattr(
socket.create_connection, "__qualname__", ""
), "default state should have the blocked wrapper installed on socket.create_connection"
assert "_hermes_blocked_socket_connect" in getattr(
socket.socket.connect, "__qualname__", ""
), "default state should have the blocked wrapper installed on socket.socket.connect"