pkgproxy — Design Plan
This file holds the scope (§1), the listener (§12) and the tuning knobs (§18), which every side shares. Each area has its own document under design/, with its own table of contents. The § numbers are shared by all of them, so a reference such as §16.9 names one place; the map below says which file holds each section.
- 1. Goal
- §2–§8: apt: what apt asks for, the forward-proxy shape, normalization, the caching policy and the fetch path.
- §11: snap: the Snap Store’s packages and assertions, through the snap relay.
- 12. Listener: TCP or UNIX socket
- §13, §15: stats: the read-only stats API, and the default socket path both sides agree on.
- §14: pkgproxyctl: pkgproxyctl as a multi-call binary, the command layout, the piped setup scripts and the help layout.
- §16–§16.4: Go modules: the Go module proxy (the GOPROXY protocol) and checksum database verification.
- §16.6–§16.9: build containers: pointing LXD build containers at pkgproxy, and the admin API socket that lets that run without root.
- §17: LXD images: caching LXD simplestreams image mirrors.
- 18. Tuning knobs: flags, environment,
snap set - §19: Docker: the Docker Hub registry mirror.
- §20: setup page: the setup page the daemon serves for remote clients, and the desktop launcher that opens it.
- §21: offline mode: serving what is cached, with nothing sent upstream.
- §22: localization: translatable errors, log messages and setup guidance.
- §23: access log: one line per request, with an
outcome.
1. Goal
Build a Go proxy that sits between apt clients and upstream Debian/Ubuntu-style
archives, reachable over plain HTTP only, and caches everything it fetches
forever. Multiple mirror hosts that serve the same archive content should
collapse onto one backing store entry instead of being cached once per mirror.
Storage is behind an interface; the first implementation is local disk, a
future one is S3-compatible object storage.
Non-goals (v1)
- Caching/verifying HTTPS content. We never terminate TLS, MITM, cache,
or hash-verify anything fetched over
https://.CONNECTis supported, but only as a blind, uncached TCP tunnel to port 443 — see §3. It has to be: apt sendsCONNECTto whateverAcquire::http::Proxynames for anyhttps://source, with noAcquire::https::Proxyneeded, so a client with one https-only source among its http ones cannot keep that traffic away from us, and refusing it breaks itsapt update. - PDiff (incremental index patches). We always serve full index files.
Not caching patches is safe — it just costs a client an extra full
download instead of a diff. Only Debian publishes
*.diff/Index; Ubuntu’s archive does not. - Repository signing / GPG. We are not a Verify layer.
aptverifiesRelease/InReleasesignatures itself; our only obligation is to serve those files byte-identical to the origin so signatures still validate. - Garbage collection. “Cache indefinitely” is the explicit ask; nothing
is ever evicted. The intended direction for later is a mark-and-sweep
that keeps pool and by-hash objects still referenced from a cached
Release/Packages/Sourcesof a tracked suite, which first needs a rule for how many historical suites stay reachable.
12. Listener: TCP or UNIX socket
Every handler operates purely on net/http’s request/response model, so
the listener is a protocol-agnostic choice made once at startup and shared
by all of them through internal/gateway.
-addr (default :3142) is the TCP bind address, and it always drives
the printed setup instructions, whatever the transport: that text describes
the http://host:port clients connect to, which is a property of how
clients reach pkgproxy, not of the local net.Listen call.
-socket-path (default empty) replaces the TCP listener with a UNIX socket,
for deployments that want none, such as a host proxy bridged into a VM guest
over QEMU’s guestfwd unix: form. pkgproxy removes a stale socket file
left by a previous run before binding (this is not singleton enforcement).
-addr still drives the instructions in this mode, so pass the
hostname:port clients actually reach.
-addr and -lxd-tls-addr (unless off) are checked before anything
binds, and by -check-config (§18), with checkListenAddr: host:port
as net.SplitHostPort splits it, the host empty, an IP address or a host
name, and the port a decimal number from 0 to 65535. net.Listen also
takes a service name (:http) or no port, but the instructions name the
port and need its number. A host name is not resolved, so the result does
not depend on the network; one that does not resolve fails at start.
The socket is created mode 0777: bind(2) applies the umask, a socket left
0755 by snapd’s umask 0022 refuses connect(2) from an unprivileged relay
(connecting needs write permission), and this is a single-host trust
boundary. listenAt (cmd/pkgproxy) clears the umask around the bind, for
every UNIX socket the daemon opens, the stats (§13) and admin sockets
included.
18. Tuning knobs: flags, environment, snap set
Every interval is a daemon flag; DEPLOYMENT.md’s settings table lists them
with their defaults. -upstream-idle-timeout bounds inactivity, not total
time: every side’s upstream client goes through one
internal/idletimeout.Transport, which fails a request once the upstream
has made no progress for that long, while headers are awaited or inside a
body Read. Time the caller spends between reads does not count, so a slow
consumer never cancels a healthy upstream, and a 2 GB layer at 1 MB/s may
take its 33 minutes. A fill whose upstream stalls fails like on any
upstream error and leaves the in-progress table, so the next request starts
a fresh one instead of everyone joining a dead connection. pkgproxyctl’s
dial timeout and the LXD certificate validity stay constants: neither is a
per-site concern.
Environment. applyEnvDefaults (cmd/pkgproxy/envflags.go) gives
every daemon flag a variable PKGPROXY_<FLAG> (upper-cased, dashes to
underscores), applied before the command line is parsed, so precedence is
flag > environment > default with no per-flag code. The rule is generic on
purpose: a new flag is configurable by environment the moment it exists.
snap set. snapd configuration is only readable through snapctl,
and the daemon’s command line is static, so the snap maps options onto the
environment: pkgproxy-env, in the daemon’s command-chain:, exports
PKGPROXY_* from snapctl get for an explicit key list and execs the
daemon. Option names equal flag names. Only keys the snap’s own command
line does not pin can be listed, since a flag beats the environment, so
-cache-dir and the content sockets are not options. -offline is not one
either: offline-mode (§21) is the one name for that setting. The
configure hook runs pkgproxy -check-config (parse flags and environment,
then exit) through the same script, so an invalid snap set, a listen
address included, fails up front instead of putting the service in a
restart loop. A test keeps
pkgproxy-env’s key list, the daemon’s flags (each either mapped or listed
as not a snap option, with a reason) and DEPLOYMENT.md’s settings table in
step.
Restart or not. The configure hook cannot ask snapd which options
changed, so it compares the PKGPROXY_* environment it would start the
daemon with, minus PKGPROXY_OFFLINE_MODE, against the copy saved in
$SNAP_DATA/configured.env by its previous run. Equal means at most the
offline mode changed, and the hook switches the running daemon with
pkgproxyctl offline mode <mode>. A difference, a missing copy, or a
pkgproxyctl that fails means a restart; a stopped daemon is left alone.
Rejecting unknown keys waits for the hooks to move to Go
(gitlab.com/zygoon/pkgproxy#49): snapctl get cannot list the keys that
are set. pkgproxyctl setup and pkgproxyctl open learn the configured
ports from the running daemon’s /pkgproxy/v1/info (§13.5), not from snapctl get, which works only inside the snap; the daemon is the authority on what
it actually listens on.