summaryrefslogtreecommitdiffhomepage
path: root/test/iptables
diff options
context:
space:
mode:
Diffstat (limited to 'test/iptables')
-rw-r--r--test/iptables/BUILD2
-rw-r--r--test/iptables/filter_input.go198
-rw-r--r--test/iptables/iptables_test.go32
-rw-r--r--test/iptables/iptables_util.go2
-rw-r--r--test/iptables/nat.go265
5 files changed, 472 insertions, 27 deletions
diff --git a/test/iptables/BUILD b/test/iptables/BUILD
index 66453772a..ae4bba847 100644
--- a/test/iptables/BUILD
+++ b/test/iptables/BUILD
@@ -15,7 +15,9 @@ go_library(
],
visibility = ["//test/iptables:__subpackages__"],
deps = [
+ "//pkg/binary",
"//pkg/test/testutil",
+ "//pkg/usermem",
],
)
diff --git a/test/iptables/filter_input.go b/test/iptables/filter_input.go
index 37a1a6694..c47660026 100644
--- a/test/iptables/filter_input.go
+++ b/test/iptables/filter_input.go
@@ -51,6 +51,12 @@ func init() {
RegisterTestCase(FilterInputInvertDestination{})
RegisterTestCase(FilterInputSource{})
RegisterTestCase(FilterInputInvertSource{})
+ RegisterTestCase(FilterInputInterfaceAccept{})
+ RegisterTestCase(FilterInputInterfaceDrop{})
+ RegisterTestCase(FilterInputInterface{})
+ RegisterTestCase(FilterInputInterfaceBeginsWith{})
+ RegisterTestCase(FilterInputInterfaceInvertDrop{})
+ RegisterTestCase(FilterInputInterfaceInvertAccept{})
}
// FilterInputDropUDP tests that we can drop UDP traffic.
@@ -744,3 +750,195 @@ func (FilterInputInvertSource) ContainerAction(ctx context.Context, ip net.IP, i
func (FilterInputInvertSource) LocalAction(ctx context.Context, ip net.IP, ipv6 bool) error {
return sendUDPLoop(ctx, ip, acceptPort)
}
+
+// FilterInputInterfaceAccept tests that packets are accepted from interface
+// matching the iptables rule.
+type FilterInputInterfaceAccept struct{ localCase }
+
+var _ TestCase = FilterInputInterfaceAccept{}
+
+// Name implements TestCase.Name.
+func (FilterInputInterfaceAccept) Name() string {
+ return "FilterInputInterfaceAccept"
+}
+
+// ContainerAction implements TestCase.ContainerAction.
+func (FilterInputInterfaceAccept) ContainerAction(ctx context.Context, ip net.IP, ipv6 bool) error {
+ ifname, ok := getInterfaceName()
+ if !ok {
+ return fmt.Errorf("no interface is present, except loopback")
+ }
+ if err := filterTable(ipv6, "-A", "INPUT", "-p", "udp", "-i", ifname, "-j", "ACCEPT"); err != nil {
+ return err
+ }
+ if err := listenUDP(ctx, acceptPort); err != nil {
+ return fmt.Errorf("packets on port %d should be allowed, but encountered an error: %w", acceptPort, err)
+ }
+
+ return nil
+}
+
+// LocalAction implements TestCase.LocalAction.
+func (FilterInputInterfaceAccept) LocalAction(ctx context.Context, ip net.IP, ipv6 bool) error {
+ return sendUDPLoop(ctx, ip, acceptPort)
+}
+
+// FilterInputInterfaceDrop tests that packets are dropped from interface
+// matching the iptables rule.
+type FilterInputInterfaceDrop struct{ localCase }
+
+var _ TestCase = FilterInputInterfaceDrop{}
+
+// Name implements TestCase.Name.
+func (FilterInputInterfaceDrop) Name() string {
+ return "FilterInputInterfaceDrop"
+}
+
+// ContainerAction implements TestCase.ContainerAction.
+func (FilterInputInterfaceDrop) ContainerAction(ctx context.Context, ip net.IP, ipv6 bool) error {
+ ifname, ok := getInterfaceName()
+ if !ok {
+ return fmt.Errorf("no interface is present, except loopback")
+ }
+ if err := filterTable(ipv6, "-A", "INPUT", "-p", "udp", "-i", ifname, "-j", "DROP"); err != nil {
+ return err
+ }
+ timedCtx, cancel := context.WithTimeout(ctx, NegativeTimeout)
+ defer cancel()
+ if err := listenUDP(timedCtx, acceptPort); err != nil {
+ if errors.Is(err, context.DeadlineExceeded) {
+ return nil
+ }
+ return fmt.Errorf("error reading: %w", err)
+ }
+ return fmt.Errorf("packets should have been dropped, but got a packet")
+}
+
+// LocalAction implements TestCase.LocalAction.
+func (FilterInputInterfaceDrop) LocalAction(ctx context.Context, ip net.IP, ipv6 bool) error {
+ return sendUDPLoop(ctx, ip, acceptPort)
+}
+
+// FilterInputInterface tests that packets are not dropped from interface which
+// is not matching the interface name in the iptables rule.
+type FilterInputInterface struct{ localCase }
+
+var _ TestCase = FilterInputInterface{}
+
+// Name implements TestCase.Name.
+func (FilterInputInterface) Name() string {
+ return "FilterInputInterface"
+}
+
+// ContainerAction implements TestCase.ContainerAction.
+func (FilterInputInterface) ContainerAction(ctx context.Context, ip net.IP, ipv6 bool) error {
+ if err := filterTable(ipv6, "-A", "INPUT", "-p", "udp", "-i", "lo", "-j", "DROP"); err != nil {
+ return err
+ }
+ if err := listenUDP(ctx, acceptPort); err != nil {
+ return fmt.Errorf("packets on port %d should be allowed, but encountered an error: %w", acceptPort, err)
+ }
+ return nil
+}
+
+// LocalAction implements TestCase.LocalAction.
+func (FilterInputInterface) LocalAction(ctx context.Context, ip net.IP, ipv6 bool) error {
+ return sendUDPLoop(ctx, ip, acceptPort)
+}
+
+// FilterInputInterfaceBeginsWith tests that packets are dropped from an
+// interface which begins with the given interface name.
+type FilterInputInterfaceBeginsWith struct{ localCase }
+
+var _ TestCase = FilterInputInterfaceBeginsWith{}
+
+// Name implements TestCase.Name.
+func (FilterInputInterfaceBeginsWith) Name() string {
+ return "FilterInputInterfaceBeginsWith"
+}
+
+// ContainerAction implements TestCase.ContainerAction.
+func (FilterInputInterfaceBeginsWith) ContainerAction(ctx context.Context, ip net.IP, ipv6 bool) error {
+ if err := filterTable(ipv6, "-A", "INPUT", "-p", "udp", "-i", "e+", "-j", "DROP"); err != nil {
+ return err
+ }
+ timedCtx, cancel := context.WithTimeout(ctx, NegativeTimeout)
+ defer cancel()
+ if err := listenUDP(timedCtx, acceptPort); err != nil {
+ if errors.Is(err, context.DeadlineExceeded) {
+ return nil
+ }
+ return fmt.Errorf("error reading: %w", err)
+ }
+ return fmt.Errorf("packets should have been dropped, but got a packet")
+}
+
+// LocalAction implements TestCase.LocalAction.
+func (FilterInputInterfaceBeginsWith) LocalAction(ctx context.Context, ip net.IP, ipv6 bool) error {
+ return sendUDPLoop(ctx, ip, acceptPort)
+}
+
+// FilterInputInterfaceInvertDrop tests that we selectively drop packets from
+// interface not matching the interface name.
+type FilterInputInterfaceInvertDrop struct{ baseCase }
+
+var _ TestCase = FilterInputInterfaceInvertDrop{}
+
+// Name implements TestCase.Name.
+func (FilterInputInterfaceInvertDrop) Name() string {
+ return "FilterInputInterfaceInvertDrop"
+}
+
+// ContainerAction implements TestCase.ContainerAction.
+func (FilterInputInterfaceInvertDrop) ContainerAction(ctx context.Context, ip net.IP, ipv6 bool) error {
+ if err := filterTable(ipv6, "-A", "INPUT", "-p", "tcp", "!", "-i", "lo", "-j", "DROP"); err != nil {
+ return err
+ }
+ timedCtx, cancel := context.WithTimeout(ctx, NegativeTimeout)
+ defer cancel()
+ if err := listenTCP(timedCtx, acceptPort); err != nil {
+ if errors.Is(err, context.DeadlineExceeded) {
+ return nil
+ }
+ return fmt.Errorf("error reading: %w", err)
+ }
+ return fmt.Errorf("connection on port %d should not be accepted, but was accepted", acceptPort)
+}
+
+// LocalAction implements TestCase.LocalAction.
+func (FilterInputInterfaceInvertDrop) LocalAction(ctx context.Context, ip net.IP, ipv6 bool) error {
+ timedCtx, cancel := context.WithTimeout(ctx, NegativeTimeout)
+ defer cancel()
+ if err := connectTCP(timedCtx, ip, acceptPort); err != nil {
+ var operr *net.OpError
+ if errors.As(err, &operr) && operr.Timeout() {
+ return nil
+ }
+ return fmt.Errorf("error connecting: %w", err)
+ }
+ return fmt.Errorf("connection destined to port %d should not be accepted, but was accepted", acceptPort)
+}
+
+// FilterInputInterfaceInvertAccept tests that we can selectively accept packets
+// not matching the specific incoming interface.
+type FilterInputInterfaceInvertAccept struct{ baseCase }
+
+var _ TestCase = FilterInputInterfaceInvertAccept{}
+
+// Name implements TestCase.Name.
+func (FilterInputInterfaceInvertAccept) Name() string {
+ return "FilterInputInterfaceInvertAccept"
+}
+
+// ContainerAction implements TestCase.ContainerAction.
+func (FilterInputInterfaceInvertAccept) ContainerAction(ctx context.Context, ip net.IP, ipv6 bool) error {
+ if err := filterTable(ipv6, "-A", "INPUT", "-p", "tcp", "!", "-i", "lo", "-j", "ACCEPT"); err != nil {
+ return err
+ }
+ return listenTCP(ctx, acceptPort)
+}
+
+// LocalAction implements TestCase.LocalAction.
+func (FilterInputInterfaceInvertAccept) LocalAction(ctx context.Context, ip net.IP, ipv6 bool) error {
+ return connectTCP(ctx, ip, acceptPort)
+}
diff --git a/test/iptables/iptables_test.go b/test/iptables/iptables_test.go
index 4733146c0..ef92e3fff 100644
--- a/test/iptables/iptables_test.go
+++ b/test/iptables/iptables_test.go
@@ -392,6 +392,30 @@ func TestInputInvertSource(t *testing.T) {
singleTest(t, FilterInputInvertSource{})
}
+func TestInputInterfaceAccept(t *testing.T) {
+ singleTest(t, FilterInputInterfaceAccept{})
+}
+
+func TestInputInterfaceDrop(t *testing.T) {
+ singleTest(t, FilterInputInterfaceDrop{})
+}
+
+func TestInputInterface(t *testing.T) {
+ singleTest(t, FilterInputInterface{})
+}
+
+func TestInputInterfaceBeginsWith(t *testing.T) {
+ singleTest(t, FilterInputInterfaceBeginsWith{})
+}
+
+func TestInputInterfaceInvertDrop(t *testing.T) {
+ singleTest(t, FilterInputInterfaceInvertDrop{})
+}
+
+func TestInputInterfaceInvertAccept(t *testing.T) {
+ singleTest(t, FilterInputInterfaceInvertAccept{})
+}
+
func TestFilterAddrs(t *testing.T) {
tcs := []struct {
ipv6 bool
@@ -424,3 +448,11 @@ func TestNATPreOriginalDst(t *testing.T) {
func TestNATOutOriginalDst(t *testing.T) {
singleTest(t, NATOutOriginalDst{})
}
+
+func TestNATPreRECVORIGDSTADDR(t *testing.T) {
+ singleTest(t, NATPreRECVORIGDSTADDR{})
+}
+
+func TestNATOutRECVORIGDSTADDR(t *testing.T) {
+ singleTest(t, NATOutRECVORIGDSTADDR{})
+}
diff --git a/test/iptables/iptables_util.go b/test/iptables/iptables_util.go
index a6ec5cca3..4cd770a65 100644
--- a/test/iptables/iptables_util.go
+++ b/test/iptables/iptables_util.go
@@ -171,7 +171,7 @@ func connectTCP(ctx context.Context, ip net.IP, port int) error {
return err
}
if err := testutil.PollContext(ctx, callback); err != nil {
- return fmt.Errorf("timed out waiting to connect IP on port %v, most recent error: %v", port, err)
+ return fmt.Errorf("timed out waiting to connect IP on port %v, most recent error: %w", port, err)
}
return nil
diff --git a/test/iptables/nat.go b/test/iptables/nat.go
index 495241482..c3874240f 100644
--- a/test/iptables/nat.go
+++ b/test/iptables/nat.go
@@ -20,6 +20,9 @@ import (
"fmt"
"net"
"syscall"
+
+ "gvisor.dev/gvisor/pkg/binary"
+ "gvisor.dev/gvisor/pkg/usermem"
)
const redirectPort = 42
@@ -43,6 +46,8 @@ func init() {
RegisterTestCase(NATLoopbackSkipsPrerouting{})
RegisterTestCase(NATPreOriginalDst{})
RegisterTestCase(NATOutOriginalDst{})
+ RegisterTestCase(NATPreRECVORIGDSTADDR{})
+ RegisterTestCase(NATOutRECVORIGDSTADDR{})
}
// NATPreRedirectUDPPort tests that packets are redirected to different port.
@@ -538,9 +543,9 @@ func (NATOutOriginalDst) LocalAction(ctx context.Context, ip net.IP, ipv6 bool)
}
func listenForRedirectedConn(ctx context.Context, ipv6 bool, originalDsts []net.IP) error {
- // The net package doesn't give guarantee access to the connection's
+ // The net package doesn't give guaranteed access to the connection's
// underlying FD, and thus we cannot call getsockopt. We have to use
- // traditional syscalls for SO_ORIGINAL_DST.
+ // traditional syscalls.
// Create the listening socket, bind, listen, and accept.
family := syscall.AF_INET
@@ -609,36 +614,14 @@ func listenForRedirectedConn(ctx context.Context, ipv6 bool, originalDsts []net.
if err != nil {
return err
}
- // The original destination could be any of our IPs.
- for _, dst := range originalDsts {
- want := syscall.RawSockaddrInet6{
- Family: syscall.AF_INET6,
- Port: htons(dropPort),
- }
- copy(want.Addr[:], dst.To16())
- if got == want {
- return nil
- }
- }
- return fmt.Errorf("SO_ORIGINAL_DST returned %+v, but wanted one of %+v (note: port numbers are in network byte order)", got, originalDsts)
+ return addrMatches6(got, originalDsts, dropPort)
}
got, err := originalDestination4(connFD)
if err != nil {
return err
}
- // The original destination could be any of our IPs.
- for _, dst := range originalDsts {
- want := syscall.RawSockaddrInet4{
- Family: syscall.AF_INET,
- Port: htons(dropPort),
- }
- copy(want.Addr[:], dst.To4())
- if got == want {
- return nil
- }
- }
- return fmt.Errorf("SO_ORIGINAL_DST returned %+v, but wanted one of %+v (note: port numbers are in network byte order)", got, originalDsts)
+ return addrMatches4(got, originalDsts, dropPort)
}
// loopbackTests runs an iptables rule and ensures that packets sent to
@@ -662,3 +645,233 @@ func loopbackTest(ctx context.Context, ipv6 bool, dest net.IP, args ...string) e
return err
}
}
+
+// NATPreRECVORIGDSTADDR tests that IP{V6}_RECVORIGDSTADDR gets the post-NAT
+// address on the PREROUTING chain.
+type NATPreRECVORIGDSTADDR struct{ containerCase }
+
+// Name implements TestCase.Name.
+func (NATPreRECVORIGDSTADDR) Name() string {
+ return "NATPreRECVORIGDSTADDR"
+}
+
+// ContainerAction implements TestCase.ContainerAction.
+func (NATPreRECVORIGDSTADDR) ContainerAction(ctx context.Context, ip net.IP, ipv6 bool) error {
+ if err := natTable(ipv6, "-A", "PREROUTING", "-p", "udp", "-j", "REDIRECT", "--to-ports", fmt.Sprintf("%d", redirectPort)); err != nil {
+ return err
+ }
+
+ if err := recvWithRECVORIGDSTADDR(ctx, ipv6, nil, redirectPort); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// LocalAction implements TestCase.LocalAction.
+func (NATPreRECVORIGDSTADDR) LocalAction(ctx context.Context, ip net.IP, ipv6 bool) error {
+ return sendUDPLoop(ctx, ip, acceptPort)
+}
+
+// NATOutRECVORIGDSTADDR tests that IP{V6}_RECVORIGDSTADDR gets the post-NAT
+// address on the OUTPUT chain.
+type NATOutRECVORIGDSTADDR struct{ containerCase }
+
+// Name implements TestCase.Name.
+func (NATOutRECVORIGDSTADDR) Name() string {
+ return "NATOutRECVORIGDSTADDR"
+}
+
+// ContainerAction implements TestCase.ContainerAction.
+func (NATOutRECVORIGDSTADDR) ContainerAction(ctx context.Context, ip net.IP, ipv6 bool) error {
+ if err := natTable(ipv6, "-A", "OUTPUT", "-p", "udp", "-j", "REDIRECT", "--to-ports", fmt.Sprintf("%d", redirectPort)); err != nil {
+ return err
+ }
+
+ sendCh := make(chan error)
+ go func() {
+ // Packets will be sent to a non-container IP and redirected
+ // back to the container.
+ sendCh <- sendUDPLoop(ctx, ip, acceptPort)
+ }()
+
+ expectedIP := &net.IP{127, 0, 0, 1}
+ if ipv6 {
+ expectedIP = &net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
+ }
+ if err := recvWithRECVORIGDSTADDR(ctx, ipv6, expectedIP, redirectPort); err != nil {
+ return err
+ }
+
+ select {
+ case err := <-sendCh:
+ return err
+ default:
+ return nil
+ }
+}
+
+// LocalAction implements TestCase.LocalAction.
+func (NATOutRECVORIGDSTADDR) LocalAction(ctx context.Context, ip net.IP, ipv6 bool) error {
+ // No-op.
+ return nil
+}
+
+func recvWithRECVORIGDSTADDR(ctx context.Context, ipv6 bool, expectedDst *net.IP, port uint16) error {
+ // The net package doesn't give guaranteed access to a connection's
+ // underlying FD, and thus we cannot call getsockopt. We have to use
+ // traditional syscalls for IP_RECVORIGDSTADDR.
+
+ // Create the listening socket.
+ var (
+ family = syscall.AF_INET
+ level = syscall.SOL_IP
+ option = syscall.IP_RECVORIGDSTADDR
+ bindAddr syscall.Sockaddr = &syscall.SockaddrInet4{
+ Port: int(port),
+ Addr: [4]byte{0, 0, 0, 0}, // INADDR_ANY
+ }
+ )
+ if ipv6 {
+ family = syscall.AF_INET6
+ level = syscall.SOL_IPV6
+ option = 74 // IPV6_RECVORIGDSTADDR, which is missing from the syscall package.
+ bindAddr = &syscall.SockaddrInet6{
+ Port: int(port),
+ Addr: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // in6addr_any
+ }
+ }
+ sockfd, err := syscall.Socket(family, syscall.SOCK_DGRAM, 0)
+ if err != nil {
+ return fmt.Errorf("failed Socket(%d, %d, 0): %w", family, syscall.SOCK_DGRAM, err)
+ }
+ defer syscall.Close(sockfd)
+
+ if err := syscall.Bind(sockfd, bindAddr); err != nil {
+ return fmt.Errorf("failed Bind(%d, %+v): %v", sockfd, bindAddr, err)
+ }
+
+ // Enable IP_RECVORIGDSTADDR.
+ if err := syscall.SetsockoptInt(sockfd, level, option, 1); err != nil {
+ return fmt.Errorf("failed SetsockoptByte(%d, %d, %d, 1): %v", sockfd, level, option, err)
+ }
+
+ addrCh := make(chan interface{})
+ errCh := make(chan error)
+ go func() {
+ var addr interface{}
+ var err error
+ if ipv6 {
+ addr, err = recvOrigDstAddr6(sockfd)
+ } else {
+ addr, err = recvOrigDstAddr4(sockfd)
+ }
+ if err != nil {
+ errCh <- err
+ } else {
+ addrCh <- addr
+ }
+ }()
+
+ // Wait to receive a packet.
+ var addr interface{}
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case err := <-errCh:
+ return err
+ case addr = <-addrCh:
+ }
+
+ // Get a list of local IPs to verify that the packet now appears to have
+ // been sent to us.
+ var localAddrs []net.IP
+ if expectedDst != nil {
+ localAddrs = []net.IP{*expectedDst}
+ } else {
+ localAddrs, err = getInterfaceAddrs(ipv6)
+ if err != nil {
+ return fmt.Errorf("failed to get local interfaces: %w", err)
+ }
+ }
+
+ // Verify that the address has the post-NAT port and address.
+ if ipv6 {
+ return addrMatches6(addr.(syscall.RawSockaddrInet6), localAddrs, redirectPort)
+ }
+ return addrMatches4(addr.(syscall.RawSockaddrInet4), localAddrs, redirectPort)
+}
+
+func recvOrigDstAddr4(sockfd int) (syscall.RawSockaddrInet4, error) {
+ buf, err := recvOrigDstAddr(sockfd, syscall.SOL_IP, syscall.SizeofSockaddrInet4)
+ if err != nil {
+ return syscall.RawSockaddrInet4{}, err
+ }
+ var addr syscall.RawSockaddrInet4
+ binary.Unmarshal(buf, usermem.ByteOrder, &addr)
+ return addr, nil
+}
+
+func recvOrigDstAddr6(sockfd int) (syscall.RawSockaddrInet6, error) {
+ buf, err := recvOrigDstAddr(sockfd, syscall.SOL_IP, syscall.SizeofSockaddrInet6)
+ if err != nil {
+ return syscall.RawSockaddrInet6{}, err
+ }
+ var addr syscall.RawSockaddrInet6
+ binary.Unmarshal(buf, usermem.ByteOrder, &addr)
+ return addr, nil
+}
+
+func recvOrigDstAddr(sockfd int, level uintptr, addrSize int) ([]byte, error) {
+ buf := make([]byte, 64)
+ oob := make([]byte, syscall.CmsgSpace(addrSize))
+ for {
+ _, oobn, _, _, err := syscall.Recvmsg(
+ sockfd,
+ buf, // Message buffer.
+ oob, // Out-of-band buffer.
+ 0) // Flags.
+ if errors.Is(err, syscall.EINTR) {
+ continue
+ }
+ if err != nil {
+ return nil, fmt.Errorf("failed when calling Recvmsg: %w", err)
+ }
+ oob = oob[:oobn]
+
+ // Parse out the control message.
+ msgs, err := syscall.ParseSocketControlMessage(oob)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse control message: %w", err)
+ }
+ return msgs[0].Data, nil
+ }
+}
+
+func addrMatches4(got syscall.RawSockaddrInet4, wantAddrs []net.IP, port uint16) error {
+ for _, wantAddr := range wantAddrs {
+ want := syscall.RawSockaddrInet4{
+ Family: syscall.AF_INET,
+ Port: htons(port),
+ }
+ copy(want.Addr[:], wantAddr.To4())
+ if got == want {
+ return nil
+ }
+ }
+ return fmt.Errorf("got %+v, but wanted one of %+v (note: port numbers are in network byte order)", got, wantAddrs)
+}
+
+func addrMatches6(got syscall.RawSockaddrInet6, wantAddrs []net.IP, port uint16) error {
+ for _, wantAddr := range wantAddrs {
+ want := syscall.RawSockaddrInet6{
+ Family: syscall.AF_INET6,
+ Port: htons(port),
+ }
+ copy(want.Addr[:], wantAddr.To16())
+ if got == want {
+ return nil
+ }
+ }
+ return fmt.Errorf("got %+v, but wanted one of %+v (note: port numbers are in network byte order)", got, wantAddrs)
+}