From d777ed231c2416f044e636813d6001853187ee61 Mon Sep 17 00:00:00 2001 From: Benedict Wong Date: Tue, 10 Oct 2017 20:47:43 -0700 Subject: [PATCH] Add UDP encap socket test to CTS tests Tests basic functionality of UDP encap sockets: > Creates SPIs > Creates & applies transforms > Creates UDP encap socket > Ensures UDP encap socket port is non-zero > Validates that ESP, IKE packets can be sent correctly Test: Ran on aosp_angler-eng Bug: 67662580 Change-Id: Ia7eca384cd779e663d53cbede97ec70da33d28ec --- .../src/android/net/cts/IpSecManagerTest.java | 273 ++++++++++++++++-- 1 file changed, 252 insertions(+), 21 deletions(-) diff --git a/tests/cts/net/src/android/net/cts/IpSecManagerTest.java b/tests/cts/net/src/android/net/cts/IpSecManagerTest.java index bcecee17e4..599102cf08 100644 --- a/tests/cts/net/src/android/net/cts/IpSecManagerTest.java +++ b/tests/cts/net/src/android/net/cts/IpSecManagerTest.java @@ -22,13 +22,15 @@ import android.net.IpSecAlgorithm; import android.net.IpSecManager; import android.net.IpSecTransform; import android.os.ParcelFileDescriptor; +import android.system.ErrnoException; import android.system.Os; import android.system.OsConstants; import android.test.AndroidTestCase; -import java.io.ByteArrayOutputStream; -import java.net.DatagramSocket; import java.io.FileDescriptor; +import java.net.DatagramSocket; import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.ServerSocket; import java.net.UnknownHostException; import java.util.Arrays; @@ -40,34 +42,36 @@ public class IpSecManagerTest extends AndroidTestCase { private ConnectivityManager mCM; - private static final InetAddress GOOGLE_DNS_4; - private static final InetAddress GOOGLE_DNS_6; - - static { + private static InetAddress IpAddress(String addrString) { try { - // Google Public DNS Addresses; - GOOGLE_DNS_4 = InetAddress.getByName("8.8.8.8"); - GOOGLE_DNS_6 = InetAddress.getByName("2001:4860:4860::8888"); + return InetAddress.getByName(addrString); } catch (UnknownHostException e) { - throw new RuntimeException("Could not resolve DNS Addresses", e); + throw new IllegalArgumentException("Invalid IP address: " + e); } } + private static final InetAddress GOOGLE_DNS_4 = IpAddress("8.8.8.8"); + private static final InetAddress GOOGLE_DNS_6 = IpAddress("2001:4860:4860::8888"); + private static final InetAddress LOOPBACK_4 = IpAddress("127.0.0.1"); + private static final InetAddress[] GOOGLE_DNS_LIST = new InetAddress[] {GOOGLE_DNS_4, GOOGLE_DNS_6}; private static final int DROID_SPI = 0xD1201D; + private static final int MAX_PORT_BIND_ATTEMPTS = 10; - private static final byte[] CRYPT_KEY = - new byte[] { - 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, - 0x0E, 0x0F - }; - private static final byte[] AUTH_KEY = - new byte[] { - 0x7A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x7F - }; + private static final byte[] CRYPT_KEY = { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F + }; + private static final byte[] AUTH_KEY = { + 0x7A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, + 0x7A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F + }; protected void setUp() throws Exception { super.setUp(); @@ -119,7 +123,7 @@ public class IpSecManagerTest extends AndroidTestCase { * send data (expect exception) */ public void testCreateTransform() throws Exception { - InetAddress local = InetAddress.getLoopbackAddress(); + InetAddress local = LOOPBACK_4; IpSecManager.SecurityParameterIndex outSpi = mISM.reserveSecurityParameterIndex(IpSecTransform.DIRECTION_OUT, local); @@ -168,4 +172,231 @@ public class IpSecManagerTest extends AndroidTestCase { Os.close(udpSocket); transform.close(); } + + public void testOpenUdpEncapSocketSpecificPort() throws Exception { + IpSecManager.UdpEncapsulationSocket encapSocket = null; + int port = -1; + for (int i = 0; i < MAX_PORT_BIND_ATTEMPTS; i++) { + try { + port = findUnusedPort(); + encapSocket = mISM.openUdpEncapsulationSocket(port); + break; + } catch (ErrnoException e) { + if (e.errno == OsConstants.EADDRINUSE) { + // Someone claimed the port since we called findUnusedPort. + continue; + } + throw e; + } finally { + if (encapSocket != null) { + encapSocket.close(); + } + } + } + + if (encapSocket == null) { + fail("Failed " + MAX_PORT_BIND_ATTEMPTS + " attempts to bind to a port"); + } + + assertTrue("Returned invalid port", encapSocket.getPort() == port); + } + + public void testOpenUdpEncapSocketRandomPort() throws Exception { + try (IpSecManager.UdpEncapsulationSocket encapSocket = mISM.openUdpEncapsulationSocket()) { + assertTrue("Returned invalid port", encapSocket.getPort() != 0); + } + } + + public void testUdpEncapsulation() throws Exception { + InetAddress local = LOOPBACK_4; + + // TODO: Refactor to make this more representative of a normal application use case. (use + // separate sockets for inbound and outbound) + // Create SPIs, UDP encap socket + try (IpSecManager.UdpEncapsulationSocket encapSocket = mISM.openUdpEncapsulationSocket(); + IpSecManager.SecurityParameterIndex outSpi = + mISM.reserveSecurityParameterIndex(IpSecTransform.DIRECTION_OUT, local); + IpSecManager.SecurityParameterIndex inSpi = + mISM.reserveSecurityParameterIndex( + IpSecTransform.DIRECTION_IN, local, outSpi.getSpi()); + IpSecTransform transform = + buildIpSecTransform(mContext, inSpi, outSpi, encapSocket, local)) { + + // Create user socket, apply transform to it + FileDescriptor udpSocket = null; + try { + udpSocket = getTestV4UdpSocket(local); + int port = getPort(udpSocket); + + mISM.applyTransportModeTransform(udpSocket, transform); + + // Send an ESP packet from this socket to itself. Since the inbound and + // outbound transforms match, we should receive the data we sent. + byte[] data = new String("IPSec UDP-encap-ESP test data").getBytes("UTF-8"); + Os.sendto(udpSocket, data, 0, data.length, 0, local, port); + byte[] in = new byte[data.length]; + Os.read(udpSocket, in, 0, in.length); + assertTrue("Encapsulated data did not match.", Arrays.equals(data, in)); + + // Send an IKE packet from this socket to itself. IKE packets (SPI of 0) + // are not transformed in any way, and should be sent in the clear + // We expect this to work too (no inbound transforms) + final byte[] header = new byte[] {0, 0, 0, 0}; + final String message = "Sample IKE Packet"; + data = (new String(header) + message).getBytes("UTF-8"); + Os.sendto( + encapSocket.getSocket(), + data, + 0, + data.length, + 0, + local, + encapSocket.getPort()); + in = new byte[data.length]; + Os.read(encapSocket.getSocket(), in, 0, in.length); + assertTrue( + "Encap socket was unable to send/receive IKE data", + Arrays.equals(data, in)); + + mISM.removeTransportModeTransform(udpSocket, transform); + } finally { + if (udpSocket != null) { + Os.close(udpSocket); + } + } + } + } + + public void testIke() throws Exception { + InetAddress local = LOOPBACK_4; + + // TODO: Refactor to make this more representative of a normal application use case. (use + // separate sockets for inbound and outbound) + try (IpSecManager.UdpEncapsulationSocket encapSocket = mISM.openUdpEncapsulationSocket(); + IpSecManager.SecurityParameterIndex outSpi = + mISM.reserveSecurityParameterIndex(IpSecTransform.DIRECTION_OUT, local); + IpSecManager.SecurityParameterIndex inSpi = + mISM.reserveSecurityParameterIndex(IpSecTransform.DIRECTION_IN, local); + IpSecTransform transform = + buildIpSecTransform(mContext, inSpi, outSpi, encapSocket, local)) { + + // Create user socket, apply transform to it + FileDescriptor sock = null; + + try { + sock = getTestV4UdpSocket(local); + int port = getPort(sock); + + mISM.applyTransportModeTransform(sock, transform); + + // TODO: Find a way to set a timeout on the socket, and assert the ESP packet + // doesn't make it through. Setting sockopts currently throws EPERM (possibly + // because it is owned by a different UID). + + // Send ESP packet from our socket to the encap socket. The SPIs do not + // match, and we should expect this packet to be dropped. + byte[] header = new byte[] {1, 1, 1, 1}; + String message = "Sample ESP Packet"; + byte[] data = (new String(header) + message).getBytes("UTF-8"); + Os.sendto(sock, data, 0, data.length, 0, local, encapSocket.getPort()); + + // Send IKE packet from the encap socket to itself. Since IKE is not + // transformed in any way, this should succeed. + header = new byte[] {0, 0, 0, 0}; + message = "Sample IKE Packet"; + data = (new String(header) + message).getBytes("UTF-8"); + Os.sendto( + encapSocket.getSocket(), + data, + 0, + data.length, + 0, + local, + encapSocket.getPort()); + + // ESP data should be dropped, due to different input SPI (as opposed to being + // readable from the encapSocket) + // Thus, only IKE data should be received from the socket. + // If the first four bytes are zero, assume non-ESP (IKE) traffic. + // Expect an nulled out SPI just as we sent out, without being modified. + byte[] in = new byte[4]; + in[0] = 1; // Make sure the array has to be overwritten to pass + Os.read(encapSocket.getSocket(), in, 0, in.length); + assertTrue( + "Encap socket received UDP-encap-ESP data despite invalid SPIs", + Arrays.equals(header, in)); + + mISM.removeTransportModeTransform(sock, transform); + } finally { + if (sock != null) { + Os.close(sock); + } + } + } + } + + /** This function finds an available port */ + private static int findUnusedPort() throws Exception { + // Get an available port. + ServerSocket s = new ServerSocket(0); + int port = s.getLocalPort(); + s.close(); + return port; + } + + private static IpSecTransform buildIpSecTransform( + Context mContext, + IpSecManager.SecurityParameterIndex inSpi, + IpSecManager.SecurityParameterIndex outSpi, + IpSecManager.UdpEncapsulationSocket encapSocket, + InetAddress remoteAddr) + throws Exception { + return new IpSecTransform.Builder(mContext) + .setSpi(IpSecTransform.DIRECTION_IN, inSpi) + .setSpi(IpSecTransform.DIRECTION_OUT, outSpi) + .setEncryption( + IpSecTransform.DIRECTION_IN, + new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY)) + .setEncryption( + IpSecTransform.DIRECTION_OUT, + new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY)) + .setAuthentication( + IpSecTransform.DIRECTION_IN, + new IpSecAlgorithm( + IpSecAlgorithm.AUTH_HMAC_SHA256, AUTH_KEY, AUTH_KEY.length * 4)) + .setAuthentication( + IpSecTransform.DIRECTION_OUT, + new IpSecAlgorithm( + IpSecAlgorithm.AUTH_HMAC_SHA256, AUTH_KEY, AUTH_KEY.length * 4)) + .setIpv4Encapsulation(encapSocket, encapSocket.getPort()) + .buildTransportModeTransform(remoteAddr); + } + + private static int getPort(FileDescriptor sock) throws Exception { + return ((InetSocketAddress) Os.getsockname(sock)).getPort(); + } + + private static FileDescriptor getTestV4UdpSocket(InetAddress v4Addr) throws Exception { + FileDescriptor sock = + Os.socket(OsConstants.AF_INET, OsConstants.SOCK_DGRAM, OsConstants.IPPROTO_UDP); + + for (int i = 0; i < MAX_PORT_BIND_ATTEMPTS; i++) { + try { + int port = findUnusedPort(); + Os.bind(sock, v4Addr, port); + break; + } catch (ErrnoException e) { + // Someone claimed the port since we called findUnusedPort. + if (e.errno == OsConstants.EADDRINUSE) { + if (i == MAX_PORT_BIND_ATTEMPTS - 1) { + + fail("Failed " + MAX_PORT_BIND_ATTEMPTS + " attempts to bind to a port"); + } + continue; + } + throw e.rethrowAsIOException(); + } + } + return sock; + } }