diff --git a/infra/conf/transport_finalmask.go b/infra/conf/transport_finalmask.go index 5c95e84b0..c2d099ec8 100644 --- a/infra/conf/transport_finalmask.go +++ b/infra/conf/transport_finalmask.go @@ -99,6 +99,7 @@ var ( type IPFilter struct { BlockedIPs []string `json:"blockedIPs"` + AllowedIPs []string `json:"allowedIPs"` } func (c *IPFilter) Build() (proto.Message, error) { @@ -106,7 +107,11 @@ func (c *IPFilter) Build() (proto.Message, error) { if err != nil { return nil, errors.New(`invalid "blockedIPs"`).Base(err) } - return &ipfilter.Config{BlockedIps: rules}, nil + allowed, err := geodata.ParseIPRules(c.AllowedIPs) + if err != nil { + return nil, errors.New(`invalid "allowedIPs"`).Base(err) + } + return &ipfilter.Config{BlockedIps: rules, AllowedIps: allowed}, nil } type TCPItem struct { diff --git a/infra/conf/transport_finalmask_ipfilter_test.go b/infra/conf/transport_finalmask_ipfilter_test.go index 3cfdc9bae..10c7ddc41 100644 --- a/infra/conf/transport_finalmask_ipfilter_test.go +++ b/infra/conf/transport_finalmask_ipfilter_test.go @@ -7,6 +7,7 @@ import ( "strings" "testing" + "github.com/xtls/xray-core/common/geodata" . "github.com/xtls/xray-core/infra/conf" "github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet/finalmask/ipfilter" @@ -14,44 +15,65 @@ import ( func TestFinalMaskIPFilter(t *testing.T) { for _, network := range []string{"tcp", "udp"} { - t.Run(network, func(t *testing.T) { - input := fmt.Sprintf(`{"finalmask":{"%s":[{"type":"ip-filter","settings":{"blockedIPs":["192.0.2.0/24"]}}]}}`, network) - var config StreamConfig - if err := json.Unmarshal([]byte(input), &config); err != nil { - t.Fatal(err) - } - built, err := config.Build() - if err != nil { - t.Fatal(err) - } - masks := built.Tcpmasks - if network == "udp" { - masks = built.Udpmasks - } - if len(masks) != 1 { - t.Fatalf("expected one mask, got %d", len(masks)) - } - instance, err := masks[0].GetInstance() - if err != nil { - t.Fatal(err) - } - filter, ok := instance.(*ipfilter.Config) - if !ok { - t.Fatalf("unexpected mask type: %T", instance) - } - if len(filter.BlockedIps) != 1 { - t.Fatalf("expected one blocked IP rule, got %d", len(filter.BlockedIps)) - } - cidr := filter.BlockedIps[0].GetCustom().GetCidr() - if cidr.GetPrefix() != 24 || !net.IP(cidr.GetIp()).Equal(net.IPv4(192, 0, 2, 0)) { - t.Fatalf("unexpected blocked IP rule: %v", cidr) - } - if _, err := internet.ToMemoryStreamConfig(built); err != nil { - t.Fatalf("failed to initialize runtime finalmask: %v", err) - } - }) + for _, test := range []struct { + name, settings string + blocked, allowed int + }{ + {"blacklist", `{"blockedIPs":["192.0.2.0/24"]}`, 1, 0}, + {"whitelist", `{"allowedIPs":["192.0.2.0/24"]}`, 0, 1}, + {"both", `{"blockedIPs":["192.0.2.0/24"],"allowedIPs":["192.0.2.0/24"]}`, 1, 1}, + {"empty_whitelist", `{"allowedIPs":[]}`, 0, 0}, + {"empty_blacklist", `{"blockedIPs":[]}`, 0, 0}, + } { + t.Run(network+"/"+test.name, func(t *testing.T) { + input := fmt.Sprintf(`{"finalmask":{"%s":[{"type":"ip-filter","settings":%s}]}}`, network, test.settings) + var config StreamConfig + if err := json.Unmarshal([]byte(input), &config); err != nil { + t.Fatal(err) + } + built, err := config.Build() + if err != nil { + t.Fatal(err) + } + masks := built.Tcpmasks + if network == "udp" { + masks = built.Udpmasks + } + if len(masks) != 1 { + t.Fatalf("expected one mask, got %d", len(masks)) + } + instance, err := masks[0].GetInstance() + if err != nil { + t.Fatal(err) + } + filter, ok := instance.(*ipfilter.Config) + if !ok { + t.Fatalf("unexpected mask type: %T", instance) + } + if len(filter.BlockedIps) != test.blocked || len(filter.AllowedIps) != test.allowed { + t.Fatalf("unexpected IP filter policy: %v", filter) + } + for _, rules := range [][]*geodata.IPRule{filter.BlockedIps, filter.AllowedIps} { + for _, rule := range rules { + cidr := rule.GetCustom().GetCidr() + if cidr.GetPrefix() != 24 || !net.IP(cidr.GetIp()).Equal(net.IPv4(192, 0, 2, 0)) { + t.Fatalf("unexpected IP rule: %v", cidr) + } + } + } + if _, err := internet.ToMemoryStreamConfig(built); err != nil { + t.Fatalf("failed to initialize runtime finalmask: %v", err) + } + }) + } } - if _, err := (&IPFilter{BlockedIPs: []string{"192.0.2.0/33"}}).Build(); err == nil || !strings.Contains(err.Error(), "blockedIPs") { - t.Errorf("expected invalid blockedIPs, got %v", err) + for _, field := range []string{"blockedIPs", "allowedIPs"} { + var config IPFilter + if err := json.Unmarshal([]byte(fmt.Sprintf(`{"%s":["192.0.2.0/33"]}`, field)), &config); err != nil { + t.Fatal(err) + } + if _, err := config.Build(); err == nil || !strings.Contains(err.Error(), field) { + t.Errorf("expected invalid %s, got %v", field, err) + } } } diff --git a/transport/internet/finalmask/ipfilter/config.go b/transport/internet/finalmask/ipfilter/config.go index 8dcbc83d6..55eff8631 100644 --- a/transport/internet/finalmask/ipfilter/config.go +++ b/transport/internet/finalmask/ipfilter/config.go @@ -6,22 +6,36 @@ import ( "github.com/xtls/xray-core/transport/internet/finalmask" ) -func (c *Config) matcher() (geodata.IPMatcher, error) { - if len(c.BlockedIps) == 0 { +func (c *Config) buildFilter() (*filter, error) { + if len(c.BlockedIps) == 0 && len(c.AllowedIps) == 0 { return nil, nil } - return geodata.IPReg.BuildIPMatcher(c.BlockedIps) + f := new(filter) + var err error + if len(c.BlockedIps) > 0 { + f.blocked, err = geodata.IPReg.BuildIPMatcher(c.BlockedIps) + if err != nil { + return nil, err + } + } + if len(c.AllowedIps) > 0 { + f.allowed, err = geodata.IPReg.BuildIPMatcher(c.AllowedIps) + if err != nil { + return nil, err + } + } + return f, nil } func (c *Config) Listen(inner net.Listener) (net.Listener, error) { - matcher, err := c.matcher() + f, err := c.buildFilter() if err != nil { return nil, err } - if matcher == nil { + if f == nil { return inner, nil } - return &listener{Listener: inner, matcher: matcher}, nil + return &listener{Listener: inner, filter: f}, nil } func (c *Config) WrapConnClient(conn net.Conn, dest *net.Destination, dialer *finalmask.Dialer) (net.Conn, error) { @@ -38,12 +52,12 @@ func (c *Config) WrapPacketConnClient(conn net.PacketConn, dest *net.Destination } func (c *Config) WrapPacketConnServer(conn net.PacketConn, addr net.Addr, lc *finalmask.ListenConfig) (net.PacketConn, error) { - matcher, err := c.matcher() + f, err := c.buildFilter() if err != nil { return nil, err } - if matcher == nil { + if f == nil { return conn, nil } - return &packetConn{PacketConn: conn, matcher: matcher}, nil + return &packetConn{PacketConn: conn, filter: f}, nil } diff --git a/transport/internet/finalmask/ipfilter/config.pb.go b/transport/internet/finalmask/ipfilter/config.pb.go index eeaf59386..04e3bfc41 100644 --- a/transport/internet/finalmask/ipfilter/config.pb.go +++ b/transport/internet/finalmask/ipfilter/config.pb.go @@ -25,6 +25,7 @@ const ( type Config struct { state protoimpl.MessageState `protogen:"open.v1"` BlockedIps []*geodata.IPRule `protobuf:"bytes,1,rep,name=blocked_ips,json=blockedIps,proto3" json:"blocked_ips,omitempty"` + AllowedIps []*geodata.IPRule `protobuf:"bytes,2,rep,name=allowed_ips,json=allowedIps,proto3" json:"allowed_ips,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -66,14 +67,23 @@ func (x *Config) GetBlockedIps() []*geodata.IPRule { return nil } +func (x *Config) GetAllowedIps() []*geodata.IPRule { + if x != nil { + return x.AllowedIps + } + return nil +} + var File_transport_internet_finalmask_ipfilter_config_proto protoreflect.FileDescriptor const file_transport_internet_finalmask_ipfilter_config_proto_rawDesc = "" + "\n" + - "2transport/internet/finalmask/ipfilter/config.proto\x12*xray.transport.internet.finalmask.ipfilter\x1a\x1bcommon/geodata/geodat.proto\"F\n" + + "2transport/internet/finalmask/ipfilter/config.proto\x12*xray.transport.internet.finalmask.ipfilter\x1a\x1bcommon/geodata/geodat.proto\"\x84\x01\n" + "\x06Config\x12<\n" + "\vblocked_ips\x18\x01 \x03(\v2\x1b.xray.common.geodata.IPRuleR\n" + - "blockedIpsB\xa0\x01\n" + + "blockedIps\x12<\n" + + "\vallowed_ips\x18\x02 \x03(\v2\x1b.xray.common.geodata.IPRuleR\n" + + "allowedIpsB\xa0\x01\n" + ".com.xray.transport.internet.finalmask.ipfilterP\x01Z?github.com/xtls/xray-core/transport/internet/finalmask/ipfilter\xaa\x02*Xray.Transport.Internet.Finalmask.Ipfilterb\x06proto3" var ( @@ -95,11 +105,12 @@ var file_transport_internet_finalmask_ipfilter_config_proto_goTypes = []any{ } var file_transport_internet_finalmask_ipfilter_config_proto_depIdxs = []int32{ 1, // 0: xray.transport.internet.finalmask.ipfilter.Config.blocked_ips:type_name -> xray.common.geodata.IPRule - 1, // [1:1] is the sub-list for method output_type - 1, // [1:1] is the sub-list for method input_type - 1, // [1:1] is the sub-list for extension type_name - 1, // [1:1] is the sub-list for extension extendee - 0, // [0:1] is the sub-list for field type_name + 1, // 1: xray.transport.internet.finalmask.ipfilter.Config.allowed_ips:type_name -> xray.common.geodata.IPRule + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name } func init() { file_transport_internet_finalmask_ipfilter_config_proto_init() } diff --git a/transport/internet/finalmask/ipfilter/config.proto b/transport/internet/finalmask/ipfilter/config.proto index b50620e07..558abde15 100644 --- a/transport/internet/finalmask/ipfilter/config.proto +++ b/transport/internet/finalmask/ipfilter/config.proto @@ -10,4 +10,5 @@ import "common/geodata/geodat.proto"; message Config { repeated xray.common.geodata.IPRule blocked_ips = 1; + repeated xray.common.geodata.IPRule allowed_ips = 2; } diff --git a/transport/internet/finalmask/ipfilter/ip_filter.go b/transport/internet/finalmask/ipfilter/ip_filter.go index e25a03628..1046b4c73 100644 --- a/transport/internet/finalmask/ipfilter/ip_filter.go +++ b/transport/internet/finalmask/ipfilter/ip_filter.go @@ -7,7 +7,7 @@ import ( type listener struct { net.Listener - matcher geodata.IPMatcher + filter *filter } // Accept filters connections after the TCP handshake and before higher-level processing. @@ -17,7 +17,7 @@ func (l *listener) Accept() (net.Conn, error) { if err != nil { return nil, err } - if matchAddress(l.matcher, conn.RemoteAddr()) { + if l.filter.rejects(conn.RemoteAddr()) { conn.Close() continue } @@ -27,14 +27,14 @@ func (l *listener) Accept() (net.Conn, error) { type packetConn struct { net.PacketConn - matcher geodata.IPMatcher + filter *filter } func (c *packetConn) ReadFrom(p []byte) (int, net.Addr, error) { for { n, addr, err := c.PacketConn.ReadFrom(p) // Reads may return both data and an error; filter the data first. - if matchAddress(c.matcher, addr) { + if c.filter.rejects(addr) { if err != nil { return 0, nil, err } @@ -44,15 +44,26 @@ func (c *packetConn) ReadFrom(p []byte) (int, net.Addr, error) { } } -func matchAddress(matcher geodata.IPMatcher, addr net.Addr) bool { +type filter struct { + blocked geodata.IPMatcher + allowed geodata.IPMatcher +} + +func (f *filter) rejects(addr net.Addr) bool { + var ip net.IP switch addr := addr.(type) { case *net.TCPAddr: - return matcher.Match(addr.IP) + ip = addr.IP case *net.UDPAddr: - return matcher.Match(addr.IP) + ip = addr.IP case *net.IPAddr: - return matcher.Match(addr.IP) + ip = addr.IP default: + // Unknown sources cannot satisfy an allowlist. + return f.allowed != nil + } + if f.blocked != nil && f.blocked.Match(ip) { return true } + return f.allowed != nil && !f.allowed.Match(ip) } diff --git a/transport/internet/finalmask/ipfilter/ip_filter_test.go b/transport/internet/finalmask/ipfilter/ip_filter_test.go index 8561d3815..1373e006b 100644 --- a/transport/internet/finalmask/ipfilter/ip_filter_test.go +++ b/transport/internet/finalmask/ipfilter/ip_filter_test.go @@ -15,33 +15,49 @@ import ( ) func TestTCPFilterBeforeOtherMasks(t *testing.T) { - blocked4 := sourceConn("192.0.2.1") - blocked6 := sourceConn("2001:db8::1") - unknown := &testConn{addr: &net.UnixAddr{Name: "ipfilter.sock", Net: "unix"}} - missing := new(testConn) - allowed := sourceConn("198.51.100.1") - raw := &testListener{conns: []net.Conn{blocked4, blocked6, unknown, missing, allowed}} - spy := new(recordMask) - fm := finalmask.NewFinalMask([]finalmask.TCPMask{spy, testConfig(t, "192.0.2.0/24", "2001:db8::/32")}, nil, nil, - func(context.Context, net.Addr) (net.Listener, error) { return raw, nil }, nil, nil) - l, err := fm.Listen(context.Background(), &net.TCPAddr{}) - if err != nil { - t.Fatal(err) - } - defer l.Close() - got, err := l.Accept() - if err != nil || got != allowed { - t.Fatalf("Accept() = (%v, %v), want unblocked connection", got, err) - } - // Fake sockets panic on Read or Write. Rejection must not require handshake data. - if !blocked4.closed || !blocked6.closed || !unknown.closed || !missing.closed || allowed.closed || raw.closed { - t.Fatal("filter did not close only the blocked connections") - } - if len(spy.seen) != 1 || spy.seen[0] != allowed { - t.Fatal("blocked connections reached the next mask") - } - if conn, err := l.Accept(); conn != nil || !errors.Is(err, io.EOF) { - t.Fatalf("listener error was not propagated: (%v, %v)", conn, err) + for _, policy := range testPolicies(t) { + t.Run(policy.name, func(t *testing.T) { + blocked4 := sourceConn("192.0.2.1") + blocked6 := sourceConn("2001:db8::1") + unknown := &testConn{addr: &net.UnixAddr{Name: "ipfilter.sock", Net: "unix"}} + missing := new(testConn) + allowed := sourceConn("198.51.100.1") + allowed6 := sourceConn("2001:db8:1::1") + raw := &testListener{conns: []net.Conn{blocked4, blocked6, unknown, missing, allowed, allowed6}} + spy := new(recordMask) + fm := finalmask.NewFinalMask([]finalmask.TCPMask{spy, policy.config}, nil, nil, + func(context.Context, net.Addr) (net.Listener, error) { return raw, nil }, nil, nil) + l, err := fm.Listen(context.Background(), &net.TCPAddr{}) + if err != nil { + t.Fatal(err) + } + defer l.Close() + whitelist := len(policy.config.AllowedIps) > 0 + want := []net.Conn{allowed, allowed6} + if !whitelist { + want = append([]net.Conn{unknown, missing}, want...) + } + for _, expected := range want { + if got, err := l.Accept(); err != nil || got != expected { + t.Fatalf("Accept() = (%v, %v), want %v", got, err, expected) + } + } + // Fake sockets panic on Read or Write. Rejection must not require handshake data. + if !blocked4.closed || !blocked6.closed || unknown.closed != whitelist || missing.closed != whitelist || allowed.closed || allowed6.closed || raw.closed { + t.Fatal("filter did not close only the blocked connections") + } + if len(spy.seen) != len(want) { + t.Fatal("blocked connections reached the next mask") + } + for i, conn := range spy.seen { + if conn != want[i] { + t.Fatal("unexpected connection reached the next mask") + } + } + if conn, err := l.Accept(); conn != nil || !errors.Is(err, io.EOF) { + t.Fatalf("listener error was not propagated: (%v, %v)", conn, err) + } + }) } } @@ -93,84 +109,128 @@ func TestTCPFilterUsesProxyHeader(t *testing.T) { } func TestUDPFilter(t *testing.T) { - allowed := &net.UDPAddr{IP: net.ParseIP("198.51.100.1"), Port: 1234} - raw := &testPacketConn{reads: []datagram{ - {data: []byte("blocked"), addr: &net.UDPAddr{IP: net.ParseIP("192.0.2.1"), Port: 1234}}, - {addr: &net.UDPAddr{IP: net.ParseIP("2001:db8::1"), Port: 1234}}, - {data: []byte("unknown"), addr: &net.UnixAddr{Name: "ipfilter.sock", Net: "unixgram"}}, - {data: []byte("missing address")}, - {addr: allowed}, - {data: []byte("allowed"), addr: allowed}, - {data: []byte("blocked again"), addr: &net.UDPAddr{IP: net.ParseIP("192.0.2.2"), Port: 1234}}, - }} - fm := finalmask.NewFinalMask(nil, []finalmask.UDPMask{testConfig(t, "192.0.2.0/24", "2001:db8::/32")}, nil, nil, nil, - func(context.Context, net.Addr) (net.PacketConn, error) { return raw, nil }) - conn, err := fm.ListenPacket(context.Background(), &net.UDPAddr{}) - if err != nil { - t.Fatal(err) - } - defer conn.Close() - buf := make([]byte, 64) - if n, addr, err := conn.ReadFrom(buf); n != 0 || addr != allowed || err != nil { - t.Fatalf("zero-length allowed datagram was lost: (%d, %v, %v)", n, addr, err) - } - n, addr, err := conn.ReadFrom(buf) - if err != nil || addr != allowed || string(buf[:n]) != "allowed" { - t.Fatalf("ReadFrom() = (%q, %v, %v)", buf[:n], addr, err) - } - if _, _, err := conn.ReadFrom(buf); !errors.Is(err, io.EOF) { - t.Fatalf("underlying read error was not propagated: %v", err) - } - if raw.closed { - t.Fatal("dropping a packet closed the shared UDP socket") + for _, policy := range testPolicies(t) { + t.Run(policy.name, func(t *testing.T) { + allowed := &net.UDPAddr{IP: net.ParseIP("198.51.100.1"), Port: 1234} + allowed6 := &net.UDPAddr{IP: net.ParseIP("2001:db8:1::1"), Port: 1234} + unknown := datagram{data: []byte("unknown"), addr: &net.UnixAddr{Name: "ipfilter.sock", Net: "unixgram"}} + missing := datagram{data: []byte("missing address")} + raw := &testPacketConn{reads: []datagram{ + {data: []byte("blocked"), addr: &net.UDPAddr{IP: net.ParseIP("192.0.2.1"), Port: 1234}}, + {addr: &net.UDPAddr{IP: net.ParseIP("2001:db8::1"), Port: 1234}}, + unknown, + missing, + {addr: allowed}, + {data: []byte("allowed"), addr: allowed}, + {data: []byte("allowed6"), addr: allowed6}, + {data: []byte("blocked again"), addr: &net.UDPAddr{IP: net.ParseIP("192.0.2.2"), Port: 1234}}, + }} + fm := finalmask.NewFinalMask(nil, []finalmask.UDPMask{policy.config}, nil, nil, nil, + func(context.Context, net.Addr) (net.PacketConn, error) { return raw, nil }) + conn, err := fm.ListenPacket(context.Background(), &net.UDPAddr{}) + if err != nil { + t.Fatal(err) + } + defer conn.Close() + buf := make([]byte, 64) + if len(policy.config.AllowedIps) == 0 { + for _, expected := range []datagram{unknown, missing} { + n, addr, err := conn.ReadFrom(buf) + if err != nil || addr != expected.addr || string(buf[:n]) != string(expected.data) { + t.Fatalf("unknown source was not allowed by blacklist: (%q, %v, %v)", buf[:n], addr, err) + } + } + } + if n, addr, err := conn.ReadFrom(buf); n != 0 || addr != allowed || err != nil { + t.Fatalf("zero-length allowed datagram was lost: (%d, %v, %v)", n, addr, err) + } + n, addr, err := conn.ReadFrom(buf) + if err != nil || addr != allowed || string(buf[:n]) != "allowed" { + t.Fatalf("ReadFrom() = (%q, %v, %v)", buf[:n], addr, err) + } + n, addr, err = conn.ReadFrom(buf) + if err != nil || addr != allowed6 || string(buf[:n]) != "allowed6" { + t.Fatalf("IPv6 ReadFrom() = (%q, %v, %v)", buf[:n], addr, err) + } + if _, _, err := conn.ReadFrom(buf); !errors.Is(err, io.EOF) { + t.Fatalf("underlying read error was not propagated: %v", err) + } + if raw.closed { + t.Fatal("dropping a packet closed the shared UDP socket") + } + }) } } func TestEmptyFilter(t *testing.T) { - rawListener := new(testListener) - rawPacket := new(testPacketConn) - empty := new(Config) - if l, err := empty.Listen(rawListener); err != nil || l != rawListener { - t.Fatal("an empty TCP filter should preserve the listener") - } - if conn, err := empty.WrapPacketConnServer(rawPacket, nil, nil); err != nil || conn != rawPacket { - t.Fatal("an empty UDP filter should preserve the socket") + for _, empty := range []*Config{{}, {AllowedIps: []*geodata.IPRule{}}} { + rawListener := new(testListener) + rawPacket := new(testPacketConn) + if l, err := empty.Listen(rawListener); err != nil || l != rawListener { + t.Fatal("an empty TCP filter should preserve the listener") + } + if conn, err := empty.WrapPacketConnServer(rawPacket, nil, nil); err != nil || conn != rawPacket { + t.Fatal("an empty UDP filter should preserve the socket") + } } } func TestUDPFilterReadError(t *testing.T) { - for _, test := range []struct { - name string - source net.Addr - blocked bool - }{ - {"blocked", &net.UDPAddr{IP: net.ParseIP("192.0.2.1")}, true}, - {"allowed", &net.UDPAddr{IP: net.ParseIP("198.51.100.1")}, false}, - {"missing address", nil, true}, - } { - t.Run(test.name, func(t *testing.T) { - source := test.source - raw := &testPacketConn{reads: []datagram{{data: []byte("partial"), addr: source, err: io.ErrShortBuffer}}} - conn, err := testConfig(t, "192.0.2.0/24").WrapPacketConnServer(raw, nil, nil) - if err != nil { - t.Fatal(err) - } - buf := make([]byte, 32) - n, addr, err := conn.ReadFrom(buf) - if !errors.Is(err, io.ErrShortBuffer) { - t.Fatalf("read error was not preserved: %v", err) - } - if test.blocked { - if n != 0 || addr != nil { - t.Fatal("partial data from a blocked source was returned with an error") - } - } else if n != 7 || addr != source || string(buf[:n]) != "partial" { - t.Fatal("partial data from an unblocked source was changed") + for _, policy := range testPolicies(t) { + t.Run(policy.name, func(t *testing.T) { + for _, test := range []struct { + name string + source net.Addr + blocked bool + }{ + {"blocked", &net.UDPAddr{IP: net.ParseIP("192.0.2.1")}, true}, + {"allowed", &net.UDPAddr{IP: net.ParseIP("198.51.100.1")}, false}, + {"missing address", nil, len(policy.config.AllowedIps) > 0}, + {"unknown address", &net.UnixAddr{Name: "ipfilter.sock", Net: "unixgram"}, len(policy.config.AllowedIps) > 0}, + } { + t.Run(test.name, func(t *testing.T) { + source := test.source + raw := &testPacketConn{reads: []datagram{{data: []byte("partial"), addr: source, err: io.ErrShortBuffer}}} + conn, err := policy.config.WrapPacketConnServer(raw, nil, nil) + if err != nil { + t.Fatal(err) + } + buf := make([]byte, 32) + n, addr, err := conn.ReadFrom(buf) + if !errors.Is(err, io.ErrShortBuffer) { + t.Fatalf("read error was not preserved: %v", err) + } + if test.blocked { + if n != 0 || addr != nil { + t.Fatal("partial data from a blocked source was returned with an error") + } + } else if n != 7 || addr != source || string(buf[:n]) != "partial" { + t.Fatal("partial data from an unblocked source was changed") + } + }) } }) } } +func testPolicies(t *testing.T) []struct { + name string + config *Config +} { + t.Helper() + blocked := testConfig(t, "192.0.2.0/24", "2001:db8::/48").BlockedIps + allowed := testConfig(t, "198.51.100.0/24", "2001:db8:1::/48").BlockedIps + broader := testConfig(t, "192.0.2.0/24", "198.51.100.0/24", "2001:db8::/32").BlockedIps + return []struct { + name string + config *Config + }{ + {"blacklist", &Config{BlockedIps: blocked}}, + {"whitelist", &Config{AllowedIps: allowed}}, + {"both", &Config{BlockedIps: blocked, AllowedIps: broader}}, + } +} + func testConfig(t *testing.T, entries ...string) *Config { t.Helper() rules, err := geodata.ParseIPRules(entries)