package guard import ( "context" "errors" "net" "net/http" "net/netip" "net/url" "strings" "syscall" "testing" "time" ) type nopConn struct{} func (nopConn) Control(func(uintptr)) error { return nil } func (nopConn) Read(func(uintptr) bool) error { return nil } func (nopConn) Write(func(uintptr) bool) error { return nil } func mustAddr(s string) netip.Addr { a, err := netip.ParseAddr(s) if err != nil { panic(err) } return a } func TestCheckAddrTable(t *testing.T) { t.Parallel() tests := []struct { addr string blocked bool reason string }{ {"8.8.8.8", false, ""}, {"1.1.1.1", false, ""}, {"93.184.216.34", false, ""}, {"2001:4860:4860::8888", false, ""}, {"172.15.255.255", false, ""}, // just below 172.16/12 {"172.32.0.1", false, ""}, // just above 172.16/12 {"100.63.255.255", false, ""}, // just below CGNAT {"100.128.0.1", false, ""}, // just above CGNAT {"127.0.0.1", true, "loopback"}, {"127.255.255.254", true, "loopback"}, {"::1", true, "loopback"}, {"10.0.0.5", true, "private"}, {"10.255.255.255", true, "private"}, {"192.168.1.1", true, "private"}, {"172.16.0.1", true, "private"}, {"172.31.255.255", true, "private"}, {"100.64.0.1", true, "tailnet"}, {"100.95.252.120", true, "tailnet"}, // rift {"100.127.255.254", true, "tailnet"}, {"fd7a:115c:a1e0::1", true, "tailnet"}, {"fd7a:115c:a1e0:ffff::1", true, "tailnet"}, {"fc00::1", true, "private"}, // ULA {"fe80::1", true, "link-local"}, {"169.254.169.254", true, "link-local"}, // cloud metadata {"224.0.0.1", true, "multicast"}, {"ff02::1", true, "multicast"}, {"0.0.0.0", true, "unspecified"}, {"0.1.2.3", true, "unspecified"}, // 0.0.0.0/8 {"::", true, "unspecified"}, {"::ffff:127.0.0.1", true, "loopback"}, {"::ffff:10.0.0.1", true, "private"}, {"::ffff:c0a8:1", true, "private"}, // 192.168.0.1 mapped {"::ffff:8.8.8.8", false, ""}, // mapped public → unmapped {"64:ff9b::10.0.0.1", true, "NAT64"}, {"64:ff9b::8.8.8.8", true, "NAT64"}, {"64:ff9b::a00:1", true, "NAT64"}, // 10.0.0.1 } for _, tt := range tests { t.Run(tt.addr, func(t *testing.T) { t.Parallel() err := CheckAddr(mustAddr(tt.addr)) if tt.blocked && err == nil { t.Fatalf("CheckAddr(%s) = nil, want blocked (%s)", tt.addr, tt.reason) } if !tt.blocked && err != nil { t.Fatalf("CheckAddr(%s) = %v, want allow", tt.addr, err) } if tt.blocked && err != nil && tt.reason != "" && !strings.Contains(err.Error(), tt.reason) && !strings.Contains(err.Error(), "private") && !strings.Contains(err.Error(), "loopback") && !strings.Contains(err.Error(), "tailnet") && !strings.Contains(err.Error(), "NAT64") && !strings.Contains(err.Error(), "unspecified") && !strings.Contains(err.Error(), "link-local") && !strings.Contains(err.Error(), "multicast") && !strings.Contains(err.Error(), "non-global") { t.Logf("CheckAddr(%s) = %v (wanted reason %q; format is still blocked:)", tt.addr, err, tt.reason) } if tt.blocked && err != nil && !strings.HasPrefix(err.Error(), "blocked:") { t.Fatalf("error %q does not start with blocked:", err) } }) } } func TestParseURLRejects(t *testing.T) { t.Parallel() cases := []struct { raw string substr string }{ {"", "empty"}, {"ftp://example.com/", "scheme"}, {"file:///etc/passwd", "scheme"}, {"gopher://example.com/", "scheme"}, {"http://user:pass@example.com/", "userinfo"}, {"http://user@example.com/", "userinfo"}, {"http://127.0.0.1:80@8.8.8.8/", "userinfo"}, {"http:///", "missing host"}, {"http://example.com:8080/", "port"}, {"http://example.com:22/", "port"}, {"http://dixie:11434", "port"}, {"http://100.95.252.120:8000", "port"}, {"https://example.com:8443/", "port"}, {"http://exa mple.com/", "invalid url"}, } for _, tt := range cases { t.Run(tt.raw, func(t *testing.T) { _, _, err := ParseURL(tt.raw) if err == nil { t.Fatalf("ParseURL(%q) = nil, want error", tt.raw) } if tt.substr != "" && !strings.Contains(err.Error(), tt.substr) { t.Fatalf("ParseURL(%q) = %v, want substr %q", tt.raw, err, tt.substr) } }) } } func TestParseURLAllows80And443(t *testing.T) { t.Parallel() for _, raw := range []string{ "http://example.com/", "http://example.com:80/", "https://example.com/", "https://example.com:443/", "http://example.com:443/", "https://example.com:80/", } { if _, _, err := ParseURL(raw); err != nil { t.Errorf("ParseURL(%q) = %v, want nil", raw, err) } } } func TestDecimalOctalHexIPs(t *testing.T) { t.Parallel() // All of these are 127.0.0.1 in inet_aton form. loopbacks := []string{ "http://127.0.0.1/", "http://2130706433/", // decimal "http://0x7f000001/", // hex dword "http://017700000001/", // octal dword "http://0177.0.0.1/", // octal dotted "http://0x7f.0.0.1/", // hex dotted "http://127.1/", // short "http://127.0.1/", // 3-part "http://0x7f.1/", } g := &Guard{} ctx := context.Background() for _, raw := range loopbacks { t.Run(raw, func(t *testing.T) { _, _, err := g.CheckURL(ctx, raw) if err == nil { t.Fatalf("CheckURL(%q) allowed, want blocked", raw) } if !strings.HasPrefix(err.Error(), "blocked:") { t.Fatalf("CheckURL(%q) = %v, want blocked:", raw, err) } // These must be classified as IP literals, never sent to DNS. }) } privates := []string{ "http://10.0.0.1/", "http://0x0a000001/", "http://167772161/", // 10.0.0.1 decimal "http://192.168.0.1/", "http://0xc0a80001/", } for _, raw := range privates { t.Run(raw, func(t *testing.T) { _, _, err := g.CheckURL(ctx, raw) if err == nil { t.Fatalf("CheckURL(%q) allowed, want blocked", raw) } }) } } func TestNamedCasesFromPlan(t *testing.T) { t.Parallel() g := &Guard{ LookupIP: func(_ context.Context, host string) ([]netip.Addr, error) { switch host { case "dixie": return []netip.Addr{mustAddr("100.64.0.10")}, nil default: t.Fatalf("unexpected lookup %q", host) return nil, nil } }, } ctx := context.Background() cases := []string{ "http://100.95.252.120:8000", "http://dixie:11434", "http://[fd7a:115c:a1e0::1]", "http://[fd7a:115c:a1e0::1]/", } for _, raw := range cases { t.Run(raw, func(t *testing.T) { _, _, err := g.CheckURL(ctx, raw) if err == nil { t.Fatalf("CheckURL(%q) allowed", raw) } var b *BlockedError if !errors.As(err, &b) { t.Fatalf("CheckURL(%q) = %v (%T), want BlockedError", raw, err, err) } }) } } func TestMixedRecordsRejected(t *testing.T) { t.Parallel() g := &Guard{ LookupIP: func(_ context.Context, host string) ([]netip.Addr, error) { return []netip.Addr{mustAddr("8.8.8.8"), mustAddr("10.0.0.1")}, nil }, } _, _, err := g.CheckURL(context.Background(), "http://evil.example/") if err == nil { t.Fatal("mixed public+private records allowed") } if !strings.Contains(err.Error(), "10.0.0.1") { t.Fatalf("error %v should name the private address", err) } } func TestAllPublicRecordsAllowed(t *testing.T) { t.Parallel() g := &Guard{ LookupIP: func(_ context.Context, host string) ([]netip.Addr, error) { return []netip.Addr{mustAddr("8.8.8.8"), mustAddr("2001:4860:4860::8888")}, nil }, } _, addrs, err := g.CheckURL(context.Background(), "https://dns.google/") if err != nil { t.Fatal(err) } if len(addrs) != 2 { t.Fatalf("got %d addrs", len(addrs)) } } func TestRebindingBlockedAtControl(t *testing.T) { t.Parallel() g := &Guard{ LookupIP: func(_ context.Context, host string) ([]netip.Addr, error) { // Pre-check sees a public address. return []netip.Addr{mustAddr("93.184.216.34")}, nil }, } _, _, err := g.CheckURL(context.Background(), "http://rebind.example/") if err != nil { t.Fatalf("pre-check should pass on public record: %v", err) } // After pre-check, DNS rebinds to loopback. Control sees the dial IP. err = g.Control("tcp", "127.0.0.1:80", nopConn{}) if err == nil { t.Fatal("Control allowed 127.0.0.1 after rebind") } if !strings.Contains(err.Error(), "127.0.0.1") { t.Fatalf("Control error %v should name 127.0.0.1", err) } err = g.Control("tcp", "10.0.0.5:443", nopConn{}) if err == nil { t.Fatal("Control allowed 10.0.0.5") } err = g.Control("tcp", "8.8.8.8:80", nopConn{}) if err != nil { t.Fatalf("Control public: %v", err) } err = g.Control("tcp", "8.8.8.8:22", nopConn{}) if err == nil { t.Fatal("Control allowed port 22") } } func TestControlHappyEyeballsPrivateLeg(t *testing.T) { t.Parallel() g := &Guard{} // Simulate two racing dials; the private one must fail even if the // public one would succeed. if err := g.Control("tcp", "1.1.1.1:443", nopConn{}); err != nil { t.Fatalf("public leg: %v", err) } if err := g.Control("tcp6", "[::1]:443", nopConn{}); err == nil { t.Fatal("loopback v6 leg allowed") } if err := g.Control("tcp", "100.95.252.120:80", nopConn{}); err == nil { t.Fatal("tailnet leg allowed") } } func TestRedirectToPrivate(t *testing.T) { t.Parallel() g := &Guard{ LookupIP: func(_ context.Context, host string) ([]netip.Addr, error) { return []netip.Addr{mustAddr("93.184.216.34")}, nil }, } orig, err := http.NewRequest("GET", "http://example.com/", nil) if err != nil { t.Fatal(err) } next, err := http.NewRequest("GET", "http://127.0.0.1/secret", nil) if err != nil { t.Fatal(err) } if err := g.CheckRedirect(next, []*http.Request{orig}); err == nil { t.Fatal("redirect to 127.0.0.1 allowed") } next, _ = http.NewRequest("GET", "http://10.0.0.5/", nil) if err := g.CheckRedirect(next, []*http.Request{orig}); err == nil { t.Fatal("redirect to 10.0.0.5 allowed") } next, _ = http.NewRequest("GET", "http://[fd7a:115c:a1e0::1]/", nil) if err := g.CheckRedirect(next, []*http.Request{orig}); err == nil { t.Fatal("redirect to tailnet ULA allowed") } next, _ = http.NewRequest("GET", "http://dixie:11434/", nil) if err := g.CheckRedirect(next, []*http.Request{orig}); err == nil { t.Fatal("redirect to dixie:11434 allowed") } next, _ = http.NewRequest("GET", "https://safe.example/", nil) if err := g.CheckRedirect(next, []*http.Request{orig}); err != nil { t.Fatalf("redirect to public https: %v", err) } } func TestTooManyRedirects(t *testing.T) { t.Parallel() g := &Guard{ LookupIP: func(_ context.Context, _ string) ([]netip.Addr, error) { return []netip.Addr{mustAddr("93.184.216.34")}, nil }, } via := make([]*http.Request, 5) for i := range via { via[i], _ = http.NewRequest("GET", "http://example.com/", nil) } next, _ := http.NewRequest("GET", "http://example.com/next", nil) err := g.CheckRedirect(next, via) if err == nil || !strings.Contains(err.Error(), "too many redirects") { t.Fatalf("got %v", err) } } func TestDialLoopbackIsBlocked(t *testing.T) { t.Parallel() g := &Guard{} d := g.Dialer() c, err := d.Dial("tcp", "127.0.0.1:80") if err == nil { c.Close() t.Fatal("Dial 127.0.0.1:80 succeeded") } if !strings.HasPrefix(err.Error(), "blocked:") && !strings.Contains(err.Error(), "blocked:") { t.Fatalf("Dial error %v should be a blocked: error", err) } c, err = d.Dial("tcp", "[::1]:443") if err == nil { c.Close() t.Fatal("Dial [::1]:443 succeeded") } } func TestTransportHasNoProxy(t *testing.T) { t.Parallel() g := &Guard{} tr := g.Transport() if tr.Proxy != nil { req, _ := http.NewRequest("GET", "http://example.com/", nil) if u, err := tr.Proxy(req); u != nil || err != nil { t.Fatalf("Proxy returned %v, %v; want nil", u, err) } } } func TestBlockedErrorFormat(t *testing.T) { t.Parallel() err := CheckAddr(mustAddr("10.0.0.5")) if err == nil || err.Error() != "blocked: private address 10.0.0.5" { t.Fatalf("got %v", err) } } func TestNoAddresses(t *testing.T) { t.Parallel() g := &Guard{ LookupIP: func(_ context.Context, _ string) ([]netip.Addr, error) { return nil, nil }, } _, _, err := g.CheckURL(context.Background(), "http://empty.example/") if err == nil || !strings.Contains(err.Error(), "no addresses") { t.Fatalf("got %v", err) } } func TestParseIPv4AtOn(t *testing.T) { t.Parallel() got, ok := parseIPv4AtOn("2130706433") if !ok || got.String() != "127.0.0.1" { t.Fatalf("decimal: got %v ok=%v", got, ok) } got, ok = parseIPv4AtOn("0177.0.0.1") if !ok || got.String() != "127.0.0.1" { t.Fatalf("octal dotted: got %v ok=%v", got, ok) } got, ok = parseIPv4AtOn("0x7f.0.0.1") if !ok || got.String() != "127.0.0.1" { t.Fatalf("hex dotted: got %v ok=%v", got, ok) } got, ok = parseIPv4AtOn("127.1") if !ok || got.String() != "127.0.0.1" { t.Fatalf("short: got %v ok=%v", got, ok) } if _, ok := parseIPv4AtOn("example.com"); ok { t.Fatal("hostname parsed as IP") } if _, ok := parseIPv4AtOn("256.1.1.1"); ok { t.Fatal("256.1.1.1 should not parse") } } func TestClientCheckRedirectWired(t *testing.T) { t.Parallel() g := &Guard{} cl := g.Client() if cl.CheckRedirect == nil { t.Fatal("CheckRedirect not set") } if cl.Timeout != 20*time.Second { t.Fatalf("Timeout = %v", cl.Timeout) } } func TestURLStringRoundTripRedirect(t *testing.T) { t.Parallel() // A 302 Location that url.URL.String() produces must still be rejected. u, err := url.Parse("http://127.0.0.1/x") if err != nil { t.Fatal(err) } g := &Guard{} req := &http.Request{URL: u} via := []*http.Request{{URL: mustURL("http://example.com/")}} if err := g.CheckRedirect(req, via); err == nil { t.Fatal("allowed") } } func mustURL(s string) *url.URL { u, err := url.Parse(s) if err != nil { panic(err) } return u } // Ensure Control implements the Dialer hook shape used by net.Dialer. var ( _ func(network, address string, c syscall.RawConn) error = (*Guard)(nil).Control _ func(ctx context.Context, network, address string, c syscall.RawConn) error = (*Guard)(nil).ControlContext _ func(req *http.Request, via []*http.Request) error = (*Guard)(nil).CheckRedirect ) func TestLocalhostName(t *testing.T) { t.Parallel() g := &Guard{ LookupIP: func(_ context.Context, host string) ([]netip.Addr, error) { if host != "localhost" { t.Fatalf("host %q", host) } return []netip.Addr{mustAddr("127.0.0.1"), mustAddr("::1")}, nil }, } _, _, err := g.CheckURL(context.Background(), "http://localhost/") if err == nil { t.Fatal("localhost allowed") } } func TestNonASCIIHostRejected(t *testing.T) { t.Parallel() _, _, err := ParseURL("http://exämple.com/") if err == nil { t.Fatal("non-ascii host allowed") } } func TestPublicIPLiteralAllowed(t *testing.T) { t.Parallel() g := &Guard{} _, addrs, err := g.CheckURL(context.Background(), "http://1.1.1.1/") if err != nil { t.Fatal(err) } if len(addrs) != 1 || addrs[0].String() != "1.1.1.1" { t.Fatalf("got %v", addrs) } } func TestSplitHostPortIPv6Control(t *testing.T) { t.Parallel() g := &Guard{} if err := g.Control("tcp", "[fd7a:115c:a1e0::1]:80", nopConn{}); err == nil { t.Fatal("allowed tailnet v6") } if err := g.Control("tcp", "[2001:4860:4860::8888]:443", nopConn{}); err != nil { t.Fatal(err) } } func TestDialerType(t *testing.T) { t.Parallel() d := (&Guard{}).Dialer() if d.Control == nil || d.ControlContext == nil { t.Fatal("missing Control hooks") } if _, ok := interface{}(d).(*net.Dialer); !ok { t.Fatal("not a Dialer") } } func TestIANASpecialPurpose(t *testing.T) { t.Parallel() // One address per required prefix from the P0 review. blocked := []string{ "2002:a00:1::1", // 6to4 of 10.0.0.1 "2002:7f00:1::1", // 6to4 of 127.0.0.1 "2001::1", // Teredo 2001::/32 (inside 2001::/23) "64:ff9b:1::a00:1", // RFC 8215 local-use NAT64 embedding 10.0.0.1 "fec0::1", // deprecated site-local "198.18.0.1", // benchmarking "198.19.255.255", "192.0.0.1", "240.0.0.1", "255.255.255.255", "100::1", "2001:db8::1", "2001:10::1", // ORCHID "2001:20::1", // ORCHIDv2 } for _, s := range blocked { t.Run(s, func(t *testing.T) { t.Parallel() err := CheckAddr(mustAddr(s)) if err == nil { t.Fatalf("CheckAddr(%s) = nil, want blocked", s) } if !strings.HasPrefix(err.Error(), "blocked:") { t.Fatalf("%s: %v", s, err) } }) } allowed := []string{"8.8.8.8", "1.1.1.1", "2001:4860:4860::8888"} for _, s := range allowed { if err := CheckAddr(mustAddr(s)); err != nil { t.Fatalf("CheckAddr(%s) = %v, want allow", s, err) } } } func TestConfiguredDenyPrefix(t *testing.T) { t.Parallel() // 8.8.8.8 is allowed by the IANA table; a configured deny must still block // it (the hairpin/WAN case). g := &Guard{ DenyPrefixes: []netip.Prefix{netip.MustParsePrefix("8.8.8.8/32")}, LookupIP: func(_ context.Context, host string) ([]netip.Addr, error) { return []netip.Addr{mustAddr("8.8.8.8")}, nil }, } if err := CheckAddr(mustAddr("8.8.8.8")); err != nil { t.Fatalf("default CheckAddr allowed-set: %v", err) } if err := g.CheckAddr(mustAddr("8.8.8.8")); err == nil { t.Fatal("configured /32 allowed") } if !strings.Contains(errString(g.CheckAddr(mustAddr("8.8.8.8"))), "denied prefix") { t.Fatalf("want denied prefix, got %v", g.CheckAddr(mustAddr("8.8.8.8"))) } if _, _, err := g.CheckURL(context.Background(), "http://hairpin.example/"); err == nil { t.Fatal("CheckURL with injected denied address allowed") } if err := g.Control("tcp", "8.8.8.8:80", nopConn{}); err == nil { t.Fatal("Control allowed configured deny") } if err := g.CheckAddr(mustAddr("1.1.1.1")); err != nil { t.Fatalf("unrelated public: %v", err) } } func errString(err error) string { if err == nil { return "" } return err.Error() } func TestControlRejectsNonTCP(t *testing.T) { t.Parallel() g := &Guard{} if err := g.Control("udp", "8.8.8.8:80", nopConn{}); err == nil { t.Fatal("udp allowed") } if err := g.Control("tcp4", "8.8.8.8:80", nopConn{}); err != nil { t.Fatal(err) } if err := g.Control("tcp6", "[2001:4860:4860::8888]:443", nopConn{}); err != nil { t.Fatal(err) } }