diff options
author | evitayan <evitayan@google.com> | 2019-03-26 17:55:58 -0700 |
---|---|---|
committer | android-build-merger <android-build-merger@google.com> | 2019-03-26 17:55:58 -0700 |
commit | 4533d61d224adf826f90653c2d765895df89702f (patch) | |
tree | 5d40197e4e1761037b28dd6d576a2d725d20b252 | |
parent | 3a74cae3d5b191b6a1ba66bd066d17279eaa59dc (diff) | |
parent | e85a7217b237cb16fc4e96995665299a39d6ed7f (diff) | |
download | ike-4533d61d224adf826f90653c2d765895df89702f.tar.gz |
Create IkeSocket
am: e85a7217b2
Change-Id: I9a1ce807b2720390bf0e745f9cb78609e422f0ad
-rw-r--r-- | Android.mk | 2 | ||||
-rw-r--r-- | src/java/com/android/ike/ikev2/IkeSocket.java | 219 | ||||
-rw-r--r-- | tests/iketests/Android.mk | 3 | ||||
-rw-r--r-- | tests/iketests/AndroidManifest.xml | 2 | ||||
-rw-r--r-- | tests/iketests/src/java/com/android/ike/ikev2/IkeSocketTest.java | 283 |
5 files changed, 507 insertions, 2 deletions
@@ -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(); + } + } +} |