Skip to content

Commit 30e72f5

Browse files
committed
Filter unreachable beacon addresses from registry-discovered list
Production registries return ALL registered beacons including those running on a private VPC (e.g. GCP 10.128.0.x). When pickBeacon's hash lands on one of those, every relay packet vanishes — off-VPC daemons cannot reach RFC1918 addresses. Symptom is a tunnel that establishes fine but where new dials hang and existing connections die at the keepalive interval, requiring a daemon restart to resolve (until the next refresh tick re-poisons the cache). filterUnreachable drops RFC1918 / loopback / link-local / unspecified literals from the discovered list before merging with bootstrap. The operator-configured bootstrap list is preserved verbatim — intra-VPC deployments can still pin a private beacon there. DNS hostnames are kept since they may resolve to public addresses. Also fix pilotctl ping conn.Read timeout: the dial was guarded by perAttempt but the echo read was not, so a relay-degraded peer caused ping to hang indefinitely instead of reporting timeout.
1 parent 134074a commit 30e72f5

4 files changed

Lines changed: 96 additions & 1 deletion

File tree

cmd/pilotctl/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4342,6 +4342,7 @@ func cmdPing(args []string) {
43424342
payload := fmt.Sprintf("ping-%d", i)
43434343
conn.Write([]byte(payload))
43444344

4345+
conn.SetReadDeadline(time.Now().Add(perAttempt))
43454346
buf := make([]byte, 1024)
43464347
n, err := conn.Read(buf)
43474348
conn.Close()

pkg/daemon/beacon_discovery.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,13 @@ func fetchBeaconList(client beaconLister) ([]string, error) {
6262
out = append(out, addr)
6363
}
6464
}
65+
// Drop addresses that an off-VPC client cannot reach (RFC1918,
66+
// loopback, link-local). The registry returns whatever beacons have
67+
// registered, including ones running on a private VPC; if our hash
68+
// lands on one of those, all relay traffic vanishes (silent black-
69+
// hole). Bootstrap entries are NOT filtered — operators on the same
70+
// VPC can still pin a private beacon there.
71+
out = filterUnreachable(out)
6572
// Sort to give a deterministic order even when the registry's map
6673
// iteration produces a different order each call. mergeBeaconLists
6774
// dedupes against bootstrap, but stable order keeps the hash-pick
@@ -130,7 +137,10 @@ func loadBeaconCache(identityPath string) ([]string, error) {
130137
if err := json.Unmarshal(data, &entry); err != nil {
131138
return nil, fmt.Errorf("parse beacon cache: %w", err)
132139
}
133-
return entry.Addrs, nil
140+
// Filter unreachable addresses from disk too — a previous daemon may
141+
// have persisted a list that included private VPC IPs before this
142+
// fix was in place.
143+
return filterUnreachable(entry.Addrs), nil
134144
}
135145

136146
// beaconSelectionState tracks the daemon's beacon picks across refresh

pkg/daemon/beacon_select.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,45 @@ package daemon
44

55
import (
66
"hash/fnv"
7+
"net"
78
"strings"
89
)
910

11+
// isUnreachableBeaconHost reports whether host is an RFC1918 / loopback /
12+
// link-local / unspecified address — an address that a public-internet
13+
// daemon cannot reach. The registry's beacon_list endpoint can return
14+
// internal addresses for beacons running on the same VPC (e.g. GCP
15+
// 10.128.0.0/16); those are useless to off-VPC daemons and, if picked,
16+
// silently black-hole all relay traffic. Only IP literals are checked
17+
// — DNS hostnames are kept (they may resolve to public addresses).
18+
func isUnreachableBeaconHost(host string) bool {
19+
ip := net.ParseIP(host)
20+
if ip == nil {
21+
return false
22+
}
23+
return ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsUnspecified()
24+
}
25+
26+
// filterUnreachable removes addresses whose host part is an RFC1918 /
27+
// loopback / link-local literal. Used on the DISCOVERED list before
28+
// merging with bootstrap — the operator's bootstrap list is preserved
29+
// verbatim (intra-VPC deployments can still pin a private beacon there).
30+
func filterUnreachable(addrs []string) []string {
31+
out := make([]string, 0, len(addrs))
32+
for _, a := range addrs {
33+
host, _, err := net.SplitHostPort(a)
34+
if err != nil {
35+
// Fall back to the whole string if there's no port.
36+
host = a
37+
}
38+
if isUnreachableBeaconHost(host) {
39+
continue
40+
}
41+
out = append(out, a)
42+
}
43+
return out
44+
}
45+
1046
// Multi-beacon support
1147
//
1248
// A daemon may be configured with EITHER:

pkg/daemon/beacon_select_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,3 +146,51 @@ func TestPickBeaconStableAcrossSeparateListInstances(t *testing.T) {
146146
}
147147
}
148148
}
149+
150+
// TestFilterUnreachableDropsPrivateAndLoopback covers the silent-blackhole
151+
// fix: discovered beacon lists from the registry can include addresses
152+
// running on a private VPC (e.g. GCP 10.128.0.x). When the daemon's
153+
// pickBeacon hash lands on one of those, all relay traffic vanishes
154+
// because off-VPC clients cannot reach it. filterUnreachable must drop
155+
// RFC1918, loopback, link-local and unspecified literals. DNS hostnames
156+
// are kept because they may resolve to public IPs.
157+
func TestFilterUnreachableDropsPrivateAndLoopback(t *testing.T) {
158+
in := []string{
159+
"34.71.57.205:9001", // public — kept
160+
"10.128.0.78:9001", // private RFC1918 — dropped
161+
"192.168.1.5:9001", // private RFC1918 — dropped
162+
"172.16.0.5:9001", // private RFC1918 — dropped
163+
"127.0.0.1:9001", // loopback — dropped
164+
"169.254.1.1:9001", // link-local — dropped
165+
"0.0.0.0:9001", // unspecified — dropped
166+
"beacon.example.com:9001", // DNS hostname — kept
167+
"8.8.8.8:9001", // public — kept
168+
}
169+
got := filterUnreachable(in)
170+
want := []string{
171+
"34.71.57.205:9001",
172+
"beacon.example.com:9001",
173+
"8.8.8.8:9001",
174+
}
175+
if len(got) != len(want) {
176+
t.Fatalf("filtered len = %d (%v), want %d (%v)", len(got), got, len(want), want)
177+
}
178+
for i := range want {
179+
if got[i] != want[i] {
180+
t.Fatalf("got[%d] = %q, want %q", i, got[i], want[i])
181+
}
182+
}
183+
}
184+
185+
// TestFilterUnreachableEmptyInputs guards against panics on edge inputs.
186+
func TestFilterUnreachableEmptyInputs(t *testing.T) {
187+
if got := filterUnreachable(nil); len(got) != 0 {
188+
t.Fatalf("nil input -> %v, want empty", got)
189+
}
190+
if got := filterUnreachable([]string{}); len(got) != 0 {
191+
t.Fatalf("empty input -> %v, want empty", got)
192+
}
193+
if got := filterUnreachable([]string{"malformed-no-port"}); len(got) != 1 {
194+
t.Fatalf("malformed entry should be kept (host parses cleanly w/o port): got %v", got)
195+
}
196+
}

0 commit comments

Comments
 (0)