blob: 71b353586fe4b32323116415d934bfb8a2d184e2 [file] [log] [blame]
// Copyright 2021 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// +build !build_with_native_toolchain
package filter
import (
"testing"
"fidl/fuchsia/net"
"fidl/fuchsia/net/filter"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/link/loopback"
"gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
"gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
"gvisor.dev/gvisor/pkg/tcpip/stack"
"gvisor.dev/gvisor/pkg/tcpip/transport/icmp"
"gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
)
func TestFilterUpdates(t *testing.T) {
cmpOpts := []cmp.Option{
cmp.AllowUnexported(TCPSourcePortMatcher{}),
cmp.AllowUnexported(TCPDestinationPortMatcher{}),
cmp.AllowUnexported(UDPSourcePortMatcher{}),
cmp.AllowUnexported(UDPDestinationPortMatcher{}),
cmp.AllowUnexported(portMatcher{}),
// We aren't testing NIC filtering here.
cmpopts.IgnoreUnexported(filterDisabledNICMatcher{}),
}
defaultTables := stack.DefaultTables()
defaultV4Table := defaultTables.GetTable(stack.FilterID, false /* ipv6 */)
defaultV6Table := defaultTables.GetTable(stack.FilterID, true /* ipv6 */)
ipv4Addr1Bytes := [4]uint8{10}
const ipv4Addr1Prefix = 8
ipv4Subnet1 := net.Subnet{
Addr: net.IpAddressWithIpv4(net.Ipv4Address{Addr: ipv4Addr1Bytes}),
PrefixLen: ipv4Addr1Prefix,
}
ipv4TCPIPSubnet1 := tcpip.AddressWithPrefix{
Address: tcpip.Address(ipv4Addr1Bytes[:]),
PrefixLen: ipv4Addr1Prefix,
}.Subnet()
ipv4Addr2Bytes := [4]uint8{192}
const ipv4Addr2Prefix = 8
ipv4Subnet2 := net.Subnet{
Addr: net.IpAddressWithIpv4(net.Ipv4Address{Addr: ipv4Addr2Bytes}),
PrefixLen: ipv4Addr2Prefix,
}
ipv4TCPIPSubnet2 := tcpip.AddressWithPrefix{
Address: tcpip.Address(ipv4Addr2Bytes[:]),
PrefixLen: ipv4Addr2Prefix,
}.Subnet()
ipv6Addr1Bytes := [16]uint8{0xfe, 0x80}
const ipv6Addr1Prefix = 64
ipv6Subnet1 := net.Subnet{
Addr: net.IpAddressWithIpv6(net.Ipv6Address{Addr: ipv6Addr1Bytes}),
PrefixLen: ipv6Addr1Prefix,
}
ipv6TCPIPSubnet1 := tcpip.AddressWithPrefix{
Address: tcpip.Address(ipv6Addr1Bytes[:]),
PrefixLen: ipv6Addr1Prefix,
}.Subnet()
ipv6Addr2Bytes := [16]uint8{0xa0}
const ipv6Addr2Prefix = 96
ipv6Subnet2 := net.Subnet{
Addr: net.IpAddressWithIpv6(net.Ipv6Address{Addr: ipv6Addr2Bytes}),
PrefixLen: ipv6Addr2Prefix,
}
ipv6TCPIPSubnet2 := tcpip.AddressWithPrefix{
Address: tcpip.Address(ipv6Addr2Bytes[:]),
PrefixLen: ipv6Addr2Prefix,
}.Subnet()
var emptyFilterDisabledNICMatcher filterDisabledNICMatcher
emptyFilterDisabledNICMatcher.init()
tests := []struct {
name string
rules []filter.Rule
status filter.Status
v4Table stack.Table
v6Table stack.Table
}{
{
name: "empty",
rules: nil,
status: filter.StatusOk,
v4Table: defaultV4Table,
v6Table: defaultV6Table,
},
{
name: "port filter",
rules: []filter.Rule{
{
Action: filter.ActionDrop,
Direction: filter.DirectionIncoming,
Proto: filter.SocketProtocolTcp,
SrcPortRange: filter.PortRange{
Start: 1,
End: 2,
},
DstPortRange: filter.PortRange{
Start: 3,
End: 4,
},
},
{
Action: filter.ActionDrop,
Direction: filter.DirectionOutgoing,
Proto: filter.SocketProtocolIcmp,
// Port ranges are ignored for ICMP.
},
{
Action: filter.ActionPass,
Direction: filter.DirectionOutgoing,
Proto: filter.SocketProtocolUdp,
SrcPortRange: filter.PortRange{
Start: 9,
End: 10,
},
DstPortRange: filter.PortRange{
Start: 11,
End: 12,
},
},
{
Action: filter.ActionPass,
Direction: filter.DirectionIncoming,
Proto: filter.SocketProtocolIcmpv6,
// Port ranges are ignored for ICMPv6.
},
},
status: filter.StatusOk,
v4Table: stack.Table{
Rules: []stack.Rule{
// Initial Accept for non input/output hooks.
{
Target: &stack.AcceptTarget{},
},
// Input chain.
{
// Don't run filters on an interface with filters disabled.
Matchers: []stack.Matcher{
&emptyFilterDisabledNICMatcher,
},
Target: &stack.AcceptTarget{},
},
{
Filter: stack.IPHeaderFilter{
Protocol: tcp.ProtocolNumber,
CheckProtocol: true,
},
Matchers: []stack.Matcher{
NewTCPSourcePortMatcher(1, 2),
NewTCPDestinationPortMatcher(3, 4),
},
Target: &stack.DropTarget{},
},
{
Target: &stack.AcceptTarget{},
},
// Output chain.
{
// Don't run filters on an interface with filters disabled.
Matchers: []stack.Matcher{
&emptyFilterDisabledNICMatcher,
},
Target: &stack.AcceptTarget{},
},
{
Filter: stack.IPHeaderFilter{
Protocol: icmp.ProtocolNumber4,
CheckProtocol: true,
},
Matchers: nil,
Target: &stack.DropTarget{},
},
{
Filter: stack.IPHeaderFilter{
Protocol: udp.ProtocolNumber,
CheckProtocol: true,
},
Matchers: []stack.Matcher{
NewUDPSourcePortMatcher(9, 10),
NewUDPDestinationPortMatcher(11, 12),
},
Target: &stack.AcceptTarget{},
},
{
Target: &stack.AcceptTarget{},
},
},
BuiltinChains: [stack.NumHooks]int{
stack.Prerouting: 0,
stack.Input: 1,
stack.Forward: 0,
stack.Output: 4,
stack.Postrouting: 0,
},
},
v6Table: stack.Table{
Rules: []stack.Rule{
// Initial Accept for non input/output hooks.
{
Target: &stack.AcceptTarget{},
},
// Input chain.
{
// Don't run filters on an interface with filters disabled.
Matchers: []stack.Matcher{
&emptyFilterDisabledNICMatcher,
},
Target: &stack.AcceptTarget{},
},
{
Filter: stack.IPHeaderFilter{
Protocol: tcp.ProtocolNumber,
CheckProtocol: true,
},
Matchers: []stack.Matcher{
NewTCPSourcePortMatcher(1, 2),
NewTCPDestinationPortMatcher(3, 4),
},
Target: &stack.DropTarget{},
},
{
Filter: stack.IPHeaderFilter{
Protocol: icmp.ProtocolNumber6,
CheckProtocol: true,
},
Matchers: nil,
Target: &stack.AcceptTarget{},
},
{
Target: &stack.AcceptTarget{},
},
// Output chain.
{
// Don't run filters on an interface with filters disabled.
Matchers: []stack.Matcher{
&emptyFilterDisabledNICMatcher,
},
Target: &stack.AcceptTarget{},
},
{
Filter: stack.IPHeaderFilter{
Protocol: udp.ProtocolNumber,
CheckProtocol: true,
},
Matchers: []stack.Matcher{
NewUDPSourcePortMatcher(9, 10),
NewUDPDestinationPortMatcher(11, 12),
},
Target: &stack.AcceptTarget{},
},
{
Target: &stack.AcceptTarget{},
},
},
BuiltinChains: [stack.NumHooks]int{
stack.Prerouting: 0,
stack.Input: 1,
stack.Forward: 0,
stack.Output: 5,
stack.Postrouting: 0,
},
},
},
{
name: "address filters",
rules: []filter.Rule{
{
Action: filter.ActionPass,
Direction: filter.DirectionIncoming,
Proto: filter.SocketProtocolTcp,
SrcSubnet: &ipv4Subnet1,
SrcSubnetInvertMatch: false,
DstSubnet: nil,
DstSubnetInvertMatch: true,
},
{
Action: filter.ActionDrop,
Direction: filter.DirectionIncoming,
Proto: filter.SocketProtocolIcmp,
SrcSubnet: nil,
SrcSubnetInvertMatch: false,
DstSubnet: &ipv4Subnet2,
DstSubnetInvertMatch: true,
},
{
Action: filter.ActionDrop,
Direction: filter.DirectionOutgoing,
Proto: filter.SocketProtocolUdp,
SrcSubnet: &ipv6Subnet1,
SrcSubnetInvertMatch: true,
DstSubnet: nil,
DstSubnetInvertMatch: false,
},
{
Action: filter.ActionPass,
Direction: filter.DirectionOutgoing,
Proto: filter.SocketProtocolIcmpv6,
SrcSubnet: nil,
SrcSubnetInvertMatch: true,
DstSubnet: &ipv6Subnet2,
DstSubnetInvertMatch: false,
},
{
Action: filter.ActionDrop,
Direction: filter.DirectionOutgoing,
SrcSubnet: &ipv4Subnet1,
SrcSubnetInvertMatch: false,
DstSubnet: &ipv4Subnet2,
DstSubnetInvertMatch: true,
},
{
Action: filter.ActionPass,
Direction: filter.DirectionIncoming,
SrcSubnet: &ipv6Subnet1,
SrcSubnetInvertMatch: true,
DstSubnet: &ipv6Subnet2,
DstSubnetInvertMatch: false,
},
},
status: filter.StatusOk,
v4Table: stack.Table{
Rules: []stack.Rule{
// Initial Accept for non input/output hooks.
{
Target: &stack.AcceptTarget{},
},
// Input chain.
{
// Don't run filters on an interface with filters disabled.
Matchers: []stack.Matcher{
&emptyFilterDisabledNICMatcher,
},
Target: &stack.AcceptTarget{},
},
{
Filter: stack.IPHeaderFilter{
Protocol: tcp.ProtocolNumber,
CheckProtocol: true,
Src: ipv4TCPIPSubnet1.ID(),
SrcMask: tcpip.Address(ipv4TCPIPSubnet1.Mask()),
SrcInvert: false,
},
Target: &stack.AcceptTarget{},
},
{
Filter: stack.IPHeaderFilter{
Protocol: icmp.ProtocolNumber4,
CheckProtocol: true,
Dst: ipv4TCPIPSubnet2.ID(),
DstMask: tcpip.Address(ipv4TCPIPSubnet2.Mask()),
DstInvert: true,
},
Target: &stack.DropTarget{},
},
{
Target: &stack.AcceptTarget{},
},
// Output chain.
{
// Don't run filters on an interface with filters disabled.
Matchers: []stack.Matcher{
&emptyFilterDisabledNICMatcher,
},
Target: &stack.AcceptTarget{},
},
{
Filter: stack.IPHeaderFilter{
Src: ipv4TCPIPSubnet1.ID(),
SrcMask: tcpip.Address(ipv4TCPIPSubnet1.Mask()),
SrcInvert: false,
Dst: ipv4TCPIPSubnet2.ID(),
DstMask: tcpip.Address(ipv4TCPIPSubnet2.Mask()),
DstInvert: true,
},
Target: &stack.DropTarget{},
},
{
Target: &stack.AcceptTarget{},
},
},
BuiltinChains: [stack.NumHooks]int{
stack.Prerouting: 0,
stack.Input: 1,
stack.Forward: 0,
stack.Output: 5,
stack.Postrouting: 0,
},
},
v6Table: stack.Table{
Rules: []stack.Rule{
// Initial Accept for non input/output hooks.
{
Target: &stack.AcceptTarget{},
},
// Input chain.
{
// Don't run filters on an interface with filters disabled.
Matchers: []stack.Matcher{
&emptyFilterDisabledNICMatcher,
},
Target: &stack.AcceptTarget{},
},
{
Filter: stack.IPHeaderFilter{
Src: ipv6TCPIPSubnet1.ID(),
SrcMask: tcpip.Address(ipv6TCPIPSubnet1.Mask()),
SrcInvert: true,
Dst: ipv6TCPIPSubnet2.ID(),
DstMask: tcpip.Address(ipv6TCPIPSubnet2.Mask()),
DstInvert: false,
},
Target: &stack.AcceptTarget{},
},
{
Target: &stack.AcceptTarget{},
},
// Output chain.
{
// Don't run filters on an interface with filters disabled.
Matchers: []stack.Matcher{
&emptyFilterDisabledNICMatcher,
},
Target: &stack.AcceptTarget{},
},
{
Filter: stack.IPHeaderFilter{
Protocol: udp.ProtocolNumber,
CheckProtocol: true,
Src: ipv6TCPIPSubnet1.ID(),
SrcMask: tcpip.Address(ipv6TCPIPSubnet1.Mask()),
SrcInvert: true,
},
Matchers: nil,
Target: &stack.DropTarget{},
},
{
Filter: stack.IPHeaderFilter{
Protocol: icmp.ProtocolNumber6,
CheckProtocol: true,
Dst: ipv6TCPIPSubnet2.ID(),
DstMask: tcpip.Address(ipv6TCPIPSubnet2.Mask()),
DstInvert: false,
},
Target: &stack.AcceptTarget{},
},
{
Target: &stack.AcceptTarget{},
},
},
BuiltinChains: [stack.NumHooks]int{
stack.Prerouting: 0,
stack.Input: 1,
stack.Forward: 0,
stack.Output: 4,
stack.Postrouting: 0,
},
},
},
{
name: "IPv4 src with IPv6 dst",
rules: []filter.Rule{
{
Action: filter.ActionDrop,
SrcSubnet: &ipv4Subnet1,
DstSubnet: &ipv6Subnet1,
},
},
status: filter.StatusErrBadRule,
v4Table: defaultV4Table,
v6Table: defaultV6Table,
},
{
name: "IPv6 src with IPv4 dst",
rules: []filter.Rule{
{
Action: filter.ActionDrop,
SrcSubnet: &ipv6Subnet1,
DstSubnet: &ipv4Subnet1,
},
},
status: filter.StatusErrBadRule,
v4Table: defaultV4Table,
v6Table: defaultV6Table,
},
{
name: "IPv4 src with ICMPv6",
rules: []filter.Rule{
{
Action: filter.ActionDrop,
Proto: filter.SocketProtocolIcmpv6,
SrcSubnet: &ipv4Subnet1,
},
},
status: filter.StatusErrBadRule,
v4Table: defaultV4Table,
v6Table: defaultV6Table,
},
{
name: "IPv4 dst with ICMPv6",
rules: []filter.Rule{
{
Action: filter.ActionDrop,
Proto: filter.SocketProtocolIcmpv6,
DstSubnet: &ipv4Subnet1,
},
},
status: filter.StatusErrBadRule,
v4Table: defaultV4Table,
v6Table: defaultV6Table,
},
{
name: "IPv6 src with ICMPv4",
rules: []filter.Rule{
{
Action: filter.ActionDrop,
Proto: filter.SocketProtocolIcmp,
SrcSubnet: &ipv6Subnet1,
},
},
status: filter.StatusErrBadRule,
v4Table: defaultV4Table,
v6Table: defaultV6Table,
},
{
name: "IPv6 dst with ICMPv4",
rules: []filter.Rule{
{
Action: filter.ActionDrop,
Proto: filter.SocketProtocolIcmp,
DstSubnet: &ipv6Subnet1,
},
},
status: filter.StatusErrBadRule,
v4Table: defaultV4Table,
v6Table: defaultV6Table,
},
{
name: "unrecognized proto",
rules: []filter.Rule{
{
Action: filter.ActionDrop,
Proto: 255,
},
},
status: filter.StatusErrBadRule,
v4Table: defaultV4Table,
v6Table: defaultV6Table,
},
{
name: "unrecognized action",
rules: []filter.Rule{
{
Action: 255,
Proto: filter.SocketProtocolTcp,
},
},
status: filter.StatusErrBadRule,
v4Table: defaultV4Table,
v6Table: defaultV6Table,
},
{
name: "unrecognized direction",
rules: []filter.Rule{
{
Action: filter.ActionDrop,
Direction: 255,
Proto: filter.SocketProtocolTcp,
},
},
status: filter.StatusErrBadRule,
v4Table: defaultV4Table,
v6Table: defaultV6Table,
},
{
name: "bad src port range",
rules: []filter.Rule{
{
Action: filter.ActionDrop,
Direction: filter.DirectionIncoming,
Proto: filter.SocketProtocolTcp,
SrcPortRange: filter.PortRange{
Start: 2,
End: 1,
},
DstPortRange: filter.PortRange{
Start: 3,
End: 4,
},
},
},
status: filter.StatusErrBadRule,
v4Table: defaultV4Table,
v6Table: defaultV6Table,
},
{
name: "bad dst port range",
rules: []filter.Rule{
{
Action: filter.ActionDrop,
Direction: filter.DirectionIncoming,
Proto: filter.SocketProtocolTcp,
SrcPortRange: filter.PortRange{
Start: 1,
End: 2,
},
DstPortRange: filter.PortRange{
Start: 4,
End: 3,
},
},
},
status: filter.StatusErrBadRule,
v4Table: defaultV4Table,
v6Table: defaultV6Table,
},
{
// TODO(https://fxbug.dev/68501): Keep state is not supported.
name: "keep state",
rules: []filter.Rule{
{
Action: filter.ActionPass,
Direction: filter.DirectionOutgoing,
Proto: filter.SocketProtocolTcp,
SrcSubnet: nil,
SrcSubnetInvertMatch: true,
DstSubnet: &ipv6Subnet2,
DstSubnetInvertMatch: false,
KeepState: true,
},
},
status: filter.StatusErrBadRule,
v4Table: defaultV4Table,
v6Table: defaultV6Table,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
s := stack.New(stack.Options{
NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol, ipv6.NewProtocol},
})
f := New(s)
if status := f.updateRules(test.rules, 0); status != test.status {
t.Fatalf("got f.updateRules(_, 0) = %d, want = %d", status, test.status)
}
iptables := s.IPTables()
v4table := iptables.GetTable(stack.FilterID, false /* ipv6 */)
if diff := cmp.Diff(v4table, test.v4Table, cmpOpts...); diff != "" {
t.Errorf("IPv4 filter table mispatch (-want +got):\n%s", diff)
}
v6table := iptables.GetTable(stack.FilterID, true /* ipv6 */)
if diff := cmp.Diff(v6table, test.v6Table, cmpOpts...); diff != "" {
t.Errorf("IPv6 filter table mispatch (-want +got):\n%s", diff)
}
})
}
}
func TestFilterEnableInterface(t *testing.T) {
const (
nicID = 1
nicName = "lo"
)
tests := []struct {
name string
actions func(*Filter)
disabled bool
}{
{
name: "Initial state",
actions: func(*Filter) {
/* do nothing */
},
disabled: true,
},
{
name: "Enable",
actions: func(f *Filter) {
f.EnableInterface(nicID)
},
disabled: false,
},
{
name: "Enable, then Disable",
actions: func(f *Filter) {
f.EnableInterface(nicID)
f.DisableInterface(nicID)
},
disabled: true,
},
{
name: "Enable, then RemovedNIC",
actions: func(f *Filter) {
f.EnableInterface(nicID)
f.RemovedNIC(nicID)
},
disabled: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
s := stack.New(stack.Options{
NetworkProtocols: []stack.NetworkProtocolFactory{ipv6.NewProtocol},
})
nicOpts := stack.NICOptions{Name: nicName}
if err := s.CreateNICWithOptions(nicID, loopback.New(), nicOpts); err != nil {
t.Fatalf("CreateNICWithOptions(%d, _, %#v)= %s", nicID, nicOpts, err)
}
f := New(s)
test.actions(f)
if got, want := f.IsInterfaceDisabled(nicName), test.disabled; got != want {
t.Errorf("IsInterfaceDisabled(nicID): got = %t, want = %t", got, want)
}
})
}
}