Merge pull request #1943 from kevinGC:ipt-filter-ip

PiperOrigin-RevId: 301197007
This commit is contained in:
gVisor bot 2020-03-16 11:13:14 -07:00
commit 159a230b9b
9 changed files with 486 additions and 12 deletions

View File

@ -158,10 +158,32 @@ type IPTIP struct {
// Flags define matching behavior for the IP header.
Flags uint8
// InverseFlags invert the meaning of fields in struct IPTIP.
// InverseFlags invert the meaning of fields in struct IPTIP. See the
// IPT_INV_* flags.
InverseFlags uint8
}
// Flags in IPTIP.InverseFlags. Corresponding constants are in
// include/uapi/linux/netfilter_ipv4/ip_tables.h.
const (
// Invert the meaning of InputInterface.
IPT_INV_VIA_IN = 0x01
// Invert the meaning of OutputInterface.
IPT_INV_VIA_OUT = 0x02
// Unclear what this is, as no references to it exist in the kernel.
IPT_INV_TOS = 0x04
// Invert the meaning of Src.
IPT_INV_SRCIP = 0x08
// Invert the meaning of Dst.
IPT_INV_DSTIP = 0x10
// Invert the meaning of the IPT_F_FRAG flag.
IPT_INV_FRAG = 0x20
// Invert the meaning of the Protocol field.
IPT_INV_PROTO = 0x40
// Enable all flags.
IPT_INV_MASK = 0x7F
)
// SizeOfIPTIP is the size of an IPTIP.
const SizeOfIPTIP = 84

View File

@ -703,25 +703,34 @@ func filterFromIPTIP(iptip linux.IPTIP) (iptables.IPHeaderFilter, error) {
if containsUnsupportedFields(iptip) {
return iptables.IPHeaderFilter{}, fmt.Errorf("unsupported fields in struct iptip: %+v", iptip)
}
if len(iptip.Dst) != header.IPv4AddressSize || len(iptip.DstMask) != header.IPv4AddressSize {
return iptables.IPHeaderFilter{}, fmt.Errorf("incorrect length of destination (%d) and/or destination mask (%d) fields", len(iptip.Dst), len(iptip.DstMask))
}
return iptables.IPHeaderFilter{
Protocol: tcpip.TransportProtocolNumber(iptip.Protocol),
Protocol: tcpip.TransportProtocolNumber(iptip.Protocol),
Dst: tcpip.Address(iptip.Dst[:]),
DstMask: tcpip.Address(iptip.DstMask[:]),
DstInvert: iptip.InverseFlags&linux.IPT_INV_DSTIP != 0,
}, nil
}
func containsUnsupportedFields(iptip linux.IPTIP) bool {
// Currently we check that everything except protocol is zeroed.
// The following features are supported:
// - Protocol
// - Dst and DstMask
// - The inverse destination IP check flag
var emptyInetAddr = linux.InetAddr{}
var emptyInterface = [linux.IFNAMSIZ]byte{}
return iptip.Dst != emptyInetAddr ||
iptip.Src != emptyInetAddr ||
// Disable any supported inverse flags.
inverseMask := uint8(linux.IPT_INV_DSTIP)
return iptip.Src != emptyInetAddr ||
iptip.SrcMask != emptyInetAddr ||
iptip.DstMask != emptyInetAddr ||
iptip.InputInterface != emptyInterface ||
iptip.OutputInterface != emptyInterface ||
iptip.InputInterfaceMask != emptyInterface ||
iptip.OutputInterfaceMask != emptyInterface ||
iptip.Flags != 0 ||
iptip.InverseFlags != 0
iptip.InverseFlags&^inverseMask != 0
}
func validUnderflow(rule iptables.Rule) bool {

View File

@ -267,9 +267,8 @@ func (it *IPTables) checkRule(hook Hook, pkt tcpip.PacketBuffer, table Table, ru
pkt.NetworkHeader = pkt.Data.First()
}
// First check whether the packet matches the IP header filter.
// TODO(gvisor.dev/issue/170): Support other fields of the filter.
if rule.Filter.Protocol != 0 && rule.Filter.Protocol != header.IPv4(pkt.NetworkHeader).TransportProtocol() {
// Check whether the packet matches the IP header filter.
if !filterMatch(rule.Filter, header.IPv4(pkt.NetworkHeader)) {
// Continue on to the next rule.
return RuleJump, ruleIdx + 1
}
@ -290,3 +289,26 @@ func (it *IPTables) checkRule(hook Hook, pkt tcpip.PacketBuffer, table Table, ru
// All the matchers matched, so run the target.
return rule.Target.Action(pkt)
}
func filterMatch(filter IPHeaderFilter, hdr header.IPv4) bool {
// TODO(gvisor.dev/issue/170): Support other fields of the filter.
// Check the transport protocol.
if filter.Protocol != 0 && filter.Protocol != hdr.TransportProtocol() {
return false
}
// Check the destination IP.
dest := hdr.DestinationAddress()
matches := true
for i := range filter.Dst {
if dest[i]&filter.DstMask[i] != filter.Dst[i] {
matches = false
break
}
}
if matches == filter.DstInvert {
return false
}
return true
}

View File

@ -144,6 +144,18 @@ type Rule struct {
type IPHeaderFilter struct {
// Protocol matches the transport protocol.
Protocol tcpip.TransportProtocolNumber
// Dst matches the destination IP address.
Dst tcpip.Address
// DstMask masks bits of the destination IP address when comparing with
// Dst.
DstMask tcpip.Address
// DstInvert inverts the meaning of the destination IP check, i.e. when
// true the filter will match packets that fail the destination
// comparison.
DstInvert bool
}
// A Matcher is the interface for matching packets.

View File

@ -47,6 +47,8 @@ func init() {
RegisterTestCase(FilterInputJumpReturnDrop{})
RegisterTestCase(FilterInputJumpBuiltin{})
RegisterTestCase(FilterInputJumpTwice{})
RegisterTestCase(FilterInputDestination{})
RegisterTestCase(FilterInputInvertDestination{})
}
// FilterInputDropUDP tests that we can drop UDP traffic.
@ -596,3 +598,66 @@ func (FilterInputJumpTwice) ContainerAction(ip net.IP) error {
func (FilterInputJumpTwice) LocalAction(ip net.IP) error {
return sendUDPLoop(ip, acceptPort, sendloopDuration)
}
// FilterInputDestination verifies that we can filter packets via `-d
// <ipaddr>`.
type FilterInputDestination struct{}
// Name implements TestCase.Name.
func (FilterInputDestination) Name() string {
return "FilterInputDestination"
}
// ContainerAction implements TestCase.ContainerAction.
func (FilterInputDestination) ContainerAction(ip net.IP) error {
addrs, err := localAddrs()
if err != nil {
return err
}
// Make INPUT's default action DROP, then ACCEPT all packets bound for
// this machine.
rules := [][]string{{"-P", "INPUT", "DROP"}}
for _, addr := range addrs {
rules = append(rules, []string{"-A", "INPUT", "-d", addr, "-j", "ACCEPT"})
}
if err := filterTableRules(rules); err != nil {
return err
}
return listenUDP(acceptPort, sendloopDuration)
}
// LocalAction implements TestCase.LocalAction.
func (FilterInputDestination) LocalAction(ip net.IP) error {
return sendUDPLoop(ip, acceptPort, sendloopDuration)
}
// FilterInputInvertDestination verifies that we can filter packets via `! -d
// <ipaddr>`.
type FilterInputInvertDestination struct{}
// Name implements TestCase.Name.
func (FilterInputInvertDestination) Name() string {
return "FilterInputInvertDestination"
}
// ContainerAction implements TestCase.ContainerAction.
func (FilterInputInvertDestination) ContainerAction(ip net.IP) error {
// Make INPUT's default action DROP, then ACCEPT all packets not bound
// for 127.0.0.1.
rules := [][]string{
{"-P", "INPUT", "DROP"},
{"-A", "INPUT", "!", "-d", localIP, "-j", "ACCEPT"},
}
if err := filterTableRules(rules); err != nil {
return err
}
return listenUDP(acceptPort, sendloopDuration)
}
// LocalAction implements TestCase.LocalAction.
func (FilterInputInvertDestination) LocalAction(ip net.IP) error {
return sendUDPLoop(ip, acceptPort, sendloopDuration)
}

View File

@ -22,6 +22,8 @@ import (
func init() {
RegisterTestCase(FilterOutputDropTCPDestPort{})
RegisterTestCase(FilterOutputDropTCPSrcPort{})
RegisterTestCase(FilterOutputDestination{})
RegisterTestCase(FilterOutputInvertDestination{})
}
// FilterOutputDropTCPDestPort tests that connections are not accepted on
@ -87,3 +89,57 @@ func (FilterOutputDropTCPSrcPort) LocalAction(ip net.IP) error {
return nil
}
// FilterOutputDestination tests that we can selectively allow packets to
// certain destinations.
type FilterOutputDestination struct{}
// Name implements TestCase.Name.
func (FilterOutputDestination) Name() string {
return "FilterOutputDestination"
}
// ContainerAction implements TestCase.ContainerAction.
func (FilterOutputDestination) ContainerAction(ip net.IP) error {
rules := [][]string{
{"-A", "OUTPUT", "-d", ip.String(), "-j", "ACCEPT"},
{"-P", "OUTPUT", "DROP"},
}
if err := filterTableRules(rules); err != nil {
return err
}
return sendUDPLoop(ip, acceptPort, sendloopDuration)
}
// LocalAction implements TestCase.LocalAction.
func (FilterOutputDestination) LocalAction(ip net.IP) error {
return listenUDP(acceptPort, sendloopDuration)
}
// FilterOutputInvertDestination tests that we can selectively allow packets
// not headed for a particular destination.
type FilterOutputInvertDestination struct{}
// Name implements TestCase.Name.
func (FilterOutputInvertDestination) Name() string {
return "FilterOutputInvertDestination"
}
// ContainerAction implements TestCase.ContainerAction.
func (FilterOutputInvertDestination) ContainerAction(ip net.IP) error {
rules := [][]string{
{"-A", "OUTPUT", "!", "-d", localIP, "-j", "ACCEPT"},
{"-P", "OUTPUT", "DROP"},
}
if err := filterTableRules(rules); err != nil {
return err
}
return sendUDPLoop(ip, acceptPort, sendloopDuration)
}
// LocalAction implements TestCase.LocalAction.
func (FilterOutputInvertDestination) LocalAction(ip net.IP) error {
return listenUDP(acceptPort, sendloopDuration)
}

View File

@ -303,3 +303,69 @@ func TestJumpTwice(t *testing.T) {
t.Fatal(err)
}
}
func TestInputDestination(t *testing.T) {
if err := singleTest(FilterInputDestination{}); err != nil {
t.Fatal(err)
}
}
func TestInputInvertDestination(t *testing.T) {
if err := singleTest(FilterInputInvertDestination{}); err != nil {
t.Fatal(err)
}
}
func TestOutputDestination(t *testing.T) {
if err := singleTest(FilterOutputDestination{}); err != nil {
t.Fatal(err)
}
}
func TestOutputInvertDestination(t *testing.T) {
if err := singleTest(FilterOutputInvertDestination{}); err != nil {
t.Fatal(err)
}
}
func TestNATOutRedirectIP(t *testing.T) {
if err := singleTest(NATOutRedirectIP{}); err != nil {
t.Fatal(err)
}
}
func TestNATOutDontRedirectIP(t *testing.T) {
if err := singleTest(NATOutDontRedirectIP{}); err != nil {
t.Fatal(err)
}
}
func TestNATOutRedirectInvert(t *testing.T) {
if err := singleTest(NATOutRedirectInvert{}); err != nil {
t.Fatal(err)
}
}
func TestNATPreRedirectIP(t *testing.T) {
if err := singleTest(NATPreRedirectIP{}); err != nil {
t.Fatal(err)
}
}
func TestNATPreDontRedirectIP(t *testing.T) {
if err := singleTest(NATPreDontRedirectIP{}); err != nil {
t.Fatal(err)
}
}
func TestNATPreRedirectInvert(t *testing.T) {
if err := singleTest(NATPreRedirectInvert{}); err != nil {
t.Fatal(err)
}
}
func TestNATRedirectRequiresProtocol(t *testing.T) {
if err := singleTest(NATRedirectRequiresProtocol{}); err != nil {
t.Fatal(err)
}
}

View File

@ -24,6 +24,7 @@ import (
)
const iptablesBinary = "iptables"
const localIP = "127.0.0.1"
// filterTable calls `iptables -t filter` with the given args.
func filterTable(args ...string) error {
@ -46,8 +47,17 @@ func tableCmd(table string, args []string) error {
// filterTableRules is like filterTable, but runs multiple iptables commands.
func filterTableRules(argsList [][]string) error {
return tableRules("filter", argsList)
}
// natTableRules is like natTable, but runs multiple iptables commands.
func natTableRules(argsList [][]string) error {
return tableRules("nat", argsList)
}
func tableRules(table string, argsList [][]string) error {
for _, args := range argsList {
if err := filterTable(args...); err != nil {
if err := tableCmd(table, args); err != nil {
return err
}
}
@ -146,3 +156,16 @@ func connectTCP(ip net.IP, port int, timeout time.Duration) error {
return nil
}
// localAddrs returns a list of local network interface addresses.
func localAddrs() ([]string, error) {
addrs, err := net.InterfaceAddrs()
if err != nil {
return nil, err
}
addrStrs := make([]string, 0, len(addrs))
for _, addr := range addrs {
addrStrs = append(addrStrs, addr.String())
}
return addrStrs, nil
}

View File

@ -15,8 +15,10 @@
package iptables
import (
"errors"
"fmt"
"net"
"time"
)
const (
@ -28,6 +30,13 @@ func init() {
RegisterTestCase(NATRedirectTCPPort{})
RegisterTestCase(NATDropUDP{})
RegisterTestCase(NATAcceptAll{})
RegisterTestCase(NATPreRedirectIP{})
RegisterTestCase(NATPreDontRedirectIP{})
RegisterTestCase(NATPreRedirectInvert{})
RegisterTestCase(NATOutRedirectIP{})
RegisterTestCase(NATOutDontRedirectIP{})
RegisterTestCase(NATOutRedirectInvert{})
RegisterTestCase(NATRedirectRequiresProtocol{})
}
// NATRedirectUDPPort tests that packets are redirected to different port.
@ -79,7 +88,8 @@ func (NATRedirectTCPPort) LocalAction(ip net.IP) error {
return connectTCP(ip, dropPort, sendloopDuration)
}
// NATDropUDP tests that packets are not received in ports other than redirect port.
// NATDropUDP tests that packets are not received in ports other than redirect
// port.
type NATDropUDP struct{}
// Name implements TestCase.Name.
@ -130,3 +140,192 @@ func (NATAcceptAll) ContainerAction(ip net.IP) error {
func (NATAcceptAll) LocalAction(ip net.IP) error {
return sendUDPLoop(ip, acceptPort, sendloopDuration)
}
// NATOutRedirectIP uses iptables to select packets based on destination IP and
// redirects them.
type NATOutRedirectIP struct{}
// Name implements TestCase.Name.
func (NATOutRedirectIP) Name() string {
return "NATOutRedirectIP"
}
// ContainerAction implements TestCase.ContainerAction.
func (NATOutRedirectIP) ContainerAction(ip net.IP) error {
// Redirect OUTPUT packets to a listening localhost port.
dest := net.IP([]byte{200, 0, 0, 2})
return loopbackTest(dest, "-A", "OUTPUT", "-d", dest.String(), "-p", "udp", "-j", "REDIRECT", "--to-port", fmt.Sprintf("%d", acceptPort))
}
// LocalAction implements TestCase.LocalAction.
func (NATOutRedirectIP) LocalAction(ip net.IP) error {
// No-op.
return nil
}
// NATOutDontRedirectIP tests that iptables matching with "-d" does not match
// packets it shouldn't.
type NATOutDontRedirectIP struct{}
// Name implements TestCase.Name.
func (NATOutDontRedirectIP) Name() string {
return "NATOutDontRedirectIP"
}
// ContainerAction implements TestCase.ContainerAction.
func (NATOutDontRedirectIP) ContainerAction(ip net.IP) error {
if err := natTable("-A", "OUTPUT", "-d", localIP, "-p", "udp", "-j", "REDIRECT", "--to-port", fmt.Sprintf("%d", dropPort)); err != nil {
return err
}
return sendUDPLoop(ip, acceptPort, sendloopDuration)
}
// LocalAction implements TestCase.LocalAction.
func (NATOutDontRedirectIP) LocalAction(ip net.IP) error {
return listenUDP(acceptPort, sendloopDuration)
}
// NATOutRedirectInvert tests that iptables can match with "! -d".
type NATOutRedirectInvert struct{}
// Name implements TestCase.Name.
func (NATOutRedirectInvert) Name() string {
return "NATOutRedirectInvert"
}
// ContainerAction implements TestCase.ContainerAction.
func (NATOutRedirectInvert) ContainerAction(ip net.IP) error {
// Redirect OUTPUT packets to a listening localhost port.
dest := []byte{200, 0, 0, 3}
destStr := "200.0.0.2"
return loopbackTest(dest, "-A", "OUTPUT", "!", "-d", destStr, "-p", "udp", "-j", "REDIRECT", "--to-port", fmt.Sprintf("%d", acceptPort))
}
// LocalAction implements TestCase.LocalAction.
func (NATOutRedirectInvert) LocalAction(ip net.IP) error {
// No-op.
return nil
}
// NATPreRedirectIP tests that we can use iptables to select packets based on
// destination IP and redirect them.
type NATPreRedirectIP struct{}
// Name implements TestCase.Name.
func (NATPreRedirectIP) Name() string {
return "NATPreRedirectIP"
}
// ContainerAction implements TestCase.ContainerAction.
func (NATPreRedirectIP) ContainerAction(ip net.IP) error {
addrs, err := localAddrs()
if err != nil {
return err
}
var rules [][]string
for _, addr := range addrs {
rules = append(rules, []string{"-A", "PREROUTING", "-p", "udp", "-d", addr, "-j", "REDIRECT", "--to-ports", fmt.Sprintf("%d", acceptPort)})
}
if err := natTableRules(rules); err != nil {
return err
}
return listenUDP(acceptPort, sendloopDuration)
}
// LocalAction implements TestCase.LocalAction.
func (NATPreRedirectIP) LocalAction(ip net.IP) error {
return sendUDPLoop(ip, dropPort, sendloopDuration)
}
// NATPreDontRedirectIP tests that iptables matching with "-d" does not match
// packets it shouldn't.
type NATPreDontRedirectIP struct{}
// Name implements TestCase.Name.
func (NATPreDontRedirectIP) Name() string {
return "NATPreDontRedirectIP"
}
// ContainerAction implements TestCase.ContainerAction.
func (NATPreDontRedirectIP) ContainerAction(ip net.IP) error {
if err := natTable("-A", "PREROUTING", "-p", "udp", "-d", localIP, "-j", "REDIRECT", "--to-ports", fmt.Sprintf("%d", dropPort)); err != nil {
return err
}
return listenUDP(acceptPort, sendloopDuration)
}
// LocalAction implements TestCase.LocalAction.
func (NATPreDontRedirectIP) LocalAction(ip net.IP) error {
return sendUDPLoop(ip, acceptPort, sendloopDuration)
}
// NATPreRedirectInvert tests that iptables can match with "! -d".
type NATPreRedirectInvert struct{}
// Name implements TestCase.Name.
func (NATPreRedirectInvert) Name() string {
return "NATPreRedirectInvert"
}
// ContainerAction implements TestCase.ContainerAction.
func (NATPreRedirectInvert) ContainerAction(ip net.IP) error {
if err := natTable("-A", "PREROUTING", "-p", "udp", "!", "-d", localIP, "-j", "REDIRECT", "--to-ports", fmt.Sprintf("%d", acceptPort)); err != nil {
return err
}
return listenUDP(acceptPort, sendloopDuration)
}
// LocalAction implements TestCase.LocalAction.
func (NATPreRedirectInvert) LocalAction(ip net.IP) error {
return sendUDPLoop(ip, dropPort, sendloopDuration)
}
// NATRedirectRequiresProtocol tests that use of the --to-ports flag requires a
// protocol to be specified with -p.
type NATRedirectRequiresProtocol struct{}
// Name implements TestCase.Name.
func (NATRedirectRequiresProtocol) Name() string {
return "NATRedirectRequiresProtocol"
}
// ContainerAction implements TestCase.ContainerAction.
func (NATRedirectRequiresProtocol) ContainerAction(ip net.IP) error {
if err := natTable("-A", "PREROUTING", "-d", localIP, "-j", "REDIRECT", "--to-ports", fmt.Sprintf("%d", acceptPort)); err == nil {
return errors.New("expected an error using REDIRECT --to-ports without a protocol")
}
return nil
}
// LocalAction implements TestCase.LocalAction.
func (NATRedirectRequiresProtocol) LocalAction(ip net.IP) error {
// No-op.
return nil
}
// loopbackTests runs an iptables rule and ensures that packets sent to
// dest:dropPort are received by localhost:acceptPort.
func loopbackTest(dest net.IP, args ...string) error {
if err := natTable(args...); err != nil {
return err
}
sendCh := make(chan error)
listenCh := make(chan error)
go func() {
sendCh <- sendUDPLoop(dest, dropPort, sendloopDuration)
}()
go func() {
listenCh <- listenUDP(acceptPort, sendloopDuration)
}()
select {
case err := <-listenCh:
if err != nil {
return err
}
case <-time.After(sendloopDuration):
return errors.New("timed out")
}
// sendCh will always take the full sendloop time.
return <-sendCh
}