Skip to content

dns

dns

DNS resolution with timestamp-based caching.

Resolves domain names from allowlist profiles via dig and caches the results so containers do not block on DNS at every start. Profiles prefer domain names over raw IPs because CDN addresses rotate.

Falls back to getent hosts when dig is not installed — fewer IPs are captured (no parallel A + AAAA query), but resolution still works. When the dnsmasq tier is active, domain resolution happens at runtime via --nftset; this module then only handles raw IPs.

DnsResolver(*, runner)

Stateless DNS resolver — all persistence lives in the cache file.

The only dependency is a :class:CommandRunner for dig / getent subprocess calls.

Inject the command runner used for all DNS subprocess calls.

Source code in src/terok_shield/core/dns.py
def __init__(self, *, runner: CommandRunner) -> None:
    """Inject the command runner used for all DNS subprocess calls."""
    self._runner = runner

resolve_and_cache(entries, cache_path, *, max_age=3600)

Resolve profile entries and cache the result.

Profiles mix domain names with literal IPs/CIDRs — domains go through DNS resolution, literals pass through unchanged.

Parameters:

Name Type Description Default
entries list[str]

Domain names and/or raw IPs from composed profiles.

required
cache_path Path

File to store resolved IPs in, per-container scoped.

required
max_age int

Cache freshness threshold in seconds (default: 1 hour).

3600

Returns:

Type Description
list[str]

Resolved IPv4/IPv6 addresses combined with raw IPs/CIDRs.

Source code in src/terok_shield/core/dns.py
def resolve_and_cache(
    self,
    entries: list[str],
    cache_path: Path,
    *,
    max_age: int = 3600,
) -> list[str]:
    """Resolve profile entries and cache the result.

    Profiles mix domain names with literal IPs/CIDRs — domains go
    through DNS resolution, literals pass through unchanged.

    Args:
        entries: Domain names and/or raw IPs from composed profiles.
        cache_path: File to store resolved IPs in, per-container scoped.
        max_age: Cache freshness threshold in seconds (default: 1 hour).

    Returns:
        Resolved IPv4/IPv6 addresses combined with raw IPs/CIDRs.
    """
    if self._cache_fresh(cache_path, max_age):
        return self._read_cache(cache_path)

    domains, raw_ips = self._split_entries(entries)
    resolved = self.resolve_domains(domains)
    all_ips = raw_ips + resolved

    self._write_cache(cache_path, all_ips)
    return all_ips

resolve_domains(domains)

Resolve domain names to IP addresses (A + AAAA), best-effort.

Unresolvable domains are skipped with a warning. Results are deduplicated in first-seen order.

Source code in src/terok_shield/core/dns.py
def resolve_domains(self, domains: list[str]) -> list[str]:
    """Resolve domain names to IP addresses (A + AAAA), best-effort.

    Unresolvable domains are skipped with a warning.  Results are
    deduplicated in first-seen order.
    """
    seen: set[str] = set()
    result: list[str] = []
    use_getent = False
    for domain in domains:
        try:
            ips = self._resolve_one(domain, use_getent=use_getent)
        except DigNotFoundError:
            # dig missing — degrade gracefully for the rest of this batch
            logger.warning("dig not found — falling back to getent for DNS resolution")
            use_getent = True
            ips = self._resolve_one(domain, use_getent=True)
        if not ips:
            logger.warning("Domain %r resolved to no IPs (typo or DNS failure?)", domain)
        for ip in ips:
            if ip not in seen:
                seen.add(ip)
                result.append(ip)
    return result