mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-10-20 16:24:30 +00:00
add whitelist
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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() }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user