Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e9949afbc | ||
|
|
294474a2cd | ||
|
|
04d9a6bf37 |
@@ -199,9 +199,7 @@ into place, so a crash leaves no file under its own name with part of its data.
|
|||||||
When pixa starts, and every five minutes after, it removes each cached source or
|
When pixa starts, and every five minutes after, it removes each cached source or
|
||||||
transformed image whose file is empty, or not the size the database records for
|
transformed image whose file is empty, or not the size the database records for
|
||||||
it, with a warning naming the file; the next request for it fetches the source
|
it, with a warning naming the file; the next request for it fetches the source
|
||||||
or converts the image again. Looking for those files reads the database once for
|
or converts the image again.
|
||||||
every cached file, so after a look that took longer than 30 seconds, pixa waits
|
|
||||||
ten times as long as it took before looking again.
|
|
||||||
|
|
||||||
pixa's target is 1-5k r/s, which has not been measured at that rate (see Load
|
pixa's target is 1-5k r/s, which has not been measured at that rate (see Load
|
||||||
Test). Toward it, pixa keeps in memory the content types of the 10,000
|
Test). Toward it, pixa keeps in memory the content types of the 10,000
|
||||||
@@ -696,10 +694,7 @@ Key settings in more detail:
|
|||||||
and generator pages. Default: empty
|
and generator pages. Default: empty
|
||||||
- `blocked_networks` — list of CIDR ranges to refuse for SSRF protection, added
|
- `blocked_networks` — list of CIDR ranges to refuse for SSRF protection, added
|
||||||
to the always-enforced built-in ranges (loopback, private, link-local, CGNAT,
|
to the always-enforced built-in ranges (loopback, private, link-local, CGNAT,
|
||||||
benchmark, NAT64, and the like); an invalid CIDR aborts startup. An IPv6
|
benchmark, NAT64, and the like); an invalid CIDR aborts startup
|
||||||
address in the IPv4-mapped form (`::ffff:a.b.c.d`) is matched only as the IPv4
|
|
||||||
address it holds, so an IPv6 range such as `::/0` refuses neither it nor a
|
|
||||||
plain IPv4 address
|
|
||||||
- `trusted_proxies` — list of CIDR ranges of the reverse proxies in front of
|
- `trusted_proxies` — list of CIDR ranges of the reverse proxies in front of
|
||||||
pixa. `X-Forwarded-For` is believed only when the direct peer falls inside one
|
pixa. `X-Forwarded-For` is believed only when the direct peer falls inside one
|
||||||
of these ranges; the logged and login-recorded client address is then the
|
of these ranges; the logged and login-recorded client address is then the
|
||||||
|
|||||||
@@ -30,20 +30,15 @@ P2: security: per-origin rate limiting
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
- 2026-10-18 the next reconciliation pass waits ten times as long as the last
|
|
||||||
one took, so passes on a large cache no longer run back to back (closes #250).
|
|
||||||
- 2026-10-16 P1 security bug: the check that keeps pixa from fetching internal
|
- 2026-10-16 P1 security bug: the check that keeps pixa from fetching internal
|
||||||
addresses refuses the local-use NAT64 range (`64:ff9b:1::/48`, RFC 8215) as it
|
addresses refuses the local-use NAT64 range (`64:ff9b:1::/48`, RFC 8215) as it
|
||||||
refuses `64:ff9b::/96`, and refuses an IPv6 address in the IPv4-compatible
|
refuses `64:ff9b::/96`, and matches an IPv6 address in the IPv4-compatible
|
||||||
form (`::/96`) when `blocked_networks` contains either the address as written
|
form (`::/96`) against `blocked_networks` by the IPv4 address it holds, as it
|
||||||
or the IPv4 address it holds (closes #299). A source host such as
|
already matched the mapped form (`::ffff:a.b.c.d`) (closes #299). A source
|
||||||
`[64:ff9b:1::a00:1]` was fetched, which on a network that runs NAT64 on that
|
host such as `[64:ff9b:1::a00:1]` was fetched, which on a network that runs
|
||||||
range reaches the IPv4 address it holds, here `10.0.0.1`; and with
|
NAT64 on that range reaches the IPv4 address it holds, here `10.0.0.1`; and
|
||||||
`203.0.113.0/24` in `blocked_networks`, `[::ffff:203.0.113.5]` was refused but
|
with `203.0.113.0/24` in `blocked_networks`, `[::ffff:203.0.113.5]` was
|
||||||
`[::203.0.113.5]` was fetched. Both now answer 403. The mapped form
|
refused but `[::203.0.113.5]` was fetched. Both now answer 403.
|
||||||
(`::ffff:a.b.c.d`) is still matched only as the IPv4 address it holds: Go
|
|
||||||
keeps a plain IPv4 address in that form, and an IPv6 entry such as `::/0` must
|
|
||||||
not refuse every IPv4 address.
|
|
||||||
- 2026-10-16 P1 security bug: the check that keeps pixa from fetching internal
|
- 2026-10-16 P1 security bug: the check that keeps pixa from fetching internal
|
||||||
addresses judges an IPv6 address in the IPv4-compatible form (`::/96`), such
|
addresses judges an IPv6 address in the IPv4-compatible form (`::/96`), such
|
||||||
as `::127.0.0.1` or `::10.0.0.1`, by the IPv4 address in its last four bytes
|
as `::127.0.0.1` or `::10.0.0.1`, by the IPv4 address in its last four bytes
|
||||||
|
|||||||
@@ -599,31 +599,23 @@ func isPrivateIP(ip net.IP) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// isBlockedIP reports whether ip is refused, either by the built-in
|
// isBlockedIP reports whether ip is refused, either by the built-in
|
||||||
// internal-range check or by one of the operator-supplied prefixes. An
|
// internal-range check or by one of the operator-supplied prefixes.
|
||||||
// IPv4-compatible address (in ::/96) is refused when a prefix contains the
|
|
||||||
// address as written or the IPv4 address it holds. An IPv4-mapped address (in
|
|
||||||
// ::ffff:0:0/96) is matched only as the IPv4 address it holds: Go keeps a
|
|
||||||
// plain IPv4 address in that form too, and an IPv6 prefix such as ::/0 must
|
|
||||||
// not refuse every IPv4 address.
|
|
||||||
func isBlockedIP(ip net.IP, blocked []netip.Prefix) bool {
|
func isBlockedIP(ip net.IP, blocked []netip.Prefix) bool {
|
||||||
if isPrivateIP(ip) {
|
if isPrivateIP(ip) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
addr, ok := netip.AddrFromSlice(ip)
|
// An IPv4-compatible or IPv4-mapped IPv6 address is matched as the IPv4
|
||||||
|
// address it holds
|
||||||
|
addr, ok := netip.AddrFromSlice(ipv4CompatibleToIPv4(ip))
|
||||||
if !ok {
|
if !ok {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
addr = addr.Unmap()
|
addr = addr.Unmap()
|
||||||
|
|
||||||
// ip read as IPv4 where it is or holds an IPv4 address, otherwise the
|
|
||||||
// zero netip.Addr, which no prefix contains. Matching asIPv4 as well as
|
|
||||||
// addr refuses more only for an IPv4-compatible address.
|
|
||||||
asIPv4, _ := netip.AddrFromSlice(ipv4CompatibleToIPv4(ip).To4())
|
|
||||||
|
|
||||||
return slices.ContainsFunc(blocked, func(prefix netip.Prefix) bool {
|
return slices.ContainsFunc(blocked, func(prefix netip.Prefix) bool {
|
||||||
return prefix.Contains(addr) || prefix.Contains(asIPv4)
|
return prefix.Contains(addr)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,14 +18,6 @@ import (
|
|||||||
// write-pressure wakeups triggered by stores.
|
// write-pressure wakeups triggered by stores.
|
||||||
const DefaultEvictionInterval = 5 * time.Minute
|
const DefaultEvictionInterval = 5 * time.Minute
|
||||||
|
|
||||||
// reconciliationWaitFactor is how many times as long as a
|
|
||||||
// reconciliation pass took the eviction loop waits, after that pass
|
|
||||||
// ends, before it starts the next one. A pass reads a row and stats a
|
|
||||||
// file for every cached file, so its time grows with the cache; waiting
|
|
||||||
// ten times as long keeps reconciliation to at most a tenth of the time,
|
|
||||||
// however large the cache is.
|
|
||||||
const reconciliationWaitFactor = 10
|
|
||||||
|
|
||||||
// evictionBatchSize is how many LRU candidates of each class (variants
|
// evictionBatchSize is how many LRU candidates of each class (variants
|
||||||
// and source blobs) one eviction pass fetches from the database.
|
// and source blobs) one eviction pass fetches from the database.
|
||||||
const evictionBatchSize = 100
|
const evictionBatchSize = 100
|
||||||
@@ -446,12 +438,10 @@ func (c *Cache) notifyWritePressure() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// StartEviction launches the background eviction goroutine, which
|
// StartEviction launches the background eviction goroutine, which
|
||||||
|
// reconciles the database accounting with the cache directories and
|
||||||
// removes the expired negative cache rows at startup and again on every
|
// removes the expired negative cache rows at startup and again on every
|
||||||
// periodic tick thereafter, reconciles the database accounting with the
|
// periodic tick thereafter, and evicts to the configured limit on the
|
||||||
// cache directories at startup and again on the first periodic tick
|
// given periodic interval and on write-pressure notifications. On a
|
||||||
// that comes ten times as long after a reconciliation pass ended as
|
|
||||||
// that pass took, and evicts to the configured limit on the given
|
|
||||||
// periodic interval and on write-pressure notifications. On a
|
|
||||||
// disabled cache it only removes the expired negative cache rows, as
|
// disabled cache it only removes the expired negative cache rows, as
|
||||||
// the negative cache is used either way. It is a no-op when already
|
// the negative cache is used either way. It is a no-op when already
|
||||||
// started. The goroutine outlives the caller, so it runs with its own
|
// started. The goroutine outlives the caller, so it runs with its own
|
||||||
@@ -492,7 +482,7 @@ func (c *Cache) StopEviction(ctx context.Context) error {
|
|||||||
func (c *Cache) evictionLoop(ctx context.Context, interval time.Duration) {
|
func (c *Cache) evictionLoop(ctx context.Context, interval time.Duration) {
|
||||||
defer close(c.evictionDone)
|
defer close(c.evictionDone)
|
||||||
|
|
||||||
nextReconciliation := c.runReconciliationPass(ctx)
|
c.runReconciliationPass(ctx)
|
||||||
c.runCleanExpired(ctx)
|
c.runCleanExpired(ctx)
|
||||||
c.runEvictionPass(ctx)
|
c.runEvictionPass(ctx)
|
||||||
|
|
||||||
@@ -504,20 +494,16 @@ func (c *Cache) evictionLoop(ctx context.Context, interval time.Duration) {
|
|||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
// Reconciliation walks the cache directories and reads a
|
// Reconciliation walks the cache directories, so it only
|
||||||
// row for every file, so it runs only on the periodic
|
// runs on the periodic ticker rather than on every
|
||||||
// ticker rather than on every write-pressure wakeup, and
|
// write-pressure wakeup, keeping it off the per-store hot
|
||||||
// not before nextReconciliation, which is ten times as long
|
// path. Reusing the eviction interval itself (rather than a
|
||||||
// after the last pass ended as that pass took. Passes then
|
// separate, longer one) is a deliberate choice: it is the
|
||||||
// never run back to back, however long one takes. A file
|
// simplest option that still bounds how long a store's
|
||||||
// whose best-effort accounting insert failed in a store
|
// best-effort accounting insert can stay silently
|
||||||
// stays unaccounted for until the next pass, for about the
|
// unaccounted for to one interval, on a process that is
|
||||||
// longer of one eviction interval and ten times the last
|
// already running this loop regardless.
|
||||||
// pass's time.
|
c.runReconciliationPass(ctx)
|
||||||
if !time.Now().Before(nextReconciliation) {
|
|
||||||
nextReconciliation = c.runReconciliationPass(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.runCleanExpired(ctx)
|
c.runCleanExpired(ctx)
|
||||||
case <-c.evictionPressure:
|
case <-c.evictionPressure:
|
||||||
}
|
}
|
||||||
@@ -541,25 +527,17 @@ func (c *Cache) runEvictionPass(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// runReconciliationPass runs one reconciliation pass, logging failures
|
// runReconciliationPass runs one reconciliation pass, logging failures
|
||||||
// instead of propagating them (the loop must keep running), and returns
|
// instead of propagating them (the loop must keep running). It does
|
||||||
// the earliest time the next pass may start: reconciliationWaitFactor
|
// nothing once ctx is cancelled.
|
||||||
// times as long after this pass ended as it took. It does nothing once
|
func (c *Cache) runReconciliationPass(ctx context.Context) {
|
||||||
// ctx is cancelled.
|
|
||||||
func (c *Cache) runReconciliationPass(ctx context.Context) time.Time {
|
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return time.Now()
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
started := time.Now()
|
|
||||||
|
|
||||||
err := c.reconcileAccounting(ctx)
|
err := c.reconcileAccounting(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.log.Warn("cache accounting reconciliation failed", "error", err)
|
c.log.Warn("cache accounting reconciliation failed", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ended := time.Now()
|
|
||||||
|
|
||||||
return ended.Add(reconciliationWaitFactor * ended.Sub(started))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// runCleanExpired runs CleanExpired, logging a failure instead of
|
// runCleanExpired runs CleanExpired, logging a failure instead of
|
||||||
@@ -578,8 +556,7 @@ func (c *Cache) runCleanExpired(ctx context.Context) {
|
|||||||
|
|
||||||
// reconcileAccounting synchronizes the database size accounting with
|
// reconcileAccounting synchronizes the database size accounting with
|
||||||
// the actual contents of the cache directories. It runs at startup and
|
// the actual contents of the cache directories. It runs at startup and
|
||||||
// again on later periodic eviction ticks, each no sooner than ten times
|
// again on every periodic eviction tick thereafter, off the request
|
||||||
// as long after the last pass ended as that pass took, off the request
|
|
||||||
// hot path: it adopts variant files that predate the accounting table
|
// hot path: it adopts variant files that predate the accounting table
|
||||||
// (or whose accounting insert failed, e.g. StoreVariant's best-effort
|
// (or whose accounting insert failed, e.g. StoreVariant's best-effort
|
||||||
// insert under transient DB contention), drops accounting rows whose
|
// insert under transient DB contention), drops accounting rows whose
|
||||||
@@ -589,10 +566,9 @@ func (c *Cache) runCleanExpired(ctx context.Context) {
|
|||||||
// files left behind by crashed writes, and last checks the total cache
|
// files left behind by crashed writes, and last checks the total cache
|
||||||
// usage against the tables. It reads the tables a page at a time.
|
// usage against the tables. It reads the tables a page at a time.
|
||||||
// Running it periodically, not just once, bounds how long such drift can
|
// Running it periodically, not just once, bounds how long such drift can
|
||||||
// accumulate unaccounted for on a long-running process to about the
|
// accumulate unaccounted for on a long-running process to one eviction
|
||||||
// longer of one eviction interval and ten times the last pass's time.
|
// interval. Once ctx is cancelled, it stops at the next file or row and
|
||||||
// Once ctx is cancelled, it stops at the next file or row and returns
|
// returns ctx's error.
|
||||||
// ctx's error.
|
|
||||||
func (c *Cache) reconcileAccounting(ctx context.Context) error {
|
func (c *Cache) reconcileAccounting(ctx context.Context) error {
|
||||||
if c.disabled {
|
if c.disabled {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -918,89 +918,6 @@ func TestPeriodicReconciliationAdoptsFileThatAppearsAfterStartup(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestReconciliationPassesAreSpacedByTenTimesTheirTime makes the hook
|
|
||||||
// called after every read of the reconciliation pass sleep 10 ms, runs
|
|
||||||
// the eviction loop on a 10 ms interval, and records when each of those
|
|
||||||
// calls starts and ends. A pass starts before its first call and ends
|
|
||||||
// after its last, so the time from the first call's start to the last
|
|
||||||
// call's end is at most the pass's time. The next pass's first call must
|
|
||||||
// start at least ten times that long after the last call ended.
|
|
||||||
func TestReconciliationPassesAreSpacedByTenTimesTheirTime(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
cache, _ := newEvictionTestCache(t, 1<<30)
|
|
||||||
|
|
||||||
// A pass made here, before the loop starts, counts the reads one
|
|
||||||
// pass makes on this empty cache, so the reads recorded below can be
|
|
||||||
// split into passes.
|
|
||||||
readsPerPass := 0
|
|
||||||
cache.reconciliationReadTestHook = func(int) { readsPerPass++ }
|
|
||||||
|
|
||||||
err := cache.reconcileAccounting(t.Context())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("reconcileAccounting failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("one reconciliation pass makes %d reads", readsPerPass)
|
|
||||||
|
|
||||||
if readsPerPass == 0 {
|
|
||||||
t.Fatal("a reconciliation pass made no read")
|
|
||||||
}
|
|
||||||
|
|
||||||
// The pass at startup and two after it.
|
|
||||||
const passes = 3
|
|
||||||
|
|
||||||
type read struct{ start, end time.Time }
|
|
||||||
|
|
||||||
// The channel holds every read the test looks at, so none of those
|
|
||||||
// is dropped; reads after them are, and never block the loop.
|
|
||||||
reads := make(chan read, passes*readsPerPass)
|
|
||||||
|
|
||||||
cache.reconciliationReadTestHook = func(int) {
|
|
||||||
start := time.Now()
|
|
||||||
|
|
||||||
time.Sleep(10 * time.Millisecond)
|
|
||||||
|
|
||||||
select {
|
|
||||||
case reads <- read{start: start, end: time.Now()}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cache.StartEviction(10 * time.Millisecond)
|
|
||||||
defer func() { _ = cache.StopEviction(t.Context()) }()
|
|
||||||
|
|
||||||
recorded := make([]read, 0, passes*readsPerPass)
|
|
||||||
timeout := time.After(15 * time.Second)
|
|
||||||
|
|
||||||
for len(recorded) < passes*readsPerPass {
|
|
||||||
select {
|
|
||||||
case r := <-reads:
|
|
||||||
recorded = append(recorded, r)
|
|
||||||
case <-timeout:
|
|
||||||
t.Fatalf("saw %d reconciliation reads in 15 s, want %d (%d passes)",
|
|
||||||
len(recorded), passes*readsPerPass, passes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for pass := range passes - 1 {
|
|
||||||
first := recorded[pass*readsPerPass]
|
|
||||||
last := recorded[(pass+1)*readsPerPass-1]
|
|
||||||
next := recorded[(pass+1)*readsPerPass]
|
|
||||||
|
|
||||||
passTime := last.end.Sub(first.start)
|
|
||||||
wait := next.start.Sub(last.end)
|
|
||||||
|
|
||||||
t.Logf("pass %d took at least %v, and the next pass started %v after it",
|
|
||||||
pass+1, passTime, wait)
|
|
||||||
|
|
||||||
if wait < 10*passTime {
|
|
||||||
t.Errorf("pass %d took at least %v, and the next pass started %v "+
|
|
||||||
"after it, want at least %v", pass+1, passTime, wait, 10*passTime)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestStopEvictionInterruptsPassInProgress holds the test database's
|
// TestStopEvictionInterruptsPassInProgress holds the test database's
|
||||||
// only connection, so the startup reconciliation pass waits for it, and
|
// only connection, so the startup reconciliation pass waits for it, and
|
||||||
// checks that StopEviction stops that pass instead of waiting for the
|
// checks that StopEviction stops that pass instead of waiting for the
|
||||||
|
|||||||
@@ -589,26 +589,19 @@ func TestImageRoutesRefuseIPv6SourceHostHoldingInternalIPv4Address(t *testing.T)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TestImageRoutesCheckIPv4AddressInIPv6SourceHost requests ipv6SourcePath on
|
// TestImageRoutesCheckIPv4AddressInIPv6SourceHost requests ipv6SourcePath on
|
||||||
// source hosts that hold an IPv4 address, through a /v1/image/ URL with a
|
// IPv6 source hosts that hold an IPv4 address, through a /v1/image/ URL with a
|
||||||
// signature and through an encrypted URL, with the fetcher pixad uses.
|
// signature and through an encrypted URL, with the fetcher pixad uses.
|
||||||
// [64:ff9b:1::a00:1], in the local-use NAT64 range (64:ff9b:1::/48, RFC 8215),
|
// [64:ff9b:1::a00:1], in the local-use NAT64 range (64:ff9b:1::/48, RFC 8215),
|
||||||
// answers 403. [::203.0.113.5] holds the public address 203.0.113.5 in the
|
// answers 403. [::203.0.113.5] and [::ffff:203.0.113.5] hold a public IPv4
|
||||||
// IPv4-compatible form (::/96) and answers 403 when blocked_networks lists
|
// address in the IPv4-compatible form (::/96) and in the IPv4-mapped form
|
||||||
// 203.0.113.0/24 or ::/96. [::ffff:203.0.113.5] holds it in the IPv4-mapped
|
// (::ffff:0:0/96). Each answers 403 when blocked_networks lists
|
||||||
// form (::ffff:0:0/96) and answers 403 when blocked_networks lists
|
// 203.0.113.0/24, and when it lists the range of its own form. When
|
||||||
// 203.0.113.0/24. Go keeps the plain address 203.0.113.5 in the mapped form
|
// blocked_networks is empty [::203.0.113.5] is not refused: the fetcher tries
|
||||||
// too, and an IPv6 range must not refuse it: with ::/0 listed it is not
|
// to connect to it, finds nothing there, and the request fails with another
|
||||||
// refused. Neither is [::203.0.113.5] with blocked_networks empty. Where a
|
// status.
|
||||||
// host is not refused, the fetcher tries to connect to it, finds nothing
|
|
||||||
// there, and the request fails with another status.
|
|
||||||
func TestImageRoutesCheckIPv4AddressInIPv6SourceHost(t *testing.T) {
|
func TestImageRoutesCheckIPv4AddressInIPv6SourceHost(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const (
|
|
||||||
ipv4CompatibleHost = "[::203.0.113.5]"
|
|
||||||
ipv4MappedHost = "[::ffff:203.0.113.5]"
|
|
||||||
)
|
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
sourceHost string
|
sourceHost string
|
||||||
@@ -617,19 +610,19 @@ func TestImageRoutesCheckIPv4AddressInIPv6SourceHost(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{"local-use NAT64 form of 10.0.0.1", "[64:ff9b:1::a00:1]", nil, true},
|
{"local-use NAT64 form of 10.0.0.1", "[64:ff9b:1::a00:1]", nil, true},
|
||||||
{"IPv4-compatible form of an address in blocked_networks",
|
{"IPv4-compatible form of an address in blocked_networks",
|
||||||
ipv4CompatibleHost,
|
"[::203.0.113.5]",
|
||||||
[]netip.Prefix{netip.MustParsePrefix("203.0.113.0/24")}, true},
|
[]netip.Prefix{netip.MustParsePrefix("203.0.113.0/24")}, true},
|
||||||
{"IPv4-compatible form with ::/96 in blocked_networks",
|
{"IPv4-compatible form with ::/96 in blocked_networks",
|
||||||
ipv4CompatibleHost,
|
"[::203.0.113.5]",
|
||||||
[]netip.Prefix{netip.MustParsePrefix("::/96")}, true},
|
[]netip.Prefix{netip.MustParsePrefix("::/96")}, true},
|
||||||
{"IPv4-mapped form of an address in blocked_networks",
|
{"IPv4-mapped form of an address in blocked_networks",
|
||||||
ipv4MappedHost,
|
"[::ffff:203.0.113.5]",
|
||||||
[]netip.Prefix{netip.MustParsePrefix("203.0.113.0/24")}, true},
|
[]netip.Prefix{netip.MustParsePrefix("203.0.113.0/24")}, true},
|
||||||
{"plain IPv4 address with ::/0 in blocked_networks",
|
{"IPv4-mapped form with ::ffff:0:0/96 in blocked_networks",
|
||||||
"203.0.113.5",
|
"[::ffff:203.0.113.5]",
|
||||||
[]netip.Prefix{netip.MustParsePrefix("::/0")}, false},
|
[]netip.Prefix{netip.MustParsePrefix("::ffff:0:0/96")}, true},
|
||||||
{"IPv4-compatible form of an address with blocked_networks empty",
|
{"IPv4-compatible form of an address with blocked_networks empty",
|
||||||
ipv4CompatibleHost, nil, false},
|
"[::203.0.113.5]", nil, false},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
|||||||
Reference in New Issue
Block a user