aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorevitayan <evitayan@google.com>2019-03-26 17:55:58 -0700
committerandroid-build-merger <android-build-merger@google.com>2019-03-26 17:55:58 -0700
commit4533d61d224adf826f90653c2d765895df89702f (patch)
tree5d40197e4e1761037b28dd6d576a2d725d20b252
parent3a74cae3d5b191b6a1ba66bd066d17279eaa59dc (diff)
parente85a7217b237cb16fc4e96995665299a39d6ed7f (diff)
downloadike-4533d61d224adf826f90653c2d765895df89702f.tar.gz
Create IkeSocket
am: e85a7217b2 Change-Id: I9a1ce807b2720390bf0e745f9cb78609e422f0ad
-rw-r--r--Android.mk2
-rw-r--r--src/java/com/android/ike/ikev2/IkeSocket.java219
-rw-r--r--tests/iketests/Android.mk3
-rw-r--r--tests/iketests/AndroidManifest.xml2
-rw-r--r--tests/iketests/src/java/com/android/ike/ikev2/IkeSocketTest.java283
5 files changed, 507 insertions, 2 deletions
diff --git a/Android.mk b/Android.mk
index 16642a29..14e9e9ee 100644
--- a/Android.mk
+++ b/Android.mk
@@ -20,7 +20,7 @@ LOCAL_AIDL_INCLUDES := $(LOCAL_PATH)/src/java
LOCAL_SRC_FILES := \
$(call all-java-files-under, src/java)
-LOCAL_JAVA_LIBRARIES := bouncycastle
+LOCAL_JAVA_LIBRARIES := bouncycastle NetworkStackBase
LOCAL_MODULE_TAGS := optional
LOCAL_MODULE := ike
diff --git a/src/java/com/android/ike/ikev2/IkeSocket.java b/src/java/com/android/ike/ikev2/IkeSocket.java
new file mode 100644
index 00000000..71daddd2
--- /dev/null
+++ b/src/java/com/android/ike/ikev2/IkeSocket.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+
+package com.android.ike.ikev2;
+
+import static android.system.OsConstants.F_SETFL;
+import static android.system.OsConstants.SOCK_DGRAM;
+import static android.system.OsConstants.SOCK_NONBLOCK;
+
+import android.net.IpSecManager.UdpEncapsulationSocket;
+import android.net.util.PacketReader;
+import android.os.Handler;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.LongSparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * IkeSocket sends and receives IKE packets via the user provided {@link UdpEncapsulationSocket}.
+ *
+ * <p>One UdpEncapsulationSocket instance can only be bound to one IkeSocket instance. IkeSocket
+ * maintains a static map to cache all bound UdpEncapsulationSockets and their IkeSocket instances.
+ * It returns the existing IkeSocket when it has been bound with user provided {@link
+ * UdpEncapsulationSocket}.
+ *
+ * <p>As a packet receiver, IkeSocket registers a file descriptor with a thread's Looper and handles
+ * read events (and errors). Users can expect a call life-cycle like the following:
+ *
+ * <pre>
+ * [1] when user gets a new initiated IkeSocket, start() is called and followed by createFd().
+ * [2] yield, waiting for a read event which will invoke handlePacket()
+ * [3] when user closes this IkeSocket, its reference count decreases. Then stop() is called when
+ * there is no reference of this instance.
+ * </pre>
+ *
+ * <p>IkeSocket is constructed and called only on a single IKE working thread by {@link
+ * IkeSessionStateMachine}. Since all {@link IkeSessionStateMachine}s run on the same working
+ * thread, there will not be concurrent modification problems.
+ */
+public final class IkeSocket extends PacketReader implements AutoCloseable {
+ // TODO: b/129358324 Consider supporting IKE exchange without UDP Encapsulation.
+ // UDP-encapsulated IKE packets MUST be sent to 4500.
+ @VisibleForTesting static final int IKE_SERVER_PORT = 4500;
+ // A Non-ESP marker helps the recipient to distinguish IKE packets from ESP packets.
+ @VisibleForTesting static final int NON_ESP_MARKER_LEN = 4;
+
+ // Package private map from UdpEncapsulationSocket to IkeSocket instances.
+ static Map<UdpEncapsulationSocket, IkeSocket> sFdToIkeSocketMap = new HashMap<>();
+
+ private static IPacketReceiver sPacketReceiver = new PacketReceiver();
+
+ // Map from locally generated IKE SPI to IkeSessionStateMachine instances.
+ private final LongSparseArray<IkeSessionStateMachine> mSpiToIkeSession =
+ new LongSparseArray<>();
+ // UdpEncapsulationSocket for sending and receving IKE packet.
+ private final UdpEncapsulationSocket mUdpEncapSocket;
+
+ /** Package private */
+ int mRefCount;
+
+ private IkeSocket(UdpEncapsulationSocket udpEncapSocket, Handler handler) {
+ super(handler);
+ mRefCount = 1;
+ mUdpEncapSocket = udpEncapSocket;
+ }
+
+ /**
+ * Get an IkeSocket instance.
+ *
+ * <p>Return the existing IkeSocket instance if it has been created for the input
+ * udpEncapSocket. Otherwise, create and return a new IkeSocket instance.
+ *
+ * @param udpEncapSocket user provided UdpEncapsulationSocket
+ * @return an IkSocket instance
+ */
+ public static IkeSocket getIkeSocket(UdpEncapsulationSocket udpEncapSocket)
+ throws ErrnoException {
+ FileDescriptor fd = udpEncapSocket.getFileDescriptor();
+ // All created IkeSocket has modified its FileDescriptor to non-blocking type for handling
+ // read events in a non-blocking way.
+ Os.fcntlInt(fd, F_SETFL, SOCK_DGRAM | SOCK_NONBLOCK);
+
+ if (sFdToIkeSocketMap.containsKey(udpEncapSocket)) {
+ IkeSocket ikeSocket = sFdToIkeSocketMap.get(udpEncapSocket);
+ ikeSocket.mRefCount++;
+ return ikeSocket;
+ } else {
+ IkeSocket ikeSocket = new IkeSocket(udpEncapSocket, new Handler());
+ // Create and register FileDescriptor for receiving IKE packet on current thread.
+ ikeSocket.start();
+
+ sFdToIkeSocketMap.put(udpEncapSocket, ikeSocket);
+ return ikeSocket;
+ }
+ }
+
+ /**
+ * Get FileDecriptor of mUdpEncapSocket.
+ *
+ * <p>PacketReader registers a listener for this file descriptor on the thread where IkeSocket
+ * is constructed. When there is a read event, this listener is invoked and then calls {@link
+ * handlePacket} to handle the received packet.
+ */
+ @Override
+ protected FileDescriptor createFd() {
+ return mUdpEncapSocket.getFileDescriptor();
+ }
+
+ /**
+ * IPacketReceiver provides a package private interface for handling received packet.
+ *
+ * <p>IPacketReceiver exists so that the interface is injectable for testing.
+ */
+ interface IPacketReceiver {
+ void handlePacket(byte[] recvbuf, LongSparseArray<IkeSessionStateMachine> spiToIkeSession);
+ }
+
+ /** Package private */
+ static final class PacketReceiver implements IPacketReceiver {
+ public void handlePacket(
+ byte[] recvbuf, LongSparseArray<IkeSessionStateMachine> spiToIkeSession) {
+ // TODO: Decode IKE header and demultiplex IKE packet
+ }
+ }
+
+ /** Package private */
+ @VisibleForTesting
+ static void setPacketReceiver(IPacketReceiver receiver) {
+ sPacketReceiver = receiver;
+ }
+
+ /**
+ * Handle received IKE packet. Invoked when there is a read event. Any desired copies of
+ * |recvbuf| should be made in here, as the underlying byte array is reused across all reads.
+ */
+ @Override
+ protected void handlePacket(byte[] recvbuf, int length) {
+ sPacketReceiver.handlePacket(Arrays.copyOfRange(recvbuf, 0, length), mSpiToIkeSession);
+ }
+
+ /**
+ * Send encoded IKE packet to destination address
+ *
+ * @param ikePacket encoded IKE packet
+ * @param serverAddress IP address of remote server
+ */
+ public void sendIkePacket(byte[] ikePacket, InetAddress serverAddress) {
+ try {
+ ByteBuffer buffer = ByteBuffer.allocate(NON_ESP_MARKER_LEN + ikePacket.length);
+
+ // Build outbound UDP Encapsulation packet body for sending IKE message.
+ buffer.put(new byte[NON_ESP_MARKER_LEN]).put(ikePacket);
+ buffer.rewind();
+
+ // Use unconnected UDP socket because one {@UdpEncapsulationSocket} may be shared by
+ // multiple IKE sessions that send messages to different destinations.
+ Os.sendto(
+ mUdpEncapSocket.getFileDescriptor(), buffer, 0, serverAddress, IKE_SERVER_PORT);
+ } catch (ErrnoException | IOException e) {
+ // TODO: Handle exception
+ }
+ }
+
+ /**
+ * Register new created IKE SA
+ *
+ * @param spi the locally generated IKE SPI
+ * @param ikeSession the IKE session this IKE SA belongs to
+ */
+ public void registerIke(long spi, IkeSessionStateMachine ikeSession) {
+ mSpiToIkeSession.put(spi, ikeSession);
+ }
+
+ /**
+ * Unregister a deleted IKE SA
+ *
+ * @param spi the locally generated IKE SPI
+ */
+ public void unregisterIke(long spi) {
+ mSpiToIkeSession.remove(spi);
+ }
+
+ /** Release reference of current IkeSocket when the IKE session is closed. */
+ public void releaseReference() {
+ mRefCount--;
+ if (mRefCount == 0) close();
+ }
+
+ /** Implement {@link AutoCloseable#close()} */
+ @Override
+ public void close() {
+ sFdToIkeSocketMap.remove(mUdpEncapSocket);
+ // PackeReader unregisters file descriptor on thread with which the Handler constructor
+ // argument is associated.
+ stop();
+ }
+}
diff --git a/tests/iketests/Android.mk b/tests/iketests/Android.mk
index a45703c1..0da49b47 100644
--- a/tests/iketests/Android.mk
+++ b/tests/iketests/Android.mk
@@ -29,6 +29,7 @@ LOCAL_JAVA_LIBRARIES := android.test.runner
LOCAL_STATIC_JAVA_LIBRARIES := ike \
androidx.test.rules \
frameworks-base-testutils \
- mockito-target-minus-junit4
+ mockito-target-minus-junit4 \
+ NetworkStackBase
include $(BUILD_PACKAGE) \ No newline at end of file
diff --git a/tests/iketests/AndroidManifest.xml b/tests/iketests/AndroidManifest.xml
index 80f54e8d..d69cbc8f 100644
--- a/tests/iketests/AndroidManifest.xml
+++ b/tests/iketests/AndroidManifest.xml
@@ -18,6 +18,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.ike.tests">
+ <uses-permission android:name="android.permission.INTERNET"/>
+
<application android:label="FrameworksIkeTests">
<uses-library android:name="android.test.runner" />
</application>
diff --git a/tests/iketests/src/java/com/android/ike/ikev2/IkeSocketTest.java b/tests/iketests/src/java/com/android/ike/ikev2/IkeSocketTest.java
new file mode 100644
index 00000000..2dd44096
--- /dev/null
+++ b/tests/iketests/src/java/com/android/ike/ikev2/IkeSocketTest.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+
+package com.android.ike.ikev2;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+import android.net.IpSecManager;
+import android.net.IpSecManager.UdpEncapsulationSocket;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+import android.util.Log;
+import android.util.LongSparseArray;
+
+import androidx.test.InstrumentationRegistry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.FileDescriptor;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public final class IkeSocketTest {
+ private static final int REMOTE_RECV_BUFF_SIZE = 2048;
+ private static final int TIMEOUT = 1000;
+
+ private static final String DATA_ONE = "one 1";
+ private static final String DATA_TWO = "two 2";
+
+ private static final String IPV4_LOOPBACK = "127.0.0.1";
+
+ private byte[] mDataOne;
+ private byte[] mDataTwo;
+
+ private UdpEncapsulationSocket mClientUdpEncapSocket;
+ private InetAddress mLocalAddress;
+ private FileDescriptor mDummyRemoteServerFd;
+
+ @Before
+ public void setUp() throws Exception {
+ Context context = InstrumentationRegistry.getContext();
+ IpSecManager ipSecManager = (IpSecManager) context.getSystemService(Context.IPSEC_SERVICE);
+ mClientUdpEncapSocket = ipSecManager.openUdpEncapsulationSocket();
+
+ mLocalAddress = InetAddress.getByName(IPV4_LOOPBACK);
+ mDummyRemoteServerFd = getBoundUdpSocket(mLocalAddress);
+
+ mDataOne = DATA_ONE.getBytes("UTF-8");
+ mDataTwo = DATA_TWO.getBytes("UTF-8");
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mClientUdpEncapSocket.close();
+ IkeSocket.setPacketReceiver(new IkeSocket.PacketReceiver());
+ Os.close(mDummyRemoteServerFd);
+ }
+
+ private static FileDescriptor getBoundUdpSocket(InetAddress address) throws Exception {
+ FileDescriptor sock =
+ Os.socket(OsConstants.AF_INET, OsConstants.SOCK_DGRAM, OsConstants.IPPROTO_UDP);
+ Os.bind(sock, address, IkeSocket.IKE_SERVER_PORT);
+ return sock;
+ }
+
+ @Test
+ public void testGetAndCloseIkeSocket() throws Exception {
+ if (Looper.myLooper() == null) Looper.myLooper().prepare();
+
+ IkeSocket ikeSocketOne = IkeSocket.getIkeSocket(mClientUdpEncapSocket);
+ assertEquals(1, ikeSocketOne.mRefCount);
+
+ IkeSocket ikeSocketTwo = IkeSocket.getIkeSocket(mClientUdpEncapSocket);
+ assertEquals(ikeSocketOne, ikeSocketTwo);
+ assertEquals(2, ikeSocketTwo.mRefCount);
+
+ ikeSocketOne.releaseReference();
+ assertEquals(1, ikeSocketOne.mRefCount);
+
+ ikeSocketTwo.releaseReference();
+ assertEquals(0, ikeSocketTwo.mRefCount);
+ }
+
+ @Test
+ public void testSendIkePacket() throws Exception {
+ if (Looper.myLooper() == null) Looper.myLooper().prepare();
+
+ // Send IKE packet
+ IkeSocket ikeSocket = IkeSocket.getIkeSocket(mClientUdpEncapSocket);
+ ikeSocket.sendIkePacket(mDataOne, mLocalAddress);
+
+ byte[] receivedData = receive(mDummyRemoteServerFd);
+
+ // Verify received data
+ ByteBuffer expectedBuffer =
+ ByteBuffer.allocate(IkeSocket.NON_ESP_MARKER_LEN + mDataOne.length);
+ expectedBuffer.put(new byte[IkeSocket.NON_ESP_MARKER_LEN]).put(mDataOne);
+
+ assertArrayEquals(expectedBuffer.array(), receivedData);
+
+ ikeSocket.releaseReference();
+ }
+
+ @Test
+ public void testReceiveIkePacket() throws Exception {
+ // Create working thread.
+ HandlerThread mIkeThread = new HandlerThread("IkeSocketTest");
+ mIkeThread.start();
+
+ // Create IkeSocket on working thread.
+ IkeSocketReceiver socketReceiver = new IkeSocketReceiver();
+ TestCountDownLatch createLatch = new TestCountDownLatch();
+ mIkeThread
+ .getThreadHandler()
+ .post(
+ () -> {
+ try {
+ socketReceiver.setIkeSocket(
+ IkeSocket.getIkeSocket(mClientUdpEncapSocket));
+ createLatch.countDown();
+ Log.d("IkeSocketTest", "IkeSocket created.");
+ } catch (ErrnoException e) {
+ Log.e("IkeSocketTest", "error encountered creating IkeSocket ", e);
+ }
+ });
+ createLatch.await();
+
+ IkeSocket ikeSocket = socketReceiver.getIkeSocket();
+ assertNotNull(ikeSocket);
+
+ // Configure IkeSocket
+ TestCountDownLatch receiveLatch = new TestCountDownLatch();
+ DummyPacketReceiver packetReceiver = new DummyPacketReceiver(receiveLatch);
+ IkeSocket.setPacketReceiver(packetReceiver);
+
+ // Send first packet.
+ sendToIkeSocket(mDummyRemoteServerFd, mDataOne, mLocalAddress);
+ receiveLatch.await();
+
+ assertEquals(1, ikeSocket.numPacketsReceived());
+ assertArrayEquals(mDataOne, packetReceiver.mReceivedData);
+
+ // Send second packet.
+ sendToIkeSocket(mDummyRemoteServerFd, mDataTwo, mLocalAddress);
+ receiveLatch.await();
+
+ assertEquals(2, ikeSocket.numPacketsReceived());
+ assertArrayEquals(mDataTwo, packetReceiver.mReceivedData);
+
+ // Close IkeSocket.
+ TestCountDownLatch closeLatch = new TestCountDownLatch();
+ ikeSocket
+ .getHandler()
+ .post(
+ () -> {
+ ikeSocket.releaseReference();
+ closeLatch.countDown();
+ });
+ closeLatch.await();
+
+ mIkeThread.quitSafely();
+ }
+
+ private byte[] receive(FileDescriptor mfd) throws Exception {
+ byte[] receiveBuffer = new byte[REMOTE_RECV_BUFF_SIZE];
+ AtomicInteger bytesRead = new AtomicInteger(-1);
+ Thread receiveThread =
+ new Thread(
+ () -> {
+ while (bytesRead.get() < 0) {
+ try {
+ bytesRead.set(
+ Os.recvfrom(
+ mDummyRemoteServerFd,
+ receiveBuffer,
+ 0,
+ REMOTE_RECV_BUFF_SIZE,
+ 0,
+ null));
+ } catch (Exception e) {
+ Log.e(
+ "IkeSocketTest",
+ "Error encountered reading from socket",
+ e);
+ }
+ }
+ Log.d(
+ "IkeSocketTest",
+ "Packet received with size of " + bytesRead.get());
+ });
+
+ receiveThread.start();
+ receiveThread.join(TIMEOUT);
+
+ return Arrays.copyOfRange(receiveBuffer, 0, bytesRead.get());
+ }
+
+ private void sendToIkeSocket(FileDescriptor fd, byte[] data, InetAddress destAddress)
+ throws Exception {
+ Os.sendto(fd, data, 0, data.length, 0, destAddress, mClientUdpEncapSocket.getPort());
+ }
+
+ private static class IkeSocketReceiver {
+ private IkeSocket mIkeSocket;
+
+ void setIkeSocket(IkeSocket ikeSocket) {
+ mIkeSocket = ikeSocket;
+ }
+
+ IkeSocket getIkeSocket() {
+ return mIkeSocket;
+ }
+ }
+
+ private static class DummyPacketReceiver implements IkeSocket.IPacketReceiver {
+ byte[] mReceivedData = null;
+ final TestCountDownLatch mLatch;
+
+ DummyPacketReceiver(TestCountDownLatch latch) {
+ mLatch = latch;
+ }
+
+ public void handlePacket(
+ byte[] revbuf, LongSparseArray<IkeSessionStateMachine> spiToIkeSession) {
+ mReceivedData = Arrays.copyOfRange(revbuf, 0, revbuf.length);
+ mLatch.countDown();
+ Log.d("IkeSocketTest", "Packet received");
+ }
+ }
+
+ private static class TestCountDownLatch {
+ private CountDownLatch mLatch;
+
+ TestCountDownLatch() {
+ reset();
+ }
+
+ private void reset() {
+ mLatch = new CountDownLatch(1);
+ }
+
+ void countDown() {
+ mLatch.countDown();
+ }
+
+ void await() {
+ try {
+ if (!mLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)) {
+ fail("Time out");
+ }
+ } catch (InterruptedException e) {
+ fail(e.toString());
+ }
+ reset();
+ }
+ }
+}