On this page
The audit-hook network guard: it refuses connects, datagram sends and name resolution made through Python's socket module -- a C driver is invisible to it.
#python.src.stricttest.socketguard
#python.src.stricttest.socketguard
Audit-hook socket guard.
Net-new to stricttest (rlsbl's floor had no in-process network guard). Built on sys.addaudithook rather than by monkeypatching socket, so it cannot be un-patched by a test, a library, or a reload. Audit hooks are permanent for the life of the process by design: the hook is installed at most once and reads a module-level policy, and raising from it propagates the refusal to whoever attempted the connect.
Granularity is host:port and unix-socket path -- pytest-socket only offers host-level blocking, which is unusable for database tests that need exactly one port or exactly one unix socket.
Scope, stated plainly: this guard sees the connects, datagram sends and name resolutions made through Python's socket module -- resolution included, because a DNS query for a forbidden host has already left the machine by the time a connect could be refused. Network performed by a spawned subprocess (git, gh, psql) is invisible to it. Whole-process network isolation is the sandbox runner's job (--unshare-net); this guard is the in-process floor beneath it.
A C extension that calls connect() itself is equally invisible, and this is worth being blunt about because it is easy to assume otherwise. The audit events this guard listens for are raised by Python's socket module, so a libpq-backed driver (psycopg) or any other native client opens its connection at a level the hook never runs at. That is not a gap an allowlist can close: there is no event to allow, so allowlisting changes nothing in either direction, and no stance offered here protects such a consumer. Clients implemented in Python (asyncpg, httpx, requests, urllib) go through socket and are covered. For the ones that are not, the protection has to be structural -- an ephemeral database at the end of the socket (:mod:stricttest.pgcluster), or the sandbox runner's network namespace.
#NetworkBlocked
Raised inside the audit hook when a connection is refused.
Subclasses BaseException on purpose: a bare except Exception in production code under test must not be able to swallow the refusal and turn a real egress attempt into a silent pass. Same reasoning as the push guard's use of pytest.fail.
#Policy
The resolved network stance for one process.
#from_settings
def from_settings(cls, settings: Settings) -> Policy#allowlist_hosts
def allowlist_hosts(self) -> frozenset[str]#describe
def describe(self) -> str#is_loopback
def is_loopback(host: str) -> boolTrue when host names the loopback interface.
#_check_resolution
def _check_resolution(policy: Policy, host, port=None) -> NoneRefuse name resolution that could only serve a forbidden connect.
Resolution is blocked before the connect so the failure names the host instead of surfacing later as an opaque timeout -- and, more importantly, because a resolution IS egress: a DNS query for a forbidden host has already left the machine by the time the connect would be refused.
port is present for getaddrinfo and absent for the gethostbyname / gethostbyaddr family, which resolve a bare name.
#install
def install(settings: Settings) -> NoneArm the guard for this process. The audit hook is added at most once.
#current_policy
def current_policy() -> Policy | NoneThe active policy, or None when the guard has not been armed.