proxy-pool/internal/gateway/policy/target_test.go

263 lines
7.7 KiB
Go

package policy
import (
"context"
"errors"
"net/netip"
"reflect"
"testing"
)
func TestParseURLAuthorityUsesDefaultPortAndPreservesHost(t *testing.T) {
authority, err := ParseURLAuthority("https://example.com/path?q=1")
if err != nil {
t.Fatalf("ParseURLAuthority(): %v", err)
}
if authority.Host != "example.com" {
t.Fatalf("host = %q, want example.com", authority.Host)
}
if authority.Port != 443 {
t.Fatalf("port = %d, want 443", authority.Port)
}
if authority.LiteralIP.IsValid() {
t.Fatalf("literal ip = %v, want invalid", authority.LiteralIP)
}
}
func TestParseConnectAuthoritySupportsIPv6Literal(t *testing.T) {
authority, err := ParseConnectAuthority("[2001:db8::1]:8443")
if err != nil {
t.Fatalf("ParseConnectAuthority(): %v", err)
}
if authority.Host != "2001:db8::1" {
t.Fatalf("host = %q, want 2001:db8::1", authority.Host)
}
if authority.Port != 8443 {
t.Fatalf("port = %d, want 8443", authority.Port)
}
if got := authority.LiteralIP.String(); got != "2001:db8::1" {
t.Fatalf("literal ip = %q, want 2001:db8::1", got)
}
}
func TestEvaluateURLRejectsLiteralMetadataAddressWithoutDNSLookup(t *testing.T) {
resolver := &stubResolver{}
policy, err := NewTargetPolicy(Config{Resolver: resolver})
if err != nil {
t.Fatalf("NewTargetPolicy(): %v", err)
}
_, err = policy.EvaluateURL(context.Background(), "http://169.254.169.254/latest/meta-data")
if !errors.Is(err, ErrTargetDenied) {
t.Fatalf("EvaluateURL() error = %v, want ErrTargetDenied", err)
}
if resolver.calls != nil {
t.Fatalf("resolver calls = %v, want nil", resolver.calls)
}
}
func TestEvaluateConnectAuthorityRejectsResolvedMixedResults(t *testing.T) {
resolver := &stubResolver{
results: map[string][]netip.Addr{
"example.com": {
netip.MustParseAddr("93.184.216.34"),
netip.MustParseAddr("10.0.0.8"),
},
},
}
policy, err := NewTargetPolicy(Config{Resolver: resolver})
if err != nil {
t.Fatalf("NewTargetPolicy(): %v", err)
}
_, err = policy.EvaluateConnectAuthority(context.Background(), "example.com:443")
if !errors.Is(err, ErrTargetDenied) {
t.Fatalf("EvaluateConnectAuthority() error = %v, want ErrTargetDenied", err)
}
if got, want := resolver.calls, []string{"example.com"}; !reflect.DeepEqual(got, want) {
t.Fatalf("resolver calls = %v, want %v", got, want)
}
}
func TestEvaluateURLPinsTheValidatedResolutionForTransport(t *testing.T) {
t.Parallel()
policy, err := NewTargetPolicy(Config{Resolver: &stubResolver{results: map[string][]netip.Addr{
"example.test": {
netip.MustParseAddr("8.8.8.8"),
netip.MustParseAddr("2001:4860:4860::8888"),
},
}}})
if err != nil {
t.Fatalf("NewTargetPolicy(): %v", err)
}
authority, err := policy.EvaluateURL(context.Background(), "http://example.test/path")
if err != nil {
t.Fatalf("EvaluateURL() error = %v", err)
}
if got := authority.DialAddress(); got != "8.8.8.8:80" {
t.Fatalf("DialAddress() = %q, want 8.8.8.8:80", got)
}
}
func TestEvaluateAuthorityRejectsReservedAndDisallowedPortsBeforeDNS(t *testing.T) {
t.Parallel()
resolver := &stubResolver{}
targets, err := NewTargetPolicy(Config{Resolver: resolver})
if err != nil {
t.Fatalf("NewTargetPolicy(): %v", err)
}
for _, target := range []string{
"http://100.64.0.1/",
"http://198.51.100.10/",
"http://[2001:db8::1]/",
"http://[3fff::1]/",
"http://168.63.129.16/",
"http://example.test:25/",
} {
if _, err := targets.EvaluateURL(context.Background(), target); !errors.Is(err, ErrTargetDenied) {
t.Fatalf("EvaluateURL(%q) error = %v, want ErrTargetDenied", target, err)
}
}
if resolver.calls != nil {
t.Fatalf("resolver calls = %v, want nil for disallowed port", resolver.calls)
}
}
func TestEvaluateAuthorityAllowsExplicitPort(t *testing.T) {
t.Parallel()
targets, err := NewTargetPolicy(Config{
AllowedPorts: []uint16{8443},
Resolver: &stubResolver{results: map[string][]netip.Addr{
"example.test": {netip.MustParseAddr("8.8.8.8")},
}},
})
if err != nil {
t.Fatalf("NewTargetPolicy(): %v", err)
}
if _, err := targets.EvaluateConnectAuthority(context.Background(), "example.test:8443"); err != nil {
t.Fatalf("EvaluateConnectAuthority() error = %v", err)
}
}
func TestEvaluateConnectAuthorityRejectsIPv6LoopbackLiteral(t *testing.T) {
policy, err := NewTargetPolicy(Config{Resolver: &stubResolver{}})
if err != nil {
t.Fatalf("NewTargetPolicy(): %v", err)
}
_, err = policy.EvaluateConnectAuthority(context.Background(), "[::1]:443")
if !errors.Is(err, ErrTargetDenied) {
t.Fatalf("EvaluateConnectAuthority() error = %v, want ErrTargetDenied", err)
}
}
func TestEvaluateURLAllowsPrivateNetworksWhenExplicitlyEnabled(t *testing.T) {
resolver := &stubResolver{
results: map[string][]netip.Addr{
"internal.example": {
netip.MustParseAddr("10.1.2.3"),
},
},
}
policy, err := NewTargetPolicy(Config{
Resolver: resolver,
AllowPrivateNetworks: true,
})
if err != nil {
t.Fatalf("NewTargetPolicy(): %v", err)
}
authority, err := policy.EvaluateURL(context.Background(), "http://internal.example")
if err != nil {
t.Fatalf("EvaluateURL(): %v", err)
}
if authority.Host != "internal.example" || authority.Port != 80 {
t.Fatalf("authority = %+v, want internal.example:80", authority)
}
}
func TestEvaluateURLCanConfigureLoopbackAndLinkLocalIndependently(t *testing.T) {
t.Parallel()
loopback, err := NewTargetPolicy(Config{AllowLoopback: true})
if err != nil {
t.Fatalf("NewTargetPolicy(loopback): %v", err)
}
if _, err := loopback.EvaluateURL(context.Background(), "http://127.0.0.1/"); err != nil {
t.Fatalf("EvaluateURL(loopback) error = %v", err)
}
linkLocal, err := NewTargetPolicy(Config{AllowLinkLocal: true})
if err != nil {
t.Fatalf("NewTargetPolicy(link-local): %v", err)
}
if _, err := linkLocal.EvaluateURL(context.Background(), "http://169.254.10.20/"); err != nil {
t.Fatalf("EvaluateURL(link-local) error = %v", err)
}
if _, err := linkLocal.EvaluateURL(context.Background(), "http://127.0.0.1/"); !errors.Is(err, ErrTargetDenied) {
t.Fatalf("link-local policy loopback error = %v, want ErrTargetDenied", err)
}
if _, err := linkLocal.EvaluateURL(context.Background(), "http://169.254.169.254/"); !errors.Is(err, ErrTargetDenied) {
t.Fatalf("link-local policy metadata error = %v, want ErrTargetDenied", err)
}
}
func TestEvaluateURLAllowsPrivateWhenEnabledButStillHonorsDenyCIDRs(t *testing.T) {
resolver := &stubResolver{
results: map[string][]netip.Addr{
"internal.example": {
netip.MustParseAddr("10.1.2.3"),
netip.MustParseAddr("169.254.169.254"),
},
},
}
policy, err := NewTargetPolicy(Config{
Resolver: resolver,
AllowPrivateNetworks: true,
DenyCIDRs: []string{"169.254.169.254/32"},
})
if err != nil {
t.Fatalf("NewTargetPolicy(): %v", err)
}
_, err = policy.EvaluateURL(context.Background(), "http://internal.example")
if !errors.Is(err, ErrTargetDenied) {
t.Fatalf("EvaluateURL() error = %v, want ErrTargetDenied", err)
}
}
func TestEvaluateConnectAuthorityPropagatesContextCancellation(t *testing.T) {
resolver := &stubResolver{
err: context.Canceled,
}
policy, err := NewTargetPolicy(Config{Resolver: resolver})
if err != nil {
t.Fatalf("NewTargetPolicy(): %v", err)
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, err = policy.EvaluateConnectAuthority(ctx, "example.com:443")
if !errors.Is(err, context.Canceled) {
t.Fatalf("EvaluateConnectAuthority() error = %v, want context.Canceled", err)
}
}
type stubResolver struct {
results map[string][]netip.Addr
err error
calls []string
}
func (s *stubResolver) LookupNetIP(ctx context.Context, host string) ([]netip.Addr, error) {
s.calls = append(s.calls, host)
if s.err != nil {
return nil, s.err
}
return append([]netip.Addr(nil), s.results[host]...), nil
}