diff options
Diffstat (limited to 'src/java/com/android/ike/ikev2/IkeSocket.java')
-rw-r--r-- | src/java/com/android/ike/ikev2/IkeSocket.java | 268 |
1 files changed, 268 insertions, 0 deletions
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..e235ab88 --- /dev/null +++ b/src/java/com/android/ike/ikev2/IkeSocket.java @@ -0,0 +1,268 @@ +/* + * 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.Log; +import android.util.LongSparseArray; + +import com.android.ike.ikev2.exceptions.IkeException; +import com.android.ike.ikev2.message.IkeHeader; +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 { + private static final String TAG = "IkeSocket"; + + // 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; + @VisibleForTesting static final byte[] NON_ESP_MARKER = new byte[NON_ESP_MARKER_LEN]; + + // Map from UdpEncapsulationSocket to IkeSocket instances. + private static Map<UdpEncapsulationSocket, IkeSocket> sFdToIkeSocketMap = new HashMap<>(); + + private static IPacketReceiver sPacketReceiver = new PacketReceiver(); + + // Package private map from locally generated IKE SPI to IkeSessionStateMachine instances. + @VisibleForTesting + final LongSparseArray<IkeSessionStateMachine> mSpiToIkeSession = + new LongSparseArray<>(); + // UdpEncapsulationSocket for sending and receving IKE packet. + private final UdpEncapsulationSocket mUdpEncapSocket; + + /** Package private */ + @VisibleForTesting + 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 */ + @VisibleForTesting + static final class PacketReceiver implements IPacketReceiver { + public void handlePacket( + byte[] recvbuf, LongSparseArray<IkeSessionStateMachine> spiToIkeSession) { + // TODO: b/129708574 Consider only logging the error some % of the time it happens, or + // only logging the error the first time it happens and then keep a count to prevent + // logspam. + + ByteBuffer byteBuffer = ByteBuffer.wrap(recvbuf); + + // Check the existence of the Non-ESP Marker. A received packet can be either an IKE + // packet starts with 4 zero-valued bytes Non-ESP Marker or an ESP packet starts with 4 + // bytes ESP SPI. ESP SPI value can never be zero. + byte[] espMarker = new byte[NON_ESP_MARKER_LEN]; + byteBuffer.get(espMarker); + if (!Arrays.equals(NON_ESP_MARKER, espMarker)) { + // Drop the received ESP packet. + Log.e(TAG, "Receive an ESP packet."); + return; + } + + try { + // Re-direct IKE packet to IkeSessionStateMachine according to the locally generated + // IKE SPI. + byte[] ikePacketBytes = new byte[byteBuffer.remaining()]; + byteBuffer.get(ikePacketBytes); + IkeHeader ikeHeader = new IkeHeader(ikePacketBytes); + + long localGeneratedSpi = + ikeHeader.fromIkeInitiator + ? ikeHeader.ikeResponderSpi + : ikeHeader.ikeInitiatorSpi; + + IkeSessionStateMachine ikeStateMachine = spiToIkeSession.get(localGeneratedSpi); + if (ikeStateMachine == null) { + Log.e(TAG, "Unrecognized IKE SPI."); + // TODO: Handle invalid IKE SPI error + } else { + ikeStateMachine.receiveIkePacket(ikeHeader, ikePacketBytes); + } + } catch (IkeException e) { + // Handle invalid IKE header + Log.e(TAG, "Can't parse malformed IKE packet header."); + } + } + } + + /** 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(NON_ESP_MARKER).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(); + } +} |