Skip to content

state

state

Per-container state bundle layout contract.

Every shielded container gets an isolated state directory. This module is the single source of truth for where files live within it — all paths are derived from a single state_dir root. Zero dependencies beyond pathlib.

Bundle layout::

{state_dir}/
├── hooks/
│   ├── terok-shield-createRuntime.json
│   └── terok-shield-poststop.json
├── terok-shield-hook              # entrypoint script (stdlib-only Python)
├── ruleset.nft                    # pre-generated nft ruleset (written by pre_start)
├── gateway                        # discovered gateway IP (written by OCI hook)
├── gateway_v6                     # discovered IPv6 gateway
├── upstream.dns                   # upstream DNS address
├── dns.tier                       # active DNS tier (dig/getent/dnsmasq)
├── profile.allowed                # IPs from DNS resolution
├── profile.domains                # domain names for dnsmasq config
├── live.allowed                   # IPs from allow/deny
├── live.domains                   # domain overrides from allow_domain
├── deny.list                      # persistent deny overrides
├── denied.domains                 # denied domains from deny_domain
├── dnsmasq.conf                   # generated dnsmasq configuration
├── dnsmasq.pid                    # dnsmasq PID (in container netns)
├── dnsmasq.log                    # dnsmasq query log (for shield watch)
├── resolv.conf                    # bind-mounted over /etc/resolv.conf (dnsmasq tier)
├── interactive                    # interactive tier marker (e.g. "nflog")
├── container.id                   # podman container ID (short, 12-char hex)
└── audit.jsonl                    # per-container audit log

BUNDLE_VERSION = 4 module-attribute

Integer version of the state bundle layout.

Bumped whenever the file layout changes in a backwards-incompatible way. The OCI hook hard-fails if the annotation version does not match.

hooks_dir(state_dir)

Return the OCI hooks directory within the state bundle.

Source code in src/terok_shield/core/state.py
def hooks_dir(state_dir: Path) -> Path:
    """Return the OCI hooks directory within the state bundle."""
    return state_dir / "hooks"

hook_entrypoint(state_dir)

Return the path to the hook entrypoint script.

Source code in src/terok_shield/core/state.py
def hook_entrypoint(state_dir: Path) -> Path:
    """Return the path to the hook entrypoint script."""
    return state_dir / "terok-shield-hook"

hook_json_path(state_dir, stage)

Return the path to a hook JSON file for a given OCI stage.

Source code in src/terok_shield/core/state.py
def hook_json_path(state_dir: Path, stage: str) -> Path:
    """Return the path to a hook JSON file for a given OCI stage."""
    return hooks_dir(state_dir) / f"terok-shield-{stage}.json"

ruleset_path(state_dir)

Return the path to the pre-generated nft ruleset file.

Source code in src/terok_shield/core/state.py
def ruleset_path(state_dir: Path) -> Path:
    """Return the path to the pre-generated nft ruleset file."""
    return state_dir / "ruleset.nft"

gateway_path(state_dir)

Return the path to the persisted discovered IPv4 gateway IP.

Source code in src/terok_shield/core/state.py
def gateway_path(state_dir: Path) -> Path:
    """Return the path to the persisted discovered IPv4 gateway IP."""
    return state_dir / "gateway"

gateway_v6_path(state_dir)

Return the path to the persisted discovered IPv6 gateway IP.

Source code in src/terok_shield/core/state.py
def gateway_v6_path(state_dir: Path) -> Path:
    """Return the path to the persisted discovered IPv6 gateway IP."""
    return state_dir / "gateway_v6"

upstream_dns_path(state_dir)

Return the path to the persisted upstream DNS address.

Source code in src/terok_shield/core/state.py
def upstream_dns_path(state_dir: Path) -> Path:
    """Return the path to the persisted upstream DNS address."""
    return state_dir / "upstream.dns"

dns_tier_path(state_dir)

Return the path to the persisted DNS tier value.

Source code in src/terok_shield/core/state.py
def dns_tier_path(state_dir: Path) -> Path:
    """Return the path to the persisted DNS tier value."""
    return state_dir / "dns.tier"

profile_allowed_path(state_dir)

Return the path to the profile-derived allowlist file.

Source code in src/terok_shield/core/state.py
def profile_allowed_path(state_dir: Path) -> Path:
    """Return the path to the profile-derived allowlist file."""
    return state_dir / "profile.allowed"

profile_domains_path(state_dir)

Return the path to the profile domain names list (for dnsmasq config).

Source code in src/terok_shield/core/state.py
def profile_domains_path(state_dir: Path) -> Path:
    """Return the path to the profile domain names list (for dnsmasq config)."""
    return state_dir / "profile.domains"

live_allowed_path(state_dir)

Return the path to the live allow/deny allowlist file.

Source code in src/terok_shield/core/state.py
def live_allowed_path(state_dir: Path) -> Path:
    """Return the path to the live allow/deny allowlist file."""
    return state_dir / "live.allowed"

live_domains_path(state_dir)

Return the path to the live domain overrides file (from allow_domain).

Source code in src/terok_shield/core/state.py
def live_domains_path(state_dir: Path) -> Path:
    """Return the path to the live domain overrides file (from allow_domain)."""
    return state_dir / "live.domains"

deny_path(state_dir)

Return the path to the persistent denylist file.

Source code in src/terok_shield/core/state.py
def deny_path(state_dir: Path) -> Path:
    """Return the path to the persistent denylist file."""
    return state_dir / "deny.list"

denied_domains_path(state_dir)

Return the path to the denied domains file (from deny_domain).

Source code in src/terok_shield/core/state.py
def denied_domains_path(state_dir: Path) -> Path:
    """Return the path to the denied domains file (from deny_domain)."""
    return state_dir / "denied.domains"

dnsmasq_conf_path(state_dir)

Return the path to the generated dnsmasq configuration file.

Source code in src/terok_shield/core/state.py
def dnsmasq_conf_path(state_dir: Path) -> Path:
    """Return the path to the generated dnsmasq configuration file."""
    return state_dir / "dnsmasq.conf"

dnsmasq_pid_path(state_dir)

Return the path to the dnsmasq PID file.

Source code in src/terok_shield/core/state.py
def dnsmasq_pid_path(state_dir: Path) -> Path:
    """Return the path to the dnsmasq PID file."""
    return state_dir / "dnsmasq.pid"

dnsmasq_log_path(state_dir)

Return the path to the dnsmasq query log (tailed by shield watch).

Source code in src/terok_shield/core/state.py
def dnsmasq_log_path(state_dir: Path) -> Path:
    """Return the path to the dnsmasq query log (tailed by ``shield watch``)."""
    return state_dir / "dnsmasq.log"

resolv_conf_path(state_dir)

Return the path to the pre-written resolv.conf for the dnsmasq tier.

pre_start() writes nameserver 127.0.0.1 here and passes --volume {path}:/etc/resolv.conf:ro to podman. Podman detects the user-supplied mount and skips its automatic pasta-generated resolv.conf, so the container's DNS is directed to the per-container dnsmasq instance at 127.0.0.1:53. The read-only mount prevents the container payload from redirecting DNS away from dnsmasq.

Source code in src/terok_shield/core/state.py
def resolv_conf_path(state_dir: Path) -> Path:
    """Return the path to the pre-written ``resolv.conf`` for the dnsmasq tier.

    ``pre_start()`` writes ``nameserver 127.0.0.1`` here and passes
    ``--volume {path}:/etc/resolv.conf:ro`` to podman.  Podman detects the
    user-supplied mount and skips its automatic pasta-generated ``resolv.conf``,
    so the container's DNS is directed to the per-container dnsmasq instance
    at ``127.0.0.1:53``.  The read-only mount prevents the container payload
    from redirecting DNS away from dnsmasq.
    """
    return state_dir / "resolv.conf"

interactive_path(state_dir)

Return the path to the interactive tier marker file.

Source code in src/terok_shield/core/state.py
def interactive_path(state_dir: Path) -> Path:
    """Return the path to the interactive tier marker file."""
    return state_dir / "interactive"

container_id_path(state_dir)

Return the path to the persisted podman container ID file.

Source code in src/terok_shield/core/state.py
def container_id_path(state_dir: Path) -> Path:
    """Return the path to the persisted podman container ID file."""
    return state_dir / "container.id"

audit_path(state_dir)

Return the path to the per-container audit log.

Source code in src/terok_shield/core/state.py
def audit_path(state_dir: Path) -> Path:
    """Return the path to the per-container audit log."""
    return state_dir / "audit.jsonl"

read_interactive_tier(state_dir)

Read the interactive tier from the state bundle.

Returns "nflog" (or whatever tier string is stored) if the file exists and contains a non-empty value, otherwise None.

Source code in src/terok_shield/core/state.py
def read_interactive_tier(state_dir: Path) -> str | None:
    """Read the interactive tier from the state bundle.

    Returns ``"nflog"`` (or whatever tier string is stored) if the file
    exists and contains a non-empty value, otherwise ``None``.
    """
    path = interactive_path(state_dir)
    if not path.is_file():
        return None
    value = path.read_text().strip()
    return value or None

read_allowed_ips(state_dir)

Merge IPs from profile.allowed and live.allowed, deduplicated.

Returns a stable-order list: profile IPs first, then live IPs, with duplicates removed (first occurrence wins).

Source code in src/terok_shield/core/state.py
def read_allowed_ips(state_dir: Path) -> list[str]:
    """Merge IPs from profile.allowed and live.allowed, deduplicated.

    Returns a stable-order list: profile IPs first, then live IPs,
    with duplicates removed (first occurrence wins).
    """
    ips: list[str] = []
    for path in (profile_allowed_path(state_dir), live_allowed_path(state_dir)):
        if path.is_file():
            ips.extend(line.strip() for line in path.read_text().splitlines() if line.strip())
    seen: set[str] = set()
    unique: list[str] = []
    for ip in ips:
        if ip not in seen:
            seen.add(ip)
            unique.append(ip)
    return unique

read_denied_ips(state_dir)

Read IPs from deny.list.

Returns an empty set if the file does not exist.

Source code in src/terok_shield/core/state.py
def read_denied_ips(state_dir: Path) -> set[str]:
    """Read IPs from deny.list.

    Returns an empty set if the file does not exist.
    """
    path = deny_path(state_dir)
    if not path.is_file():
        return set()
    return {line.strip() for line in path.read_text().splitlines() if line.strip()}

read_effective_ips(state_dir)

Compute effective allowed IPs: (profile ∪ live) − deny.

Returns a stable-order list with denied IPs subtracted.

Source code in src/terok_shield/core/state.py
def read_effective_ips(state_dir: Path) -> list[str]:
    """Compute effective allowed IPs: (profile ∪ live) − deny.

    Returns a stable-order list with denied IPs subtracted.
    """
    allowed = read_allowed_ips(state_dir)
    denied = read_denied_ips(state_dir)
    return [ip for ip in allowed if ip not in denied]

ensure_state_dirs(state_dir)

Create the state directory and its required subdirectories.

Source code in src/terok_shield/core/state.py
def ensure_state_dirs(state_dir: Path) -> None:
    """Create the state directory and its required subdirectories."""
    state_dir.mkdir(parents=True, exist_ok=True)
    hooks_dir(state_dir).mkdir(parents=True, exist_ok=True)