Skip to content
9 changes: 6 additions & 3 deletions core/listener/egressguard/guard.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ import (
)

// Guard checks and dials outbound destinations. Construct with New; the zero
// value refuses nothing and dials nothing. Safe for concurrent use.
// value checks literal addresses but cannot resolve names — check runs with an
// empty permit list, so a loopback, private or metadata literal is refused and
// a public one is allowed, while a name fails with "no resolver configured".
// Safe for concurrent use.
type Guard struct {
// permit holds the operator-configured prefixes within the refused space.
permit []netip.Prefix
Expand Down Expand Up @@ -247,8 +250,8 @@ func (g *Guard) refusable(ip netip.Addr) bool {
}
enumerate := g.ifaceAddrs
if enumerate == nil {
// A zero-value Guard (and one built before this field existed) still
// asks the host.
// A zero-value Guard still asks the host, so the host-owned rule holds
// for one as it does for a constructed Guard.
enumerate = net.InterfaceAddrs
}
ifaces, err := enumerate()
Expand Down
19 changes: 18 additions & 1 deletion core/listener/egressguard/guard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,12 +343,29 @@ func TestCheckAddr_ZoneKeptOnTheDialedAddress(t *testing.T) {
// The same hole through the full CheckAddr path, for the ranges a zone never
// moved: a zoned loopback and link-local stay refused. Pins that stripping the
// zone for the check did not loosen anything that was already closed.
//
// The two NAT64 rows are the ones a zone DID move, and they are here because
// the stripping and the NAT64 decode are one fix rather than two:
// nat64Prefix.Contains is false for a zoned address, so before the zone was
// stripped the embedded-v4 check never ran on one. Measured against the code
// with only the decode's input reverted to the zoned address:
// [64:ff9b::7f00:1%eth0] and [64:ff9b::a9fe:a9fe%eth0] were both ALLOWED —
// loopback and the cloud metadata service, the latter never-permitted — while
// their unzoned forms were refused. Every other test in egressguard and
// forwardproxy stayed green, so without these two rows the decode's input is
// unpinned and a later edit could silently reopen it (#1539 review).
func TestCheckAddr_ZonedV6StillRefused(t *testing.T) {
g, err := New(nil)
if err != nil {
t.Fatalf("New: %v", err)
}
for _, addr := range []string{"[::1%lo0]:443", "[fe80::1%eth0]:443"} {
for _, addr := range []string{
"[::1%lo0]:443",
"[fe80::1%eth0]:443",
// NAT64 well-known prefix, zoned: judged by the IPv4 each embeds.
"[64:ff9b::7f00:1%eth0]:80", // 127.0.0.1 — loopback
"[64:ff9b::a9fe:a9fe%eth0]:80", // 169.254.169.254 — metadata, never-permitted
} {
if _, cerr := g.CheckAddr(context.Background(), "tcp", addr); cerr == nil {
t.Errorf("CheckAddr(%s) = nil, want a refusal", addr)
}
Expand Down
Loading