Compare commits
1
Commits
next
..
2d570fba94
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d570fba94 |
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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"},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user