diff --git a/framework-t/src/android/net/IIpSecService.aidl b/framework-t/src/android/net/IIpSecService.aidl index 88ffd0ea9e..f972ab9d2e 100644 --- a/framework-t/src/android/net/IIpSecService.aidl +++ b/framework-t/src/android/net/IIpSecService.aidl @@ -22,6 +22,7 @@ import android.net.IpSecConfig; import android.net.IpSecUdpEncapResponse; import android.net.IpSecSpiResponse; import android.net.IpSecTransformResponse; +import android.net.IpSecTransformState; import android.net.IpSecTunnelInterfaceResponse; import android.os.Bundle; import android.os.IBinder; @@ -74,6 +75,8 @@ interface IIpSecService void deleteTransform(int transformId); + IpSecTransformState getTransformState(int transformId); + void applyTransportModeTransform( in ParcelFileDescriptor socket, int direction, int transformId); diff --git a/framework-t/src/android/net/IpSecManager.java b/framework-t/src/android/net/IpSecManager.java index 3afa6ef60f..3f74e1c3cb 100644 --- a/framework-t/src/android/net/IpSecManager.java +++ b/framework-t/src/android/net/IpSecManager.java @@ -65,6 +65,13 @@ import java.util.Objects; public class IpSecManager { private static final String TAG = "IpSecManager"; + // TODO : remove this class when udc-mainline-prod is abandoned and android.net.flags.Flags is + // available here + /** @hide */ + public static class Flags { + static final String IPSEC_TRANSFORM_STATE = "com.android.net.flags.ipsec_transform_state"; + } + /** * Feature flag to declare the kernel support of updating IPsec SAs. * @@ -1084,6 +1091,12 @@ public class IpSecManager { } } + /** @hide */ + public IpSecTransformState getTransformState(int transformId) + throws IllegalStateException, RemoteException { + return mService.getTransformState(transformId); + } + /** * Construct an instance of IpSecManager within an application context. * diff --git a/framework-t/src/android/net/IpSecTransform.java b/framework-t/src/android/net/IpSecTransform.java index c236b6cb3a..0412a76dbe 100644 --- a/framework-t/src/android/net/IpSecTransform.java +++ b/framework-t/src/android/net/IpSecTransform.java @@ -15,8 +15,11 @@ */ package android.net; +import static android.net.IpSecManager.Flags.IPSEC_TRANSFORM_STATE; import static android.net.IpSecManager.INVALID_RESOURCE_ID; +import android.annotation.CallbackExecutor; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -26,6 +29,8 @@ import android.annotation.SystemApi; import android.content.Context; import android.content.pm.PackageManager; import android.os.Binder; +import android.os.OutcomeReceiver; +import android.os.RemoteException; import android.os.ServiceSpecificException; import android.util.Log; @@ -38,6 +43,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.net.InetAddress; import java.util.Objects; +import java.util.concurrent.Executor; /** * This class represents a transform, which roughly corresponds to an IPsec Security Association. @@ -200,6 +206,44 @@ public final class IpSecTransform implements AutoCloseable { return mResourceId; } + /** + * Retrieve the current state of this IpSecTransform. + * + * @param executor The {@link Executor} on which to call the supplied callback. + * @param callback Callback that's called after the transform state is ready or when an error + * occurs. + * @see IpSecTransformState + * @hide + */ + @FlaggedApi(IPSEC_TRANSFORM_STATE) + public void getIpSecTransformState( + @CallbackExecutor @NonNull Executor executor, + @NonNull OutcomeReceiver callback) { + Objects.requireNonNull(executor); + Objects.requireNonNull(callback); + + // TODO: Consider adding check to prevent DDoS attack. + + try { + final IpSecTransformState ipSecTransformState = + getIpSecManager(mContext).getTransformState(mResourceId); + executor.execute( + () -> { + callback.onResult(ipSecTransformState); + }); + } catch (IllegalStateException e) { + executor.execute( + () -> { + callback.onError(e); + }); + } catch (RemoteException e) { + executor.execute( + () -> { + callback.onError(e.rethrowFromSystemServer()); + }); + } + } + /** * A callback class to provide status information regarding a NAT-T keepalive session * diff --git a/framework-t/src/android/net/IpSecTransformState.aidl b/framework-t/src/android/net/IpSecTransformState.aidl new file mode 100644 index 0000000000..69cce28ee7 --- /dev/null +++ b/framework-t/src/android/net/IpSecTransformState.aidl @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2023 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 android.net; + +/** @hide */ +parcelable IpSecTransformState; \ No newline at end of file diff --git a/framework-t/src/android/net/IpSecTransformState.java b/framework-t/src/android/net/IpSecTransformState.java new file mode 100644 index 0000000000..daebbc429c --- /dev/null +++ b/framework-t/src/android/net/IpSecTransformState.java @@ -0,0 +1,302 @@ +/* + * Copyright (C) 2023 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 android.net; + +import static android.net.IpSecManager.Flags.IPSEC_TRANSFORM_STATE; + +import static com.android.internal.annotations.VisibleForTesting.Visibility; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.net.module.util.HexDump; + +import java.util.Objects; + +/** + * This class represents a snapshot of the state of an IpSecTransform + * + *

This class provides the current state of an IpSecTransform, enabling link metric analysis by + * the caller. Use cases include understanding transform usage, such as packet and byte counts, as + * well as observing out-of-order delivery by checking the bitmap. Additionally, callers can query + * IpSecTransformStates at two timestamps. By comparing the changes in packet counts and sequence + * numbers, callers can estimate IPsec data loss in the inbound direction. + * + * @hide + */ +@FlaggedApi(IPSEC_TRANSFORM_STATE) +public final class IpSecTransformState implements Parcelable { + private final long mTimeStamp; + private final long mTxHighestSequenceNumber; + private final long mRxHighestSequenceNumber; + private final long mPacketCount; + private final long mByteCount; + private final byte[] mReplayBitmap; + + private IpSecTransformState( + long timestamp, + long txHighestSequenceNumber, + long rxHighestSequenceNumber, + long packetCount, + long byteCount, + byte[] replayBitmap) { + mTimeStamp = timestamp; + mTxHighestSequenceNumber = txHighestSequenceNumber; + mRxHighestSequenceNumber = rxHighestSequenceNumber; + mPacketCount = packetCount; + mByteCount = byteCount; + + Objects.requireNonNull(replayBitmap, "replayBitmap is null"); + mReplayBitmap = replayBitmap.clone(); + + validate(); + } + + private void validate() { + Objects.requireNonNull(mReplayBitmap, "mReplayBitmap is null"); + } + + /** + * Deserializes a IpSecTransformState from a PersistableBundle. + * + * @hide + */ + @VisibleForTesting(visibility = Visibility.PRIVATE) + public IpSecTransformState(@NonNull Parcel in) { + Objects.requireNonNull(in, "The input PersistableBundle is null"); + mTimeStamp = in.readLong(); + mTxHighestSequenceNumber = in.readLong(); + mRxHighestSequenceNumber = in.readLong(); + mPacketCount = in.readLong(); + mByteCount = in.readLong(); + mReplayBitmap = HexDump.hexStringToByteArray(in.readString()); + + validate(); + } + + // Parcelable methods + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel out, int flags) { + out.writeLong(mTimeStamp); + out.writeLong(mTxHighestSequenceNumber); + out.writeLong(mRxHighestSequenceNumber); + out.writeLong(mPacketCount); + out.writeLong(mByteCount); + out.writeString(HexDump.toHexString(mReplayBitmap)); + } + + @NonNull + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @NonNull + public IpSecTransformState createFromParcel(Parcel in) { + return new IpSecTransformState(in); + } + + @NonNull + public IpSecTransformState[] newArray(int size) { + return new IpSecTransformState[size]; + } + }; + + /** + * Retrieve the epoch timestamp (milliseconds) for when this state was created + * + * @see Builder#setTimestamp(long) + * @hide + */ + public long getTimestamp() { + return mTimeStamp; + } + + /** + * Retrieve the highest sequence number sent so far + * + * @see Builder#setTxHighestSequenceNumber(long) + * @hide + */ + public long getTxHighestSequenceNumber() { + return mTxHighestSequenceNumber; + } + + /** + * Retrieve the highest sequence number received so far + * + * @see Builder#setRxHighestSequenceNumber(long) + * @hide + */ + public long getRxHighestSequenceNumber() { + return mRxHighestSequenceNumber; + } + + /** + * Retrieve the number of packets received AND sent so far + * + * @see Builder#setPacketCount(long) + * @hide + */ + public long getPacketCount() { + return mPacketCount; + } + + /** + * Retrieve the number of bytes received AND sent so far + * + * @see Builder#setByteCount(long) + * @hide + */ + public long getByteCount() { + return mByteCount; + } + + /** + * Retrieve the replay bitmap + * + *

This bitmap represents a replay window, allowing the caller to observe out-of-order + * delivery. The last bit represents the highest sequence number received so far and bits for + * the received packets will be marked as true. + * + *

The size of a replay bitmap will never change over the lifetime of an IpSecTransform + * + *

The replay bitmap is solely useful for inbound IpSecTransforms. For outbound + * IpSecTransforms, all bits will be unchecked. + * + * @see Builder#setReplayBitmap(byte[]) + * @hide + */ + @NonNull + public byte[] getReplayBitmap() { + return mReplayBitmap.clone(); + } + + /** + * Builder class for testing purposes + * + * @hide + */ + @FlaggedApi(IPSEC_TRANSFORM_STATE) + public static final class Builder { + private long mTimeStamp; + private long mTxHighestSequenceNumber; + private long mRxHighestSequenceNumber; + private long mPacketCount; + private long mByteCount; + private byte[] mReplayBitmap; + + /** @hide */ + public Builder() { + mTimeStamp = System.currentTimeMillis(); + } + + /** + * Set the epoch timestamp (milliseconds) for when this state was created + * + * @see IpSecTransformState#getTimestamp() + * @hide + */ + @NonNull + public Builder setTimestamp(long timeStamp) { + mTimeStamp = timeStamp; + return this; + } + + /** + * Set the highest sequence number sent so far + * + * @see IpSecTransformState#getTxHighestSequenceNumber() + * @hide + */ + @NonNull + public Builder setTxHighestSequenceNumber(long seqNum) { + mTxHighestSequenceNumber = seqNum; + return this; + } + + /** + * Set the highest sequence number received so far + * + * @see IpSecTransformState#getRxHighestSequenceNumber() + * @hide + */ + @NonNull + public Builder setRxHighestSequenceNumber(long seqNum) { + mRxHighestSequenceNumber = seqNum; + return this; + } + + /** + * Set the number of packets received AND sent so far + * + * @see IpSecTransformState#getPacketCount() + * @hide + */ + @NonNull + public Builder setPacketCount(long packetCount) { + mPacketCount = packetCount; + return this; + } + + /** + * Set the number of bytes received AND sent so far + * + * @see IpSecTransformState#getByteCount() + * @hide + */ + @NonNull + public Builder setByteCount(long byteCount) { + mByteCount = byteCount; + return this; + } + + /** + * Set the replay bitmap + * + * @see IpSecTransformState#getReplayBitmap() + * @hide + */ + @NonNull + public Builder setReplayBitmap(@NonNull byte[] bitMap) { + mReplayBitmap = bitMap.clone(); + return this; + } + + /** + * Build and validate the IpSecTransformState + * + * @return an immutable IpSecTransformState instance + * @hide + */ + @NonNull + public IpSecTransformState build() { + return new IpSecTransformState( + mTimeStamp, + mTxHighestSequenceNumber, + mRxHighestSequenceNumber, + mPacketCount, + mByteCount, + mReplayBitmap); + } + } +} diff --git a/service-t/src/com/android/server/IpSecService.java b/service-t/src/com/android/server/IpSecService.java index a884840b77..ea91e6471c 100644 --- a/service-t/src/com/android/server/IpSecService.java +++ b/service-t/src/com/android/server/IpSecService.java @@ -42,6 +42,7 @@ import android.net.IpSecMigrateInfoParcel; import android.net.IpSecSpiResponse; import android.net.IpSecTransform; import android.net.IpSecTransformResponse; +import android.net.IpSecTransformState; import android.net.IpSecTunnelInterfaceResponse; import android.net.IpSecUdpEncapResponse; import android.net.LinkAddress; @@ -70,6 +71,7 @@ import com.android.modules.utils.build.SdkLevel; import com.android.net.module.util.BinderUtils; import com.android.net.module.util.NetdUtils; import com.android.net.module.util.PermissionUtils; +import com.android.net.module.util.netlink.xfrm.XfrmNetlinkNewSaMessage; import libcore.io.IoUtils; @@ -109,6 +111,7 @@ public class IpSecService extends IIpSecService.Stub { @VisibleForTesting static final int MAX_PORT_BIND_ATTEMPTS = 10; private final INetd mNetd; + private final IpSecXfrmController mIpSecXfrmCtrl; static { try { @@ -152,6 +155,11 @@ public class IpSecService extends IIpSecService.Stub { } return netd; } + + /** Get a instance of IpSecXfrmController */ + public IpSecXfrmController getIpSecXfrmController() { + return new IpSecXfrmController(); + } } final UidFdTagger mUidFdTagger; @@ -1111,6 +1119,7 @@ public class IpSecService extends IIpSecService.Stub { mContext = context; mDeps = Objects.requireNonNull(deps, "Missing dependencies."); mUidFdTagger = uidFdTagger; + mIpSecXfrmCtrl = mDeps.getIpSecXfrmController(); try { mNetd = mDeps.getNetdInstance(mContext); } catch (RemoteException e) { @@ -1862,6 +1871,48 @@ public class IpSecService extends IIpSecService.Stub { releaseResource(userRecord.mTransformRecords, resourceId); } + @Override + public synchronized IpSecTransformState getTransformState(int transformId) + throws IllegalStateException, RemoteException { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.ACCESS_NETWORK_STATE, "IpsecService#getTransformState"); + + UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid()); + TransformRecord transformInfo = + userRecord.mTransformRecords.getResourceOrThrow(transformId); + + final int spi = transformInfo.getSpiRecord().getSpi(); + final InetAddress destAddress = + InetAddresses.parseNumericAddress( + transformInfo.getConfig().getDestinationAddress()); + Log.d(TAG, "getTransformState for spi " + spi + " destAddress " + destAddress); + + // Make netlink call + final XfrmNetlinkNewSaMessage xfrmNewSaMsg; + try { + xfrmNewSaMsg = mIpSecXfrmCtrl.ipSecGetSa(destAddress, Integer.toUnsignedLong(spi)); + } catch (ErrnoException | IOException e) { + Log.e(TAG, "getTransformState: failed to get IpSecTransformState" + e.toString()); + throw new IllegalStateException("Failed to get IpSecTransformState", e); + } + + // Keep the netlink socket open to save time for the next call. It is cheap to have a + // persistent netlink socket in the system server + + if (xfrmNewSaMsg == null) { + Log.e(TAG, "getTransformState: failed to get IpSecTransformState xfrmNewSaMsg is null"); + throw new IllegalStateException("Failed to get IpSecTransformState"); + } + + return new IpSecTransformState.Builder() + .setTxHighestSequenceNumber(xfrmNewSaMsg.getTxSequenceNumber()) + .setRxHighestSequenceNumber(xfrmNewSaMsg.getRxSequenceNumber()) + .setPacketCount(xfrmNewSaMsg.getPacketCount()) + .setByteCount(xfrmNewSaMsg.getByteCount()) + .setReplayBitmap(xfrmNewSaMsg.getBitmap()) + .build(); + } + /** * Apply an active transport mode transform to a socket, which will apply the IPsec security * association as a correspondent policy to the provided socket diff --git a/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java b/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java index 1618a62c81..8037542ae4 100644 --- a/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java +++ b/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java @@ -33,6 +33,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; @@ -74,6 +75,7 @@ import android.util.ArraySet; import androidx.test.filters.SmallTest; +import com.android.net.module.util.netlink.xfrm.XfrmNetlinkNewSaMessage; import com.android.server.IpSecService.TunnelInterfaceRecord; import com.android.testutils.DevSdkIgnoreRule; @@ -85,6 +87,7 @@ import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import java.net.Inet4Address; +import java.net.InetAddress; import java.net.Socket; import java.util.Arrays; import java.util.Collection; @@ -149,6 +152,7 @@ public class IpSecServiceParameterizedTest { private Set mAllowedPermissions = new ArraySet<>(Arrays.asList( android.Manifest.permission.MANAGE_IPSEC_TUNNELS, android.Manifest.permission.NETWORK_STACK, + android.Manifest.permission.ACCESS_NETWORK_STATE, PERMISSION_MAINLINE_NETWORK_STACK)); private void setAllowedPermissions(String... permissions) { @@ -202,11 +206,13 @@ public class IpSecServiceParameterizedTest { private IpSecService.Dependencies makeDependencies() throws RemoteException { final IpSecService.Dependencies deps = mock(IpSecService.Dependencies.class); when(deps.getNetdInstance(mTestContext)).thenReturn(mMockNetd); + when(deps.getIpSecXfrmController()).thenReturn(mMockXfrmCtrl); return deps; } INetd mMockNetd; PackageManager mMockPkgMgr; + IpSecXfrmController mMockXfrmCtrl; IpSecService.Dependencies mDeps; IpSecService mIpSecService; Network fakeNetwork = new Network(0xAB); @@ -235,6 +241,7 @@ public class IpSecServiceParameterizedTest { @Before public void setUp() throws Exception { mMockNetd = mock(INetd.class); + mMockXfrmCtrl = mock(IpSecXfrmController.class); mMockPkgMgr = mock(PackageManager.class); mDeps = makeDependencies(); mIpSecService = new IpSecService(mTestContext, mDeps); @@ -505,6 +512,32 @@ public class IpSecServiceParameterizedTest { } } + @Test + public void getTransformState() throws Exception { + XfrmNetlinkNewSaMessage mockXfrmNewSaMsg = mock(XfrmNetlinkNewSaMessage.class); + when(mockXfrmNewSaMsg.getBitmap()).thenReturn(new byte[512]); + when(mMockXfrmCtrl.ipSecGetSa(any(InetAddress.class), anyLong())) + .thenReturn(mockXfrmNewSaMsg); + + // Create transform + IpSecConfig ipSecConfig = new IpSecConfig(); + addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig); + addAuthAndCryptToIpSecConfig(ipSecConfig); + + IpSecTransformResponse createTransformResp = + mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE); + assertEquals(IpSecManager.Status.OK, createTransformResp.status); + + // Get transform state + mIpSecService.getTransformState(createTransformResp.resourceId); + + // Verifications + verify(mMockXfrmCtrl) + .ipSecGetSa( + eq(InetAddresses.parseNumericAddress(mDestinationAddr)), + eq(Integer.toUnsignedLong(TEST_SPI))); + } + @Test public void testReleaseOwnedSpi() throws Exception { IpSecConfig ipSecConfig = new IpSecConfig();