blob: adc8f496717ae386f77ef073369f37cac9651776 [file] [log] [blame]
/*
* 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.internal.net.ipsec.ike;
import static android.net.ipsec.ike.IkeSessionConfiguration.EXTENSION_TYPE_FRAGMENTATION;
import static android.net.ipsec.ike.IkeSessionConfiguration.EXTENSION_TYPE_MOBIKE;
import static android.net.ipsec.ike.IkeSessionParams.ESP_ENCAP_TYPE_AUTO;
import static android.net.ipsec.ike.IkeSessionParams.ESP_ENCAP_TYPE_NONE;
import static android.net.ipsec.ike.IkeSessionParams.ESP_ENCAP_TYPE_UDP;
import static android.net.ipsec.ike.IkeSessionParams.ESP_IP_VERSION_AUTO;
import static android.net.ipsec.ike.IkeSessionParams.ESP_IP_VERSION_IPV4;
import static android.net.ipsec.ike.IkeSessionParams.ESP_IP_VERSION_IPV6;
import static android.net.ipsec.ike.IkeSessionParams.IKE_NATT_KEEPALIVE_DELAY_SEC_MAX;
import static android.net.ipsec.ike.IkeSessionParams.IKE_NATT_KEEPALIVE_DELAY_SEC_MIN;
import static android.net.ipsec.ike.IkeSessionParams.IKE_OPTION_EAP_ONLY_AUTH;
import static android.net.ipsec.ike.IkeSessionParams.IKE_OPTION_INITIAL_CONTACT;
import static android.net.ipsec.ike.IkeSessionParams.IKE_OPTION_MOBIKE;
import static android.net.ipsec.ike.IkeSessionParams.IKE_OPTION_REKEY_MOBILITY;
import static android.net.ipsec.ike.IkeSessionParams.NATT_KEEPALIVE_INTERVAL_AUTO;
import static android.net.ipsec.ike.exceptions.IkeException.wrapAsIkeException;
import static android.net.ipsec.ike.exceptions.IkeProtocolException.ERROR_TYPE_CHILD_SA_NOT_FOUND;
import static android.net.ipsec.ike.exceptions.IkeProtocolException.ERROR_TYPE_INVALID_SYNTAX;
import static android.net.ipsec.ike.exceptions.IkeProtocolException.ERROR_TYPE_NO_ADDITIONAL_SAS;
import static android.net.ipsec.ike.exceptions.IkeProtocolException.ERROR_TYPE_TEMPORARY_FAILURE;
import static android.net.ipsec.ike.exceptions.IkeProtocolException.ErrorType;
import static android.os.PowerManager.PARTIAL_WAKE_LOCK;
import static com.android.internal.net.ipsec.ike.message.IkeConfigPayload.CONFIG_TYPE_REPLY;
import static com.android.internal.net.ipsec.ike.message.IkeHeader.EXCHANGE_TYPE_INFORMATIONAL;
import static com.android.internal.net.ipsec.ike.message.IkeMessage.DECODE_STATUS_OK;
import static com.android.internal.net.ipsec.ike.message.IkeMessage.DECODE_STATUS_PARTIAL;
import static com.android.internal.net.ipsec.ike.message.IkeMessage.DECODE_STATUS_PROTECTED_ERROR;
import static com.android.internal.net.ipsec.ike.message.IkeMessage.DECODE_STATUS_UNPROTECTED_ERROR;
import static com.android.internal.net.ipsec.ike.message.IkeMessage.IKE_EXCHANGE_SUBTYPE_CREATE_CHILD;
import static com.android.internal.net.ipsec.ike.message.IkeMessage.IKE_EXCHANGE_SUBTYPE_DELETE_CHILD;
import static com.android.internal.net.ipsec.ike.message.IkeMessage.IKE_EXCHANGE_SUBTYPE_DELETE_IKE;
import static com.android.internal.net.ipsec.ike.message.IkeMessage.IKE_EXCHANGE_SUBTYPE_GENERIC_INFO;
import static com.android.internal.net.ipsec.ike.message.IkeMessage.IKE_EXCHANGE_SUBTYPE_IKE_AUTH;
import static com.android.internal.net.ipsec.ike.message.IkeMessage.IKE_EXCHANGE_SUBTYPE_IKE_INIT;
import static com.android.internal.net.ipsec.ike.message.IkeMessage.IKE_EXCHANGE_SUBTYPE_INVALID;
import static com.android.internal.net.ipsec.ike.message.IkeMessage.IKE_EXCHANGE_SUBTYPE_REKEY_CHILD;
import static com.android.internal.net.ipsec.ike.message.IkeMessage.IKE_EXCHANGE_SUBTYPE_REKEY_IKE;
import static com.android.internal.net.ipsec.ike.message.IkeNotifyPayload.NOTIFY_TYPE_COOKIE;
import static com.android.internal.net.ipsec.ike.message.IkeNotifyPayload.NOTIFY_TYPE_COOKIE2;
import static com.android.internal.net.ipsec.ike.message.IkeNotifyPayload.NOTIFY_TYPE_EAP_ONLY_AUTHENTICATION;
import static com.android.internal.net.ipsec.ike.message.IkeNotifyPayload.NOTIFY_TYPE_IKEV2_FRAGMENTATION_SUPPORTED;
import static com.android.internal.net.ipsec.ike.message.IkeNotifyPayload.NOTIFY_TYPE_INITIAL_CONTACT;
import static com.android.internal.net.ipsec.ike.message.IkeNotifyPayload.NOTIFY_TYPE_MOBIKE_SUPPORTED;
import static com.android.internal.net.ipsec.ike.message.IkeNotifyPayload.NOTIFY_TYPE_NAT_DETECTION_DESTINATION_IP;
import static com.android.internal.net.ipsec.ike.message.IkeNotifyPayload.NOTIFY_TYPE_NAT_DETECTION_SOURCE_IP;
import static com.android.internal.net.ipsec.ike.message.IkeNotifyPayload.NOTIFY_TYPE_REKEY_SA;
import static com.android.internal.net.ipsec.ike.message.IkeNotifyPayload.NOTIFY_TYPE_SIGNATURE_HASH_ALGORITHMS;
import static com.android.internal.net.ipsec.ike.message.IkeNotifyPayload.NOTIFY_TYPE_UPDATE_SA_ADDRESSES;
import static com.android.internal.net.ipsec.ike.message.IkePayload.PAYLOAD_TYPE_AUTH;
import static com.android.internal.net.ipsec.ike.message.IkePayload.PAYLOAD_TYPE_CP;
import static com.android.internal.net.ipsec.ike.message.IkePayload.PAYLOAD_TYPE_DELETE;
import static com.android.internal.net.ipsec.ike.message.IkePayload.PAYLOAD_TYPE_EAP;
import static com.android.internal.net.ipsec.ike.message.IkePayload.PAYLOAD_TYPE_NOTIFY;
import static com.android.internal.net.ipsec.ike.message.IkePayload.PAYLOAD_TYPE_SA;
import static com.android.internal.net.ipsec.ike.message.IkePayload.PAYLOAD_TYPE_TS_INITIATOR;
import static com.android.internal.net.ipsec.ike.message.IkePayload.PAYLOAD_TYPE_TS_RESPONDER;
import static com.android.internal.net.ipsec.ike.message.IkePayload.PAYLOAD_TYPE_VENDOR;
import static com.android.internal.net.ipsec.ike.net.IkeConnectionController.NAT_DETECTED;
import static com.android.internal.net.ipsec.ike.net.IkeConnectionController.NAT_TRAVERSAL_SUPPORT_NOT_CHECKED;
import static com.android.internal.net.ipsec.ike.net.IkeConnectionController.NAT_TRAVERSAL_UNSUPPORTED;
import static com.android.internal.net.ipsec.ike.utils.IkeAlarm.IkeAlarmConfig;
import static com.android.internal.net.ipsec.ike.utils.IkeAlarmReceiver.ACTION_DELETE_CHILD;
import static com.android.internal.net.ipsec.ike.utils.IkeAlarmReceiver.ACTION_DELETE_IKE;
import static com.android.internal.net.ipsec.ike.utils.IkeAlarmReceiver.ACTION_DPD;
import static com.android.internal.net.ipsec.ike.utils.IkeAlarmReceiver.ACTION_KEEPALIVE;
import static com.android.internal.net.ipsec.ike.utils.IkeAlarmReceiver.ACTION_REKEY_CHILD;
import static com.android.internal.net.ipsec.ike.utils.IkeAlarmReceiver.ACTION_REKEY_IKE;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.IpSecManager;
import android.net.IpSecManager.ResourceUnavailableException;
import android.net.IpSecManager.SpiUnavailableException;
import android.net.IpSecManager.UdpEncapsulationSocket;
import android.net.Network;
import android.net.TrafficStats;
import android.net.eap.EapInfo;
import android.net.eap.EapSessionConfig;
import android.net.ipsec.ike.ChildSessionCallback;
import android.net.ipsec.ike.ChildSessionParams;
import android.net.ipsec.ike.IkeManager;
import android.net.ipsec.ike.IkeSaProposal;
import android.net.ipsec.ike.IkeSessionCallback;
import android.net.ipsec.ike.IkeSessionConfiguration;
import android.net.ipsec.ike.IkeSessionConnectionInfo;
import android.net.ipsec.ike.IkeSessionParams;
import android.net.ipsec.ike.IkeSessionParams.IkeAuthConfig;
import android.net.ipsec.ike.IkeSessionParams.IkeAuthDigitalSignLocalConfig;
import android.net.ipsec.ike.IkeSessionParams.IkeAuthDigitalSignRemoteConfig;
import android.net.ipsec.ike.IkeSessionParams.IkeAuthPskConfig;
import android.net.ipsec.ike.TransportModeChildSessionParams;
import android.net.ipsec.ike.exceptions.AuthenticationFailedException;
import android.net.ipsec.ike.exceptions.IkeException;
import android.net.ipsec.ike.exceptions.IkeNetworkLostException;
import android.net.ipsec.ike.exceptions.IkeProtocolException;
import android.net.ipsec.ike.exceptions.InvalidKeException;
import android.net.ipsec.ike.exceptions.InvalidSyntaxException;
import android.net.ipsec.ike.exceptions.NoValidProposalChosenException;
import android.net.ipsec.ike.exceptions.UnsupportedCriticalPayloadException;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.PowerManager;
import android.os.Process;
import android.util.LongSparseArray;
import android.util.Pair;
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.net.eap.EapAuthenticator;
import com.android.internal.net.eap.EapResult;
import com.android.internal.net.eap.IEapCallback;
import com.android.internal.net.ipsec.ike.ChildSessionStateMachine.CreateChildSaHelper;
import com.android.internal.net.ipsec.ike.IkeLocalRequestScheduler.ChildLocalRequest;
import com.android.internal.net.ipsec.ike.IkeLocalRequestScheduler.IkeLocalRequest;
import com.android.internal.net.ipsec.ike.IkeLocalRequestScheduler.LocalRequest;
import com.android.internal.net.ipsec.ike.IkeLocalRequestScheduler.LocalRequestFactory;
import com.android.internal.net.ipsec.ike.SaRecord.IkeSaRecord;
import com.android.internal.net.ipsec.ike.SaRecord.SaLifetimeAlarmScheduler;
import com.android.internal.net.ipsec.ike.crypto.IkeCipher;
import com.android.internal.net.ipsec.ike.crypto.IkeMacIntegrity;
import com.android.internal.net.ipsec.ike.crypto.IkeMacPrf;
import com.android.internal.net.ipsec.ike.ike3gpp.Ike3gppExtensionExchange;
import com.android.internal.net.ipsec.ike.message.IkeAuthDigitalSignPayload;
import com.android.internal.net.ipsec.ike.message.IkeAuthPayload;
import com.android.internal.net.ipsec.ike.message.IkeAuthPskPayload;
import com.android.internal.net.ipsec.ike.message.IkeCertPayload;
import com.android.internal.net.ipsec.ike.message.IkeCertX509CertPayload;
import com.android.internal.net.ipsec.ike.message.IkeConfigPayload;
import com.android.internal.net.ipsec.ike.message.IkeConfigPayload.ConfigAttribute;
import com.android.internal.net.ipsec.ike.message.IkeDeletePayload;
import com.android.internal.net.ipsec.ike.message.IkeEapPayload;
import com.android.internal.net.ipsec.ike.message.IkeHeader;
import com.android.internal.net.ipsec.ike.message.IkeHeader.ExchangeType;
import com.android.internal.net.ipsec.ike.message.IkeIdPayload;
import com.android.internal.net.ipsec.ike.message.IkeInformationalPayload;
import com.android.internal.net.ipsec.ike.message.IkeKePayload;
import com.android.internal.net.ipsec.ike.message.IkeMessage;
import com.android.internal.net.ipsec.ike.message.IkeMessage.DecodeResult;
import com.android.internal.net.ipsec.ike.message.IkeMessage.DecodeResultError;
import com.android.internal.net.ipsec.ike.message.IkeMessage.DecodeResultOk;
import com.android.internal.net.ipsec.ike.message.IkeMessage.DecodeResultPartial;
import com.android.internal.net.ipsec.ike.message.IkeMessage.DecodeResultProtectedError;
import com.android.internal.net.ipsec.ike.message.IkeNoncePayload;
import com.android.internal.net.ipsec.ike.message.IkeNotifyPayload;
import com.android.internal.net.ipsec.ike.message.IkePayload;
import com.android.internal.net.ipsec.ike.message.IkeSaPayload;
import com.android.internal.net.ipsec.ike.message.IkeSaPayload.IkeProposal;
import com.android.internal.net.ipsec.ike.message.IkeVendorPayload;
import com.android.internal.net.ipsec.ike.net.IkeConnectionController;
import com.android.internal.net.ipsec.ike.shim.IIkeSessionStateMachineShim;
import com.android.internal.net.ipsec.ike.shim.ShimUtils;
import com.android.internal.net.ipsec.ike.utils.IkeAlarm;
import com.android.internal.net.ipsec.ike.utils.IkeAlarmReceiver;
import com.android.internal.net.ipsec.ike.utils.IkeMetrics;
import com.android.internal.net.ipsec.ike.utils.IkeSecurityParameterIndex;
import com.android.internal.net.ipsec.ike.utils.IkeSpiGenerator;
import com.android.internal.net.ipsec.ike.utils.IpSecSpiGenerator;
import com.android.internal.net.ipsec.ike.utils.LivenessAssister;
import com.android.internal.net.ipsec.ike.utils.RandomnessFactory;
import com.android.internal.net.ipsec.ike.utils.Retransmitter;
import com.android.internal.util.State;
import com.android.modules.utils.build.SdkLevel;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.security.GeneralSecurityException;
import java.security.cert.TrustAnchor;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* IkeSessionStateMachine tracks states and manages exchanges of this IKE session.
*
* <p>IkeSessionStateMachine has two types of states. One type are states where there is no ongoing
* procedure affecting IKE session (non-procedure state), including Initial, Idle and Receiving. All
* other states are "procedure" states which are named as follows:
*
* <pre>
* State Name = [Procedure Type] + [Exchange Initiator] + [Exchange Type].
* - An IKE procedure consists of one or two IKE exchanges:
* Procedure Type = {CreateIke | DeleteIke | Info | RekeyIke | SimulRekeyIke}.
* - Exchange Initiator indicates whether local or remote peer is the exchange initiator:
* Exchange Initiator = {Local | Remote}
* - Exchange type defines the function of this exchange. To make it more descriptive, we separate
* Delete Exchange from generic Informational Exchange:
* Exchange Type = {IkeInit | IkeAuth | Create | Delete | Info}
* </pre>
*/
public class IkeSessionStateMachine extends AbstractSessionStateMachine
implements IkeConnectionController.Callback,
IkeSocket.Callback,
IIkeSessionStateMachineShim,
LivenessAssister.IIkeMetricsCallback {
// Package private
static final String TAG = "IkeSessionStateMachine";
// "192.0.2.0" is selected from RFC5737, "IPv4 Address Blocks Reserved for Documentation"
private static final InetAddress FORCE_ENCAP_FAKE_LOCAL_ADDRESS_IPV4 =
new InetSocketAddress("192.0.2.0", 0).getAddress();
// "001:DB8::" is selected from RFC3849, "IPv6 Address Prefix Reserved for Documentation"
private static final InetAddress FORCE_ENCAP_FAKE_LOCAL_ADDRESS_IPV6 =
new InetSocketAddress("2001:DB8::", 0).getAddress();
@VisibleForTesting static final String BUSY_WAKE_LOCK_TAG = "mBusyWakeLock";
// TODO: b/140579254 Allow users to configure fragment size.
private static final HashMap<Context, Set<IkeSessionStateMachine>> sContextToIkeSmMap =
new HashMap<>();
/** Alarm receiver that will be shared by all IkeSessionStateMachine */
private static final IkeAlarmReceiver sIkeAlarmReceiver = new IkeAlarmReceiver();
/** Intent filter for all Intents that should be received by sIkeAlarmReceiver */
// The only read/write operation is in a static block which is thread safe.
private static final IntentFilter sIntentFilter = new IntentFilter();
static {
sIntentFilter.addAction(ACTION_DELETE_CHILD);
sIntentFilter.addAction(ACTION_DELETE_IKE);
sIntentFilter.addAction(ACTION_DPD);
sIntentFilter.addAction(ACTION_REKEY_CHILD);
sIntentFilter.addAction(ACTION_REKEY_IKE);
sIntentFilter.addAction(ACTION_KEEPALIVE);
}
private static final AtomicInteger sIkeSessionIdGenerator = new AtomicInteger();
// Bundle key for remote IKE SPI. Package private
@VisibleForTesting static final String BUNDLE_KEY_IKE_REMOTE_SPI = "BUNDLE_KEY_IKE_REMOTE_SPI";
// Bundle key for remote Child SPI. Package private
@VisibleForTesting
static final String BUNDLE_KEY_CHILD_REMOTE_SPI = "BUNDLE_KEY_CHILD_REMOTE_SPI";
// Default fragment size in bytes.
@VisibleForTesting static final int DEFAULT_FRAGMENT_SIZE = 1280;
// Close IKE Session when all responses during this time were TEMPORARY_FAILURE(s). This
// indicates that something has gone wrong, and we are out of sync.
@VisibleForTesting
static final long TEMP_FAILURE_RETRY_TIMEOUT_MS = TimeUnit.MINUTES.toMillis(5L);
/** Package private signals accessible for testing code. */
private static final int CMD_GENERAL_BASE = CMD_PRIVATE_BASE;
/** Receive encoded IKE packet on IkeSessionStateMachine. */
static final int CMD_RECEIVE_IKE_PACKET = CMD_GENERAL_BASE + 1;
/** Receive encoded IKE packet with unrecognized IKE SPI on IkeSessionStateMachine. */
static final int CMD_RECEIVE_PACKET_INVALID_IKE_SPI = CMD_GENERAL_BASE + 2;
/** Receive an remote request for a Child procedure. */
static final int CMD_RECEIVE_REQUEST_FOR_CHILD = CMD_GENERAL_BASE + 3;
/** Receive payloads from Child Session for building an outbound IKE message. */
static final int CMD_OUTBOUND_CHILD_PAYLOADS_READY = CMD_GENERAL_BASE + 4;
/** A Child Session has finished its procedure. */
static final int CMD_CHILD_PROCEDURE_FINISHED = CMD_GENERAL_BASE + 5;
/** Send request/response payloads to ChildSessionStateMachine for further processing. */
static final int CMD_HANDLE_FIRST_CHILD_NEGOTIATION = CMD_GENERAL_BASE + 6;
/** Receive a local request to execute from the scheduler */
static final int CMD_EXECUTE_LOCAL_REQ = CMD_GENERAL_BASE + 7;
/** Trigger a retransmission. */
public static final int CMD_RETRANSMIT = CMD_GENERAL_BASE + 8;
/** Send EAP request payloads to EapAuthenticator for further processing. */
static final int CMD_EAP_START_EAP_AUTH = CMD_GENERAL_BASE + 9;
/** Send the outbound IKE-wrapped EAP-Response message. */
static final int CMD_EAP_OUTBOUND_MSG_READY = CMD_GENERAL_BASE + 10;
/** Proxy to IkeSessionStateMachine handler to notify of errors */
static final int CMD_EAP_ERRORED = CMD_GENERAL_BASE + 11;
/** Proxy to IkeSessionStateMachine handler to notify of failures */
static final int CMD_EAP_FAILED = CMD_GENERAL_BASE + 12;
/** Proxy to IkeSessionStateMachine handler to notify of success, to continue to post-auth */
static final int CMD_EAP_FINISH_EAP_AUTH = CMD_GENERAL_BASE + 14;
/** Alarm goes off for a scheduled event, check {@link Message.arg2} for event type */
static final int CMD_ALARM_FIRED = CMD_GENERAL_BASE + 15;
/** Send keepalive packet */
static final int CMD_SEND_KEEPALIVE = CMD_GENERAL_BASE + 16;
/**
* Update the Session's underlying Network
* obj = NetworkParams : params containing network, IP version, encap type and keepalive delay.
**/
static final int CMD_SET_NETWORK = CMD_GENERAL_BASE + 17;
/**
* Proxy to IkeSessionStateMachine handler to notify of the IKE fatal error hit in a Child
* procedure
*/
static final int CMD_IKE_FATAL_ERROR_FROM_CHILD = CMD_GENERAL_BASE + 18;
/**
* Set the underpinned network
* obj = Network : the underpinned network
*/
static final int CMD_SET_UNDERPINNED_NETWORK = CMD_GENERAL_BASE + 19;
/** Initiate liveness check and sends callbacks. */
static final int CMD_REQUEST_LIVENESS_CHECK = CMD_GENERAL_BASE + 20;
/** Event for underlying network died with mobility */
static final int CMD_UNDERLYING_NETWORK_DIED_WITH_MOBILITY = CMD_GENERAL_BASE + 21;
/** Event for underlying network updated with mobility */
static final int CMD_UNDERLYING_NETWORK_UPDATED_WITH_MOBILITY = CMD_GENERAL_BASE + 22;
/** Force state machine to a target state for testing purposes. */
static final int CMD_FORCE_TRANSITION = CMD_GENERAL_BASE + 99;
static final int CMD_IKE_LOCAL_REQUEST_BASE = CMD_GENERAL_BASE + CMD_CATEGORY_SIZE;
static final int CMD_LOCAL_REQUEST_CREATE_IKE = CMD_IKE_LOCAL_REQUEST_BASE + 1;
static final int CMD_LOCAL_REQUEST_DELETE_IKE = CMD_IKE_LOCAL_REQUEST_BASE + 2;
static final int CMD_LOCAL_REQUEST_REKEY_IKE = CMD_IKE_LOCAL_REQUEST_BASE + 3;
static final int CMD_LOCAL_REQUEST_INFO = CMD_IKE_LOCAL_REQUEST_BASE + 4;
static final int CMD_LOCAL_REQUEST_DPD = CMD_IKE_LOCAL_REQUEST_BASE + 5;
static final int CMD_LOCAL_REQUEST_MOBIKE = CMD_IKE_LOCAL_REQUEST_BASE + 6;
static final int CMD_LOCAL_REQUEST_ON_DEMAND_DPD = CMD_IKE_LOCAL_REQUEST_BASE + 7;
private static final SparseArray<String> CMD_TO_STR;
static {
CMD_TO_STR = new SparseArray<>();
CMD_TO_STR.put(CMD_RECEIVE_IKE_PACKET, "Rcv packet");
CMD_TO_STR.put(CMD_RECEIVE_PACKET_INVALID_IKE_SPI, "Rcv invalid IKE SPI");
CMD_TO_STR.put(CMD_RECEIVE_REQUEST_FOR_CHILD, "Rcv Child request");
CMD_TO_STR.put(CMD_OUTBOUND_CHILD_PAYLOADS_READY, "Out child payloads ready");
CMD_TO_STR.put(CMD_CHILD_PROCEDURE_FINISHED, "Child procedure finished");
CMD_TO_STR.put(CMD_HANDLE_FIRST_CHILD_NEGOTIATION, "Negotiate first Child");
CMD_TO_STR.put(CMD_EXECUTE_LOCAL_REQ, "Execute local request");
CMD_TO_STR.put(CMD_RETRANSMIT, "Retransmit");
CMD_TO_STR.put(CMD_EAP_START_EAP_AUTH, "Start EAP");
CMD_TO_STR.put(CMD_EAP_OUTBOUND_MSG_READY, "EAP outbound msg ready");
CMD_TO_STR.put(CMD_EAP_ERRORED, "EAP errored");
CMD_TO_STR.put(CMD_EAP_FAILED, "EAP failed");
CMD_TO_STR.put(CMD_EAP_FINISH_EAP_AUTH, "Finish EAP");
CMD_TO_STR.put(CMD_ALARM_FIRED, "Alarm Fired");
CMD_TO_STR.put(CMD_SET_NETWORK, "Update underlying Network");
CMD_TO_STR.put(CMD_SET_UNDERPINNED_NETWORK, "Set underpinned Network");
CMD_TO_STR.put(CMD_REQUEST_LIVENESS_CHECK, "Request liveness check");
CMD_TO_STR.put(
CMD_UNDERLYING_NETWORK_DIED_WITH_MOBILITY, "UnderlyingNetwork died with mobility");
CMD_TO_STR.put(
CMD_UNDERLYING_NETWORK_UPDATED_WITH_MOBILITY,
"UnderlyingNetwork updated with mobility");
CMD_TO_STR.put(CMD_IKE_FATAL_ERROR_FROM_CHILD, "IKE fatal error from Child");
CMD_TO_STR.put(CMD_LOCAL_REQUEST_CREATE_IKE, "Create IKE");
CMD_TO_STR.put(CMD_LOCAL_REQUEST_DELETE_IKE, "Delete IKE");
CMD_TO_STR.put(CMD_LOCAL_REQUEST_REKEY_IKE, "Rekey IKE");
CMD_TO_STR.put(CMD_LOCAL_REQUEST_INFO, "Info");
CMD_TO_STR.put(CMD_LOCAL_REQUEST_DPD, "DPD");
CMD_TO_STR.put(CMD_LOCAL_REQUEST_MOBIKE, "Mobility event");
CMD_TO_STR.put(CMD_LOCAL_REQUEST_ON_DEMAND_DPD, "On-demand DPD");
}
/** Package */
@VisibleForTesting final IkeSessionParams mIkeSessionParams;
/** Map that stores all IkeSaRecords, keyed by locally generated IKE SPI. */
private final LongSparseArray<IkeSaRecord> mLocalSpiToIkeSaRecordMap;
/**
* Map that stores all ChildSessionStateMachines, keyed by remotely generated Child SPI for
* sending IPsec packet. Different SPIs may point to the same ChildSessionStateMachine if this
* Child Session is doing Rekey.
*/
private final SparseArray<ChildSessionStateMachine> mRemoteSpiToChildSessionMap;
private final int mIkeSessionId;
private final IpSecManager mIpSecManager;
private final AlarmManager mAlarmManager;
private final IkeLocalRequestScheduler mScheduler;
private final IkeSessionCallback mIkeSessionCallback;
private final TempFailureHandler mTempFailHandler;
private final Dependencies mDeps;
private final IkeConnectionController mIkeConnectionCtrl;
private final LocalRequestFactory mLocalRequestFactory;
/**
* mIkeSpiGenerator will be used by all IKE SA creations in this IKE Session to avoid SPI
* collision in test mode.
*/
private final IkeSpiGenerator mIkeSpiGenerator;
/**
* mIpSecSpiGenerator will be shared by all Child Sessions under this IKE Session to avoid SPI
* collision in test mode.
*/
private final IpSecSpiGenerator mIpSecSpiGenerator;
/** Ensures that the system does not go to sleep in the middle of an exchange. */
private final PowerManager.WakeLock mBusyWakeLock;
@VisibleForTesting
@GuardedBy("mChildCbToSessions")
final HashMap<ChildSessionCallback, ChildSessionStateMachine> mChildCbToSessions =
new HashMap<>();
/** Package private IkeSaProposal that represents the negotiated IKE SA proposal. */
@VisibleForTesting IkeSaProposal mSaProposal;
@VisibleForTesting IkeCipher mIkeCipher;
@VisibleForTesting IkeMacIntegrity mIkeIntegrity;
@VisibleForTesting IkeMacPrf mIkePrf;
@VisibleForTesting List<byte[]> mRemoteVendorIds = new ArrayList<>();
@VisibleForTesting List<Integer> mEnabledExtensions = new ArrayList<>();
/** Package */
@VisibleForTesting IkeSaRecord mCurrentIkeSaRecord;
/** Package */
@VisibleForTesting IkeSaRecord mLocalInitNewIkeSaRecord;
/** Package */
@VisibleForTesting IkeSaRecord mRemoteInitNewIkeSaRecord;
/** Package */
@VisibleForTesting IkeSaRecord mIkeSaRecordSurviving;
/** Package */
@VisibleForTesting IkeSaRecord mIkeSaRecordAwaitingLocalDel;
/** Package */
@VisibleForTesting IkeSaRecord mIkeSaRecordAwaitingRemoteDel;
private final Ike3gppExtensionExchange mIke3gppExtensionExchange;
/** Package */
@VisibleForTesting LivenessAssister mLivenessAssister;
/** Package */
@VisibleForTesting boolean mIsRetransmitSuspended;
// States
@VisibleForTesting
final KillIkeSessionParent mKillIkeSessionParent = new KillIkeSessionParent();
@VisibleForTesting
final Initial mInitial = new Initial();
@VisibleForTesting
final Idle mIdle = new Idle();
@VisibleForTesting
final ChildProcedureOngoing mChildProcedureOngoing = new ChildProcedureOngoing();
@VisibleForTesting
final Receiving mReceiving = new Receiving();
@VisibleForTesting
final CreateIkeLocalIkeInit mCreateIkeLocalIkeInit = new CreateIkeLocalIkeInit();
@VisibleForTesting
final CreateIkeLocalIkeAuth mCreateIkeLocalIkeAuth = new CreateIkeLocalIkeAuth();
@VisibleForTesting
final CreateIkeLocalIkeAuthInEap mCreateIkeLocalIkeAuthInEap = new CreateIkeLocalIkeAuthInEap();
@VisibleForTesting
final CreateIkeLocalIkeAuthPostEap mCreateIkeLocalIkeAuthPostEap =
new CreateIkeLocalIkeAuthPostEap();
@VisibleForTesting
final RekeyIkeLocalCreate mRekeyIkeLocalCreate = new RekeyIkeLocalCreate();
@VisibleForTesting
final SimulRekeyIkeLocalCreate mSimulRekeyIkeLocalCreate = new SimulRekeyIkeLocalCreate();
@VisibleForTesting
final SimulRekeyIkeLocalDeleteRemoteDelete mSimulRekeyIkeLocalDeleteRemoteDelete =
new SimulRekeyIkeLocalDeleteRemoteDelete();
@VisibleForTesting
final SimulRekeyIkeLocalDelete mSimulRekeyIkeLocalDelete = new SimulRekeyIkeLocalDelete();
@VisibleForTesting
final SimulRekeyIkeRemoteDelete mSimulRekeyIkeRemoteDelete = new SimulRekeyIkeRemoteDelete();
@VisibleForTesting
final RekeyIkeLocalDelete mRekeyIkeLocalDelete = new RekeyIkeLocalDelete();
@VisibleForTesting
final RekeyIkeRemoteDelete mRekeyIkeRemoteDelete = new RekeyIkeRemoteDelete();
@VisibleForTesting
final DeleteIkeLocalDelete mDeleteIkeLocalDelete = new DeleteIkeLocalDelete();
@VisibleForTesting
final DpdIkeLocalInfo mDpdIkeLocalInfo = new DpdIkeLocalInfo();
@VisibleForTesting
final DpdOnDemandIkeLocalInfo mDpdOnDemandIkeLocalInfo = new DpdOnDemandIkeLocalInfo();
@VisibleForTesting final MobikeLocalInfo mMobikeLocalInfo = new MobikeLocalInfo();
/** Constructor for testing. */
@VisibleForTesting
public IkeSessionStateMachine(
Looper looper,
Context context,
IpSecManager ipSecManager,
ConnectivityManager connectMgr,
IkeSessionParams ikeParams,
ChildSessionParams firstChildParams,
Executor userCbExecutor,
IkeSessionCallback ikeSessionCallback,
ChildSessionCallback firstChildSessionCallback,
Dependencies deps) {
super(
TAG,
deps.newIkeContext(looper, context, ikeParams.getConfiguredNetwork()),
userCbExecutor);
if (ikeParams.hasIkeOption(IkeSessionParams.IKE_OPTION_MOBIKE)
|| ikeParams.hasIkeOption(IkeSessionParams.IKE_OPTION_REKEY_MOBILITY)) {
if (firstChildParams instanceof TransportModeChildSessionParams) {
throw new IllegalArgumentException(
"Transport Mode SAs not supported when MOBIKE is enabled");
} else if (!SdkLevel.isAtLeastS()) {
throw new IllegalStateException("MOBIKE only supported for S+");
}
}
// TODO: Statically store the ikeSessionCallback to prevent user from providing the
// same callback instance in the future
PowerManager pm = context.getSystemService(PowerManager.class);
mBusyWakeLock = pm.newWakeLock(PARTIAL_WAKE_LOCK, TAG + BUSY_WAKE_LOCK_TAG);
mBusyWakeLock.setReferenceCounted(false);
mIkeSessionId = sIkeSessionIdGenerator.getAndIncrement();
mIkeSessionParams = ikeParams;
mTempFailHandler = new TempFailureHandler(looper);
// There are at most three IkeSaRecords co-existing during simultaneous rekeying.
mLocalSpiToIkeSaRecordMap = new LongSparseArray<>(3);
mRemoteSpiToChildSessionMap = new SparseArray<>();
mIpSecManager = ipSecManager;
mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
mDeps = deps;
mLocalRequestFactory = mDeps.newLocalRequestFactory();
mIkeConnectionCtrl =
mDeps.newIkeConnectionController(
mIkeContext,
new IkeConnectionController.Config(
mIkeSessionParams,
mIkeSessionId,
CMD_ALARM_FIRED,
CMD_SEND_KEEPALIVE,
this));
mIkeSpiGenerator = new IkeSpiGenerator(mIkeContext.getRandomnessFactory());
mIpSecSpiGenerator =
new IpSecSpiGenerator(mIpSecManager, mIkeContext.getRandomnessFactory());
mIkeSessionCallback = ikeSessionCallback;
registerChildSessionCallback(firstChildParams, firstChildSessionCallback, true);
mIke3gppExtensionExchange =
new Ike3gppExtensionExchange(
mIkeSessionParams.getIke3gppExtension(), mUserCbExecutor);
mLivenessAssister = new LivenessAssister(mIkeSessionCallback, mUserCbExecutor, this);
// CHECKSTYLE:OFF IndentationCheck
addState(mKillIkeSessionParent);
addState(mInitial, mKillIkeSessionParent);
addState(mCreateIkeLocalIkeInit, mKillIkeSessionParent);
addState(mCreateIkeLocalIkeAuth, mKillIkeSessionParent);
addState(mCreateIkeLocalIkeAuthInEap, mKillIkeSessionParent);
addState(mCreateIkeLocalIkeAuthPostEap, mKillIkeSessionParent);
addState(mIdle, mKillIkeSessionParent);
addState(mChildProcedureOngoing, mKillIkeSessionParent);
addState(mReceiving, mKillIkeSessionParent);
addState(mRekeyIkeLocalCreate, mKillIkeSessionParent);
addState(mSimulRekeyIkeLocalCreate, mRekeyIkeLocalCreate);
addState(mSimulRekeyIkeLocalDeleteRemoteDelete, mKillIkeSessionParent);
addState(mSimulRekeyIkeLocalDelete, mSimulRekeyIkeLocalDeleteRemoteDelete);
addState(mSimulRekeyIkeRemoteDelete, mSimulRekeyIkeLocalDeleteRemoteDelete);
addState(mRekeyIkeLocalDelete, mKillIkeSessionParent);
addState(mRekeyIkeRemoteDelete, mKillIkeSessionParent);
addState(mDeleteIkeLocalDelete, mKillIkeSessionParent);
addState(mDpdIkeLocalInfo, mKillIkeSessionParent);
addState(mDpdOnDemandIkeLocalInfo, mKillIkeSessionParent);
addState(mMobikeLocalInfo, mKillIkeSessionParent);
// CHECKSTYLE:ON IndentationCheck
// Peer-selected DH group to use. Defaults to first proposed DH group in first SA proposal.
int peerSelectedDhGroup =
mIkeSessionParams.getSaProposals().get(0).getDhGroupTransforms()[0].id;
mInitial.setIkeSetupData(
new InitialSetupData(
firstChildParams, firstChildSessionCallback, peerSelectedDhGroup));
setInitialState(mInitial);
// TODO: Find a way to make it safe to release WakeLock when #onNewProcedureReady is called
mScheduler =
new IkeLocalRequestScheduler(
localReq -> {
sendMessageAtFrontOfQueue(CMD_EXECUTE_LOCAL_REQ, localReq);
},
mIkeContext.getContext());
mBusyWakeLock.acquire();
start();
}
/** Construct an instance of IkeSessionStateMachine. */
public IkeSessionStateMachine(
Looper looper,
Context context,
IpSecManager ipSecManager,
IkeSessionParams ikeParams,
ChildSessionParams firstChildParams,
Executor userCbExecutor,
IkeSessionCallback ikeSessionCallback,
ChildSessionCallback firstChildSessionCallback) {
this(
looper,
context,
ipSecManager,
context.getSystemService(ConnectivityManager.class),
ikeParams,
firstChildParams,
userCbExecutor,
ikeSessionCallback,
firstChildSessionCallback,
new Dependencies());
}
/**
* InitialSetupData contains original caller configurations that will be used in IKE setup.
*
* <p>This class will be instantiated in IkeSessionStateMachine constructor, and then passed to
* Initial state and eventually CreateIkeLocalIkeInit state
*/
@VisibleForTesting
static class InitialSetupData {
public final ChildSessionParams firstChildSessionParams;
public final ChildSessionCallback firstChildCallback;
/** Peer-selected DH group to use. */
public final int peerSelectedDhGroup;
InitialSetupData(
ChildSessionParams firstChildSessionParams,
ChildSessionCallback firstChildCallback,
int peerSelectedDhGroup) {
this.firstChildSessionParams = firstChildSessionParams;
this.firstChildCallback = firstChildCallback;
this.peerSelectedDhGroup = peerSelectedDhGroup;
}
InitialSetupData(InitialSetupData initialSetupData) {
this(
initialSetupData.firstChildSessionParams,
initialSetupData.firstChildCallback,
initialSetupData.peerSelectedDhGroup);
}
}
/**
* IkeInitData contains caller configurations and IKE INIT exchange results that will be used in
* IKE AUTH.
*
* <p>This class will be instantiated in CreateIkeLocalIkeInit state, and then passed to
* CreateIkeLocalIkeAuth state for IKE AUTH exchange(s).
*/
@VisibleForTesting
static class IkeInitData extends InitialSetupData {
public final byte[] ikeInitRequestBytes;
public final byte[] ikeInitResponseBytes;
public final IkeNoncePayload ikeInitNoncePayload;
public final IkeNoncePayload ikeRespNoncePayload;
/** Set of peer-supported Signature Hash Algorithms. Optionally set in IKE INIT. */
public final Set<Short> peerSignatureHashAlgorithms = new HashSet<>();
IkeInitData(
InitialSetupData initialSetupData,
byte[] ikeInitRequestBytes,
byte[] ikeInitResponseBytes,
IkeNoncePayload ikeInitNoncePayload,
IkeNoncePayload ikeRespNoncePayload,
Set<Short> peerSignatureHashAlgorithms) {
super(initialSetupData);
this.ikeInitRequestBytes = ikeInitRequestBytes;
this.ikeInitResponseBytes = ikeInitResponseBytes;
this.ikeInitNoncePayload = ikeInitNoncePayload;
this.ikeRespNoncePayload = ikeRespNoncePayload;
this.peerSignatureHashAlgorithms.addAll(peerSignatureHashAlgorithms);
}
IkeInitData(IkeInitData ikeInitData) {
this(
new InitialSetupData(
ikeInitData.firstChildSessionParams,
ikeInitData.firstChildCallback,
ikeInitData.peerSelectedDhGroup),
ikeInitData.ikeInitRequestBytes,
ikeInitData.ikeInitResponseBytes,
ikeInitData.ikeInitNoncePayload,
ikeInitData.ikeRespNoncePayload,
ikeInitData.peerSignatureHashAlgorithms);
}
}
/**
* IkeAuthData contains caller configuration and results of IKE INIT and first IKE AUTH exchange
* that will be used in the remaining IKE AUTH exchanges.
*
* <p>This class will be instantiated in CreateIkeLocalIkeAuth state, ane then passed to the
* later IKE AUTH states if the authentication requires multiple IKE exchanges.
*/
@VisibleForTesting
static class IkeAuthData extends IkeInitData {
public final IkeIdPayload initIdPayload;
public final IkeIdPayload respIdPayload;
public final List<IkePayload> firstChildReqList;
IkeAuthData(
IkeInitData ikeInitData,
IkeIdPayload initIdPayload,
IkeIdPayload respIdPayload,
List<IkePayload> firstChildReqList) {
super(ikeInitData);
this.initIdPayload = initIdPayload;
this.respIdPayload = respIdPayload;
this.firstChildReqList = new ArrayList<IkePayload>();
this.firstChildReqList.addAll(firstChildReqList);
}
}
/** External dependencies, for injection in tests */
@VisibleForTesting
public static class Dependencies {
/** Builds and returns a new IkeContext */
public IkeContext newIkeContext(Looper looper, Context context, Network network) {
return new IkeContext(looper, context, new RandomnessFactory(context, network));
}
/**
* Builds and returns a new EapAuthenticator
*
* @param ikeContext context of an IKE Session
* @param cb IEapCallback for callbacks to the client
* @param eapSessionConfig EAP session configuration
*/
public EapAuthenticator newEapAuthenticator(
IkeContext ikeContext, IEapCallback cb, EapSessionConfig eapSessionConfig) {
return new EapAuthenticator(ikeContext, cb, eapSessionConfig);
}
/** Builds and starts a new ChildSessionStateMachine */
public ChildSessionStateMachine newChildSessionStateMachine(
IkeContext ikeContext,
ChildSessionStateMachine.Config childSessionSmConfig,
ChildSessionCallback userCallbacks,
ChildSessionStateMachine.IChildSessionSmCallback childSmCallback) {
ChildSessionStateMachine childSession =
new ChildSessionStateMachine(
ikeContext, childSessionSmConfig, userCallbacks, childSmCallback);
childSession.start();
return childSession;
}
/** Builds and returns a new IkeConnectionController */
public IkeConnectionController newIkeConnectionController(
IkeContext ikeContext, IkeConnectionController.Config config) {
return new IkeConnectionController(ikeContext, config);
}
/** Gets a LocalRequestFactory */
public LocalRequestFactory newLocalRequestFactory() {
return new LocalRequestFactory();
}
/**
* Creates an alarm to be delivered precisely at the stated time, even when the system is in
* low-power idle (a.k.a. doze) modes.
*/
public IkeAlarm newExactAndAllowWhileIdleAlarm(IkeAlarmConfig alarmConfig) {
return IkeAlarm.newExactAndAllowWhileIdleAlarm(alarmConfig);
}
}
private boolean hasChildSessionCallback(ChildSessionCallback callback) {
synchronized (mChildCbToSessions) {
return mChildCbToSessions.containsKey(callback);
}
}
/**
* Synchronously builds and registers a child session.
*
* <p>Setup of the child state machines MUST be done in two stages to ensure that if an external
* caller calls openChildSession and then calls closeChildSession before the state machine has
* gotten a chance to negotiate the sessions, a valid callback mapping exists (and does not
* throw an exception that the callback was not found).
*
* <p>In the edge case where a child creation fails, and deletes itself, all pending requests
* will no longer find the session in the map. Assume it has errored/failed, and skip/ignore.
* This is safe, as closeChildSession() (previously) validated that the callback was registered.
*/
@VisibleForTesting
void registerChildSessionCallback(
ChildSessionParams childParams, ChildSessionCallback callbacks, boolean isFirstChild) {
synchronized (mChildCbToSessions) {
if (!isFirstChild && getCurrentState() == null) {
throw new IllegalStateException(
"Request rejected because IKE Session is being closed. ");
}
mChildCbToSessions.put(
callbacks,
mDeps.newChildSessionStateMachine(
mIkeContext,
new ChildSessionStateMachine.Config(
mIkeSessionId,
getHandler(),
childParams,
(IpSecManager)
mIkeContext
.getContext()
.getSystemService(Context.IPSEC_SERVICE),
mIpSecSpiGenerator,
mUserCbExecutor),
callbacks,
new ChildSessionSmCallback()));
}
}
/** Initiates IKE setup procedure. */
public void openSession() {
sendMessage(
CMD_LOCAL_REQUEST_CREATE_IKE,
mLocalRequestFactory.getIkeLocalRequest(CMD_LOCAL_REQUEST_CREATE_IKE));
}
/** Schedules a Create Child procedure. */
public void openChildSession(
ChildSessionParams childSessionParams, ChildSessionCallback childSessionCallback) {
if (childSessionCallback == null) {
throw new IllegalArgumentException("Child Session Callback must be provided");
}
if (hasChildSessionCallback(childSessionCallback)) {
throw new IllegalArgumentException("Child Session Callback handle already registered");
}
if (mIkeSessionParams.hasIkeOption(IKE_OPTION_MOBIKE)
&& childSessionParams instanceof TransportModeChildSessionParams) {
throw new IllegalArgumentException(
"Transport Mode SAs not supported when MOBIKE is enabled");
}
registerChildSessionCallback(
childSessionParams, childSessionCallback, false /*isFirstChild*/);
sendMessage(
CMD_LOCAL_REQUEST_CREATE_CHILD,
mLocalRequestFactory.getChildLocalRequest(
CMD_LOCAL_REQUEST_CREATE_CHILD, childSessionCallback, childSessionParams));
}
/** Schedules a Delete Child procedure. */
public void closeChildSession(ChildSessionCallback childSessionCallback) {
if (childSessionCallback == null) {
throw new IllegalArgumentException("Child Session Callback must be provided");
}
if (!hasChildSessionCallback(childSessionCallback)) {
throw new IllegalArgumentException("Child Session Callback handle not registered");
}
sendMessage(
CMD_LOCAL_REQUEST_DELETE_CHILD,
mLocalRequestFactory.getChildLocalRequest(
CMD_LOCAL_REQUEST_DELETE_CHILD, childSessionCallback, null));
}
/** Initiates Delete IKE procedure. */
public void closeSession() {
sendMessage(
CMD_LOCAL_REQUEST_DELETE_IKE,
mLocalRequestFactory.getIkeLocalRequest(CMD_LOCAL_REQUEST_DELETE_IKE));
}
/** Update the IkeSessionStateMachine to use the specified Network. */
public void setNetwork(
Network network,
@IkeSessionParams.EspIpVersion int ipVersion,
@IkeSessionParams.EspEncapType int encapType,
int keepaliveDelaySeconds) {
if (network == null) {
throw new IllegalArgumentException("network must not be null");
}
if (ipVersion != ESP_IP_VERSION_AUTO
&& ipVersion != ESP_IP_VERSION_IPV4
&& ipVersion != ESP_IP_VERSION_IPV6) {
throw new IllegalArgumentException("Invalid IP version: " + ipVersion);
}
if (encapType != ESP_ENCAP_TYPE_AUTO
&& encapType != ESP_ENCAP_TYPE_NONE
&& encapType != ESP_ENCAP_TYPE_UDP) {
throw new IllegalArgumentException("Invalid encap type: " + encapType);
}
if (keepaliveDelaySeconds != NATT_KEEPALIVE_INTERVAL_AUTO
&& (keepaliveDelaySeconds < IKE_NATT_KEEPALIVE_DELAY_SEC_MIN
|| keepaliveDelaySeconds > IKE_NATT_KEEPALIVE_DELAY_SEC_MAX)) {
throw new IllegalArgumentException("Invalid NATT keepalive delay value");
}
if (!mIkeSessionParams.hasIkeOption(IKE_OPTION_MOBIKE)
&& !mIkeSessionParams.hasIkeOption(IKE_OPTION_REKEY_MOBILITY)) {
throw new IllegalStateException(
"This IKE Session is not able to handle network or address changes");
}
if (mIkeSessionParams.getConfiguredNetwork() == null) {
throw new IllegalStateException(
"setNetwork() requires this IkeSession to be configured to use caller-specified"
+ " network instead of default network");
}
sendMessage(CMD_SET_NETWORK,
new NetworkParams(network, ipVersion, encapType, keepaliveDelaySeconds));
}
/**
* Update the IkeSessionMachine to know that it underpins the specified Network.
*
* In particular, this is used to tell the system to stop keepalives when there are no
* open connections on the underpinned network, if automatic on/off keepalives are turned on.
*/
public void setUnderpinnedNetwork(@NonNull Network underpinnedNetwork) {
Objects.requireNonNull(underpinnedNetwork);
sendMessage(CMD_SET_UNDERPINNED_NETWORK, underpinnedNetwork);
}
/**
* Schedules checking liveness procedure. The on-demand DPD may be triggered or check with
* existing any IKE message.
*/
public void requestLivenessCheck() {
sendMessage(CMD_REQUEST_LIVENESS_CHECK, LivenessAssister.REQ_TYPE_INITIAL);
}
private void scheduleRetry(LocalRequest localRequest) {
sendMessageDelayed(localRequest.procedureType, localRequest, RETRY_INTERVAL_MS);
}
private boolean needEnableForceUdpEncap() {
// When IKE library uses IPv4 and needs to do NAT detection, it needs to enforce UDP
// encapsulation to prevent the server from sending non-UDP-encap packets.
//
// NOTE: Although the IKE spec requires implementations to handle both UDP-encap and
// non-UDP-encap ESP packets when both the IKE client and server support NAT-T, due to
// kernel restrictions, the Android IPsec stack is unable to allow receiving two types of
// packets with a single SA. As a result, before kernel issues (b/210164853) are resolved,
// the IKE library MUST enforce UDP Encap to ensure that the server only sends UDP-encap
// packets in order to avoid dropping packets.
return (mIkeConnectionCtrl.getRemoteAddress() instanceof Inet4Address);
}
private static class NetworkParams {
public final Network network;
public final int ipVersion;
public final int encapType;
public final int keepaliveDelaySeconds;
NetworkParams(Network network, int ipVersion, int encapType,
int keepaliveDelaySeconds) {
this.network = network;
this.ipVersion = ipVersion;
this.encapType = encapType;
this.keepaliveDelaySeconds = keepaliveDelaySeconds;
}
}
// TODO: Support initiating Delete IKE exchange when IKE SA expires
// TODO: Add interfaces to initiate IKE exchanges.
/**
* This class is for handling temporary failure.
*
* <p>Receiving a TEMPORARY_FAILURE is caused by a temporary condition. IKE Session should be
* closed if it continues to receive this error after several minutes.
*/
@VisibleForTesting
class TempFailureHandler extends Handler {
private static final int TEMP_FAILURE_RETRY_TIMEOUT = 1;
private boolean mTempFailureReceived = false;
TempFailureHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
if (msg.what == TEMP_FAILURE_RETRY_TIMEOUT) {
IOException error =
new IOException(
"Kept receiving TEMPORARY_FAILURE error. State information is out"
+ " of sync.");
executeUserCallback(
() -> {
mIkeSessionCallback.onClosedWithException(wrapAsIkeException(error));
});
loge("Fatal error", error);
closeAllSaRecords(false /*expectSaClosed*/);
recordMetricsEvent_sessionTerminated(wrapAsIkeException(error));
quitSessionNow();
} else {
logWtf("Unknown message.what: " + msg.what);
}
}
/**
* Schedule temporary failure timeout.
*
* <p>Caller of this method is responsible for scheduling retry of the rejected request.
*/
public void handleTempFailure() {
logd("TempFailureHandler: Receive TEMPORARY FAILURE");
if (!mTempFailureReceived) {
sendEmptyMessageDelayed(TEMP_FAILURE_RETRY_TIMEOUT, TEMP_FAILURE_RETRY_TIMEOUT_MS);
mTempFailureReceived = true;
}
}
/** Stop tracking temporary condition when request was not rejected by TEMPORARY_FAILURE. */
public void reset() {
logd("TempFailureHandler: Reset Temporary failure retry timeout");
removeMessages(TEMP_FAILURE_RETRY_TIMEOUT);
mTempFailureReceived = false;
}
}
// TODO: Add methods for building and validating general Informational packet.
@VisibleForTesting
void addIkeSaRecord(IkeSaRecord record) {
mLocalSpiToIkeSaRecordMap.put(record.getLocalSpi(), record);
// In IKE_INIT exchange, local SPI was registered with this IkeSessionStateMachine before
// IkeSaRecord is created. Calling this method at the end of exchange will double-register
// the SPI but it is safe because the key and value are not changed.
mIkeConnectionCtrl.registerIkeSaRecord(record);
}
@VisibleForTesting
void removeIkeSaRecord(IkeSaRecord record) {
mIkeConnectionCtrl.unregisterIkeSaRecord(record);
mLocalSpiToIkeSaRecordMap.remove(record.getLocalSpi());
}
/**
* ReceivedIkePacket is a package private data container consists of decoded IkeHeader and
* encoded IKE packet in a byte array.
*/
static class ReceivedIkePacket {
/** Decoded IKE header */
public final IkeHeader ikeHeader;
/** Entire encoded IKE message including IKE header */
public final byte[] ikePacketBytes;
ReceivedIkePacket(IkeHeader ikeHeader, byte[] ikePacketBytes) {
this.ikeHeader = ikeHeader;
this.ikePacketBytes = ikePacketBytes;
}
}
/** Class to group parameters for negotiating the first Child SA. */
private static class FirstChildNegotiationData {
public final ChildSessionParams childSessionParams;
public final ChildSessionCallback childSessionCallback;
public final List<IkePayload> reqPayloads;
public final List<IkePayload> respPayloads;
FirstChildNegotiationData(
ChildSessionParams childSessionParams,
ChildSessionCallback childSessionCallback,
List<IkePayload> reqPayloads,
List<IkePayload> respPayloads) {
this.childSessionParams = childSessionParams;
this.childSessionCallback = childSessionCallback;
this.reqPayloads = reqPayloads;
this.respPayloads = respPayloads;
}
}
/** Class to group parameters for notifying the IKE fatal error. */
private static class IkeFatalErrorFromChild {
public final Exception exception;
IkeFatalErrorFromChild(Exception exception) {
this.exception = exception;
}
}
/** Class to group parameters for building an outbound message for ChildSessions. */
private static class ChildOutboundData {
@ExchangeType public final int exchangeType;
public final boolean isResp;
public final List<IkePayload> payloadList;
public final ChildSessionStateMachine childSession;
ChildOutboundData(
@ExchangeType int exchangeType,
boolean isResp,
List<IkePayload> payloadList,
ChildSessionStateMachine childSession) {
this.exchangeType = exchangeType;
this.isResp = isResp;
this.payloadList = payloadList;
this.childSession = childSession;
}
}
/** Callback for ChildSessionStateMachine to notify IkeSessionStateMachine. */
@VisibleForTesting
class ChildSessionSmCallback implements ChildSessionStateMachine.IChildSessionSmCallback {
@Override
public void onChildSaCreated(int remoteSpi, ChildSessionStateMachine childSession) {
mRemoteSpiToChildSessionMap.put(remoteSpi, childSession);
}
@Override
public void onChildSaDeleted(int remoteSpi) {
mRemoteSpiToChildSessionMap.remove(remoteSpi);
}
@Override
public void scheduleRetryLocalRequest(ChildLocalRequest childRequest) {
scheduleRetry(childRequest);
}
@Override
public void onOutboundPayloadsReady(
@ExchangeType int exchangeType,
boolean isResp,
List<IkePayload> payloadList,
ChildSessionStateMachine childSession) {
sendMessage(
CMD_OUTBOUND_CHILD_PAYLOADS_READY,
new ChildOutboundData(exchangeType, isResp, payloadList, childSession));
}
@Override
public void onProcedureFinished(ChildSessionStateMachine childSession) {
if (getHandler() == null) {
// If the state machine has quit (because IKE Session is being closed), do not send
// any message.
return;
}
sendMessage(CMD_CHILD_PROCEDURE_FINISHED, childSession);
}
@Override
public void onChildSessionClosed(ChildSessionCallback userCallbacks) {
synchronized (mChildCbToSessions) {
mChildCbToSessions.remove(userCallbacks);
}
}
@Override
public void onFatalIkeSessionError(Exception exception) {
sendMessage(CMD_IKE_FATAL_ERROR_FROM_CHILD, new IkeFatalErrorFromChild(exception));
}
}
/** Top level state for handling uncaught exceptions for all subclasses. */
abstract class ExceptionHandler extends ExceptionHandlerBase {
@Override
protected void cleanUpAndQuit(RuntimeException e) {
// Clean up all SaRecords.
closeAllSaRecords(false /*expectSaClosed*/);
executeUserCallback(
() -> {
mIkeSessionCallback.onClosedWithException(wrapAsIkeException(e));
});
recordMetricsEvent_sessionTerminated(wrapAsIkeException(e));
logWtf("Unexpected exception in " + getCurrentStateName(), e);
quitSessionNow();
}
@Override
protected String getCmdString(int cmd) {
return CMD_TO_STR.get(cmd);
}
}
/** Called when this StateMachine quits. */
@Override
protected void onQuitting() {
// Clean up all SaRecords.
closeAllSaRecords(true /*expectSaClosed*/);
synchronized (mChildCbToSessions) {
for (ChildSessionStateMachine child : mChildCbToSessions.values()) {
// Fire asynchronous call for Child Sessions to do cleanup and remove itself
// from the map.
child.killSession();
}
}
mIkeConnectionCtrl.tearDown();
releaseAlarmReceiver(mIkeContext.getContext(), this, mIkeSessionId);
mIke3gppExtensionExchange.close();
mBusyWakeLock.release();
mScheduler.releaseAllLocalRequestWakeLocks();
}
private void closeAllSaRecords(boolean expectSaClosed) {
closeIkeSaRecord(mCurrentIkeSaRecord, expectSaClosed);
closeIkeSaRecord(mLocalInitNewIkeSaRecord, expectSaClosed);
closeIkeSaRecord(mRemoteInitNewIkeSaRecord, expectSaClosed);
mCurrentIkeSaRecord = null;
mLocalInitNewIkeSaRecord = null;
mRemoteInitNewIkeSaRecord = null;
}
private void closeIkeSaRecord(IkeSaRecord ikeSaRecord, boolean expectSaClosed) {
if (ikeSaRecord == null) return;
removeIkeSaRecord(ikeSaRecord);
ikeSaRecord.close();
if (!expectSaClosed) return;
logWtf(
"IkeSaRecord with local SPI: "
+ ikeSaRecord.getLocalSpi()
+ " is not correctly closed.");
}
private void handleIkeFatalError(Exception error) {
handleIkeFatalError(error, false /* isFromChild */);
}
private void handleIkeFatalError(Exception error, boolean isFromChild) {
IkeException ikeException = wrapAsIkeException(error);
loge("IKE Session fatal error in " + getCurrentState().getName(), ikeException);
try {
// Clean up all SaRecords.
closeAllSaRecords(false /*expectSaClosed*/);
} catch (Exception e) {
// This try catch block is to add a protection in case there is a program error. The
// error is not actionable to IKE callers.
logWtf("Unexpected error in #handleIkeFatalError", e);
} finally {
executeUserCallback(
() -> {
mIkeSessionCallback.onClosedWithException(ikeException);
});
// Fatal child session event metrics gathered in ChildSessionStateMachine
if (!isFromChild) {
recordMetricsEvent_sessionTerminated(ikeException);
}
quitSessionNow();
}
}
/** Parent state used to delete IKE sessions */
class KillIkeSessionParent extends ExceptionHandler {
@Override
public boolean processStateMessage(Message message) {
switch (message.what) {
case CMD_KILL_SESSION:
closeAllSaRecords(false /*expectSaClosed*/);
executeUserCallback(
() -> {
mIkeSessionCallback.onClosed();
});
recordMetricsEvent_sessionTerminated(null);
quitSessionNow();
return HANDLED;
default:
return NOT_HANDLED;
}
}
@Override
protected @IkeMetrics.IkeState int getMetricsStateCode() {
return IkeMetrics.IKE_STATE_IKE_KILL;
}
}
// This method should always run on the IKE worker thread
private static void setupAlarmReceiver(
Handler ikeHandler, Context context, IkeSessionStateMachine ike, int ikeSessionId) {
if (!sContextToIkeSmMap.containsKey(context)) {
int flags = SdkLevel.isAtLeastT() ? Context.RECEIVER_NOT_EXPORTED : 0;
// Pass in a Handler so #onReceive will run on the StateMachine thread
context.registerReceiver(
sIkeAlarmReceiver,
sIntentFilter,
null /* broadcastPermission */,
ikeHandler,
flags);
sContextToIkeSmMap.put(context, new HashSet<IkeSessionStateMachine>());
}
sContextToIkeSmMap.get(context).add(ike);
sIkeAlarmReceiver.registerIkeSession(ikeSessionId, ikeHandler);
}
// This method should always run on the IKE worker thread
private static void releaseAlarmReceiver(
Context context, IkeSessionStateMachine ike, int ikeSessionId) {
sIkeAlarmReceiver.unregisterIkeSession(ikeSessionId);
Set<IkeSessionStateMachine> ikeSet = sContextToIkeSmMap.get(context);
ikeSet.remove(ike);
if (ikeSet.isEmpty()) {
context.unregisterReceiver(sIkeAlarmReceiver);
sContextToIkeSmMap.remove(context);
}
}
/** Initial state of IkeSessionStateMachine. */
class Initial extends ExceptionHandler {
private InitialSetupData mInitialSetupData;
/** Reset resources that might have been created when this state was entered previously */
private void reset() {
mIkeConnectionCtrl.tearDown();
}
@Override
public void enterState() {
if (mInitialSetupData == null) {
handleIkeFatalError(
wrapAsIkeException(new IllegalStateException("mInitialSetupData is null")));
return;
}
reset();
setupAlarmReceiver(
getHandler(),
mIkeContext.getContext(),
IkeSessionStateMachine.this,
mIkeSessionId);
try {
mIkeConnectionCtrl.setUp();
// TODO(b/191673438): Set a specific tag for VPN.
TrafficStats.setThreadStatsTag(Process.myUid());
} catch (IkeException e) {
handleIkeFatalError(e);
}
}
public void setIkeSetupData(InitialSetupData setupData) {
mInitialSetupData = setupData;
}
@Override
public boolean processStateMessage(Message message) {
switch (message.what) {
case CMD_LOCAL_REQUEST_CREATE_IKE:
mCreateIkeLocalIkeInit.setIkeSetupData(mInitialSetupData);
transitionTo(mCreateIkeLocalIkeInit);
return HANDLED;
case CMD_FORCE_TRANSITION:
transitionTo((State) message.obj);
return HANDLED;
default:
return NOT_HANDLED;
}
}
@Override
public void exitState() {
mInitialSetupData = null;
}
@Override
protected @IkeMetrics.IkeState int getMetricsStateCode() {
return IkeMetrics.IKE_STATE_IKE_INITIAL;
}
}
/**
* Idle represents a state when there is no ongoing IKE exchange affecting established IKE SA.
*/
class Idle extends LocalRequestQueuer {
private IkeAlarm mDpdAlarm;
// TODO (b/152236790): Add wakelock for awaiting LocalRequests and ongoing procedures.
@Override
public void enterState() {
if (!mScheduler.readyForNextProcedure()) {
mBusyWakeLock.release();
}
// If a liveness check has been requested but the success has not been marked yet,
// enqueue a on-demand DPD when entering to idle state.
if (mLivenessAssister.isLivenessCheckRequested()) {
sendMessage(CMD_REQUEST_LIVENESS_CHECK, LivenessAssister.REQ_TYPE_ON_DEMAND);
}
int dpdDelaySeconds = mIkeSessionParams.getDpdDelaySeconds();
if (dpdDelaySeconds != IkeSessionParams.IKE_DPD_DELAY_SEC_DISABLED) {
long dpdDelayMs = TimeUnit.SECONDS.toMillis(dpdDelaySeconds);
long remoteIkeSpi = mCurrentIkeSaRecord.getRemoteSpi();
Message intentIkeMsg = getIntentIkeSmMsg(CMD_LOCAL_REQUEST_DPD, remoteIkeSpi);
PendingIntent dpdIntent =
IkeAlarm.buildIkeAlarmIntent(
mIkeContext.getContext(),
ACTION_DPD,
getIntentIdentifier(mIkeSessionId, remoteIkeSpi),
intentIkeMsg);
// Initiating DPD is a way to detect the aliveness of the remote server and also a
// way to assert the aliveness of IKE library. Considering this, the alarm to
// trigger DPD needs to go off even when device is in doze mode to decrease the
// chance the remote server thinks IKE library is dead. Also, since DPD initiation
// is time-critical, we need to use "setExact" to avoid the batching alarm delay
// which can be at most 75% for the alarm timeout
// (@see AlarmManagerService#maxTriggerTime).
// Please check AlarmManager#setExactAndAllowWhileIdle for more details.
mDpdAlarm =
mDeps.newExactAndAllowWhileIdleAlarm(
new IkeAlarmConfig(
mIkeContext.getContext(),
ACTION_DPD,
dpdDelayMs,
dpdIntent,
intentIkeMsg));
mDpdAlarm.schedule();
logd("DPD Alarm scheduled with DPD delay: " + dpdDelayMs + "ms");
}
}
@Override
protected void exitState() {
// #exitState is guaranteed to be invoked when quit() or quitSessionNow() is called
if (mDpdAlarm != null) {
mDpdAlarm.cancel();
logd("DPD Alarm canceled");
}
mBusyWakeLock.acquire();
}
@Override
public boolean processStateMessage(Message message) {
switch (message.what) {
case CMD_RECEIVE_IKE_PACKET:
deferMessage(message);
transitionTo(mReceiving);
return HANDLED;
case CMD_ALARM_FIRED:
handleFiredAlarm(message);
return HANDLED;
case CMD_FORCE_TRANSITION: // Testing command
transitionTo((State) message.obj);
return HANDLED;
case CMD_EXECUTE_LOCAL_REQ:
executeLocalRequest((LocalRequest) message.obj, message);
return HANDLED;
case CMD_KILL_SESSION:
// Notify the remote that the IKE Session is being deleted. This notification is
// sent as a best-effort, so don't worry about retransmitting.
sendEncryptedIkeMessage(buildIkeDeleteReq(mCurrentIkeSaRecord));
// Let KillIkeSessionParent handle the rest of the cleanup.
return NOT_HANDLED;
case CMD_SET_NETWORK:
if (!mIkeConnectionCtrl.isMobilityEnabled()) {
logi("setNetwork() called for session without mobility support.");
// TODO(b/224686889): Notify caller of failed mobility attempt.
return HANDLED;
}
try {
final NetworkParams params = (NetworkParams) message.obj;
mIkeConnectionCtrl.onNetworkSetByUser(
params.network,
params.ipVersion,
params.encapType,
params.keepaliveDelaySeconds);
} catch (IkeException e) {
handleIkeFatalError(e);
}
return HANDLED;
case CMD_SET_UNDERPINNED_NETWORK:
try {
mIkeConnectionCtrl.onUnderpinnedNetworkSetByUser((Network) message.obj);
} catch (IkeException e) {
handleIkeFatalError(e);
}
return HANDLED;
case CMD_UNDERLYING_NETWORK_DIED_WITH_MOBILITY:
// Set a flag in the IkeSessionStateMachine to suspend retransmission.
mIsRetransmitSuspended = true;
return HANDLED;
case CMD_UNDERLYING_NETWORK_UPDATED_WITH_MOBILITY:
// Unset a flag to resume retransmission.
mIsRetransmitSuspended = false;
return HANDLED;
case CMD_REQUEST_LIVENESS_CHECK:
// Since there is no other running requests in idle state, the on-demand DPD
// can be taken place in the scheduler. At this time, the liveness check can be
// performed through an on-demand DPD LocalRequest.
if (!mLivenessAssister.isLivenessCheckRequested()
|| message.arg1 == LivenessAssister.REQ_TYPE_INITIAL) {
// If this is the initial liveness check request has been made or a request
// has been received from a client, it is marked as a request and notifies.
mLivenessAssister.livenessCheckRequested(
LivenessAssister.REQ_TYPE_ON_DEMAND);
}
handleLocalRequest(
CMD_LOCAL_REQUEST_ON_DEMAND_DPD,
mLocalRequestFactory.getIkeLocalRequest(
CMD_LOCAL_REQUEST_ON_DEMAND_DPD,
mCurrentIkeSaRecord.getRemoteSpi()));
mScheduler.readyForNextProcedure();
return HANDLED;
default:
// Queue local requests, and trigger next procedure
if (isLocalRequest(message.what)) {
handleLocalRequest(message.what, (LocalRequest) message.obj);
// Synchronously calls through to the scheduler callback, which will
// post the CMD_EXECUTE_LOCAL_REQ to the front of the queue, ensuring
// it is always the next request processed.
mScheduler.readyForNextProcedure();
return HANDLED;
}
return NOT_HANDLED;
}
}
private void executeLocalRequest(LocalRequest req, Message message) {
req.releaseWakeLock();
if (!isRequestForCurrentSa(req)) {
logd("Request is for a deleted SA. Ignore it.");
mScheduler.readyForNextProcedure();
return;
}
switch (req.procedureType) {
case CMD_LOCAL_REQUEST_REKEY_IKE:
transitionTo(mRekeyIkeLocalCreate);
break;
case CMD_LOCAL_REQUEST_DELETE_IKE:
transitionTo(mDeleteIkeLocalDelete);
break;
case CMD_LOCAL_REQUEST_DPD:
transitionTo(mDpdIkeLocalInfo);
break;
case CMD_LOCAL_REQUEST_ON_DEMAND_DPD:
transitionTo(mDpdOnDemandIkeLocalInfo);
break;
case CMD_LOCAL_REQUEST_CREATE_CHILD: // fallthrough
case CMD_LOCAL_REQUEST_REKEY_CHILD: // fallthrough
case CMD_LOCAL_REQUEST_REKEY_CHILD_MOBIKE: // fallthrough
case CMD_LOCAL_REQUEST_MIGRATE_CHILD: // fallthrough
case CMD_LOCAL_REQUEST_DELETE_CHILD:
deferMessage(message);
transitionTo(mChildProcedureOngoing);
break;
case CMD_LOCAL_REQUEST_MOBIKE:
transitionTo(mMobikeLocalInfo);
break;
default:
cleanUpAndQuit(
new IllegalStateException(
"Invalid local request procedure type: " + req.procedureType));
}
}
// When in Idle state, this IkeSessionStateMachine and all its ChildSessionStateMachines
// only have one alive IKE/Child SA respectively. Returns true if this local request is for
// the current IKE/Child SA, or false if the request is for a deleted SA.
private boolean isRequestForCurrentSa(LocalRequest localRequest) {
if (localRequest.isChildRequest()) {
ChildLocalRequest req = (ChildLocalRequest) localRequest;
if (req.remoteSpi == IkeLocalRequestScheduler.SPI_NOT_INCLUDED
|| mRemoteSpiToChildSessionMap.get(req.remoteSpi) != null) {
return true;
}
} else {
IkeLocalRequest req = (IkeLocalRequest) localRequest;
if (req.remoteSpi == IkeLocalRequestScheduler.SPI_NOT_INCLUDED
|| req.remoteSpi == mCurrentIkeSaRecord.getRemoteSpi()) {
return true;
}
}
return false;
}
@Override
protected @IkeMetrics.IkeState int getMetricsStateCode() {
return IkeMetrics.IKE_STATE_IKE_IDLE;
}
}
private static String getIntentIdentifier(int ikeSessionId, long remoteIkeSpi) {
return TAG + "_" + ikeSessionId + "_" + remoteIkeSpi;
}
private Message getIntentIkeSmMsg(int localRequestType, long remoteIkeSpi) {
Bundle spiBundle = new Bundle();
spiBundle.putLong(BUNDLE_KEY_IKE_REMOTE_SPI, remoteIkeSpi);
return obtainMessage(CMD_ALARM_FIRED, mIkeSessionId, localRequestType, spiBundle);
}
@VisibleForTesting
SaLifetimeAlarmScheduler buildSaLifetimeAlarmScheduler(long remoteSpi) {
Message deleteMsg = getIntentIkeSmMsg(CMD_LOCAL_REQUEST_DELETE_IKE, remoteSpi);
Message rekeyMsg = getIntentIkeSmMsg(CMD_LOCAL_REQUEST_REKEY_IKE, remoteSpi);
PendingIntent deleteSaIntent =
IkeAlarm.buildIkeAlarmIntent(
mIkeContext.getContext(),
ACTION_DELETE_IKE,
getIntentIdentifier(mIkeSessionId, remoteSpi),
deleteMsg);
PendingIntent rekeySaIntent =
IkeAlarm.buildIkeAlarmIntent(
mIkeContext.getContext(),
ACTION_REKEY_IKE,
getIntentIdentifier(mIkeSessionId, remoteSpi),
rekeyMsg);
return new SaLifetimeAlarmScheduler(
new IkeAlarmConfig(
mIkeContext.getContext(),
ACTION_DELETE_IKE,
mIkeSessionParams.getHardLifetimeMsInternal(),
deleteSaIntent,
deleteMsg),
new IkeAlarmConfig(
mIkeContext.getContext(),
ACTION_REKEY_IKE,
mIkeSessionParams.getSoftLifetimeMsInternal(),
rekeySaIntent,
rekeyMsg));
}
// Sends the provided IkeMessage using the current IKE SA record
@VisibleForTesting
void sendEncryptedIkeMessage(IkeMessage msg) {
sendEncryptedIkeMessage(mCurrentIkeSaRecord, msg);
}
// Sends the provided IkeMessage using the provided IKE SA record
@VisibleForTesting
void sendEncryptedIkeMessage(IkeSaRecord ikeSaRecord, IkeMessage msg) {
byte[][] packetList =
msg.encryptAndEncode(
mIkeIntegrity,
mIkeCipher,
ikeSaRecord,
mEnabledExtensions.contains(EXTENSION_TYPE_FRAGMENTATION),
DEFAULT_FRAGMENT_SIZE);
sendEncryptedIkePackets(packetList);
if (msg.ikeHeader.isResponseMsg) {
ikeSaRecord.updateLastSentRespAllPackets(
Arrays.asList(packetList), msg.ikeHeader.messageId);
}
}
private void sendEncryptedIkePackets(byte[][] packetList) {
for (byte[] packet : packetList) {
mIkeConnectionCtrl.sendIkePacket(packet);
}
}
// Builds and sends IKE-level error notification response on the provided IKE SA record
@VisibleForTesting
void buildAndSendErrorNotificationResponse(
IkeSaRecord ikeSaRecord, int messageId, @ErrorType int errorType) {
IkeNotifyPayload error = new IkeNotifyPayload(errorType);
buildAndSendNotificationResponse(ikeSaRecord, messageId, error);
}
// Builds and sends error notification response on the provided IKE SA record
@VisibleForTesting
void buildAndSendNotificationResponse(
IkeSaRecord ikeSaRecord, int messageId, IkeNotifyPayload notifyPayload) {
IkeMessage msg =
buildEncryptedNotificationMessage(
ikeSaRecord,
new IkeInformationalPayload[] {notifyPayload},
EXCHANGE_TYPE_INFORMATIONAL,
true /*isResponse*/,
messageId);
sendEncryptedIkeMessage(ikeSaRecord, msg);
}
// Builds an Encrypted IKE Informational Message for the given IkeInformationalPayload using the
// current IKE SA record.
@VisibleForTesting
IkeMessage buildEncryptedInformationalMessage(
IkeInformationalPayload[] payloads, boolean isResponse, int messageId) {
return buildEncryptedInformationalMessage(
mCurrentIkeSaRecord, payloads, isResponse, messageId);
}
// Builds an Encrypted IKE Informational Message for the given IkeInformationalPayload using the
// provided IKE SA record.
@VisibleForTesting
IkeMessage buildEncryptedInformationalMessage(
IkeSaRecord saRecord,
IkeInformationalPayload[] payloads,
boolean isResponse,
int messageId) {
return buildEncryptedNotificationMessage(
saRecord, payloads, IkeHeader.EXCHANGE_TYPE_INFORMATIONAL, isResponse, messageId);
}
// Builds an Encrypted IKE Message for the given IkeInformationalPayload using the provided IKE
// SA record and exchange type.
@VisibleForTesting
IkeMessage buildEncryptedNotificationMessage(
IkeSaRecord saRecord,
IkeInformationalPayload[] payloads,
@ExchangeType int exchangeType,
boolean isResponse,
int messageId) {
IkeHeader header =
new IkeHeader(
saRecord.getInitiatorSpi(),
saRecord.getResponderSpi(),
IkePayload.PAYLOAD_TYPE_SK,
exchangeType,
isResponse /*isResponseMsg*/,
saRecord.isLocalInit /*fromIkeInitiator*/,
messageId);
return new IkeMessage(header, Arrays.asList(payloads));
}
private abstract class LocalRequestQueuer extends ExceptionHandler {
/**
* Reroutes all local requests to the scheduler
*
* @param requestVal The command value of the request
* @param req The instance of the LocalRequest to be queued.
*/
protected void handleLocalRequest(int requestVal, LocalRequest req) {
switch (requestVal) {
case CMD_LOCAL_REQUEST_DELETE_IKE: // Fallthrough
case CMD_LOCAL_REQUEST_MOBIKE: // Fallthrough
case CMD_LOCAL_REQUEST_REKEY_IKE: // Fallthrough
case CMD_LOCAL_REQUEST_INFO: // Fallthrough
case CMD_LOCAL_REQUEST_DPD: // Fallthrough
case CMD_LOCAL_REQUEST_ON_DEMAND_DPD:
mScheduler.addRequest(req);
return;
case CMD_LOCAL_REQUEST_CREATE_CHILD: // Fallthrough
case CMD_LOCAL_REQUEST_REKEY_CHILD: // Fallthrough
case CMD_LOCAL_REQUEST_REKEY_CHILD_MOBIKE: // Fallthrough
case CMD_LOCAL_REQUEST_MIGRATE_CHILD: // Fallthrough
case CMD_LOCAL_REQUEST_DELETE_CHILD:
ChildLocalRequest childReq = (ChildLocalRequest) req;
if (childReq.procedureType != requestVal) {
cleanUpAndQuit(
new IllegalArgumentException(
"ChildLocalRequest procedure type was invalid"));
}
mScheduler.addRequest(childReq);
return;
default:
cleanUpAndQuit(
new IllegalStateException(
"Unknown local request passed to handleLocalRequest"));
}
}
/** Check if received signal is a local request. */
protected boolean isLocalRequest(int msgWhat) {
if ((msgWhat >= CMD_IKE_LOCAL_REQUEST_BASE
&& msgWhat < CMD_IKE_LOCAL_REQUEST_BASE + CMD_CATEGORY_SIZE)
|| (msgWhat >= CMD_CHILD_LOCAL_REQUEST_BASE
&& msgWhat < CMD_CHILD_LOCAL_REQUEST_BASE + CMD_CATEGORY_SIZE)) {
return true;
}
return false;
}
protected void handleFiredAlarm(Message message) {
switch (message.arg2) {
case CMD_SEND_KEEPALIVE:
mIkeConnectionCtrl.fireKeepAlive();
return;
case CMD_LOCAL_REQUEST_DELETE_CHILD: // Hits hard lifetime; fall through
case CMD_LOCAL_REQUEST_REKEY_CHILD: // Hits soft lifetime
int remoteChildSpi = ((Bundle) message.obj).getInt(BUNDLE_KEY_CHILD_REMOTE_SPI);
enqueueLocalRequestSynchronously(
mLocalRequestFactory.getChildLocalRequest(
message.arg2, remoteChildSpi));
return;
case CMD_LOCAL_REQUEST_DELETE_IKE: // Hits hard lifetime; fall through
case CMD_LOCAL_REQUEST_REKEY_IKE: // Hits soft lifetime; fall through
case CMD_LOCAL_REQUEST_DPD:
// IKE Session has not received any protectd IKE packet for the whole DPD delay
long remoteIkeSpi = ((Bundle) message.obj).getLong(BUNDLE_KEY_IKE_REMOTE_SPI);
enqueueLocalRequestSynchronously(
mLocalRequestFactory.getIkeLocalRequest(message.arg2, remoteIkeSpi));
// TODO(b/152442041): Cancel the scheduled DPD request if IKE Session starts any
// procedure before DPD get executed.
return;
default:
logWtf("Invalid alarm action: " + message.arg2);
}
}
private void enqueueLocalRequestSynchronously(LocalRequest request) {
// Use dispatchMessage to synchronously handle this message so that the AlarmManager
// WakeLock can keep protecting this message until it is enquequed in mScheduler. It is
// safe because the alarmReceiver is called on the Ike HandlerThread, and the
// IkeSessionStateMachine is not currently in a state transition.
getHandler().dispatchMessage(obtainMessage(request.procedureType, request));
}
/** Builds a IKE Delete Request for the given IKE SA. */
protected IkeMessage buildIkeDeleteReq(IkeSaRecord ikeSaRecord) {
IkeInformationalPayload[] payloads =
new IkeInformationalPayload[] {new IkeDeletePayload()};
return buildEncryptedInformationalMessage(
ikeSaRecord,
payloads,
false /* isResp */,
ikeSaRecord.getLocalRequestMessageId());
}
}
/**
* Base state defines common behaviours when receiving an IKE packet.
*
* <p>State that represents an ongoing IKE procedure MUST extend BusyState to handle received
* IKE packet. Idle state will defer the received packet to a BusyState to process it.
*/
private abstract class BusyState extends LocalRequestQueuer {
@Nullable protected Retransmitter mRetransmitter;
@Override
public boolean processStateMessage(Message message) {
switch (message.what) {
case CMD_RECEIVE_IKE_PACKET:
handleReceivedIkePacket(message);
return HANDLED;
case CMD_ALARM_FIRED:
handleFiredAlarm(message);
return HANDLED;
case CMD_FORCE_TRANSITION:
transitionTo((State) message.obj);
return HANDLED;
case CMD_EXECUTE_LOCAL_REQ:
logWtf("Invalid execute local request command in non-idle state");
return NOT_HANDLED;
case CMD_RETRANSMIT:
triggerRetransmit();
return HANDLED;
case CMD_SET_NETWORK:
if (!mIkeConnectionCtrl.isMobilityEnabled()) {
logi("setNetwork() called for session without mobility support.");
// TODO(b/224686889): Notify caller of failed mobility attempt.
return HANDLED;
}
try {
final NetworkParams params = (NetworkParams) message.obj;
mIkeConnectionCtrl.onNetworkSetByUser(
params.network,
params.ipVersion,
params.encapType,
params.keepaliveDelaySeconds);
} catch (IkeException e) {
handleIkeFatalError(e);
}
return HANDLED;
case CMD_SET_UNDERPINNED_NETWORK:
try {
mIkeConnectionCtrl.onUnderpinnedNetworkSetByUser((Network) message.obj);
} catch (IkeException e) {
handleIkeFatalError(e);
}
return HANDLED;
case CMD_REQUEST_LIVENESS_CHECK:
if (mLivenessAssister.isLivenessCheckRequested()
&& message.arg1 == LivenessAssister.REQ_TYPE_ON_DEMAND) {
return HANDLED;
}
mLivenessAssister.livenessCheckRequested(LivenessAssister.REQ_TYPE_BACKGROUND);
return HANDLED;
case CMD_UNDERLYING_NETWORK_DIED_WITH_MOBILITY:
// Sets a flag to suspend retransmission.
mIsRetransmitSuspended = true;
// Suspends retransmissions only if retransmission is in progress.
if (mRetransmitter != null) {
mRetransmitter.suspendRetransmitting();
}
return HANDLED;
case CMD_UNDERLYING_NETWORK_UPDATED_WITH_MOBILITY:
// Unsets a flag to resume retransmission.
mIsRetransmitSuspended = false;
// Restarts retransmissions only when in suspend state.
if (mRetransmitter != null) {
mRetransmitter.restartRetransmitting();
}
return HANDLED;
default:
// Queue local requests, and trigger next procedure
if (isLocalRequest(message.what)) {
handleLocalRequest(message.what, (LocalRequest) message.obj);
return HANDLED;
}
return NOT_HANDLED;
}
}
/**
* Handler for retransmission timer firing
*
* <p>By default, the trigger is logged and dropped. States that have a retransmitter should
* override this function, and proxy the call to Retransmitter.retransmit()
*/
protected void triggerRetransmit() {
logWtf("Retransmission trigger dropped in state: " + this.getClass().getSimpleName());
}
protected IkeSaRecord getIkeSaRecordForPacket(IkeHeader ikeHeader) {
if (ikeHeader.fromIkeInitiator) {
return mLocalSpiToIkeSaRecordMap.get(ikeHeader.ikeResponderSpi);
} else {
return mLocalSpiToIkeSaRecordMap.get(ikeHeader.ikeInitiatorSpi);
}
}
protected void handleReceivedIkePacket(Message message) {
// TODO: b/138411550 Notify subclasses when discarding a received packet. Receiving MUST
// go back to Idle state in this case.
String methodTag = "handleReceivedIkePacket: ";
ReceivedIkePacket receivedIkePacket = (ReceivedIkePacket) message.obj;
IkeHeader ikeHeader = receivedIkePacket.ikeHeader;
byte[] ikePacketBytes = receivedIkePacket.ikePacketBytes;
IkeSaRecord ikeSaRecord = getIkeSaRecordForPacket(ikeHeader);
String msgDirection = ikeHeader.isResponseMsg ? "response" : "request";
// Drop packets that we don't have an SA for:
if (ikeSaRecord == null) {
// TODO: Print a summary of the IKE message (perhaps the IKE header)
cleanUpAndQuit(
new IllegalStateException(
"Received an IKE "
+ msgDirection
+ "but found no matching SA for it"));
return;
}
logd(
methodTag
+ "Received an "
+ ikeHeader.getBasicInfoString()
+ " on IKE SA with local SPI: "
+ ikeSaRecord.getLocalSpi()
+ ". Packet size: "
+ ikePacketBytes.length);
if (ikeHeader.isResponseMsg) {
int expectedMsgId = ikeSaRecord.getLocalRequestMessageId();
if (expectedMsgId - 1 == ikeHeader.messageId) {
logd(methodTag + "Received re-transmitted response. Discard it.");
return;
}
DecodeResult decodeResult =
IkeMessage.decode(
expectedMsgId,
mIkeIntegrity,
mIkeCipher,
ikeSaRecord,
ikeHeader,
ikePacketBytes,
ikeSaRecord.getCollectedFragments(true /*isResp*/));
switch (decodeResult.status) {
case DECODE_STATUS_OK:
mLivenessAssister.markPeerAsAlive();
ikeSaRecord.incrementLocalRequestMessageId();
ikeSaRecord.resetCollectedFragments(true /*isResp*/);
DecodeResultOk resultOk = (DecodeResultOk) decodeResult;
if (isTempFailure(resultOk.ikeMessage)) {
handleTempFailure();
} else {
mTempFailHandler.reset();
}
handleResponseIkeMessage(resultOk.ikeMessage);
break;
case DECODE_STATUS_PARTIAL:
ikeSaRecord.updateCollectedFragments(
(DecodeResultPartial) decodeResult, true /*isResp*/);
break;
case DECODE_STATUS_PROTECTED_ERROR:
IkeException ikeException = ((DecodeResultError) decodeResult).ikeException;
logi(methodTag + "Protected error", ikeException);
ikeSaRecord.incrementLocalRequestMessageId();
ikeSaRecord.resetCollectedFragments(true /*isResp*/);
handleResponseGenericProcessError(
ikeSaRecord,
new InvalidSyntaxException(
"Generic processing error in the received response",
ikeException));
break;
case DECODE_STATUS_UNPROTECTED_ERROR:
logi(
methodTag
+ "Message authentication or decryption failed on received"
+ " response. Discard it",
((DecodeResultError) decodeResult).ikeException);
break;
default:
cleanUpAndQuit(
new IllegalStateException(
"Unrecognized decoding status: " + decodeResult.status));
}
} else {
int expectedMsgId = ikeSaRecord.getRemoteRequestMessageId();
if (expectedMsgId - 1 == ikeHeader.messageId) {
if (ikeSaRecord.isRetransmittedRequest(ikePacketBytes)) {
if (ikeSaRecord.getLastSentRespMsgId() == ikeHeader.messageId) {
logd(
"Received re-transmitted request "
+ ikeHeader.messageId
+ " Retransmitting response");
for (byte[] packet : ikeSaRecord.getLastSentRespAllPackets()) {
mIkeConnectionCtrl.sendIkePacket(packet);
}
} else {
logd(
"Received re-transmitted request "
+ ikeHeader.messageId
+ " Original request is still being processed");
}
// TODO:Support resetting remote rekey delete timer.
} else {
logi(methodTag + "Received a request with invalid message ID. Discard it.");
}
} else {
DecodeResult decodeResult =
IkeMessage.decode(
expectedMsgId,
mIkeIntegrity,
mIkeCipher,
ikeSaRecord,
ikeHeader,
ikePacketBytes,
ikeSaRecord.getCollectedFragments(false /*isResp*/));
switch (decodeResult.status) {
case DECODE_STATUS_OK:
mLivenessAssister.markPeerAsAlive();
ikeSaRecord.incrementRemoteRequestMessageId();
ikeSaRecord.resetCollectedFragments(false /*isResp*/);
DecodeResultOk resultOk = (DecodeResultOk) decodeResult;
IkeMessage ikeMessage = resultOk.ikeMessage;
ikeSaRecord.updateLastReceivedReqFirstPacket(resultOk.firstPacket);
// Handle DPD here.
if (ikeMessage.isDpdRequest()) {
logd(methodTag + "Received DPD request");
IkeMessage dpdResponse =
buildEncryptedInformationalMessage(
ikeSaRecord,
new IkeInformationalPayload[] {},
true,
ikeHeader.messageId);
sendEncryptedIkeMessage(ikeSaRecord, dpdResponse);
break;
}
int ikeExchangeSubType = ikeMessage.getIkeExchangeSubType();
logd(
methodTag
+ "Request exchange subtype: "
+ IkeMessage.getIkeExchangeSubTypeString(
ikeExchangeSubType));
if (ikeExchangeSubType == IKE_EXCHANGE_SUBTYPE_INVALID
|| ikeExchangeSubType == IKE_EXCHANGE_SUBTYPE_IKE_INIT
|| ikeExchangeSubType == IKE_EXCHANGE_SUBTYPE_IKE_AUTH) {
// Reply with INVALID_SYNTAX and close IKE Session.
buildAndSendErrorNotificationResponse(
mCurrentIkeSaRecord,
ikeHeader.messageId,
ERROR_TYPE_INVALID_SYNTAX);
handleIkeFatalError(
new InvalidSyntaxException(
"Cannot handle message with invalid or unexpected"
+ " IkeExchangeSubType: "
+ ikeExchangeSubType));
return;
}
handleRequestIkeMessage(ikeMessage, ikeExchangeSubType, message);
break;
case DECODE_STATUS_PARTIAL:
ikeSaRecord.updateCollectedFragments(
(DecodeResultPartial) decodeResult, false /*isResp*/);
break;
case DECODE_STATUS_PROTECTED_ERROR:
DecodeResultProtectedError resultError =
(DecodeResultProtectedError) decodeResult;
IkeException ikeException = resultError.ikeException;
logi(methodTag + "Protected error", resultError.ikeException);
ikeSaRecord.incrementRemoteRequestMessageId();
ikeSaRecord.resetCollectedFragments(false /*isResp*/);
ikeSaRecord.updateLastReceivedReqFirstPacket(resultError.firstPacket);
// IkeException MUST be already wrapped into an IkeProtocolException
handleRequestGenericProcessError(
ikeSaRecord,
ikeHeader.messageId,
(IkeProtocolException) ikeException);
break;
case DECODE_STATUS_UNPROTECTED_ERROR:
logi(
methodTag
+ "Message authentication or decryption failed on"
+ " received request. Discard it",
((DecodeResultError) decodeResult).ikeException);
break;
default:
cleanUpAndQuit(
new IllegalStateException(
"Unrecognized decoding status: "
+ decodeResult.status));
}
}
}
}
private boolean isTempFailure(IkeMessage message) {
List<IkeNotifyPayload> notifyPayloads =
message.getPayloadListForType(PAYLOAD_TYPE_NOTIFY, IkeNotifyPayload.class);
for (IkeNotifyPayload notify : notifyPayloads) {
if (notify.notifyType == ERROR_TYPE_TEMPORARY_FAILURE) {
return true;
}
}
return false;
}
protected void handleTempFailure() {
// Log and close IKE Session due to unexpected TEMPORARY_FAILURE. This error should
// only occur during CREATE_CHILD_SA exchange.
handleIkeFatalError(
new InvalidSyntaxException("Received unexpected TEMPORARY_FAILURE"));
// States that accept a TEMPORARY MUST override this method to schedule a retry.
}
protected void handleGenericInfoRequest(IkeMessage ikeMessage) {
try {
List<IkeInformationalPayload> infoPayloadList = new ArrayList<>();
for (IkePayload payload : ikeMessage.ikePayloadList) {
switch (payload.payloadType) {
case PAYLOAD_TYPE_CP:
// TODO(b/150327849): Respond with config payload responses.
break;
case PAYLOAD_TYPE_NOTIFY:
IkeNotifyPayload notify = (IkeNotifyPayload) payload;
if (notify.notifyType == NOTIFY_TYPE_COOKIE2) {
infoPayloadList.add(
IkeNotifyPayload.handleCookie2AndGenerateCopy(notify));
}
// No action for other notifications
break;
default:
logw(
"Received unexpected payload in an INFORMATIONAL request."
+ " Payload type: "
+ payload.payloadType);
}
}
// add any 3GPP informational payloads if needed
List<IkePayload> ikePayloads =
mIke3gppExtensionExchange.getResponsePayloads(
IKE_EXCHANGE_SUBTYPE_GENERIC_INFO, ikeMessage.ikePayloadList);
for (IkePayload payload : ikePayloads) {
if (payload instanceof IkeInformationalPayload) {
infoPayloadList.add((IkeInformationalPayload) payload);
} else {
logd(
"Ignoring unexpected payload that is not an IkeInformationalPayload"
+ payload);
}
}
IkeMessage infoResp =
buildEncryptedInformationalMessage(
infoPayloadList.toArray(
new IkeInformationalPayload[infoPayloadList.size()]),
true /* isResponse */,
ikeMessage.ikeHeader.messageId);
sendEncryptedIkeMessage(infoResp);
} catch (InvalidSyntaxException e) {
buildAndSendErrorNotificationResponse(
mCurrentIkeSaRecord,
ikeMessage.ikeHeader.messageId,
ERROR_TYPE_INVALID_SYNTAX);
handleIkeFatalError(e);
return;
}
}
protected void handleRequestIkeMessage(
IkeMessage ikeMessage, int ikeExchangeSubType, Message message) {
// Subclasses MUST override it if they care
cleanUpAndQuit(
new IllegalStateException(
"Do not support handling an encrypted request: " + ikeExchangeSubType));
}
protected void handleResponseIkeMessage(IkeMessage ikeMessage) {
// Subclasses MUST override it if they care
cleanUpAndQuit(
new IllegalStateException("Do not support handling an encrypted response"));
}
/**
* Method for handling generic processing error of a request.
*
* <p>A generic processing error is usally syntax error, unsupported critical payload error
* and major version error. IKE SA that should reply with corresponding error notifications
*/
protected void handleRequestGenericProcessError(
IkeSaRecord ikeSaRecord, int messageId, IkeProtocolException exception) {
IkeNotifyPayload errNotify = exception.buildNotifyPayload();
sendEncryptedIkeMessage(
ikeSaRecord,
buildEncryptedInformationalMessage(
ikeSaRecord,
new IkeInformationalPayload[] {errNotify},
true /*isResponse*/,
messageId));
// Receiver of INVALID_SYNTAX error notification should delete the IKE SA
if (exception.getErrorType() == ERROR_TYPE_INVALID_SYNTAX) {
handleIkeFatalError(exception);
}
}
/**
* Method for handling generic processing error of a response.
*
* <p>Detailed error is wrapped in the InvalidSyntaxException, which is usally syntax error,
* unsupported critical payload error and major version error. IKE SA that receives a
* response with these errors should be closed.
*/
protected void handleResponseGenericProcessError(
IkeSaRecord ikeSaRecord, InvalidSyntaxException ikeException) {
// Subclasses MUST override it if they care
cleanUpAndQuit(
new IllegalStateException(
"Do not support handling generic processing error of encrypted"
+ " response"));
}
/**
* Method for handling and extracting 3GPP-specific payloads from the IKE response payloads.
*
* <p>Returns the extracted 3GPP payloads after they have been handled. Only non
* error-notify payloads are returned.
*/
protected List<IkePayload> handle3gppRespAndExtractNonError3gppPayloads(
int exchangeSubtype, List<IkePayload> respPayloads) throws InvalidSyntaxException {
List<IkePayload> ike3gppPayloads =
mIke3gppExtensionExchange.extract3gppResponsePayloads(
exchangeSubtype, respPayloads);
mIke3gppExtensionExchange.handle3gppResponsePayloads(exchangeSubtype, ike3gppPayloads);
List<IkePayload> ike3gppErrorNotifyPayloads = new ArrayList<>();
for (IkePayload payload : ike3gppPayloads) {
if (payload instanceof IkeNotifyPayload) {
IkeNotifyPayload notifyPayload = (IkeNotifyPayload) payload;
if (notifyPayload.isErrorNotify()) {
ike3gppErrorNotifyPayloads.add(payload);
}
}
}
ike3gppPayloads.removeAll(ike3gppErrorNotifyPayloads);
return ike3gppPayloads;
}
}
/**
* Retransmitter represents a RAII class to send the initial request, and retransmit as needed.
*
* <p>The Retransmitter class will automatically start transmission upon creation.
*/
@VisibleForTesting
class EncryptedRetransmitter extends Retransmitter {
private final byte[][] mIkePacketList;
@VisibleForTesting
EncryptedRetransmitter(IkeMessage msg) {
this(mCurrentIkeSaRecord, msg);
}
private EncryptedRetransmitter(IkeSaRecord ikeSaRecord, IkeMessage msg) {
this(ikeSaRecord, msg, mIkeSessionParams.getRetransmissionTimeoutsMillis());
}
private EncryptedRetransmitter(
IkeSaRecord ikeSaRecord, IkeMessage msg, int[] retransmissionTimeouts) {
super(getHandler(), msg, retransmissionTimeouts);
mIkePacketList =
msg.encryptAndEncode(
mIkeIntegrity,
mIkeCipher,
ikeSaRecord,
mEnabledExtensions.contains(EXTENSION_TYPE_FRAGMENTATION),
DEFAULT_FRAGMENT_SIZE);
if (mIsRetransmitSuspended) {
// If already suspended retransmit, set as suspended.
suspendRetransmitting();
} else {
// start retransmit.
retransmit();
}
}
@Override
public void send() {
sendEncryptedIkePackets(mIkePacketList);
}
@Override
public void handleRetransmissionFailure() {
mLivenessAssister.markPeerAsDead();
handleIkeFatalError(
ShimUtils.getInstance()
.getRetransmissionFailedException("Retransmitting failure"));
}
}
/**
* DeleteResponderBase represents all states after IKE_INIT and IKE_AUTH.
*
* <p>All post-init states share common functionality of being able to respond to IKE_DELETE
* requests.
*/
private abstract class DeleteResponderBase extends BusyState {
/** Builds a IKE Delete Response for the given IKE SA and request. */
protected IkeMessage buildIkeDeleteResp(IkeMessage req, IkeSaRecord ikeSaRecord) {
IkeInformationalPayload[] payloads = new IkeInformationalPayload[] {};
return buildEncryptedInformationalMessage(
ikeSaRecord, payloads, true /* isResp */, req.ikeHeader.messageId);
}
/**
* Validates that the delete request is acceptable.
*
* <p>The request message must be guaranteed by previous checks to be of SUBTYPE_DELETE_IKE,
* and therefore contains an IkeDeletePayload. This is checked in getIkeExchangeSubType.
*/
protected void validateIkeDeleteReq(IkeMessage req, IkeSaRecord expectedRecord)
throws InvalidSyntaxException {
if (expectedRecord != getIkeSaRecordForPacket(req.ikeHeader)) {
throw new InvalidSyntaxException("Delete request received in wrong SA");
}
}
/**
* Helper method for responding to a session deletion request
*
* <p>Note that this method expects that the session is keyed on the current IKE SA session,
* and closing the IKE SA indicates that the remote wishes to end the session as a whole. As
* such, this should not be used in rekey cases where there is any ambiguity as to which IKE
* SA the session is reliant upon.
*
* <p>Note that this method will also quit the state machine.
*
* @param ikeMessage The received session deletion request
*/
protected void handleDeleteSessionRequest(IkeMessage ikeMessage) {
try {
validateIkeDeleteReq(ikeMessage, mCurrentIkeSaRecord);
IkeMessage resp = buildIkeDeleteResp(ikeMessage, mCurrentIkeSaRecord);
executeUserCallback(
() -> {
mIkeSessionCallback.onClosed();
});
sendEncryptedIkeMessage(mCurrentIkeSaRecord, resp);
removeIkeSaRecord(mCurrentIkeSaRecord);
mCurrentIkeSaRecord.close();
mCurrentIkeSaRecord = null;
recordMetricsEvent_sessionTerminated(null);
quitSessionNow();
} catch (InvalidSyntaxException e) {
// Got deletion of a non-Current IKE SA. Program error.
cleanUpAndQuit(new IllegalStateException(e));
}
}
}
/**
* DeleteBase abstracts deletion handling for all states initiating a delete exchange
*
* <p>All subclasses of this state share common functionality that a deletion request is sent,
* and the response is received.
*/
private abstract class DeleteBase extends DeleteResponderBase {
protected void validateIkeDeleteResp(IkeMessage resp, IkeSaRecord expectedSaRecord)
throws InvalidSyntaxException {
if (expectedSaRecord != getIkeSaRecordForPacket(resp.ikeHeader)) {
throw new IllegalStateException("Response received on incorrect SA");
}
if (resp.ikeHeader.exchangeType != IkeHeader.EXCHANGE_TYPE_INFORMATIONAL) {
throw new InvalidSyntaxException(
"Invalid exchange type; expected INFORMATIONAL, but got: "
+ resp.ikeHeader.exchangeType);
}
if (!resp.ikePayloadList.isEmpty()) {
throw new InvalidSyntaxException(
"Unexpected payloads - IKE Delete response should be empty.");
}
}
}
/**
* Receiving represents a state when idle IkeSessionStateMachine receives an incoming packet.
*
* <p>If this incoming packet is fully handled by Receiving state and does not trigger any
* further state transition or deletion of whole IKE Session, IkeSessionStateMachine MUST
* transition back to Idle.
*/
class Receiving extends RekeyIkeHandlerBase {
private boolean mProcedureFinished = true;
@Override
public void enterState() {
mProcedureFinished = true;
}
@Override
protected void handleReceivedIkePacket(Message message) {
super.handleReceivedIkePacket(message);
// If the IKE process triggered by the received packet is completed in this
// state, transition back to Idle. Otherwise, either stay in this state, or transition
// to another state specified in #handleRequestIkeMessage.
if (mProcedureFinished) transitionTo(mIdle);
}
@Override
protected void handleRequestIkeMessage(
IkeMessage ikeMessage, int ikeExchangeSubType, Message message) {
switch (ikeExchangeSubType) {
case IKE_EXCHANGE_SUBTYPE_REKEY_IKE:
// Errors in this exchange with no specific protocol error code will all be
// classified to use NO_PROPOSAL_CHOSEN. The reason that we don't use
// NO_ADDITIONAL_SAS is because it indicates "responder is unwilling to accept
// any more Child SAs on this IKE SA.", according to RFC 7296. Sending this
// error may mislead the remote peer.
try {
validateIkeRekeyReq(ikeMessage);
// Build a rekey response payload with our previously selected proposal,
// against which we will validate the received proposals. Re-negotiating
// proposal with different algorithms is not supported since there
// is no use case.
IkeSaPayload reqSaPayload =
ikeMessage.getPayloadForType(
IkePayload.PAYLOAD_TYPE_SA, IkeSaPayload.class);
byte respProposalNumber =
reqSaPayload.getNegotiatedProposalNumber(mSaProposal);
IkeKePayload reqKePayload =
ikeMessage.getPayloadForType(
IkePayload.PAYLOAD_TYPE_KE, IkeKePayload.class);
if (reqKePayload.dhGroup != mSaProposal.getDhGroups().get(0)) {
throw new InvalidKeException(mSaProposal.getDhGroups().get(0));
}
List<IkePayload> payloadList =
CreateIkeSaHelper.getRekeyIkeSaResponsePayloads(
respProposalNumber,
mSaProposal,
mIkeSpiGenerator,
mIkeConnectionCtrl.getLocalAddress(),
mIkeContext.getRandomnessFactory());
// Build IKE header
IkeHeader ikeHeader =
new IkeHeader(
mCurrentIkeSaRecord.getInitiatorSpi(),
mCurrentIkeSaRecord.getResponderSpi(),
IkePayload.PAYLOAD_TYPE_SK,
IkeHeader.EXCHANGE_TYPE_CREATE_CHILD_SA,
true /*isResponseMsg*/,
mCurrentIkeSaRecord.isLocalInit,
ikeMessage.ikeHeader.messageId);
IkeMessage responseIkeMessage = new IkeMessage(ikeHeader, payloadList);
// Build new SA first to ensure that we can find a valid proposal.
mRemoteInitNewIkeSaRecord =
validateAndBuildIkeSa(
ikeMessage, responseIkeMessage, false /*isLocalInit*/);
sendEncryptedIkeMessage(responseIkeMessage);
transitionTo(mRekeyIkeRemoteDelete);
mProcedureFinished = false;
} catch (IkeProtocolException e) {
handleRekeyCreationFailure(ikeMessage.ikeHeader.messageId, e);
} catch (GeneralSecurityException e) {
handleRekeyCreationFailure(
ikeMessage.ikeHeader.messageId,
new NoValidProposalChosenException(
"Error in building new IKE SA", e));
} catch (IOException e) {
handleRekeyCreationFailure(
ikeMessage.ikeHeader.messageId,
new NoValidProposalChosenException(
"IKE SPI allocation collided - they reused an SPI.", e));
}
return;
case IKE_EXCHANGE_SUBTYPE_DELETE_IKE:
handleDeleteSessionRequest(ikeMessage);
// Directly quit from this state. Do not need to transition back to Idle state
mProcedureFinished = false;
return;
case IKE_EXCHANGE_SUBTYPE_CREATE_CHILD: // Fall through
case IKE_EXCHANGE_SUBTYPE_DELETE_CHILD: // Fall through
case IKE_EXCHANGE_SUBTYPE_REKEY_CHILD:
deferMessage(
obtainMessage(
CMD_RECEIVE_REQUEST_FOR_CHILD,
ikeExchangeSubType,
0 /*placeHolder*/,
ikeMessage));
transitionTo(mChildProcedureOngoing);
mProcedureFinished = false;
return;
case IKE_EXCHANGE_SUBTYPE_GENERIC_INFO:
handleGenericInfoRequest(ikeMessage);
return;
default:
}
}
private void handleRekeyCreationFailure(int messageId, IkeProtocolException e) {
loge("Received invalid Rekey IKE request. Reject with error notification", e);
buildAndSendNotificationResponse(
mCurrentIkeSaRecord, messageId, e.buildNotifyPayload());
}
@Override
protected @IkeMetrics.IkeState int getMetricsStateCode() {
return IkeMetrics.IKE_STATE_IKE_RECEIVING;
}
}
/**
* This class represents a state when there is at least one ongoing Child procedure
* (Create/Rekey/Delete Child)
*
* <p>For a locally initiated Child procedure, this state is responsible for notifying Child
* Session to initiate the exchange, building outbound request IkeMessage with Child Session
* provided payload list and redirecting the inbound response to Child Session for validation.
*
* <p>For a remotely initiated Child procedure, this state is responsible for redirecting the
* inbound request to Child Session(s) and building outbound response IkeMessage with Child
* Session provided payload list. Exchange collision on a Child Session will be resolved inside
* the Child Session.
*
* <p>For a remotely initiated IKE procedure, this state will only accept a Delete IKE request
* and reject other types with TEMPORARY_FAILURE, since it causes conflict with the ongoing
* Child procedure.
*
* <p>For most inbound request/response, this state will first pick out and handle IKE related
* payloads and then send the rest of the payloads to Child Session for further validation. It
* is the Child Session's responsibility to check required payloads (and verify the exchange
* type) according to its procedure type. Only when receiving an inbound delete Child request,
* as the only case where multiple Child Sessions will be affected by one IkeMessage, this state
* will only send Delete Payload(s) to Child Session.
*/
class ChildProcedureOngoing extends DeleteBase {
// It is possible that mChildInLocalProcedure is also in mChildInRemoteProcedures when both
// sides initiated exchange for the same Child Session.
private ChildSessionStateMachine mChildInLocalProcedure;
private Set<ChildSessionStateMachine> mChildInRemoteProcedures;
private ChildLocalRequest mLocalRequestOngoing;
// Keep a reference to the first Child SA request so that if IKE Session is killed before
// first Child negotiation is done, ChildProcedureOngoing can release the IPSec SPI resource
// using this reference.
private List<IkePayload> mFirstChildReqList;
private int mLastInboundRequestMsgId;
private List<IkePayload> mOutboundRespPayloads;
private Set<ChildSessionStateMachine> mAwaitingChildResponse;
@Override
public void enterState() {
mChildInLocalProcedure = null;
mChildInRemoteProcedures = new HashSet<>();
mLocalRequestOngoing = null;
mLastInboundRequestMsgId = 0;
mOutboundRespPayloads = new LinkedList<>();
mAwaitingChildResponse = new HashSet<>();
}
@Override
protected void triggerRetransmit() {
mRetransmitter.retransmit();
}
@Override
public boolean processStateMessage(Message message) {
switch (message.what) {
case CMD_RECEIVE_REQUEST_FOR_CHILD:
// Handle remote request (and do state transition)
handleRequestIkeMessage(
(IkeMessage) message.obj,
message.arg1 /*ikeExchangeSubType*/,
null /*ReceivedIkePacket*/);
return HANDLED;
case CMD_OUTBOUND_CHILD_PAYLOADS_READY:
ChildOutboundData outboundData = (ChildOutboundData) message.obj;
int exchangeType = outboundData.exchangeType;
List<IkePayload> outboundPayloads = outboundData.payloadList;
if (outboundData.isResp) {
handleOutboundResponse(
exchangeType, outboundPayloads, outboundData.childSession);
} else {
handleOutboundRequest(exchangeType, outboundPayloads);
}
return HANDLED;
case CMD_CHILD_PROCEDURE_FINISHED:
ChildSessionStateMachine childSession = (ChildSessionStateMachine) message.obj;
if (mChildInLocalProcedure == childSession) {
mChildInLocalProcedure = null;
mLocalRequestOngoing = null;
}
mChildInRemoteProcedures.remove(childSession);
transitionToIdleIfAllProceduresDone();
return HANDLED;
case CMD_HANDLE_FIRST_CHILD_NEGOTIATION:
FirstChildNegotiationData childData = (FirstChildNegotiationData) message.obj;
mFirstChildReqList = childData.reqPayloads;
mChildInLocalProcedure = getChildSession(childData.childSessionCallback);
if (mChildInLocalProcedure == null) {
cleanUpAndQuit(new IllegalStateException("First child not found."));
return HANDLED;
}
mChildInLocalProcedure.handleFirstChildExchange(
childData.reqPayloads,
childData.respPayloads,
mIkeConnectionCtrl.getLocalAddress(),
mIkeConnectionCtrl.getRemoteAddress(),
getEncapSocketOrNull(),
mIkePrf,
mSaProposal.getDhGroupTransforms()[0].id, // negotiated DH
mCurrentIkeSaRecord.getSkD());
return HANDLED;
case CMD_EXECUTE_LOCAL_REQ:
executeLocalRequest((ChildLocalRequest) message.obj);
return HANDLED;
case CMD_KILL_SESSION:
// If mChildInLocalProcedure is null, there are no unfinished locally initiated
// procedures. It is safe to notify the remote that the session is being
// deleted.
if (mChildInLocalProcedure == null) {
// The delete notification is sent as a best-effort, so don't worry about
// retransmitting.
sendEncryptedIkeMessage(buildIkeDeleteReq(mCurrentIkeSaRecord));
}
// Let KillIkeSessionParent handle the rest of the cleanup.
return NOT_HANDLED;
case CMD_IKE_FATAL_ERROR_FROM_CHILD:
IkeFatalErrorFromChild fatalError = (IkeFatalErrorFromChild) message.obj;
handleIkeFatalError(fatalError.exception, true /* isFromChild */);
return HANDLED;
default:
return super.processStateMessage(message);
}
}
@Override
public void exitState() {
if (mIsClosing && mFirstChildReqList != null) {
CreateChildSaHelper.releaseSpiResources(mFirstChildReqList);
}
super.exitState();
}
@Override
protected void handleTempFailure() {
// The ChildSessionStateMachine will be responsible for rescheduling the rejected
// request.
mTempFailHandler.handleTempFailure();
}
private void transitionToIdleIfAllProceduresDone() {
if (mChildInLocalProcedure == null && mChildInRemoteProcedures.isEmpty()) {
transitionTo(mIdle);
}
}
private ChildSessionStateMachine getChildSession(ChildLocalRequest req) {
if (req.childSessionCallback == null) {
return mRemoteSpiToChildSessionMap.get(req.remoteSpi);
}
return getChildSession(req.childSessionCallback);
}
private ChildSessionStateMachine getChildSession(ChildSessionCallback callback) {
synchronized (mChildCbToSessions) {
return mChildCbToSessions.get(callback);
}
}
// Returns the UDP-Encapsulation socket to the newly created ChildSessionStateMachine if
// a NAT is detected or if NAT-T AND MOBIKE are enabled by both parties. It allows the
// ChildSessionStateMachine to build IPsec transforms that can send and receive IPsec
// traffic through a NAT.
private UdpEncapsulationSocket getEncapSocketOrNull() {
if (!mIkeConnectionCtrl.useUdpEncapSocket()) {
return null;
}
return ((IkeUdpEncapSocket) mIkeConnectionCtrl.getIkeSocket())
.getUdpEncapsulationSocket();
}
private void executeLocalRequest(ChildLocalRequest req) {
req.releaseWakeLock();
mChildInLocalProcedure = getChildSession(req);
mLocalRequestOngoing = req;
if (mChildInLocalProcedure == null) {
// This request has been validated to have a recognized target Child Session when
// it was sent to IKE Session at the begginnig. Failing to find this Child Session
// now means the Child creation has failed.
logd(
"Child state machine not found for local request: "
+ req.procedureType
+ " Creation of Child Session may have been failed.");
transitionToIdleIfAllProceduresDone();
return;
}
switch (req.procedureType) {
case CMD_LOCAL_REQUEST_CREATE_CHILD:
mChildInLocalProcedure.createChildSession(
mIkeConnectionCtrl.getLocalAddress(),
mIkeConnectionCtrl.getRemoteAddress(),
getEncapSocketOrNull(),
mIkePrf,
mSaProposal.getDhGroupTransforms()[0].id, // negotiated DH
mCurrentIkeSaRecord.getSkD());
break;
case CMD_LOCAL_REQUEST_REKEY_CHILD:
mChildInLocalProcedure.rekeyChildSession();
break;
case CMD_LOCAL_REQUEST_MIGRATE_CHILD:
mChildInLocalProcedure.performMigration(
mIkeConnectionCtrl.getLocalAddress(),
mIkeConnectionCtrl.getRemoteAddress(),
getEncapSocketOrNull());
break;
case CMD_LOCAL_REQUEST_REKEY_CHILD_MOBIKE:
mChildInLocalProcedure.performRekeyMigration(
mIkeConnectionCtrl.getLocalAddress(),
mIkeConnectionCtrl.getRemoteAddress(),
getEncapSocketOrNull());
break;
case CMD_LOCAL_REQUEST_DELETE_CHILD:
mChildInLocalProcedure.deleteChildSession();
break;
default:
cleanUpAndQuit(
new IllegalStateException(
"Invalid Child procedure type: " + req.procedureType));
break;
}
}
/**
* This method is called when this state receives an inbound request or when mReceiving
* received an inbound Child request and deferred it to this state.
*/
@Override
protected void handleRequestIkeMessage(
IkeMessage ikeMessage, int ikeExchangeSubType, Message message) {
// TODO: Grab a remote lock and hand payloads to the Child Session
mLastInboundRequestMsgId = ikeMessage.ikeHeader.messageId;
switch (ikeExchangeSubType) {
case IKE_EXCHANGE_SUBTYPE_CREATE_CHILD:
buildAndSendErrorNotificationResponse(
mCurrentIkeSaRecord,
ikeMessage.ikeHeader.messageId,
ERROR_TYPE_NO_ADDITIONAL_SAS);
break;
case IKE_EXCHANGE_SUBTYPE_DELETE_IKE:
// Send response and quit state machine
handleDeleteSessionRequest(ikeMessage);
// Return immediately to avoid transitioning to mIdle
return;
case IKE_EXCHANGE_SUBTYPE_DELETE_CHILD:
handleInboundDeleteChildRequest(ikeMessage);
break;
case IKE_EXCHANGE_SUBTYPE_REKEY_IKE:
buildAndSendErrorNotificationResponse(
mCurrentIkeSaRecord,
ikeMessage.ikeHeader.messageId,
ERROR_TYPE_TEMPORARY_FAILURE);
break;
case IKE_EXCHANGE_SUBTYPE_REKEY_CHILD:
handleInboundRekeyChildRequest(ikeMessage);
break;
case IKE_EXCHANGE_SUBTYPE_GENERIC_INFO:
handleGenericInfoRequest(ikeMessage);
break;
default:
cleanUpAndQuit(
new IllegalStateException(
"Invalid IKE exchange subtype: " + ikeExchangeSubType));
return;
}
transitionToIdleIfAllProceduresDone();
}
@Override
protected void handleResponseIkeMessage(IkeMessage ikeMessage) {
mRetransmitter.stopRetransmitting();
List<IkePayload> handledPayloads = new LinkedList<>();
for (IkePayload payload : ikeMessage.ikePayloadList) {
switch (payload.payloadType) {
case PAYLOAD_TYPE_NOTIFY:
// TODO: Handle fatal IKE error notification and IKE status notification.
break;
case PAYLOAD_TYPE_VENDOR:
// TODO: Handle Vendor ID Payload
handledPayloads.add(payload);
break;
case PAYLOAD_TYPE_CP:
// TODO: Handle IKE related configuration attributes and pass the payload to
// Child to further handle internal IP address attributes.
break;
default:
break;
}
}
List<IkePayload> payloads = new LinkedList<>();
payloads.addAll(ikeMessage.ikePayloadList);
payloads.removeAll(handledPayloads);
mChildInLocalProcedure.receiveResponse(ikeMessage.ikeHeader.exchangeType, payloads);
}
@Override
protected void handleResponseGenericProcessError(
IkeSaRecord ikeSaRecord, InvalidSyntaxException ikeException) {
mRetransmitter.stopRetransmitting();
sendEncryptedIkeMessage(buildIkeDeleteReq(mCurrentIkeSaRecord));
handleIkeFatalError(ikeException);
}
private void handleInboundDeleteChildRequest(IkeMessage ikeMessage) {
// It is guaranteed in #getIkeExchangeSubType that at least one Delete Child Payload
// exists.
HashMap<ChildSessionStateMachine, List<IkePayload>> childToDelPayloadsMap =
new HashMap<>();
Set<Integer> spiHandled = new HashSet<>();
for (IkePayload payload : ikeMessage.ikePayloadList) {
switch (payload.payloadType) {
case PAYLOAD_TYPE_VENDOR:
// TODO: Investigate if Vendor ID Payload can be in an INFORMATIONAL
// message.
break;
case PAYLOAD_TYPE_NOTIFY:
logw(
"Unexpected or unknown notification: "
+ ((IkeNotifyPayload) payload).notifyType);
break;
case PAYLOAD_TYPE_DELETE:
IkeDeletePayload delPayload = (IkeDeletePayload) payload;
for (int spi : delPayload.spisToDelete) {
ChildSessionStateMachine child = mRemoteSpiToChildSessionMap.get(spi);
if (child == null) {
// TODO: Investigate how other implementations handle that.
logw("Child SA not found with received SPI: " + spi);
} else if (!spiHandled.add(spi)) {
logw("Received repeated Child SPI: " + spi);
} else {
// Store Delete Payload with its target ChildSession
if (!childToDelPayloadsMap.containsKey(child)) {
childToDelPayloadsMap.put(child, new LinkedList<>());
}
List<IkePayload> delPayloads = childToDelPayloadsMap.get(child);
// Avoid storing repeated Delete Payload
if (!delPayloads.contains(delPayload)) delPayloads.add(delPayload);
}
}
break;
case PAYLOAD_TYPE_CP:
// TODO: Handle it
break;
default:
logw("Unexpected payload types found: " + payload.payloadType);
}
}
// If no Child SA is found, only reply with IKE related payloads or an empty
// message
if (childToDelPayloadsMap.isEmpty()) {
logd("No Child SA is found for this request.");
sendEncryptedIkeMessage(
buildEncryptedInformationalMessage(
new IkeInformationalPayload[0],
true /*isResp*/,
ikeMessage.ikeHeader.messageId));
return;
}
// Send Delete Payloads to Child Sessions
for (ChildSessionStateMachine child : childToDelPayloadsMap.keySet()) {
child.receiveRequest(
IKE_EXCHANGE_SUBTYPE_DELETE_CHILD,
EXCHANGE_TYPE_INFORMATIONAL,
childToDelPayloadsMap.get(child));
mAwaitingChildResponse.add(child);
mChildInRemoteProcedures.add(child);
}
}
private void handleInboundRekeyChildRequest(IkeMessage ikeMessage) {
// It is guaranteed in #getIkeExchangeSubType that at least one Notify-Rekey Child
// Payload exists.
List<IkePayload> handledPayloads = new LinkedList<>();
ChildSessionStateMachine targetChild = null;
Set<Integer> unrecognizedSpis = new HashSet<>();
for (IkePayload payload : ikeMessage.ikePayloadList) {
switch (payload.payloadType) {
case PAYLOAD_TYPE_VENDOR:
// TODO: Handle it.
handledPayloads.add(payload);
break;
case PAYLOAD_TYPE_NOTIFY:
IkeNotifyPayload notifyPayload = (IkeNotifyPayload) payload;
if (NOTIFY_TYPE_REKEY_SA != notifyPayload.notifyType) break;
int childSpi = notifyPayload.spi;
ChildSessionStateMachine child = mRemoteSpiToChildSessionMap.get(childSpi);
if (child == null) {
// Remember unrecognized SPIs and reply error notification if no
// recognized SPI found.
unrecognizedSpis.add(childSpi);
logw("Child SA not found with received SPI: " + childSpi);
} else if (targetChild == null) {
// Each message should have only one Notify-Rekey Payload. If there are
// multiple of them, we only process the first valid one and ignore
// others.
targetChild = mRemoteSpiToChildSessionMap.get(childSpi);
} else {
logw("More than one Notify-Rekey Payload found with SPI: " + childSpi);
handledPayloads.add(notifyPayload);
}
break;
case PAYLOAD_TYPE_CP:
// TODO: Handle IKE related configuration attributes and pass the payload to
// Child to further handle internal IP address attributes.
break;
default:
break;
}
}
// Reject request with error notification.
if (targetChild == null) {
IkeInformationalPayload[] errorPayloads =
new IkeInformationalPayload[unrecognizedSpis.size()];
int i = 0;
for (Integer spi : unrecognizedSpis) {
errorPayloads[i++] =
new IkeNotifyPayload(
IkePayload.PROTOCOL_ID_ESP,
spi,
ERROR_TYPE_CHILD_SA_NOT_FOUND,
new byte[0]);
}
IkeMessage msg =
buildEncryptedNotificationMessage(
mCurrentIkeSaRecord,
errorPayloads,
EXCHANGE_TYPE_INFORMATIONAL,
true /*isResponse*/,
ikeMessage.ikeHeader.messageId);
sendEncryptedIkeMessage(mCurrentIkeSaRecord, msg);
return;
}
// Normal path
List<IkePayload> payloads = new LinkedList<>();
payloads.addAll(ikeMessage.ikePayloadList);
payloads.removeAll(handledPayloads);
mAwaitingChildResponse.add(targetChild);
mChildInRemoteProcedures.add(targetChild);
targetChild.receiveRequest(
IKE_EXCHANGE_SUBTYPE_REKEY_CHILD, ikeMessage.ikeHeader.exchangeType, payloads);
}
private void handleOutboundRequest(int exchangeType, List<IkePayload> outboundPayloads) {
IkeHeader ikeHeader =
new IkeHeader(
mCurrentIkeSaRecord.getInitiatorSpi(),
mCurrentIkeSaRecord.getResponderSpi(),
IkePayload.PAYLOAD_TYPE_SK,
exchangeType,
false /*isResp*/,
mCurrentIkeSaRecord.isLocalInit,
mCurrentIkeSaRecord.getLocalRequestMessageId());
IkeMessage ikeMessage = new IkeMessage(ikeHeader, outboundPayloads);
mRetransmitter = new EncryptedRetransmitter(ikeMessage);
}
private void handleOutboundResponse(
int exchangeType,
List<IkePayload> outboundPayloads,
ChildSessionStateMachine childSession) {
// For each request IKE passed to Child, Child will send back to IKE a response. Even
// if the Child Session is under simultaneous deletion, it will send back an empty
// payload list.
mOutboundRespPayloads.addAll(outboundPayloads);
mAwaitingChildResponse.remove(childSession);
// When the server tries to delete multiple Child Sessions in one IKE exchange,
// mAwaitingChildResponse may not be empty. It means that there are Child Sessions
// have not sent IKE Session the delete responses. In this case IKE Session needs to
// return and keep waiting for all the Child responses in this state.
if (!mAwaitingChildResponse.isEmpty()) return;
IkeHeader ikeHeader =
new IkeHeader(
mCurrentIkeSaRecord.getInitiatorSpi(),
mCurrentIkeSaRecord.getResponderSpi(),
IkePayload.PAYLOAD_TYPE_SK,
exchangeType,
true /*isResp*/,
mCurrentIkeSaRecord.isLocalInit,
mLastInboundRequestMsgId);
IkeMessage ikeMessage = new IkeMessage(ikeHeader, mOutboundRespPayloads);
sendEncryptedIkeMessage(ikeMessage);
// Clear mOutboundRespPayloads so that in a two-exchange process (e.g. Rekey Child), the
// response of the first exchange won't be added to the response of the second exchange.
mOutboundRespPayloads.clear();
}
@Override
protected @IkeMetrics.IkeState int getMetricsStateCode() {
return IkeMetrics.IKE_STATE_IKE_CHILD_PROCEDURE_ONGOING;
}
}
/** CreateIkeLocalIkeInit represents state when IKE library initiates IKE_INIT exchange. */
@VisibleForTesting
public class CreateIkeLocalIkeInit extends BusyState {
private InitialSetupData mInitialSetupData;
private byte[] mIkeInitRequestBytes;
private byte[] mIkeInitResponseBytes;
private IkeNoncePayload mIkeInitNoncePayload;
private IkeNoncePayload mIkeRespNoncePayload;
private Set<Short> mPeerSignatureHashAlgorithms = new HashSet<>();
private IkeSecurityParameterIndex mLocalIkeSpiResource;
private IkeSecurityParameterIndex mRemoteIkeSpiResource;
// TODO: Support negotiating IKE fragmentation
@Override
public void enterState() {
if (mInitialSetupData == null) {
handleIkeFatalError(
wrapAsIkeException(new IllegalStateException("mInitialSetupData is null")));
return;
}
try {
sendRequest(buildIkeInitReq());
} catch (IOException e) {
// Fail to assign IKE SPI
handleIkeFatalError(e);
}
}
private void sendRequest(IkeMessage request) {
// Register local SPI to receive the IKE INIT response.
mIkeConnectionCtrl.registerIkeSpi(request.ikeHeader.ikeInitiatorSpi);
mIkeInitRequestBytes = request.encode();
mIkeInitNoncePayload =
request.getPayloadForType(IkePayload.PAYLOAD_TYPE_NONCE, IkeNoncePayload.class);
if (mRetransmitter != null) {
mRetransmitter.stopRetransmitting();
}
mRetransmitter = new UnencryptedRetransmitter(request);
}
@Override
protected void triggerRetransmit() {
mRetransmitter.retransmit();
}
public void setIkeSetupData(InitialSetupData setupData) {
mInitialSetupData = setupData;
}
@Override
public boolean processStateMessage(Message message) {
switch (message.what) {
case CMD_RECEIVE_IKE_PACKET:
handleReceivedIkePacket(message);
return HANDLED;
case CMD_SET_NETWORK:
// Shouldn't be receiving this command before MOBIKE is active - determined with
// last IKE_AUTH response
logWtf("Received SET_NETWORK cmd in " + getCurrentStateName());
return NOT_HANDLED;
default:
return super.processStateMessage(message);
}
}
protected void handleReceivedIkePacket(Message message) {
String methodTag = "handleReceivedIkePacket: ";
ReceivedIkePacket receivedIkePacket = (ReceivedIkePacket) message.obj;
IkeHeader ikeHeader = receivedIkePacket.ikeHeader;
byte[] ikePacketBytes = receivedIkePacket.ikePacketBytes;
logd(
methodTag
+ "Received an "
+ ikeHeader.getBasicInfoString()
+ ". Packet size: "
+ ikePacketBytes.length);
if (ikeHeader.isResponseMsg) {
DecodeResult decodeResult = IkeMessage.decode(0, ikeHeader, ikePacketBytes);
switch (decodeResult.status) {
case DECODE_STATUS_OK:
mIkeInitResponseBytes = ikePacketBytes;
handleResponseIkeMessage(((DecodeResultOk) decodeResult).ikeMessage);
// SA negotiation failed
if (mCurrentIkeSaRecord == null) break;
mCurrentIkeSaRecord.incrementLocalRequestMessageId();
break;
case DECODE_STATUS_PARTIAL:
// Fall through. We don't support IKE fragmentation here. We should never
// get this status.
case DECODE_STATUS_PROTECTED_ERROR:
// IKE INIT response is not protected. So we should never get this status
cleanUpAndQuit(
new IllegalStateException(
"Unexpected decoding status: " + decodeResult.status));
break;
case DECODE_STATUS_UNPROTECTED_ERROR:
logi(
"Discard unencrypted response with syntax error",
((DecodeResultError) decodeResult).ikeException);
break;
default:
cleanUpAndQuit(
new IllegalStateException(
"Invalid decoding status: " + decodeResult.status));
}
} else {
// TODO: Also prettyprint IKE header in the log.
logi("Received a request while waiting for IKE_INIT response. Discard it.");
}
}
/** Returns the Notify-Cookie payload, or null if it does not exist */
private IkeNotifyPayload getNotifyCookie(IkeMessage ikeMessage) {
List<IkeNotifyPayload> notifyPayloads =
ikeMessage.getPayloadListForType(PAYLOAD_TYPE_NOTIFY, IkeNotifyPayload.class);
for (IkeNotifyPayload notify : notifyPayloads) {
if (notify.notifyType == NOTIFY_TYPE_COOKIE) {
return notify;
}
}
return null;
}
@Override
protected void handleResponseIkeMessage(IkeMessage ikeMessage) {
// IKE_SA_INIT exchange and IKE SA setup succeed
boolean ikeInitSuccess = false;
// IKE INIT is not finished. IKE_SA_INIT request was re-sent with Notify-Cookie,
// and the same INIT SPI and other payloads.
boolean ikeInitRetriedWithCookie = false;
try {
int exchangeType = ikeMessage.ikeHeader.exchangeType;
if (exchangeType != IkeHeader.EXCHANGE_TYPE_IKE_SA_INIT) {
throw new InvalidSyntaxException(
"Expected EXCHANGE_TYPE_IKE_SA_INIT but received: " + exchangeType);
}
// Retry IKE INIT if there is Notify-Cookie
IkeNotifyPayload inCookiePayload = getNotifyCookie(ikeMessage);
if (inCookiePayload != null) {
IkeNotifyPayload outCookiePayload =
IkeNotifyPayload.handleCookieAndGenerateCopy(inCookiePayload);
IkeMessage initReq =
buildReqWithCookie(mRetransmitter.getMessage(), outCookiePayload);
sendRequest(initReq);
ikeInitRetriedWithCookie = true;
return;
}
// Negotiate IKE SA
validateIkeInitResp(mRetransmitter.getMessage(), ikeMessage);
mCurrentIkeSaRecord =
IkeSaRecord.makeFirstIkeSaRecord(
mRetransmitter.getMessage(),
ikeMessage,
mLocalIkeSpiResource,
mRemoteIkeSpiResource,
mIkePrf,
mIkeIntegrity == null ? 0 : mIkeIntegrity.getKeyLength(),
mIkeCipher.getKeyLength(),
buildSaLifetimeAlarmScheduler(mRemoteIkeSpiResource.getSpi()));
addIkeSaRecord(mCurrentIkeSaRecord);
ikeInitSuccess = true;
mCreateIkeLocalIkeAuth.setIkeSetupData(
new IkeInitData(
mInitialSetupData,
mIkeInitRequestBytes,
mIkeInitResponseBytes,
mIkeInitNoncePayload,
mIkeRespNoncePayload,
mPeerSignatureHashAlgorithms));
transitionTo(mCreateIkeLocalIkeAuth);
} catch (IkeProtocolException | GeneralSecurityException | IOException e) {
if (e instanceof InvalidKeException) {
InvalidKeException keException = (InvalidKeException) e;
int requestedDhGroup = keException.getDhGroup();
boolean doAllProposalsHaveDhGroup = true;
for (IkeSaProposal proposal : mIkeSessionParams.getSaProposalsInternal()) {
doAllProposalsHaveDhGroup &=
proposal.getDhGroups().contains(requestedDhGroup);
}
// If DH group is not acceptable for all proposals, fail. The caller explicitly
// did not want that combination, and the IKE library must honor it.
if (doAllProposalsHaveDhGroup) {
// Remove state set during request creation
mIkeConnectionCtrl.unregisterIkeSpi(
mRetransmitter.getMessage().ikeHeader.ikeInitiatorSpi);
mIkeInitRequestBytes = null;
mIkeInitNoncePayload = null;
mInitial.setIkeSetupData(
new InitialSetupData(
mInitialSetupData.firstChildSessionParams,
mInitialSetupData.firstChildCallback,
requestedDhGroup));
transitionTo(mInitial);
openSession();
return;
}
}
handleIkeFatalError(e);
} finally {
if (!ikeInitSuccess && !ikeInitRetriedWithCookie) {
if (mLocalIkeSpiResource != null) {
mLocalIkeSpiResource.close();
mLocalIkeSpiResource = null;
}
if (mRemoteIkeSpiResource != null) {
mRemoteIkeSpiResource.close();
mRemoteIkeSpiResource = null;
}
}
}
}
private IkeMessage buildIkeInitReq() throws IOException {
// Generate IKE SPI
mLocalIkeSpiResource =
mIkeSpiGenerator.allocateSpi(mIkeConnectionCtrl.getLocalAddress());
long initSpi = mLocalIkeSpiResource.getSpi();
long respSpi = 0;
// It is validated in IkeSessionParams.Builder to ensure IkeSessionParams has at least
// one IkeSaProposal and all SaProposals are valid for IKE SA negotiation.
IkeSaProposal[] saProposals = mIkeSessionParams.getSaProposalsInternal();
List<IkePayload> payloadList =
CreateIkeSaHelper.getIkeInitSaRequestPayloads(
saProposals,
mInitialSetupData.peerSelectedDhGroup,
initSpi,
respSpi,
mIkeConnectionCtrl.getLocalAddress(),
mIkeConnectionCtrl.getRemoteAddress(),
mIkeConnectionCtrl.getLocalPort(),
mIkeConnectionCtrl.getRemotePort(),
mIkeContext.getRandomnessFactory(),
needEnableForceUdpEncap());
payloadList.add(
new IkeNotifyPayload(
IkeNotifyPayload.NOTIFY_TYPE_IKEV2_FRAGMENTATION_SUPPORTED));
ByteBuffer signatureHashAlgoTypes =
ByteBuffer.allocate(
IkeAuthDigitalSignPayload.ALL_SIGNATURE_ALGO_TYPES.length * 2);
for (short type : IkeAuthDigitalSignPayload.ALL_SIGNATURE_ALGO_TYPES) {
signatureHashAlgoTypes.putShort(type);
}
payloadList.add(
new IkeNotifyPayload(
IkeNotifyPayload.NOTIFY_TYPE_SIGNATURE_HASH_ALGORITHMS,
signatureHashAlgoTypes.array()));
// TODO: Add Notification Payloads according to user configurations.
// Build IKE header
IkeHeader ikeHeader =
new IkeHeader(
initSpi,
respSpi,
IkePayload.PAYLOAD_TYPE_SA,
IkeHeader.EXCHANGE_TYPE_IKE_SA_INIT,
false /*isResponseMsg*/,
true /*fromIkeInitiator*/,
0 /*messageId*/);
return new IkeMessage(ikeHeader, payloadList);
}
/**
* Builds an IKE INIT request that has the same payloads and SPI with the original request,
* and with the new Notify-Cookie Payload as the first payload.
*/
private IkeMessage buildReqWithCookie(
IkeMessage originalReq, IkeNotifyPayload cookieNotify) {
List<IkePayload> payloads = new ArrayList<>();
// Notify-Cookie MUST be the first payload.
payloads.add(cookieNotify);
for (IkePayload payload : originalReq.ikePayloadList) {
// Keep all previous payloads except COOKIEs
if (payload instanceof IkeNotifyPayload
&& ((IkeNotifyPayload) payload).notifyType == NOTIFY_TYPE_COOKIE) {
continue;
}
payloads.add(payload);
}
IkeHeader originalHeader = originalReq.ikeHeader;
IkeHeader header =
new IkeHeader(
originalHeader.ikeInitiatorSpi,
originalHeader.ikeResponderSpi,
PAYLOAD_TYPE_NOTIFY,
IkeHeader.EXCHANGE_TYPE_IKE_SA_INIT,
false /* isResponseMsg */,
true /* fromIkeInitiator */,
0 /* messageId */);
return new IkeMessage(header, payloads);
}
private void validateIkeInitResp(IkeMessage reqMsg, IkeMessage respMsg)
throws IkeProtocolException, IOException {
IkeHeader respIkeHeader = respMsg.ikeHeader;
mRemoteIkeSpiResource =
mIkeSpiGenerator.allocateSpi(
mIkeConnectionCtrl.getRemoteAddress(), respIkeHeader.ikeResponderSpi);
IkeSaPayload respSaPayload = null;
IkeKePayload respKePayload = null;
/**
* There MAY be multiple NAT_DETECTION_SOURCE_IP payloads in a message if the sender
* does not know which of several network attachments will be used to send the packet.
*/
List<IkeNotifyPayload> natSourcePayloads = new LinkedList<>();
IkeNotifyPayload natDestPayload = null;
boolean hasNoncePayload = false;
for (IkePayload payload : respMsg.ikePayloadList) {
switch (payload.payloadType) {
case IkePayload.PAYLOAD_TYPE_SA:
respSaPayload = (IkeSaPayload) payload;
break;
case IkePayload.PAYLOAD_TYPE_KE:
respKePayload = (IkeKePayload) payload;
break;
case IkePayload.PAYLOAD_TYPE_CERT_REQUEST:
// Certificates unconditionally sent (only) for Digital Signature Auth
break;
case IkePayload.PAYLOAD_TYPE_NONCE:
hasNoncePayload = true;
mIkeRespNoncePayload = (IkeNoncePayload) payload;
break;
case IkePayload.PAYLOAD_TYPE_VENDOR:
mRemoteVendorIds.add(((IkeVendorPayload) payload).vendorId);
break;
case IkePayload.PAYLOAD_TYPE_NOTIFY:
IkeNotifyPayload notifyPayload = (IkeNotifyPayload) payload;
if (notifyPayload.isErrorNotify()) {
throw notifyPayload.validateAndBuildIkeException();
}
switch (notifyPayload.notifyType) {
case NOTIFY_TYPE_NAT_DETECTION_SOURCE_IP:
natSourcePayloads.add(notifyPayload);
break;
case NOTIFY_TYPE_NAT_DETECTION_DESTINATION_IP:
if (natDestPayload != null) {
throw new InvalidSyntaxException(
"More than one"
+ " NOTIFY_TYPE_NAT_DETECTION_DESTINATION_IP"
+ " found");
}
natDestPayload = notifyPayload;
break;
case NOTIFY_TYPE_IKEV2_FRAGMENTATION_SUPPORTED:
mEnabledExtensions.add(EXTENSION_TYPE_FRAGMENTATION);
break;
case NOTIFY_TYPE_SIGNATURE_HASH_ALGORITHMS:
mPeerSignatureHashAlgorithms.addAll(
IkeAuthDigitalSignPayload
.getSignatureHashAlgorithmsFromIkeNotifyPayload(
notifyPayload));
break;
default:
// Unknown and unexpected status notifications are ignored as per
// RFC7296.
logw(
"Received unknown or unexpected status notifications with"
+ " notify type: "
+ notifyPayload.notifyType);
}
break;
default:
logw(
"Received unexpected payload in IKE INIT response. Payload type: "
+ payload.payloadType);
}
}
if (respSaPayload == null
|| respKePayload == null
|| !hasNoncePayload) {
throw new InvalidSyntaxException("SA, KE, or Nonce payload missing.");
}
IkeSaPayload reqSaPayload =
reqMsg.getPayloadForType(IkePayload.PAYLOAD_TYPE_SA, IkeSaPayload.class);
mSaProposal =
IkeSaPayload.getVerifiedNegotiatedIkeProposalPair(
reqSaPayload,
respSaPayload,
mIkeSpiGenerator,
mIkeConnectionCtrl.getRemoteAddress())
.second
.saProposal;
// Build IKE crypto tools using mSaProposal. It is ensured that mSaProposal is valid and
// has exactly one Transform for each Transform type. Only exception is when
// combined-mode cipher is used, there will be either no integrity algorithm or an
// INTEGRITY_ALGORITHM_NONE type algorithm.
mIkeCipher = IkeCipher.create(mSaProposal.getEncryptionTransforms()[0]);
if (!mIkeCipher.isAead()) {
mIkeIntegrity = IkeMacIntegrity.create(mSaProposal.getIntegrityTransforms()[0]);
}
mIkePrf = IkeMacPrf.create(mSaProposal.getPrfTransforms()[0]);
IkeKePayload reqKePayload =
reqMsg.getPayloadForType(IkePayload.PAYLOAD_TYPE_KE, IkeKePayload.class);
if (reqKePayload.dhGroup != respKePayload.dhGroup
&& respKePayload.dhGroup != mInitialSetupData.peerSelectedDhGroup) {
throw new InvalidSyntaxException("Received KE payload with mismatched DH group.");
}
if (reqMsg.hasNotifyPayload(NOTIFY_TYPE_NAT_DETECTION_SOURCE_IP)) {
handleNatDetection(respMsg, natSourcePayloads, natDestPayload);
}
}
private void handleNatDetection(
IkeMessage respMsg,
List<IkeNotifyPayload> natSourcePayloads,
IkeNotifyPayload natDestPayload)
throws InvalidSyntaxException, IOException {
if (!didPeerIncludeNattDetectionPayloads(natSourcePayloads, natDestPayload)) {
mIkeConnectionCtrl.markSeverNattUnsupported();
return;
}
// NAT detection
long initIkeSpi = respMsg.ikeHeader.ikeInitiatorSpi;
long respIkeSpi = respMsg.ikeHeader.ikeResponderSpi;
boolean isNatDetected =
isLocalOrRemoteNatDetected(
initIkeSpi, respIkeSpi, natSourcePayloads, natDestPayload);
try {
mIkeConnectionCtrl.handleNatDetectionResultInIkeInit(isNatDetected, initIkeSpi);
} catch (IkeException e) {
handleIkeFatalError(e);
}
}
@Override
public void exitState() {
super.exitState();
mInitialSetupData = null;
if (mRetransmitter != null) {
mRetransmitter.stopRetransmitting();
}
}
private class UnencryptedRetransmitter extends Retransmitter {
private final byte[] mIkePacket;
private UnencryptedRetransmitter(IkeMessage msg) {
super(getHandler(), msg, mIkeSessionParams.getRetransmissionTimeoutsMillis());
mIkePacket = msg.encode();
if (mIsRetransmitSuspended) {
// If already suspended retransmit, set as suspended.
suspendRetransmitting();
} else {
// start retransmit.
retransmit();
}
}
@Override
public void send() {
// Sends unencrypted packet
mIkeConnectionCtrl.sendIkePacket(mIkePacket);
}
@Override
public void handleRetransmissionFailure() {
mLivenessAssister.markPeerAsDead();
handleIkeFatalError(
ShimUtils.getInstance()
.getRetransmissionFailedException(
"Retransmitting IKE INIT request failure"));
}
}
@Override
protected @IkeMetrics.IkeState int getMetricsStateCode() {
return IkeMetrics.IKE_STATE_IKE_CREATE_LOCAL_IKE_INIT;
}
}
/**
* Returns if the peer included NAT-T detection payloads
*
* @throws InvalidSyntaxException if an invalid combination of NAT-T detection payloads are
* received.
*/
private boolean didPeerIncludeNattDetectionPayloads(
List<IkeNotifyPayload> natSourcePayloads, IkeNotifyPayload natDestPayload)
throws InvalidSyntaxException {
if (!natSourcePayloads.isEmpty() && natDestPayload != null) {
return true;
} else if (natSourcePayloads.isEmpty() && natDestPayload == null) {
return false;
} else {
throw new InvalidSyntaxException(
"Missing source or destination NAT detection notification");
}
}
/** Returns whether the local or remote peer is a behind NAT. */
private boolean isLocalOrRemoteNatDetected(
long initIkeSpi,
long respIkeSpi,
List<IkeNotifyPayload> natSourcePayloads,
IkeNotifyPayload natDestPayload) {
// Check if local node is behind NAT
byte[] expectedLocalNatData =
IkeNotifyPayload.generateNatDetectionData(
initIkeSpi,
respIkeSpi,
mIkeConnectionCtrl.getLocalAddress(),
mIkeConnectionCtrl.getLocalPort());
boolean localNatDetected = !Arrays.equals(expectedLocalNatData, natDestPayload.notifyData);
// Check if the remote node is behind NAT
byte[] expectedRemoteNatData =
IkeNotifyPayload.generateNatDetectionData(
initIkeSpi,
respIkeSpi,
mIkeConnectionCtrl.getRemoteAddress(),
mIkeConnectionCtrl.getRemotePort());
boolean remoteNatDetected = true;
for (IkeNotifyPayload natPayload : natSourcePayloads) {
// If none of the received hash matches the expected value, the remote node is
// behind NAT.
if (Arrays.equals(expectedRemoteNatData, natPayload.notifyData)) {
remoteNatDetected = false;
}
}
if (!localNatDetected && needEnableForceUdpEncap()) {
logd("there is no actual local NAT, but we have faked it");
localNatDetected = true;
}
return localNatDetected || remoteNatDetected;
}
/**
* MsgValidationResult represents a validation result of an inbound IKE message.
*
* <p>An inbound IKE message might need to go through multiple stages of validations. Thus
* RESULT_OK only represents the success of the current validation stage. It does not mean the
* message is fully validated.
*/
private static class MsgValidationResult {
/** The validation succeeds. */
static final int RESULT_OK = 0;
/** The inbound message is invalid. */
static final int RESULT_ERROR_INVALID_MESSAGE = 1;
/** The inbound message includes error notification that will fail the exchange. */
static final int RESULT_ERROR_RCV_NOTIFY = 2;
private final int mResult;
@Nullable private final IkeException mException;
private MsgValidationResult(int result, @Nullable IkeException exception) {
mResult = result;
mException = exception;
}
static MsgValidationResult newResultOk() {
return new MsgValidationResult(RESULT_OK, null);
}
static MsgValidationResult newResultInvalidMsg(@NonNull IkeException exception) {
return new MsgValidationResult(RESULT_ERROR_INVALID_MESSAGE, exception);
}
static MsgValidationResult newResultRcvErrorNotify(
@NonNull IkeProtocolException exception) {
return new MsgValidationResult(RESULT_ERROR_RCV_NOTIFY, exception);
}
int getResult() {
return mResult;
}
@Nullable
IkeException getException() {
return mException;
}
}
/**
* CreateIkeLocalIkeAuthBase represents the common state and functionality required to perform
* IKE AUTH exchanges in both the EAP and non-EAP flows.
*/
abstract class CreateIkeLocalIkeAuthBase<T extends IkeInitData> extends DeleteBase {
protected T mSetupData;
protected EapInfo mEapInfo = null;
@Override
public void enterState() {
if (mSetupData == null) {
handleIkeFatalError(
wrapAsIkeException(new IllegalStateException("mSetupData is null")));
return;
}
}
public void setIkeSetupData(T setupData) {
mSetupData = setupData;
}
protected void setEapInfo(EapInfo eapInfo) {
mEapInfo = eapInfo;
}
@Override
protected void triggerRetransmit() {
mRetransmitter.retransmit();
}
@Override
public void exitState() {
mSetupData = null;
if (mRetransmitter != null) {
mRetransmitter.stopRetransmitting();
}
}
// TODO: b/139482382 If receiving a remote request while waiting for the last IKE AUTH
// response, defer it to next state.
@Override
protected void handleRequestIkeMessage(
IkeMessage ikeMessage, int ikeExchangeSubType, Message message) {
IkeSaRecord ikeSaRecord = getIkeSaRecordForPacket(ikeMessage.ikeHeader);
// Null out last received packet, so the next state (that handles the actual request)
// does not treat the message as a retransmission.
ikeSaRecord.updateLastReceivedReqFirstPacket(null);
// Send to next state; we can't handle this yet.
deferMessage(message);
}
protected IkeMessage buildIkeAuthReqMessage(List<IkePayload> payloadList) {
// Build IKE header
IkeHeader ikeHeader =
new IkeHeader(
mCurrentIkeSaRecord.getInitiatorSpi(),
mCurrentIkeSaRecord.getResponderSpi(),
IkePayload.PAYLOAD_TYPE_SK,
IkeHeader.EXCHANGE_TYPE_IKE_AUTH,
false /*isResponseMsg*/,
true /*fromIkeInitiator*/,
mCurrentIkeSaRecord.getLocalRequestMessageId());
return new IkeMessage(ikeHeader, payloadList);
}
}
/**
* CreateIkeLocalIkeAuthFirstAndLastExchangeBase represents the common states and
* functionalities required to perform the first and the last IKE AUTH exchanges.
*/
abstract class CreateIkeLocalIkeAuthFirstAndLastExchangeBase<T extends IkeInitData>
extends CreateIkeLocalIkeAuthBase<T> {
protected void authenticatePsk(
byte[] psk, IkeAuthPayload authPayload, IkeIdPayload respIdPayload)
throws AuthenticationFailedException {
if (authPayload.authMethod != IkeAuthPayload.AUTH_METHOD_PRE_SHARED_KEY) {
throw new AuthenticationFailedException(
"Expected the remote/server to use PSK-based authentication but"
+ " they used: "
+ authPayload.authMethod);
}
IkeAuthPskPayload pskPayload = (IkeAuthPskPayload) authPayload;
pskPayload.verifyInboundSignature(
psk,
mSetupData.ikeInitResponseBytes,
mCurrentIkeSaRecord.nonceInitiator,
respIdPayload.getEncodedPayloadBody(),
mIkePrf,
mCurrentIkeSaRecord.getSkPr());
}
protected List<IkePayload> extractChildPayloadsFromMessage(IkeMessage ikeMessage) {
List<IkePayload> list = new LinkedList<>();
for (IkePayload payload : ikeMessage.ikePayloadList) {
switch (payload.payloadType) {
case PAYLOAD_TYPE_SA: // fall through
case PAYLOAD_TYPE_TS_INITIATOR: // fall through
case PAYLOAD_TYPE_TS_RESPONDER: // fall through
case PAYLOAD_TYPE_CP:
list.add(payload);
break;
case PAYLOAD_TYPE_NOTIFY:
if (((IkeNotifyPayload) payload).isNewChildSaNotify()) {
list.add(payload);
}
break;
default:
// Ignore payloads unrelated with Child negotiation
}
}
// Payload validation is done in ChildSessionStateMachine
return list;
}
protected void performFirstChildNegotiation(
List<IkePayload> childReqList, List<IkePayload> childRespList) {
childReqList.add(mSetupData.ikeInitNoncePayload);
childRespList.add(mSetupData.ikeRespNoncePayload);
deferMessage(
obtainMessage(
CMD_HANDLE_FIRST_CHILD_NEGOTIATION,
new FirstChildNegotiationData(
mSetupData.firstChildSessionParams,
mSetupData.firstChildCallback,
childReqList,
childRespList)));
transitionTo(mChildProcedureOngoing);
}
protected IkeSessionConfiguration buildIkeSessionConfiguration(IkeMessage ikeMessage) {
IkeConfigPayload configPayload =
ikeMessage.getPayloadForType(
IkePayload.PAYLOAD_TYPE_CP, IkeConfigPayload.class);
if (configPayload == null) {
logi("No config payload in ikeMessage.");
} else if (configPayload.configType != CONFIG_TYPE_REPLY) {
logi("Unexpected config payload. Config Type: " + configPayload.configType);
configPayload = null;
}
return new IkeSessionConfiguration(
mIkeConnectionCtrl.buildIkeSessionConnectionInfo(),
configPayload,
mRemoteVendorIds,
mEnabledExtensions,
mEapInfo);
}
protected void notifyIkeSessionSetup(IkeMessage msg) {
IkeSessionConfiguration ikeSessionConfig = buildIkeSessionConfiguration(msg);
executeUserCallback(
() -> {
mIkeSessionCallback.onOpened(ikeSessionConfig);
});
}
protected MsgValidationResult handleNotifyInLastAuthResp(
IkeNotifyPayload notifyPayload, IkeAuthPayload authPayload) {
if (notifyPayload.isErrorNotify()) {
if (notifyPayload.isNewChildSaNotify() && authPayload != null) {
// If error is for creating Child and Auth payload is included, try
// to do authentication first and let ChildSessionStateMachine
// handle the error later.
return MsgValidationResult.newResultOk();
} else {
try {
return MsgValidationResult.newResultRcvErrorNotify(
notifyPayload.validateAndBuildIkeException());
} catch (InvalidSyntaxException e) {
return MsgValidationResult.newResultInvalidMsg(e);
}
}
} else if (notifyPayload.isNewChildSaNotify()) {
// If payload is not an error but is for the new Child, it's reasonable
// to receive here. Let the ChildSessionStateMachine handle it.
return MsgValidationResult.newResultOk();
} else if (mIkeSessionParams.hasIkeOption(IKE_OPTION_MOBIKE)
&& notifyPayload.notifyType == NOTIFY_TYPE_MOBIKE_SUPPORTED) {
logd("Both client and server support MOBIKE");
mEnabledExtensions.add(EXTENSION_TYPE_MOBIKE);
return MsgValidationResult.newResultOk();
} else {
// Unknown and unexpected status notifications are ignored as per
// RFC7296.
logw(
"Received unknown or unexpected status notifications with"
+ " notify type: "
+ notifyPayload.notifyType);
return MsgValidationResult.newResultOk();
}
}
/**
* Validate the response, perform authentication and take next steps to finish IKE setup or
* start EAP authentication.
*/
protected abstract MsgValidationResult validateAuthRespAndTakeNextStep(
IkeMessage ikeMessage);
/* Method to handle the first or the last IKE AUTH response */
protected void handleIkeAuthResponse(
IkeMessage ikeMessage, boolean isServerExpectingMoreEap) {
int exchangeType = ikeMessage.ikeHeader.exchangeType;
if (exchangeType != IkeHeader.EXCHANGE_TYPE_IKE_AUTH) {
sendEncryptedIkeMessage(buildIkeDeleteReq(mCurrentIkeSaRecord));
handleIkeFatalError(
new InvalidSyntaxException(
"Expected EXCHANGE_TYPE_IKE_AUTH but received: " + exchangeType));
return;
}
final MsgValidationResult authRespResult = validateAuthRespAndTakeNextStep(ikeMessage);
if (authRespResult.getResult() != MsgValidationResult.RESULT_OK) {
final IkeException e = authRespResult.getException();
if (!isServerExpectingMoreEap && !shouldSilentlyDelete(authRespResult)) {
// Notify the remote because they may have set up the IKE SA.
sendEncryptedIkeMessage(buildIkeDeleteReq(mCurrentIkeSaRecord));
}
handleIkeFatalError(authRespResult.getException());
}
}
/**
* Returns if this validation result indicates IKE termination without Delete exchange.
*
* <p>Receiving a fatal error notification in IKE AUTH should cause the IKE SA to be killed
* without sending a Delete request.
*/
protected boolean shouldSilentlyDelete(MsgValidationResult authRespResult) {
if (authRespResult.getResult() != MsgValidationResult.RESULT_ERROR_RCV_NOTIFY) {
return false;
}
final IkeException e = authRespResult.getException();
return (e instanceof InvalidSyntaxException
|| e instanceof AuthenticationFailedException
|| e instanceof UnsupportedCriticalPayloadException);
}
protected void maybeEnableMobility() throws IkeException {
if (mEnabledExtensions.contains(EXTENSION_TYPE_MOBIKE)) {
logd("Enabling RFC4555 MOBIKE mobility");
mIkeConnectionCtrl.enableMobility();
return;
} else if (mIkeSessionParams.hasIkeOption(IKE_OPTION_REKEY_MOBILITY)) {
logd(
"Enabling Rekey based mobility: IKE Session will try updating Child SA"
+ " addresses with Rekey");
mIkeConnectionCtrl.enableMobility();
return;
} else {
logd(
"Mobility not enabled: IKE Session will not be able to handle network or"
+ " address changes");
}
}
}
/**
* CreateIkeLocalIkeAuth represents state when IKE library initiates IKE_AUTH exchange.
*
* <p>If using EAP, CreateIkeLocalIkeAuth will transition to CreateIkeLocalIkeAuthInEap state
* after validating the IKE AUTH response.
*/
class CreateIkeLocalIkeAuth extends CreateIkeLocalIkeAuthFirstAndLastExchangeBase<IkeInitData> {
private IkeIdPayload mInitIdPayload;
private IkeIdPayload mRespIdPayload;
private List<IkePayload> mFirstChildReqList;
private boolean mUseEap;
@Override
public void enterState() {
try {
super.enterState();
mRetransmitter = new EncryptedRetransmitter(buildIkeAuthReq());
mUseEap =
(IkeSessionParams.IKE_AUTH_METHOD_EAP
== mIkeSessionParams.getLocalAuthConfig().mAuthMethod);
} catch (SpiUnavailableException | ResourceUnavailableException e) {
// Handle IPsec SPI assigning failure.
handleIkeFatalError(e);
}
}
@Override
public boolean processStateMessage(Message message) {
switch (message.what) {
case CMD_SET_NETWORK:
// Shouldn't be receiving this command before MOBIKE is active - determined with
// last IKE_AUTH response
logWtf("Received SET_NETWORK cmd in " + getCurrentStateName());
return NOT_HANDLED;
default:
return super.processStateMessage(message);
}
}
@Override
protected void handleResponseIkeMessage(IkeMessage ikeMessage) {
handleIkeAuthResponse(ikeMessage, mUseEap);
}
@Override
public MsgValidationResult validateAuthRespAndTakeNextStep(IkeMessage ikeMessage) {
MsgValidationResult validateResult = validateIkeAuthResp(ikeMessage);
if (validateResult.getResult() != MsgValidationResult.RESULT_OK) {
return validateResult;
}
List<IkePayload> childReqList =
extractChildPayloadsFromMessage(mRetransmitter.getMessage());
if (mUseEap) {
// childReqList needed after EAP completed, so persist to IkeSessionStateMachine
// state.
mFirstChildReqList = childReqList;
IkeEapPayload ikeEapPayload =
ikeMessage.getPayloadForType(
IkePayload.PAYLOAD_TYPE_EAP, IkeEapPayload.class);
if (ikeEapPayload == null) {
return MsgValidationResult.newResultInvalidMsg(
new AuthenticationFailedException("Missing EAP payload"));
}
deferMessage(obtainMessage(CMD_EAP_START_EAP_AUTH, ikeEapPayload));
mCreateIkeLocalIkeAuthInEap.setIkeSetupData(
new IkeAuthData(
mSetupData, mInitIdPayload, mRespIdPayload, mFirstChildReqList));
transitionTo(mCreateIkeLocalIkeAuthInEap);
} else {
try {
maybeEnableMobility();
} catch (IkeException e) {
return MsgValidationResult.newResultInvalidMsg(e);
}
notifyIkeSessionSetup(ikeMessage);
performFirstChildNegotiation(
childReqList, extractChildPayloadsFromMessage(ikeMessage));
}
return MsgValidationResult.newResultOk();
}
@Override
protected void handleResponseGenericProcessError(
IkeSaRecord ikeSaRecord, InvalidSyntaxException ikeException) {
if (!mUseEap) {
// Notify the remote because they may have set up the IKE SA.
sendEncryptedIkeMessage(buildIkeDeleteReq(mCurrentIkeSaRecord));
}
handleIkeFatalError(ikeException);
}
private IkeMessage buildIkeAuthReq()
throws SpiUnavailableException, ResourceUnavailableException {
List<IkePayload> payloadList = new LinkedList<>();
// Build Identification payloads
mInitIdPayload =
new IkeIdPayload(
true /*isInitiator*/, mIkeSessionParams.getLocalIdentification());
IkeIdPayload respIdPayload =
new IkeIdPayload(
false /*isInitiator*/, mIkeSessionParams.getRemoteIdentification());
payloadList.add(mInitIdPayload);
payloadList.add(respIdPayload);
if (mIkeSessionParams.hasIkeOption(IKE_OPTION_EAP_ONLY_AUTH)) {
payloadList.add(new IkeNotifyPayload(NOTIFY_TYPE_EAP_ONLY_AUTHENTICATION));
}
// Include NOTIFY_TYPE_MOBIKE_SUPPORTED only if IKE_OPTION_MOBIKE is set.
if (mIkeSessionParams.hasIkeOption(IKE_OPTION_MOBIKE)) {
payloadList.add(new IkeNotifyPayload(NOTIFY_TYPE_MOBIKE_SUPPORTED));
}
if (mIkeSessionParams.hasIkeOption(IKE_OPTION_INITIAL_CONTACT)) {
payloadList.add(new IkeNotifyPayload(NOTIFY_TYPE_INITIAL_CONTACT));
}
// Build Authentication payload
IkeAuthConfig authConfig = mIkeSessionParams.getLocalAuthConfig();
switch (authConfig.mAuthMethod) {
case IkeSessionParams.IKE_AUTH_METHOD_PSK:
IkeAuthPskPayload pskPayload =
new IkeAuthPskPayload(
((IkeAuthPskConfig) authConfig).mPsk,
mSetupData.ikeInitRequestBytes,
mCurrentIkeSaRecord.nonceResponder,
mInitIdPayload.getEncodedPayloadBody(),
mIkePrf,
mCurrentIkeSaRecord.getSkPi());
payloadList.add(pskPayload);
break;
case IkeSessionParams.IKE_AUTH_METHOD_PUB_KEY_SIGNATURE:
IkeAuthDigitalSignLocalConfig localAuthConfig =
(IkeAuthDigitalSignLocalConfig) mIkeSessionParams.getLocalAuthConfig();
// Add certificates to list
payloadList.add(
new IkeCertX509CertPayload(localAuthConfig.getClientEndCertificate()));
for (X509Certificate intermediateCert : localAuthConfig.mIntermediateCerts) {
payloadList.add(new IkeCertX509CertPayload(intermediateCert));
}
IkeAuthDigitalSignPayload digitalSignaturePayload =
new IkeAuthDigitalSignPayload(
mSetupData.peerSignatureHashAlgorithms,
localAuthConfig.mPrivateKey,
mSetupData.ikeInitRequestBytes,
mCurrentIkeSaRecord.nonceResponder,
mInitIdPayload.getEncodedPayloadBody(),
mIkePrf,
mCurrentIkeSaRecord.getSkPi());
payloadList.add(digitalSignaturePayload);
break;
case IkeSessionParams.IKE_AUTH_METHOD_EAP:
// Do not include AUTH payload when using EAP.
break;
default:
cleanUpAndQuit(
new IllegalArgumentException(
"Unrecognized authentication method: "
+ authConfig.mAuthMethod));
}
payloadList.addAll(
CreateChildSaHelper.getInitChildCreateReqPayloads(
mIkeContext.getRandomnessFactory(),
mIpSecSpiGenerator,
mIkeConnectionCtrl.getLocalAddress(),
mSetupData.firstChildSessionParams,
true /*isFirstChildSa*/));
final List<ConfigAttribute> configAttributes = new ArrayList<>();
configAttributes.addAll(
Arrays.asList(
CreateChildSaHelper.getConfigAttributes(
mSetupData.firstChildSessionParams)));
configAttributes.addAll(
Arrays.asList(mIkeSessionParams.getConfigurationAttributesInternal()));
// Always request app version
configAttributes.add(new IkeConfigPayload.ConfigAttributeAppVersion());
payloadList.add(new IkeConfigPayload(false /*isReply*/, configAttributes));
// Add 3GPP-specific payloads for this exchange subtype
payloadList.addAll(
mIke3gppExtensionExchange.getRequestPayloads(IKE_EXCHANGE_SUBTYPE_IKE_AUTH));
return buildIkeAuthReqMessage(payloadList);
}
private MsgValidationResult validateIkeAuthResp(IkeMessage authResp) {
// Validate IKE Authentication
IkeAuthPayload authPayload = null;
List<IkeCertPayload> certPayloads = new LinkedList<>();
// Process 3GPP-specific payloads before verifying IKE_AUTH to ensure that the
// caller is informed of them.
List<IkePayload> ike3gppPayloads = null;
try {
ike3gppPayloads =
handle3gppRespAndExtractNonError3gppPayloads(
IKE_EXCHANGE_SUBTYPE_IKE_AUTH, authResp.ikePayloadList);
} catch (InvalidSyntaxException e) {
return MsgValidationResult.newResultInvalidMsg(e);
}
List<IkePayload> payloadsWithout3gpp = new ArrayList<>(authResp.ikePayloadList);
payloadsWithout3gpp.removeAll(ike3gppPayloads);
for (IkePayload payload : payloadsWithout3gpp) {
switch (payload.payloadType) {
case IkePayload.PAYLOAD_TYPE_ID_RESPONDER:
mRespIdPayload = (IkeIdPayload) payload;
if (!mIkeSessionParams.hasIkeOption(
IkeSessionParams.IKE_OPTION_ACCEPT_ANY_REMOTE_ID)
&& !mIkeSessionParams
.getRemoteIdentification()
.equals(mRespIdPayload.ikeId)) {
return MsgValidationResult.newResultInvalidMsg(
new AuthenticationFailedException(
"Unrecognized Responder Identification."));
}
break;
case IkePayload.PAYLOAD_TYPE_AUTH:
authPayload = (IkeAuthPayload) payload;
break;
case IkePayload.PAYLOAD_TYPE_CERT:
certPayloads.add((IkeCertPayload) payload);
break;
case IkePayload.PAYLOAD_TYPE_NOTIFY:
MsgValidationResult result =
handleNotifyInLastAuthResp(
(IkeNotifyPayload) payload,
authResp.getPayloadForType(
PAYLOAD_TYPE_AUTH, IkeAuthPayload.class));
if (result.getResult() != MsgValidationResult.RESULT_OK) {
return result;
}
break;
case PAYLOAD_TYPE_SA: // Will be handled separately; fall through
case PAYLOAD_TYPE_CP: // Will be handled separately; fall through
case PAYLOAD_TYPE_TS_INITIATOR: // Will be handled separately; fall through
case PAYLOAD_TYPE_TS_RESPONDER: // Will be handled separately; fall through
case PAYLOAD_TYPE_EAP: // Will be handled separately
break;
default:
logw(
"Received unexpected payload in IKE AUTH response. Payload"
+ " type: "
+ payload.payloadType);
}
}
// Verify existence of payloads
if (authPayload == null && mIkeSessionParams.hasIkeOption(IKE_OPTION_EAP_ONLY_AUTH)) {
// If EAP-only option is selected, the responder will not send auth payload if it
// accepts EAP-only authentication. Currently only EAP-only safe methods are
// proposed to the responder if IKE_OPTION_EAP_ONLY_AUTH option is set. So there is
// no need to check if the responder selected an EAP-only safe method
return MsgValidationResult.newResultOk();
}
try {
// Authenticate the remote peer.
if (authPayload != null && mRespIdPayload != null) {
authenticate(authPayload, mRespIdPayload, certPayloads);
return MsgValidationResult.newResultOk();
}
} catch (AuthenticationFailedException e) {
return MsgValidationResult.newResultInvalidMsg(e);
}
return MsgValidationResult.newResultInvalidMsg(
new AuthenticationFailedException("ID-Responder or Auth payload is missing."));
}
private void authenticate(
IkeAuthPayload authPayload,
IkeIdPayload respIdPayload,
List<IkeCertPayload> certPayloads)
throws AuthenticationFailedException {
switch (mIkeSessionParams.getRemoteAuthConfig().mAuthMethod) {
case IkeSessionParams.IKE_AUTH_METHOD_PSK:
authenticatePsk(
((IkeAuthPskConfig) mIkeSessionParams.getRemoteAuthConfig()).mPsk,
authPayload,
respIdPayload);
break;
case IkeSessionParams.IKE_AUTH_METHOD_PUB_KEY_SIGNATURE:
authenticateDigitalSignature(
certPayloads,
((IkeAuthDigitalSignRemoteConfig)
mIkeSessionParams.getRemoteAuthConfig())
.mTrustAnchor,
authPayload,
respIdPayload);
break;
default:
cleanUpAndQuit(
new IllegalArgumentException(
"Unrecognized auth method: " + authPayload.authMethod));
}
}
private void authenticateDigitalSignature(
List<IkeCertPayload> certPayloads,
TrustAnchor trustAnchor,
IkeAuthPayload authPayload,
IkeIdPayload respIdPayload)
throws AuthenticationFailedException {
if (authPayload.authMethod != IkeAuthPayload.AUTH_METHOD_RSA_DIGITAL_SIGN
&& authPayload.authMethod != IkeAuthPayload.AUTH_METHOD_GENERIC_DIGITAL_SIGN) {
throw new AuthenticationFailedException(
"Expected the remote/server to use digital-signature-based authentication"
+ " but they used: "
+ authPayload.authMethod);
}
X509Certificate endCert = null;
List<X509Certificate> certList = new LinkedList<>();
// TODO: b/122676944 Extract CRL from IkeCrlPayload when we support IkeCrlPayload
for (IkeCertPayload certPayload : certPayloads) {
X509Certificate cert = ((IkeCertX509CertPayload) certPayload).certificate;
// The first certificate MUST be the end entity certificate.
if (endCert == null) endCert = cert;
certList.add(cert);
}
if (endCert == null) {
throw new AuthenticationFailedException(
"The remote/server failed to provide a end certificate");
}
respIdPayload.validateEndCertIdOrThrow(endCert);
Set<TrustAnchor> trustAnchorSet =
trustAnchor == null ? null : Collections.singleton(trustAnchor);
IkeCertPayload.validateCertificates(
endCert, certList, null /*crlList*/, trustAnchorSet);
IkeAuthDigitalSignPayload signPayload = (IkeAuthDigitalSignPayload) authPayload;
signPayload.verifyInboundSignature(
endCert,
mSetupData.ikeInitResponseBytes,
mCurrentIkeSaRecord.nonceInitiator,
respIdPayload.getEncodedPayloadBody(),
mIkePrf,
mCurrentIkeSaRecord.getSkPr());
}
@Override
public void exitState() {
if (mIsClosing && mFirstChildReqList != null) {
CreateChildSaHelper.releaseSpiResources(mFirstChildReqList);
}
super.exitState();
}
@Override
protected @IkeMetrics.IkeState int getMetricsStateCode() {
return IkeMetrics.IKE_STATE_IKE_CREATE_LOCAL_IKE_AUTH;
}
}
/**
* CreateIkeLocalIkeAuthInEap represents the state when the IKE library authenticates the client
* with an EAP session.
*/
class CreateIkeLocalIkeAuthInEap extends CreateIkeLocalIkeAuthBase<IkeAuthData> {
private EapAuthenticator mEapAuthenticator;
@Override
public void enterState() {
IkeSessionParams.IkeAuthEapConfig ikeAuthEapConfig =
(IkeSessionParams.IkeAuthEapConfig) mIkeSessionParams.getLocalAuthConfig();
// TODO(b/148689509): Pass in deterministic random when test mode is enabled
mEapAuthenticator =
mDeps.newEapAuthenticator(
mIkeContext, new IkeEapCallback(), ikeAuthEapConfig.mEapConfig);
}
@Override
public boolean processStateMessage(Message msg) {
switch (msg.what) {
case CMD_EAP_START_EAP_AUTH:
IkeEapPayload ikeEapPayload = (IkeEapPayload) msg.obj;
mEapAuthenticator.processEapMessage(ikeEapPayload.eapMessage);
return HANDLED;
case CMD_EAP_OUTBOUND_MSG_READY:
IkeEapOutboundMsgWrapper msgWrapper = (IkeEapOutboundMsgWrapper) msg.obj;
IkeEapPayload eapPayload = new IkeEapPayload(msgWrapper.getEapMsg());
List<IkePayload> payloadList = new LinkedList<>();
payloadList.add(eapPayload);
// Add 3GPP-specific payloads for this exchange subtype
payloadList.addAll(
mIke3gppExtensionExchange.getRequestPayloadsInEap(
msgWrapper.isServerAuthenticated()));
// Setup new retransmitter with EAP response
mRetransmitter =
new EncryptedRetransmitter(buildIkeAuthReqMessage(payloadList));
return HANDLED;
case CMD_EAP_ERRORED:
handleIkeFatalError(new AuthenticationFailedException((Throwable) msg.obj));
return HANDLED;
case CMD_EAP_FAILED:
AuthenticationFailedException exception =
new AuthenticationFailedException("EAP Authentication Failed");
handleIkeFatalError(exception);
return HANDLED;
case CMD_EAP_FINISH_EAP_AUTH:
deferMessage(msg);
mCreateIkeLocalIkeAuthPostEap.setIkeSetupData(mSetupData);
transitionTo(mCreateIkeLocalIkeAuthPostEap);
return HANDLED;
case CMD_SET_NETWORK:
// Shouldn't be receiving this command before MOBIKE is active - determined with
// last IKE_AUTH response
logWtf("Received SET_NETWORK cmd in " + getCurrentStateName());
return NOT_HANDLED;
default:
return super.processStateMessage(msg);
}
}
@Override
protected void handleResponseIkeMessage(IkeMessage ikeMessage) {
try {
mRetransmitter.stopRetransmitting();
int exchangeType = ikeMessage.ikeHeader.exchangeType;
if (exchangeType != IkeHeader.EXCHANGE_TYPE_IKE_AUTH) {
throw new InvalidSyntaxException(
"Expected EXCHANGE_TYPE_IKE_AUTH but received: " + exchangeType);
}
// Process 3GPP-specific payloads before verifying IKE_AUTH to ensure that the
// caller is informed of them.
List<IkePayload> ike3gppPayloads =
handle3gppRespAndExtractNonError3gppPayloads(
IKE_EXCHANGE_SUBTYPE_IKE_AUTH, ikeMessage.ikePayloadList);
List<IkePayload> payloadsWithout3gpp = new ArrayList<>(ikeMessage.ikePayloadList);
payloadsWithout3gpp.removeAll(ike3gppPayloads);
IkeEapPayload eapPayload = null;
for (IkePayload payload : payloadsWithout3gpp) {
switch (payload.payloadType) {
case IkePayload.PAYLOAD_TYPE_EAP:
eapPayload = (IkeEapPayload) payload;
break;
case IkePayload.PAYLOAD_TYPE_NOTIFY:
IkeNotifyPayload notifyPayload = (IkeNotifyPayload) payload;
if (notifyPayload.isErrorNotify()) {
throw notifyPayload.validateAndBuildIkeException();
} else {
// Unknown and unexpected status notifications are ignored as per
// RFC7296.
logw(
"Received unknown or unexpected status notifications with"
+ " notify type: "
+ notifyPayload.notifyType);
}
break;
default:
logw(
"Received unexpected payload in IKE AUTH response. Payload"
+ " type: "
+ payload.payloadType);
}
}
if (eapPayload == null) {
throw new AuthenticationFailedException("EAP Payload is missing.");
}
mEapAuthenticator.processEapMessage(eapPayload.eapMessage);
} catch (IkeProtocolException exception) {
handleIkeFatalError(exception);
}
}
@Override
protected void handleResponseGenericProcessError(
IkeSaRecord ikeSaRecord, InvalidSyntaxException ikeException) {
mRetransmitter.stopRetransmitting();
handleIkeFatalError(ikeException);
}
private class IkeEapCallback implements IEapCallback {
@Override
public void onSuccess(byte[] msk, byte[] emsk, @Nullable EapInfo eapInfo) {
// Extended MSK not used in IKEv2, drop.
mCreateIkeLocalIkeAuthPostEap.setEapInfo(eapInfo);
sendMessage(CMD_EAP_FINISH_EAP_AUTH, msk);
}
@Override
public void onFail() {
sendMessage(CMD_EAP_FAILED);
}
@Override
public void onResponse(byte[] eapMsg, int flagMask) {
// for now we only check if server is authenticated for EAP-AKA
boolean serverAuthenticated =
EapResult.EapResponse.hasFlag(
flagMask,
EapResult.EapResponse.RESPONSE_FLAG_EAP_AKA_SERVER_AUTHENTICATED);
IkeEapOutboundMsgWrapper msg =
new IkeEapOutboundMsgWrapper(serverAuthenticated, eapMsg);
sendMessage(CMD_EAP_OUTBOUND_MSG_READY, msg);
}
@Override
public void onError(Throwable cause) {
sendMessage(CMD_EAP_ERRORED, cause);
}
}
@Override
public void exitState() {
if (mIsClosing) {
CreateChildSaHelper.releaseSpiResources(mSetupData.firstChildReqList);
}
super.exitState();
}
@Override
protected @IkeMetrics.IkeState int getMetricsStateCode() {
return IkeMetrics.IKE_STATE_IKE_CREATE_LOCAL_IKE_AUTH_IN_EAP;
}
}
/**
* CreateIkeLocalIkeAuthPostEap represents the state when the IKE library is performing the
* post-EAP PSK-base authentication run.
*/
class CreateIkeLocalIkeAuthPostEap
extends CreateIkeLocalIkeAuthFirstAndLastExchangeBase<IkeAuthData> {
private byte[] mEapMsk = new byte[0];
@Override
public boolean processStateMessage(Message msg) {
switch (msg.what) {
case CMD_EAP_FINISH_EAP_AUTH:
mEapMsk = (byte[]) msg.obj;
IkeAuthPskPayload pskPayload =
new IkeAuthPskPayload(
mEapMsk,
mSetupData.ikeInitRequestBytes,
mCurrentIkeSaRecord.nonceResponder,
mSetupData.initIdPayload.getEncodedPayloadBody(),
mIkePrf,
mCurrentIkeSaRecord.getSkPi());
IkeMessage postEapAuthMsg = buildIkeAuthReqMessage(Arrays.asList(pskPayload));
mRetransmitter = new EncryptedRetransmitter(postEapAuthMsg);
return HANDLED;
case CMD_SET_NETWORK:
// Shouldn't be receiving this command before MOBIKE is active - determined with
// last IKE_AUTH response
logWtf("Received SET_NETWORK cmd in " + getCurrentStateName());
return NOT_HANDLED;
default:
return super.processStateMessage(msg);
}
}
@Override
protected void handleResponseIkeMessage(IkeMessage ikeMessage) {
handleIkeAuthResponse(ikeMessage, true /* isServerExpectingMoreEap */);
}
@Override
public MsgValidationResult validateAuthRespAndTakeNextStep(IkeMessage ikeMessage) {
MsgValidationResult validateResult = validateIkeAuthResp(ikeMessage);
if (validateResult.getResult() != MsgValidationResult.RESULT_OK) {
return validateResult;
}
try {
maybeEnableMobility();
} catch (IkeException e) {
MsgValidationResult.newResultInvalidMsg(e);
}
notifyIkeSessionSetup(ikeMessage);
performFirstChildNegotiation(
mSetupData.firstChildReqList, extractChildPayloadsFromMessage(ikeMessage));
return MsgValidationResult.newResultOk();
}
@Override
protected void handleResponseGenericProcessError(
IkeSaRecord ikeSaRecord, InvalidSyntaxException ikeException) {
mRetransmitter.stopRetransmitting();
// Notify the remote because they may have set up the IKE SA.
sendEncryptedIkeMessage(buildIkeDeleteReq(mCurrentIkeSaRecord));
handleIkeFatalError(ikeException);
}
private MsgValidationResult validateIkeAuthResp(IkeMessage authResp) {
IkeAuthPayload authPayload = null;
// Process 3GPP-specific payloads before verifying IKE_AUTH to ensure that the
// caller is informed of them.
List<IkePayload> ike3gppPayloads = null;
try {
ike3gppPayloads =
handle3gppRespAndExtractNonError3gppPayloads(
IKE_EXCHANGE_SUBTYPE_IKE_AUTH, authResp.ikePayloadList);
} catch (InvalidSyntaxException e) {
return MsgValidationResult.newResultInvalidMsg(e);
}
List<IkePayload> payloadsWithout3gpp = new ArrayList<>(authResp.ikePayloadList);
payloadsWithout3gpp.removeAll(ike3gppPayloads);
for (IkePayload payload : payloadsWithout3gpp) {
switch (payload.payloadType) {
case IkePayload.PAYLOAD_TYPE_AUTH:
authPayload = (IkeAuthPayload) payload;
break;
case IkePayload.PAYLOAD_TYPE_NOTIFY:
MsgValidationResult result =
handleNotifyInLastAuthResp(
(IkeNotifyPayload) payload,
authResp.getPayloadForType(
PAYLOAD_TYPE_AUTH, IkeAuthPayload.class));
if (result.getResult() != MsgValidationResult.RESULT_OK) {
return result;
}
break;
case PAYLOAD_TYPE_SA: // Will be handled separately; fall through
case PAYLOAD_TYPE_CP: // Will be handled separately; fall through
case PAYLOAD_TYPE_TS_INITIATOR: // Will be handled separately; fall through
case PAYLOAD_TYPE_TS_RESPONDER: // Will be handled separately; fall through
break;
default:
logw(
"Received unexpected payload in IKE AUTH response. Payload"
+ " type: "
+ payload.payloadType);
}
}
// Verify existence of payloads
if (authPayload == null) {
return MsgValidationResult.newResultInvalidMsg(
new AuthenticationFailedException("Post-EAP Auth payload missing."));
}
try {
authenticatePsk(mEapMsk, authPayload, mSetupData.respIdPayload);
return MsgValidationResult.newResultOk();
} catch (AuthenticationFailedException e) {
return MsgValidationResult.newResultInvalidMsg(e);
}
}
@Override
public void exitState() {
if (mIsClosing) {
CreateChildSaHelper.releaseSpiResources(mSetupData.firstChildReqList);
}
super.exitState();
}
@Override
protected @IkeMetrics.IkeState int getMetricsStateCode() {
return IkeMetrics.IKE_STATE_IKE_CREATE_LOCAL_IKE_AUTH_POST_EAP;
}
}
private abstract class RekeyIkeHandlerBase extends DeleteBase {
private void validateIkeRekeyCommon(IkeMessage ikeMessage) throws InvalidSyntaxException {
boolean hasSaPayload = false;
boolean hasKePayload = false;
boolean hasNoncePayload = false;
for (IkePayload payload : ikeMessage.ikePayloadList) {
switch (payload.payloadType) {
case IkePayload.PAYLOAD_TYPE_SA:
hasSaPayload = true;
break;
case IkePayload.PAYLOAD_TYPE_KE:
hasKePayload = true;
break;
case IkePayload.PAYLOAD_TYPE_NONCE:
hasNoncePayload = true;
break;
case IkePayload.PAYLOAD_TYPE_VENDOR:
// Vendor payloads allowed, but not verified
break;
case IkePayload.PAYLOAD_TYPE_NOTIFY:
// Notification payloads allowed, but left to handler methods to process.
break;
default:
logw(
"Received unexpected payload in IKE REKEY request. Payload type: "
+ payload.payloadType);
}
}
if (!hasSaPayload || !hasKePayload || !hasNoncePayload) {
throw new InvalidSyntaxException("SA, KE or Nonce payload missing.");
}
}
@VisibleForTesting
void validateIkeRekeyReq(IkeMessage ikeMessage) throws InvalidSyntaxException {
// Skip validation of exchange type since it has been done during decoding request.
List<IkeNotifyPayload> notificationPayloads =
ikeMessage.getPayloadListForType(
IkePayload.PAYLOAD_TYPE_NOTIFY, IkeNotifyPayload.class);
for (IkeNotifyPayload notifyPayload : notificationPayloads) {
if (notifyPayload.isErrorNotify()) {
logw("Error notifications invalid in request: " + notifyPayload.notifyType);
}
}
validateIkeRekeyCommon(ikeMessage);
}
@VisibleForTesting
void validateIkeRekeyResp(IkeMessage reqMsg, IkeMessage respMsg)
throws InvalidSyntaxException {
int exchangeType = respMsg.ikeHeader.exchangeType;
if (exchangeType != IkeHeader.EXCHANGE_TYPE_CREATE_CHILD_SA
&& exchangeType != IkeHeader.EXCHANGE_TYPE_INFORMATIONAL) {
throw new InvalidSyntaxException(
"Expected Rekey response (CREATE_CHILD_SA or INFORMATIONAL) but received: "
+ exchangeType);
}
List<IkeNotifyPayload> notificationPayloads =
respMsg.getPayloadListForType(
IkePayload.PAYLOAD_TYPE_NOTIFY, IkeNotifyPayload.class);
for (IkeNotifyPayload notifyPayload : notificationPayloads) {
if (notifyPayload.isErrorNotify()) {
// Error notifications found. Stop validation for SA negotiation.
return;
}
}
validateIkeRekeyCommon(respMsg);
// Verify DH groups matching
IkeKePayload reqKePayload =
reqMsg.getPayloadForType(IkePayload.PAYLOAD_TYPE_KE, IkeKePayload.class);
IkeKePayload respKePayload =
respMsg.getPayloadForType(IkePayload.PAYLOAD_TYPE_KE, IkeKePayload.class);
if (reqKePayload.dhGroup != respKePayload.dhGroup) {
throw new InvalidSyntaxException("Received KE payload with mismatched DH group.");
}
}
// It doesn't make sense to include multiple error notify payloads in one response. If it
// happens, IKE Session will only handle the most severe one.
protected boolean handleErrorNotifyIfExists(IkeMessage respMsg, boolean isSimulRekey) {
IkeNotifyPayload invalidSyntaxNotifyPayload = null;
IkeNotifyPayload tempFailureNotifyPayload = null;
IkeNotifyPayload firstErrorNotifyPayload = null;
List<IkeNotifyPayload> notificationPayloads =
respMsg.getPayloadListForType(
IkePayload.PAYLOAD_TYPE_NOTIFY, IkeNotifyPayload.class);
for (IkeNotifyPayload notifyPayload : notificationPayloads) {
if (!notifyPayload.isErrorNotify()) continue;
if (firstErrorNotifyPayload == null) firstErrorNotifyPayload = notifyPayload;
if (ERROR_TYPE_INVALID_SYNTAX == notifyPayload.notifyType) {
invalidSyntaxNotifyPayload = notifyPayload;
} else if (ERROR_TYPE_TEMPORARY_FAILURE == notifyPayload.notifyType) {
tempFailureNotifyPayload = notifyPayload;
}
}
// No error Notify Payload included in this response.
if (firstErrorNotifyPayload == null) return NOT_HANDLED;
// Handle Invalid Syntax if it exists
if (invalidSyntaxNotifyPayload != null) {
try {
IkeProtocolException exception =
invalidSyntaxNotifyPayload.validateAndBuildIkeException();
handleIkeFatalError(exception);
} catch (InvalidSyntaxException e) {
// Error notify payload has invalid syntax
handleIkeFatalError(e);
}
return HANDLED;
}
if (tempFailureNotifyPayload != null) {
// Handle Temporary Failure if exists
loge("Received TEMPORARY_FAILURE for rekey IKE. Already handled during decoding.");
} else {
// Handle other errors
loge(
"Received error notification: "
+ firstErrorNotifyPayload.notifyType
+ " for rekey IKE. Schedule a retry");
if (!isSimulRekey) {
mCurrentIkeSaRecord.rescheduleRekey(RETRY_INTERVAL_MS);
}
}
if (isSimulRekey) {
transitionTo(mRekeyIkeRemoteDelete);
} else {
transitionTo(mIdle);
}
return HANDLED;
}
protected IkeSaRecord validateAndBuildIkeSa(
IkeMessage reqMsg, IkeMessage respMessage, boolean isLocalInit)
throws IkeProtocolException, GeneralSecurityException, IOException {
InetAddress initAddr =
isLocalInit
? mIkeConnectionCtrl.getLocalAddress()
: mIkeConnectionCtrl.getRemoteAddress();
InetAddress respAddr =
isLocalInit
? mIkeConnectionCtrl.getRemoteAddress()
: mIkeConnectionCtrl.getLocalAddress();
Pair<IkeProposal, IkeProposal> negotiatedProposals = null;
try {
IkeSaPayload reqSaPayload =
reqMsg.getPayloadForType(IkePayload.PAYLOAD_TYPE_SA, IkeSaPayload.class);
IkeSaPayload respSaPayload =
respMessage.getPayloadForType(
IkePayload.PAYLOAD_TYPE_SA, IkeSaPayload.class);
// Throw exception or return valid negotiated proposal with allocated SPIs
negotiatedProposals =
IkeSaPayload.getVerifiedNegotiatedIkeProposalPair(
reqSaPayload,
respSaPayload,
mIkeSpiGenerator,
mIkeConnectionCtrl.getRemoteAddress());
IkeProposal reqProposal = negotiatedProposals.first;
IkeProposal respProposal = negotiatedProposals.second;
IkeMacPrf newPrf;
IkeCipher newCipher;
IkeMacIntegrity newIntegrity = null;
newCipher = IkeCipher.create(respProposal.saProposal.getEncryptionTransforms()[0]);
if (!newCipher.isAead()) {
newIntegrity =
IkeMacIntegrity.create(
respProposal.saProposal.getIntegrityTransforms()[0]);
}
newPrf = IkeMacPrf.create(respProposal.saProposal.getPrfTransforms()[0]);
// Build new SaRecord
long remoteSpi =
isLocalInit
? respProposal.getIkeSpiResource().getSpi()
: reqProposal.getIkeSpiResource().getSpi();
IkeSaRecord newSaRecord =
IkeSaRecord.makeRekeyedIkeSaRecord(
mCurrentIkeSaRecord,
mIkePrf,
reqMsg,
respMessage,
reqProposal.getIkeSpiResource(),
respProposal.getIkeSpiResource(),
newPrf,
newIntegrity == null ? 0 : newIntegrity.getKeyLength(),
newCipher.getKeyLength(),
isLocalInit,
buildSaLifetimeAlarmScheduler(remoteSpi));
addIkeSaRecord(newSaRecord);
mIkeCipher = newCipher;
mIkePrf = newPrf;
mIkeIntegrity = newIntegrity;
return newSaRecord;
} catch (IkeProtocolException | GeneralSecurityException | IOException e) {
if (negotiatedProposals != null) {
negotiatedProposals.first.getIkeSpiResource().close();
negotiatedProposals.second.getIkeSpiResource().close();
}
throw e;
}
}
}
/** RekeyIkeLocalCreate represents state when IKE library initiates Rekey IKE exchange. */
class RekeyIkeLocalCreate extends RekeyIkeHandlerBase {
@Override
public void enterState() {
try {
mRetransmitter = new EncryptedRetransmitter(buildIkeRekeyReq());
} catch (IOException e) {
loge("Fail to assign IKE SPI for rekey. Schedule a retry.", e);
mCurrentIkeSaRecord.rescheduleRekey(RETRY_INTERVAL_MS);
transitionTo(mIdle);
}
}
@Override
protected void triggerRetransmit() {
mRetransmitter.retransmit();
}
@Override
protected void handleTempFailure() {
mTempFailHandler.handleTempFailure();
mCurrentIkeSaRecord.rescheduleRekey(RETRY_INTERVAL_MS);
}
/**
* Builds a IKE Rekey request, reusing the current proposal
*
* <p>As per RFC 7296, rekey messages are of format: { HDR { SK { SA, Ni, KEi } } }
*
* <p>This method currently reuses agreed upon proposal.
*/
private IkeMessage buildIkeRekeyReq() throws IOException {
// TODO: Evaluate if we need to support different proposals for rekeys
IkeSaProposal[] saProposals = new IkeSaProposal[] {mSaProposal};
// No need to allocate SPIs; they will be allocated as part of the
// getRekeyIkeSaRequestPayloads
List<IkePayload> payloadList =
CreateIkeSaHelper.getRekeyIkeSaRequestPayloads(
saProposals,
mIkeSpiGenerator,
mIkeConnectionCtrl.getLocalAddress(),
mIkeContext.getRandomnessFactory());
// Build IKE header
IkeHeader ikeHeader =
new IkeHeader(
mCurrentIkeSaRecord.getInitiatorSpi(),
mCurrentIkeSaRecord.getResponderSpi(),
IkePayload.PAYLOAD_TYPE_SK,
IkeHeader.EXCHANGE_TYPE_CREATE_CHILD_SA,
false /*isResponseMsg*/,
mCurrentIkeSaRecord.isLocalInit,
mCurrentIkeSaRecord.getLocalRequestMessageId());
return new IkeMessage(ikeHeader, payloadList);
}
@Override
protected void handleRequestIkeMessage(
IkeMessage ikeMessage, int ikeExchangeSubType, Message message) {
switch (ikeExchangeSubType) {
case IKE_EXCHANGE_SUBTYPE_DELETE_IKE:
handleDeleteSessionRequest(ikeMessage);
break;
case IKE_EXCHANGE_SUBTYPE_GENERIC_INFO:
handleGenericInfoRequest(ikeMessage);
break;
default:
// TODO: Implement simultaneous rekey
buildAndSendErrorNotificationResponse(
mCurrentIkeSaRecord,
ikeMessage.ikeHeader.messageId,
ERROR_TYPE_TEMPORARY_FAILURE);
}
}
@Override
protected void handleResponseIkeMessage(IkeMessage ikeMessage) {
try {
// Validate syntax
validateIkeRekeyResp(mRetransmitter.getMessage(), ikeMessage);
// Handle error notifications if they exist
if (handleErrorNotifyIfExists(ikeMessage, false /*isSimulRekey*/) == NOT_HANDLED) {
// No error notifications included. Negotiate new SA
mLocalInitNewIkeSaRecord =
validateAndBuildIkeSa(
mRetransmitter.getMessage(), ikeMessage, true /*isLocalInit*/);
transitionTo(mRekeyIkeLocalDelete);
}
// Stop retransmissions
mRetransmitter.stopRetransmitting();
} catch (IkeProtocolException e) {
if (e instanceof InvalidSyntaxException) {
handleProcessRespOrSaCreationFailureAndQuit(e);
} else {
handleProcessRespOrSaCreationFailureAndQuit(
new InvalidSyntaxException(
"Error in processing IKE Rekey-Create response", e));
}
} catch (GeneralSecurityException | IOException e) {
handleProcessRespOrSaCreationFailureAndQuit(wrapAsIkeException(e));
}
}
@Override
protected void handleResponseGenericProcessError(
IkeSaRecord ikeSaRecord, InvalidSyntaxException ikeException) {
handleProcessRespOrSaCreationFailureAndQuit(ikeException);
}
private void handleProcessRespOrSaCreationFailureAndQuit(IkeException exception) {
// We don't retry rekey if failure was caused by invalid response or SA creation error.
// Reason is there is no way to notify the remote side the old SA is still alive but the
// new one has failed.
mRetransmitter.stopRetransmitting();
sendEncryptedIkeMessage(buildIkeDeleteReq(mCurrentIkeSaRecord));
handleIkeFatalError(exception);
}
@Override
protected @IkeMetrics.IkeState int getMetricsStateCode() {
return IkeMetrics.IKE_STATE_IKE_REKEY_LOCAL_CREATE;
}
}
/**
* SimulRekeyIkeLocalCreate represents the state where IKE library has replied to rekey request
* sent from the remote and is waiting for a rekey response for a locally initiated rekey
* request.
*
* <p>SimulRekeyIkeLocalCreate extends RekeyIkeLocalCreate so that it can call super class to
* validate incoming rekey response against locally initiated rekey request.
*/
class SimulRekeyIkeLocalCreate extends RekeyIkeLocalCreate {
@Override
public void enterState() {
mRetransmitter = new EncryptedRetransmitter(null);
// TODO: Populate super.mRetransmitter from state initialization data
// Do not send request.
}
public IkeMessage buildRequest() {
throw new UnsupportedOperationException(
"Do not support sending request in " + getCurrentStateName());
}
@Override
public void exitState() {
// Do nothing.
}
@Override
public boolean processStateMessage(Message message) {
switch (message.what) {
case CMD_RECEIVE_IKE_PACKET:
ReceivedIkePacket receivedIkePacket = (ReceivedIkePacket) message.obj;
IkeHeader ikeHeader = receivedIkePacket.ikeHeader;
if (mRemoteInitNewIkeSaRecord == getIkeSaRecordForPacket(ikeHeader)) {
deferMessage(message);
} else {
handleReceivedIkePacket(message);
}
return HANDLED;
default:
return super.processStateMessage(message);
}
}
@Override
protected void handleRequestIkeMessage(
IkeMessage ikeMessage, int ikeExchangeSubType, Message message) {
switch (ikeExchangeSubType) {
case IKE_EXCHANGE_SUBTYPE_DELETE_IKE:
deferMessage(message);
return;
default:
// TODO: Add more cases for other types of request.
}
}
@Override
protected void handleResponseIkeMessage(IkeMessage ikeMessage) {
try {
validateIkeRekeyResp(mRetransmitter.getMessage(), ikeMessage);
// TODO: Check and handle error notifications before SA negotiation
mLocalInitNewIkeSaRecord =
validateAndBuildIkeSa(
mRetransmitter.getMessage(), ikeMessage, true /*isLocalInit*/);
transitionTo(mSimulRekeyIkeLocalDeleteRemoteDelete);
} catch (IkeProtocolException e) {
// TODO: Handle processing errors.
} catch (GeneralSecurityException e) {
// TODO: Fatal - kill session.
} catch (IOException e) {
// TODO: SPI allocation collided - delete new IKE SA, retry rekey.
}
}
@Override
protected @IkeMetrics.IkeState int getMetricsStateCode() {
return IkeMetrics.IKE_STATE_IKE_SIMULTANEOUS_REKEY_LOCAL_CREATE;
}
}
/** RekeyIkeDeleteBase represents common behaviours of deleting stage during rekeying IKE SA. */
private abstract class RekeyIkeDeleteBase extends DeleteBase {
@Override
public boolean processStateMessage(Message message) {
switch (message.what) {
case CMD_RECEIVE_IKE_PACKET:
ReceivedIkePacket receivedIkePacket = (ReceivedIkePacket) message.obj;
IkeHeader ikeHeader = receivedIkePacket.ikeHeader;
// Verify that this message is correctly authenticated and encrypted:
IkeSaRecord ikeSaRecord = getIkeSaRecordForPacket(ikeHeader);
boolean isMessageOnNewSa = false;
if (ikeSaRecord != null && mIkeSaRecordSurviving == ikeSaRecord) {
DecodeResult decodeResult =
IkeMessage.decode(
ikeHeader.isResponseMsg
? ikeSaRecord.getLocalRequestMessageId()
: ikeSaRecord.getRemoteRequestMessageId(),
mIkeIntegrity,
mIkeCipher,
ikeSaRecord,
ikeHeader,
receivedIkePacket.ikePacketBytes,
ikeSaRecord.getCollectedFragments(ikeHeader.isResponseMsg));
isMessageOnNewSa =
(decodeResult.status == DECODE_STATUS_PROTECTED_ERROR)
|| (decodeResult.status == DECODE_STATUS_OK)
|| (decodeResult.status == DECODE_STATUS_PARTIAL);
}
// Authenticated request received on the new/surviving SA; treat it as
// an acknowledgement that the remote has successfully rekeyed.
if (isMessageOnNewSa) {
State nextState = mIdle;
// This is the first IkeMessage seen on the new SA. It cannot be a response.
// Likewise, if it a request, it must not be a retransmission. Verify msgId.
// If either condition happens, consider rekey a success, but immediately
// kill the session.
if (ikeHeader.isResponseMsg
|| ikeSaRecord.getRemoteRequestMessageId() - ikeHeader.messageId
!= 0) {
nextState = mDeleteIkeLocalDelete;
} else {
deferMessage(message);
}
// Locally close old (and losing) IKE SAs. As a result of not waiting for
// delete responses, the old SA can be left in a state where the stored ID
// is no longer correct. However, this finishRekey() call will remove that
// SA, so it doesn't matter.
finishRekey();
transitionTo(nextState);
} else {
handleReceivedIkePacket(message);
}
return HANDLED;
default:
return super.processStateMessage(message);
// TODO: Add more cases for other packet types.
}
}
// Rekey timer for old (and losing) SAs will be cancelled as part of the closing of the SA.
protected void finishRekey() {
mCurrentIkeSaRecord = mIkeSaRecordSurviving;
mLocalInitNewIkeSaRecord = null;
mRemoteInitNewIkeSaRecord = null;
mIkeSaRecordSurviving = null;
if (mIkeSaRecordAwaitingLocalDel != null) {
removeIkeSaRecord(mIkeSaRecordAwaitingLocalDel);
mIkeSaRecordAwaitingLocalDel.close();
mIkeSaRecordAwaitingLocalDel = null;
}
if (mIkeSaRecordAwaitingRemoteDel != null) {
removeIkeSaRecord(mIkeSaRecordAwaitingRemoteDel);
mIkeSaRecordAwaitingRemoteDel.close();
mIkeSaRecordAwaitingRemoteDel = null;
}
synchronized (mChildCbToSessions) {
for (ChildSessionStateMachine child : mChildCbToSessions.values()) {
child.setSkD(mCurrentIkeSaRecord.getSkD());
}
}
// TODO: Update prf of all child sessions
}
}
/**
* SimulRekeyIkeLocalDeleteRemoteDelete represents the deleting stage during simultaneous
* rekeying when IKE library is waiting for both a Delete request and a Delete response.
*/
class SimulRekeyIkeLocalDeleteRemoteDelete extends RekeyIkeDeleteBase {
@Override
public void enterState() {
// Detemine surviving IKE SA. According to RFC 7296: "The new IKE SA containing the
// lowest nonce SHOULD be deleted by the node that created it, and the other surviving
// new IKE SA MUST inherit all the Child SAs."
if (mLocalInitNewIkeSaRecord.compareTo(mRemoteInitNewIkeSaRecord) > 0) {
mIkeSaRecordSurviving = mLocalInitNewIkeSaRecord;
mIkeSaRecordAwaitingLocalDel = mCurrentIkeSaRecord;
mIkeSaRecordAwaitingRemoteDel = mRemoteInitNewIkeSaRecord;
} else {
mIkeSaRecordSurviving = mRemoteInitNewIkeSaRecord;
mIkeSaRecordAwaitingLocalDel = mLocalInitNewIkeSaRecord;
mIkeSaRecordAwaitingRemoteDel = mCurrentIkeSaRecord;
}
mRetransmitter =
new EncryptedRetransmitter(
mIkeSaRecordAwaitingLocalDel,
buildIkeDeleteReq(mIkeSaRecordAwaitingLocalDel));
// TODO: Set timer awaiting for delete request.
}
@Override
protected void triggerRetransmit() {
mRetransmitter.retransmit();
}
@Override
protected void handleRequestIkeMessage(
IkeMessage ikeMessage, int ikeExchangeSubType, Message message) {
IkeSaRecord ikeSaRecordForPacket = getIkeSaRecordForPacket(ikeMessage.ikeHeader);
switch (ikeExchangeSubType) {
case IKE_EXCHANGE_SUBTYPE_DELETE_IKE:
try {
validateIkeDeleteReq(ikeMessage, mIkeSaRecordAwaitingRemoteDel);
IkeMessage respMsg =
buildIkeDeleteResp(ikeMessage, mIkeSaRecordAwaitingRemoteDel);
removeIkeSaRecord(mIkeSaRecordAwaitingRemoteDel);
// TODO: Encode and send response and close
// mIkeSaRecordAwaitingRemoteDel.
// TODO: Stop timer awating delete request.
transitionTo(mSimulRekeyIkeLocalDelete);
} catch (InvalidSyntaxException e) {
logd("Validation failed for delete request", e);
// TODO: Shutdown - fatal error
}
return;
default:
// TODO: Reply with TEMPORARY_FAILURE
}
}
@Override
protected void handleResponseIkeMessage(IkeMessage ikeMessage) {
try {
validateIkeDeleteResp(ikeMessage, mIkeSaRecordAwaitingLocalDel);
finishDeleteIkeSaAwaitingLocalDel();
} catch (InvalidSyntaxException e) {
loge("Invalid syntax on IKE Delete response. Shutting down anyways", e);
finishDeleteIkeSaAwaitingLocalDel();
} catch (IllegalStateException e) {
// Response received on incorrect SA
cleanUpAndQuit(e);
}
}
@Override
protected void handleResponseGenericProcessError(
IkeSaRecord ikeSaRecord, InvalidSyntaxException exception) {
if (mIkeSaRecordAwaitingLocalDel == ikeSaRecord) {
loge("Invalid syntax on IKE Delete response. Shutting down anyways", exception);
finishDeleteIkeSaAwaitingLocalDel();
} else {
cleanUpAndQuit(
new IllegalStateException("Delete response received on incorrect SA"));
}
}
private void finishDeleteIkeSaAwaitingLocalDel() {
mRetransmitter.stopRetransmitting();
removeIkeSaRecord(mIkeSaRecordAwaitingLocalDel);
mIkeSaRecordAwaitingLocalDel.close();
mIkeSaRecordAwaitingLocalDel = null;
transitionTo(mSimulRekeyIkeRemoteDelete);
}
@Override
public void exitState() {
finishRekey();
mRetransmitter.stopRetransmitting();
// TODO: Stop awaiting delete request timer.
}
@Override
protected @IkeMetrics.IkeState int getMetricsStateCode() {
return IkeMetrics.IKE_STATE_IKE_SIMULTANEOUS_REKEY_LOCAL_DELETE_REMOTE_DELETE;
}
}
/**
* SimulRekeyIkeLocalDelete represents the state when IKE library is waiting for a Delete
* response during simultaneous rekeying.
*/
class SimulRekeyIkeLocalDelete extends RekeyIkeDeleteBase {
@Override
public void enterState() {
mRetransmitter = new EncryptedRetransmitter(mIkeSaRecordAwaitingLocalDel, null);
// TODO: Populate mRetransmitter from state initialization data.
}
@Override
protected void triggerRetransmit() {
mRetransmitter.retransmit();
}
@Override
protected void handleRequestIkeMessage(
IkeMessage ikeMessage, int ikeExchangeSubType, Message message) {
// Always return a TEMPORARY_FAILURE. In no case should we accept a message on an SA
// that is going away. All messages on the new SA is caught in RekeyIkeDeleteBase
buildAndSendErrorNotificationResponse(
mIkeSaRecordAwaitingLocalDel,
ikeMessage.ikeHeader.messageId,
ERROR_TYPE_TEMPORARY_FAILURE);
}
@Override
protected void handleResponseIkeMessage(IkeMessage ikeMessage) {
try {
validateIkeDeleteResp(ikeMessage, mIkeSaRecordAwaitingLocalDel);
finishRekey();
transitionTo(mIdle);
} catch (InvalidSyntaxException e) {
loge(
"Invalid syntax on IKE Delete response. Shutting down old IKE SA and"
+ " finishing rekey",
e);
finishRekey();
transitionTo(mIdle);
} catch (IllegalStateException e) {
// Response received on incorrect SA
cleanUpAndQuit(e);
}
}
@Override
protected void handleResponseGenericProcessError(
IkeSaRecord ikeSaRecord, InvalidSyntaxException exception) {
if (mIkeSaRecordAwaitingLocalDel == ikeSaRecord) {
loge(
"Invalid syntax on IKE Delete response. Shutting down old IKE SA and"
+ " finishing rekey",
exception);
finishRekey();
transitionTo(mIdle);
} else {
cleanUpAndQuit(
new IllegalStateException("Delete response received on incorrect SA"));
}
}
@Override
protected @IkeMetrics.IkeState int getMetricsStateCode() {
return IkeMetrics.IKE_STATE_IKE_SIMULTANEOUS_REKEY_LOCAL_DELETE;
}
}
/**
* SimulRekeyIkeRemoteDelete represents the state that waiting for a Delete request during
* simultaneous rekeying.
*/
class SimulRekeyIkeRemoteDelete extends RekeyIkeDeleteBase {
@Override
protected void handleRequestIkeMessage(
IkeMessage ikeMessage, int ikeExchangeSubType, Message message) {
// At this point, the incoming request can ONLY be on mIkeSaRecordAwaitingRemoteDel - if
// it was on the surviving SA, it is deferred and the rekey is finished. It is likewise
// impossible to have this on the local-deleted SA, since the delete has already been
// acknowledged in the SimulRekeyIkeLocalDeleteRemoteDelete state.
switch (ikeExchangeSubType) {
case IKE_EXCHANGE_SUBTYPE_DELETE_IKE:
try {
validateIkeDeleteReq(ikeMessage, mIkeSaRecordAwaitingRemoteDel);
IkeMessage respMsg =
buildIkeDeleteResp(ikeMessage, mIkeSaRecordAwaitingRemoteDel);
sendEncryptedIkeMessage(mIkeSaRecordAwaitingRemoteDel, respMsg);
finishRekey();
transitionTo(mIdle);
} catch (InvalidSyntaxException e) {
// Program error.
cleanUpAndQuit(new IllegalStateException(e));
}
return;
default:
buildAndSendErrorNotificationResponse(
mIkeSaRecordAwaitingRemoteDel,
ikeMessage.ikeHeader.messageId,
ERROR_TYPE_TEMPORARY_FAILURE);
}
}
@Override
protected @IkeMetrics.IkeState int getMetricsStateCode() {
return IkeMetrics.IKE_STATE_IKE_SIMULTANEOUS_REKEY_REMOTE_DELETE;
}
}
/**
* RekeyIkeLocalDelete represents the deleting stage when IKE library is initiating a Rekey
* procedure.
*
* <p>RekeyIkeLocalDelete and SimulRekeyIkeLocalDelete have same behaviours in
* processStateMessage(). While RekeyIkeLocalDelete overrides enterState() and exitState()
* methods for initiating and finishing the deleting stage for IKE rekeying.
*/
class RekeyIkeLocalDelete extends SimulRekeyIkeLocalDelete {
@Override
public void enterState() {
mIkeSaRecordSurviving = mLocalInitNewIkeSaRecord;
mIkeSaRecordAwaitingLocalDel = mCurrentIkeSaRecord;
mRetransmitter =
new EncryptedRetransmitter(
mIkeSaRecordAwaitingLocalDel,
buildIkeDeleteReq(mIkeSaRecordAwaitingLocalDel));
}
@Override
protected void triggerRetransmit() {
mRetransmitter.retransmit();
}
@Override
public void exitState() {
mRetransmitter.stopRetransmitting();
}
@Override
protected @IkeMetrics.IkeState int getMetricsStateCode() {
return IkeMetrics.IKE_STATE_IKE_REKEY_LOCAL_DELETE;
}
}
/**
* RekeyIkeRemoteDelete represents the deleting stage when responding to a Rekey procedure.
*
* <p>RekeyIkeRemoteDelete and SimulRekeyIkeRemoteDelete have same behaviours in
* processStateMessage(). While RekeyIkeLocalDelete overrides enterState() and exitState()
* methods for waiting incoming delete request and for finishing the deleting stage for IKE
* rekeying.
*/
class RekeyIkeRemoteDelete extends SimulRekeyIkeRemoteDelete {
@Override
public void enterState() {
mIkeSaRecordSurviving = mRemoteInitNewIkeSaRecord;
mIkeSaRecordAwaitingRemoteDel = mCurrentIkeSaRecord;
sendMessageDelayed(TIMEOUT_REKEY_REMOTE_DELETE, REKEY_DELETE_TIMEOUT_MS);
}
@Override
public boolean processStateMessage(Message message) {
// Intercept rekey delete timeout. Assume rekey succeeded since no retransmissions
// were received.
if (message.what == TIMEOUT_REKEY_REMOTE_DELETE) {
finishRekey();
transitionTo(mIdle);
return HANDLED;
} else {
return super.processStateMessage(message);
}
}
@Override
public void exitState() {
removeMessages(TIMEOUT_REKEY_REMOTE_DELETE);
}
@Override
protected @IkeMetrics.IkeState int getMetricsStateCode() {
return IkeMetrics.IKE_STATE_IKE_REKEY_REMOTE_DELETE;
}
}
/** DeleteIkeLocalDelete initiates a deletion request of the current IKE Session. */
class DeleteIkeLocalDelete extends DeleteBase {
@Override
public void enterState() {
mRetransmitter = new EncryptedRetransmitter(buildIkeDeleteReq(mCurrentIkeSaRecord));
}
@Override
protected void triggerRetransmit() {
mRetransmitter.retransmit();
}
@Override
protected void handleRequestIkeMessage(
IkeMessage ikeMessage, int ikeExchangeSubType, Message message) {
switch (ikeExchangeSubType) {
case IKE_EXCHANGE_SUBTYPE_DELETE_IKE:
handleDeleteSessionRequest(ikeMessage);
return;
default:
buildAndSendErrorNotificationResponse(
mCurrentIkeSaRecord,
ikeMessage.ikeHeader.messageId,
ERROR_TYPE_TEMPORARY_FAILURE);
}
}
@Override
protected void handleResponseIkeMessage(IkeMessage ikeMessage) {
try {
validateIkeDeleteResp(ikeMessage, mCurrentIkeSaRecord);
executeUserCallback(
() -> {
mIkeSessionCallback.onClosed();
});
removeIkeSaRecord(mCurrentIkeSaRecord);
mCurrentIkeSaRecord.close();
mCurrentIkeSaRecord = null;
recordMetricsEvent_sessionTerminated(null);
quitSessionNow();
} catch (InvalidSyntaxException e) {
handleResponseGenericProcessError(mCurrentIkeSaRecord, e);
}
}
@Override
protected void handleResponseGenericProcessError(
IkeSaRecord ikeSaRecord, InvalidSyntaxException exception) {
loge("Invalid syntax on IKE Delete response. Shutting down anyways", exception);
handleIkeFatalError(exception);
quitSessionNow();
}
@Override
public void exitState() {
mRetransmitter.stopRetransmitting();
}
@Override
protected @IkeMetrics.IkeState int getMetricsStateCode() {
return IkeMetrics.IKE_STATE_IKE_DELETE_LOCAL_DELETE;
}
}
/** DpdIkeLocalInfo initiates a dead peer detection for IKE Session. */
class DpdIkeLocalInfo extends DeleteBase {
@Override
public void enterState() {
mRetransmitter =
new EncryptedRetransmitter(
mCurrentIkeSaRecord,
buildEncryptedInformationalMessage(
new IkeInformationalPayload[0],
false /*isResp*/,
mCurrentIkeSaRecord.getLocalRequestMessageId()),
getRetransmissionTimeoutsMillis());
}
protected int[] getRetransmissionTimeoutsMillis() {
return mIkeSessionParams.getRetransmissionTimeoutsMillis();
}
@Override
protected void triggerRetransmit() {
mRetransmitter.retransmit();
}
@Override
protected void handleRequestIkeMessage(
IkeMessage ikeMessage, int ikeExchangeSubType, Message message) {
switch (ikeExchangeSubType) {
case IKE_EXCHANGE_SUBTYPE_GENERIC_INFO:
handleGenericInfoRequest(ikeMessage);
return;
case IKE_EXCHANGE_SUBTYPE_DELETE_IKE:
// Reply and close IKE
handleDeleteSessionRequest(ikeMessage);
return;
default:
// Reply and stay in current state
buildAndSendErrorNotificationResponse(
mCurrentIkeSaRecord,
ikeMessage.ikeHeader.messageId,
ERROR_TYPE_TEMPORARY_FAILURE);
return;
}
}
@Override
protected void handleResponseIkeMessage(IkeMessage ikeMessage) {
// DPD response usually contains no payload. But since there is not any requirement of
// it, payload validation will be skipped.
if (ikeMessage.ikeHeader.exchangeType == IkeHeader.EXCHANGE_TYPE_INFORMATIONAL) {
mLivenessAssister.markPeerAsAlive();
transitionTo(mIdle);
return;
}
handleResponseGenericProcessError(
mCurrentIkeSaRecord,
new InvalidSyntaxException(
"Invalid exchange type; expected INFORMATIONAL, but got: "
+ ikeMessage.ikeHeader.exchangeType));
}
@Override
protected void handleResponseGenericProcessError(
IkeSaRecord ikeSaRecord, InvalidSyntaxException exception) {
loge("Invalid syntax on IKE DPD response.", exception);
handleIkeFatalError(exception);
// #exitState will be called when StateMachine quits
quitSessionNow();
}
@Override
public void exitState() {
mRetransmitter.stopRetransmitting();
}
@Override
protected @IkeMetrics.IkeState int getMetricsStateCode() {
return IkeMetrics.IKE_STATE_IKE_DPD_LOCAL_INFO;
}
}
/**
* DpdOnDemandIkeLocalInfo extends DpdIkeLocalInfo to initiate dead peer detection by using more
* aggressive retransmission timeouts for IKE sessions requested by the client.
*/
class DpdOnDemandIkeLocalInfo extends DpdIkeLocalInfo {
@Override
protected int[] getRetransmissionTimeoutsMillis() {
return mIkeSessionParams.getLivenessRetransmissionTimeoutsMillis();
}
@Override
protected @IkeMetrics.IkeState int getMetricsStateCode() {
return IkeMetrics.IKE_STATE_IKE_DPD_ON_DEMAND_LOCAL_INFO;
}
}
/**
* MobikeLocalInfo handles mobility event for the IKE Session.
*
* <p>When MOBIKE is supported by both sides, MobikeLocalInfo will initiate an
* UPDATE_SA_ADDRESSES exchange for the IKE Session.
*/
class MobikeLocalInfo extends DeleteBase {
@Override
public void enterState() {
if (!mEnabledExtensions.contains(EXTENSION_TYPE_MOBIKE)) {
logd(
"Non-MOBIKE mobility event: Server does not send"
+ " NOTIFY_TYPE_MOBIKE_SUPPORTED. Skip UPDATE_SA_ADDRESSES exchange");
migrateAllChildSAs(false /* mobikeEnabled */);
notifyConnectionInfoChanged();
transitionTo(mIdle);
return;
}
logd("RFC4555 MOBIKE mobility event: Perform UPDATE_SA_ADDRESSES exchange");
mRetransmitter = new EncryptedRetransmitter(buildUpdateSaAddressesReq());
}
private boolean needNatDetection() {
if (mIkeConnectionCtrl.getRemoteAddress() instanceof Inet4Address) {
// Add NAT_DETECTION payloads when it is unknown if server supports NAT-T or not, or
// it is known that server supports NAT-T.
return mIkeConnectionCtrl.getNatStatus() == NAT_TRAVERSAL_SUPPORT_NOT_CHECKED
|| mIkeConnectionCtrl.getNatStatus() != NAT_TRAVERSAL_UNSUPPORTED;
} else {
// Add NAT_DETECTION payloads only when a NAT has been detected previously. This is
// mainly for updating the previous NAT detection result, so that if IKE Session
// migrates from a v4 NAT environment to a v6 non-NAT environment, both sides can
// switch to use non-encap ESP SA. This is especially beneficial for implementations
// that do not support Ipv6 NAT-T.
return mIkeConnectionCtrl.getNatStatus() == NAT_DETECTED;
}
}
private IkeMessage buildUpdateSaAddressesReq() {
// Generics required for addNatDetectionPayloadsToList that takes List<IkePayload> and
// buildEncryptedInformationalMessage that takes InformationalPayload[].
List<? super IkeInformationalPayload> payloadList = new ArrayList<>();
payloadList.add(new IkeNotifyPayload(NOTIFY_TYPE_UPDATE_SA_ADDRESSES));
if (needNatDetection()) {
addNatDetectionPayloadsToList(
(List<IkePayload>) payloadList,
mIkeConnectionCtrl.getLocalAddress(),
mIkeConnectionCtrl.getRemoteAddress(),
mIkeConnectionCtrl.getLocalPort(),
mIkeConnectionCtrl.getRemotePort(),
mCurrentIkeSaRecord.getInitiatorSpi(),
mCurrentIkeSaRecord.getResponderSpi(),
needEnableForceUdpEncap());
}
return buildEncryptedInformationalMessage(
mCurrentIkeSaRecord,
payloadList.toArray(new IkeInformationalPayload[payloadList.size()]),
false /* isResp */,
mCurrentIkeSaRecord.getLocalRequestMessageId());
}
@Override
protected void triggerRetransmit() {
mRetransmitter.retransmit();
}
@Override
public void exitState() {
super.exitState();
if (mRetransmitter != null) {
mRetransmitter.stopRetransmitting();
}
}
@Override
public void handleRequestIkeMessage(
IkeMessage msg, int ikeExchangeSubType, Message message) {
switch (ikeExchangeSubType) {
case IKE_EXCHANGE_SUBTYPE_DELETE_IKE:
handleDeleteSessionRequest(msg);
break;
default:
// Send a temporary failure for all non-DELETE_IKE requests
buildAndSendErrorNotificationResponse(
mCurrentIkeSaRecord,
msg.ikeHeader.messageId,
ERROR_TYPE_TEMPORARY_FAILURE);
}
}
// Only called during RFC4555 MOBIKE mobility event
@Override
public void handleResponseIkeMessage(IkeMessage resp) {
mRetransmitter.stopRetransmitting();
try {
validateResp(resp);
migrateAllChildSAs(true /* mobikeEnabled */);
notifyConnectionInfoChanged();
transitionTo(mIdle);
} catch (IkeException | IOException e) {
handleIkeFatalError(e);
}
}
private void validateResp(IkeMessage resp) throws IkeException, IOException {
if (resp.ikeHeader.exchangeType != IkeHeader.EXCHANGE_TYPE_INFORMATIONAL) {
throw new InvalidSyntaxException(
"Invalid exchange type; expected INFORMATIONAL, but got: "
+ resp.ikeHeader.exchangeType);
}
List<IkeNotifyPayload> natSourcePayloads = new ArrayList<>();
IkeNotifyPayload natDestPayload = null;
for (IkePayload payload : resp.ikePayloadList) {
switch (payload.payloadType) {
case PAYLOAD_TYPE_NOTIFY:
IkeNotifyPayload notifyPayload = (IkeNotifyPayload) payload;
if (notifyPayload.isErrorNotify()) {
// TODO(b/): handle UNACCEPTABLE_ADDRESSES payload
throw notifyPayload.validateAndBuildIkeException();
}
switch (notifyPayload.notifyType) {
case NOTIFY_TYPE_NAT_DETECTION_SOURCE_IP:
natSourcePayloads.add(notifyPayload);
break;
case NOTIFY_TYPE_NAT_DETECTION_DESTINATION_IP:
if (natDestPayload != null) {
throw new InvalidSyntaxException(
"More than one"
+ " NOTIFY_TYPE_NAT_DETECTION_DESTINATION_IP"
+ " found");
}
natDestPayload = notifyPayload;
break;
default:
// Unknown and unexpected status notifications are ignored as per
// RFC7296.
logw(
"Received unknown or unexpected status notifications with"
+ " notify type: "
+ notifyPayload.notifyType);
}
break;
default:
logw("Unexpected payload types found: " + payload.payloadType);
}
}
if (mRetransmitter.getMessage().hasNotifyPayload(NOTIFY_TYPE_NAT_DETECTION_SOURCE_IP)) {
handleNatDetection(resp, natSourcePayloads, natDestPayload);
}
}
/** Handle NAT detection and switch socket if needed */
private void handleNatDetection(
IkeMessage resp,
List<IkeNotifyPayload> natSourcePayloads,
IkeNotifyPayload natDestPayload)
throws IkeException {
if (!didPeerIncludeNattDetectionPayloads(natSourcePayloads, natDestPayload)) {
// If this is first time that IKE client sends NAT_DETECTION payloads, mark that the
// server does not support NAT-T
if (mIkeConnectionCtrl.getNatStatus() == NAT_TRAVERSAL_SUPPORT_NOT_CHECKED) {
mIkeConnectionCtrl.markSeverNattUnsupported();
}
return;
}
boolean isNatDetected =
isLocalOrRemoteNatDetected(
resp.ikeHeader.ikeInitiatorSpi,
resp.ikeHeader.ikeResponderSpi,
natSourcePayloads,
natDestPayload);
mIkeConnectionCtrl.handleNatDetectionResultInMobike(isNatDetected);
}
private void migrateAllChildSAs(boolean mobikeEnabled) {
final int command =
mobikeEnabled
? CMD_LOCAL_REQUEST_MIGRATE_CHILD
: CMD_LOCAL_REQUEST_REKEY_CHILD_MOBIKE;
// Schedule MOBIKE for all Child Sessions
for (int i = 0; i < mRemoteSpiToChildSessionMap.size(); i++) {
int remoteChildSpi = mRemoteSpiToChildSessionMap.keyAt(i);
sendMessage(
command,
mLocalRequestFactory.getChildLocalRequest(command, remoteChildSpi));
}
}
private void notifyConnectionInfoChanged() {
IkeSessionConnectionInfo connectionInfo =
mIkeConnectionCtrl.buildIkeSessionConnectionInfo();
executeUserCallback(
() -> mIkeSessionCallback.onIkeSessionConnectionInfoChanged(connectionInfo));
}
@Override
protected @IkeMetrics.IkeState int getMetricsStateCode() {
return IkeMetrics.IKE_STATE_IKE_MOBIKE_LOCAL_INFO;
}
}
private static void addNatDetectionPayloadsToList(
List<IkePayload> payloadList,
InetAddress localAddr,
InetAddress remoteAddr,
int localPort,
int remotePort,
long initIkeSpi,
long respIkeSpi,
boolean isForceUdpEncapEnabled) {
// Though RFC says Notify-NAT payload is "just after the Ni and Nr payloads (before
// the optional CERTREQ payload)", it also says recipient MUST NOT reject " messages
// in which the payloads were not in the "right" order" due to the lack of clarity
// of the payload order
InetAddress localAddressToUse = localAddr;
if (isForceUdpEncapEnabled) {
IkeManager.getIkeLog().d(TAG, " Faking NAT situation to enforce UDP encapsulation");
localAddressToUse =
(remoteAddr instanceof Inet4Address)
? FORCE_ENCAP_FAKE_LOCAL_ADDRESS_IPV4
: FORCE_ENCAP_FAKE_LOCAL_ADDRESS_IPV6;
}
IkeNotifyPayload natdSrcIp =
new IkeNotifyPayload(
NOTIFY_TYPE_NAT_DETECTION_SOURCE_IP,
IkeNotifyPayload.generateNatDetectionData(
initIkeSpi, respIkeSpi, localAddressToUse, localPort));
IkeNotifyPayload natdDstIp =
new IkeNotifyPayload(
NOTIFY_TYPE_NAT_DETECTION_DESTINATION_IP,
IkeNotifyPayload.generateNatDetectionData(
initIkeSpi, respIkeSpi, remoteAddr, remotePort));
payloadList.add(natdSrcIp);
payloadList.add(natdDstIp);
}
/**
* Dumps the state of {@link IkeSessionStateMachine}
*
* @param pw {@link PrintWriter} to write the state of the object.
*/
public void dump(PrintWriter pw) {
super.dump(new FileDescriptor(), pw, new String[0]);
// TODO(b/310058405): To use IndentingPrintWriter Utility Class for Indentation purpose
String prefix = " ";
// dump ike session params data
if (mIkeSessionParams != null) {
mIkeSessionParams.dump(pw, prefix);
}
// dump ike connection controller data
if (mIkeConnectionCtrl != null) {
mIkeConnectionCtrl.dump(pw, prefix);
}
}
private static class IkeEapOutboundMsgWrapper {
private final boolean serverAuthenticated;
private final byte[] eapMsg;
public IkeEapOutboundMsgWrapper(boolean serverAuthenticated, byte[] eapMsg) {
this.serverAuthenticated = serverAuthenticated;
this.eapMsg = eapMsg;
}
public boolean isServerAuthenticated() {
return serverAuthenticated;
}
public byte[] getEapMsg() {
return eapMsg;
}
}
/**
* Helper class to generate IKE SA creation payloads, in both request and response directions.
*/
private static class CreateIkeSaHelper {
public static List<IkePayload> getIkeInitSaRequestPayloads(
IkeSaProposal[] saProposals,
int selectedDhGroup,
long initIkeSpi,
long respIkeSpi,
InetAddress localAddr,
InetAddress remoteAddr,
int localPort,
int remotePort,
RandomnessFactory randomFactory,
boolean isForceUdpEncapEnabled)
throws IOException {
List<IkePayload> payloadList =
getCreateIkeSaPayloads(
selectedDhGroup,
IkeSaPayload.createInitialIkeSaPayload(saProposals),
randomFactory);
if (remoteAddr instanceof Inet4Address) {
// TODO(b/184869678): support NAT detection for all cases
// UdpEncap for V6 is not supported in Android yet, so only send NAT Detection
// payloads when using IPv4 addresses
addNatDetectionPayloadsToList(
payloadList,
localAddr,
remoteAddr,
localPort,
remotePort,
initIkeSpi,
respIkeSpi,
isForceUdpEncapEnabled);
}
return payloadList;
}
public static List<IkePayload> getRekeyIkeSaRequestPayloads(
IkeSaProposal[] saProposals,
IkeSpiGenerator ikeSpiGenerator,
InetAddress localAddr,
RandomnessFactory randomFactory)
throws IOException {
if (localAddr == null) {
throw new IllegalArgumentException("Local address was null for rekey");
}
// Guaranteed to have at least one SA Proposal, since the IKE session was set up
// properly.
int selectedDhGroup = saProposals[0].getDhGroupTransforms()[0].id;
return getCreateIkeSaPayloads(
selectedDhGroup,
IkeSaPayload.createRekeyIkeSaRequestPayload(
saProposals, ikeSpiGenerator, localAddr),
randomFactory);
}
public static List<IkePayload> getRekeyIkeSaResponsePayloads(
byte respProposalNumber,
IkeSaProposal saProposal,
IkeSpiGenerator ikeSpiGenerator,
InetAddress localAddr,
RandomnessFactory randomFactory)
throws IOException {
if (localAddr == null) {
throw new IllegalArgumentException("Local address was null for rekey");
}
int selectedDhGroup = saProposal.getDhGroupTransforms()[0].id;
return getCreateIkeSaPayloads(
selectedDhGroup,
IkeSaPayload.createRekeyIkeSaResponsePayload(
respProposalNumber, saProposal, ikeSpiGenerator, localAddr),
randomFactory);
}
/**
* Builds the initial or rekey IKE creation payloads.
*
* <p>Will return a non-empty list of IkePayloads, the first of which WILL be the SA payload
*/
private static List<IkePayload> getCreateIkeSaPayloads(
int selectedDhGroup, IkeSaPayload saPayload, RandomnessFactory randomFactory)
throws IOException {
if (saPayload.proposalList.size() == 0) {
throw new IllegalArgumentException("Invalid SA proposal list - was empty");
}
List<IkePayload> payloadList = new ArrayList<>(3);
// The old IKE spec RFC 4306 (section 2.5 and 2.6) requires the payload order in IKE
// INIT to be SAi, KEi, Ni and allow responders to reject requests with wrong order.
// Although starting from RFC 5996, the protocol removed the allowance for rejecting
// messages in which the payloads were not in the "right" order, there are few responder
// implementations are still following the old spec when handling IKE INIT request with
// COOKIE payload. Thus IKE library should follow the payload order to be compatible
// with older implementations.
payloadList.add(saPayload);
// SaPropoals.Builder guarantees that each SA proposal has at least one DH group.
payloadList.add(IkeKePayload.createOutboundKePayload(selectedDhGroup, randomFactory));
payloadList.add(new IkeNoncePayload(randomFactory));
return payloadList;
}
}
// This call will be only fired when mIkeConnectionCtrl.isMobilityEnabled() is true
@Override
public void onUnderlyingNetworkUpdated() {
// Send event for mobility.
sendMessage(CMD_UNDERLYING_NETWORK_UPDATED_WITH_MOBILITY);
// UPDATE_SA
sendMessage(
CMD_LOCAL_REQUEST_MOBIKE,
mLocalRequestFactory.getIkeLocalRequest(CMD_LOCAL_REQUEST_MOBIKE));
}
@Override
public void onUnderlyingNetworkDied(Network network) {
if (mIkeConnectionCtrl.isMobilityEnabled()) {
// Send event for mobility.
sendMessage(CMD_UNDERLYING_NETWORK_DIED_WITH_MOBILITY);
// Do not tear down the session because 1) callers might want to migrate the IKE Session
// when another network is available; 2) the termination from IKE Session might be
// racing with the termination call from the callers.
executeUserCallback(
() -> mIkeSessionCallback.onError(new IkeNetworkLostException(network)));
} else {
ShimUtils.getInstance().onUnderlyingNetworkDiedWithoutMobility(this, network);
}
}
@Override
public void onError(IkeException exception) {
handleIkeFatalError(exception);
}
@Override
public void onIkePacketReceived(IkeHeader ikeHeader, byte[] ikePacketBytes) {
sendMessage(CMD_RECEIVE_IKE_PACKET, new ReceivedIkePacket(ikeHeader, ikePacketBytes));
}
// Implementation of IIkeSessionStateMachineShim
@Override
public void onNonFatalError(Exception e) {
executeUserCallback(() -> mIkeSessionCallback.onError(wrapAsIkeException(e)));
}
@Override
public void onFatalError(Exception e) {
handleIkeFatalError(e);
}
@Override
protected @IkeMetrics.IkeSessionType int getMetricsSessionType() {
return IkeMetrics.IKE_SESSION_TYPE_IKE;
}
@Override
public void onLivenessCheckCompleted(
int elapsedTimeInMillis, int numberOfOnGoing, boolean resultSuccess) {
recordMetricsEvent_LivenssCheckCompletion(
mIkeConnectionCtrl, elapsedTimeInMillis, numberOfOnGoing, resultSuccess);
}
}