diff options
29 files changed, 2190 insertions, 387 deletions
diff --git a/net/test/OWNERS b/net/test/OWNERS index f002a84..cbbfa70 100644 --- a/net/test/OWNERS +++ b/net/test/OWNERS @@ -1,2 +1,2 @@ -ek@google.com lorenzo@google.com +maze@google.com diff --git a/net/test/all_tests.py b/net/test/all_tests.py index 72d3c4e..bfba0e5 100755 --- a/net/test/all_tests.py +++ b/net/test/all_tests.py @@ -27,6 +27,7 @@ test_modules = [ 'leak_test', 'multinetwork_test', 'neighbour_test', + 'nf_test', 'pf_key_test', 'ping6_test', 'qtaguid_test', diff --git a/net/test/anycast_test.py b/net/test/anycast_test.py index 82130db..6222580 100755..100644 --- a/net/test/anycast_test.py +++ b/net/test/anycast_test.py @@ -93,7 +93,14 @@ class AnycastTest(multinetwork_base.MultiNetworkBaseTest): # This will hang if the kernel has the bug. thread = CloseFileDescriptorThread(self.tuns[netid]) thread.start() - time.sleep(0.1) + # Wait up to 3 seconds for the thread to finish, but + # continue and fail the test if the thread hangs. + + # For kernels with MPTCP ported, closing tun interface need more + # than 0.5 sec. DAD procedure within MPTCP fullmesh module takes + # more time, because duplicate address-timer takes a refcount + # on the IPv6-address, preventing it from getting closed. + thread.join(3) # Make teardown work. del self.tuns[netid] diff --git a/net/test/bpf.py b/net/test/bpf.py index c9ad264..43502bd 100755 --- a/net/test/bpf.py +++ b/net/test/bpf.py @@ -21,13 +21,24 @@ import csocket import cstruct import net_test import socket +import platform # __NR_bpf syscall numbers for various architectures. +# NOTE: If python inherited COMPAT_UTS_MACHINE, uname's 'machine' field will +# return the 32-bit architecture name, even if python itself is 64-bit. To work +# around this problem and pick the right syscall nr, we can additionally check +# the bitness of the python interpreter. Assume that the 64-bit architectures +# are not running with COMPAT_UTS_MACHINE and must be 64-bit at all times. # TODO: is there a better way of doing this? __NR_bpf = { - "aarch64": 280, - "armv8l": 386, - "x86_64": 321}[os.uname()[4]] + "aarch64-64bit": 280, + "armv7l-32bit": 386, + "armv8l-32bit": 386, + "armv8l-64bit": 280, + "i686-32bit": 357, + "i686-64bit": 321, + "x86_64-64bit": 321, +}[os.uname()[4] + "-" + platform.architecture()[0]] LOG_LEVEL = 1 LOG_SIZE = 65536 @@ -141,6 +152,7 @@ BPF_FUNC_unspec = 0 BPF_FUNC_map_lookup_elem = 1 BPF_FUNC_map_update_elem = 2 BPF_FUNC_map_delete_elem = 3 +BPF_FUNC_get_current_uid_gid = 15 BPF_FUNC_get_socket_cookie = 46 BPF_FUNC_get_socket_uid = 47 diff --git a/net/test/bpf_test.py b/net/test/bpf_test.py index e7a4edb..ea3e56b 100755 --- a/net/test/bpf_test.py +++ b/net/test/bpf_test.py @@ -30,9 +30,12 @@ import sock_diag libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True) HAVE_EBPF_ACCOUNTING = net_test.LINUX_VERSION >= (4, 9, 0) +HAVE_EBPF_SOCKET = net_test.LINUX_VERSION >= (4, 14, 0) KEY_SIZE = 8 VALUE_SIZE = 4 TOTAL_ENTRIES = 20 +TEST_UID = 54321 +TEST_GID = 12345 # Offset to store the map key in stack register REG10 key_offset = -8 # Offset to store the map value in stack register REG10 @@ -300,7 +303,7 @@ class BpfTest(net_test.NetworkTest): + INS_SK_FILTER_ACCEPT) self.prog_fd = BpfProgLoad(BPF_PROG_TYPE_SOCKET_FILTER, instructions) packet_count = 10 - uid = 12345 + uid = TEST_UID with net_test.RunAsUid(uid): self.assertRaisesErrno(errno.ENOENT, LookupMap, self.map_fd, uid) SocketUDPLoopBack(packet_count, 4, self.prog_fd) @@ -349,6 +352,10 @@ class BpfCgroupTest(net_test.NetworkTest): BpfProgDetach(self._cg_fd, BPF_CGROUP_INET_INGRESS) except socket.error: pass + try: + BpfProgDetach(self._cg_fd, BPF_CGROUP_INET_SOCK_CREATE) + except socket.error: + pass def testCgroupBpfAttach(self): self.prog_fd = BpfProgLoad(BPF_PROG_TYPE_CGROUP_SKB, INS_BPF_EXIT_BLOCK) @@ -385,14 +392,55 @@ class BpfCgroupTest(net_test.NetworkTest): + INS_CGROUP_ACCEPT + INS_PACK_COUNT_UPDATE + INS_CGROUP_ACCEPT) self.prog_fd = BpfProgLoad(BPF_PROG_TYPE_CGROUP_SKB, instructions) BpfProgAttach(self.prog_fd, self._cg_fd, BPF_CGROUP_INET_INGRESS) - uid = os.getuid() packet_count = 20 - SocketUDPLoopBack(packet_count, 4, None) - self.assertEquals(packet_count, LookupMap(self.map_fd, uid).value) - DeleteMap(self.map_fd, uid); - SocketUDPLoopBack(packet_count, 6, None) - self.assertEquals(packet_count, LookupMap(self.map_fd, uid).value) + uid = TEST_UID + with net_test.RunAsUid(uid): + self.assertRaisesErrno(errno.ENOENT, LookupMap, self.map_fd, uid) + SocketUDPLoopBack(packet_count, 4, None) + self.assertEquals(packet_count, LookupMap(self.map_fd, uid).value) + DeleteMap(self.map_fd, uid) + SocketUDPLoopBack(packet_count, 6, None) + self.assertEquals(packet_count, LookupMap(self.map_fd, uid).value) BpfProgDetach(self._cg_fd, BPF_CGROUP_INET_INGRESS) + def checkSocketCreate(self, family, socktype, success): + try: + sock = socket.socket(family, socktype, 0) + sock.close() + except socket.error, e: + if success: + self.fail("Failed to create socket family=%d type=%d err=%s" % + (family, socktype, os.strerror(e.errno))) + return; + if not success: + self.fail("unexpected socket family=%d type=%d created, should be blocked" % + (family, socktype)) + + + def trySocketCreate(self, success): + for family in [socket.AF_INET, socket.AF_INET6]: + for socktype in [socket.SOCK_DGRAM, socket.SOCK_STREAM]: + self.checkSocketCreate(family, socktype, success) + + @unittest.skipUnless(HAVE_EBPF_SOCKET, + "Cgroup BPF socket is not supported") + def testCgroupSocketCreateBlock(self): + instructions = [ + BpfFuncCall(BPF_FUNC_get_current_uid_gid), + BpfAlu64Imm(BPF_AND, BPF_REG_0, 0xfffffff), + BpfJumpImm(BPF_JNE, BPF_REG_0, TEST_UID, 2), + ] + instructions += INS_BPF_EXIT_BLOCK + INS_CGROUP_ACCEPT; + self.prog_fd = BpfProgLoad(BPF_PROG_TYPE_CGROUP_SOCK, instructions) + BpfProgAttach(self.prog_fd, self._cg_fd, BPF_CGROUP_INET_SOCK_CREATE) + with net_test.RunAsUid(TEST_UID): + # Socket creation with target uid should fail + self.trySocketCreate(False); + # Socket create with different uid should success + self.trySocketCreate(True) + BpfProgDetach(self._cg_fd, BPF_CGROUP_INET_SOCK_CREATE) + with net_test.RunAsUid(TEST_UID): + self.trySocketCreate(True) + if __name__ == "__main__": unittest.main() diff --git a/net/test/build_rootfs.sh b/net/test/build_rootfs.sh new file mode 100755 index 0000000..ce09da1 --- /dev/null +++ b/net/test/build_rootfs.sh @@ -0,0 +1,139 @@ +#!/bin/bash +# +# Copyright (C) 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +set -e + +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P) + +usage() { + echo -n "usage: $0 [-h] [-s wheezy|stretch] [-a amd64|arm64] " + echo "[-m http://mirror/debian] [-n net_test.rootfs.`date +%Y%m%d`]" + exit 1 +} + +mirror=http://ftp.debian.org/debian +debootstrap=debootstrap +suite=stretch +arch=amd64 + +while getopts ":hs:a:m:n:" opt; do + case $opt in + h) + usage + ;; + s) + if [ "$OPTARG" != "wheezy" -a "$OPTARG" != "stretch" ]; then + echo "Invalid suite: $OPTARG" >&2 + usage + fi + suite=$OPTARG + ;; + a) + if [ "$OPTARG" != "amd64" -a "$OPTARG" != "arm64" ]; then + echo "Invalid arch: $OPTARG" >&2 + usage + fi + arch=$OPTARG + ;; + m) + mirror=$OPTARG + ;; + n) + name=$OPTARG + ;; + \?) + echo "Invalid option: $OPTARG" >&2 + usage + ;; + :) + echo "Invalid option: $OPTARG requires an argument" >&2 + usage + ;; + esac +done + +name=net_test.rootfs.$arch.`date +%Y%m%d` + +# Switch to qemu-debootstrap for incompatible architectures +if [ "$arch" = "arm64" ]; then + debootstrap=qemu-debootstrap +fi + +# Sometimes it isn't obvious when the script fails +failure() { + echo "Filesystem generation process failed." >&2 +} +trap failure ERR + +# Import the package list for this release +packages=`cat $SCRIPT_DIR/rootfs/$suite.list | xargs | tr -s ' ' ','` + +# For the debootstrap intermediates +workdir=`mktemp -d` +workdir_remove() { + echo "Removing temporary files.." >&2 + sudo rm -rf $workdir +} +trap workdir_remove EXIT + +# Run the debootstrap first +cd $workdir +sudo $debootstrap --arch=$arch --variant=minbase --include=$packages \ + $suite . $mirror +# Workarounds for bugs in the debootstrap suite scripts +for mount in `cat /proc/mounts | cut -d' ' -f2 | grep -e ^$workdir`; do + echo "Unmounting mountpoint $mount.." >&2 + sudo umount $mount +done +# Copy the chroot preparation scripts, and enter the chroot +for file in $suite.sh common.sh net_test.sh; do + sudo cp -a $SCRIPT_DIR/rootfs/$file root/$file + sudo chown root:root root/$file +done +sudo chroot . /root/$suite.sh + +# Leave the workdir, to build the filesystem +cd - + +# For the final image mount +mount=`mktemp -d` +mount_remove() { + rmdir $mount + workdir_remove +} +trap mount_remove EXIT + +# Create a 1G empty ext3 filesystem +truncate -s 1G $name +mke2fs -F -t ext3 -L ROOT $name + +# Mount the new filesystem locally +sudo mount -o loop -t ext3 $name $mount +image_unmount() { + sudo umount $mount + mount_remove +} +trap image_unmount EXIT + +# Copy the patched debootstrap results into the new filesystem +sudo cp -a $workdir/* $mount + +# Fill the rest of the space with zeroes, to optimize compression +sudo dd if=/dev/zero of=$mount/sparse bs=1M 2>/dev/null || true +sudo rm -f $mount/sparse + +echo "Debian $suite for $arch filesystem generated at '$name'." diff --git a/net/test/csocket.py b/net/test/csocket.py index bdd501c..ccabf4a 100644 --- a/net/test/csocket.py +++ b/net/test/csocket.py @@ -17,6 +17,7 @@ import ctypes import ctypes.util import os +import re import socket import struct @@ -78,9 +79,9 @@ libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True) # TODO: Unlike most of this file, these functions aren't specific to wrapping C # library calls. Move them to a utils.py or constants.py file, once we have one. def LinuxVersion(): - # Example: "3.4.67-00753-gb7a556f". - # Get the part before the dash. - version = os.uname()[2].split("-")[0] + # Example: "3.4.67-00753-gb7a556f", "4.4.135+". + # Get the prefix consisting of digits and dots. + version = re.search("^[0-9.]*", os.uname()[2]).group() # Convert it into a tuple such as (3, 4, 67). That allows comparing versions # using < and >, since tuples are compared lexicographically. version = tuple(int(i) for i in version.split(".")) diff --git a/net/test/iproute.py b/net/test/iproute.py index 9cfafc6..8376eb6 100644 --- a/net/test/iproute.py +++ b/net/test/iproute.py @@ -147,6 +147,7 @@ NDA_DST = 1 NDA_LLADDR = 2 NDA_CACHEINFO = 3 NDA_PROBES = 4 +NDA_IFINDEX = 8 # Neighbour cache entry states. NUD_PERMANENT = 0x80 @@ -208,13 +209,17 @@ IFLA_PAD = 42 IFLA_XDP = 43 IFLA_EVENT = 44 -# linux/include/uapi/if_link.h +# include/uapi/linux/if_link.h IFLA_INFO_UNSPEC = 0 IFLA_INFO_KIND = 1 IFLA_INFO_DATA = 2 IFLA_INFO_XSTATS = 3 -# linux/if_tunnel.h +IFLA_XFRM_UNSPEC = 0 +IFLA_XFRM_LINK = 1 +IFLA_XFRM_IF_ID = 2 + +# include/uapi/linux/if_tunnel.h IFLA_VTI_UNSPEC = 0 IFLA_VTI_LINK = 1 IFLA_VTI_IKEY = 2 @@ -635,9 +640,10 @@ class IPRoute(netlink.NetlinkSocket): self._Neighbour(version, True, addr, lladdr, dev, state, flags=netlink.NLM_F_REPLACE) - def DumpNeighbours(self, version): + def DumpNeighbours(self, version, ifindex): ndmsg = NdMsg((self._AddressFamily(version), 0, 0, 0, 0)) - return self._Dump(RTM_GETNEIGH, ndmsg, NdMsg, "") + attrs = self._NlAttrU32(NDA_IFINDEX, ifindex) if ifindex else "" + return self._Dump(RTM_GETNEIGH, ndmsg, NdMsg, attrs) def ParseNeighbourMessage(self, msg): msg, _ = self._ParseNLMsg(msg, NdMsg) @@ -680,7 +686,7 @@ class IPRoute(netlink.NetlinkSocket): attrs = self._ParseAttributes(RTM_NEWLINK, IfinfoMsg, attrs) return attrs["IFLA_STATS64"] - def GetVtiInfoData(self, dev_name): + def GetIfinfoData(self, dev_name): """Returns an IFLA_INFO_DATA dict object for the specified interface.""" _, attrs = self.GetIfinfo(dev_name) attrs = self._ParseAttributes(RTM_NEWLINK, IfinfoMsg, attrs) @@ -743,6 +749,22 @@ class IPRoute(netlink.NetlinkSocket): flags |= netlink.NLM_F_EXCL return self._SendNlRequest(RTM_NEWLINK, ifinfo, flags) + def CreateXfrmInterface(self, dev_name, xfrm_if_id, underlying_ifindex): + """Creates an XFRM interface with the specified parameters.""" + # The netlink attribute structure is essentially identical to the one + # for VTI above (q.v). + ifdata = self._NlAttrU32(IFLA_XFRM_LINK, underlying_ifindex) + ifdata += self._NlAttrU32(IFLA_XFRM_IF_ID, xfrm_if_id) + + linkinfo = self._NlAttrStr(IFLA_INFO_KIND, "xfrm") + linkinfo += self._NlAttr(IFLA_INFO_DATA, ifdata) + + msg = IfinfoMsg().Pack() + msg += self._NlAttrStr(IFLA_IFNAME, dev_name) + msg += self._NlAttr(IFLA_LINKINFO, linkinfo) + + return self._SendNlRequest(RTM_NEWLINK, msg) + if __name__ == "__main__": iproute = IPRoute() diff --git a/net/test/neighbour_test.py b/net/test/neighbour_test.py index 2caba44..caf2e6e 100755 --- a/net/test/neighbour_test.py +++ b/net/test/neighbour_test.py @@ -87,14 +87,14 @@ class NeighbourTest(multinetwork_base.MultiNetworkBaseTest): self.netid = random.choice(self.tuns.keys()) self.ifindex = self.ifindices[self.netid] - def GetNeighbour(self, addr): + def GetNeighbour(self, addr, ifindex): version = csocket.AddressVersion(addr) - for msg, args in self.iproute.DumpNeighbours(version): + for msg, args in self.iproute.DumpNeighbours(version, ifindex): if args["NDA_DST"] == addr: return msg, args def GetNdEntry(self, addr): - return self.GetNeighbour(addr) + return self.GetNeighbour(addr, self.ifindex) def CheckNoNdEvents(self): self.assertRaisesErrno(errno.EAGAIN, self.sock.recvfrom, 4096, MSG_PEEK) diff --git a/net/test/net_test.py b/net/test/net_test.py index 6b19f54..1c7f32f 100755 --- a/net/test/net_test.py +++ b/net/test/net_test.py @@ -369,17 +369,17 @@ class RunAsUidGid(object): def __enter__(self): if self.uid: - self.saved_uid = os.geteuid() + self.saved_uids = os.getresuid() self.saved_groups = os.getgroups() os.setgroups(self.saved_groups + [AID_INET]) - os.seteuid(self.uid) + os.setresuid(self.uid, self.uid, self.saved_uids[0]) if self.gid: self.saved_gid = os.getgid() os.setgid(self.gid) def __exit__(self, unused_type, unused_value, unused_traceback): if self.uid: - os.seteuid(self.saved_uid) + os.setresuid(*self.saved_uids) os.setgroups(self.saved_groups) if self.gid: os.setgid(self.saved_gid) @@ -390,7 +390,6 @@ class RunAsUid(RunAsUidGid): def __init__(self, uid): RunAsUidGid.__init__(self, uid, 0) - class NetworkTest(unittest.TestCase): def assertRaisesErrno(self, err_num, f=None, *args): @@ -433,7 +432,7 @@ class NetworkTest(unittest.TestCase): if protocol.startswith("tcp"): # Real sockets have 5 extra numbers, timewait sockets have none. - end_regexp = "(| +[0-9]+ [0-9]+ [0-9]+ [0-9]+ -?[0-9]+|)$" + end_regexp = "(| +[0-9]+ [0-9]+ [0-9]+ [0-9]+ -?[0-9]+)$" elif re.match("icmp|udp|raw", protocol): # Drops. end_regexp = " +([0-9]+) *$" @@ -458,8 +457,11 @@ class NetworkTest(unittest.TestCase): # TODO: consider returning a dict or namedtuple instead. out = [] for line in lines: + m = regexp.match(line) + if m is None: + raise ValueError("Failed match on [%s]" % line) (_, src, dst, state, mem, - _, _, uid, _, _, refcnt, _, extra) = regexp.match(line).groups() + _, _, uid, _, _, refcnt, _, extra) = m.groups() out.append([src, dst, state, mem, uid, refcnt, extra]) return out diff --git a/net/test/net_test.sh b/net/test/net_test.sh index bade6de..72c67a9 100755 --- a/net/test/net_test.sh +++ b/net/test/net_test.sh @@ -1,4 +1,135 @@ #!/bin/bash +if [[ -n "${verbose}" ]]; then + echo 'Current working directory:' + echo " - according to builtin: [$(pwd)]" + echo " - according to /bin/pwd: [$(/bin/pwd)]" + echo + + echo 'Shell environment:' + env + echo + + echo -n "net_test.sh (pid $$, parent ${PPID}, tty $(tty)) running [$0] with args:" + for arg in "$@"; do + echo -n " [${arg}]" + done + echo + echo +fi + +if [[ "$(tty)" == '/dev/console' ]]; then + ARCH="$(uname -m)" + # Underscore is illegal in hostname, replace with hyphen + ARCH="${ARCH//_/-}" + + # setsid + /dev/tty{,AMA,S}0 allows bash's job control to work, ie. Ctrl+C/Z + if [[ -c '/dev/tty0' ]]; then + # exists in UML, does not exist on graphics/vga/curses-less QEMU + CON='/dev/tty0' + hostname "uml-${ARCH}" + elif [[ -c '/dev/ttyAMA0' ]]; then + # Qemu for arm (note: /dev/ttyS0 also exists for exitcode) + CON='/dev/ttyAMA0' + hostname "qemu-${ARCH}" + elif [[ -c '/dev/ttyS0' ]]; then + # Qemu for x86 (note: /dev/ttyS1 also exists for exitcode) + CON='/dev/ttyS0' + hostname "qemu-${ARCH}" + else + # Can't figure it out, job control won't work, tough luck + echo 'Unable to figure out proper console - job control will not work.' >&2 + CON='' + hostname "local-${ARCH}" + fi + + unset ARCH + + echo -n "$(hostname): Currently tty[/dev/console], but it should be [${CON}]..." + + if [[ -n "${CON}" ]]; then + # Redirect std{in,out,err} to the console equivalent tty + # which actually supports all standard tty ioctls + exec <"${CON}" >&"${CON}" + + # Bash wants to be session leader, hence need for setsid + echo " re-executing..." + exec /usr/bin/setsid "$0" "$@" + # If the above exec fails, we just fall through... + # (this implies failure to *find* setsid, not error return from bash, + # in practice due to image construction this cannot happen) + else + echo + fi + + # In case we fall through, clean up + unset CON +fi + +if [[ -n "${verbose}" ]]; then + echo 'TTY settings:' + stty + echo + + echo 'TTY settings (verbose):' + stty -a + echo + + echo 'Restoring TTY sanity...' +fi + +stty sane +stty 115200 +[[ -z "${console_cols}" ]] || stty columns "${console_cols}" +[[ -z "${console_rows}" ]] || stty rows "${console_rows}" + +if [[ -n "${verbose}" ]]; then + echo + + echo 'TTY settings:' + stty + echo + + echo 'TTY settings (verbose):' + stty -a + echo +fi + +# By the time we get here we should have a sane console: +# - 115200 baud rate +# - appropriate (and known) width and height (note: this assumes +# that the terminal doesn't get further resized) +# - it is no longer /dev/console, so job control should function +# (this means working ctrl+c [abort] and ctrl+z [suspend]) + + +# This defaults to 60 which is needlessly long during boot +# (we will reset it back to the default later) +echo 0 > /proc/sys/kernel/random/urandom_min_reseed_secs + +if [[ -n "${entropy}" ]]; then + echo "adding entropy from hex string [${entropy}]" >&2 + + # In kernel/include/uapi/linux/random.h RNDADDENTROPY is defined as + # _IOW('R', 0x03, int[2]) =(R is 0x52)= 0x40085203 = 1074287107 + /usr/bin/python 3>/dev/random <<EOF +import fcntl, struct +rnd = '${entropy}'.decode('base64') +fcntl.ioctl(3, 0x40085203, struct.pack('ii', len(rnd) * 8, len(rnd)) + rnd) +EOF + +fi + +# Make sure the urandom pool has a chance to initialize before we reset +# the reseed timer back to 60 seconds. One timer tick should be enough. +sleep 1.1 + +# By this point either 'random: crng init done' (newer kernels) +# or 'random: nonblocking pool is initialized' (older kernels) +# should have been printed out to dmesg/console. + +# Reset it back to boot time default +echo 60 > /proc/sys/kernel/random/urandom_min_reseed_secs + # In case IPv6 is compiled as a module. [ -f /proc/net/if_inet6 ] || insmod $DIR/kernel/net-next/net/ipv6/ipv6.ko @@ -22,6 +153,6 @@ fi echo -e "Running $net_test $net_test_args\n" $net_test $net_test_args -# Write exit code of net_test to /proc/exitcode so that the builder can use it +# Write exit code of net_test to a file so that the builder can use it # to signal failure if any tests fail. -echo $? >/proc/exitcode +echo $? >$net_test_exitcode diff --git a/net/test/nf_test.py b/net/test/nf_test.py new file mode 100755 index 0000000..cd6c976 --- /dev/null +++ b/net/test/nf_test.py @@ -0,0 +1,86 @@ +#!/usr/bin/python +# +# Copyright 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +import errno +from socket import * + +import multinetwork_base +import net_test + +_TEST_IP4_ADDR = "192.0.2.1" +_TEST_IP6_ADDR = "2001:db8::" + + +# Regression tests for interactions between kernel networking and netfilter +# +# These tests were added to ensure that the lookup path for local-ICMP errors +# do not cause failures. Specifically, local-ICMP packets do not have a +# net_device in the skb, and has been known to trigger bugs in surrounding code. +class NetilterRejectTargetTest(multinetwork_base.MultiNetworkBaseTest): + + def setUp(self): + multinetwork_base.MultiNetworkBaseTest.setUp(self) + net_test.RunIptablesCommand(4, "-A OUTPUT -d " + _TEST_IP4_ADDR + " -j REJECT") + net_test.RunIptablesCommand(6, "-A OUTPUT -d " + _TEST_IP6_ADDR + " -j REJECT") + + def tearDown(self): + net_test.RunIptablesCommand(4, "-D OUTPUT -d " + _TEST_IP4_ADDR + " -j REJECT") + net_test.RunIptablesCommand(6, "-D OUTPUT -d " + _TEST_IP6_ADDR + " -j REJECT") + multinetwork_base.MultiNetworkBaseTest.tearDown(self) + + # Test a rejected TCP connect. The responding ICMP may not have skb->dev set. + # This tests the local-ICMP output-input path. + def CheckRejectedTcp(self, version, addr): + sock = net_test.TCPSocket(net_test.GetAddressFamily(version)) + netid = self.RandomNetid() + self.SelectInterface(sock, netid, "mark") + + # Expect this to fail with ICMP unreachable + try: + sock.connect((addr, 53)) + except IOError: + pass + + def testRejectTcp4(self): + self.CheckRejectedTcp(4, _TEST_IP4_ADDR) + + def testRejectTcp6(self): + self.CheckRejectedTcp(6, _TEST_IP6_ADDR) + + # Test a rejected UDP connect. The responding ICMP may not have skb->dev set. + # This tests the local-ICMP output-input path. + def CheckRejectedUdp(self, version, addr): + sock = net_test.UDPSocket(net_test.GetAddressFamily(version)) + netid = self.RandomNetid() + self.SelectInterface(sock, netid, "mark") + + # Expect this to fail with ICMP unreachable + try: + sock.sendto(net_test.UDP_PAYLOAD, (addr, 53)) + except IOError: + pass + + def testRejectUdp4(self): + self.CheckRejectedUdp(4, _TEST_IP4_ADDR) + + def testRejectUdp6(self): + self.CheckRejectedUdp(6, _TEST_IP6_ADDR) + + +if __name__ == "__main__": + unittest.main()
\ No newline at end of file diff --git a/net/test/no_test b/net/test/no_test new file mode 100755 index 0000000..b23e556 --- /dev/null +++ b/net/test/no_test @@ -0,0 +1 @@ +#!/bin/true diff --git a/net/test/parallel_tests.sh b/net/test/parallel_tests.sh index 93a43c8..eb67421 100755 --- a/net/test/parallel_tests.sh +++ b/net/test/parallel_tests.sh @@ -15,7 +15,7 @@ function runtests() { local test=$3 local j=0 while ((j < runs)); do - $DIR/run_net_test.sh --readonly --builder --nobuild $test \ + $DIR/run_net_test.sh --builder --nobuild $test \ > /dev/null 2> $RESULTSDIR/results.$worker.$j j=$((j + 1)) echo -n "." >&2 diff --git a/net/test/parameterization_test.py b/net/test/parameterization_test.py new file mode 100755 index 0000000..8f9e130 --- /dev/null +++ b/net/test/parameterization_test.py @@ -0,0 +1,83 @@ +#!/usr/bin/python +# +# Copyright 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import itertools +import unittest + +import net_test +import util + + +def InjectTests(): + ParmeterizationTest.InjectTests() + + +# This test class ensures that the Parameterized Test generator in utils.py +# works properly. It injects test methods into itself, and ensures that they +# are generated as expected, and that the TestClosures being run are properly +# defined, and running different parameterized tests each time. +class ParmeterizationTest(net_test.NetworkTest): + tests_run_list = [] + + @staticmethod + def NameGenerator(a, b, c): + return str(a) + "_" + str(b) + "_" + str(c) + + @classmethod + def InjectTests(cls): + PARAMS_A = (1, 2) + PARAMS_B = (3, 4) + PARAMS_C = (5, 6) + + param_list = itertools.product(PARAMS_A, PARAMS_B, PARAMS_C) + util.InjectParameterizedTest(cls, param_list, cls.NameGenerator) + + def ParamTestDummyFunc(self, a, b, c): + self.tests_run_list.append( + "testDummyFunc_" + ParmeterizationTest.NameGenerator(a, b, c)) + + def testParameterization(self): + expected = [ + "testDummyFunc_1_3_5", + "testDummyFunc_1_3_6", + "testDummyFunc_1_4_5", + "testDummyFunc_1_4_6", + "testDummyFunc_2_3_5", + "testDummyFunc_2_3_6", + "testDummyFunc_2_4_5", + "testDummyFunc_2_4_6", + ] + + actual = [name for name in dir(self) if name.startswith("testDummyFunc")] + + # Check that name and contents are equal + self.assertEqual(len(expected), len(actual)) + self.assertEqual(sorted(expected), sorted(actual)) + + # Start a clean list, and run all the tests. + self.tests_run_list = list() + for test_name in expected: + test_method = getattr(self, test_name) + test_method() + + # Make sure all tests have been run with the correct parameters + for test_name in expected: + self.assertTrue(test_name in self.tests_run_list) + + +if __name__ == "__main__": + ParmeterizationTest.InjectTests() + unittest.main() diff --git a/net/test/qtaguid_test.py b/net/test/qtaguid_test.py index ad99a57..c121df2 100755 --- a/net/test/qtaguid_test.py +++ b/net/test/qtaguid_test.py @@ -27,7 +27,10 @@ import tcp_test CTRL_PROCPATH = "/proc/net/xt_qtaguid/ctrl" OTHER_UID_GID = 12345 +HAVE_QTAGUID = os.path.exists(CTRL_PROCPATH) + +@unittest.skipUnless(HAVE_QTAGUID, "xt_qtaguid not supported") class QtaguidTest(tcp_test.TcpBaseTest): def RunIptablesCommand(self, args): diff --git a/net/test/rootfs/common.sh b/net/test/rootfs/common.sh new file mode 100644 index 0000000..172d9b6 --- /dev/null +++ b/net/test/rootfs/common.sh @@ -0,0 +1,57 @@ +#!/bin/sh +# +# Copyright (C) 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +chroot_sanity_check() { + if [ ! -f /var/log/bootstrap.log ]; then + echo "Do not run this script directly!" + echo "This is supposed to be run from inside a debootstrap chroot!" + echo "Aborting." + exit 1 + fi +} + +chroot_cleanup() { + # Read-only root breaks booting via init + cat >/etc/fstab << EOF +tmpfs /tmp tmpfs defaults 0 0 +tmpfs /var/log tmpfs defaults 0 0 +tmpfs /var/tmp tmpfs defaults 0 0 +EOF + + # systemd will attempt to re-create this symlink if it does not exist, + # which fails if it is booting from a read-only root filesystem (which + # is normally the case). The syslink must be relative, not absolute, + # and it must point to /proc/self/mounts, not /proc/mounts. + ln -sf ../proc/self/mounts /etc/mtab + + # Remove contaminants coming from the debootstrap process + echo vm >/etc/hostname + echo "nameserver 127.0.0.1" >/etc/resolv.conf + + # Put the helper net_test.sh script into place + mv /root/net_test.sh /sbin/net_test.sh + + # Make sure the /host mountpoint exists for net_test.sh + mkdir /host + + # Disable the root password + passwd -d root + + # Clean up any junk created by the imaging process + rm -rf /var/lib/apt/lists/* /var/log/bootstrap.log /root/* /tmp/* + find /var/log -type f -exec rm -f '{}' ';' +} diff --git a/net/test/rootfs/net_test.sh b/net/test/rootfs/net_test.sh new file mode 100755 index 0000000..9c94d06 --- /dev/null +++ b/net/test/rootfs/net_test.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# +# Copyright (C) 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +mount -t proc none /proc +mount -t sysfs none /sys +mount -t tmpfs tmpfs /tmp +mount -t tmpfs tmpfs /run + +# If this system was booted under UML, it will always have a /proc/exitcode +# file. If it was booted natively or under QEMU, it will not have this file. +if [ -e /proc/exitcode ]; then + mount -t hostfs hostfs /host +else + mount -t 9p -o trans=virtio,version=9p2000.L host /host +fi + +test=$(cat /proc/cmdline | sed -re 's/.*net_test=([^ ]*).*/\1/g') +cd $(dirname $test) +./net_test.sh +poweroff -f diff --git a/net/test/rootfs/stretch.list b/net/test/rootfs/stretch.list new file mode 100644 index 0000000..fbeddde --- /dev/null +++ b/net/test/rootfs/stretch.list @@ -0,0 +1,33 @@ +apt +apt-utils +bash-completion +bsdmainutils +ca-certificates +file +gpgv +ifupdown +insserv +iputils-ping +less +libnetfilter-conntrack3 +libnfnetlink0 +mime-support +netbase +netcat-openbsd +netcat-traditional +net-tools +openssl +pciutils +procps +psmisc +python +python-scapy +strace +systemd-sysv +tcpdump +traceroute +udev +udhcpc +usbutils +vim-tiny +wget diff --git a/net/test/rootfs/stretch.sh b/net/test/rootfs/stretch.sh new file mode 100755 index 0000000..6d8a9a4 --- /dev/null +++ b/net/test/rootfs/stretch.sh @@ -0,0 +1,150 @@ +#!/bin/bash +# +# Copyright (C) 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +set -e + +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P) + +. $SCRIPT_DIR/common.sh + +chroot_sanity_check + +cd /root + +# Add the needed debian sources +cat >/etc/apt/sources.list <<EOF +deb http://ftp.debian.org/debian stretch main +deb-src http://ftp.debian.org/debian stretch main +deb http://ftp.debian.org/debian stretch-backports main +deb-src http://ftp.debian.org/debian stretch-backports main +deb http://ftp.debian.org/debian buster main +deb-src http://ftp.debian.org/debian buster main +EOF + +# Make sure apt doesn't want to install from buster by default +cat >/etc/apt/apt.conf.d/80default <<EOF +APT::Default-Release "stretch"; +EOF + +# Disable the automatic installation of recommended packages +cat >/etc/apt/apt.conf.d/90recommends <<EOF +APT::Install-Recommends "0"; +EOF + +# Deprioritize buster, so it must be specified manually +cat >/etc/apt/preferences.d/90buster <<EOF +Package: * +Pin: release a=buster +Pin-Priority: 90 +EOF + +# Update for the above changes +apt-get update + +# Install python-scapy from buster, because stretch's version is broken +apt-get install -y -t buster python-scapy + +# Note what we have installed; we will go back to this +LANG=C dpkg --get-selections | sort >originally-installed + +# Install everything needed from stretch to build iptables +apt-get install -y \ + build-essential \ + autoconf \ + automake \ + bison \ + debhelper \ + devscripts \ + fakeroot \ + flex \ + libmnl-dev \ + libnetfilter-conntrack-dev \ + libnfnetlink-dev \ + libnftnl-dev \ + libtool + +# Install newer linux-libc headers (these are from 4.16) +apt-get install -y -t stretch-backports linux-libc-dev + +# We are done with apt; reclaim the disk space +apt-get clean + +# Construct the iptables source package to build +iptables=iptables-1.6.1 +mkdir -p /usr/src/$iptables + +cd /usr/src/$iptables +# Download a specific revision of iptables from AOSP +aosp_iptables=android-wear-p-preview-2 +wget -qO - \ + https://android.googlesource.com/platform/external/iptables/+archive/$aosp_iptables.tar.gz | \ + tar -zxf - +# Download a compatible 'debian' overlay from Debian salsa +# We don't want all of the sources, just the Debian modifications +debian_iptables=1.6.1-2_bpo9+1 +debian_iptables_dir=pkg-iptables-debian-$debian_iptables +wget -qO - \ + https://salsa.debian.org/pkg-netfilter-team/pkg-iptables/-/archive/debian/$debian_iptables/$debian_iptables_dir.tar.gz | \ + tar --strip-components 1 -zxf - \ + $debian_iptables_dir/debian +cd - + +cd /usr/src +# Generate a source package to leave in the filesystem. This is done for license +# compliance and build reproducibility. +tar --exclude=debian -cf - $iptables | \ + xz -9 >`echo $iptables | tr -s '-' '_'`.orig.tar.xz +cd - + +cd /usr/src/$iptables +# Build debian packages from the integrated iptables source +dpkg-buildpackage -F -us -uc +cd - + +# Record the list of packages we have installed now +LANG=C dpkg --get-selections | sort >installed + +# Compute the difference, and remove anything installed between the snapshots +dpkg -P `comm -3 originally-installed installed | sed -e 's,install,,' -e 's,\t,,' | xargs` + +cd /usr/src +# Find any packages generated, resolve to the debian package name, then +# exclude any compat, header or symbol packages +packages=`find -maxdepth 1 -name '*.deb' | colrm 1 2 | cut -d'_' -f1 | + grep -ve '-compat$\|-dbg$\|-dbgsym$\|-dev$' | xargs` +# Install the patched iptables packages, and 'hold' then so +# "apt-get dist-upgrade" doesn't replace them +dpkg -i ` +for package in $packages; do + echo ${package}_*.deb +done | xargs` +for package in $packages; do + echo "$package hold" | dpkg --set-selections +done +# Tidy up the mess we left behind, leaving just the source tarballs +rm -rf $iptables *.buildinfo *.changes *.deb *.dsc +cd - + +# Ensure a getty is spawned on ttyS0, if booting the image manually +ln -s /lib/systemd/system/serial-getty\@.service \ + /etc/systemd/system/getty.target.wants/serial-getty\@ttyS0.service + +# systemd needs some directories to be created +mkdir -p /var/lib/systemd/coredump /var/lib/systemd/rfkill + +# Finalize and tidy up the created image +chroot_cleanup diff --git a/net/test/rootfs/wheezy.list b/net/test/rootfs/wheezy.list new file mode 100644 index 0000000..44e3d85 --- /dev/null +++ b/net/test/rootfs/wheezy.list @@ -0,0 +1,33 @@ +adduser +apt +apt-utils +bash-completion +binutils +bsdmainutils +ca-certificates +file +gpgv +ifupdown +insserv +iptables +iputils-ping +less +libpopt0 +mime-support +netbase +netcat6 +netcat-traditional +net-tools +module-init-tools +openssl +procps +psmisc +python2.7 +python-scapy +strace +tcpdump +traceroute +udev +udhcpc +vim-tiny +wget diff --git a/net/test/rootfs/wheezy.sh b/net/test/rootfs/wheezy.sh new file mode 100755 index 0000000..81cfad7 --- /dev/null +++ b/net/test/rootfs/wheezy.sh @@ -0,0 +1,50 @@ +#!/bin/bash +# +# Copyright (C) 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# NOTE: It is highly recommended that you do not create new wheezy rootfs +# images. This script is here for forensic purposes only, to understand +# how the original rootfs was created. + +set -e + +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P) + +. $SCRIPT_DIR/common.sh + +chroot_sanity_check + +# Remove things pulled in by debootstrap that we do not need +dpkg -P \ + debconf-i18n \ + liblocale-gettext-perl \ + libtext-charwidth-perl \ + libtext-iconv-perl \ + libtext-wrapi18n-perl \ + python2.6 \ + python2.6-minimal \ + xz-utils + +# We are done with apt; reclaim the disk space +apt-get clean + +# Ensure a getty is spawned on ttyS0, if booting the image manually +# This also removes the vt gettys, as we may have no vt +sed -i '/tty[123456]/d' /etc/inittab +echo "s0:1235:respawn:/sbin/getty 115200 ttyS0 linux" >>/etc/inittab + +# Finalize and tidy up the created image +chroot_cleanup diff --git a/net/test/run_net_test.sh b/net/test/run_net_test.sh index 8c256b4..a81ad33 100755 --- a/net/test/run_net_test.sh +++ b/net/test/run_net_test.sh @@ -1,47 +1,72 @@ #!/bin/bash -# Kernel configuration options. +# Builds mysteriously fail if stdout is non-blocking. +fixup_ptys() { + python << 'EOF' +import fcntl, os, sys +fd = sys.stdout.fileno() +flags = fcntl.fcntl(fd, fcntl.F_GETFL) +flags &= ~(fcntl.FASYNC | os.O_NONBLOCK | os.O_APPEND) +fcntl.fcntl(fd, fcntl.F_SETFL, flags) +EOF +} + +# Common kernel options OPTIONS=" DEBUG_SPINLOCK DEBUG_ATOMIC_SLEEP DEBUG_MUTEXES DEBUG_RT_MUTEXES" +OPTIONS="$OPTIONS DEVTMPFS DEVTMPFS_MOUNT FHANDLE" OPTIONS="$OPTIONS IPV6 IPV6_ROUTER_PREF IPV6_MULTIPLE_TABLES IPV6_ROUTE_INFO" OPTIONS="$OPTIONS TUN SYN_COOKIES IP_ADVANCED_ROUTER IP_MULTIPLE_TABLES" OPTIONS="$OPTIONS NETFILTER NETFILTER_ADVANCED NETFILTER_XTABLES" OPTIONS="$OPTIONS NETFILTER_XT_MARK NETFILTER_XT_TARGET_MARK" OPTIONS="$OPTIONS IP_NF_IPTABLES IP_NF_MANGLE IP_NF_FILTER" OPTIONS="$OPTIONS IP6_NF_IPTABLES IP6_NF_MANGLE IP6_NF_FILTER INET6_IPCOMP" -OPTIONS="$OPTIONS IPV6_PRIVACY IPV6_OPTIMISTIC_DAD" -OPTIONS="$OPTIONS CONFIG_IPV6_ROUTE_INFO CONFIG_IPV6_ROUTER_PREF" -OPTIONS="$OPTIONS CONFIG_NETFILTER_XT_TARGET_NFLOG" -OPTIONS="$OPTIONS CONFIG_NETFILTER_XT_MATCH_QUOTA" -OPTIONS="$OPTIONS CONFIG_NETFILTER_XT_MATCH_QUOTA2" -OPTIONS="$OPTIONS CONFIG_NETFILTER_XT_MATCH_QUOTA2_LOG" -OPTIONS="$OPTIONS CONFIG_NETFILTER_TPROXY" -OPTIONS="$OPTIONS CONFIG_NETFILTER_XT_MATCH_SOCKET" -OPTIONS="$OPTIONS CONFIG_NETFILTER_XT_MATCH_QTAGUID" -# For 4.14 CONFIG_NETFILTER_XT_MATCH_SOCKET demands CONFIG_NF_SOCKET_IPV4/6 -OPTIONS="$OPTIONS CONFIG_NF_SOCKET_IPV4 CONFIG_NF_SOCKET_IPV6" -OPTIONS="$OPTIONS CONFIG_INET_UDP_DIAG CONFIG_INET_DIAG_DESTROY" -OPTIONS="$OPTIONS IP_SCTP INET_SCTP_DIAG" -OPTIONS="$OPTIONS CONFIG_IP_NF_TARGET_REJECT CONFIG_IP_NF_TARGET_REJECT_SKERR" -OPTIONS="$OPTIONS CONFIG_IP6_NF_TARGET_REJECT CONFIG_IP6_NF_TARGET_REJECT_SKERR" -OPTIONS="$OPTIONS BPF_SYSCALL NET_KEY XFRM_USER XFRM_STATISTICS CRYPTO_CBC" -OPTIONS="$OPTIONS CRYPTO_CTR CRYPTO_HMAC CRYPTO_AES CRYPTO_SHA1 CRYPTO_SHA256" -OPTIONS="$OPTIONS CRYPTO_SHA12 CRYPTO_USER INET_AH INET_ESP INET_XFRM_MODE" -OPTIONS="$OPTIONS TRANSPORT INET_XFRM_MODE_TUNNEL INET6_AH INET6_ESP" +OPTIONS="$OPTIONS IPV6_OPTIMISTIC_DAD" +OPTIONS="$OPTIONS IPV6_ROUTE_INFO IPV6_ROUTER_PREF" +OPTIONS="$OPTIONS NETFILTER_XT_TARGET_NFLOG" +OPTIONS="$OPTIONS NETFILTER_XT_MATCH_QUOTA" +OPTIONS="$OPTIONS NETFILTER_XT_MATCH_QUOTA2" +OPTIONS="$OPTIONS NETFILTER_XT_MATCH_QUOTA2_LOG" +OPTIONS="$OPTIONS NETFILTER_XT_MATCH_SOCKET" +OPTIONS="$OPTIONS NETFILTER_XT_MATCH_QTAGUID" +OPTIONS="$OPTIONS INET_UDP_DIAG INET_DIAG_DESTROY" +OPTIONS="$OPTIONS IP_SCTP" +OPTIONS="$OPTIONS IP_NF_TARGET_REJECT IP_NF_TARGET_REJECT_SKERR" +OPTIONS="$OPTIONS IP6_NF_TARGET_REJECT IP6_NF_TARGET_REJECT_SKERR" +OPTIONS="$OPTIONS NET_KEY XFRM_USER XFRM_STATISTICS CRYPTO_CBC" +OPTIONS="$OPTIONS CRYPTO_CTR CRYPTO_HMAC CRYPTO_AES CRYPTO_SHA1" +OPTIONS="$OPTIONS CRYPTO_USER INET_ESP INET_XFRM_MODE_TRANSPORT" +OPTIONS="$OPTIONS INET_XFRM_MODE_TUNNEL INET6_ESP" OPTIONS="$OPTIONS INET6_XFRM_MODE_TRANSPORT INET6_XFRM_MODE_TUNNEL" OPTIONS="$OPTIONS CRYPTO_SHA256 CRYPTO_SHA512 CRYPTO_AES_X86_64 CRYPTO_NULL" -OPTIONS="$OPTIONS CRYPTO_GCM CRYPTO_ECHAINIV NET_IPVTI IPV6_VTI" -OPTIONS="$OPTIONS SOCK_CGROUP_DATA CGROUP_BPF" +OPTIONS="$OPTIONS CRYPTO_GCM CRYPTO_ECHAINIV NET_IPVTI" -# For 4.14 kernels, where UBD and HOSTFS are not set -OPTIONS="$OPTIONS CONFIG_BLK_DEV_UBD CONFIG_HOSTFS" +# Kernel version specific options +OPTIONS="$OPTIONS XFRM_INTERFACE" # Various device kernels +OPTIONS="$OPTIONS CGROUP_BPF" # Added in android-4.9 +OPTIONS="$OPTIONS NF_SOCKET_IPV4 NF_SOCKET_IPV6" # Added in 4.9 +OPTIONS="$OPTIONS INET_SCTP_DIAG" # Added in 4.7 +OPTIONS="$OPTIONS SOCK_CGROUP_DATA" # Added in 4.5 +OPTIONS="$OPTIONS CRYPTO_ECHAINIV" # Added in 4.1 +OPTIONS="$OPTIONS BPF_SYSCALL" # Added in 3.18 +OPTIONS="$OPTIONS IPV6_VTI" # Added in 3.13 +OPTIONS="$OPTIONS IPV6_PRIVACY" # Removed in 3.12 +OPTIONS="$OPTIONS NETFILTER_TPROXY" # Removed in 3.11 -# For 3.1 kernels, where devtmpfs is not on by default. -OPTIONS="$OPTIONS DEVTMPFS DEVTMPFS_MOUNT" +# UML specific options +OPTIONS="$OPTIONS BLK_DEV_UBD HOSTFS" + +# QEMU specific options +OPTIONS="$OPTIONS VIRTIO VIRTIO_PCI VIRTIO_BLK NET_9P NET_9P_VIRTIO 9P_FS" +OPTIONS="$OPTIONS SERIAL_8250 SERIAL_8250_PCI" + +# Obsolete options present at some time in Android kernels +OPTIONS="$OPTIONS IP_NF_TARGET_REJECT_SKERR IP6_NF_TARGET_REJECT_SKERR" # These two break the flo kernel due to differences in -Werror on recent GCC. -DISABLE_OPTIONS=" CONFIG_REISERFS_FS CONFIG_ANDROID_PMEM" +DISABLE_OPTIONS=" REISERFS_FS ANDROID_PMEM" + # This one breaks the fugu kernel due to a nonexistent sem_wait_array. -DISABLE_OPTIONS="$DISABLE_OPTIONS CONFIG_SYSVIPC" +DISABLE_OPTIONS="$DISABLE_OPTIONS SYSVIPC" # How many TAP interfaces to create to provide the VM with real network access # via the host. This requires privileges (e.g., root access) on the host. @@ -56,11 +81,12 @@ DISABLE_OPTIONS="$DISABLE_OPTIONS CONFIG_SYSVIPC" NUMTAPINTERFACES=0 # The root filesystem disk image we'll use. -ROOTFS=net_test.rootfs.20150203 +ROOTFS=${ROOTFS:-net_test.rootfs.20150203} COMPRESSED_ROOTFS=$ROOTFS.xz URL=https://dl.google.com/dl/android/$COMPRESSED_ROOTFS # Parse arguments and figure out which test to run. +ARCH=${ARCH:-um} J=${J:-64} MAKE="make" OUT_DIR=$(readlink -f ${OUT_DIR:-.}) @@ -72,25 +98,42 @@ SCRIPT_DIR=$(dirname $(readlink -f $0)) CONFIG_SCRIPT=${KERNEL_DIR}/scripts/config CONFIG_FILE=${OUT_DIR}/.config consolemode= +netconfig= testmode= -blockdevice=ubda +cmdline= +nowrite=1 nobuild=0 norun=0 -while [ -n "$1" ]; do - if [ "$1" = "--builder" ]; then +if tty >/dev/null; then + verbose= +else + verbose=1 +fi + +while [[ -n "$1" ]]; do + if [[ "$1" == "--builder" ]]; then consolemode="con=null,fd:1" testmode=builder shift - elif [ "$1" == "--readonly" ]; then - blockdevice="${blockdevice}r" + elif [[ "$1" == "--readwrite" || "$1" == "--rw" ]]; then + nowrite=0 shift - elif [ "$1" == "--nobuild" ]; then + elif [[ "$1" == "--readonly" || "$1" == "--ro" ]]; then + nowrite=1 + shift + elif [[ "$1" == "--nobuild" ]]; then nobuild=1 shift - elif [ "$1" == "--norun" ]; then + elif [[ "$1" == "--norun" ]]; then norun=1 shift + elif [[ "$1" == "--verbose" ]]; then + verbose=1 + shift + elif [[ "$1" == "--noverbose" ]]; then + verbose= + shift else test=$1 break # Arguments after the test file are passed to the test itself. @@ -122,7 +165,7 @@ function isBuildOnly() { if ! isRunningTest && ! isBuildOnly; then echo "Usage:" >&2 - echo " $0 [--builder] [--readonly] [--nobuild] <test>" >&2 + echo " $0 [--builder] [--readonly|--ro|--readwrite|--rw] [--nobuild] [--verbose] <test>" >&2 echo " $0 --norun" >&2 exit 1 fi @@ -152,12 +195,16 @@ cd - if (( $NUMTAPINTERFACES > 0 )); then user=${USER:0:10} tapinterfaces= - netconfig= for id in $(seq 0 $(( NUMTAPINTERFACES - 1 )) ); do tap=${user}TAP$id tapinterfaces="$tapinterfaces $tap" mac=$(printf fe:fd:00:00:00:%02x $id) - netconfig="$netconfig eth$id=tuntap,$tap,$mac" + if [ "$ARCH" == "um" ]; then + netconfig="$netconfig eth$id=tuntap,$tap,$mac" + else + netconfig="$netconfig -netdev tap,id=hostnet$id,ifname=$tap,script=no,downscript=no" + netconfig="$netconfig -device virtio-net-pci,netdev=hostnet$id,id=net$id,mac=$mac" + fi done for tap in $tapinterfaces; do @@ -172,49 +219,184 @@ fi if [ -n "$KERNEL_BINARY" ]; then nobuild=1 else - KERNEL_BINARY=./linux + # Set default KERNEL_BINARY location if it was not provided. + if [ "$ARCH" == "um" ]; then + KERNEL_BINARY=./linux + elif [ "$ARCH" == "i386" -o "$ARCH" == "x86_64" -o "$ARCH" == "x86" ]; then + KERNEL_BINARY=./arch/x86/boot/bzImage + elif [ "$ARCH" == "arm64" ]; then + KERNEL_BINARY=./arch/arm64/boot/Image.gz + fi fi if ((nobuild == 0)); then - # Exporting ARCH=um SUBARCH=x86_64 doesn't seem to work, as it "sometimes" - # (?) results in a 32-bit kernel. + make_flags= + if [ "$ARCH" == "um" ]; then + # Exporting ARCH=um SUBARCH=x86_64 doesn't seem to work, as it + # "sometimes" (?) results in a 32-bit kernel. + make_flags="$make_flags ARCH=$ARCH SUBARCH=x86_64 CROSS_COMPILE= " + fi + if [ -n "$CC" ]; then + # The CC flag is *not* inherited from the environment, so it must be + # passed in on the command line. + make_flags="$make_flags CC=$CC" + fi # If there's no kernel config at all, create one or UML won't work. - [ -f $CONFIG_FILE ] || (cd $KERNEL_DIR && $MAKE defconfig ARCH=um SUBARCH=x86_64) + [ -n "$DEFCONFIG" ] || DEFCONFIG=defconfig + [ -f $CONFIG_FILE ] || (cd $KERNEL_DIR && $MAKE $make_flags $DEFCONFIG) # Enable the kernel config options listed in $OPTIONS. - cmdline=${OPTIONS// / -e } - $CONFIG_SCRIPT --file $CONFIG_FILE $cmdline + $CONFIG_SCRIPT --file $CONFIG_FILE ${OPTIONS// / -e } # Disable the kernel config options listed in $DISABLE_OPTIONS. - cmdline=${DISABLE_OPTIONS// / -d } - $CONFIG_SCRIPT --file $CONFIG_FILE $cmdline - - # olddefconfig doesn't work on old kernels. - if ! $MAKE olddefconfig ARCH=um SUBARCH=x86_64 CROSS_COMPILE= ; then - cat >&2 << EOF - -Warning: "make olddefconfig" failed. -Perhaps this kernel is too old to support it. -You may get asked lots of questions. -Keep enter pressed to accept the defaults. + $CONFIG_SCRIPT --file $CONFIG_FILE ${DISABLE_OPTIONS// / -d } -EOF - fi + $MAKE $make_flags olddefconfig # Compile the kernel. - $MAKE -j$J linux ARCH=um SUBARCH=x86_64 CROSS_COMPILE= + if [ "$ARCH" == "um" ]; then + $MAKE -j$J $make_flags linux + else + $MAKE -j$J $make_flags + fi fi if (( norun == 1 )); then exit 0 fi -# Get the absolute path to the test file that's being run. -dir=/host$SCRIPT_DIR +if (( nowrite == 1 )); then + cmdline="ro" +fi + +if (( verbose == 1 )); then + cmdline="$cmdline verbose=1" +fi + +cmdline="$cmdline init=/sbin/net_test.sh" +cmdline="$cmdline net_test_args=\"$test_args\" net_test_mode=$testmode" + +if [ "$ARCH" == "um" ]; then + # Get the absolute path to the test file that's being run. + cmdline="$cmdline net_test=/host$SCRIPT_DIR/$test" + + # Use UML's /proc/exitcode feature to communicate errors on test failure + cmdline="$cmdline net_test_exitcode=/proc/exitcode" + + # Experience shows that we need at least 128 bits of entropy for the + # kernel's crng init to complete (before it fully initializes stuff behaves + # *weirdly* and there's plenty of kernel warnings and some tests even fail), + # hence net_test.sh needs at least 32 hex chars (which is the amount of hex + # in a single random UUID) provided to it on the kernel cmdline. + # + # Just to be safe, we'll pass in 384 bits, and we'll do this as a random + # 64 character base64 seed (because this is shorter than base16). + # We do this by getting *three* random UUIDs and concatenating their hex + # digits into an *even* length hex encoded string, which we then convert + # into base64. + entropy="$(cat /proc/sys/kernel/random{/,/,/}uuid | tr -d '\n-')" + entropy="$(xxd -r -p <<< "${entropy}" | base64 -w 0)" + cmdline="${cmdline} entropy=${entropy}" + + # Map the --readonly flag to UML block device names + if ((nowrite == 0)); then + blockdevice=ubda + else + blockdevice=ubdar + fi + + exitcode=0 + $KERNEL_BINARY >&2 umid=net_test mem=512M \ + $blockdevice=$SCRIPT_DIR/$ROOTFS $netconfig $consolemode $cmdline \ + || exitcode=$? + + # UML is kind of crazy in how guest syscalls work. It requires host kernel + # to not be in vsyscall=none mode. + if [[ "${exitcode}" != '0' ]]; then + { + # Hopefully one of these exists + cat /proc/config || : + zcat /proc/config.gz || : + cat "/boot/config-$(uname -r)" || : + zcat "/boot/config-$(uname -r).gz" || : + } 2>/dev/null \ + | egrep -q '^CONFIG_LEGACY_VSYSCALL_NONE=y' \ + && ! egrep -q '(^| )vsyscall=(native|emulate)( |$)' /proc/cmdline \ + && { + echo '-----=====-----' + echo 'If above you saw a "net_test.sh[1]: segfault at ..." followed by' + echo '"Kernel panic - not syncing: Attempted to kill init!" then please' + echo 'set "vsyscall=emulate" on *host* kernel command line.' + echo '(for example via GRUB_CMDLINE_LINUX in /etc/default/grub)' + echo '-----=====-----' + } + fi +else + # We boot into the filesystem image directly in all cases + cmdline="$cmdline root=/dev/vda" + + # The path is stripped by the 9p export; we don't need SCRIPT_DIR + cmdline="$cmdline net_test=/host/$test" + + # Map the --readonly flag to a QEMU block device flag + if ((nowrite > 0)); then + blockdevice=",readonly" + else + blockdevice= + fi + blockdevice="-drive file=$SCRIPT_DIR/$ROOTFS,format=raw,if=none,id=drive-virtio-disk0$blockdevice" + blockdevice="$blockdevice -device virtio-blk-pci,drive=drive-virtio-disk0" + + # Pass through our current console/screen size to inner shell session + read rows cols < <(stty size 2>/dev/null) + [[ -z "${rows}" ]] || cmdline="${cmdline} console_rows=${rows}" + [[ -z "${cols}" ]] || cmdline="${cmdline} console_cols=${cols}" + unset rows cols + + # QEMU has no way to modify its exitcode; simulate it with a serial port. + # + # Choose to do it this way over writing a file to /host, because QEMU will + # initialize the 'exitcode' file for us, it avoids unnecessary writes to the + # host filesystem (which is normally not written to) and it allows us to + # communicate an exit code back in cases we do not have /host mounted. + # + if [ "$ARCH" == "i386" -o "$ARCH" == "x86_64" -o "$ARCH" == "x86" ]; then + # Assume we have hardware-accelerated virtualization support for amd64 + qemu="qemu-system-x86_64 -machine pc,accel=kvm -cpu host" + + # The assignment of 'ttyS1' here is magical -- we know 'ttyS0' will be our + # serial port from the hard-coded '-serial stdio' flag below, and so this + # second serial port will be 'ttyS1'. + cmdline="$cmdline net_test_exitcode=/dev/ttyS1" + elif [ "$ARCH" == "arm64" ]; then + # This uses a software model CPU, based on cortex-a57 + qemu="qemu-system-aarch64 -machine virt -cpu cortex-a57" + + # The kernel will print messages via a virtual ARM serial port (ttyAMA0), + # but for command line consistency with x86, we put the exitcode serial + # port on the PCI bus, and it will be the only one. + cmdline="$cmdline net_test_exitcode=/dev/ttyS0" + fi + + $qemu >&2 -name net_test -m 512 \ + -kernel $KERNEL_BINARY \ + -no-user-config -nodefaults -no-reboot \ + -display none -nographic -serial mon:stdio -parallel none \ + -smp 4,sockets=4,cores=1,threads=1 \ + -device virtio-rng-pci \ + -chardev file,id=exitcode,path=exitcode \ + -device pci-serial,chardev=exitcode \ + -fsdev local,security_model=mapped-xattr,id=fsdev0,fmode=0644,dmode=0755,path=$SCRIPT_DIR \ + -device virtio-9p-pci,id=fs0,fsdev=fsdev0,mount_tag=host \ + $blockdevice $netconfig -append "$cmdline" + [[ -s exitcode ]] && exitcode=`cat exitcode | tr -d '\r'` || exitcode=1 + rm -f exitcode +fi + +# UML reliably screws up the ptys, QEMU probably can as well... +fixup_ptys +stty sane || : -# Start the VM. -exec $KERNEL_BINARY umid=net_test $blockdevice=$SCRIPT_DIR/$ROOTFS \ - mem=512M init=/sbin/net_test.sh net_test=$dir/$test \ - net_test_args=\"$test_args\" \ - net_test_mode=$testmode $netconfig $consolemode >&2 +echo "Returning exit code ${exitcode}." 1>&2 +exit "${exitcode}" diff --git a/net/test/sock_diag_test.py b/net/test/sock_diag_test.py index e25035b..daa2fa4 100755 --- a/net/test/sock_diag_test.py +++ b/net/test/sock_diag_test.py @@ -25,24 +25,42 @@ import threading import time import unittest +import cstruct import multinetwork_base import net_test import packets import sock_diag import tcp_test +# Mostly empty structure definition containing only the fields we currently use. +TcpInfo = cstruct.Struct("TcpInfo", "64xI", "tcpi_rcv_ssthresh") NUM_SOCKETS = 30 NO_BYTECODE = "" -HAVE_SO_COOKIE_SUPPORT = net_test.LINUX_VERSION >= (4, 9, 0) +LINUX_4_9_OR_ABOVE = net_test.LINUX_VERSION >= (4, 9, 0) +LINUX_4_19_OR_ABOVE = net_test.LINUX_VERSION >= (4, 19, 0) IPPROTO_SCTP = 132 def HaveUdpDiag(): - # There is no way to tell whether a dump succeeded: if the appropriate handler - # wasn't found, __inet_diag_dump just returns an empty result instead of an - # error. So, just check to see if a UDP dump returns no sockets when we know - # it should return one. + """Checks if the current kernel has config CONFIG_INET_UDP_DIAG enabled. + + This config is required for device running 4.9 kernel that ship with P, In + this case always assume the config is there and use the tests to check if the + config is enabled as required. + + For all ther other kernel version, there is no way to tell whether a dump + succeeded: if the appropriate handler wasn't found, __inet_diag_dump just + returns an empty result instead of an error. So, just check to see if a UDP + dump returns no sockets when we know it should return one. If not, some tests + will be skipped. + + Returns: + True if the kernel is 4.9 or above, or the CONFIG_INET_UDP_DIAG is enabled. + False otherwise. + """ + if LINUX_4_9_OR_ABOVE: + return True; s = socket(AF_INET6, SOCK_DGRAM, 0) s.bind(("::", 0)) s.connect((s.getsockname())) @@ -192,10 +210,10 @@ class SockDiagTest(SockDiagBaseTest): self.sock_diag.GetSockInfo(diag_req) # No errors? Good. - def testFindsAllMySockets(self): + def CheckFindsAllMySockets(self, socktype, proto): """Tests that basic socket dumping works.""" - self.socketpairs = self._CreateLotsOfSockets(SOCK_STREAM) - sockets = self.sock_diag.DumpAllInetSockets(IPPROTO_TCP, NO_BYTECODE) + self.socketpairs = self._CreateLotsOfSockets(socktype) + sockets = self.sock_diag.DumpAllInetSockets(proto, NO_BYTECODE) self.assertGreaterEqual(len(sockets), NUM_SOCKETS) # Find the cookies for all of our sockets. @@ -225,9 +243,21 @@ class SockDiagTest(SockDiagBaseTest): # Check that we can find a diag_msg once we know the cookie. req = self.sock_diag.DiagReqFromSocket(sock) req.id.cookie = cookie + if proto == IPPROTO_UDP: + # Kernel bug: for UDP sockets, the order of arguments must be swapped. + # See testDemonstrateUdpGetSockIdBug. + req.id.sport, req.id.dport = req.id.dport, req.id.sport + req.id.src, req.id.dst = req.id.dst, req.id.src info = self.sock_diag.GetSockInfo(req) self.assertSockInfoMatchesSocket(sock, info) + def testFindsAllMySocketsTcp(self): + self.CheckFindsAllMySockets(SOCK_STREAM, IPPROTO_TCP) + + @unittest.skipUnless(HAVE_UDP_DIAG, "INET_UDP_DIAG not enabled") + def testFindsAllMySocketsUdp(self): + self.CheckFindsAllMySockets(SOCK_DGRAM, IPPROTO_UDP) + def testBytecodeCompilation(self): # pylint: disable=bad-whitespace instructions = [ @@ -360,11 +390,45 @@ class SockDiagTest(SockDiagBaseTest): cookie = sock.getsockopt(net_test.SOL_SOCKET, net_test.SO_COOKIE, 8) self.assertEqual(diag_msg.id.cookie, cookie) - @unittest.skipUnless(HAVE_SO_COOKIE_SUPPORT, "SO_COOKIE not supported") + @unittest.skipUnless(LINUX_4_9_OR_ABOVE, "SO_COOKIE not supported") def testGetsockoptcookie(self): self.CheckSocketCookie(AF_INET, "127.0.0.1") self.CheckSocketCookie(AF_INET6, "::1") + @unittest.skipUnless(HAVE_UDP_DIAG, "INET_UDP_DIAG not enabled") + def testDemonstrateUdpGetSockIdBug(self): + # TODO: this is because udp_dump_one mistakenly uses __udp[46]_lib_lookup + # by passing the source address as the source address argument. + # Unfortunately those functions are intended to match local sockets based + # on received packets, and the argument that ends up being compared with + # e.g., sk_daddr is actually saddr, not daddr. udp_diag_destroy does not + # have this bug. Upstream has confirmed that this will not be fixed: + # https://www.mail-archive.com/netdev@vger.kernel.org/msg248638.html + """Documents a bug: getting UDP sockets requires swapping src and dst.""" + for version in [4, 5, 6]: + family = net_test.GetAddressFamily(version) + s = socket(family, SOCK_DGRAM, 0) + self.SelectInterface(s, self.RandomNetid(), "mark") + s.connect((self.GetRemoteSocketAddress(version), 53)) + + # Create a fully-specified diag req from our socket, including cookie if + # we can get it. + req = self.sock_diag.DiagReqFromSocket(s) + if LINUX_4_9_OR_ABOVE: + req.id.cookie = s.getsockopt(net_test.SOL_SOCKET, net_test.SO_COOKIE, 8) + else: + req.id.cookie = "\xff" * 16 # INET_DIAG_NOCOOKIE[2] + + # As is, this request does not find anything. + with self.assertRaisesErrno(ENOENT): + self.sock_diag.GetSockInfo(req) + + # But if we swap src and dst, the kernel finds our socket. + req.id.sport, req.id.dport = req.id.dport, req.id.sport + req.id.src, req.id.dst = req.id.dst, req.id.src + + self.assertSockInfoMatchesSocket(s, self.sock_diag.GetSockInfo(req)) + class SockDestroyTest(SockDiagBaseTest): """Tests that SOCK_DESTROY works correctly. @@ -487,6 +551,50 @@ class SockDiagTcpTest(tcp_test.TcpBaseTest, SockDiagBaseTest): child.id.src) +class TcpRcvWindowTest(tcp_test.TcpBaseTest, SockDiagBaseTest): + + RWND_SIZE = 64000 if LINUX_4_19_OR_ABOVE else 42000 + TCP_DEFAULT_INIT_RWND = "/proc/sys/net/ipv4/tcp_default_init_rwnd" + + def setUp(self): + super(TcpRcvWindowTest, self).setUp() + if LINUX_4_19_OR_ABOVE: + self.assertRaisesErrno(ENOENT, open, self.TCP_DEFAULT_INIT_RWND, "w") + return + + f = open(self.TCP_DEFAULT_INIT_RWND, "w") + f.write("60") + + def checkInitRwndSize(self, version, netid): + self.IncomingConnection(version, tcp_test.TCP_ESTABLISHED, netid) + tcpInfo = TcpInfo(self.accepted.getsockopt(net_test.SOL_TCP, + net_test.TCP_INFO, len(TcpInfo))) + self.assertLess(self.RWND_SIZE, tcpInfo.tcpi_rcv_ssthresh, + "Tcp rwnd of netid=%d, version=%d is not enough. " + "Expect: %d, actual: %d" % (netid, version, self.RWND_SIZE, + tcpInfo.tcpi_rcv_ssthresh)) + + def checkSynPacketWindowSize(self, version, netid): + s = self.BuildSocket(version, net_test.TCPSocket, netid, "mark") + myaddr = self.MyAddress(version, netid) + dstaddr = self.GetRemoteAddress(version) + dstsockaddr = self.GetRemoteSocketAddress(version) + desc, expected = packets.SYN(53, version, myaddr, dstaddr, + sport=None, seq=None) + self.assertRaisesErrno(EINPROGRESS, s.connect, (dstsockaddr, 53)) + msg = "IPv%s TCP connect: expected %s on %s" % ( + version, desc, self.GetInterfaceName(netid)) + syn = self.ExpectPacketOn(netid, msg, expected) + self.assertLess(self.RWND_SIZE, syn.window) + s.close() + + def testTcpCwndSize(self): + for version in [4, 5, 6]: + for netid in self.NETIDS: + self.checkInitRwndSize(version, netid) + self.checkSynPacketWindowSize(version, netid) + + class SockDestroyTcpTest(tcp_test.TcpBaseTest, SockDiagBaseTest): def setUp(self): @@ -877,7 +985,7 @@ class SockDestroyUdpTest(SockDiagBaseTest): family = {4: AF_INET, 5: AF_INET6, 6: AF_INET6}[version] s = net_test.UDPSocket(family) self.SelectInterface(s, random.choice(self.NETIDS), "mark") - addr = self.GetRemoteAddress(version) + addr = self.GetRemoteSocketAddress(version) # Check that reads on connected sockets are interrupted. s.connect((addr, 53)) diff --git a/net/test/util.py b/net/test/util.py index bed3e1d..cbcd2d0 100644 --- a/net/test/util.py +++ b/net/test/util.py @@ -13,4 +13,59 @@ # limitations under the License. def GetPadLength(block_size, length): - return (block_size - (length % block_size)) % block_size
\ No newline at end of file + return (block_size - (length % block_size)) % block_size + + +def InjectParameterizedTest(cls, param_list, name_generator): + """Injects parameterized tests into the provided class + + This method searches for all tests that start with the name "ParamTest", + and injects a test method for each set of parameters in param_list. Names + are generated via the use of the name_generator. + + Args: + cls: the class for which to inject all parameterized tests + param_list: a list of tuples, where each tuple is a combination of + of parameters to test (i.e. representing a single test case) + name_generator: A function that takes a combination of parameters and + returns a string that identifies the test case. + """ + param_test_names = [name for name in dir(cls) if name.startswith("ParamTest")] + + # Force param_list to an actual list; otherwise itertools.Product will hit + # the end, resulting in only the first ParamTest* method actually being + # parameterized + param_list = list(param_list) + + # Parameterize each test method starting with "ParamTest" + for test_name in param_test_names: + func = getattr(cls, test_name) + + for params in param_list: + # Give the test method a readable, debuggable name. + param_string = name_generator(*params) + new_name = "%s_%s" % (func.__name__.replace("ParamTest", "test"), + param_string) + new_name = new_name.replace("(", "-").replace(")", "") # remove parens + + # Inject the test method + setattr(cls, new_name, _GetTestClosure(func, params)) + + +def _GetTestClosure(func, params): + """ Creates a no-argument test method for the given function and parameters. + + This is required to be separate from the InjectParameterizedTest method, due + to some interesting scoping issues with internal function declarations. If + left in InjectParameterizedTest, all the tests end up using the same + instance of TestClosure + + Args: + func: the function for which this test closure should run + params: the parameters for the run of this test function + """ + + def TestClosure(self): + func(self, *params) + + return TestClosure diff --git a/net/test/xfrm.py b/net/test/xfrm.py index 1bd10da..acdfd4f 100755 --- a/net/test/xfrm.py +++ b/net/test/xfrm.py @@ -85,6 +85,8 @@ XFRMA_ADDRESS_FILTER = 26 XFRMA_PAD = 27 XFRMA_OFFLOAD_DEV = 28 XFRMA_OUTPUT_MARK = 29 +XFRMA_INPUT_MARK = 30 +XFRMA_IF_ID = 31 # Other netlink constants. See include/uapi/linux/xfrm.h. @@ -206,7 +208,7 @@ NO_LIFETIME_CFG = XfrmLifetimeCfg((_INF, _INF, _INF, _INF, 0, 0, 0, 0)) NO_LIFETIME_CUR = "\x00" * len(XfrmLifetimeCur) # IPsec constants. -IPSEC_PROTO_ANY = 255 +IPSEC_PROTO_ANY = 255 # ESP header, not technically XFRM but we need a place for a protocol # header and this is the only one we have. @@ -217,6 +219,11 @@ EspHdr = cstruct.Struct("EspHdr", "!II", "spi seqnum") _DEFAULT_REPLAY_WINDOW = 4 ALL_ALGORITHMS = 0xffffffff +# Policy-SA match method (for VTI/XFRM-I). +MATCH_METHOD_ALL = "all" +MATCH_METHOD_MARK = "mark" +MATCH_METHOD_IFID = "ifid" + def RawAddress(addr): """Converts an IP address string to binary format.""" @@ -369,21 +376,25 @@ class Xfrm(netlink.NetlinkSocket): data = struct.unpack("=I", nla_data)[0] elif name == "XFRMA_TMPL": data = cstruct.Read(nla_data, XfrmUserTmpl)[0] + elif name == "XFRMA_IF_ID": + data = struct.unpack("=I", nla_data)[0] else: data = nla_data return name, data - def _UpdatePolicyInfo(self, msg, policy, tmpl, mark): + def _UpdatePolicyInfo(self, msg, policy, tmpl, mark, xfrm_if_id): """Send a policy to the Security Policy Database""" nlattrs = [] if tmpl is not None: nlattrs.append((XFRMA_TMPL, tmpl)) if mark is not None: nlattrs.append((XFRMA_MARK, mark)) + if xfrm_if_id is not None: + nlattrs.append((XFRMA_IF_ID, struct.pack("=I", xfrm_if_id))) self.SendXfrmNlRequest(msg, policy, nlattrs) - def AddPolicyInfo(self, policy, tmpl, mark): + def AddPolicyInfo(self, policy, tmpl, mark, xfrm_if_id=None): """Add a new policy to the Security Policy Database If the policy exists, then return an error (EEXIST). @@ -392,10 +403,11 @@ class Xfrm(netlink.NetlinkSocket): policy: an unpacked XfrmUserpolicyInfo tmpl: an unpacked XfrmUserTmpl mark: an unpacked XfrmMark + xfrm_if_id: the XFRM interface ID as an integer, or None """ - self._UpdatePolicyInfo(XFRM_MSG_NEWPOLICY, policy, tmpl, mark) + self._UpdatePolicyInfo(XFRM_MSG_NEWPOLICY, policy, tmpl, mark, xfrm_if_id) - def UpdatePolicyInfo(self, policy, tmpl, mark): + def UpdatePolicyInfo(self, policy, tmpl, mark, xfrm_if_id): """Update an existing policy in the Security Policy Database If the policy does not exist, then create it; otherwise, update the @@ -405,10 +417,11 @@ class Xfrm(netlink.NetlinkSocket): policy: an unpacked XfrmUserpolicyInfo tmpl: an unpacked XfrmUserTmpl to update mark: an unpacked XfrmMark to match the existing policy or None + xfrm_if_id: an XFRM interface ID or None """ - self._UpdatePolicyInfo(XFRM_MSG_UPDPOLICY, policy, tmpl, mark) + self._UpdatePolicyInfo(XFRM_MSG_UPDPOLICY, policy, tmpl, mark, xfrm_if_id) - def DeletePolicyInfo(self, selector, direction, mark): + def DeletePolicyInfo(self, selector, direction, mark, xfrm_if_id=None): """Delete a policy from the Security Policy Database Args: @@ -419,6 +432,8 @@ class Xfrm(netlink.NetlinkSocket): nlattrs = [] if mark is not None: nlattrs.append((XFRMA_MARK, mark)) + if xfrm_if_id is not None: + nlattrs.append((XFRMA_IF_ID, struct.pack("=I", xfrm_if_id))) self.SendXfrmNlRequest(XFRM_MSG_DELPOLICY, XfrmUserpolicyId(sel=selector, dir=direction), nlattrs) @@ -440,11 +455,35 @@ class Xfrm(netlink.NetlinkSocket): if nlattrs is None: nlattrs = [] for attr_type, attr_msg in nlattrs: - msg += self._NlAttr(attr_type, attr_msg.Pack()) + # TODO: find a better way to deal with the fact that many XFRM messages + # use nlattrs that aren't cstructs. + # + # This code allows callers to pass in either something that has a Pack() + # method or a packed netlink attr, but not other types of attributes. + # Alternatives include: + # + # 1. Require callers to marshal netlink attributes themselves and call + # _SendNlRequest directly. Delete this method. + # 2. Rename this function to _SendXfrmNlRequestCstructOnly (or other name + # that makes it clear that this only takes cstructs). Switch callers + # that need non-cstruct elements to calling _SendNlRequest directly. + # 3. Make this function somehow automatically detect what to do for + # all types of XFRM attributes today and in the future. This may be + # feasible because all XFRM attributes today occupy the same number + # space, but what about nested attributes? It is unlikley feasible via + # things like "if isinstance(attr_msg, str): ...", because that would + # not be able to determine the right size or byte order for non-struct + # types such as int. + # 4. Define fictitious cstructs which have no correspondence to actual + # kernel structs such as the following to represent a raw integer. + # XfrmAttrOutputMark = cstruct.Struct("=I", mark) + if hasattr(attr_msg, "Pack"): + attr_msg = attr_msg.Pack() + msg += self._NlAttr(attr_type, attr_msg) return self._SendNlRequest(msg_type, msg, flags) def AddSaInfo(self, src, dst, spi, mode, reqid, encryption, auth_trunc, aead, - encap, mark, output_mark, is_update=False): + encap, mark, output_mark, is_update=False, xfrm_if_id=None): """Adds an IPsec security association. Args: @@ -463,6 +502,7 @@ class Xfrm(netlink.NetlinkSocket): output_mark: An integer, the output mark. 0 means unset. is_update: If true, update an existing SA otherwise create a new SA. For compatibility reasons, this value defaults to False. + xfrm_if_id: The XFRM interface ID, or None. """ proto = IPPROTO_ESP xfrm_id = XfrmId((PaddedAddress(dst), spi, proto)) @@ -488,6 +528,8 @@ class Xfrm(netlink.NetlinkSocket): nlattrs += self._NlAttr(XFRMA_ENCAP, encap.Pack()) if output_mark is not None: nlattrs += self._NlAttrU32(XFRMA_OUTPUT_MARK, output_mark) + if xfrm_if_id is not None: + nlattrs += self._NlAttrU32(XFRMA_IF_ID, xfrm_if_id) # The kernel ignores these on input, so make them empty. cur = XfrmLifetimeCur() @@ -519,7 +561,7 @@ class Xfrm(netlink.NetlinkSocket): nl_msg_type = XFRM_MSG_UPDSA if is_update else XFRM_MSG_NEWSA self._SendNlRequest(nl_msg_type, msg, flags) - def DeleteSaInfo(self, dst, spi, proto, mark=None): + def DeleteSaInfo(self, dst, spi, proto, mark=None, xfrm_if_id=None): """Delete an SA from the SAD Args: @@ -530,12 +572,13 @@ class Xfrm(netlink.NetlinkSocket): mark: A mark match specifier, such as returned by ExactMatchMark(), or None for an SA without a Mark attribute. """ - # TODO: deletes take a mark as well. family = AF_INET6 if ":" in dst else AF_INET usersa_id = XfrmUsersaId((PaddedAddress(dst), spi, family, proto)) nlattrs = [] if mark is not None: nlattrs.append((XFRMA_MARK, mark)) + if xfrm_if_id is not None: + nlattrs.append((XFRMA_IF_ID, struct.pack("=I", xfrm_if_id))) self.SendXfrmNlRequest(XFRM_MSG_DELSA, usersa_id, nlattrs) def AllocSpi(self, dst, proto, min_spi, max_spi): @@ -592,7 +635,7 @@ class Xfrm(netlink.NetlinkSocket): self._SendNlRequest(XFRM_MSG_FLUSHSA, usersa_flush.Pack(), flags) def CreateTunnel(self, direction, selector, src, dst, spi, encryption, - auth_trunc, mark, output_mark): + auth_trunc, mark, output_mark, xfrm_if_id, match_method): """Create an XFRM Tunnel Consisting of a Policy and an SA. Create a unidirectional XFRM tunnel, which entails one Policy and one @@ -610,15 +653,37 @@ class Xfrm(netlink.NetlinkSocket): encryption: A tuple (XfrmAlgo, key), the encryption parameters. auth_trunc: A tuple (XfrmAlgoAuth, key), the authentication parameters. mark: An XfrmMark, the mark used for selecting packets to be tunneled, and - for matching the security policy and security association. None means - unspecified. + for matching the security policy. None means unspecified. output_mark: The mark used to select the underlying network for packets outbound from xfrm. None means unspecified. + xfrm_if_id: The ID of the XFRM interface to use or None. + match_method: One of MATCH_METHOD_[MARK | ALL | IFID]. This determines how + SAs and policies are matched. """ outer_family = net_test.GetAddressFamily(net_test.GetAddressVersion(dst)) + # SA mark is currently unused due to UPDSA not updating marks. + # Kept as documentation of ideal/desired behavior. + if match_method == MATCH_METHOD_MARK: + # sa_mark = mark + tmpl_spi = 0 + if_id = None + elif match_method == MATCH_METHOD_ALL: + # sa_mark = mark + tmpl_spi = spi + if_id = xfrm_if_id + elif match_method == MATCH_METHOD_IFID: + # sa_mark = None + tmpl_spi = 0 + if_id = xfrm_if_id + else: + raise ValueError("Unknown match_method supplied: %s" % match_method) + + # Device code does not use mark; during AllocSpi, the mark is unset, and + # UPDSA does not update marks at this time. Actual use case will have no + # mark set. Test this use case. self.AddSaInfo(src, dst, spi, XFRM_MODE_TUNNEL, 0, encryption, auth_trunc, - None, None, mark, output_mark) + None, None, None, output_mark, xfrm_if_id=xfrm_if_id) if selector is None: selectors = [EmptySelector(AF_INET), EmptySelector(AF_INET6)] @@ -627,17 +692,20 @@ class Xfrm(netlink.NetlinkSocket): for selector in selectors: policy = UserPolicy(direction, selector) - tmpl = UserTemplate(outer_family, spi, 0, (src, dst)) - self.AddPolicyInfo(policy, tmpl, mark) + tmpl = UserTemplate(outer_family, tmpl_spi, 0, (src, dst)) + self.AddPolicyInfo(policy, tmpl, mark, xfrm_if_id=xfrm_if_id) + + def DeleteTunnel(self, direction, selector, dst, spi, mark, xfrm_if_id): + if mark is not None: + mark = ExactMatchMark(mark) - def DeleteTunnel(self, direction, selector, dst, spi, mark): - self.DeleteSaInfo(dst, spi, IPPROTO_ESP, ExactMatchMark(mark)) + self.DeleteSaInfo(dst, spi, IPPROTO_ESP, mark, xfrm_if_id) if selector is None: selectors = [EmptySelector(AF_INET), EmptySelector(AF_INET6)] else: selectors = [selector] for selector in selectors: - self.DeletePolicyInfo(selector, direction, ExactMatchMark(mark)) + self.DeletePolicyInfo(selector, direction, mark, xfrm_if_id) if __name__ == "__main__": diff --git a/net/test/xfrm_algorithm_test.py b/net/test/xfrm_algorithm_test.py index 6adc461..0176265 100755 --- a/net/test/xfrm_algorithm_test.py +++ b/net/test/xfrm_algorithm_test.py @@ -27,6 +27,7 @@ import unittest import multinetwork_base import net_test from tun_twister import TapTwister +import util import xfrm import xfrm_base @@ -72,49 +73,26 @@ AEAD_ALGOS = [ ] def InjectTests(): - XfrmAlgorithmTest.InjectTests() + XfrmAlgorithmTest.InjectTests() + class XfrmAlgorithmTest(xfrm_base.XfrmLazyTest): @classmethod def InjectTests(cls): - """Inject parameterized test cases into this class. - - Because a library for parameterized testing is not availble in - net_test.rootfs.20150203, this does a minimal parameterization. - - This finds methods named like "ParamTestFoo" and replaces them with several - "testFoo(*)" methods taking different parameter dicts. A set of test - parameters is generated from every combination of encryption, - authentication, IP version, and TCP/UDP. - - The benefit of this approach is that an individually failing tests have a - clearly separated stack trace, and one failed test doesn't prevent the rest - from running. - """ - param_test_names = [ - name for name in dir(cls) if name.startswith("ParamTest") - ] VERSIONS = (4, 6) TYPES = (SOCK_DGRAM, SOCK_STREAM) # Tests all combinations of auth & crypt. Mutually exclusive with aead. - for crypt, auth, version, proto, name in itertools.product( - CRYPT_ALGOS, AUTH_ALGOS, VERSIONS, TYPES, param_test_names): - XfrmAlgorithmTest.InjectSingleTest(name, version, proto, crypt=crypt, auth=auth) + param_list = itertools.product(VERSIONS, TYPES, AUTH_ALGOS, CRYPT_ALGOS, + [None]) + util.InjectParameterizedTest(cls, param_list, cls.TestNameGenerator) # Tests all combinations of aead. Mutually exclusive with auth/crypt. - for aead, version, proto, name in itertools.product( - AEAD_ALGOS, VERSIONS, TYPES, param_test_names): - XfrmAlgorithmTest.InjectSingleTest(name, version, proto, aead=aead) - - @classmethod - def InjectSingleTest(cls, name, version, proto, crypt=None, auth=None, aead=None): - func = getattr(cls, name) - - def TestClosure(self): - func(self, {"crypt": crypt, "auth": auth, "aead": aead, - "version": version, "proto": proto}) + param_list = itertools.product(VERSIONS, TYPES, [None], [None], AEAD_ALGOS) + util.InjectParameterizedTest(cls, param_list, cls.TestNameGenerator) + @staticmethod + def TestNameGenerator(version, proto, auth, crypt, aead): # Produce a unique and readable name for each test. e.g. # testSocketPolicySimple_cbc-aes_256_hmac-sha512_512_256_IPv6_UDP param_string = "" @@ -131,12 +109,9 @@ class XfrmAlgorithmTest(xfrm_base.XfrmLazyTest): param_string += "%s_%s" % ("IPv4" if version == 4 else "IPv6", "UDP" if proto == SOCK_DGRAM else "TCP") - new_name = "%s_%s" % (func.__name__.replace("ParamTest", "test"), - param_string) - new_name = new_name.replace("(", "-").replace(")", "") # remove parens - setattr(cls, new_name, TestClosure) + return param_string - def ParamTestSocketPolicySimple(self, params): + def ParamTestSocketPolicySimple(self, version, proto, auth, crypt, aead): """Test two-way traffic using transport mode and socket policies.""" def AssertEncrypted(packet): @@ -153,37 +128,21 @@ class XfrmAlgorithmTest(xfrm_base.XfrmLazyTest): # other using transport mode ESP. Because of TapTwister, both sockets # perceive each other as owning "remote_addr". netid = self.RandomNetid() - family = net_test.GetAddressFamily(params["version"]) - local_addr = self.MyAddress(params["version"], netid) - remote_addr = self.GetRemoteSocketAddress(params["version"]) - crypt_left = (xfrm.XfrmAlgo(( - params["crypt"].name, - params["crypt"].key_len)), - os.urandom(params["crypt"].key_len / 8)) if params["crypt"] else None - crypt_right = (xfrm.XfrmAlgo(( - params["crypt"].name, - params["crypt"].key_len)), - os.urandom(params["crypt"].key_len / 8)) if params["crypt"] else None - auth_left = (xfrm.XfrmAlgoAuth(( - params["auth"].name, - params["auth"].key_len, - params["auth"].trunc_len)), - os.urandom(params["auth"].key_len / 8)) if params["auth"] else None - auth_right = (xfrm.XfrmAlgoAuth(( - params["auth"].name, - params["auth"].key_len, - params["auth"].trunc_len)), - os.urandom(params["auth"].key_len / 8)) if params["auth"] else None - aead_left = (xfrm.XfrmAlgoAead(( - params["aead"].name, - params["aead"].key_len, - params["aead"].icv_len)), - os.urandom(params["aead"].key_len / 8)) if params["aead"] else None - aead_right = (xfrm.XfrmAlgoAead(( - params["aead"].name, - params["aead"].key_len, - params["aead"].icv_len)), - os.urandom(params["aead"].key_len / 8)) if params["aead"] else None + family = net_test.GetAddressFamily(version) + local_addr = self.MyAddress(version, netid) + remote_addr = self.GetRemoteSocketAddress(version) + auth_left = (xfrm.XfrmAlgoAuth((auth.name, auth.key_len, auth.trunc_len)), + os.urandom(auth.key_len / 8)) if auth else None + auth_right = (xfrm.XfrmAlgoAuth((auth.name, auth.key_len, auth.trunc_len)), + os.urandom(auth.key_len / 8)) if auth else None + crypt_left = (xfrm.XfrmAlgo((crypt.name, crypt.key_len)), + os.urandom(crypt.key_len / 8)) if crypt else None + crypt_right = (xfrm.XfrmAlgo((crypt.name, crypt.key_len)), + os.urandom(crypt.key_len / 8)) if crypt else None + aead_left = (xfrm.XfrmAlgoAead((aead.name, aead.key_len, aead.icv_len)), + os.urandom(aead.key_len / 8)) if aead else None + aead_right = (xfrm.XfrmAlgoAead((aead.name, aead.key_len, aead.icv_len)), + os.urandom(aead.key_len / 8)) if aead else None spi_left = 0xbeefface spi_right = 0xcafed00d req_ids = [100, 200, 300, 400] # Used to match templates and SAs. @@ -242,20 +201,20 @@ class XfrmAlgorithmTest(xfrm_base.XfrmLazyTest): output_mark=None) # Make two sockets. - sock_left = socket(family, params["proto"], 0) + sock_left = socket(family, proto, 0) sock_left.settimeout(2.0) sock_left.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) self.SelectInterface(sock_left, netid, "mark") - sock_right = socket(family, params["proto"], 0) + sock_right = socket(family, proto, 0) sock_right.settimeout(2.0) sock_right.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) self.SelectInterface(sock_right, netid, "mark") # For UDP, set SO_LINGER to 0, to prevent TCP sockets from hanging around # in a TIME_WAIT state. - if params["proto"] == SOCK_STREAM: - net_test.DisableFinWait(sock_left) - net_test.DisableFinWait(sock_right) + if proto == SOCK_STREAM: + net_test.DisableFinWait(sock_left) + net_test.DisableFinWait(sock_right) # Apply the left outbound socket policy. xfrm_base.ApplySocketPolicy(sock_left, family, xfrm.XFRM_POLICY_OUT, @@ -302,14 +261,14 @@ class XfrmAlgorithmTest(xfrm_base.XfrmLazyTest): sock.close() # Server and client need to know each other's port numbers in advance. - wildcard_addr = net_test.GetWildcardAddress(params["version"]) + wildcard_addr = net_test.GetWildcardAddress(version) sock_left.bind((wildcard_addr, 0)) sock_right.bind((wildcard_addr, 0)) left_port = sock_left.getsockname()[1] right_port = sock_right.getsockname()[1] # Start the appropriate server type on sock_right. - target = TcpServer if params["proto"] == SOCK_STREAM else UdpServer + target = TcpServer if proto == SOCK_STREAM else UdpServer server = threading.Thread( target=target, args=(sock_right, left_port), diff --git a/net/test/xfrm_test.py b/net/test/xfrm_test.py index 93c66f4..3a3d9b0 100755 --- a/net/test/xfrm_test.py +++ b/net/test/xfrm_test.py @@ -132,6 +132,14 @@ class XfrmFunctionalTest(xfrm_base.XfrmLazyTest): EAGAIN, s.sendto, net_test.UDP_PAYLOAD, (remotesockaddr, 53)) + # If there is a user space key manager, calling sendto() after applying the socket policy + # creates an SA whose state is XFRM_STATE_ACQ. So this just deletes it. + # If there is no user space key manager, deleting SA returns ESRCH as the error code. + try: + self.xfrm.DeleteSaInfo(self.GetRemoteAddress(xfrm_version), TEST_SPI, IPPROTO_ESP) + except IOError as e: + self.assertEquals(ESRCH, e.errno, "Unexpected error when deleting ACQ SA") + # Adding a matching SA causes the packet to go out encrypted. The SA's # SPI must match the one in our template, and the destination address must # match the packet's destination address (in tunnel mode, it has to match @@ -139,6 +147,7 @@ class XfrmFunctionalTest(xfrm_base.XfrmLazyTest): self.CreateNewSa( net_test.GetWildcardAddress(xfrm_version), self.GetRemoteAddress(xfrm_version), TEST_SPI, reqid, None) + s.sendto(net_test.UDP_PAYLOAD, (remotesockaddr, 53)) expected_length = xfrm_base.GetEspPacketLength(xfrm.XFRM_MODE_TRANSPORT, version, False, @@ -631,14 +640,14 @@ class XfrmFunctionalTest(xfrm_base.XfrmLazyTest): self.assertEquals(attributes['XFRMA_TMPL'], tmpl) # Create a new policy using update. - self.xfrm.UpdatePolicyInfo(policy, tmpl1, mark) + self.xfrm.UpdatePolicyInfo(policy, tmpl1, mark, None) # NEWPOLICY will not update the existing policy. This checks both that # UPDPOLICY created a policy and that NEWPOLICY will not perform updates. _CheckTemplateMatch(tmpl1) with self.assertRaisesErrno(EEXIST): - self.xfrm.AddPolicyInfo(policy, tmpl2, mark) + self.xfrm.AddPolicyInfo(policy, tmpl2, mark, None) # Update the policy using UPDPOLICY. - self.xfrm.UpdatePolicyInfo(policy, tmpl2, mark) + self.xfrm.UpdatePolicyInfo(policy, tmpl2, mark, None) # There should only be one policy after update, and it should have the # updated template. _CheckTemplateMatch(tmpl2) diff --git a/net/test/xfrm_tunnel_test.py b/net/test/xfrm_tunnel_test.py index ac340d9..eb1a46e 100755 --- a/net/test/xfrm_tunnel_test.py +++ b/net/test/xfrm_tunnel_test.py @@ -19,32 +19,59 @@ from errno import * # pylint: disable=wildcard-import from socket import * # pylint: disable=wildcard-import import random +import itertools import struct import unittest +from scapy import all as scapy from tun_twister import TunTwister import csocket import iproute import multinetwork_base import net_test import packets +import util import xfrm import xfrm_base -# Parameters to Set up VTI as a special network -_BASE_VTI_NETID = {4: 40, 6: 60} +_LOOPBACK_IFINDEX = 1 +_TEST_XFRM_IFNAME = "ipsec42" +_TEST_XFRM_IF_ID = 42 + +# Does the kernel support xfrmi interfaces? +def HaveXfrmInterfaces(): + try: + i = iproute.IPRoute() + i.CreateXfrmInterface(_TEST_XFRM_IFNAME, _TEST_XFRM_IF_ID, + _LOOPBACK_IFINDEX) + i.DeleteLink(_TEST_XFRM_IFNAME) + try: + i.GetIfIndex(_TEST_XFRM_IFNAME) + assert "Deleted interface %s still exists!" % _TEST_XFRM_IFNAME + except IOError: + pass + return True + except IOError: + return False + +HAVE_XFRM_INTERFACES = HaveXfrmInterfaces() + +# Parameters to setup tunnels as special networks +_TUNNEL_NETID_OFFSET = 0xFC00 # Matches reserved netid range for IpSecService +_BASE_TUNNEL_NETID = {4: 40, 6: 60} _BASE_VTI_OKEY = 2000000100 _BASE_VTI_IKEY = 2000000200 -_VTI_NETID = 50 -_VTI_IFNAME = "test_vti" - _TEST_OUT_SPI = 0x1234 _TEST_IN_SPI = _TEST_OUT_SPI _TEST_OKEY = 2000000100 _TEST_IKEY = 2000000200 +_TEST_REMOTE_PORT = 1234 + +_SCAPY_IP_TYPE = {4: scapy.IP, 6: scapy.IPv6} + def _GetLocalInnerAddress(version): return {4: "10.16.5.15", 6: "2001:db8:1::1"}[version] @@ -58,61 +85,162 @@ def _GetRemoteOuterAddress(version): return {4: net_test.IPV4_ADDR, 6: net_test.IPV6_ADDR}[version] +def _GetNullAuthCryptTunnelModePkt(inner_version, src_inner, src_outer, + src_port, dst_inner, dst_outer, + dst_port, spi, seq_num, ip_hdr_options=None): + if ip_hdr_options is None: + ip_hdr_options = {} + + ip_hdr_options.update({'src': src_inner, 'dst': dst_inner}) + + # Build and receive an ESP packet destined for the inner socket + IpType = {4: scapy.IP, 6: scapy.IPv6}[inner_version] + input_pkt = ( + IpType(**ip_hdr_options) / scapy.UDP(sport=src_port, dport=dst_port) / + net_test.UDP_PAYLOAD) + input_pkt = IpType(str(input_pkt)) # Compute length, checksum. + input_pkt = xfrm_base.EncryptPacketWithNull(input_pkt, spi, seq_num, + (src_outer, dst_outer)) + + return input_pkt + + +def _CreateReceiveSock(version, port=0): + # Create a socket to receive packets. + read_sock = socket(net_test.GetAddressFamily(version), SOCK_DGRAM, 0) + read_sock.bind((net_test.GetWildcardAddress(version), port)) + # The second parameter of the tuple is the port number regardless of AF. + local_port = read_sock.getsockname()[1] + # Guard against the eventuality of the receive failing. + csocket.SetSocketTimeout(read_sock, 500) + + return read_sock, local_port + + +def _SendPacket(testInstance, netid, version, remote, remote_port): + # Send a packet out via the tunnel-backed network, bound for the port number + # of the input socket. + write_sock = socket(net_test.GetAddressFamily(version), SOCK_DGRAM, 0) + testInstance.SelectInterface(write_sock, netid, "mark") + write_sock.sendto(net_test.UDP_PAYLOAD, (remote, remote_port)) + local_port = write_sock.getsockname()[1] + + return local_port + + +def InjectTests(): + InjectParameterizedTests(XfrmTunnelTest) + InjectParameterizedTests(XfrmInterfaceTest) + InjectParameterizedTests(XfrmVtiTest) + + +def InjectParameterizedTests(cls): + VERSIONS = (4, 6) + param_list = itertools.product(VERSIONS, VERSIONS) + + def NameGenerator(*args): + return "IPv%d_in_IPv%d" % tuple(args) + + util.InjectParameterizedTest(cls, param_list, NameGenerator) + + class XfrmTunnelTest(xfrm_base.XfrmLazyTest): - def _CheckTunnelOutput(self, inner_version, outer_version): - """Test a bi-directional XFRM Tunnel with explicit selectors""" + def _CheckTunnelOutput(self, inner_version, outer_version, underlying_netid, + netid, local_inner, remote_inner, local_outer, + remote_outer, write_sock): + + write_sock.sendto(net_test.UDP_PAYLOAD, (remote_inner, 53)) + self._ExpectEspPacketOn(underlying_netid, _TEST_OUT_SPI, 1, None, + local_outer, remote_outer) + + def _CheckTunnelInput(self, inner_version, outer_version, underlying_netid, + netid, local_inner, remote_inner, local_outer, + remote_outer, read_sock): + + # The second parameter of the tuple is the port number regardless of AF. + local_port = read_sock.getsockname()[1] + + # Build and receive an ESP packet destined for the inner socket + input_pkt = _GetNullAuthCryptTunnelModePkt( + inner_version, remote_inner, remote_outer, _TEST_REMOTE_PORT, + local_inner, local_outer, local_port, _TEST_IN_SPI, 1) + self.ReceivePacketOn(underlying_netid, input_pkt) + + # Verify that the packet data and src are correct + data, src = read_sock.recvfrom(4096) + self.assertEquals(net_test.UDP_PAYLOAD, data) + self.assertEquals((remote_inner, _TEST_REMOTE_PORT), src[:2]) + + def _TestTunnel(self, inner_version, outer_version, func, direction, + test_output_mark_unset): + """Test a unidirectional XFRM Tunnel with explicit selectors""" # Select the underlying netid, which represents the external # interface from/to which to route ESP packets. - underlying_netid = self.RandomNetid() + u_netid = self.RandomNetid() # Select a random netid that will originate traffic locally and - # which represents the logical tunnel network. - netid = self.RandomNetid(exclude=underlying_netid) + # which represents the netid on which the plaintext is sent + netid = self.RandomNetid(exclude=u_netid) local_inner = self.MyAddress(inner_version, netid) remote_inner = _GetRemoteInnerAddress(inner_version) - local_outer = self.MyAddress(outer_version, underlying_netid) + local_outer = self.MyAddress(outer_version, u_netid) remote_outer = _GetRemoteOuterAddress(outer_version) - self.xfrm.CreateTunnel(xfrm.XFRM_POLICY_OUT, - xfrm.SrcDstSelector(local_inner, remote_inner), - local_outer, remote_outer, _TEST_OUT_SPI, - xfrm_base._ALGO_CBC_AES_256, - xfrm_base._ALGO_HMAC_SHA1, - None, underlying_netid) - - write_sock = socket(net_test.GetAddressFamily(inner_version), SOCK_DGRAM, 0) - # Select an interface, which provides the source address of the inner - # packet. - self.SelectInterface(write_sock, netid, "mark") - write_sock.sendto(net_test.UDP_PAYLOAD, (remote_inner, 53)) - self._ExpectEspPacketOn(underlying_netid, _TEST_OUT_SPI, 1, None, - local_outer, remote_outer) - - # TODO: Add support for the input path. + output_mark = u_netid + if test_output_mark_unset: + output_mark = None + self.SetDefaultNetwork(u_netid) - def testIpv4InIpv4TunnelOutput(self): - self._CheckTunnelOutput(4, 4) + try: + # Create input/ouput SPs, SAs and sockets to simulate a more realistic + # environment. + self.xfrm.CreateTunnel( + xfrm.XFRM_POLICY_IN, xfrm.SrcDstSelector(remote_inner, local_inner), + remote_outer, local_outer, _TEST_IN_SPI, xfrm_base._ALGO_CRYPT_NULL, + xfrm_base._ALGO_AUTH_NULL, None, None, None, xfrm.MATCH_METHOD_ALL) + + self.xfrm.CreateTunnel( + xfrm.XFRM_POLICY_OUT, xfrm.SrcDstSelector(local_inner, remote_inner), + local_outer, remote_outer, _TEST_OUT_SPI, xfrm_base._ALGO_CBC_AES_256, + xfrm_base._ALGO_HMAC_SHA1, None, output_mark, None, xfrm.MATCH_METHOD_ALL) + + write_sock = socket(net_test.GetAddressFamily(inner_version), SOCK_DGRAM, 0) + self.SelectInterface(write_sock, netid, "mark") + read_sock, _ = _CreateReceiveSock(inner_version) + + sock = write_sock if direction == xfrm.XFRM_POLICY_OUT else read_sock + func(inner_version, outer_version, u_netid, netid, local_inner, + remote_inner, local_outer, remote_outer, sock) + finally: + if test_output_mark_unset: + self.ClearDefaultNetwork() - def testIpv4InIpv6TunnelOutput(self): - self._CheckTunnelOutput(4, 6) + def ParamTestTunnelInput(self, inner_version, outer_version): + self._TestTunnel(inner_version, outer_version, self._CheckTunnelInput, + xfrm.XFRM_POLICY_IN, False) - def testIpv6InIpv4TunnelOutput(self): - self._CheckTunnelOutput(6, 4) + def ParamTestTunnelOutput(self, inner_version, outer_version): + self._TestTunnel(inner_version, outer_version, self._CheckTunnelOutput, + xfrm.XFRM_POLICY_OUT, False) - def testIpv6InIpv6TunnelOutput(self): - self._CheckTunnelOutput(6, 6) + def ParamTestTunnelOutputNoSetMark(self, inner_version, outer_version): + self._TestTunnel(inner_version, outer_version, self._CheckTunnelOutput, + xfrm.XFRM_POLICY_OUT, True) @unittest.skipUnless(net_test.LINUX_VERSION >= (3, 18, 0), "VTI Unsupported") class XfrmAddDeleteVtiTest(xfrm_base.XfrmBaseTest): - def verifyVtiInfoData(self, vti_info_data, version, local_addr, remote_addr, ikey, okey): + def _VerifyVtiInfoData(self, vti_info_data, version, local_addr, remote_addr, + ikey, okey): self.assertEquals(vti_info_data["IFLA_VTI_IKEY"], ikey) self.assertEquals(vti_info_data["IFLA_VTI_OKEY"], okey) family = AF_INET if version == 4 else AF_INET6 - self.assertEquals(inet_ntop(family, vti_info_data["IFLA_VTI_LOCAL"]), local_addr) - self.assertEquals(inet_ntop(family, vti_info_data["IFLA_VTI_REMOTE"]), remote_addr) + self.assertEquals(inet_ntop(family, vti_info_data["IFLA_VTI_LOCAL"]), + local_addr) + self.assertEquals(inet_ntop(family, vti_info_data["IFLA_VTI_REMOTE"]), + remote_addr) def testAddVti(self): """Test the creation of a Virtual Tunnel Interface.""" @@ -120,37 +248,37 @@ class XfrmAddDeleteVtiTest(xfrm_base.XfrmBaseTest): netid = self.RandomNetid() local_addr = self.MyAddress(version, netid) self.iproute.CreateVirtualTunnelInterface( - dev_name=_VTI_IFNAME, + dev_name=_TEST_XFRM_IFNAME, local_addr=local_addr, remote_addr=_GetRemoteOuterAddress(version), o_key=_TEST_OKEY, i_key=_TEST_IKEY) - self.verifyVtiInfoData(self.iproute.GetVtiInfoData(_VTI_IFNAME), - version, local_addr, _GetRemoteOuterAddress(version), - _TEST_IKEY, _TEST_OKEY) + self._VerifyVtiInfoData( + self.iproute.GetIfinfoData(_TEST_XFRM_IFNAME), version, local_addr, + _GetRemoteOuterAddress(version), _TEST_IKEY, _TEST_OKEY) new_remote_addr = {4: net_test.IPV4_ADDR2, 6: net_test.IPV6_ADDR2} - new_okey = _TEST_OKEY + _VTI_NETID - new_ikey = _TEST_IKEY + _VTI_NETID + new_okey = _TEST_OKEY + _TEST_XFRM_IF_ID + new_ikey = _TEST_IKEY + _TEST_XFRM_IF_ID self.iproute.CreateVirtualTunnelInterface( - dev_name=_VTI_IFNAME, + dev_name=_TEST_XFRM_IFNAME, local_addr=local_addr, remote_addr=new_remote_addr[version], o_key=new_okey, i_key=new_ikey, is_update=True) - self.verifyVtiInfoData(self.iproute.GetVtiInfoData(_VTI_IFNAME), - version, local_addr, new_remote_addr[version], - new_ikey, new_okey) + self._VerifyVtiInfoData( + self.iproute.GetIfinfoData(_TEST_XFRM_IFNAME), version, local_addr, + new_remote_addr[version], new_ikey, new_okey) - if_index = self.iproute.GetIfIndex(_VTI_IFNAME) + if_index = self.iproute.GetIfIndex(_TEST_XFRM_IFNAME) # Validate that the netlink interface matches the ioctl interface. - self.assertEquals(net_test.GetInterfaceIndex(_VTI_IFNAME), if_index) - self.iproute.DeleteLink(_VTI_IFNAME) + self.assertEquals(net_test.GetInterfaceIndex(_TEST_XFRM_IFNAME), if_index) + self.iproute.DeleteLink(_TEST_XFRM_IFNAME) with self.assertRaises(IOError): - self.iproute.GetIfIndex(_VTI_IFNAME) + self.iproute.GetIfIndex(_TEST_XFRM_IFNAME) def _QuietDeleteLink(self, ifname): try: @@ -161,100 +289,276 @@ class XfrmAddDeleteVtiTest(xfrm_base.XfrmBaseTest): def tearDown(self): super(XfrmAddDeleteVtiTest, self).tearDown() - self._QuietDeleteLink(_VTI_IFNAME) + self._QuietDeleteLink(_TEST_XFRM_IFNAME) + +class SaInfo(object): -class VtiInterface(object): + def __init__(self, spi): + self.spi = spi + self.seq_num = 1 - def __init__(self, iface, netid, underlying_netid, local, remote): + +class IpSecBaseInterface(object): + + def __init__(self, iface, netid, underlying_netid, local, remote, version): self.iface = iface self.netid = netid self.underlying_netid = underlying_netid self.local, self.remote = local, remote + + # XFRM interfaces technically do not have a version. This keeps track of + # the IP version of the local and remote addresses. + self.version = version self.rx = self.tx = 0 - self.ikey = _TEST_IKEY + netid - self.okey = _TEST_OKEY + netid - self.out_spi = self.in_spi = random.randint(0, 0x7fffffff) + self.addrs = {} self.iproute = iproute.IPRoute() self.xfrm = xfrm.Xfrm() - self.SetupInterface() - self.SetupXfrm() - self.addrs = {} - def Teardown(self): self.TeardownXfrm() self.TeardownInterface() - def SetupInterface(self): - self.iproute.CreateVirtualTunnelInterface( - self.iface, self.local, self.remote, self.ikey, self.okey) - def TeardownInterface(self): self.iproute.DeleteLink(self.iface) - def SetupXfrm(self): + def SetupXfrm(self, use_null_crypt): + rand_spi = random.randint(0, 0x7fffffff) + self.in_sa = SaInfo(rand_spi) + self.out_sa = SaInfo(rand_spi) + + # Select algorithms: + if use_null_crypt: + auth, crypt = xfrm_base._ALGO_AUTH_NULL, xfrm_base._ALGO_CRYPT_NULL + else: + auth, crypt = xfrm_base._ALGO_HMAC_SHA1, xfrm_base._ALGO_CBC_AES_256 + + self._SetupXfrmByType(auth, crypt) + + def Rekey(self, outer_family, new_out_sa, new_in_sa): + """Rekeys the Tunnel Interface + + Creates new SAs and updates the outbound security policy to use new SAs. + + Args: + outer_family: AF_INET or AF_INET6 + new_out_sa: An SaInfo struct representing the new outbound SA's info + new_in_sa: An SaInfo struct representing the new inbound SA's info + """ + self._Rekey(outer_family, new_out_sa, new_in_sa) + + # Update Interface object + self.out_sa = new_out_sa + self.in_sa = new_in_sa + + def TeardownXfrm(self): + raise NotImplementedError("Subclasses should implement this") + + def _SetupXfrmByType(self, auth_algo, crypt_algo): + raise NotImplementedError("Subclasses should implement this") + + def _Rekey(self, outer_family, new_out_sa, new_in_sa): + raise NotImplementedError("Subclasses should implement this") + + +class VtiInterface(IpSecBaseInterface): + + def __init__(self, iface, netid, underlying_netid, _, local, remote, version): + super(VtiInterface, self).__init__(iface, netid, underlying_netid, local, + remote, version) + + self.ikey = _TEST_IKEY + netid + self.okey = _TEST_OKEY + netid + + self.SetupInterface() + self.SetupXfrm(False) + + def SetupInterface(self): + return self.iproute.CreateVirtualTunnelInterface( + self.iface, self.local, self.remote, self.ikey, self.okey) + + def _SetupXfrmByType(self, auth_algo, crypt_algo): # For the VTI, the selectors are wildcard since packets will only # be selected if they have the appropriate mark, hence the inner # addresses are wildcard. self.xfrm.CreateTunnel(xfrm.XFRM_POLICY_OUT, None, self.local, self.remote, - self.out_spi, xfrm_base._ALGO_CBC_AES_256, - xfrm_base._ALGO_HMAC_SHA1, + self.out_sa.spi, crypt_algo, auth_algo, xfrm.ExactMatchMark(self.okey), - self.underlying_netid) + self.underlying_netid, None, xfrm.MATCH_METHOD_ALL) self.xfrm.CreateTunnel(xfrm.XFRM_POLICY_IN, None, self.remote, self.local, - self.in_spi, xfrm_base._ALGO_CBC_AES_256, - xfrm_base._ALGO_HMAC_SHA1, - xfrm.ExactMatchMark(self.ikey), None) + self.in_sa.spi, crypt_algo, auth_algo, + xfrm.ExactMatchMark(self.ikey), None, None, + xfrm.MATCH_METHOD_MARK) def TeardownXfrm(self): self.xfrm.DeleteTunnel(xfrm.XFRM_POLICY_OUT, None, self.remote, - self.out_spi, self.okey) + self.out_sa.spi, self.okey, None) self.xfrm.DeleteTunnel(xfrm.XFRM_POLICY_IN, None, self.local, - self.in_spi, self.ikey) + self.in_sa.spi, self.ikey, None) + + def _Rekey(self, outer_family, new_out_sa, new_in_sa): + # TODO: Consider ways to share code with xfrm.CreateTunnel(). It's mostly + # the same, but rekeys are asymmetric, and only update the outbound + # policy. + self.xfrm.AddSaInfo(self.local, self.remote, new_out_sa.spi, + xfrm.XFRM_MODE_TUNNEL, 0, xfrm_base._ALGO_CRYPT_NULL, + xfrm_base._ALGO_AUTH_NULL, None, None, + xfrm.ExactMatchMark(self.okey), self.underlying_netid) + + self.xfrm.AddSaInfo(self.remote, self.local, new_in_sa.spi, + xfrm.XFRM_MODE_TUNNEL, 0, xfrm_base._ALGO_CRYPT_NULL, + xfrm_base._ALGO_AUTH_NULL, None, None, + xfrm.ExactMatchMark(self.ikey), None) + + # Create new policies for IPv4 and IPv6. + for sel in [xfrm.EmptySelector(AF_INET), xfrm.EmptySelector(AF_INET6)]: + # Add SPI-specific output policy to enforce using new outbound SPI + policy = xfrm.UserPolicy(xfrm.XFRM_POLICY_OUT, sel) + tmpl = xfrm.UserTemplate(outer_family, new_out_sa.spi, 0, + (self.local, self.remote)) + self.xfrm.UpdatePolicyInfo(policy, tmpl, xfrm.ExactMatchMark(self.okey), + 0) + + def DeleteOldSaInfo(self, outer_family, old_in_spi, old_out_spi): + self.xfrm.DeleteSaInfo(self.local, old_in_spi, IPPROTO_ESP, + xfrm.ExactMatchMark(self.ikey)) + self.xfrm.DeleteSaInfo(self.remote, old_out_spi, IPPROTO_ESP, + xfrm.ExactMatchMark(self.okey)) + + +@unittest.skipUnless(HAVE_XFRM_INTERFACES, "XFRM interfaces unsupported") +class XfrmAddDeleteXfrmInterfaceTest(xfrm_base.XfrmBaseTest): + """Test the creation of an XFRM Interface.""" + + def testAddXfrmInterface(self): + self.iproute.CreateXfrmInterface(_TEST_XFRM_IFNAME, _TEST_XFRM_IF_ID, + _LOOPBACK_IFINDEX) + if_index = self.iproute.GetIfIndex(_TEST_XFRM_IFNAME) + net_test.SetInterfaceUp(_TEST_XFRM_IFNAME) + + # Validate that the netlink interface matches the ioctl interface. + self.assertEquals(net_test.GetInterfaceIndex(_TEST_XFRM_IFNAME), if_index) + self.iproute.DeleteLink(_TEST_XFRM_IFNAME) + with self.assertRaises(IOError): + self.iproute.GetIfIndex(_TEST_XFRM_IFNAME) + + +class XfrmInterface(IpSecBaseInterface): + + def __init__(self, iface, netid, underlying_netid, ifindex, local, remote, + version): + super(XfrmInterface, self).__init__(iface, netid, underlying_netid, local, + remote, version) + + self.ifindex = ifindex + self.xfrm_if_id = netid + self.SetupInterface() + self.SetupXfrm(False) -@unittest.skipUnless(net_test.LINUX_VERSION >= (3, 18, 0), "VTI Unsupported") -class XfrmVtiTest(xfrm_base.XfrmBaseTest): + def SetupInterface(self): + """Create an XFRM interface.""" + return self.iproute.CreateXfrmInterface(self.iface, self.netid, self.ifindex) + + def _SetupXfrmByType(self, auth_algo, crypt_algo): + self.xfrm.CreateTunnel(xfrm.XFRM_POLICY_OUT, None, self.local, self.remote, + self.out_sa.spi, crypt_algo, auth_algo, None, + self.underlying_netid, self.xfrm_if_id, + xfrm.MATCH_METHOD_ALL) + self.xfrm.CreateTunnel(xfrm.XFRM_POLICY_IN, None, self.remote, self.local, + self.in_sa.spi, crypt_algo, auth_algo, None, None, + self.xfrm_if_id, xfrm.MATCH_METHOD_IFID) + + def TeardownXfrm(self): + self.xfrm.DeleteTunnel(xfrm.XFRM_POLICY_OUT, None, self.remote, + self.out_sa.spi, None, self.xfrm_if_id) + self.xfrm.DeleteTunnel(xfrm.XFRM_POLICY_IN, None, self.local, + self.in_sa.spi, None, self.xfrm_if_id) + + def _Rekey(self, outer_family, new_out_sa, new_in_sa): + # TODO: Consider ways to share code with xfrm.CreateTunnel(). It's mostly + # the same, but rekeys are asymmetric, and only update the outbound + # policy. + self.xfrm.AddSaInfo( + self.local, self.remote, new_out_sa.spi, xfrm.XFRM_MODE_TUNNEL, 0, + xfrm_base._ALGO_CRYPT_NULL, xfrm_base._ALGO_AUTH_NULL, None, None, + None, self.underlying_netid, xfrm_if_id=self.xfrm_if_id) + + self.xfrm.AddSaInfo( + self.remote, self.local, new_in_sa.spi, xfrm.XFRM_MODE_TUNNEL, 0, + xfrm_base._ALGO_CRYPT_NULL, xfrm_base._ALGO_AUTH_NULL, None, None, + None, None, xfrm_if_id=self.xfrm_if_id) + + # Create new policies for IPv4 and IPv6. + for sel in [xfrm.EmptySelector(AF_INET), xfrm.EmptySelector(AF_INET6)]: + # Add SPI-specific output policy to enforce using new outbound SPI + policy = xfrm.UserPolicy(xfrm.XFRM_POLICY_OUT, sel) + tmpl = xfrm.UserTemplate(outer_family, new_out_sa.spi, 0, + (self.local, self.remote)) + self.xfrm.UpdatePolicyInfo(policy, tmpl, None, self.xfrm_if_id) + + def DeleteOldSaInfo(self, outer_family, old_in_spi, old_out_spi): + self.xfrm.DeleteSaInfo(self.local, old_in_spi, IPPROTO_ESP, None, + self.xfrm_if_id) + self.xfrm.DeleteSaInfo(self.remote, old_out_spi, IPPROTO_ESP, None, + self.xfrm_if_id) + + +class XfrmTunnelBase(xfrm_base.XfrmBaseTest): @classmethod def setUpClass(cls): xfrm_base.XfrmBaseTest.setUpClass() - # VTI interfaces use marks extensively, so configure realistic packet + # Tunnel interfaces use marks extensively, so configure realistic packet # marking rules to make the test representative, make PMTUD work, etc. cls.SetInboundMarks(True) cls.SetMarkReflectSysctls(1) - cls.vtis = {} + # Group by tunnel version to ensure that we test at least one IPv4 and one + # IPv6 tunnel + cls.tunnelsV4 = {} + cls.tunnelsV6 = {} for i, underlying_netid in enumerate(cls.tuns): for version in 4, 6: - netid = _BASE_VTI_NETID[version] + i + netid = _BASE_TUNNEL_NETID[version] + _TUNNEL_NETID_OFFSET + i iface = "ipsec%s" % netid local = cls.MyAddress(version, underlying_netid) if version == 4: - remote = net_test.IPV4_ADDR2 if (i % 2) else net_test.IPV4_ADDR + remote = (net_test.IPV4_ADDR if (i % 2) else net_test.IPV4_ADDR2) else: - remote = net_test.IPV6_ADDR2 if (i % 2) else net_test.IPV6_ADDR - vti = VtiInterface(iface, netid, underlying_netid, local, remote) + remote = (net_test.IPV6_ADDR if (i % 2) else net_test.IPV6_ADDR2) + + ifindex = cls.ifindices[underlying_netid] + tunnel = cls.INTERFACE_CLASS(iface, netid, underlying_netid, ifindex, + local, remote, version) cls._SetInboundMarking(netid, iface, True) - cls._SetupVtiNetwork(vti, True) - cls.vtis[netid] = vti + cls._SetupTunnelNetwork(tunnel, True) + + if version == 4: + cls.tunnelsV4[netid] = tunnel + else: + cls.tunnelsV6[netid] = tunnel @classmethod def tearDownClass(cls): # The sysctls are restored by MultinetworkBaseTest.tearDownClass. cls.SetInboundMarks(False) - for vti in cls.vtis.values(): - cls._SetInboundMarking(vti.netid, vti.iface, False) - cls._SetupVtiNetwork(vti, False) - vti.Teardown() + for tunnel in cls.tunnelsV4.values() + cls.tunnelsV6.values(): + cls._SetInboundMarking(tunnel.netid, tunnel.iface, False) + cls._SetupTunnelNetwork(tunnel, False) + tunnel.Teardown() xfrm_base.XfrmBaseTest.tearDownClass() + def randomTunnel(self, outer_version): + version_dict = self.tunnelsV4 if outer_version == 4 else self.tunnelsV6 + return random.choice(version_dict.values()) + def setUp(self): multinetwork_base.MultiNetworkBaseTest.setUp(self) self.iproute = iproute.IPRoute() + self.xfrm = xfrm.Xfrm() def tearDown(self): multinetwork_base.MultiNetworkBaseTest.tearDown(self) @@ -275,16 +579,23 @@ class XfrmVtiTest(xfrm_base.XfrmBaseTest): net_test.AddressLengthBits(version), ifindex) @classmethod - def _SetupVtiNetwork(cls, vti, is_add): - """Setup rules and routes for a VTI Network. + def _GetLocalAddress(cls, version, netid): + if version == 4: + return cls._MyIPv4Address(netid - _TUNNEL_NETID_OFFSET) + else: + return cls.OnlinkPrefix(6, netid - _TUNNEL_NETID_OFFSET) + "1" + + @classmethod + def _SetupTunnelNetwork(cls, tunnel, is_add): + """Setup rules and routes for a tunnel Network. Takes an interface and depending on the boolean value of is_add, either adds or removes the rules - and routes for a VTI to behave like an Android - Network for purposes of testing. + and routes for a tunnel interface to behave like an + Android Network for purposes of testing. Args: - vti: A VtiInterface, the VTI to set up. + tunnel: A VtiInterface or XfrmInterface, the tunnel to set up. is_add: Boolean that causes this method to perform setup if True or teardown if False """ @@ -292,32 +603,30 @@ class XfrmVtiTest(xfrm_base.XfrmBaseTest): # Disable router solicitations to avoid occasional spurious packets # arriving on the underlying network; there are two possible behaviors # when that occurred: either only the RA packet is read, and when it - # is echoed back to the VTI, it causes the test to fail by not receiving - # the UDP_PAYLOAD; or, two packets may arrive on the underlying - # network which fails the assertion that only one ESP packet is received. + # is echoed back to the tunnel, it causes the test to fail by not + # receiving # the UDP_PAYLOAD; or, two packets may arrive on the + # underlying # network which fails the assertion that only one ESP packet + # is received. cls.SetSysctl( - "/proc/sys/net/ipv6/conf/%s/router_solicitations" % vti.iface, 0) - net_test.SetInterfaceUp(vti.iface) + "/proc/sys/net/ipv6/conf/%s/router_solicitations" % tunnel.iface, 0) + net_test.SetInterfaceUp(tunnel.iface) for version in [4, 6]: - ifindex = net_test.GetInterfaceIndex(vti.iface) - table = vti.netid + ifindex = net_test.GetInterfaceIndex(tunnel.iface) + table = tunnel.netid # Set up routing rules. - start, end = cls.UidRangeForNetid(vti.netid) + start, end = cls.UidRangeForNetid(tunnel.netid) cls.iproute.UidRangeRule(version, is_add, start, end, table, cls.PRIORITY_UID) - cls.iproute.OifRule(version, is_add, vti.iface, table, cls.PRIORITY_OIF) - cls.iproute.FwmarkRule(version, is_add, vti.netid, cls.NETID_FWMASK, + cls.iproute.OifRule(version, is_add, tunnel.iface, table, cls.PRIORITY_OIF) + cls.iproute.FwmarkRule(version, is_add, tunnel.netid, cls.NETID_FWMASK, table, cls.PRIORITY_FWMARK) # Configure IP addresses. - if version == 4: - addr = cls._MyIPv4Address(vti.netid) - else: - addr = cls.OnlinkPrefix(6, vti.netid) + "1" + addr = cls._GetLocalAddress(version, tunnel.netid) prefixlen = net_test.AddressLengthBits(version) - vti.addrs[version] = addr + tunnel.addrs[version] = addr if is_add: cls.iproute.AddAddress(addr, prefixlen, ifindex) cls.iproute.AddRoute(version, table, "default", 0, None, ifindex) @@ -325,100 +634,320 @@ class XfrmVtiTest(xfrm_base.XfrmBaseTest): cls.iproute.DelRoute(version, table, "default", 0, None, ifindex) cls.iproute.DelAddress(addr, prefixlen, ifindex) - def assertReceivedPacket(self, vti): - vti.rx += 1 - self.assertEquals((vti.rx, vti.tx), self.iproute.GetRxTxPackets(vti.iface)) - - def assertSentPacket(self, vti): - vti.tx += 1 - self.assertEquals((vti.rx, vti.tx), self.iproute.GetRxTxPackets(vti.iface)) - - # TODO: Should we completely re-write this using null encryption and null - # authentication? We could then assemble and disassemble packets for each - # direction individually. This approach would improve debuggability, avoid the - # complexity of the twister, and allow the test to more-closely validate - # deployable configurations. - def _CheckVtiInputOutput(self, vti, inner_version): - local_outer = vti.local - remote_outer = vti.remote - - # Create a socket to receive packets. - read_sock = socket( - net_test.GetAddressFamily(inner_version), SOCK_DGRAM, 0) - read_sock.bind((net_test.GetWildcardAddress(inner_version), 0)) - # The second parameter of the tuple is the port number regardless of AF. - port = read_sock.getsockname()[1] - # Guard against the eventuality of the receive failing. - csocket.SetSocketTimeout(read_sock, 100) - - # Send a packet out via the vti-backed network, bound for the port number - # of the input socket. - write_sock = socket( - net_test.GetAddressFamily(inner_version), SOCK_DGRAM, 0) - self.SelectInterface(write_sock, vti.netid, "mark") - write_sock.sendto(net_test.UDP_PAYLOAD, - (_GetRemoteInnerAddress(inner_version), port)) + def assertReceivedPacket(self, tunnel, sa_info): + tunnel.rx += 1 + self.assertEquals((tunnel.rx, tunnel.tx), + self.iproute.GetRxTxPackets(tunnel.iface)) + sa_info.seq_num += 1 + + def assertSentPacket(self, tunnel, sa_info): + tunnel.tx += 1 + self.assertEquals((tunnel.rx, tunnel.tx), + self.iproute.GetRxTxPackets(tunnel.iface)) + sa_info.seq_num += 1 + + def _CheckTunnelInput(self, tunnel, inner_version, local_inner, remote_inner, + sa_info=None, expect_fail=False): + """Test null-crypt input path over an IPsec interface.""" + if sa_info is None: + sa_info = tunnel.in_sa + read_sock, local_port = _CreateReceiveSock(inner_version) + + input_pkt = _GetNullAuthCryptTunnelModePkt( + inner_version, remote_inner, tunnel.remote, _TEST_REMOTE_PORT, + local_inner, tunnel.local, local_port, sa_info.spi, sa_info.seq_num) + self.ReceivePacketOn(tunnel.underlying_netid, input_pkt) + + if expect_fail: + self.assertRaisesErrno(EAGAIN, read_sock.recv, 4096) + else: + # Verify that the packet data and src are correct + data, src = read_sock.recvfrom(4096) + self.assertReceivedPacket(tunnel, sa_info) + self.assertEquals(net_test.UDP_PAYLOAD, data) + self.assertEquals((remote_inner, _TEST_REMOTE_PORT), src[:2]) + + def _CheckTunnelOutput(self, tunnel, inner_version, local_inner, + remote_inner, sa_info=None): + """Test null-crypt output path over an IPsec interface.""" + if sa_info is None: + sa_info = tunnel.out_sa + local_port = _SendPacket(self, tunnel.netid, inner_version, remote_inner, + _TEST_REMOTE_PORT) # Read a tunneled IP packet on the underlying (outbound) network # verifying that it is an ESP packet. - self.assertSentPacket(vti) - pkt = self._ExpectEspPacketOn(vti.underlying_netid, vti.out_spi, vti.tx, None, - local_outer, remote_outer) - - # Perform an address switcheroo so that the inner address of the remote - # end of the tunnel is now the address on the local VTI interface; this - # way, the twisted inner packet finds a destination via the VTI once - # decrypted. - remote = _GetRemoteInnerAddress(inner_version) - local = vti.addrs[inner_version] - self._SwapInterfaceAddress(vti.iface, new_addr=remote, old_addr=local) + pkt = self._ExpectEspPacketOn(tunnel.underlying_netid, sa_info.spi, + sa_info.seq_num, None, tunnel.local, + tunnel.remote) + + # Get and update the IP headers on the inner payload so that we can do a simple + # comparison of byte data. Unfortunately, due to the scapy version this runs on, + # we cannot parse past the ESP header to the inner IP header, and thus have to + # workaround in this manner + if inner_version == 4: + ip_hdr_options = { + 'id': scapy.IP(str(pkt.payload)[8:]).id, + 'flags': scapy.IP(str(pkt.payload)[8:]).flags + } + else: + ip_hdr_options = {'fl': scapy.IPv6(str(pkt.payload)[8:]).fl} + + expected = _GetNullAuthCryptTunnelModePkt( + inner_version, local_inner, tunnel.local, local_port, remote_inner, + tunnel.remote, _TEST_REMOTE_PORT, sa_info.spi, sa_info.seq_num, + ip_hdr_options) + + # Check outer header manually (Avoids having to overwrite outer header's + # id, flags or flow label) + self.assertSentPacket(tunnel, sa_info) + self.assertEquals(expected.src, pkt.src) + self.assertEquals(expected.dst, pkt.dst) + self.assertEquals(len(expected), len(pkt)) + + # Check everything else + self.assertEquals(str(expected.payload), str(pkt.payload)) + + def _CheckTunnelEncryption(self, tunnel, inner_version, local_inner, + remote_inner): + """Test both input and output paths over an encrypted IPsec interface. + + This tests specifically makes sure that the both encryption and decryption + work together, as opposed to the _CheckTunnel(Input|Output) where the + input and output paths are tested separately, and using null encryption. + """ + src_port = _SendPacket(self, tunnel.netid, inner_version, remote_inner, + _TEST_REMOTE_PORT) + + # Make sure it appeared on the underlying interface + pkt = self._ExpectEspPacketOn(tunnel.underlying_netid, tunnel.out_sa.spi, + tunnel.out_sa.seq_num, None, tunnel.local, + tunnel.remote) + + # Check that packet is not sent in plaintext + self.assertTrue(str(net_test.UDP_PAYLOAD) not in str(pkt)) + + # Check src/dst + self.assertEquals(tunnel.local, pkt.src) + self.assertEquals(tunnel.remote, pkt.dst) + + # Check that the interface statistics recorded the outbound packet + self.assertSentPacket(tunnel, tunnel.out_sa) + try: - # Swap the packet's IP headers and write it back to the - # underlying network. + # Swap the interface addresses to pretend we are the remote + self._SwapInterfaceAddress( + tunnel.iface, new_addr=remote_inner, old_addr=local_inner) + + # Swap the packet's IP headers and write it back to the underlying + # network. pkt = TunTwister.TwistPacket(pkt) - self.ReceivePacketOn(vti.underlying_netid, pkt) - self.assertReceivedPacket(vti) - # Receive the decrypted packet on the dest port number. - read_packet = read_sock.recv(4096) - self.assertEquals(read_packet, net_test.UDP_PAYLOAD) - finally: - # Unwind the switcheroo - self._SwapInterfaceAddress(vti.iface, new_addr=local, old_addr=remote) + read_sock, local_port = _CreateReceiveSock(inner_version, + _TEST_REMOTE_PORT) + self.ReceivePacketOn(tunnel.underlying_netid, pkt) + + # Verify that the packet data and src are correct + data, src = read_sock.recvfrom(4096) + self.assertEquals(net_test.UDP_PAYLOAD, data) + self.assertEquals((local_inner, src_port), src[:2]) + # Check that the interface statistics recorded the inbound packet + self.assertReceivedPacket(tunnel, tunnel.in_sa) + finally: + # Swap the interface addresses to pretend we are the remote + self._SwapInterfaceAddress( + tunnel.iface, new_addr=local_inner, old_addr=remote_inner) + + def _CheckTunnelIcmp(self, tunnel, inner_version, local_inner, remote_inner, + sa_info=None): + """Test ICMP error path over an IPsec interface.""" + if sa_info is None: + sa_info = tunnel.out_sa # Now attempt to provoke an ICMP error. # TODO: deduplicate with multinetwork_test.py. - version = net_test.GetAddressVersion(vti.remote) dst_prefix, intermediate = { 4: ("172.19.", "172.16.9.12"), 6: ("2001:db8::", "2001:db8::1") - }[version] + }[tunnel.version] + + local_port = _SendPacket(self, tunnel.netid, inner_version, remote_inner, + _TEST_REMOTE_PORT) + pkt = self._ExpectEspPacketOn(tunnel.underlying_netid, sa_info.spi, + sa_info.seq_num, None, tunnel.local, + tunnel.remote) + self.assertSentPacket(tunnel, sa_info) - write_sock.sendto(net_test.UDP_PAYLOAD, - (_GetRemoteInnerAddress(inner_version), port)) - self.assertSentPacket(vti) - pkt = self._ExpectEspPacketOn(vti.underlying_netid, vti.out_spi, vti.tx, None, - local_outer, remote_outer) - myaddr = self.MyAddress(version, vti.underlying_netid) - _, toobig = packets.ICMPPacketTooBig(version, intermediate, myaddr, pkt) - self.ReceivePacketOn(vti.underlying_netid, toobig) + myaddr = self.MyAddress(tunnel.version, tunnel.underlying_netid) + _, toobig = packets.ICMPPacketTooBig(tunnel.version, intermediate, myaddr, + pkt) + self.ReceivePacketOn(tunnel.underlying_netid, toobig) # Check that the packet too big reduced the MTU. - routes = self.iproute.GetRoutes(vti.remote, 0, vti.underlying_netid, None) + routes = self.iproute.GetRoutes(tunnel.remote, 0, tunnel.underlying_netid, None) self.assertEquals(1, len(routes)) rtmsg, attributes = routes[0] self.assertEquals(iproute.RTN_UNICAST, rtmsg.type) self.assertEquals(packets.PTB_MTU, attributes["RTA_METRICS"]["RTAX_MTU"]) # Clear PMTU information so that future tests don't have to worry about it. - self.InvalidateDstCache(version, vti.underlying_netid) + self.InvalidateDstCache(tunnel.version, tunnel.underlying_netid) - def testVtiInputOutput(self): + def _CheckTunnelEncryptionWithIcmp(self, tunnel, inner_version, local_inner, + remote_inner): + """Test combined encryption path with ICMP errors over an IPsec tunnel""" + self._CheckTunnelEncryption(tunnel, inner_version, local_inner, + remote_inner) + self._CheckTunnelIcmp(tunnel, inner_version, local_inner, remote_inner) + self._CheckTunnelEncryption(tunnel, inner_version, local_inner, + remote_inner) + + def _TestTunnel(self, inner_version, outer_version, func, use_null_crypt): + """Bootstrap method to setup and run tests for the given parameters.""" + tunnel = self.randomTunnel(outer_version) + + try: + # Some tests require that the out_seq_num and in_seq_num are the same + # (Specifically encrypted tests), rebuild SAs to ensure seq_num is 1 + # + # Until we get better scapy support, the only way we can build an + # encrypted packet is to send it out, and read the packet from the wire. + # We then generally use this as the "inbound" encrypted packet, injecting + # it into the interface for which it is expected on. + # + # As such, this is required to ensure that encrypted packets (which we + # currently have no way to easily modify) are not considered replay + # attacks by the inbound SA. (eg: received 3 packets, seq_num_in = 3, + # sent only 1, # seq_num_out = 1, inbound SA would consider this a replay + # attack) + tunnel.TeardownXfrm() + tunnel.SetupXfrm(use_null_crypt) + + local_inner = tunnel.addrs[inner_version] + remote_inner = _GetRemoteInnerAddress(inner_version) + + for i in range(2): + func(tunnel, inner_version, local_inner, remote_inner) + finally: + if use_null_crypt: + tunnel.TeardownXfrm() + tunnel.SetupXfrm(False) + + def _CheckTunnelRekey(self, tunnel, inner_version, local_inner, remote_inner): + old_out_sa = tunnel.out_sa + old_in_sa = tunnel.in_sa + + # Check to make sure that both directions work before rekey + self._CheckTunnelInput(tunnel, inner_version, local_inner, remote_inner, + old_in_sa) + self._CheckTunnelOutput(tunnel, inner_version, local_inner, remote_inner, + old_out_sa) + + # Rekey + outer_family = net_test.GetAddressFamily(tunnel.version) + + # Create new SA + # Distinguish the new SAs with new SPIs. + new_out_sa = SaInfo(old_out_sa.spi + 1) + new_in_sa = SaInfo(old_in_sa.spi + 1) + + # Perform Rekey + tunnel.Rekey(outer_family, new_out_sa, new_in_sa) + + # Expect that the old SPI still works for inbound packets + self._CheckTunnelInput(tunnel, inner_version, local_inner, remote_inner, + old_in_sa) + + # Test both paths with new SPIs, expect outbound to use new SPI + self._CheckTunnelInput(tunnel, inner_version, local_inner, remote_inner, + new_in_sa) + self._CheckTunnelOutput(tunnel, inner_version, local_inner, remote_inner, + new_out_sa) + + # Delete old SAs + tunnel.DeleteOldSaInfo(outer_family, old_in_sa.spi, old_out_sa.spi) + + # Test both paths with new SPIs; should still work + self._CheckTunnelInput(tunnel, inner_version, local_inner, remote_inner, + new_in_sa) + self._CheckTunnelOutput(tunnel, inner_version, local_inner, remote_inner, + new_out_sa) + + # Expect failure upon trying to receive a packet with the deleted SPI + self._CheckTunnelInput(tunnel, inner_version, local_inner, remote_inner, + old_in_sa, True) + + def _TestTunnelRekey(self, inner_version, outer_version): """Test packet input and output over a Virtual Tunnel Interface.""" - for i in xrange(3 * len(self.vtis.values())): - vti = random.choice(self.vtis.values()) - self._CheckVtiInputOutput(vti, 4) - self._CheckVtiInputOutput(vti, 6) + tunnel = self.randomTunnel(outer_version) + + try: + # Always use null_crypt, so we can check input and output separately + tunnel.TeardownXfrm() + tunnel.SetupXfrm(True) + + local_inner = tunnel.addrs[inner_version] + remote_inner = _GetRemoteInnerAddress(inner_version) + + self._CheckTunnelRekey(tunnel, inner_version, local_inner, remote_inner) + finally: + tunnel.TeardownXfrm() + tunnel.SetupXfrm(False) + + +@unittest.skipUnless(net_test.LINUX_VERSION >= (3, 18, 0), "VTI Unsupported") +class XfrmVtiTest(XfrmTunnelBase): + + INTERFACE_CLASS = VtiInterface + + def ParamTestVtiInput(self, inner_version, outer_version): + self._TestTunnel(inner_version, outer_version, self._CheckTunnelInput, True) + + def ParamTestVtiOutput(self, inner_version, outer_version): + self._TestTunnel(inner_version, outer_version, self._CheckTunnelOutput, + True) + + def ParamTestVtiInOutEncrypted(self, inner_version, outer_version): + self._TestTunnel(inner_version, outer_version, self._CheckTunnelEncryption, + False) + + def ParamTestVtiIcmp(self, inner_version, outer_version): + self._TestTunnel(inner_version, outer_version, self._CheckTunnelIcmp, False) + + def ParamTestVtiEncryptionWithIcmp(self, inner_version, outer_version): + self._TestTunnel(inner_version, outer_version, + self._CheckTunnelEncryptionWithIcmp, False) + + def ParamTestVtiRekey(self, inner_version, outer_version): + self._TestTunnelRekey(inner_version, outer_version) + + +@unittest.skipUnless(HAVE_XFRM_INTERFACES, "XFRM interfaces unsupported") +class XfrmInterfaceTest(XfrmTunnelBase): + + INTERFACE_CLASS = XfrmInterface + + def ParamTestXfrmIntfInput(self, inner_version, outer_version): + self._TestTunnel(inner_version, outer_version, self._CheckTunnelInput, True) + + def ParamTestXfrmIntfOutput(self, inner_version, outer_version): + self._TestTunnel(inner_version, outer_version, self._CheckTunnelOutput, + True) + + def ParamTestXfrmIntfInOutEncrypted(self, inner_version, outer_version): + self._TestTunnel(inner_version, outer_version, self._CheckTunnelEncryption, + False) + + def ParamTestXfrmIntfIcmp(self, inner_version, outer_version): + self._TestTunnel(inner_version, outer_version, self._CheckTunnelIcmp, False) + + def ParamTestXfrmIntfEncryptionWithIcmp(self, inner_version, outer_version): + self._TestTunnel(inner_version, outer_version, + self._CheckTunnelEncryptionWithIcmp, False) + + def ParamTestXfrmIntfRekey(self, inner_version, outer_version): + self._TestTunnelRekey(inner_version, outer_version) if __name__ == "__main__": + InjectTests() unittest.main() |