Skip to content

feat(egress): the proxy includes the declared model endpoints, rendered by pi-dispatch egress render (#503, 2/6) - #517

Merged
edgehero merged 1 commit into
mainfrom
feat/503-proxy-include
Oct 2, 2026
Merged

edgehero merged 1 commit into
mainfrom
feat/503-proxy-include

Conversation

@edgehero

@edgehero edgehero commented Oct 2, 2026

Copy link
Copy Markdown
Owner

Part of #503: PR 2 of 6. The egress proxy now includes the declared model endpoints. A new verb, pi-dispatch egress render, writes the include, and every place the proxy is started mounts it.

What changes

  • Proxy rules. deploy/egress-proxy.conf (and its mirror) adds include /etc/pi-dispatch/model-endpoints.conf directly after http_access deny allowed to_host_local, before http_access deny CONNECT !SSL_ports.
    • pi sends every provider call as a CONNECT tunnel, and first match wins, so the include must come before the 443-only rule.
    • The include only adds a loopback deny and allow CONNECT <host> <port> for each declared pair. By default it is empty.
  • pi-dispatch egress render.
    • It validates and renders in memory, then writes model-endpoints.conf in place: no O_CREAT, O_NOFOLLOW, truncate, write, fsync.
    • Measured 2026-09-30: on Linux a single-file bind mount keeps the old file after a tmp+rename, and a reconfigure then silently keeps the old rules.
    • It refuses a symlink, a directory or a missing file. An unchanged render writes nothing.
    • It prints docker exec / podman exec pi-dispatch-egress-proxy squid -k reconfigure and never reloads the proxy itself. Measured: reconfigure re-reads the include, keeps open tunnels, and keeps the proxy's networks and IPs.
  • The mount, everywhere the proxy starts. model-endpoints.conf is mounted read-only (ro,z) in compose and its mirror, up's run argv, the Quadlet unit and its mirror, the hand-started recipe in docs/podman.md, podman-conformance.yml, the host check and the deploy-lint fixture.
    • squid refuses to start if an included file is missing (measured), so up, service install and doctor refuse or flag a missing include, or a directory at its path.
    • The Quadlet unit mounts the deployment's file directly, as it does the allowlist, so a render reaches the running proxy.
  • Reaching the host. compose and up give the proxy, never jobs, host.docker.internal:host-gateway. Measured on Docker Engine 29.1.3: this reaches a server on 172.17.0.1 or 0.0.0.0. On Desktop the name already resolves. Rootless Podman needs nothing (host.containers.internal).
  • Drift (egress-proxy-state.mjs).
    • A present third bind must be this folder's file.
    • A missing third bind counts as drift when endpoints are declared, or when this folder's rules include the file: such a proxy would exit on its next restart.
    • In both cases up replaces it instead of restarting it.
  • Docs and specs.
    • docs/egress.md gains a "Local model servers" stub.
    • The specs INT-EGRESS-POLICY-CONTRACT, REQ-EGRESS-ALLOWLIST, DES-EGRESS-DENY-ON-A-DEDICATED-NETWORK and INT-MODEL-ENDPOINTS-FILE-CONTRACT are amended.
    • A host with an empty port (a.lan:) now gets the "carries a port" refusal.

Tests

  • A conf-order test, plus the mirror, compose, up argv, Quadlet, podman-doc and CI-sites tests.
  • Render-in-place inode tests and a validate-first test.
  • Drift and doctor tests.
  • 18 mutations, each shown red. Among them: the include moved after the 443 rule, a mount removed at each site, a render through tmp+rename, truncating before validating, and the drift rule weakened.

@edgehero
edgehero force-pushed the feat/503-proxy-include branch from d93b422 to c2b2a93 Compare October 2, 2026 07:12
…ed by pi-dispatch egress render (#503)

The second change of #503. The proxy's rules now read the include that the
first change renders, every way of starting the proxy mounts it, and a new
verb writes it and prints the reload.

- deploy/egress-proxy.conf (and its mirror): `include
  /etc/pi-dispatch/model-endpoints.conf` directly after `http_access deny
  allowed to_host_local` and before `http_access deny CONNECT !SSL_ports`. pi
  tunnels every provider call and first match wins, so the include must come
  before the port-443 deny. It coexists with #508's port-80 rule.
- The third mount, read-only with `z`, from the deployment folder's own
  model-endpoints.conf: compose (and mirror), up's run argv, the Quadlet unit
  (a third placeholder rendered to <deployDir>/model-endpoints.conf, mounted
  directly like the allowlist and never copied like the rules, because the
  render writes that file in place), the hand-started Podman recipe,
  podman-conformance.yml and the host check's mount list. squid refuses to
  start without the file, so up, service install and doctor name it missing
  (or a directory) and start nothing.
- The proxy, never a job, gets host.docker.internal:host-gateway on Docker
  (compose and up). Docker Desktop resolves the name already and its
  host-gateway is the same host address, so one line serves both. Rootless
  Podman adds nothing: host.containers.internal resolves.
- worker/src/egress-cli.mjs, `pi-dispatch egress render`: loads the
  declaration through the worker's loader, renders in memory, then writes the
  include IN PLACE (no O_CREAT, O_NOFOLLOW, truncate, fsync). A temp file
  renamed over a single-file bind mount is invisible to the running proxy and
  `squid -k reconfigure` then reloads the old rules silently (measured on
  Docker 29.1.3 and Podman 4.9.3 and 5.8.1, 2026-09-30). A symlink, a
  directory or a missing file is refused; an unchanged render writes nothing.
  It prints `docker exec` or `podman exec ... squid -k reconfigure` for the
  deployment's venues and never runs it.
- One governing rule: the folder's rules include model-endpoints.conf only
  together with a proxy that mounts it. A two-mount proxy stays current; a
  third bind must be this folder's file; an absent one is drift only when the
  folder's rules include the file (its next start exits on squid's
  missing-include FATAL, measured), and such a proxy of this folder is said
  to be out of date. up's rules refresh and the replace of a proxy without
  the mount are one step, asked once: declined or not possible here, nothing
  is written; a failed replace puts the old rules back from the backup. In
  each of those cases the usual proxy step then runs on the old rules (start
  a stopped proxy, unpause a paused one, name declared endpoints), and a
  blocked step names the way out (rm -f -v the proxy, then up).
- Endpoints declared under rules that predate #503 are not drift (a replace
  would cut tunnels and fix nothing): up, doctor and egress render print one
  shared line naming the rules refresh (docker: the folder's rules; podman:
  the account copy and service install --force). doctor says a missing
  include with a cross only when the running rules include it, else a
  warning.
- render: truncates only after judging the file (a hard-linked include is
  refused), empties the include when a write fails rather than leaving a
  prefix, compares PI_MODEL_ENDPOINTS_FILE as resolved paths and warns when
  only the shell sets it.
- A replaced proxy is removed with `rm -f -v`, so its anonymous squid volumes
  go with it.
- canonicalHost: `a.lan:` (an empty port) is refused as carrying a port, not
  as an IPv6 address with an IPv4 tail (PR #515's review).
- docs/egress.md gains a short "Local model servers" section (per-venue
  reachability, jobs cannot use it until the keyless gate and slots land),
  and says a hand-started proxy or an old compose copy must be recreated with
  the third mount, never just restarted.

Specs: INT-EGRESS-POLICY-CONTRACT amended (the include, its order, its
mounts, the in-place render, reload by reconfigure, the governing rule, the
endpoint case in Acceptance).
REQ-EGRESS-ALLOWLIST amended (endpoint acceptance).
DES-EGRESS-DENY-ON-A-DEDICATED-NETWORK amended (through the proxy, never the
host network). INT-MODEL-ENDPOINTS-FILE-CONTRACT amended (Status, the empty
port). INT-CONTAINER-RUNTIME-CONTRACT unchanged, checked.

Signed-off-by: Rob Boerman <robboerman@live.nl>
@edgehero
edgehero force-pushed the feat/503-proxy-include branch from c2b2a93 to 1b7f64b Compare October 2, 2026 07:39
@edgehero
edgehero merged commit ee134d8 into main Oct 2, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant