A small, fail-closed prototype for container egress enforced through chained HTTP CONNECT proxies.
This starter repo is split into two tracks:
- Smoke harness:
podman-composesetup to prove DoH and the CONNECT chain end to end. - Host deployment examples: nftables + TPROXY + owner-gating scripts for a real host.
The design goal is intentionally boring:
- HTTP CONNECT everywhere
- no direct DNS from workloads
- DNS only through a local DoH-capable resolver
- fail closed
- simple supervision and observability first
.
├── README.md
├── Makefile
├── docker-compose.yml
├── docs/
│ ├── BRINGUP-CHECKLIST.md
│ ├── FUNKYDNS-REVIEW.md
│ ├── HOST-DEPLOYMENT.md
│ ├── REPO_MAINTENANCE.md
│ └── USER-FLOW-REVIEW.md
├── egressd/
│ ├── Dockerfile
│ ├── requirements.txt
│ ├── chain.py
│ ├── readiness.py
│ ├── supervisor.py
│ ├── test_supervisor.py
│ ├── config.json5
│ ├── config.host.example.json5
│ └── systemd/egressd.service
├── proxy/
│ └── Dockerfile
├── exitserver/
│ ├── Dockerfile
│ └── echo_server.py
├── client/
│ ├── Dockerfile
│ └── test_client.py
├── scripts/
│ ├── bootstrap-third-party.sh
│ ├── host-nftables.sh
│ ├── repo_hygiene.py
│ ├── repo_maintenance.py
│ ├── host-egress-owner.sh
│ └── test_repo_hygiene.py
├── tests/
│ ├── test_readiness.py
│ └── test_supervisor.py
└── third_party/
└── README.md
For a reviewed walkthrough of the smoke-harness flow, host flow, and current
known breakpoints, see docs/USER-FLOW-REVIEW.md.
Configure authenticated GitHub access, then initialize the private submodule:
git submodule update --init --recursive third_party/FunkyDNSIf you prefer a direct clone workflow, you can still clone P4X-ng/FunkyDNS
into third_party/FunkyDNS manually:
make depsThis uses scripts/bootstrap-third-party.sh, which checks out the exact gitlink
revision for third_party/FunkyDNS and normalizes the remote URL after auth.
podman-compose build
podman-compose upOr through the task runner:
make smokeclientshould print matchingDNS OKandDoH OKlines for:smoke.test -> 203.0.113.10hosts.smoke.internal -> 198.51.100.21printer -> 198.51.100.42 (owner printer.corp.test.)
clientshould then print a successfulCONNECTfollowed byOK from exit-serverfunkyexposes the smoke DoH listener onhttps://localhost:18443
curl -sk https://localhost:18443/healthz
curl http://localhost:9191/health
curl -f http://localhost:9191/ready
curl http://localhost:9191/live- HTTPS DoH listener on
funky:443 - direct DNS and DoH lookup for
smoke.test -> 203.0.113.10 - mounted hosts-file lookup for
hosts.smoke.internal -> 198.51.100.21 - mounted
resolv.confsearch-domain lookup forprinter -> printer.corp.test -> 198.51.100.42 - local explicit CONNECT tunnel establishment
- multi-hop relay via
pproxy - end-to-end raw TCP after CONNECT
- per-hop health probes and readiness gating
- separate FunkyDNS and upstream search-DNS services for DNS work
The smoke config uses exitserver:9999 as the canary target, so readiness does
not depend on external internet reachability.
It does not prove host enforcement. For that, use the scripts in scripts/ on a real Linux host and follow docs/HOST-DEPLOYMENT.md.
GET /live: process is up (simple liveness check)GET /health: detailed state (pproxy,funkydns, per-hop probe details, and readiness block)GET /ready: returns200only whenegressdis usable for forwardingpproxymust be running- if
dns.launch_funkydns=true, FunkyDNS must also be running - hop checks must be complete and successful by default
Readiness behavior can be tuned via:
supervisor.require_all_hops_healthysupervisor.max_hop_status_age_ssupervisor.block_start_until_hops_healthy
The compose harness runs FunkyDNS as a separate service.
egressd does not launch FunkyDNS in smoke mode. That avoids double-start bugs and keeps service boundaries clean.
The smoke FunkyDNS image carries a self-signed cert, a clean local zone, a
mounted hosts file, and a mounted resolv.conf so compose can health-check:
- direct DNS on
53 - a real DoH POST on
443 - local
/etc/hostsresolution - search-domain recursion via a dedicated internal
searchdnsservice
The smoke image also uses a small local launcher to bound FunkyDNS teardown, because the upstream server path does not currently shut down reliably on container stop signals.
The vendored FunkyDNS resolver now honors local host resolution before external upstreams:
/etc/hostsis checked first for A and AAAA records- local zone files are checked next
- the system resolver from
/etc/resolv.confis preferred before explicit upstreams - single-label names use the system resolver's search domains
That makes Ubuntu-style systemd-resolved setups behave the way operators
usually expect. In containerized deployments, point FunkyDNS at a usable
resolv.conf if the container's default one references an unreachable loopback
stub.
For host mode, egressd/config.host.example.json5 shows how to launch FunkyDNS locally if you want a single host-managed stack.
egressd validates config and binary prerequisites before launching pproxy.
If preflight fails, it exits non-zero and logs each specific failure (for example:
invalid hop URL, empty chain, or missing binary path).
Useful checks:
make preflight
make validate-configegressd/config.json5: proxy hop URLs, canary target, health portegressd/config*.json5DNS section: usedoh_upstreams(list) or legacydoh_upstream(single URL)scripts/host-egress-owner.sh: upstream proxy and DoH IPsscripts/host-nftables.sh: bridge interface name and infra CIDRs
Run repository maintenance checks (unfinished markers, backup files, stale artifacts, stray cache dirs, and unexpected embedded git repos) for first-party code:
make maintenance
# equivalent: python3 scripts/repo_hygiene.py scan --repo-root .For automatic cleanup of removable clutter (backup files, stray __pycache__/ dirs, and known stale artifacts):
make maintenance-fix
# equivalent: python3 scripts/repo_hygiene.py clean --repo-root .maintenance* targets focus on first-party code by default. For a full scan that
also includes third_party/FunkyDNS, use:
make maintenance-all
make maintenance-all-jsonFor scheduled automation, keep this check in the loop to catch new TODO/STUB markers and stray files early.
For focused first-party hygiene scans and stray cleanup:
make repo-scan
make repo-clean
make repo-scan-jsonI added a short review in docs/FUNKYDNS-REVIEW.md with the concrete issues worth fixing before you rely on it in a production-ish setup.
make pycheckcompiles key Python entry points for syntax sanity.make testruns the repo's unit and hygiene checks.make preflightvalidates config in a disposable container with binary checks skipped.make validate-configvalidates the runtime image/config with normal binary checks.make cleanremoves local build/test artifacts (__pycache__,.pytest_cache, bundle tarball).