263 lines
7.7 KiB
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
|
|
}
|