pkgproxy design: files by checksum
Caching large files that have no package protocol of their own, by their checksum. Part of the pkgproxy design, whose index says which file holds each § number.
24. Files by checksum
internal/filesproxy caches large files that no other side would: the
cloud images image-garden boots (.qcow2, .img, .raw.xz), hundreds of
MB each. One pkgproxy on a fast local network then serves every image to
every CI machine after the first download.
Neither of the other ways in works for them. A forward-proxy GET of an
arbitrary http:// URL reaches the apt side, which caches only dists/
and pool/ paths and streams anything else through
(§5). An https:// URL, which most images
and checksum files are, is a CONNECT tunnel pkgproxy never sees into
(§1).
So the client names the origin URL, and pkgproxy is the HTTP(S) client to the origin. That is not the HTTPS caching §1 rules out: pkgproxy intercepts nobody’s TLS, it verifies the origin’s certificate against the system CAs as any client does, and it keeps only bytes that match the checksum the client gave.
The issue is gitlab.com/zygoon/pkgproxy#95.
24.1 The endpoint
GET /files/v1/fetch?url=<percent-encoded URL>&sha256=<hex>
GET /files/v1/fetch?url=<percent-encoded URL>&sha512=<hex>
with an optional &size=<bytes>, which a client usually knows too: from
the same data files as the checksum, or from the origin’s own listing.
The gateway routes /files to the side; /files/v1/fetch is its one
endpoint, and any other path under /files is a 404. A request is
refused with 400 when:
urlis missing, is not an absolutehttporhttpsURL with a host, or has a user name or password (authenticated origins are not supported);- neither
sha256norsha512is given, or both are, or the value is not hexadecimal of the right length; sizeis given and is not a positive number.
A request whose size is larger than -files-max-size is refused with
403 without contacting the origin (§24.4). On a hit, a size other than
the cached file’s is a 400: the checksum names that file, so the client’s
idea of it is wrong.
The checksum is required. Every image image-garden knows of has one,
published upstream (SHA256SUMS, Gentoo’s <image URL>.sha256) or pinned
in its data files, so the cache never has to decide whether a URL still
names the same bytes. A client that finds the current image with a
discovery program (Gentoo, Amazon Linux) runs it itself, fetching the
small files it reads directly, and then asks pkgproxy for the final URL
with its checksum. pkgproxy runs no discovery programs.
The response carries Content-Length, Accept-Ranges,
X-Pkgproxy-Origin-URL, the URL the bytes were downloaded from after
redirects, and X-Pkgproxy-Cache: hit from a complete entry, miss
while the file downloads, whether this request started the download or
joined it. On a hit the origin URL is that of the request that fetched
the file, which may not be this one’s; the bytes are the same.
HEAD answers from the cache only: a miss is a 404, as saying how large
the file is would mean asking the origin, and a client that wants the file
asks with GET anyway.
24.2 The cache key is the content
The entry is sha256/<hex> or sha512/<hex> in the side’s own store,
-cache-dir/files, whatever the URL. Mirrors, redirects and URLs that
move to a new file, such as Ubuntu’s current/, cannot serve stale or
wrong bytes: a new image has a new checksum, so its request misses and
fetches it. The same file at two URLs, two mirrors or a current/ URL
and its dated twin, is stored once. A file asked for once by SHA-256 and
once by SHA-512 is stored twice; that is rare enough not to matter.
An entry is immutable and never revalidated. Its metadata records the
origin URL (store.ObjectMeta.OriginURL).
24.3 Fetching
A miss joins or starts the file’s fill (internal/fill, as apt’s pool
files and LXD’s images do, §8.1):
the download runs detached from any client, every request for the file
follows it as it is written, Range requests included, so
wget --continue works, and ten CI jobs that start together download the
image once.
The fill holds back the file’s last byte until the checksum matches
(fill.Spec.HoldLastByte). A file that does not match is not kept, and
every response following it ends short, so a client that checks the
checksum, as image-garden does, or only the length, rejects it and
downloads it directly. Unlike apt’s
(§8 step 7), there is no uncached second fetch:
the checksum is all the client has to tell a good file from a bad one.
Nor is there a drain, so -fill-drain-timeout does not apply.
The size, when the client gives it, is checked as the checksum is. An
origin that announces another Content-Length is not read at all, and a
body of another length is not kept, its responses ending short. It also
lets the requests follow an origin that sends no Content-Length as it
downloads; without it, they wait for the whole file, and are answered
from the cache. The response still waits for the origin’s headers before
it starts, even with the size known (fill.Spec.Size is not used): that
keeps a fetch that fails early a 502, which a client falls back on,
rather than a 200 that ends short.
The client follows up to 10 redirects, to http and https URLs only,
which download.fedoraproject.org needs to pick a mirror. It fetches
through the offline switch’s transport, so offline mode refuses a miss
with the 503 every side gives (§21), and
serves hits. A failed fetch is a 502, a refused one a 403 (see §24.4);
the client then downloads the file directly.
24.4 Limits
fetch makes pkgproxy download a URL a client names, so it has limits:
- Hosts. Any host by default. Images come from many mirrors, and
redirectors send clients to hosts nobody can list in advance; a list
that allows nothing until it is filled in would also protect little,
as pkgproxy already tunnels
CONNECTto any host on port 443 and streams anyhttp://URL through.-files-allow-hostnarrows it to host names, or domains starting with.for every host under them. It is checked against the URL the client names, not those a redirect leads to. A host not allowed is a 403. - No private addresses. The side’s transport (
NewTransport) refuses to connect to loopback, private (RFC 1918 and unique local), link-local, unspecified, multicast, “this network” (0.0.0.0/8) and carrier-grade NAT (100.64.0.0/10) addresses, unless-files-allow-networkallows the network. The check runs in the dialer’sControlContext, on the address actually connected to, so it covers every redirect and whatever DNS answers. A refused connection is a 403. With an HTTP proxy set in the environment, it is the proxy that is connected to and checked. - Size.
-files-max-size, 8 GiB by default, refuses a file whoseContent-Lengthis larger without reading it, and cuts off one that sends more than that.
Nothing is evicted: the files cache grows, as every other side’s does,
until eviction is designed for the whole cache (#96).
24.5 Statistics
The side records each request with stats.RecordFilesServe, in a files
bucket of /pkgproxy/v1/stats, keyed <file name>@<algorithm>:<hex>:
the file name is the last element of the requested URL’s path, as Docker’s
blobs are keyed by repository. A file is content, so its hits count as a
download saved. pkgproxyctl stats shows the bucket in its total, and
the digest shortened as Docker’s.
The cache page (§20.7) shows the files
store as a kind of its own, Files by checksum: its files grouped by the
name they were served under most (stats.Recorder.FileNames), one leaf
per checksum, and those never served under a name as files of unknown
name. On the map (§20.8) a file’s
activity is found by its store key, and the live views
(§20.9) draw its requests, the cache
fetching it from the host of the URL its client named.
24.6 What is not done
- Running discovery programs, or caching the small files they read.
- Mirroring directory trees.
- Authenticated origins.
- Signature verification (GPG over
SHA256SUMS): the client verifies, and passes pkgproxy only the checksum it has verified. - A setup script or setup page section: clients opt in by URL.