1 Commits
Author SHA1 Message Date
clawbot 2d570fba94 Re-vendor the canonical files from sneak/prompts at 0653579 (closes #221)
check / check (push) Canceled after 0s
REPO_POLICIES.md, script/cibuild, script/docker, script/lint and
script/test are their copies at that commit. In every Dockerfile stage
the system package install and the Go module download now come before
ARG CACHEBUST, so check builds keep those layers until the base image,
the install line, go.mod or go.sum change. golangci-lint run also
mounts golangci-lint's cache. The test phase and the build stage
install their packages with apt-get and apk directly, as
REPO_POLICIES.md now has them do, instead of through
script/bootstrap --cgo. The test phase installs no recommended
packages and names libheif's AVIF encoder, libheif-plugin-aomenc,
which the tests use.

Model: opus-5-5
2026-10-19 03:08:04 +00:00
4 changed files with 53 additions and 90 deletions
+19 -26
View File
@@ -713,22 +713,16 @@ Key settings in more detail:
omitted key defaults to the RFC 1918 private ranges (`10.0.0.0/8`, omitted key defaults to the RFC 1918 private ranges (`10.0.0.0/8`,
`172.16.0.0/12`, `192.168.0.0/16`), since pixa is deployed behind a proxy on a `172.16.0.0/12`, `192.168.0.0/16`), since pixa is deployed behind a proxy on a
private network; an explicitly empty list (`[]`) trusts no one, and an private network; an explicitly empty list (`[]`) trusts no one, and an
explicit list replaces the default. An invalid CIDR aborts startup. An IPv6 explicit list replaces the default. An invalid CIDR aborts startup. Set this
address in the IPv4-mapped form (`::ffff:a.b.c.d`) is matched only as the IPv4 to the address pixa sees for requests that come through your proxy, such as
address it holds, so an IPv6 range such as `::/0` trusts neither it nor a `172.17.0.1/32`, when the defaults do not cover it, or to trust nothing else
plain IPv4 address. An entry written in that form is read as the IPv4 range it (see the login limit and `image_requests_per_minute` under Routes). For a
covers, so `::ffff:10.0.0.0/104` trusts what `10.0.0.0/8` trusts; such an proxy on the Docker host that connects to pixa over `127.0.0.1`, that address
entry shorter than `/96` covers more than IPv4 addresses and aborts startup. is the gateway of the container's Docker network (`172.17.0.1` on the default
Set this to the address pixa sees for requests that come through your proxy, bridge), not the proxy's own address; a proxy that connects through another of
such as `172.17.0.1/32`, when the defaults do not cover it, or to trust the host's addresses is seen with that address. To be sure which address it
nothing else (see the login limit and `image_requests_per_minute` under is, set this to `[]` (or `PIXA_TRUSTED_PROXIES` to empty), send a request
Routes). For a proxy on the Docker host that connects to pixa over through the proxy, and read `remoteIP` in pixa's request log line for it
`127.0.0.1`, that address is the gateway of the container's Docker network
(`172.17.0.1` on the default bridge), not the proxy's own address; a proxy
that connects through another of the host's addresses is seen with that
address. To be sure which address it is, set this to `[]` (or
`PIXA_TRUSTED_PROXIES` to empty), send a request through the proxy, and read
`remoteIP` in pixa's request log line for it
- `upstream_fetch_timeout` — time allowed for one fetch from an upstream host, - `upstream_fetch_timeout` — time allowed for one fetch from an upstream host,
as a duration such as `30s` (the default) or `2m` as a duration such as `30s` (the default) or `2m`
- `upstream_max_response_size` — largest upstream response accepted, in bytes; - `upstream_max_response_size` — largest upstream response accepted, in bytes;
@@ -846,16 +840,15 @@ cache is kept from build to build: the steps that compile mount it, so a package
whose inputs are unchanged is not compiled again, and both `go test` runs pass whose inputs are unchanged is not compiled again, and both `go test` runs pass
`-count=1` so that no stored test result is replayed. The lint step also mounts `-count=1` so that no stored test result is replayed. The lint step also mounts
golangci-lint's cache, which reuses a package's findings only when its files, golangci-lint's cache, which reuses a package's findings only when its files,
those of the packages it imports, `go.mod`, the linters and their settings in those of the packages it imports, `go.mod`, `.golangci.yml` and the
`.golangci.yml`, and the golangci-lint version are all unchanged. The `test` golangci-lint version are all unchanged. The `test` phase runs on the Debian Go
phase runs on the Debian Go image, with Debian's libvips and libheif; the image image, with Debian's libvips and libheif; the image pixa ships is built and runs
pixa ships is built and runs on Alpine. `script/check`, `script/cibuild`, on Alpine. `script/check`, `script/cibuild`, `script/docker`, `script/lint`,
`script/docker`, `script/lint`, `script/test`, `script/setup` and `script/test`, `script/setup` and `script/install-precommit` are the standard
`script/install-precommit` are the standard copies from `sneak/prompts`, kept copies from `sneak/prompts`, kept identical to them. `script/fmt` and
identical to them. `script/fmt` and `script/fmt-check` are the standard copies `script/fmt-check` are the standard copies with pixa's `gofmt` step kept before
with pixa's `gofmt` step kept before prettier. prettier formats the markdown prettier. prettier formats the markdown only, not the HTML templates, as it
only, not the HTML templates, as it cannot parse a Go template action inside a cannot parse a Go template action inside a tag.
tag.
## Load Test ## Load Test
-4
View File
@@ -34,10 +34,6 @@ P2: security: per-origin rate limiting
each `Dockerfile` stage installs its system packages and downloads the Go each `Dockerfile` stage installs its system packages and downloads the Go
modules above `ARG CACHEBUST`, so check builds keep them, and the lint step modules above `ARG CACHEBUST`, so check builds keep them, and the lint step
keeps golangci-lint's cache. keeps golangci-lint's cache.
- 2026-10-19 a `trusted_proxies` entry in the IPv4-mapped form trusted nothing;
it is now read as the IPv4 range it covers by the same code that reads
`blocked_networks`, and such an entry shorter than `/96` aborts startup
(closes #303).
- 2026-10-18 the next reconciliation pass waits at least ten times as long as - 2026-10-18 the next reconciliation pass waits at least ten times as long as
the last one took, so passes on a large cache no longer run back to back the last one took, so passes on a large cache no longer run back to back
(closes #250). (closes #250).
+34 -22
View File
@@ -272,7 +272,7 @@ func newFromSmartConfig(sc *smartconfig.Config) (*Config, error) {
return nil, err return nil, err
} }
blockedNetworks, err := parseCIDRList(sc, keyBlockedNetworks) blockedNetworks, err := parseBlockedNetworks(sc)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1249,13 +1249,6 @@ func defaultTrustedProxies() []netip.Prefix {
// wrong type, an empty entry, a non-string entry, or an unparseable CIDR // wrong type, an empty entry, a non-string entry, or an unparseable CIDR
// aborts startup naming the key and the offending value; the default // aborts startup naming the key and the offending value; the default
// (an empty list) applies only to an omitted key. // (an empty list) applies only to an omitted key.
//
// An entry written in the IPv4-mapped form (::ffff:a.b.c.d) is kept as the
// IPv4 range it covers: ::ffff:10.0.0.0/104 is kept as 10.0.0.0/8. Both
// blocked_networks and trusted_proxies are matched against an IPv4-mapped
// address as the IPv4 address it holds, so the entry as written would match
// nothing. Such an entry shorter than /96 covers more than the IPv4
// addresses and aborts startup.
func parseCIDRList(sc *smartconfig.Config, key string) ([]netip.Prefix, error) { func parseCIDRList(sc *smartconfig.Config, key string) ([]netip.Prefix, error) {
raw, ok := lookupValue(sc, key) raw, ok := lookupValue(sc, key)
if !ok { if !ok {
@@ -1271,10 +1264,6 @@ func parseCIDRList(sc *smartconfig.Config, key string) ([]netip.Prefix, error) {
return nil, err return nil, err
} }
// An IPv4-mapped address is the 96 bits of ::ffff:0:0/96 followed by
// the 32 bits of the IPv4 address it holds.
const mappedPrefixBits = 96
prefixes := make([]netip.Prefix, 0, len(entries)) prefixes := make([]netip.Prefix, 0, len(entries))
for _, entry := range entries { for _, entry := range entries {
@@ -1284,22 +1273,45 @@ func parseCIDRList(sc *smartconfig.Config, key string) ([]netip.Prefix, error) {
settingName(key), entry, errNotAValidCIDR) settingName(key), entry, errNotAValidCIDR)
} }
if prefix.Addr().Is4In6() && prefix.Bits() < mappedPrefixBits {
return nil, fmt.Errorf("%s: value %q is %w",
settingName(key), entry, errMappedTooShort)
}
if prefix.Addr().Is4In6() {
prefix = netip.PrefixFrom(prefix.Addr().Unmap(),
prefix.Bits()-mappedPrefixBits)
}
prefixes = append(prefixes, prefix) prefixes = append(prefixes, prefix)
} }
return prefixes, nil return prefixes, nil
} }
// parseBlockedNetworks parses blocked_networks with parseCIDRList, and keeps
// an entry written in the IPv4-mapped form (::ffff:a.b.c.d) as the IPv4
// range it covers: ::ffff:10.0.0.0/104 is kept as 10.0.0.0/8. The fetcher
// matches an IPv4-mapped address as the IPv4 address it holds, so the entry
// as written would match nothing. Such an entry shorter than /96 covers more
// than the IPv4 addresses and aborts startup.
func parseBlockedNetworks(sc *smartconfig.Config) ([]netip.Prefix, error) {
prefixes, err := parseCIDRList(sc, keyBlockedNetworks)
if err != nil {
return nil, err
}
// An IPv4-mapped address is the 96 bits of ::ffff:0:0/96 followed by
// the 32 bits of the IPv4 address it holds.
const mappedPrefixBits = 96
for i, prefix := range prefixes {
if !prefix.Addr().Is4In6() {
continue
}
if prefix.Bits() < mappedPrefixBits {
return nil, fmt.Errorf("%s: value %q is %w",
settingName(keyBlockedNetworks), prefix, errMappedTooShort)
}
prefixes[i] = netip.PrefixFrom(prefix.Addr().Unmap(),
prefix.Bits()-mappedPrefixBits)
}
return prefixes, nil
}
// parseHostList parses the value of the named config key into host patterns, // parseHostList parses the value of the named config key into host patterns,
// or returns nil if the key is omitted. It accepts a YAML list of strings or a // or returns nil if the key is omitted. It accepts a YAML list of strings or a
// comma-separated string. An explicitly null value, a wrong type, an empty // comma-separated string. An explicitly null value, a wrong type, an empty
@@ -83,41 +83,3 @@ func TestTrustedProxiesInvalidAbortsStartup(t *testing.T) {
}, },
}) })
} }
// TestTrustedProxiesMappedEntryIsIPv4Range checks that an entry written in
// the IPv4-mapped form (::ffff:a.b.c.d) is kept as the IPv4 range it covers,
// since the client address is matched as the IPv4 address it holds. /96, the
// shortest such entry, covers every IPv4 address.
func TestTrustedProxiesMappedEntryIsIPv4Range(t *testing.T) {
t.Parallel()
c, err := configFromYAML(t, signingKeyLine+
`trusted_proxies: ["::ffff:10.0.0.0/104", "::ffff:0:0/96"]`+"\n")
if err != nil {
t.Fatalf("IPv4-mapped trusted_proxies entries should load: %v", err)
}
got := make([]string, len(c.TrustedProxies))
for i, p := range c.TrustedProxies {
got[i] = p.String()
}
if joined := strings.Join(got, ","); joined != "10.0.0.0/8,0.0.0.0/0" {
t.Errorf("TrustedProxies = %v, want 10.0.0.0/8 and 0.0.0.0/0", got)
}
}
// TestTrustedProxiesMappedEntryShorterThan96AbortsStartup checks that an
// entry in the IPv4-mapped form shorter than /96, which covers more than the
// IPv4 addresses, aborts startup with an error naming the key and the value.
func TestTrustedProxiesMappedEntryShorterThan96AbortsStartup(t *testing.T) {
t.Parallel()
runAbortCases(t, []abortCase{
{
name: "mapped-95",
yaml: signingKeyLine + `trusted_proxies: ["::ffff:0:0/95"]` + "\n",
wantErrSubstrings: []string{keyTrustedProxies, "::ffff:0:0/95"},
},
})
}